148 lines
5.9 KiB
Go
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 ¬ify.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 = ¬ify.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: ¬ify.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: ¬ify.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)
|
|
}
|
|
}
|
|
}
|