Files
mcr/internal/server/admin_test.go
Kyle Isom dddc66f31b Phases 5, 6, 8: OCI pull/push paths and admin REST API
Phase 5 (OCI pull): internal/oci/ package with manifest GET/HEAD by
tag/digest, blob GET/HEAD with repo membership check, tag listing with
OCI pagination, catalog listing. Multi-segment repo names via
parseOCIPath() right-split routing. DB query layer in
internal/db/repository.go.

Phase 6 (OCI push): blob uploads (monolithic and chunked) with
uploadManager tracking in-progress BlobWriters, manifest push
implementing full ARCHITECTURE.md §5 flow in a single SQLite
transaction (create repo, upsert manifest, populate manifest_blobs,
atomic tag move). Digest verification on both blob commit and manifest
push-by-digest.

Phase 8 (admin REST): /v1 endpoints for auth (login/logout/health),
repository management (list/detail/delete), policy CRUD with engine
reload, audit log listing with filters, GC trigger/status stubs.
RequireAdmin middleware, platform-standard error format.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 18:25:18 -07:00

149 lines
4.0 KiB
Go

package server
import (
"io"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
"github.com/go-chi/chi/v5"
"git.wntrmute.dev/kyle/mcr/internal/auth"
"git.wntrmute.dev/kyle/mcr/internal/db"
"git.wntrmute.dev/kyle/mcr/internal/policy"
)
func openAdminTestDB(t *testing.T) *db.DB {
t.Helper()
path := filepath.Join(t.TempDir(), "test.db")
d, err := db.Open(path)
if err != nil {
t.Fatalf("Open: %v", err)
}
t.Cleanup(func() { _ = d.Close() })
if err := d.Migrate(); err != nil {
t.Fatalf("Migrate: %v", err)
}
return d
}
type fakePolicyReloader struct {
reloadCount int
}
func (f *fakePolicyReloader) Reload(_ policy.RuleStore) error {
f.reloadCount++
return nil
}
// buildAdminRouter creates a chi router with admin routes wired up,
// using a fakeValidator that returns admin claims for any bearer token.
func buildAdminRouter(t *testing.T, database *db.DB) (chi.Router, *fakePolicyReloader) {
t.Helper()
validator := &fakeValidator{
claims: &auth.Claims{Subject: "admin-uuid", AccountType: "human", Roles: []string{"admin"}},
}
login := &fakeLoginClient{token: "test-token", expiresIn: 3600}
reloader := &fakePolicyReloader{}
gcState := &GCState{}
r := chi.NewRouter()
MountAdminRoutes(r, validator, "mcr-test", AdminDeps{
DB: database,
Login: login,
Engine: reloader,
AuditFn: nil,
GCState: gcState,
})
return r, reloader
}
// buildNonAdminRouter creates a chi router that returns non-admin claims.
func buildNonAdminRouter(t *testing.T, database *db.DB) chi.Router {
t.Helper()
validator := &fakeValidator{
claims: &auth.Claims{Subject: "user-uuid", AccountType: "human", Roles: []string{"user"}},
}
login := &fakeLoginClient{token: "test-token", expiresIn: 3600}
reloader := &fakePolicyReloader{}
gcState := &GCState{}
r := chi.NewRouter()
MountAdminRoutes(r, validator, "mcr-test", AdminDeps{
DB: database,
Login: login,
Engine: reloader,
AuditFn: nil,
GCState: gcState,
})
return r
}
// adminReq is a convenience helper for making HTTP requests against the admin
// router, automatically including the Authorization header.
func adminReq(t *testing.T, router http.Handler, method, path string, body string) *httptest.ResponseRecorder {
t.Helper()
var bodyReader io.Reader
if body != "" {
bodyReader = strings.NewReader(body)
}
req := httptest.NewRequest(method, path, bodyReader)
req.Header.Set("Authorization", "Bearer valid-token")
if body != "" {
req.Header.Set("Content-Type", "application/json")
}
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
return rr
}
func TestRequireAdminAllowed(t *testing.T) {
claims := &auth.Claims{Subject: "admin-uuid", AccountType: "human", Roles: []string{"admin"}}
handler := RequireAdmin()(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
req := httptest.NewRequest("GET", "/test", nil)
req = req.WithContext(auth.ContextWithClaims(req.Context(), claims))
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("admin allowed: got %d, want 200", rr.Code)
}
}
func TestRequireAdminDenied(t *testing.T) {
claims := &auth.Claims{Subject: "user-uuid", AccountType: "human", Roles: []string{"user"}}
handler := RequireAdmin()(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
t.Fatal("inner handler should not be called")
}))
req := httptest.NewRequest("GET", "/test", nil)
req = req.WithContext(auth.ContextWithClaims(req.Context(), claims))
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if rr.Code != http.StatusForbidden {
t.Fatalf("non-admin denied: got %d, want 403", rr.Code)
}
}
func TestRequireAdminNoClaims(t *testing.T) {
handler := RequireAdmin()(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
t.Fatal("inner handler should not be called")
}))
req := httptest.NewRequest("GET", "/test", nil)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if rr.Code != http.StatusUnauthorized {
t.Fatalf("no claims: got %d, want 401", rr.Code)
}
}