Files
felhom.eu/hub/internal/store/app_update_seed_test.go
T
admin d3b284863e
gates / gates (push) Successful in 26s
hub v0.120.0: app_update_undone/held reach the household, per app, in its language
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
2026-09-23 13:47:13 +02:00

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
}