package web import ( "context" "encoding/json" "log" "net/http" "net/http/httptest" "strings" "testing" "gitea.dooplex.hu/admin/felhom-controller/internal/agentapi" ) // S6 old-agent degradation: the Debug page agent tab against a pre-0.83 agent (typed // 404 — the StatusError precedent) renders the Hungarian notice, ok:true, no error // spam. Companion red-proof: match the error by string instead of errors.As → // unsupported turns into an error payload → the notice assertion fails. func TestDebugAgentLogs_Pre083AgentRendersNotice(t *testing.T) { s := testServer(t) s.agentLogsFn = func(context.Context) (agentapi.AgentLogsResponse, error) { return agentapi.AgentLogsResponse{}, &agentapi.StatusError{Path: "/debug/logs", Code: http.StatusNotFound} } w := httptest.NewRecorder() s.debugAgentLogs(w, httptest.NewRequest("GET", "/api/debug/agent-logs", nil)) var resp struct { OK bool `json:"ok"` Data struct { Unsupported bool `json:"unsupported"` Notice string `json:"notice"` } `json:"data"` } if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("decode: %v (%s)", err, w.Body.String()) } if !resp.OK || !resp.Data.Unsupported { t.Fatalf("resp = %+v, want ok+unsupported (not an error)", resp) } if !strings.Contains(resp.Data.Notice, "az ügynök következő frissítése után") { t.Errorf("notice = %q, want the next-update text", resp.Data.Notice) } } // A working ≥0.83 agent's entries are proxied through verbatim. func TestDebugAgentLogs_ProxiesEntries(t *testing.T) { s := testServer(t) s.agentLogsFn = func(context.Context) (agentapi.AgentLogsResponse, error) { return agentapi.AgentLogsResponse{ Entries: []agentapi.AgentLogEntry{{Level: "DEBUG", Message: "netverify: job started"}}, Total: 1, }, nil } w := httptest.NewRecorder() s.debugAgentLogs(w, httptest.NewRequest("GET", "/api/debug/agent-logs", nil)) body := w.Body.String() if !strings.Contains(body, "netverify: job started") || !strings.Contains(body, `"total":1`) { t.Errorf("proxy body missing agent entries: %s", body) } } // A non-404 agent error (down, 5xx) is an honest error payload — never the notice. func TestDebugAgentLogs_OtherErrorIsError(t *testing.T) { s := testServer(t) s.agentLogsFn = func(context.Context) (agentapi.AgentLogsResponse, error) { return agentapi.AgentLogsResponse{}, &agentapi.StatusError{Path: "/debug/logs", Code: 502} } w := httptest.NewRecorder() s.debugAgentLogs(w, httptest.NewRequest("GET", "/api/debug/agent-logs", nil)) if strings.Contains(w.Body.String(), "unsupported") || !strings.Contains(w.Body.String(), `"ok":false`) { t.Errorf("non-404 must be an error, not the notice: %s", w.Body.String()) } } // S7 sweep smoke (controller half): a full fake NAS add at logging.level=info must // leave the EXPECTED ORDERED phase lines in the debug ring — the test that encodes // "an operator can reconstruct the NAS flow from the debug view". Companion // red-proof: drop one asserted phase line (e.g. the probe verdict Debug) → FAIL // naming the missing marker. func TestNetAdd_LogSequenceReconstructsFlow(t *testing.T) { s := testServer(t) lb := NewLogBuffer(200) // The production writer layout at logging.level=info: ring captures everything. s.logger = log.New(lb, "", log.LstdFlags) agent := &fakeNetAgent{addRes: okAddRes("media"), verify: agentapi.NetVerifyStatus{Phase: "done", JobID: "job-1"}} s.netProbeFn = func(context.Context, string) probeOutcome { return probeOutcome{OK: true} } if !s.startNetAdd(agent, netAddReq("media"), "NAS media") { t.Fatal("startNetAdd refused") } job := waitNetAdd(t, s) if job.Phase != netAddPhaseDone { t.Fatalf("phase = %s, want done", job.Phase) } lines := lb.Lines(0) sequence := []string{ `netstorage add "media" started`, "agent add accepted", "phase agent_add -> verifying", "agent verify verdict: phase=done", "phase verifying -> probing", "probe verdict: ok=true", "phase probing -> registering", "network storage added + verified", } pos := -1 for _, marker := range sequence { idx := -1 for i := pos + 1; i < len(lines); i++ { if strings.Contains(lines[i], marker) { idx = i break } } if idx < 0 { t.Fatalf("phase line %q missing (or out of order) — flow not reconstructable.\nring:\n%s", marker, strings.Join(lines, "\n")) } pos = idx } }