Files
felhom.eu/hub/internal/api/household_prefs_test.go
T

124 lines
6.0 KiB
Go

package api
import (
"encoding/json"
"net/http"
"strings"
"testing"
)
// R-1199 reopened (2026-10-22): a push that is not the household's own save never overwrites a list the household
// saved (demo-felhom's 13-event list became controller 0.338.0's default at 07:54:55 UTC); a box reads the household's
// list instead (GET /preferences, self-scoped).
// RED-PROOF: with the `!household && existing.HouseholdSaved` refusal removed from handleSavePreferences, A and A-legacy
// fail (the default replaces the household's list) — seen failing 2026-10-22.
var householdList = []string{"backup_failed", "disk_warning", "node_down"}
func eventsOf(t *testing.T, h *Handler, key string) (saved bool, events []string) {
t.Helper()
rr := do(h, http.MethodGet, "/preferences?customer_id=c1", key, "")
if rr.Code != http.StatusOK {
t.Fatalf("GET /preferences = %d %s", rr.Code, rr.Body.String())
}
var out struct {
Exists bool `json:"exists"`
HouseholdSaved bool `json:"household_saved"`
EnabledEvents []string `json:"enabled_events"`
}
if err := json.Unmarshal(rr.Body.Bytes(), &out); err != nil {
t.Fatalf("decode: %v", err)
}
return out.HouseholdSaved, out.EnabledEvents
}
// A: the household saved its list (on an earlier box); this box pushes its default → the hub keeps the household's.
func TestR1199Reopened_A_ADefaultNeverOverwritesTheHouseholdsList(t *testing.T) {
h, st := newEventTestHandler(t)
if err := st.SaveNotificationPrefsAs("c1", "family@example.hu", householdList, 6, true); err != nil {
t.Fatal(err)
}
rr := do(h, http.MethodPost, "/preferences", "ckey",
`{"customer_id":"c1","email":"","enabled_events":["backup_failed","disk_warning","node_down","storage_disconnected","crossdrive_failed"],"cooldown_hours":6,"source":"default"}`)
if rr.Code != http.StatusOK || !strings.Contains(rr.Body.String(), `"kept"`) {
t.Fatalf("a default push over a household list: %d %s, want 200 kept", rr.Code, rr.Body.String())
}
p, _ := st.GetNotificationPrefs("c1")
if len(p.EnabledEvents) != 3 || !p.HouseholdSaved || p.Email != "family@example.hu" {
t.Fatalf("the household's list was changed: %+v", p)
}
}
// A-legacy: controller 0.338.0 sends no `source`; its start-up default push (no address, no clear) is not the
// household's either — the exact push that overwrote demo-felhom's list.
func TestR1199Reopened_A_The0338DefaultPushIsKeptOutToo(t *testing.T) {
h, st := newEventTestHandler(t)
st.SaveNotificationPrefsAs("c1", "family@example.hu", householdList, 6, true)
rr := do(h, http.MethodPost, "/preferences", "ckey",
`{"customer_id":"c1","email":"","enabled_events":["backup_failed"],"cooldown_hours":6}`)
if rr.Code != http.StatusOK || !strings.Contains(rr.Body.String(), `"kept"`) {
t.Fatalf("0.338.0's push: %d %s, want kept", rr.Code, rr.Body.String())
}
if p, _ := st.GetNotificationPrefs("c1"); len(p.EnabledEvents) != 3 {
t.Fatalf("0.338.0's default push changed the household's list: %+v", p.EnabledEvents)
}
}
// B: no household ever saved (the row was seeded at the claim) → the box's default is applied, still not marked.
func TestR1199Reopened_B_ASeededListTakesTheBoxsDefault(t *testing.T) {
h, st := newEventTestHandler(t)
if ok, err := st.SeedNotificationPrefs("c1", "family@example.hu", []string{"node_down"}); err != nil || !ok {
t.Fatalf("seed: %v %v", ok, err)
}
rr := do(h, http.MethodPost, "/preferences", "ckey",
`{"customer_id":"c1","email":"","enabled_events":["backup_failed","disk_warning"],"cooldown_hours":6,"source":"default"}`)
if rr.Code != http.StatusOK || strings.Contains(rr.Body.String(), "kept") {
t.Fatalf("default over a seed: %d %s, want applied", rr.Code, rr.Body.String())
}
saved, ev := eventsOf(t, h, "ckey")
if saved || len(ev) != 2 {
t.Fatalf("after the default: household_saved=%t events=%v, want false + the 2 defaults", saved, ev)
}
if p, _ := st.GetNotificationPrefs("c1"); p.Email != "family@example.hu" {
t.Fatal("the seeded address must stay (no-clobber guard)")
}
}
// C: the household saves on the box → applied and marked; a later default does not undo it.
func TestR1199Reopened_C_AHouseholdSaveIsAppliedAndMarked(t *testing.T) {
h, st := newEventTestHandler(t)
st.SeedNotificationPrefs("c1", "family@example.hu", []string{"node_down"})
if rr := do(h, http.MethodPost, "/preferences", "ckey",
`{"customer_id":"c1","email":"","enabled_events":["backup_failed"],"cooldown_hours":6,"source":"household"}`); rr.Code != http.StatusOK {
t.Fatalf("household save: %d", rr.Code)
}
if saved, ev := eventsOf(t, h, "ckey"); !saved || len(ev) != 1 {
t.Fatalf("after the household's save: %t %v", saved, ev)
}
do(h, http.MethodPost, "/preferences", "ckey", `{"customer_id":"c1","enabled_events":["node_down","disk_warning"],"source":"default"}`)
if _, ev := eventsOf(t, h, "ckey"); len(ev) != 1 || ev[0] != "backup_failed" {
t.Fatalf("a default after the household's save changed it: %v", ev)
}
// An older controller's push WITH an address is the household's (only a saved list carries one).
do(h, http.MethodPost, "/preferences", "ckey", `{"customer_id":"c1","email":"new@example.hu","enabled_events":["node_down"]}`)
if p, _ := st.GetNotificationPrefs("c1"); p.Email != "new@example.hu" || len(p.EnabledEvents) != 1 || p.EnabledEvents[0] != "node_down" {
t.Fatalf("an older controller's address push must still apply: %+v", p)
}
}
// The read is self-scoped: another household's key is refused; no row → exists false.
func TestR1199Reopened_TheReadIsSelfScoped(t *testing.T) {
h, st := newTwoCustomerHandler(t)
if rr := do(h, http.MethodGet, "/preferences?customer_id=c1", "c2key", ""); rr.Code != http.StatusForbidden {
t.Fatalf("c2 reading c1: %d, want 403", rr.Code)
}
if rr := do(h, http.MethodGet, "/preferences?customer_id=c1", "", ""); rr.Code != http.StatusUnauthorized {
t.Fatalf("no key: %d, want 401", rr.Code)
}
rr := do(h, http.MethodGet, "/preferences?customer_id=c2", "c2key", "")
if rr.Code != http.StatusOK || !strings.Contains(rr.Body.String(), `"exists":false`) {
t.Fatalf("c2 has no row: %d %s", rr.Code, rr.Body.String())
}
_ = st
}