package web import ( "net/http" "net/http/httptest" "net/url" "strings" "testing" "gitea.dooplex.hu/admin/felhom-hub/internal/intent" "gitea.dooplex.hu/admin/felhom-hub/internal/store" ) // `09` §3 decision 185 (D1). Red test (4) of the design, web half: a host-page POST stores a row and // bumps the box's intent; an unknown action is refused with no row. Plus a render test per branch of // the card's template gate (the seam-built-but-never-wired trap covers templates). func postOperatorAction(t *testing.T, s *Server, hostID string, form url.Values) *httptest.ResponseRecorder { t.Helper() req := httptest.NewRequest(http.MethodPost, "/hosts/"+hostID+"/operator-action", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("X-Forwarded-For", "10.9.8.7") // The CLI channel's CSRF pass (validateCSRF): Basic auth + the operator header. req.SetBasicAuth("", "pw") req.Header.Set(OperatorCLIHeader, "confirm") rr := httptest.NewRecorder() s.handleOperatorAction(rr, req, hostID) return rr } func TestOperatorAction_PostStoresAndBumpsIntent(t *testing.T) { s, st := newTestServer(t) hub := intent.New() s.SetIntentHub(hub) if err := st.UpsertHost(&store.Host{HostID: "h1", CustomerID: "c1", APIKey: "k"}); err != nil { t.Fatal(err) } before := hub.Generation("c1") rr := postOperatorAction(t, s, "h1", url.Values{"action": {"abandon_extend"}, "arg": {"14"}}) if rr.Code != http.StatusSeeOther { t.Fatalf("status = %d body=%s", rr.Code, rr.Body.String()) } rows, _ := st.ListOperatorActions("c1", 10) if len(rows) != 1 || rows[0].Action != "abandon_extend" || rows[0].Arg != "14" || rows[0].DoneAt != nil { t.Fatalf("rows = %+v", rows) } if !strings.Contains(rows[0].RequestedBy, "10.9.8.7") { t.Errorf("requested_by = %q, want the operator's address", rows[0].RequestedBy) } if hub.Generation("c1") == before { t.Error("the box's intent was not bumped — its wait channel would not wake") } if p, _ := st.PendingOperatorActions("c1"); len(p) != 1 { t.Fatalf("the next ACK would list %d action(s), want 1", len(p)) } } func TestOperatorAction_UnknownRefusedNothingStored(t *testing.T) { s, st := newTestServer(t) hub := intent.New() s.SetIntentHub(hub) if err := st.UpsertHost(&store.Host{HostID: "h1", CustomerID: "c1", APIKey: "k"}); err != nil { t.Fatal(err) } before := hub.Generation("c1") for _, f := range []url.Values{ {"action": {"delete_offsite"}}, {"action": {"run_job"}, "arg": {"offsite-abandon-sweep"}}, {"action": {"abandon_extend"}, "arg": {"45"}}, } { if rr := postOperatorAction(t, s, "h1", f); rr.Code != http.StatusBadRequest { t.Errorf("%v: status = %d, want 400", f, rr.Code) } } if rows, _ := st.ListOperatorActions("c1", 10); len(rows) != 0 { t.Fatalf("a refused press stored %+v", rows) } if hub.Generation("c1") != before { t.Error("a refused press woke the box") } // A host with no customer has no controller to act. if err := st.UpsertHost(&store.Host{HostID: "lonely", APIKey: "k2"}); err != nil { t.Fatal(err) } if rr := postOperatorAction(t, s, "lonely", url.Values{"action": {"abandon_stop"}}); rr.Code != http.StatusBadRequest { t.Errorf("no-customer host: status = %d, want 400", rr.Code) } } func renderHost(t *testing.T, s *Server, hostID string) string { t.Helper() rr := httptest.NewRecorder() s.handleHostDetail(rr, httptest.NewRequest(http.MethodGet, "/hosts/"+hostID, nil), hostID) if rr.Code != http.StatusOK { t.Fatalf("status = %d", rr.Code) } return rr.Body.String() } // One render per branch: customer + no rows, customer + rows (pending and closed), no customer. func TestOperatorAction_CardRendersPerBranch(t *testing.T) { s, st := newTestServer(t) if err := st.UpsertHost(&store.Host{HostID: "h1", CustomerID: "c1", APIKey: "k"}); err != nil { t.Fatal(err) } body := renderHost(t, s, "h1") if got := strings.Count(body, `action="/hosts/h1/operator-action"`); got != 4 { t.Errorf("customer host: %d operator-action forms, want 4", got) } for _, want := range []string{`value="offsite_backup_now"`, `value="abandon_stop"`, `value="abandon_extend"`, `value="run_job"`, `