db235c1412
CRUD on Project / Stack / StaticSite now keeps a paired Workload row in sync. Secret setters (webhook secret, signing secret, require-signature toggle, notification secret) all re-sync after mutating the source-of-truth row so the workload row always reflects the canonical state. Delete cascades: DeleteProject/Stack/StaticSite now drop the matching workload row plus any container index entries owned by it, so global views don't show ghost rows. Boot-time BackfillWorkloads scans every project/stack/site and ensures each has a workload row. Idempotent — safe to run on every restart, recovers from a deleted/missing workload row. Behavior unchanged for existing call sites; the workloads table just starts being populated. Deployer / reconciler / consumer switchover land in the next commit.
191 lines
5.6 KiB
Go
191 lines
5.6 KiB
Go
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)
|
|
}
|
|
}
|