package alerts import ( "reflect" "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) } } } // THE COUNTS ONLY AUDIT EVENT AND THE TOTAL MUST BOTH KNOW ABOUT EVERY TYPE. // // This is the failure the sweep would otherwise hide: a fifth trigger fires, upserts alerts, and the audit // row and the total report a smaller number than actually happened. Nobody would notice, because the alerts // themselves appear on screen and only the accounting of them is wrong. Reflection over the summary struct // means a sixth type cannot be added without either being counted or being deliberately excluded here. func TestSweepSummaryTotalCountsEveryType(t *testing.T) { var s SweepSummary v := reflect.ValueOf(&s).Elem() want := 0 for i := 0; i < v.NumField(); i++ { if v.Field(i).Kind() != reflect.Int { continue } v.Field(i).SetInt(int64(i + 1)) want += i + 1 } if got := s.Total(); got != want { t.Fatalf("Total() = %d, want %d. A type was added to SweepSummary and left out of Total, so the "+ "sweep under reports what it actually did.", got, want) } } // Every type in the closed vocabulary must be accepted by the validator, or the sweep writes an alert the // read path refuses to serve. func TestRedeploymentDueIsAValidType(t *testing.T) { if !validTypes[TypeRedeploymentDue] { t.Fatal("redeployment_due is not in the accepted set, the sweep would write alerts nothing can read") } } // THE VOCABULARY MUST MATCH THE DATABASE CHECK, and this is the drift that proved it can silently stop // matching. Migration 0075 widened the CHECK by two members and this map was not updated for a year of // commits, which nothing caught because IsValidType has no caller. A list that claims to mirror a constraint // and does not is worse than no list, because the next person to use it trusts it. // // The seven members here are the seven in the CHECK after migration 0099. If a migration widens the CHECK // again, this fails, which is the point. func TestVocabularyMatchesTheDatabaseCheck(t *testing.T) { inCheck := []string{ "recompete_winnable", "clearance_expiring", "gap_fillable", "collision", "opportunity_amendment", "opportunity_deadline", "redeployment_due", } for _, want := range inCheck { if !validTypes[want] { t.Errorf("%q is accepted by the database CHECK and missing from validTypes", want) } } if len(validTypes) != len(inCheck) { t.Errorf("validTypes has %d members and the CHECK has %d. A value the database refuses would be "+ "accepted here and fail as a raw constraint violation.", len(validTypes), len(inCheck)) } }