package stats_test import ( "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/go-chi/chi/v5" "sourcecraft.dev/bigbes/lethe/internal/domain/stats" "sourcecraft.dev/bigbes/lethe/internal/server/auth" ) // fakeAuthMiddleware injects a fixed Identity onto the request context. func fakeAuthMiddleware(id auth.Identity) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := auth.WithIdentity(r.Context(), id) next.ServeHTTP(w, r.WithContext(ctx)) }) } } // newStatsHandler wires a Handler against a fresh in-memory database. func newStatsHandler(t *testing.T) *stats.Handler { t.Helper() repo, _ := newStatsRepo(t) h := &stats.Handler{Repo: repo} if err := h.Init(t.Context()); err != nil { t.Fatalf("stats.Handler.Init: %v", err) } return h } // mountWithIdentity builds a chi router with the fake auth middleware and the // stats handler mounted under /api/v1. func mountWithIdentity(h *stats.Handler, id auth.Identity) http.Handler { r := chi.NewRouter() r.Route("/api/v1", func(r chi.Router) { r.Use(fakeAuthMiddleware(id)) h.Mount(r) }) return r } // problemBody captures the RFC 7807 fields tests assert on. type problemBody struct { Status int `json:"status"` Code string `json:"code"` } func doStats(t *testing.T, router http.Handler, query string) (*httptest.ResponseRecorder, *stats.Stats) { t.Helper() req := httptest.NewRequest(http.MethodGet, "/api/v1/stats"+query, nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code == http.StatusOK { var body stats.Stats if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { t.Fatalf("unmarshal stats body: %v (body=%s)", err, rec.Body.String()) } return rec, &body } return rec, nil } func TestStatsHandler_Range7d_Returns200(t *testing.T) { h := newStatsHandler(t) router := mountWithIdentity(h, auth.Identity{User: "alice"}) rec, body := doStats(t, router, "?range=7d") if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } if body == nil { t.Fatal("body is nil") } // Daily window for 7d = 8 slots. if len(body.Daily) != 8 { t.Errorf("Daily len=%d; want 8", len(body.Daily)) } if len(body.Heatmap) != 84 { t.Errorf("Heatmap len=%d; want 84", len(body.Heatmap)) } if len(body.HourOfDay) != 24 { t.Errorf("HourOfDay len=%d; want 24", len(body.HourOfDay)) } } func TestStatsHandler_Range30d_Returns200(t *testing.T) { h := newStatsHandler(t) router := mountWithIdentity(h, auth.Identity{User: "alice"}) rec, body := doStats(t, router, "?range=30d") if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } // Daily window for 30d = 31 slots. if len(body.Daily) != 31 { t.Errorf("Daily len=%d; want 31", len(body.Daily)) } } func TestStatsHandler_Range90d_Returns200(t *testing.T) { h := newStatsHandler(t) router := mountWithIdentity(h, auth.Identity{User: "alice"}) rec, body := doStats(t, router, "?range=90d") if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } // Daily window for 90d = 91 slots. if len(body.Daily) != 91 { t.Errorf("Daily len=%d; want 91", len(body.Daily)) } } func TestStatsHandler_RangeAll_Returns200(t *testing.T) { h := newStatsHandler(t) router := mountWithIdentity(h, auth.Identity{User: "alice"}) rec, _ := doStats(t, router, "?range=all") if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } } func TestStatsHandler_MissingRange_Defaults30d(t *testing.T) { h := newStatsHandler(t) router := mountWithIdentity(h, auth.Identity{User: "alice"}) rec, body := doStats(t, router, "") if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } // Default 30d window = 31 slots. if len(body.Daily) != 31 { t.Errorf("default range Daily len=%d; want 31", len(body.Daily)) } } func TestStatsHandler_BadRange_Returns400(t *testing.T) { h := newStatsHandler(t) router := mountWithIdentity(h, auth.Identity{User: "alice"}) for _, bad := range []string{"?range=foo", "?range=7", "?range=7days", "?range=0d"} { rec, _ := doStats(t, router, bad) if rec.Code != http.StatusBadRequest { t.Fatalf("range %q: status=%d; want 400; body=%s", bad, rec.Code, rec.Body.String()) } var p problemBody _ = json.Unmarshal(rec.Body.Bytes(), &p) if p.Code != "INVALID" { t.Fatalf("range %q: code=%q; want INVALID", bad, p.Code) } } } func TestStatsHandler_NonAdminOwnerParam_Returns403(t *testing.T) { h := newStatsHandler(t) router := mountWithIdentity(h, auth.Identity{User: "alice", IsAdmin: false}) for _, q := range []string{"?owner=alice", "?owner=bob", "?owner=*"} { req := httptest.NewRequest(http.MethodGet, "/api/v1/stats"+q, nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusForbidden { t.Fatalf("query %q: status=%d; want 403; body=%s", q, rec.Code, rec.Body.String()) } var p problemBody _ = json.Unmarshal(rec.Body.Bytes(), &p) if p.Code != "FORBIDDEN" { t.Fatalf("query %q: code=%q; want FORBIDDEN", q, p.Code) } } } func TestStatsHandler_Mount_RegistersRoute(t *testing.T) { h := newStatsHandler(t) router := chi.NewRouter() router.Route("/api/v1", func(r chi.Router) { r.Use(fakeAuthMiddleware(auth.Identity{User: "alice"})) h.Mount(r) }) found := false _ = chi.Walk(router, func(method, route string, _ http.Handler, _ ...func(http.Handler) http.Handler) error { if method == http.MethodGet && route == "/api/v1/stats" { found = true } return nil }) if !found { t.Error("expected GET /api/v1/stats registered; not found") } }