116 lines
4.3 KiB
Go
116 lines
4.3 KiB
Go
package store
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// insertNotifLog inserts a notification_log row with an EXPLICIT created_at so pairing tests can
|
|
// order rows deterministically (datetime('now') is second-granularity — real calls tie).
|
|
func insertNotifLog(t *testing.T, s *Store, customerID, eventType, status, channel, createdAt string) {
|
|
t.Helper()
|
|
if _, err := s.db.Exec(`
|
|
INSERT INTO notification_log (customer_id, event_type, severity, message, status, error_message, channel, created_at)
|
|
VALUES (?, ?, 'error', 'm', ?, '', ?, ?)`,
|
|
customerID, eventType, status, channel, createdAt); err != nil {
|
|
t.Fatalf("insertNotifLog: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestLastCustomerSentAt_PairingQuery (v0.71.0 F11): the pairing-evidence query returns the max
|
|
// created_at over customer-channel status=sent rows of the given types ONLY — operator rows,
|
|
// failed rows and other event types must not count.
|
|
func TestLastCustomerSentAt_PairingQuery(t *testing.T) {
|
|
s := newTestStore(t)
|
|
|
|
// Noise that must NOT count:
|
|
insertNotifLog(t, s, "c1", "node_down", "sent", "operator", "2026-07-22 15:00:00") // wrong channel
|
|
insertNotifLog(t, s, "c1", "node_down", "failed", "customer", "2026-07-22 16:00:00") // wrong status
|
|
insertNotifLog(t, s, "c1", "backup_failed", "sent", "customer", "2026-07-22 17:00:00") // wrong type
|
|
insertNotifLog(t, s, "c2", "node_down", "sent", "customer", "2026-07-22 18:00:00") // wrong customer
|
|
|
|
// No qualifying row yet:
|
|
_, ok, err := s.LastCustomerSentAt("c1", []string{"node_stale", "node_down"})
|
|
if err != nil {
|
|
t.Fatalf("LastCustomerSentAt: %v", err)
|
|
}
|
|
if ok {
|
|
t.Fatal("no qualifying row must yield ok=false")
|
|
}
|
|
|
|
// Two qualifying rows — max wins:
|
|
insertNotifLog(t, s, "c1", "node_stale", "sent", "customer", "2026-07-22 12:59:00")
|
|
insertNotifLog(t, s, "c1", "node_down", "sent", "customer", "2026-07-22 13:29:00")
|
|
got, ok, err := s.LastCustomerSentAt("c1", []string{"node_stale", "node_down"})
|
|
if err != nil {
|
|
t.Fatalf("LastCustomerSentAt: %v", err)
|
|
}
|
|
if !ok {
|
|
t.Fatal("qualifying rows exist — ok must be true")
|
|
}
|
|
want := time.Date(2026, 7, 22, 13, 29, 0, 0, time.UTC)
|
|
if !got.Equal(want) {
|
|
t.Fatalf("max created_at = %v, want %v", got, want)
|
|
}
|
|
|
|
// Empty type list is a defined no-op:
|
|
if _, ok, err := s.LastCustomerSentAt("c1", nil); err != nil || ok {
|
|
t.Fatalf("empty eventTypes must return (zero, false, nil), got ok=%v err=%v", ok, err)
|
|
}
|
|
}
|
|
|
|
// TestSeedNotificationPrefs_InsertIfAbsent (v0.71.0 F12, Scenario D): the seed creates a row when
|
|
// none exists, and NEVER modifies a pre-existing row (insert-if-absent, not upsert). Companion
|
|
// red-proof: replacing the INSERT OR IGNORE with SaveNotificationPrefs makes the second half fail.
|
|
func TestSeedNotificationPrefs_InsertIfAbsent(t *testing.T) {
|
|
s := newTestStore(t)
|
|
defaults := []string{"node_down", "backup_failed", "disk_critical"}
|
|
|
|
seeded, err := s.SeedNotificationPrefs("c1", "x@example.com", defaults)
|
|
if err != nil {
|
|
t.Fatalf("seed: %v", err)
|
|
}
|
|
if !seeded {
|
|
t.Fatal("first seed must create a row")
|
|
}
|
|
prefs, err := s.GetNotificationPrefs("c1")
|
|
if err != nil || prefs == nil {
|
|
t.Fatalf("prefs after seed: %v %v", prefs, err)
|
|
}
|
|
if prefs.Email != "x@example.com" || prefs.CooldownHours != 6 || len(prefs.EnabledEvents) != 3 {
|
|
t.Fatalf("seeded row wrong: %+v", prefs)
|
|
}
|
|
|
|
// A customer-edited row must survive a later seed byte-identical:
|
|
if err := s.SaveNotificationPrefs("c1", "edited@example.com", []string{"node_down"}, 12); err != nil {
|
|
t.Fatalf("customer edit: %v", err)
|
|
}
|
|
seeded, err = s.SeedNotificationPrefs("c1", "reinstall@example.com", defaults)
|
|
if err != nil {
|
|
t.Fatalf("re-seed: %v", err)
|
|
}
|
|
if seeded {
|
|
t.Fatal("seed over an existing row must report seeded=false")
|
|
}
|
|
prefs, _ = s.GetNotificationPrefs("c1")
|
|
if prefs.Email != "edited@example.com" || prefs.CooldownHours != 12 || len(prefs.EnabledEvents) != 1 {
|
|
t.Fatalf("seed MODIFIED an existing row (upsert bug): %+v", prefs)
|
|
}
|
|
}
|
|
|
|
// TestSeedNotificationPrefs_EmptyEmailNoop: an empty registered email must not seed an
|
|
// unnotifiable row.
|
|
func TestSeedNotificationPrefs_EmptyEmailNoop(t *testing.T) {
|
|
s := newTestStore(t)
|
|
seeded, err := s.SeedNotificationPrefs("c1", "", []string{"node_down"})
|
|
if err != nil {
|
|
t.Fatalf("seed: %v", err)
|
|
}
|
|
if seeded {
|
|
t.Fatal("empty email must be a no-op")
|
|
}
|
|
if prefs, _ := s.GetNotificationPrefs("c1"); prefs != nil {
|
|
t.Fatalf("no row must exist, got %+v", prefs)
|
|
}
|
|
}
|