package incident import ( "context" "errors" "fmt" "os" "testing" "time" "github.com/itworx/pulse/internal/database" ) func TestIncidentRepositoryPostgreSQL(t *testing.T) { dsn := os.Getenv("PULSE_TEST_DATABASE_URL") if dsn == "" { t.Skip("PULSE_TEST_DATABASE_URL is not set") } ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) defer cancel() pool, err := database.NewPool(ctx, database.Config{URL: dsn, MaxConns: 12, MinConns: 1}) if err != nil { t.Fatal(err) } defer pool.Close() if err := database.Migrate(ctx, pool); err != nil { t.Fatal(err) } repository, err := NewRepository(pool) if err != nil { t.Fatal(err) } run := fmt.Sprintf("%012x", time.Now().UnixNano()&0xffffffffffff) ruleID := "00000000-0000-4000-8000-" + run versionID := fmt.Sprintf("00000000-0000-4001-8000-%012x", (time.Now().UnixNano()+1)&0xffffffffffff) alertID := fmt.Sprintf("00000000-0000-4002-8000-%012x", (time.Now().UnixNano()+2)&0xffffffffffff) entityID := fmt.Sprintf("00000000-0000-4003-8000-%012x", (time.Now().UnixNano()+3)&0xffffffffffff) incidentID := fmt.Sprintf("00000000-0000-4004-8000-%012x", (time.Now().UnixNano()+4)&0xffffffffffff) _, _ = pool.Exec(ctx, `DELETE FROM incidents WHERE id=$1; DELETE FROM alert_instances WHERE id=$2; DELETE FROM alert_rule_versions WHERE id=$3; DELETE FROM alert_rules WHERE id=$4; DELETE FROM entities WHERE id=$5`, incidentID, alertID, versionID, ruleID, entityID) if _, err := pool.Exec(ctx, `INSERT INTO entities (id,entity_type,canonical_name,display_name,first_seen_at) VALUES ($1,'host',$2,$3,now())`, entityID, "m8-09-host-"+run, "M8-09 host "+run); err != nil { t.Fatal(err) } if _, err := pool.Exec(ctx, `INSERT INTO alert_rules (id,schema_version,name,severity,condition,evaluation_interval_seconds,pending_seconds,resolve_seconds,unknown_behavior,message) VALUES ($1,1,'M8-09 rule','critical','{}',60,0,0,'become-unknown','{}')`, ruleID); err != nil { t.Fatal(err) } if _, err := pool.Exec(ctx, `INSERT INTO alert_rule_versions (id,rule_id,version_number,document) VALUES ($1,$2,1,'{}')`, versionID, ruleID); err != nil { t.Fatal(err) } if _, err := pool.Exec(ctx, `UPDATE alert_rules SET current_version_id=$1 WHERE id=$2`, versionID, ruleID); err != nil { t.Fatal(err) } if _, err := pool.Exec(ctx, `INSERT INTO alert_instances (id,rule_id,rule_version_id,fingerprint,entity_id,current_state,last_evaluated_at) VALUES ($1,$2,$3,$4,$5,'firing',now())`, alertID, ruleID, versionID, "m8-09-fingerprint-"+run, entityID); err != nil { t.Fatal(err) } defer func() { _, _ = pool.Exec(ctx, `DELETE FROM incidents WHERE id=$1; DELETE FROM alert_instances WHERE id=$2; DELETE FROM alert_rule_versions WHERE id=$3; DELETE FROM alert_rules WHERE id=$4; DELETE FROM entities WHERE id=$5`, incidentID, alertID, versionID, ruleID, entityID) }() now := time.Now().UTC().Truncate(time.Microsecond) candidate := Candidate{CorrelationKey: "dependency:" + entityID, Title: "Host unreachable", Summary: "primary dependency outage", Severity: SeverityCritical, StartedAt: now, Confidence: 1, CorrelationMethod: "primary_dependency", Signals: []Signal{{AlertID: alertID, EntityID: entityID, RuleName: "Host unreachable", Severity: SeverityCritical, State: "firing", DependencyOutage: true, ObservedAt: now}}} created, first, err := repository.UpsertCandidate(ctx, candidate) if err != nil || !first || len(created.Alerts) != 1 || len(created.Entities) != 1 { t.Fatalf("created=%+v first=%v err=%v", created, first, err) } repeated, first, err := repository.UpsertCandidate(ctx, candidate) if err != nil || first || repeated.ID != created.ID || len(repeated.Alerts) != 1 { t.Fatalf("repeated=%+v first=%v err=%v", repeated, first, err) } association, duplicate, err := repository.AssociateAlert(ctx, created.ID, alertID, "Operator confirmed common dependency.", 1, "operator-1") if err != nil || duplicate || !association.Manual { t.Fatalf("association=%+v duplicate=%v err=%v", association, duplicate, err) } if _, duplicate, err := repository.AssociateAlert(ctx, created.ID, alertID, "Operator confirmed common dependency.", 1, "operator-1"); err != nil || !duplicate { t.Fatalf("idempotent association duplicate=%v err=%v", duplicate, err) } if _, err := repository.UpdateStatus(ctx, created.ID, StatusResolved, 2, now.Add(time.Minute)); !errors.Is(err, ErrConflict) { t.Fatalf("stale status error=%v", err) } item, err := repository.Get(ctx, created.ID) if err != nil || len(item.Alerts) != 1 || !item.Alerts[0].Manual { t.Fatalf("get incident=%+v err=%v", item, err) } }