Files
felhom.eu/hub/internal/web/host_opactions_test.go
T

95 lines
3.9 KiB
Go

package web
import (
"net/http"
"net/url"
"strings"
"testing"
"time"
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
)
// Operator brief 2026-10-29: „Restart controller" (the AGENT's closed list) and „Clear hold" (D64 = B, the
// controller's list) on the host page — each behind a confirm step, CSRF like every form.
func heldFixture(t *testing.T) (*Server, *store.Store) {
t.Helper()
s, st := newTestServer(t)
if err := st.UpsertHost(&store.Host{HostID: "h1", CustomerID: "c1", APIKey: "k"}); err != nil {
t.Fatal(err)
}
now := time.Now()
if err := st.UpsertGuestFromReport(&store.Guest{GuestID: "h1-9201", CustomerID: "c1", HostID: "h1", VMID: 9201, DisplayName: "felhom",
Status: "running", ControllerVersion: "0.347.0", LastSeenAt: &now}); err != nil {
t.Fatal(err)
}
if err := st.UpsertHeldApp("c1", "radicale", "restore_mixed", now.Add(-time.Hour), "a restore stopped half-way", now); err != nil {
t.Fatal(err)
}
if err := st.UpsertHeldApp("c1", "gokapi", "unhealthy_stop", now.Add(-time.Hour), "stopped in a crash loop", now); err != nil {
t.Fatal(err)
}
return s, st
}
// RED-PROOF: seen failing before the forms existed.
func TestHostPage_RestartAndClearHoldRenderBehindAConfirm(t *testing.T) {
s, _ := heldFixture(t)
body := renderHost(t, s, "h1")
for _, want := range []string{
`data-restart-controller="9201"`, `name="action" value="restart_controller"`, `name="arg" value="9201"`,
"Restart the controller of guest 9201 on h1 (c1)?",
`data-clear-hold="radicale"`, `name="action" value="clear_hold"`, `name="arg" value="radicale"`,
"This hold protects data from a failed restore. If you clear it, the app may start on half-restored data. Check the restore copy first.",
`data-clear-hold="gokapi"`,
} {
if !strings.Contains(body, want) {
t.Errorf("host page lacks %q", want)
}
}
// the warning only on the data-protecting hold
gk := body[strings.Index(body, `data-clear-hold="gokapi"`):]
gk = gk[:strings.Index(gk, "</details>")]
if strings.Contains(gk, "half-restored") {
t.Error("a crash-loop hold shows the restore warning")
}
if !strings.Contains(gk, "unhealthy_stop") {
t.Error("the confirm step does not name the hold's reason")
}
}
// restart_controller: a row for the AGENT of this host (never the controller's ACK); clear_hold only for an app that
// IS held; every other verb or argument refused with no row. RED-PROOF: seen failing before the handler routed them.
func TestHostPage_RestartAndClearHoldPosts(t *testing.T) {
s, st := heldFixture(t)
if rr := postOperatorAction(t, s, "h1", url.Values{"action": {"restart_controller"}, "arg": {"9201"}}); rr.Code != http.StatusSeeOther {
t.Fatalf("restart: %d %s", rr.Code, rr.Body.String())
}
if a, _ := st.PendingHostOperatorActions("h1"); len(a) != 1 || a[0].Arg != "9201" {
t.Fatalf("the agent's reply would list %+v", a)
}
if c, _ := st.PendingOperatorActions("c1"); len(c) != 0 {
t.Fatalf("the restart reached the controller's ACK: %+v", c)
}
if rr := postOperatorAction(t, s, "h1", url.Values{"action": {"clear_hold"}, "arg": {"radicale"}}); rr.Code != http.StatusSeeOther {
t.Fatalf("clear_hold: %d %s", rr.Code, rr.Body.String())
}
if c, _ := st.PendingOperatorActions("c1"); len(c) != 1 || c[0].Action != "clear_hold" || c[0].Arg != "radicale" {
t.Fatalf("the controller's ACK would list %+v", c)
}
for _, f := range []url.Values{
{"action": {"clear_hold"}, "arg": {"vikunja"}}, // not held
{"action": {"restart_controller"}, "arg": {"9202"}}, // not a guest of this host
{"action": {"restart_controller"}, "arg": {"abc"}},
{"action": {"reboot_guest"}, "arg": {"9201"}},
} {
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", 20); len(rows) != 2 {
t.Fatalf("rows = %d, want the two accepted presses only", len(rows))
}
}