95 lines
3.9 KiB
Go
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))
|
|
}
|
|
}
|