package auth import ( "context" "errors" "net/http" "net/http/httptest" "testing" "github.com/google/uuid" ) // fakeAdminLookup implements AdminLookup for unit tests. type fakeAdminLookup struct { admin bool err error } func (f fakeAdminLookup) IsAdmin(ctx context.Context, id uuid.UUID) (bool, error) { return f.admin, f.err } // withUID returns a request that already has the user-id context value set, // matching what Client.WithUserID would have populated. func withUID(req *http.Request, id uuid.UUID) *http.Request { ctx := context.WithValue(req.Context(), userIDContextKey, id) return req.WithContext(ctx) } func TestRequireAdmin_AllowsAdmin(t *testing.T) { called := false next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { called = true w.WriteHeader(http.StatusOK) }) h := RequireAdmin(fakeAdminLookup{admin: true})(next) req := withUID(httptest.NewRequest("GET", "/api/admin/users", nil), uuid.New()) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if !called { t.Fatal("admin user should reach the wrapped handler") } if rec.Code != http.StatusOK { t.Errorf("status: got %d, want 200", rec.Code) } } func TestRequireAdmin_RejectsNonAdminAPI(t *testing.T) { next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Fatal("non-admin must not reach the wrapped handler") }) h := RequireAdmin(fakeAdminLookup{admin: false})(next) req := withUID(httptest.NewRequest("GET", "/api/admin/users", nil), uuid.New()) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusForbidden { t.Errorf("API path should 403 for non-admin, got %d", rec.Code) } } func TestRequireAdmin_RedirectsNonAdminBrowser(t *testing.T) { next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Fatal("non-admin must not reach the wrapped handler") }) h := RequireAdmin(fakeAdminLookup{admin: false})(next) req := withUID(httptest.NewRequest("GET", "/admin/team", nil), uuid.New()) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusFound { t.Errorf("browser path should 302 for non-admin, got %d", rec.Code) } if got := rec.Header().Get("Location"); got != "/dashboard?forbidden=admin" { t.Errorf("redirect target: got %q", got) } } func TestRequireAdmin_RejectsUnauthenticated(t *testing.T) { next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Fatal("unauthenticated must not reach the wrapped handler") }) h := RequireAdmin(fakeAdminLookup{admin: true})(next) // No userIDContextKey on the request — simulates a path that didn't // authenticate first. The middleware must fail closed even when the // lookup would have approved. req := httptest.NewRequest("GET", "/api/admin/users", nil) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusUnauthorized { t.Errorf("missing user id should 401, got %d", rec.Code) } } func TestRequireAdmin_FailsClosedOnLookupError(t *testing.T) { next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Fatal("lookup-error path must not reach the wrapped handler") }) h := RequireAdmin(fakeAdminLookup{err: errors.New("db gone")})(next) req := withUID(httptest.NewRequest("GET", "/api/admin/users", nil), uuid.New()) rec := httptest.NewRecorder() h.ServeHTTP(rec, req) if rec.Code != http.StatusInternalServerError { t.Errorf("lookup error should 500 (fail closed), got %d", rec.Code) } }