package store import ( "errors" "testing" ) // seedWorkloadForNotifications creates a minimal workload row so the FK // constraint on workload_notifications is satisfied. Returns the new // workload's ID for tests to reference. func seedWorkloadForNotifications(t *testing.T, s *Store, name string) string { t.Helper() w, err := s.CreateWorkload(Workload{ Kind: string(WorkloadKindProject), Name: name, SourceKind: "image", }) if err != nil { t.Fatalf("seed workload: %v", err) } return w.ID } func TestCreateWorkloadNotification_RoundTrip(t *testing.T) { s := newTestStore(t) wlID := seedWorkloadForNotifications(t, s, "app1") created, err := s.CreateWorkloadNotification(WorkloadNotification{ WorkloadID: wlID, Name: "Slack alerts", URL: "https://hooks.slack.test/x", Secret: "shh", EventTypes: "deploy_failure,build_failure", Enabled: true, }) if err != nil { t.Fatalf("CreateWorkloadNotification: %v", err) } if created.ID == "" { t.Fatal("expected ID to be assigned") } got, err := s.GetWorkloadNotification(created.ID) if err != nil { t.Fatalf("Get: %v", err) } if got.URL != "https://hooks.slack.test/x" || got.Name != "Slack alerts" { t.Errorf("row mismatch: %+v", got) } if !got.Enabled { t.Error("expected Enabled=true") } if got.EventTypes != "deploy_failure,build_failure" { t.Errorf("event_types = %q", got.EventTypes) } } func TestCreateWorkloadNotification_RejectsMissingURL(t *testing.T) { s := newTestStore(t) wlID := seedWorkloadForNotifications(t, s, "app1") _, err := s.CreateWorkloadNotification(WorkloadNotification{ WorkloadID: wlID, Name: "broken", URL: "", }) if err == nil { t.Fatal("expected URL validation error") } } func TestListWorkloadNotifications_SortedByOrder(t *testing.T) { s := newTestStore(t) wlID := seedWorkloadForNotifications(t, s, "app1") // Insert out of order; ListWorkloadNotifications should return // them sorted by SortOrder ascending. _, _ = s.CreateWorkloadNotification(WorkloadNotification{ WorkloadID: wlID, Name: "C", URL: "https://c.test", SortOrder: 30, }) _, _ = s.CreateWorkloadNotification(WorkloadNotification{ WorkloadID: wlID, Name: "A", URL: "https://a.test", SortOrder: 10, }) _, _ = s.CreateWorkloadNotification(WorkloadNotification{ WorkloadID: wlID, Name: "B", URL: "https://b.test", SortOrder: 20, }) rows, err := s.ListWorkloadNotifications(wlID) if err != nil { t.Fatalf("list: %v", err) } if len(rows) != 3 { t.Fatalf("len = %d, want 3", len(rows)) } if rows[0].Name != "A" || rows[1].Name != "B" || rows[2].Name != "C" { t.Errorf("sort order wrong: %q %q %q", rows[0].Name, rows[1].Name, rows[2].Name) } } func TestUpdateWorkloadNotification_PersistsChanges(t *testing.T) { s := newTestStore(t) wlID := seedWorkloadForNotifications(t, s, "app1") n, _ := s.CreateWorkloadNotification(WorkloadNotification{ WorkloadID: wlID, Name: "old", URL: "https://old.test", Enabled: true, }) n.Name = "new" n.URL = "https://new.test" n.Enabled = false n.EventTypes = "deploy_success" if err := s.UpdateWorkloadNotification(n); err != nil { t.Fatalf("update: %v", err) } got, _ := s.GetWorkloadNotification(n.ID) if got.Name != "new" || got.URL != "https://new.test" || got.Enabled { t.Errorf("update did not persist: %+v", got) } } func TestDeleteWorkloadNotification_ReturnsNotFoundForMissing(t *testing.T) { s := newTestStore(t) err := s.DeleteWorkloadNotification("nope") if !errors.Is(err, ErrNotFound) { t.Errorf("expected ErrNotFound, got %v", err) } } func TestDeleteWorkloadNotification_CascadesFromWorkload(t *testing.T) { s := newTestStore(t) wlID := seedWorkloadForNotifications(t, s, "app1") _, _ = s.CreateWorkloadNotification(WorkloadNotification{ WorkloadID: wlID, Name: "x", URL: "https://x.test", }) if err := s.DeleteWorkload(wlID); err != nil { t.Fatalf("delete workload: %v", err) } rows, err := s.ListWorkloadNotifications(wlID) if err != nil { t.Fatalf("list after cascade: %v", err) } if len(rows) != 0 { t.Errorf("expected cascade delete to remove rows, got %d", len(rows)) } } func TestMatchesEventType_AllowList(t *testing.T) { cases := []struct { eventTypes string probe string want bool }{ {"", "deploy_success", true}, // empty = all {"deploy_success,deploy_failure", "deploy_success", true}, {"deploy_success,deploy_failure", "build_failure", false}, {"build_failure", "build_failure", true}, {" deploy_success , build_failure ", "build_failure", true}, // whitespace tolerated } for _, c := range cases { n := WorkloadNotification{Enabled: true, EventTypes: c.eventTypes} got := n.MatchesEventType(c.probe) if got != c.want { t.Errorf("MatchesEventType(%q, %q) = %v, want %v", c.eventTypes, c.probe, got, c.want) } } } func TestMatchesEventType_DisabledNeverMatches(t *testing.T) { n := WorkloadNotification{Enabled: false, EventTypes: ""} if n.MatchesEventType("any") { t.Error("disabled row should never match") } }