201f936e9a
gates / gates (push) Successful in 48s
POST /api/server/poweroff (dashboard login, POST only, the typed word KIKAPCSOLÁS / TURN OFF) → agent POST /host/poweroff (MinAgent 0.155.0; hidden for an older agent). The controller refuses during its own backup/restore/app update; the agent's reasons are shown as household sentences. „Kiszolgáló újraindítása" becomes „A Felhom újraindítása" and says it restarts the Felhom part; /api/server/reboot is POST only (R-983). Tests red-proofed; parity fixtures regenerated on purpose.
145 lines
6.6 KiB
Go
145 lines
6.6 KiB
Go
package web
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/agentapi"
|
|
)
|
|
|
|
// R-998 — „A gép kikapcsolása". The consequence is asserted: was the agent asked to switch the machine off, or not?
|
|
|
|
type fakePowerAgent struct {
|
|
support agentapi.SupportState
|
|
err error
|
|
calls int
|
|
}
|
|
|
|
func (f *fakePowerAgent) HostPowerOff(context.Context) error { f.calls++; return f.err }
|
|
func (f *fakePowerAgent) Supports(context.Context, agentapi.Feature) agentapi.SupportState {
|
|
return f.support
|
|
}
|
|
|
|
func powerServer(t *testing.T, a *fakePowerAgent) *Server {
|
|
t.Helper()
|
|
s := testPageServer(t)
|
|
s.powerOffAgentFn = func() (powerOffAgent, error) { return a, nil }
|
|
s.powerOffBusyFn = func() (bool, string) { return false, "" }
|
|
return s
|
|
}
|
|
|
|
func postPowerOff(s *Server, method, body string) *httptest.ResponseRecorder {
|
|
r := httptest.NewRequest(method, "/api/server/poweroff", strings.NewReader(body))
|
|
w := httptest.NewRecorder()
|
|
s.HandleServerPowerOff(w, r)
|
|
return w
|
|
}
|
|
|
|
// Without the typed word nothing is switched off; with it (either language, either spelling) the agent is asked once.
|
|
func TestPowerOff_ConfirmationWordRequired(t *testing.T) {
|
|
for _, body := range []string{``, `{}`, `{"confirm":""}`, `{"confirm":"igen"}`, `{"confirm":"KIKAPCS"}`} {
|
|
a := &fakePowerAgent{support: agentapi.SupportYes}
|
|
w := postPowerOff(powerServer(t, a), http.MethodPost, body)
|
|
if w.Code != http.StatusBadRequest || a.calls != 0 {
|
|
t.Fatalf("body %q: got %d, agent asked %d times — want 400 and no call", body, w.Code, a.calls)
|
|
}
|
|
if !strings.Contains(w.Body.String(), "KIKAPCSOLÁS") {
|
|
t.Fatalf("the refusal does not name the word: %s", w.Body.String())
|
|
}
|
|
}
|
|
for _, word := range []string{"KIKAPCSOLÁS", "kikapcsolás", " KIKAPCSOLAS ", "TURN OFF", "turn off"} {
|
|
a := &fakePowerAgent{support: agentapi.SupportYes}
|
|
w := postPowerOff(powerServer(t, a), http.MethodPost, `{"confirm":"`+word+`"}`)
|
|
if w.Code != http.StatusAccepted || a.calls != 1 {
|
|
t.Fatalf("word %q: got %d (%s), calls %d — want 202 and one call", word, w.Code, w.Body.String(), a.calls)
|
|
}
|
|
}
|
|
}
|
|
|
|
// POST only: a GET (a link, an image tag) must never switch the machine off.
|
|
func TestPowerOff_PostOnly(t *testing.T) {
|
|
a := &fakePowerAgent{support: agentapi.SupportYes}
|
|
w := postPowerOff(powerServer(t, a), http.MethodGet, `{"confirm":"KIKAPCSOLÁS"}`)
|
|
if w.Code != http.StatusMethodNotAllowed || a.calls != 0 {
|
|
t.Fatalf("GET: %d, calls %d", w.Code, a.calls)
|
|
}
|
|
}
|
|
|
|
// The agent's refusal reaches the household as a sentence; the controller's own busy work refuses before the agent.
|
|
func TestPowerOff_RefusalsShown(t *testing.T) {
|
|
a := &fakePowerAgent{support: agentapi.SupportYes, err: &agentapi.PowerOffRefused{Reason: agentapi.PowerOffBackup}}
|
|
w := postPowerOff(powerServer(t, a), http.MethodPost, `{"confirm":"KIKAPCSOLÁS"}`)
|
|
if w.Code != http.StatusConflict || !strings.Contains(w.Body.String(), "Most mentés vagy visszaállítás fut") ||
|
|
!strings.Contains(w.Body.String(), `"code":"backup_running"`) {
|
|
t.Fatalf("agent refusal: %d %s", w.Code, w.Body.String())
|
|
}
|
|
a2 := &fakePowerAgent{support: agentapi.SupportYes}
|
|
s := powerServer(t, a2)
|
|
s.powerOffBusyFn = func() (bool, string) { return true, "a backup or restore is running" }
|
|
if w := postPowerOff(s, http.MethodPost, `{"confirm":"KIKAPCSOLÁS"}`); w.Code != http.StatusConflict || a2.calls != 0 {
|
|
t.Fatalf("controller busy: %d, agent calls %d — want 409 and no call", w.Code, a2.calls)
|
|
}
|
|
}
|
|
|
|
// An agent older than the route: the card is HIDDEN with a one-line note, and the route answers agent_outdated
|
|
// without asking the agent. An agent that 404s anyway (version unknown) gets the same answer.
|
|
func TestPowerOff_HiddenForOldAgent(t *testing.T) {
|
|
old := &fakePowerAgent{support: agentapi.SupportNo}
|
|
s := powerServer(t, old)
|
|
page := getPage(t, s, "/settings").Body.String()
|
|
if strings.Contains(page, `id="btn-poweroff"`) || !strings.Contains(page, "data-poweroff-unavailable") {
|
|
t.Fatal("an old agent: the power-off button must be hidden, with the note")
|
|
}
|
|
if w := postPowerOff(s, http.MethodPost, `{"confirm":"KIKAPCSOLÁS"}`); w.Code != http.StatusPreconditionFailed || old.calls != 0 {
|
|
t.Fatalf("old agent: %d, calls %d", w.Code, old.calls)
|
|
}
|
|
cur := &fakePowerAgent{support: agentapi.SupportYes}
|
|
page = getPage(t, powerServer(t, cur), "/settings").Body.String()
|
|
if !strings.Contains(page, `id="btn-poweroff"`) || !strings.Contains(page, "A gép kikapcsolása") ||
|
|
!strings.Contains(page, "nyomd meg a gép bekapcsológombját") {
|
|
t.Fatal("a current agent: the card, its title and the power-button sentence must be on the page")
|
|
}
|
|
unknown := &fakePowerAgent{support: agentapi.SupportUnknown, err: &agentapi.StatusError{Method: "POST", Path: "/host/poweroff", Code: 404}}
|
|
if w := postPowerOff(powerServer(t, unknown), http.MethodPost, `{"confirm":"KIKAPCSOLÁS"}`); w.Code != http.StatusPreconditionFailed {
|
|
t.Fatalf("404 from an unknown-version agent: %d, want 412", w.Code)
|
|
}
|
|
}
|
|
|
|
// R-983 (one of seven): the restart route must not act on a GET.
|
|
func TestServerReboot_PostOnly(t *testing.T) {
|
|
s := testServer(t)
|
|
w := httptest.NewRecorder()
|
|
s.HandleServerReboot(w, httptest.NewRequest(http.MethodGet, "/api/server/reboot", nil))
|
|
if w.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("GET /api/server/reboot = %d, want 405", w.Code)
|
|
}
|
|
}
|
|
|
|
// Every agent refusal reason maps to a household sentence that exists in BOTH languages (an unknown reason → the
|
|
// generic one), and an unreachable agent answers with its own sentence and asks nobody to switch anything off.
|
|
func TestPowerOff_EveryReasonHasASentence(t *testing.T) {
|
|
s := testPageServer(t)
|
|
b := s.bundle()
|
|
for _, reason := range []string{agentapi.PowerOffTooSoon, agentapi.PowerOffAlreadyAsked, agentapi.PowerOffBackup,
|
|
agentapi.PowerOffRestoreTest, agentapi.PowerOffOSUpdate, agentapi.PowerOffCtrlUpdate, agentapi.PowerOffDrive,
|
|
agentapi.PowerOffEscrow, agentapi.PowerOffBusy, agentapi.PowerOffOtherGuests, agentapi.PowerOffUnknown, "a_reason_from_the_future"} {
|
|
key := powerOffRefusalKey(reason)
|
|
for _, lang := range []string{"hu", "en"} {
|
|
if got := b.Msg(lang, key); got == "" || got == key {
|
|
t.Errorf("reason %q → key %q has no %s text", reason, key, lang)
|
|
}
|
|
}
|
|
}
|
|
down := testPageServer(t)
|
|
down.powerOffAgentFn = func() (powerOffAgent, error) { return nil, errors.New("agent not configured") }
|
|
down.powerOffBusyFn = func() (bool, string) { return false, "" }
|
|
w := postPowerOff(down, http.MethodPost, `{"confirm":"KIKAPCSOLÁS"}`)
|
|
if w.Code != http.StatusServiceUnavailable || !strings.Contains(w.Body.String(), "nem éri el a gép kezelőjét") {
|
|
t.Fatalf("agent down: %d %s", w.Code, w.Body.String())
|
|
}
|
|
}
|