package project_test
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/go-chi/chi/v5"
"sourcecraft.dev/bigbes/lethe/internal/domain/project"
"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))
})
}
}
// newHandler wires a Handler against a fresh in-memory database.
func newHandler(t *testing.T) (*project.Handler, *project.Repository) {
t.Helper()
repo, _ := newRepo(t)
h := &project.Handler{Repo: repo}
if err := h.Init(t.Context()); err != nil {
t.Fatalf("handler.Init: %v", err)
}
return h, repo
}
// mountWithIdentity builds a chi router with the fake auth middleware and
// the project handler mounted under /api/v1.
func mountWithIdentity(h *project.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
}
// projectListBody is the decoded response of GET /api/v1/projects.
type projectListBody struct {
Projects []project.Project `json:"projects"`
Limit int `json:"limit"`
Offset int `json:"offset"`
}
// problemBody captures the RFC 7807 fields tests assert on.
type problemBody struct {
Status int `json:"status"`
Code string `json:"code"`
}
func doList(t *testing.T, router http.Handler, query string) (*httptest.ResponseRecorder, projectListBody) {
t.Helper()
req := httptest.NewRequest(http.MethodGet, "/api/v1/projects"+query, nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
var body projectListBody
if rec.Code == http.StatusOK {
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
t.Fatalf("unmarshal list body: %v (body=%s)", err, rec.Body.String())
}
}
return rec, body
}
func TestProjectHandler_List_OneProject(t *testing.T) {
h, repo := newHandler(t)
db := repo.Database.DB
seedSessionWithCwd(t, db, "alice", "cc", "phoebe", "s1", 1700000000, 1700000010, "/code/x")
router := mountWithIdentity(h, auth.Identity{User: "alice"})
rec, body := doList(t, router, "")
if rec.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
}
if len(body.Projects) != 1 {
t.Fatalf("expected 1 project; got %d (%#v)", len(body.Projects), body.Projects)
}
if body.Projects[0].Cwd != "/code/x" {
t.Errorf("Cwd: got %q; want /code/x", body.Projects[0].Cwd)
}
if body.Limit != 50 || body.Offset != 0 {
t.Errorf("defaults: limit=%d offset=%d; want 50/0", body.Limit, body.Offset)
}
}
func TestProjectHandler_List_NonAdminOwnerParamReturns403(t *testing.T) {
h, _ := newHandler(t)
router := mountWithIdentity(h, auth.Identity{User: "alice", IsAdmin: false})
for _, q := range []string{"?owner=alice", "?owner=bob", "?owner=*"} {
rec, _ := doList(t, router, q)
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 TestProjectHandler_List_BadSinceReturns400(t *testing.T) {
h, _ := newHandler(t)
router := mountWithIdentity(h, auth.Identity{User: "alice"})
rec, _ := doList(t, router, "?since=not-a-number")
if rec.Code != http.StatusBadRequest {
t.Fatalf("status=%d; want 400; body=%s", rec.Code, rec.Body.String())
}
var p problemBody
_ = json.Unmarshal(rec.Body.Bytes(), &p)
if p.Code != "INVALID" {
t.Fatalf("expected INVALID; got %q", p.Code)
}
}
func TestProjectHandler_List_AdminOwnerStarReturnsAllOwners(t *testing.T) {
h, repo := newHandler(t)
db := repo.Database.DB
seedSessionWithCwd(t, db, "alice", "cc", "phoebe", "sA", 1700000000, 1700000010, "/code/x")
seedSessionWithCwd(t, db, "bob", "cc", "phoebe", "sB", 1700000020, 1700000030, "/code/y")
router := mountWithIdentity(h, auth.Identity{User: "admin", IsAdmin: true})
rec, body := doList(t, router, "?owner=*")
if rec.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
}
if len(body.Projects) != 2 {
t.Fatalf("expected 2 projects; got %d (%#v)", len(body.Projects), body.Projects)
}
}
func TestProjectHandler_Mount_RegistersRoute(t *testing.T) {
h, _ := newHandler(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/projects" {
found = true
}
return nil
})
if !found {
t.Error("expected GET /api/v1/projects registered; not found")
}
}