package handler import ( "context" "net/http" "net/http/httptest" "testing" "img_infinite_canvas/internal/domain/design" sharingmodule "img_infinite_canvas/internal/modules/sharing" ) type fakeBearerAuthenticator struct{} func (fakeBearerAuthenticator) UserIDFromBearer(_ context.Context, authorization string) (string, bool) { if authorization == "Bearer valid" { return "91cd197b-8255-4172-9981-77391fd38f33", true } return "", false } type fakeShareAuthorizer struct { access sharingmodule.Access err error } func (f fakeShareAuthorizer) ResolveAccess(_ context.Context, _ string, _ sharingmodule.Actor) (sharingmodule.Access, error) { return f.access, f.err } func TestUserContextMiddlewareUsesAuthorizationHeader(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/projects", nil) req.Header.Set("Authorization", "Bearer valid") rec := httptest.NewRecorder() called := false UserContextMiddleware(fakeBearerAuthenticator{})(func(w http.ResponseWriter, r *http.Request) { called = true if got := design.UserIDFromContext(r.Context()); got != "91cd197b-8255-4172-9981-77391fd38f33" { t.Fatalf("expected authenticated user in context, got %s", got) } w.WriteHeader(http.StatusNoContent) })(rec, req) if !called { t.Fatal("expected next handler to be called") } if rec.Code != http.StatusNoContent { t.Fatalf("expected status %d, got %d", http.StatusNoContent, rec.Code) } } func TestUserContextMiddlewareIgnoresLegacyTokenHeaderAndCookie(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/projects", nil) req.Header.Set("token", "valid") req.AddCookie(&http.Cookie{Name: "usertoken", Value: "valid"}) rec := httptest.NewRecorder() called := false UserContextMiddleware(fakeBearerAuthenticator{})(func(w http.ResponseWriter, r *http.Request) { called = true })(rec, req) if called { t.Fatal("expected next handler not to be called") } if rec.Code != http.StatusUnauthorized { t.Fatalf("expected status %d, got %d", http.StatusUnauthorized, rec.Code) } } func TestUserContextMiddlewareIgnoresSpoofedUserIDOnPublicRoute(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/dashboard", nil) req.Header.Set("X-User-Id", "91cd197b-8255-4172-9981-77391fd38f33") rec := httptest.NewRecorder() UserContextMiddleware(fakeBearerAuthenticator{})(func(w http.ResponseWriter, r *http.Request) { if got := design.UserIDFromContext(r.Context()); got != design.DefaultUserID { t.Fatalf("expected spoofed identity to be ignored, got %s", got) } w.WriteHeader(http.StatusNoContent) })(rec, req) if rec.Code != http.StatusNoContent { t.Fatalf("expected public route to continue, got %d", rec.Code) } } func TestUserContextMiddlewareAuthorizesAnonymousCanvasShare(t *testing.T) { ownerID := "d1d2fbbb-97bb-4406-8d0b-ec814cc65092" authorizer := fakeShareAuthorizer{access: sharingmodule.Access{ ProjectID: "project-1", OwnerID: ownerID, Permission: sharingmodule.PermissionCanvas, Capabilities: sharingmodule.PermissionCanvas.Capabilities(), }} req := httptest.NewRequest(http.MethodGet, "/api/projects/project-1", nil) req.Header.Set("X-Share-Id", "XhRewKnS4it7X9kEMFLcdVJentg") rec := httptest.NewRecorder() UserContextMiddleware(fakeBearerAuthenticator{}, authorizer)(func(w http.ResponseWriter, r *http.Request) { if got := design.UserIDFromContext(r.Context()); got != ownerID { t.Fatalf("expected owner-scoped repository context, got %s", got) } if _, ok := sharingmodule.AccessFromContext(r.Context()); !ok { t.Fatal("expected effective share access in context") } w.WriteHeader(http.StatusNoContent) })(rec, req) if rec.Code != http.StatusNoContent { t.Fatalf("expected shared read to continue, got %d", rec.Code) } } func TestUserContextMiddlewareDeniesCanvasShareChatHistory(t *testing.T) { authorizer := fakeShareAuthorizer{access: sharingmodule.Access{ ProjectID: "project-1", OwnerID: "d1d2fbbb-97bb-4406-8d0b-ec814cc65092", Permission: sharingmodule.PermissionCanvas, Capabilities: sharingmodule.PermissionCanvas.Capabilities(), }} req := httptest.NewRequest(http.MethodGet, "/api/projects/project-1/history", nil) req.Header.Set("X-Share-Id", "XhRewKnS4it7X9kEMFLcdVJentg") rec := httptest.NewRecorder() called := false UserContextMiddleware(fakeBearerAuthenticator{}, authorizer)(func(http.ResponseWriter, *http.Request) { called = true })(rec, req) if called { t.Fatal("expected canvas-only chat history to be blocked") } if rec.Code != http.StatusForbidden { t.Fatalf("expected forbidden, got %d", rec.Code) } } func TestUserContextMiddlewareDoesNotLetHeaderOverridePathProject(t *testing.T) { authorizer := fakeShareAuthorizer{access: sharingmodule.Access{ ProjectID: "shared-project", OwnerID: "d1d2fbbb-97bb-4406-8d0b-ec814cc65092", Permission: sharingmodule.PermissionEditor, Capabilities: sharingmodule.PermissionEditor.Capabilities(), }} req := httptest.NewRequest(http.MethodGet, "/api/projects/other-project", nil) req.Header.Set("X-Share-Id", "XhRewKnS4it7X9kEMFLcdVJentg") req.Header.Set("X-Share-Project-Id", "shared-project") rec := httptest.NewRecorder() called := false UserContextMiddleware(fakeBearerAuthenticator{}, authorizer)(func(http.ResponseWriter, *http.Request) { called = true })(rec, req) if called { t.Fatal("expected mismatched path project to be blocked") } if rec.Code != http.StatusNotFound { t.Fatalf("expected not found, got %d", rec.Code) } } func TestUserContextMiddlewareRequiresEditorForGeneratorTasks(t *testing.T) { authorizer := fakeShareAuthorizer{access: sharingmodule.Access{ ProjectID: "project-1", OwnerID: "d1d2fbbb-97bb-4406-8d0b-ec814cc65092", Permission: sharingmodule.PermissionViewer, Capabilities: sharingmodule.PermissionViewer.Capabilities(), }} req := httptest.NewRequest(http.MethodGet, "/api/generator/tasks?project_id=project-1&task_id=task-1", nil) req.Header.Set("X-Share-Id", "XhRewKnS4it7X9kEMFLcdVJentg") rec := httptest.NewRecorder() UserContextMiddleware(fakeBearerAuthenticator{}, authorizer)(func(http.ResponseWriter, *http.Request) { t.Fatal("expected viewer generator access to be blocked") })(rec, req) if rec.Code != http.StatusForbidden { t.Fatalf("expected forbidden, got %d", rec.Code) } } func TestUserContextMiddlewareProtectsV1Routes(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/v1/generator/tasks", nil) rec := httptest.NewRecorder() called := false UserContextMiddleware(fakeBearerAuthenticator{})(func(http.ResponseWriter, *http.Request) { called = true })(rec, req) if called { t.Fatal("expected v1 route to require authentication") } if rec.Code != http.StatusUnauthorized { t.Fatalf("expected unauthorized, got %d", rec.Code) } }