Files
felhom.eu/hub/internal/web/offsite_window_test.go
T
admin d85198b835
gates / gates (push) Successful in 1m51s
hub(R-1299): the one-time clean-up window gets a button on the customer page
The Backup card's off-site part shows the window state (weekly switch, the normal cap, a waiting grant, the last
window) and a form: optional limit -> a confirm step that names the customer and the limit -> the SAME route and
rule as the script address (POST /offsite/window-grant/<customer>, wr(grpOffsite), the page's CSRF). A waiting grant
can be cancelled. A raised limit above the store's measured snapshot count is refused (the 500 ceiling stays). Every
grant and cancel: a log line and an operator-only info event offsite_window_grant (who, customer, limit).
Tests TestR1299_* (five), red-proved by five mutations.
2026-10-27 17:23:37 +01:00

215 lines
9.8 KiB
Go

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&#39;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)
}
}