124 lines
6.0 KiB
Go
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
|
|
}
|