package web import ( "net/http" "net/http/httptest" "net/url" "strings" "testing" "time" "gitea.dooplex.hu/admin/felhom-hub/internal/store" ) // R-1299: the one-shot clean-up window on the customer page. The page renders the block per gate branch; a grant // through the form (session + CSRF + the confirm step) stores ONE grant with its limit; the box's next window uses it // up; a limit above the store's snapshot count is refused; a POST without CSRF never reaches the store. const windowReport = `{"customer_id":"acme","customer_name":"Acme","offsite":{"enabled":true,"state":"active", "last_status":"ok","last_success":"2026-10-27T03:17:00Z","snapshot_count":40,"stats_known":true}}` type windowEvent struct{ customer, typ, sev, msg, details string } // windowRig: a password-protected server (so RequireAuth and the CSRF gate run) with one customer whose box reported // 40 off-site snapshots, the grant functions wired as main.go wires them (the store's), and the events captured. func windowRig(t *testing.T, report string) (*Server, *store.Store, http.Handler, *[]windowEvent) { t.Helper() s, st := serverWithPassword(t, "op-pass") if err := st.SaveCustomerConfig(&store.CustomerConfig{CustomerID: "acme", CustomerName: "Acme", Domain: "acme.hu", RetrievalPassword: "pw", APIKey: "k", Status: "active"}); err != nil { t.Fatal(err) } if report != "" { if err := st.SaveReport("acme", []byte(report)); err != nil { t.Fatal(err) } } s.SetOffsiteWindowAdmin(st.GrantOffsiteWindowOnce, st.SetOffsiteWindowsEnabled) s.SetOffsiteWindowLargeGrant(st.GrantOffsiteWindowOnceMax) var evs []windowEvent s.SetEventEmitter(func(c, typ, sev, msg, det, _ string) { evs = append(evs, windowEvent{c, typ, sev, msg, det}) }) s.sessionsMu.Lock() s.sessions["sess1"] = &hubSession{expiresAt: time.Now().Add(time.Hour), csrfToken: "tok1"} s.sessionsMu.Unlock() return s, st, s.Handler(), &evs } func windowPost(h http.Handler, form url.Values, csrf bool) *httptest.ResponseRecorder { if csrf { form.Set("_csrf", "tok1") } r := httptest.NewRequest(http.MethodPost, "/offsite/window-grant/acme", strings.NewReader(form.Encode())) r.Header.Set("Content-Type", "application/x-www-form-urlencoded") r.AddCookie(&http.Cookie{Name: SessionCookieName, Value: "sess1"}) w := httptest.NewRecorder() h.ServeHTTP(w, r) return w } func windowPage(t *testing.T, h http.Handler, query string) string { t.Helper() r := httptest.NewRequest(http.MethodGet, "/customers/acme"+query, nil) r.AddCookie(&http.Cookie{Name: SessionCookieName, Value: "sess1"}) w := httptest.NewRecorder() h.ServeHTTP(w, r) if w.Code != http.StatusOK { t.Fatalf("GET /customers/acme%s: %d", query, w.Code) } return w.Body.String() } // The page, per branch of its gates (the seam-wiring rule: a handler test alone proves nothing about reachability). // RED-PROOF: remove the block from customer_unified.html → the first case fails; drop `{{if .Pending}}` → the third. func TestR1299_ThePageShowsTheWindowBlockPerBranch(t *testing.T) { _, st, h, _ := windowRig(t, windowReport) page := windowPage(t, h, "") for _, want := range []string{`id="offsite-window"`, `id="window-form"`, `name="window" value="confirm"`, "Normal cap per window", "20 (half the snapshots", "at most 40", "none waiting", "never"} { if !strings.Contains(page, want) { t.Errorf("the page lacks %q", want) } } page = windowPage(t, h, "?window=confirm&window_max=33") for _, want := range []string{`id="window-confirm"`, "a limit of 33", `name="confirm" value="1"`, `name="_csrf" value="tok1"`, `name="from" value="page"`} { if !strings.Contains(page, want) { t.Errorf("the confirm step lacks %q", want) } } if !strings.Contains(windowPage(t, h, "?window=confirm&window_max=41"), "above the store's 40 snapshot(s)") { t.Error("a limit above the snapshot count must render the refusal at the confirm step, not the confirm") } if err := st.GrantOffsiteWindowOnceMax("acme", 30); err != nil { t.Fatal(err) } page = windowPage(t, h, "") if !strings.Contains(page, "limit 30") || !strings.Contains(page, `name="cancel" value="1"`) || strings.Contains(page, `id="window-form"`) { t.Error("a waiting grant must show its limit and the cancel button, and no second form") } _, _, h2, _ := windowRig(t, `{"customer_id":"acme","offsite":{"enabled":false}}`) if strings.Contains(windowPage(t, h2, ""), `id="offsite-window"`) { t.Error("the block must not render when off-site is not enabled") } } // The form's grant: ONE grant with the limit, a redirect with its flash, an operator event naming who and the limit; // the box's window uses it up, and the next window finds none. // RED-PROOF: make the handler ignore max_remove → the stored grant has no limit → fails. func TestR1299_AGrantThroughTheFormStoresOneGrantWithItsLimit(t *testing.T) { _, st, h, evs := windowRig(t, windowReport) w := windowPost(h, url.Values{"from": {"page"}, "confirm": {"1"}, "max_remove": {"33"}}, true) if w.Code != http.StatusSeeOther || !strings.Contains(w.Header().Get("Location"), "flash=window_granted") { t.Fatalf("grant: %d Location=%q", w.Code, w.Header().Get("Location")) } if pending, max := st.PendingOffsiteWindowGrant("acme"); !pending || max != 33 { t.Fatalf("stored grant: pending=%v max=%d, want true 33", pending, max) } if len(*evs) != 1 || (*evs)[0].typ != eventWindowGrant || (*evs)[0].sev != "info" || !strings.Contains((*evs)[0].details, `"max_remove":33`) || !strings.Contains((*evs)[0].details, `"by":"operator session"`) { t.Fatalf("events: %+v — want one info %s naming the limit and who", *evs, eventWindowGrant) } if granted, max := st.TakeOffsiteWindowGrant("acme"); !granted || max != 33 { t.Fatalf("the window took granted=%v max=%d", granted, max) } if granted, _ := st.TakeOffsiteWindowGrant("acme"); granted { t.Fatal("a second window reused the one-shot grant") } if pending, _ := st.PendingOffsiteWindowGrant("acme"); pending { t.Fatal("the page would still show a used grant as waiting") } } // Refusals that store nothing: a limit above the snapshot count, a non-number, no confirm step, no CSRF token. // RED-PROOF: drop the bound in checkWindowLimit → the 41 case stores a grant → fails; drop the confirm check → the // no-confirm case stores one. func TestR1299_RefusalsStoreNothing(t *testing.T) { cases := []struct { name string form url.Values csrf bool want func(*httptest.ResponseRecorder) bool }{ {"above the snapshot count", url.Values{"from": {"page"}, "confirm": {"1"}, "max_remove": {"41"}}, true, func(w *httptest.ResponseRecorder) bool { return strings.Contains(w.Header().Get("Location"), "flash=window_limit_refused") }}, {"not a number", url.Values{"from": {"page"}, "confirm": {"1"}, "max_remove": {"2e2"}}, true, func(w *httptest.ResponseRecorder) bool { return strings.Contains(w.Header().Get("Location"), "flash=window_limit_refused") }}, {"script above the snapshot count", url.Values{"max_remove": {"500"}}, true, func(w *httptest.ResponseRecorder) bool { return w.Code == http.StatusBadRequest }}, {"no confirm step", url.Values{"from": {"page"}, "max_remove": {"10"}}, true, func(w *httptest.ResponseRecorder) bool { return strings.Contains(w.Header().Get("Location"), "flash=window_needs_confirm") }}, {"no CSRF token", url.Values{"from": {"page"}, "confirm": {"1"}}, false, func(w *httptest.ResponseRecorder) bool { return w.Code == http.StatusForbidden }}, } for _, c := range cases { _, st, h, evs := windowRig(t, windowReport) w := windowPost(h, c.form, c.csrf) if !c.want(w) { t.Errorf("%s: %d Location=%q", c.name, w.Code, w.Header().Get("Location")) } if pending, _ := st.PendingOffsiteWindowGrant("acme"); pending { t.Errorf("%s: a grant was stored", c.name) } if len(*evs) != 0 { t.Errorf("%s: %d event(s) for a refused request", c.name, len(*evs)) } } } // Without a measured snapshot count only the ceiling bounds the limit (D55's 260 must stay possible). func TestR1299_WithoutACountOnlyTheCeilingBounds(t *testing.T) { _, st, h, _ := windowRig(t, "") if w := windowPost(h, url.Values{"from": {"page"}, "confirm": {"1"}, "max_remove": {"260"}}, true); !strings.Contains(w.Header().Get("Location"), "flash=window_granted") { t.Fatalf("260 with no count: Location=%q", w.Header().Get("Location")) } if _, max := st.PendingOffsiteWindowGrant("acme"); max != 260 { t.Fatalf("stored max %d, want 260", max) } _, st2, h2, _ := windowRig(t, "") if w := windowPost(h2, url.Values{"from": {"page"}, "confirm": {"1"}, "max_remove": {"501"}}, true); !strings.Contains(w.Header().Get("Location"), "flash=window_limit_refused") { t.Fatalf("501: Location=%q", w.Header().Get("Location")) } if pending, _ := st2.PendingOffsiteWindowGrant("acme"); pending { t.Fatal("501 stored a grant") } } // The cancel removes a waiting grant and records who; with none waiting it changes nothing. func TestR1299_CancelRemovesAWaitingGrant(t *testing.T) { _, st, h, evs := windowRig(t, windowReport) if w := windowPost(h, url.Values{"from": {"page"}, "cancel": {"1"}}, true); !strings.Contains(w.Header().Get("Location"), "flash=window_none_waiting") { t.Fatalf("cancel with none waiting: Location=%q", w.Header().Get("Location")) } _ = st.GrantOffsiteWindowOnce("acme") if w := windowPost(h, url.Values{"from": {"page"}, "cancel": {"1"}}, true); !strings.Contains(w.Header().Get("Location"), "flash=window_cancelled") { t.Fatalf("cancel: Location=%q", w.Header().Get("Location")) } if pending, _ := st.PendingOffsiteWindowGrant("acme"); pending { t.Fatal("the grant is still waiting after the cancel") } if granted, _ := st.TakeOffsiteWindowGrant("acme"); granted { t.Fatal("the box's window would still use the cancelled grant") } if len(*evs) != 1 || !strings.Contains((*evs)[0].details, `"cancelled":true`) { t.Fatalf("events: %+v — want one cancel event", *evs) } }