v0.310.0: „A gép kikapcsolása" — the household turns the whole machine off (R-998); honest restart text (R-999)
gates / gates (push) Successful in 48s
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.
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user