Files
felhom-controller/controller/cmd/controller/prefs_sync_wiring_test.go
T

148 lines
5.9 KiB
Go

package main
import (
"errors"
"io"
"log"
"path/filepath"
"testing"
"gitea.dooplex.hu/admin/felhom-controller/internal/notify"
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
)
// R-1199 REOPENED (2026-10-22) — the brief's four cases, against the real settings file and a hub that behaves like
// hub ≥ 0.165.0 (it keeps a household's list over a default push).
// RED-PROOF: with v0.338.0's rule restored (no hub read: a box with nothing saved pushes its default at every start)
// case A fails (the default reaches the hub) and case D fails (a second push) — seen failing 2026-10-22.
type fakeHub struct {
stored *notify.HubPrefs
readErr error
pushes []string // the source of every push
lastEvents []string
}
func (f *fakeHub) FetchPreferences() (*notify.HubPrefs, error) {
if f.readErr != nil {
return nil, f.readErr
}
if f.stored == nil {
return &notify.HubPrefs{Exists: false}, nil
}
cp := *f.stored
return &cp, nil
}
func (f *fakeHub) SyncPreferencesAs(source, email string, ev []string, cd int, cleared bool) (string, error) {
f.pushes = append(f.pushes, source)
if source != notify.PrefsSourceHousehold && f.stored != nil && f.stored.HouseholdSaved {
return "kept", nil
}
if f.stored == nil {
f.stored = &notify.HubPrefs{Exists: true}
}
f.stored.EnabledEvents, f.lastEvents = ev, ev
if email != "" {
f.stored.Email = email
}
if source == notify.PrefsSourceHousehold {
f.stored.HouseholdSaved = true
}
return "ok", nil
}
func newSettings(t *testing.T) *settings.Settings {
t.Helper()
s, err := settings.Load(filepath.Join(t.TempDir(), "settings.json"), log.New(io.Discard, "", 0))
if err != nil {
t.Fatal(err)
}
return s
}
var familyList = []string{"backup_failed", "disk_warning", "node_down"}
// A: the household saved on an earlier box; this box has no local save → nothing reaches the hub, the box adopts the
// list and its page (GetNotificationPrefs) shows it.
func TestR1199Reopened_A_ARebuiltBoxAdoptsTheHouseholdsList(t *testing.T) {
hub := &fakeHub{stored: &notify.HubPrefs{Exists: true, HouseholdSaved: true, Email: "family@example.hu", EnabledEvents: familyList, CooldownHours: 12}}
st := newSettings(t)
if got := startupPrefsSync(hub, st, nil); got != "adopted" {
t.Fatalf("startup = %q, want adopted", got)
}
if len(hub.pushes) != 0 {
t.Fatalf("a box with no list of its own pushed %v over the household's list", hub.pushes)
}
p := st.GetNotificationPrefs()
if !st.NotificationPrefsSaved() || len(p.EnabledEvents) != 3 || p.Email != "family@example.hu" || p.CooldownHours != 12 {
t.Fatalf("the page must show the household's list: %+v", p)
}
// D after A: the next start re-pushes the adopted list as the household's (it carries an address, R-922) — the
// same list, never the default.
startupPrefsSync(hub, st, nil)
if len(hub.pushes) != 1 || hub.pushes[0] != notify.PrefsSourceHousehold || len(hub.lastEvents) != 3 {
t.Fatalf("after adoption: pushes %v events %v", hub.pushes, hub.lastEvents)
}
}
// B: no household ever saved (a seeded row, or none) → the default goes once, is saved here, and the hub mails by it.
func TestR1199Reopened_B_NoHouseholdListAnywhereTakesTheDefaultOnce(t *testing.T) {
for _, stored := range []*notify.HubPrefs{nil, {Exists: true, Email: "seed@example.hu", EnabledEvents: []string{"node_down"}}} {
hub := &fakeHub{stored: stored}
st := newSettings(t)
if got := startupPrefsSync(hub, st, nil); got != "default-pushed" {
t.Fatalf("startup = %q, want default-pushed", got)
}
if len(hub.pushes) != 1 || hub.pushes[0] != notify.PrefsSourceDefault || len(hub.stored.EnabledEvents) != len(settings.DefaultEnabledEvents) {
t.Fatalf("the default must reach the hub once, as a default: %v %v", hub.pushes, hub.stored.EnabledEvents)
}
if !st.NotificationPrefsSaved() {
t.Fatal("the pushed default must be saved on the box")
}
// D: a restart sends nothing (a local save exists, with no address).
if got := startupPrefsSync(hub, st, nil); got != "nothing" || len(hub.pushes) != 1 {
t.Fatalf("restart after B: %q, pushes %v — want nothing", got, hub.pushes)
}
}
}
// C: the household saves on the box → the page save pushes as the household's (web handler, unchanged path); at start
// a saved list with an address is re-pushed as the household's (R-922).
func TestR1199Reopened_C_ASavedListIsTheHouseholds(t *testing.T) {
hub := &fakeHub{stored: &notify.HubPrefs{Exists: true, HouseholdSaved: true, EnabledEvents: familyList}}
st := newSettings(t)
st.SetNotificationPrefs(&settings.NotificationPrefs{Email: "new@example.hu", EnabledEvents: []string{"node_down"}})
if got := startupPrefsSync(hub, st, nil); got != "pushed" || hub.pushes[0] != notify.PrefsSourceHousehold {
t.Fatalf("a saved list: %q %v", got, hub.pushes)
}
if len(hub.stored.EnabledEvents) != 1 {
t.Fatalf("the household's new choice must reach the hub: %v", hub.stored.EnabledEvents)
}
}
// A hub that cannot be read gets nothing (never a blind default); a hub that keeps its list over a race is not
// overwritten and the box saves nothing (it adopts at the next start).
func TestR1199Reopened_AnUnreadableHubGetsNothing(t *testing.T) {
hub := &fakeHub{readErr: errors.New("hub answered HTTP 404")}
st := newSettings(t)
if got := startupPrefsSync(hub, st, nil); got != "no-hub" || len(hub.pushes) != 0 || st.NotificationPrefsSaved() {
t.Fatalf("unreadable hub: %q pushes %v saved %t", got, hub.pushes, st.NotificationPrefsSaved())
}
}
// The default list the page shows carries the three events the rehearsal household did not get (kept from R-1199).
func TestR1199_TheDefaultListHoldsTheAlarmsTheHouseholdMissed(t *testing.T) {
want := map[string]bool{"disk_warning": false, "storage_disconnected": false, "crossdrive_failed": false}
for _, e := range settings.DefaultEnabledEvents {
if _, ok := want[e]; ok {
want[e] = true
}
}
for e, ok := range want {
if !ok {
t.Fatalf("%s is not in the default list", e)
}
}
}