gates / gates (push) Successful in 1m51s
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.
215 lines
9.8 KiB
Go
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'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)
|
|
}
|
|
}
|