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