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()) } }