package store import ( "errors" "testing" ) func TestCreateProjectAlsoCreatesWorkload(t *testing.T) { s := newTestStore(t) p, err := s.CreateProject(Project{ Name: "wf-project", Image: "nginx", Port: 80, Env: "{}", Volumes: "{}", NotificationURL: "https://example.test/hook", }) if err != nil { t.Fatalf("CreateProject: %v", err) } w, err := s.GetWorkloadByRef(WorkloadKindProject, p.ID) if err != nil { t.Fatalf("workload should exist after CreateProject: %v", err) } if w.Name != "wf-project" { t.Fatalf("workload name not synced: got %q", w.Name) } if w.WebhookSecret == "" { t.Fatal("webhook secret should be carried into workload row") } if w.NotificationURL != "https://example.test/hook" { t.Fatalf("notification url not synced: got %q", w.NotificationURL) } } func TestUpdateProjectSyncsWorkload(t *testing.T) { s := newTestStore(t) p, _ := s.CreateProject(Project{ Name: "before", Image: "i", Env: "{}", Volumes: "{}", }) p.Name = "after" p.NotificationURL = "https://new.test/hook" if err := s.UpdateProject(p); err != nil { t.Fatalf("UpdateProject: %v", err) } w, _ := s.GetWorkloadByRef(WorkloadKindProject, p.ID) if w.Name != "after" { t.Fatalf("workload name not updated: got %q", w.Name) } if w.NotificationURL != "https://new.test/hook" { t.Fatalf("workload notification url not updated: got %q", w.NotificationURL) } } func TestDeleteProjectCascadesWorkload(t *testing.T) { s := newTestStore(t) p, _ := s.CreateProject(Project{Name: "doomed", Image: "i", Env: "{}", Volumes: "{}"}) w, _ := s.GetWorkloadByRef(WorkloadKindProject, p.ID) // Add a container under this workload to verify cascade. if _, err := s.CreateContainer(Container{ WorkloadID: w.ID, WorkloadKind: "project", State: "running", }); err != nil { t.Fatalf("CreateContainer: %v", err) } if err := s.DeleteProject(p.ID); err != nil { t.Fatalf("DeleteProject: %v", err) } if _, err := s.GetWorkloadByID(w.ID); !errors.Is(err, ErrNotFound) { t.Fatalf("workload should be deleted, got %v", err) } containers, _ := s.ListContainersByWorkload(w.ID) if len(containers) != 0 { t.Fatalf("containers should be deleted, got %d", len(containers)) } } func TestSetProjectWebhookSecretSyncsWorkload(t *testing.T) { s := newTestStore(t) p, _ := s.CreateProject(Project{Name: "n", Image: "i", Env: "{}", Volumes: "{}"}) newSecret := "new-secret-value-with-enough-entropy-1234" if err := s.SetProjectWebhookSecret(p.ID, newSecret); err != nil { t.Fatalf("SetProjectWebhookSecret: %v", err) } w, _ := s.GetWorkloadByRef(WorkloadKindProject, p.ID) if w.WebhookSecret != newSecret { t.Fatalf("workload webhook secret not synced: got %q", w.WebhookSecret) } } func TestCreateStackAlsoCreatesWorkload(t *testing.T) { s := newTestStore(t) st, err := s.CreateStack(Stack{Name: "wf-stack", ComposeProjectName: "wf-stack"}) if err != nil { t.Fatalf("CreateStack: %v", err) } w, err := s.GetWorkloadByRef(WorkloadKindStack, st.ID) if err != nil { t.Fatalf("workload should exist after CreateStack: %v", err) } if w.Name != "wf-stack" { t.Fatalf("workload name not synced: got %q", w.Name) } } func TestUpdateStackSyncsWorkload(t *testing.T) { s := newTestStore(t) st, _ := s.CreateStack(Stack{Name: "before", ComposeProjectName: "before-cp"}) st.Name = "after" if err := s.UpdateStack(st); err != nil { t.Fatalf("UpdateStack: %v", err) } w, _ := s.GetWorkloadByRef(WorkloadKindStack, st.ID) if w.Name != "after" { t.Fatalf("workload name not updated: got %q", w.Name) } } func TestDeleteStackCascadesWorkload(t *testing.T) { s := newTestStore(t) st, _ := s.CreateStack(Stack{Name: "doomed-stack", ComposeProjectName: "doomed-cp"}) w, _ := s.GetWorkloadByRef(WorkloadKindStack, st.ID) if err := s.DeleteStack(st.ID); err != nil { t.Fatalf("DeleteStack: %v", err) } if _, err := s.GetWorkloadByID(w.ID); !errors.Is(err, ErrNotFound) { t.Fatalf("workload should be deleted, got %v", err) } } func TestBackfillWorkloadsIdempotent(t *testing.T) { s := newTestStore(t) // Create rows directly via the store (which already auto-syncs), then run // the backfill twice — it must be a no-op the second time and not error. p, _ := s.CreateProject(Project{Name: "p1", Image: "i", Env: "{}", Volumes: "{}"}) st, _ := s.CreateStack(Stack{Name: "s1", ComposeProjectName: "s1-cp"}) if err := s.BackfillWorkloads(); err != nil { t.Fatalf("first backfill: %v", err) } if err := s.BackfillWorkloads(); err != nil { t.Fatalf("second backfill (should be idempotent): %v", err) } all, _ := s.ListWorkloads("") // Expect exactly 2: one project workload, one stack workload, no duplicates. if len(all) != 2 { t.Fatalf("expected 2 workloads after backfill, got %d", len(all)) } // Confirm both refs are findable. if _, err := s.GetWorkloadByRef(WorkloadKindProject, p.ID); err != nil { t.Fatalf("project workload not found: %v", err) } if _, err := s.GetWorkloadByRef(WorkloadKindStack, st.ID); err != nil { t.Fatalf("stack workload not found: %v", err) } } func TestBackfillRecoversFromMissingWorkloads(t *testing.T) { s := newTestStore(t) p, _ := s.CreateProject(Project{Name: "p1", Image: "i", Env: "{}", Volumes: "{}"}) // Simulate the legacy state: a project exists but its workload row is gone // (e.g. the rollout from before the refactor). Backfill must restore it. w, _ := s.GetWorkloadByRef(WorkloadKindProject, p.ID) _ = s.DeleteWorkload(w.ID) if err := s.BackfillWorkloads(); err != nil { t.Fatalf("backfill: %v", err) } if _, err := s.GetWorkloadByRef(WorkloadKindProject, p.ID); err != nil { t.Fatalf("workload should be restored: %v", err) } }