d3b284863e
gates / gates (push) Successful in 26s
Allowlisted, not operator-only, seeded for new households and added once (add-only) to every existing enabled_events row. mail.event entries in hu and en name the app from details.stack_name. Per-app cooldown on both the operator and the household leg. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
102 lines
3.5 KiB
Go
102 lines
3.5 KiB
Go
package store
|
|
|
|
import (
|
|
"io"
|
|
"log"
|
|
"path/filepath"
|
|
"reflect"
|
|
"testing"
|
|
)
|
|
|
|
// v0.120.0 — every EXISTING household hears about an undone or held update (`09` §3 decision 15),
|
|
// by a ONE-TIME, ADD-ONLY migration. Run the real way: a DB written by an older hub (no guard row),
|
|
// reopened by this one.
|
|
//
|
|
// COMPANION RED-PROOF (REPORT.md): drop the SeedEventTypesOnce call from migrate() — c1 then keeps
|
|
// [backup_failed] and this test fails on the first row.
|
|
func TestAppUpdateSeed_ExistingHouseholdsGainBothTypesOnce(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "hub.db")
|
|
s, err := New(path, log.New(io.Discard, "", 0))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// An older hub's DB: no guard row, four households.
|
|
if _, err := s.db.Exec(`DELETE FROM hub_settings WHERE key = 'seed_app_update_events_v1'`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for id, ev := range map[string][]string{
|
|
"c1": {"backup_failed"},
|
|
"c2": {"app_update_undone"},
|
|
"c3": {"app_update_undone", "app_update_held", "disk_warning"},
|
|
} {
|
|
if err := s.SaveNotificationPrefs(id, id+"@example.com", ev, 6); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := s.SaveNotificationPrefs("c4", "c4@example.com", nil, 6); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := s.db.Exec(`UPDATE customer_notifications SET enabled_events = 'not json' WHERE customer_id = 'c4'`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s.Close()
|
|
|
|
s = reopen(t, path)
|
|
want := map[string][]string{
|
|
"c1": {"backup_failed", "app_update_undone", "app_update_held"},
|
|
"c2": {"app_update_undone", "app_update_held"},
|
|
"c3": {"app_update_undone", "app_update_held", "disk_warning"}, // untouched — order kept
|
|
}
|
|
for id, w := range want {
|
|
p, err := s.GetNotificationPrefs(id)
|
|
if err != nil || p == nil {
|
|
t.Fatalf("%s: %v", id, err)
|
|
}
|
|
if !reflect.DeepEqual(p.EnabledEvents, w) {
|
|
t.Errorf("%s: enabled_events = %v, want %v (add-only: nothing removed, nothing reordered)", id, p.EnabledEvents, w)
|
|
}
|
|
}
|
|
var raw string
|
|
if err := s.db.QueryRow(`SELECT enabled_events FROM customer_notifications WHERE customer_id='c4'`).Scan(&raw); err != nil || raw != "not json" {
|
|
t.Errorf("a corrupt row must be left exactly as it was, got %q (%v)", raw, err)
|
|
}
|
|
|
|
// A household that opts OUT afterwards stays out: the migration runs once.
|
|
if err := s.SaveNotificationPrefs("c1", "c1@example.com", []string{"backup_failed"}, 6); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s.Close()
|
|
s = reopen(t, path)
|
|
if p, _ := s.GetNotificationPrefs("c1"); !reflect.DeepEqual(p.EnabledEvents, []string{"backup_failed"}) {
|
|
t.Fatalf("a later opt-out must stick — the seed must run ONCE; got %v", p.EnabledEvents)
|
|
}
|
|
}
|
|
|
|
// The direct call reports which rows changed — that list is what the startup log names.
|
|
func TestAppUpdateSeed_ReportsTheChangedRows(t *testing.T) {
|
|
s := newTestStore(t)
|
|
if err := s.SaveNotificationPrefs("a", "a@example.com", []string{"backup_failed"}, 6); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.SaveNotificationPrefs("b", "b@example.com", []string{"x_undone", "x_held"}, 6); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := s.SeedEventTypesOnce("test_guard", []string{"x_undone", "x_held"})
|
|
if err != nil || !reflect.DeepEqual(got, []string{"a"}) {
|
|
t.Fatalf("changed = %v (%v), want [a]", got, err)
|
|
}
|
|
if again, err := s.SeedEventTypesOnce("test_guard", []string{"x_undone", "x_held"}); err != nil || again != nil {
|
|
t.Fatalf("a second run must do nothing, got %v (%v)", again, err)
|
|
}
|
|
}
|
|
|
|
func reopen(t *testing.T, path string) *Store {
|
|
t.Helper()
|
|
s, err := New(path, log.New(io.Discard, "", 0))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { s.Close() })
|
|
return s
|
|
}
|