package alerts import ( "strings" "testing" ) // TestDedupeKey pins the stable dedupe key construction. The key is the type prefix joined to the subject id, // and the org scoping comes from the UNIQUE (organization_id, dedupe_key) constraint, so the same event // always yields the same key (the basis for the idempotent UPSERT) and two different subjects never collide. func TestDedupeKey(t *testing.T) { cases := []struct { alertType string subjectID string want string }{ {TypeClearanceExpiring, "11111111-1111-1111-1111-111111111111", "clearance_expiring:11111111-1111-1111-1111-111111111111"}, {TypeRecompeteWinnable, "22222222-2222-2222-2222-222222222222", "recompete_winnable:22222222-2222-2222-2222-222222222222"}, {TypeGapFillable, "33333333-3333-3333-3333-333333333333", "gap_fillable:33333333-3333-3333-3333-333333333333"}, {TypeCollision, "44444444-4444-4444-4444-444444444444", "collision:44444444-4444-4444-4444-444444444444"}, } for _, c := range cases { if got := dedupeKey(c.alertType, c.subjectID); got != c.want { t.Errorf("dedupeKey(%q, %q) = %q, want %q", c.alertType, c.subjectID, got, c.want) } } // The same event is byte stable: a re sweep recomputes the identical key, which is what makes the UPSERT // idempotent. a := dedupeKey(TypeClearanceExpiring, "abc") b := dedupeKey(TypeClearanceExpiring, "abc") if a != b { t.Errorf("dedupeKey must be stable for the same event, got %q then %q", a, b) } // Two different subjects never collide, so two candidates each get their own alert row. if dedupeKey(TypeClearanceExpiring, "abc") == dedupeKey(TypeClearanceExpiring, "def") { t.Error("different subjects must yield different dedupe keys") } // Two different types over the same subject never collide either. if dedupeKey(TypeGapFillable, "abc") == dedupeKey(TypeClearanceExpiring, "abc") { t.Error("different alert types must yield different dedupe keys for the same subject") } } // TestIsValidType pins the closed set of alert types the DB CHECK enforces. func TestIsValidType(t *testing.T) { for _, ty := range []string{TypeRecompeteWinnable, TypeClearanceExpiring, TypeGapFillable, TypeCollision} { if !IsValidType(ty) { t.Errorf("%q must be a valid alert type", ty) } } for _, ty := range []string{"", "recompete", "RECOMPETE_WINNABLE", "gap", "clearance_expiring "} { if IsValidType(ty) { t.Errorf("%q must NOT be a valid alert type", ty) } } } // TestIsValidSeverity pins the closed set of severities the DB CHECK enforces. func TestIsValidSeverity(t *testing.T) { for _, s := range []string{SeverityInfo, SeverityWatch, SeverityUrgent} { if !IsValidSeverity(s) { t.Errorf("%q must be a valid severity", s) } } for _, s := range []string{"", "low", "URGENT", "critical"} { if IsValidSeverity(s) { t.Errorf("%q must NOT be a valid severity", s) } } } // TestIsValidStatus pins the closed set of statuses the DB CHECK enforces and the UI PATCH validates against. func TestIsValidStatus(t *testing.T) { for _, s := range []string{StatusUnread, StatusRead, StatusDismissed} { if !IsValidStatus(s) { t.Errorf("%q must be a valid status", s) } } for _, s := range []string{"", "open", "archived", "READ", "dismissed "} { if IsValidStatus(s) { t.Errorf("%q must NOT be a valid status", s) } } } // TestSweepSummaryTotal pins the total as the sum of the per type counts. func TestSweepSummaryTotal(t *testing.T) { s := SweepSummary{ClearanceExpiring: 2, RecompeteWinnable: 3, GapFillable: 4, Collision: 5} if s.Total() != 14 { t.Errorf("Total() = %d, want 14", s.Total()) } if (SweepSummary{}).Total() != 0 { t.Errorf("an empty summary must total 0") } } // TestCollisionToAlert pins the collision severity logic and the advisory shape: a scarce cleared person OR a // true double commitment (approved on two or more) is urgent, an ordinary proposed collision is watch, and the // finding always names the deterministic suggested primary and discloses clearance is self reported, not // verified. It is advisory (a candidate subject, never an auto rejection). func TestCollisionToAlert(t *testing.T) { // A scarce cleared candidate proposed on two bids: urgent. scarce := collision{candidateID: "c1", fullName: "Ada", scarceCleared: true, oppCount: 2, approvedCount: 0, suggestedPrimary: "Bid A", opportunityNames: "Bid A, Bid B"} a := scarce.toAlert() if a.severity != SeverityUrgent { t.Errorf("a scarce cleared collision must be urgent, got %q", a.severity) } if a.alertType != TypeCollision || a.subjectType != SubjectCandidate || a.subjectID != "c1" { t.Errorf("collision alert shape wrong: %+v", a) } if a.dedupeKey != "collision:c1" { t.Errorf("dedupe key must be one per candidate, got %q", a.dedupeKey) } // A non scarce candidate merely proposed (approved on zero or one): watch. proposed := collision{candidateID: "c2", fullName: "Grace", scarceCleared: false, oppCount: 2, approvedCount: 1, suggestedPrimary: "Bid X", opportunityNames: "Bid X, Bid Y"} if p := proposed.toAlert(); p.severity != SeverityWatch { t.Errorf("an ordinary proposed collision must be watch, got %q", p.severity) } // A true double commitment (approved on two) is urgent even without a scarce clearance. doubled := collision{candidateID: "c3", fullName: "Kay", scarceCleared: false, oppCount: 2, approvedCount: 2, suggestedPrimary: "Bid M", opportunityNames: "Bid M, Bid N"} d := doubled.toAlert() if d.severity != SeverityUrgent { t.Errorf("a double commitment must be urgent, got %q", d.severity) } // The disclosure and the suggested primary must always be present in the detail. for _, want := range []string{"Suggested primary", "not verified"} { if !strings.Contains(d.detail, want) { t.Errorf("collision detail must contain %q, got %q", want, d.detail) } } }