05becb04d0
gates / gates (push) Successful in 4m9s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
77 lines
3.1 KiB
Go
77 lines
3.1 KiB
Go
package store
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// R-901: a deleted customer's events and notification_log rows go 1 year after the deletion — and nothing else goes.
|
|
// RED-PROOF: return early from PruneDeletedCustomerAudit (the pre-R-901 state: nothing prunes notification_log) →
|
|
// the c-old rows survive → this FAILS on the first assertion.
|
|
func TestR901_DeletedCustomerAuditGoesAfterOneYear(t *testing.T) {
|
|
s := newTestStore(t)
|
|
now := time.Date(2026, 10, 8, 12, 0, 0, 0, time.UTC)
|
|
ts := func(d time.Duration) string { return now.Add(-d).Format("2006-01-02 15:04:05") }
|
|
day := 24 * time.Hour
|
|
|
|
journal := func(cid, legs string, completedAgo time.Duration) {
|
|
if _, err := s.db.Exec(`INSERT INTO customer_resets (customer_id, started_at, completed_at, legs_json) VALUES (?, ?, ?, ?)`,
|
|
cid, ts(completedAgo+time.Minute), ts(completedAgo), legs); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
row := func(cid string, ago time.Duration) {
|
|
if _, err := s.db.Exec(`INSERT INTO events (customer_id, event_type, severity, message, created_at) VALUES (?, 'x', 'info', 'm', ?)`, cid, ts(ago)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := s.db.Exec(`INSERT INTO notification_log (customer_id, event_type, severity, message, status, created_at) VALUES (?, 'x', 'info', 'm', 'sent', ?)`, cid, ts(ago)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
deleted := `{"hosts":"ok","residue":"ok","customer_delete":"ok"}`
|
|
journal("c-old", deleted, 400*day) // deleted 400 days ago → its audit rows go
|
|
journal("c-recent", deleted, 100*day) // deleted 100 days ago → kept
|
|
journal("c-reset", `{"hetzner":"ok","pbs":"ok","db_purge":"ok"}`, 400*day) // a RESET, not a deletion → kept
|
|
row("c-old", 500*day)
|
|
row("c-old", 401*day)
|
|
row("c-old", 10*day) // the id re-used AFTER the deletion → kept
|
|
row("c-recent", 200*day)
|
|
row("c-reset", 500*day)
|
|
row("c-live", 900*day) // never deleted → kept (events' own 90-day prune is not this function's business)
|
|
|
|
ev, nl, err := s.PruneDeletedCustomerAudit(now, DeletedCustomerAuditKeep)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if ev != 2 || nl != 2 {
|
|
t.Fatalf("deleted events=%d notification_log=%d, want 2 and 2 (c-old's two rows before its deletion)", ev, nl)
|
|
}
|
|
count := func(table, cid string) int {
|
|
var n int
|
|
if err := s.db.QueryRow(`SELECT COUNT(*) FROM `+table+` WHERE customer_id = ?`, cid).Scan(&n); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return n
|
|
}
|
|
for _, c := range []struct {
|
|
cid string
|
|
want int
|
|
}{{"c-old", 1}, {"c-recent", 1}, {"c-reset", 1}, {"c-live", 1}} {
|
|
for _, tb := range []string{"events", "notification_log"} {
|
|
if got := count(tb, c.cid); got != c.want {
|
|
t.Errorf("%s rows for %s = %d, want %d", tb, c.cid, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
// The journal row (provenance) stays.
|
|
var j int
|
|
_ = s.db.QueryRow(`SELECT COUNT(*) FROM customer_resets WHERE customer_id = 'c-old'`).Scan(&j)
|
|
if j != 1 {
|
|
t.Fatalf("the deletion journal row must stay, got %d", j)
|
|
}
|
|
// Idempotent.
|
|
if ev, nl, _ := s.PruneDeletedCustomerAudit(now, DeletedCustomerAuditKeep); ev+nl != 0 {
|
|
t.Fatalf("a second run deleted %d+%d rows, want 0", ev, nl)
|
|
}
|
|
}
|