package api import ( "net/http" "strings" "testing" "time" "gitea.dooplex.hu/admin/felhom-hub/internal/store" ) // R-366 slice 2 (`09` §3 decision 168): archives the box's restore-test skipped as written with ANOTHER key reach the // operator as ONE line per change of the set — not per archive, not per report; never mailed (info), never the // household's. func reportWithForeign(foreign string) string { body := validReportBody("h1") return strings.TrimSuffix(body, "}") + `,"foreign_key_archives":{"tiers":` + foreign + `}}` } func foreignEvents(t *testing.T, st *store.Store) []store.Event { t.Helper() ev, err := st.GetEventsByType("c1", eventForeignKeyArchives, time.Now().Add(-time.Hour)) if err != nil { t.Fatal(err) } return ev } // COMPANION RED-PROOF (observed): remove the noteForeignKeyArchives call from handleHostReport → this fails with // "a new set of other-key archives must record exactly one operator event; got 0". Restored. func TestR366_ForeignKeyArchivesOneEventPerChange(t *testing.T) { h, st, _ := newTestHandler(t) st.SaveCustomerConfig(&store.CustomerConfig{CustomerID: "c1", APIKey: "ckey", RetrievalPassword: "p"}) st.UpsertHost(&store.Host{HostID: "h1", CustomerID: "c1", APIKey: "HKEY"}) set := `[{"target":"felhom-pbs","count":2,"oldest":"2026-08-18T03:58:43Z","newest":"2026-08-19T03:58:43Z"}]` for i := 0; i < 3; i++ { // the same set on three reports → one event if rr := do(h, http.MethodPost, "/host-report", "HKEY", reportWithForeign(set)); rr.Code != http.StatusOK { t.Fatalf("report %d: %d %s", i, rr.Code, rr.Body.String()) } } ev := foreignEvents(t, st) if len(ev) != 1 { t.Fatalf("a new set of other-key archives must record exactly one operator event; got %d", len(ev)) } if ev[0].Severity != "info" || !strings.Contains(ev[0].Message, "2 whole-guest backup archive(s)") || !strings.Contains(ev[0].Message, "felhom-pbs (2026-08-18 … 2026-08-19)") { t.Fatalf("the line must name the count, the tier and the date range; got %s %q", ev[0].Severity, ev[0].Message) } // An older agent or a fresh restart (the stanza absent) keeps the state: no new event. do(h, http.MethodPost, "/host-report", "HKEY", validReportBody("h1")) if n := len(foreignEvents(t, st)); n != 1 { t.Fatalf("an absent stanza must keep the state; got %d events", n) } // The set changes (a third archive) → one more line. do(h, http.MethodPost, "/host-report", "HKEY", reportWithForeign(`[{"target":"felhom-pbs","count":3,"oldest":"2026-08-18T03:58:43Z","newest":"2026-08-25T03:58:43Z"}]`)) if n := len(foreignEvents(t, st)); n != 2 { t.Fatalf("a changed set must record one more line; got %d events", n) } // Evaluated, none left → the state clears silently; the same set coming back later is news again. do(h, http.MethodPost, "/host-report", "HKEY", reportWithForeign(`[]`)) if n := len(foreignEvents(t, st)); n != 2 { t.Fatalf("an empty list clears without an event; got %d", n) } do(h, http.MethodPost, "/host-report", "HKEY", reportWithForeign(set)) if n := len(foreignEvents(t, st)); n != 3 { t.Fatalf("after a clear, a set is news again; got %d", n) } }