package localapi import ( "bytes" "encoding/json" "io" "log/slog" "strings" "testing" applog "gitea.dooplex.hu/admin/felhom-agent/internal/log" ) // newDebugLogServer builds a minimal server with the debug ring wired and one line // of each level captured (emit level info — the capture-at-info posture). func newDebugLogServer(t *testing.T) (*Server, *applog.Ring) { t.Helper() logger, ring := applog.NewWithWriter(&bytes.Buffer{}, "info", 50) logger.Debug("netverify: flow detail", "step", "probe") logger.Info("netmount: ensured network mount", "name", "nas") srv, err := NewServer(Options{ ListenAddr: "127.0.0.1:0", Guests: &fakeGuests{}, Backups: &fakeBackups{}, Store: &fakeStore{}, Storage: fakeStorage{}, Tokens: staticTokens{"A": 8200}, LogRing: ring, Logger: slog.New(slog.NewTextHandler(io.Discard, nil)), }) if err != nil { t.Fatalf("new server: %v", err) } return srv, ring } // GET /debug/logs serves the ring's entries — INCLUDING the DEBUG line captured at // emit level info (the endpoint exists so the controller's agent tab can show flow // detail without a config flip). Companion red-proof: unwire LogRing → 503. func TestDebugLogs_ServesRingEntriesIncludingDebug(t *testing.T) { srv, _ := newDebugLogServer(t) w := do(t, srv.Handler(), "GET", "/debug/logs", "A", "") if w.Code != 200 { t.Fatalf("status = %d body=%s", w.Code, w.Body.String()) } var resp struct { OK bool `json:"ok"` Data struct { Entries []applog.Entry `json:"entries"` Total int `json:"total"` } `json:"data"` } if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Fatalf("decode: %v", err) } if !resp.OK || resp.Data.Total != 2 || len(resp.Data.Entries) != 2 { t.Fatalf("resp = %+v, want ok with 2 entries", resp) } if resp.Data.Entries[0].Level != "DEBUG" || !strings.Contains(resp.Data.Entries[0].Message, "flow detail") { t.Errorf("entry 0 = %+v, want the captured DEBUG line", resp.Data.Entries[0]) } } // ?raw=1 serves plain text lines (the controller viewer's raw variant). func TestDebugLogs_RawVariant(t *testing.T) { srv, _ := newDebugLogServer(t) w := do(t, srv.Handler(), "GET", "/debug/logs?raw=1", "A", "") if w.Code != 200 || !strings.HasPrefix(w.Header().Get("Content-Type"), "text/plain") { t.Fatalf("status=%d content-type=%q", w.Code, w.Header().Get("Content-Type")) } body := w.Body.String() if !strings.Contains(body, "[DEBUG]") || !strings.Contains(body, "ensured network mount") { t.Errorf("raw body missing lines:\n%s", body) } } // The route is auth-gated like every sibling (no token → 401), and reports "not // configured" (503) when the ring is not wired — never a panic, never an empty 200. func TestDebugLogs_AuthAndUnconfigured(t *testing.T) { srv, _ := newDebugLogServer(t) if w := do(t, srv.Handler(), "GET", "/debug/logs", "", ""); w.Code != 401 { t.Errorf("unauthed status = %d, want 401", w.Code) } bare, err := NewServer(Options{ ListenAddr: "127.0.0.1:0", Guests: &fakeGuests{}, Backups: &fakeBackups{}, Store: &fakeStore{}, Storage: fakeStorage{}, Tokens: staticTokens{"A": 8200}, Logger: slog.New(slog.NewTextHandler(io.Discard, nil)), }) if err != nil { t.Fatalf("new server: %v", err) } if w := do(t, bare.Handler(), "GET", "/debug/logs", "A", ""); w.Code != 503 { t.Errorf("unconfigured status = %d, want 503", w.Code) } } // The request middleware logs method/path/status/duration at DEBUG into the wired // logger — proven through a ring-backed server logger (the line must land in the ring). func TestRequestLogging_DebugLineInRing(t *testing.T) { logger, ring := applog.NewWithWriter(&bytes.Buffer{}, "info", 50) srv, err := NewServer(Options{ ListenAddr: "127.0.0.1:0", Guests: &fakeGuests{}, Backups: &fakeBackups{}, Store: &fakeStore{}, Storage: fakeStorage{}, Tokens: staticTokens{"A": 8200}, LogRing: ring, Logger: logger, }) if err != nil { t.Fatalf("new server: %v", err) } do(t, srv.Handler(), "GET", "/storage", "A", "") entries, _ := ring.Entries(0) found := false for _, e := range entries { if e.Level == "DEBUG" && strings.Contains(e.Message, "local-api: request") && strings.Contains(e.Message, "path=/storage") { found = true } } if !found { t.Errorf("no request DEBUG line in the ring; entries=%+v", entries) } }