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