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>
This commit is contained in:
148
internal/server/admin_test.go
Normal file
148
internal/server/admin_test.go
Normal file
@@ -0,0 +1,148 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user