# tests/api/routes/syndarix/test_projects.py """ Comprehensive tests for the Projects API endpoints. Tests cover: - CRUD operations (create, read, update, archive) - Lifecycle operations (pause, resume) - Authorization (ownership checks, superuser access) - Pagination and filtering - Error handling (not found, validation, duplicates) """ import uuid import pytest from fastapi import status @pytest.mark.asyncio class TestCreateProject: """Tests for POST /api/v1/projects endpoint.""" async def test_create_project_success(self, client, user_token): """Test successful project creation.""" response = await client.post( "/api/v1/projects", json={ "name": "Test Project", "slug": "test-project", "description": "A test project description", "autonomy_level": "milestone", }, headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_201_CREATED data = response.json() assert data["name"] == "Test Project" assert data["slug"] == "test-project" assert data["description"] == "A test project description" assert data["autonomy_level"] == "milestone" assert data["status"] == "active" assert data["agent_count"] == 0 assert data["issue_count"] == 0 assert data["active_sprint_name"] is None assert "id" in data assert "created_at" in data assert "updated_at" in data async def test_create_project_minimal_fields(self, client, user_token): """Test creating project with only required fields.""" response = await client.post( "/api/v1/projects", json={ "name": "Minimal Project", "slug": "minimal-project", }, headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_201_CREATED data = response.json() assert data["name"] == "Minimal Project" assert data["slug"] == "minimal-project" assert data["autonomy_level"] == "milestone" # Default assert data["status"] == "active" # Default async def test_create_project_with_settings(self, client, user_token): """Test creating project with custom settings.""" response = await client.post( "/api/v1/projects", json={ "name": "Project With Settings", "slug": "project-with-settings", "settings": {"theme": "dark", "notifications": True}, }, headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_201_CREATED data = response.json() assert data["settings"]["theme"] == "dark" assert data["settings"]["notifications"] is True async def test_create_project_duplicate_slug(self, client, user_token): """Test that duplicate slugs are rejected.""" # Create first project await client.post( "/api/v1/projects", json={"name": "First Project", "slug": "duplicate-slug"}, headers={"Authorization": f"Bearer {user_token}"}, ) # Attempt duplicate response = await client.post( "/api/v1/projects", json={"name": "Second Project", "slug": "duplicate-slug"}, headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_409_CONFLICT data = response.json() assert "already exists" in data["errors"][0]["message"].lower() async def test_create_project_invalid_slug_format(self, client, user_token): """Test that invalid slug formats are rejected.""" # Uppercase letters response = await client.post( "/api/v1/projects", json={"name": "Bad Slug", "slug": "Invalid-Slug"}, headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY # Starting with hyphen response = await client.post( "/api/v1/projects", json={"name": "Bad Slug", "slug": "-invalid"}, headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY # Consecutive hyphens response = await client.post( "/api/v1/projects", json={"name": "Bad Slug", "slug": "invalid--slug"}, headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY async def test_create_project_empty_name(self, client, user_token): """Test that empty name is rejected.""" response = await client.post( "/api/v1/projects", json={"name": "", "slug": "empty-name"}, headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY async def test_create_project_unauthenticated(self, client): """Test that unauthenticated requests are rejected.""" response = await client.post( "/api/v1/projects", json={"name": "Test Project", "slug": "test-project"}, ) assert response.status_code == status.HTTP_401_UNAUTHORIZED async def test_create_project_all_autonomy_levels(self, client, user_token): """Test creating projects with all autonomy levels.""" levels = ["full_control", "milestone", "autonomous"] for level in levels: # Slugs must use hyphens, not underscores slug = f"project-{level.replace('_', '-')}" response = await client.post( "/api/v1/projects", json={ "name": f"Project {level}", "slug": slug, "autonomy_level": level, }, headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_201_CREATED assert response.json()["autonomy_level"] == level @pytest.mark.asyncio class TestListProjects: """Tests for GET /api/v1/projects endpoint.""" async def test_list_projects_empty(self, client, user_token): """Test listing projects when none exist.""" response = await client.get( "/api/v1/projects", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK data = response.json() assert "data" in data assert "pagination" in data assert data["data"] == [] assert data["pagination"]["total"] == 0 async def test_list_projects_with_data(self, client, user_token): """Test listing projects returns created projects.""" # Create projects for i in range(3): await client.post( "/api/v1/projects", json={"name": f"Project {i}", "slug": f"project-{i}"}, headers={"Authorization": f"Bearer {user_token}"}, ) response = await client.get( "/api/v1/projects", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK data = response.json() assert len(data["data"]) == 3 assert data["pagination"]["total"] == 3 async def test_list_projects_pagination(self, client, user_token): """Test pagination works correctly.""" # Create 5 projects for i in range(5): await client.post( "/api/v1/projects", json={"name": f"Project {i}", "slug": f"project-{i}"}, headers={"Authorization": f"Bearer {user_token}"}, ) # Get first page (2 items) response = await client.get( "/api/v1/projects?page=1&limit=2", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK data = response.json() assert len(data["data"]) == 2 assert data["pagination"]["total"] == 5 assert data["pagination"]["page"] == 1 assert data["pagination"]["total_pages"] == 3 # Get second page response = await client.get( "/api/v1/projects?page=2&limit=2", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK data = response.json() assert len(data["data"]) == 2 assert data["pagination"]["page"] == 2 async def test_list_projects_filter_by_status(self, client, user_token): """Test filtering projects by status.""" # Create active project await client.post( "/api/v1/projects", json={"name": "Active Project", "slug": "active-project", "status": "active"}, headers={"Authorization": f"Bearer {user_token}"}, ) # Create paused project await client.post( "/api/v1/projects", json={"name": "Paused Project", "slug": "paused-project", "status": "paused"}, headers={"Authorization": f"Bearer {user_token}"}, ) # Filter by active response = await client.get( "/api/v1/projects?status=active", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK data = response.json() assert len(data["data"]) == 1 assert data["data"][0]["status"] == "active" async def test_list_projects_search(self, client, user_token): """Test searching projects by name.""" await client.post( "/api/v1/projects", json={"name": "Alpha Project", "slug": "alpha-project"}, headers={"Authorization": f"Bearer {user_token}"}, ) await client.post( "/api/v1/projects", json={"name": "Beta Project", "slug": "beta-project"}, headers={"Authorization": f"Bearer {user_token}"}, ) response = await client.get( "/api/v1/projects?search=alpha", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK data = response.json() assert len(data["data"]) == 1 assert "alpha" in data["data"][0]["name"].lower() async def test_list_projects_user_isolation( self, client, user_token, superuser_token, async_test_user ): """Test that users only see their own projects.""" # Create project as regular user await client.post( "/api/v1/projects", json={"name": "User Project", "slug": "user-project"}, headers={"Authorization": f"Bearer {user_token}"}, ) # Create project as superuser await client.post( "/api/v1/projects", json={"name": "Admin Project", "slug": "admin-project"}, headers={"Authorization": f"Bearer {superuser_token}"}, ) # Regular user should only see their project response = await client.get( "/api/v1/projects", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK data = response.json() assert len(data["data"]) == 1 assert data["data"][0]["slug"] == "user-project" async def test_list_projects_superuser_all_projects( self, client, user_token, superuser_token ): """Test that superuser can see all projects with all_projects flag.""" # Create project as regular user await client.post( "/api/v1/projects", json={"name": "User Project", "slug": "user-project"}, headers={"Authorization": f"Bearer {user_token}"}, ) # Create project as superuser await client.post( "/api/v1/projects", json={"name": "Admin Project", "slug": "admin-project"}, headers={"Authorization": f"Bearer {superuser_token}"}, ) # Superuser with all_projects=true should see both response = await client.get( "/api/v1/projects?all_projects=true", headers={"Authorization": f"Bearer {superuser_token}"}, ) assert response.status_code == status.HTTP_200_OK data = response.json() assert len(data["data"]) == 2 @pytest.mark.asyncio class TestGetProject: """Tests for GET /api/v1/projects/{project_id} endpoint.""" async def test_get_project_success(self, client, user_token): """Test getting a project by ID.""" # Create project create_response = await client.post( "/api/v1/projects", json={"name": "Get Test Project", "slug": "get-test-project"}, headers={"Authorization": f"Bearer {user_token}"}, ) project_id = create_response.json()["id"] # Get project response = await client.get( f"/api/v1/projects/{project_id}", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK data = response.json() assert data["id"] == project_id assert data["name"] == "Get Test Project" async def test_get_project_not_found(self, client, user_token): """Test getting a non-existent project.""" fake_id = str(uuid.uuid4()) response = await client.get( f"/api/v1/projects/{fake_id}", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_404_NOT_FOUND assert "not found" in response.json()["errors"][0]["message"].lower() async def test_get_project_unauthorized(self, client, user_token, superuser_token): """Test that users cannot access other users' projects.""" # Create project as superuser create_response = await client.post( "/api/v1/projects", json={"name": "Admin Project", "slug": "admin-project"}, headers={"Authorization": f"Bearer {superuser_token}"}, ) project_id = create_response.json()["id"] # Try to access as regular user response = await client.get( f"/api/v1/projects/{project_id}", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_403_FORBIDDEN async def test_get_project_superuser_can_access_any( self, client, user_token, superuser_token ): """Test that superuser can access any project.""" # Create project as regular user create_response = await client.post( "/api/v1/projects", json={"name": "User Project", "slug": "user-project"}, headers={"Authorization": f"Bearer {user_token}"}, ) project_id = create_response.json()["id"] # Access as superuser response = await client.get( f"/api/v1/projects/{project_id}", headers={"Authorization": f"Bearer {superuser_token}"}, ) assert response.status_code == status.HTTP_200_OK @pytest.mark.asyncio class TestGetProjectBySlug: """Tests for GET /api/v1/projects/slug/{slug} endpoint.""" async def test_get_project_by_slug_success(self, client, user_token): """Test getting a project by slug.""" # Create project await client.post( "/api/v1/projects", json={"name": "Slug Test Project", "slug": "slug-test-project"}, headers={"Authorization": f"Bearer {user_token}"}, ) # Get by slug response = await client.get( "/api/v1/projects/slug/slug-test-project", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK data = response.json() assert data["slug"] == "slug-test-project" assert data["name"] == "Slug Test Project" async def test_get_project_by_slug_not_found(self, client, user_token): """Test getting a non-existent project by slug.""" response = await client.get( "/api/v1/projects/slug/non-existent-slug", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_404_NOT_FOUND async def test_get_project_by_slug_unauthorized( self, client, user_token, superuser_token ): """Test that users cannot access other users' projects by slug.""" # Create project as superuser await client.post( "/api/v1/projects", json={"name": "Admin Project", "slug": "admin-project"}, headers={"Authorization": f"Bearer {superuser_token}"}, ) # Try to access as regular user response = await client.get( "/api/v1/projects/slug/admin-project", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_403_FORBIDDEN @pytest.mark.asyncio class TestUpdateProject: """Tests for PATCH /api/v1/projects/{project_id} endpoint.""" async def test_update_project_success(self, client, user_token): """Test updating a project.""" # Create project create_response = await client.post( "/api/v1/projects", json={"name": "Original Name", "slug": "original-slug"}, headers={"Authorization": f"Bearer {user_token}"}, ) project_id = create_response.json()["id"] # Update project response = await client.patch( f"/api/v1/projects/{project_id}", json={"name": "Updated Name", "description": "New description"}, headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK data = response.json() assert data["name"] == "Updated Name" assert data["description"] == "New description" assert data["slug"] == "original-slug" # Unchanged async def test_update_project_slug(self, client, user_token): """Test updating project slug.""" # Create project create_response = await client.post( "/api/v1/projects", json={"name": "Test Project", "slug": "old-slug"}, headers={"Authorization": f"Bearer {user_token}"}, ) project_id = create_response.json()["id"] # Update slug response = await client.patch( f"/api/v1/projects/{project_id}", json={"slug": "new-slug"}, headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK assert response.json()["slug"] == "new-slug" async def test_update_project_autonomy_level(self, client, user_token): """Test updating project autonomy level.""" # Create project with milestone autonomy create_response = await client.post( "/api/v1/projects", json={ "name": "Test Project", "slug": "test-project", "autonomy_level": "milestone", }, headers={"Authorization": f"Bearer {user_token}"}, ) project_id = create_response.json()["id"] # Update to autonomous response = await client.patch( f"/api/v1/projects/{project_id}", json={"autonomy_level": "autonomous"}, headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK assert response.json()["autonomy_level"] == "autonomous" async def test_update_project_not_found(self, client, user_token): """Test updating a non-existent project.""" fake_id = str(uuid.uuid4()) response = await client.patch( f"/api/v1/projects/{fake_id}", json={"name": "New Name"}, headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_404_NOT_FOUND async def test_update_project_unauthorized( self, client, user_token, superuser_token ): """Test that users cannot update other users' projects.""" # Create project as superuser create_response = await client.post( "/api/v1/projects", json={"name": "Admin Project", "slug": "admin-project"}, headers={"Authorization": f"Bearer {superuser_token}"}, ) project_id = create_response.json()["id"] # Try to update as regular user response = await client.patch( f"/api/v1/projects/{project_id}", json={"name": "Hacked Name"}, headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_403_FORBIDDEN async def test_update_project_duplicate_slug(self, client, user_token): """Test that updating to a duplicate slug is rejected.""" # Create two projects await client.post( "/api/v1/projects", json={"name": "First Project", "slug": "first-project"}, headers={"Authorization": f"Bearer {user_token}"}, ) create_response = await client.post( "/api/v1/projects", json={"name": "Second Project", "slug": "second-project"}, headers={"Authorization": f"Bearer {user_token}"}, ) project_id = create_response.json()["id"] # Try to update second project's slug to first's response = await client.patch( f"/api/v1/projects/{project_id}", json={"slug": "first-project"}, headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_409_CONFLICT @pytest.mark.asyncio class TestArchiveProject: """Tests for DELETE /api/v1/projects/{project_id} endpoint.""" async def test_archive_project_success(self, client, user_token): """Test archiving a project.""" # Create project create_response = await client.post( "/api/v1/projects", json={"name": "Archive Test", "slug": "archive-test"}, headers={"Authorization": f"Bearer {user_token}"}, ) project_id = create_response.json()["id"] # Archive project response = await client.delete( f"/api/v1/projects/{project_id}", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK data = response.json() assert data["success"] is True assert "archived" in data["message"].lower() # Verify project is archived get_response = await client.get( f"/api/v1/projects/{project_id}", headers={"Authorization": f"Bearer {user_token}"}, ) assert get_response.json()["status"] == "archived" async def test_archive_project_already_archived(self, client, user_token): """Test archiving an already archived project.""" # Create and archive project create_response = await client.post( "/api/v1/projects", json={"name": "Archive Test", "slug": "archive-test"}, headers={"Authorization": f"Bearer {user_token}"}, ) project_id = create_response.json()["id"] await client.delete( f"/api/v1/projects/{project_id}", headers={"Authorization": f"Bearer {user_token}"}, ) # Try to archive again response = await client.delete( f"/api/v1/projects/{project_id}", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK assert "already archived" in response.json()["message"].lower() async def test_archive_project_not_found(self, client, user_token): """Test archiving a non-existent project.""" fake_id = str(uuid.uuid4()) response = await client.delete( f"/api/v1/projects/{fake_id}", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_404_NOT_FOUND async def test_archive_project_unauthorized( self, client, user_token, superuser_token ): """Test that users cannot archive other users' projects.""" # Create project as superuser create_response = await client.post( "/api/v1/projects", json={"name": "Admin Project", "slug": "admin-project"}, headers={"Authorization": f"Bearer {superuser_token}"}, ) project_id = create_response.json()["id"] # Try to archive as regular user response = await client.delete( f"/api/v1/projects/{project_id}", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_403_FORBIDDEN @pytest.mark.asyncio class TestPauseProject: """Tests for POST /api/v1/projects/{project_id}/pause endpoint.""" async def test_pause_project_success(self, client, user_token): """Test pausing an active project.""" # Create project create_response = await client.post( "/api/v1/projects", json={"name": "Pause Test", "slug": "pause-test"}, headers={"Authorization": f"Bearer {user_token}"}, ) project_id = create_response.json()["id"] # Pause project response = await client.post( f"/api/v1/projects/{project_id}/pause", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK assert response.json()["status"] == "paused" async def test_pause_already_paused_project(self, client, user_token): """Test that pausing an already paused project returns validation error.""" # Create and pause project create_response = await client.post( "/api/v1/projects", json={"name": "Pause Test", "slug": "pause-test"}, headers={"Authorization": f"Bearer {user_token}"}, ) project_id = create_response.json()["id"] await client.post( f"/api/v1/projects/{project_id}/pause", headers={"Authorization": f"Bearer {user_token}"}, ) # Try to pause again response = await client.post( f"/api/v1/projects/{project_id}/pause", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY assert "already paused" in response.json()["errors"][0]["message"].lower() async def test_pause_archived_project(self, client, user_token): """Test that pausing an archived project returns validation error.""" # Create and archive project create_response = await client.post( "/api/v1/projects", json={"name": "Archive Test", "slug": "archive-test"}, headers={"Authorization": f"Bearer {user_token}"}, ) project_id = create_response.json()["id"] await client.delete( f"/api/v1/projects/{project_id}", headers={"Authorization": f"Bearer {user_token}"}, ) # Try to pause response = await client.post( f"/api/v1/projects/{project_id}/pause", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY assert "archived" in response.json()["errors"][0]["message"].lower() async def test_pause_completed_project(self, client, user_token): """Test that pausing a completed project returns validation error.""" # Create project with completed status create_response = await client.post( "/api/v1/projects", json={ "name": "Completed Test", "slug": "completed-test", "status": "completed", }, headers={"Authorization": f"Bearer {user_token}"}, ) project_id = create_response.json()["id"] # Try to pause response = await client.post( f"/api/v1/projects/{project_id}/pause", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY assert "completed" in response.json()["errors"][0]["message"].lower() async def test_pause_project_not_found(self, client, user_token): """Test pausing a non-existent project.""" fake_id = str(uuid.uuid4()) response = await client.post( f"/api/v1/projects/{fake_id}/pause", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_404_NOT_FOUND async def test_pause_project_unauthorized( self, client, user_token, superuser_token ): """Test that users cannot pause other users' projects.""" # Create project as superuser create_response = await client.post( "/api/v1/projects", json={"name": "Admin Project", "slug": "admin-project"}, headers={"Authorization": f"Bearer {superuser_token}"}, ) project_id = create_response.json()["id"] # Try to pause as regular user response = await client.post( f"/api/v1/projects/{project_id}/pause", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_403_FORBIDDEN @pytest.mark.asyncio class TestResumeProject: """Tests for POST /api/v1/projects/{project_id}/resume endpoint.""" async def test_resume_project_success(self, client, user_token): """Test resuming a paused project.""" # Create and pause project create_response = await client.post( "/api/v1/projects", json={"name": "Resume Test", "slug": "resume-test"}, headers={"Authorization": f"Bearer {user_token}"}, ) project_id = create_response.json()["id"] await client.post( f"/api/v1/projects/{project_id}/pause", headers={"Authorization": f"Bearer {user_token}"}, ) # Resume project response = await client.post( f"/api/v1/projects/{project_id}/resume", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK assert response.json()["status"] == "active" async def test_resume_already_active_project(self, client, user_token): """Test that resuming an active project returns validation error.""" # Create project (active by default) create_response = await client.post( "/api/v1/projects", json={"name": "Active Test", "slug": "active-test"}, headers={"Authorization": f"Bearer {user_token}"}, ) project_id = create_response.json()["id"] # Try to resume response = await client.post( f"/api/v1/projects/{project_id}/resume", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY assert "already active" in response.json()["errors"][0]["message"].lower() async def test_resume_archived_project(self, client, user_token): """Test that resuming an archived project returns validation error.""" # Create and archive project create_response = await client.post( "/api/v1/projects", json={"name": "Archive Test", "slug": "archive-test"}, headers={"Authorization": f"Bearer {user_token}"}, ) project_id = create_response.json()["id"] await client.delete( f"/api/v1/projects/{project_id}", headers={"Authorization": f"Bearer {user_token}"}, ) # Try to resume response = await client.post( f"/api/v1/projects/{project_id}/resume", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY assert "archived" in response.json()["errors"][0]["message"].lower() async def test_resume_completed_project(self, client, user_token): """Test that resuming a completed project returns validation error.""" # Create project with completed status create_response = await client.post( "/api/v1/projects", json={ "name": "Completed Test", "slug": "completed-test", "status": "completed", }, headers={"Authorization": f"Bearer {user_token}"}, ) project_id = create_response.json()["id"] # Try to resume response = await client.post( f"/api/v1/projects/{project_id}/resume", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY assert "completed" in response.json()["errors"][0]["message"].lower() async def test_resume_project_not_found(self, client, user_token): """Test resuming a non-existent project.""" fake_id = str(uuid.uuid4()) response = await client.post( f"/api/v1/projects/{fake_id}/resume", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_404_NOT_FOUND async def test_resume_project_unauthorized( self, client, user_token, superuser_token ): """Test that users cannot resume other users' projects.""" # Create and pause project as superuser create_response = await client.post( "/api/v1/projects", json={"name": "Admin Project", "slug": "admin-project"}, headers={"Authorization": f"Bearer {superuser_token}"}, ) project_id = create_response.json()["id"] await client.post( f"/api/v1/projects/{project_id}/pause", headers={"Authorization": f"Bearer {superuser_token}"}, ) # Try to resume as regular user response = await client.post( f"/api/v1/projects/{project_id}/resume", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_403_FORBIDDEN @pytest.mark.asyncio class TestProjectWorkflow: """Integration tests for project lifecycle workflows.""" async def test_full_project_lifecycle(self, client, user_token): """Test complete project lifecycle: create -> pause -> resume -> archive.""" # Create create_response = await client.post( "/api/v1/projects", json={ "name": "Lifecycle Test", "slug": "lifecycle-test", "description": "Testing full lifecycle", }, headers={"Authorization": f"Bearer {user_token}"}, ) assert create_response.status_code == status.HTTP_201_CREATED project_id = create_response.json()["id"] # Pause pause_response = await client.post( f"/api/v1/projects/{project_id}/pause", headers={"Authorization": f"Bearer {user_token}"}, ) assert pause_response.status_code == status.HTTP_200_OK assert pause_response.json()["status"] == "paused" # Resume resume_response = await client.post( f"/api/v1/projects/{project_id}/resume", headers={"Authorization": f"Bearer {user_token}"}, ) assert resume_response.status_code == status.HTTP_200_OK assert resume_response.json()["status"] == "active" # Archive archive_response = await client.delete( f"/api/v1/projects/{project_id}", headers={"Authorization": f"Bearer {user_token}"}, ) assert archive_response.status_code == status.HTTP_200_OK # Verify final state get_response = await client.get( f"/api/v1/projects/{project_id}", headers={"Authorization": f"Bearer {user_token}"}, ) assert get_response.json()["status"] == "archived" async def test_superuser_can_manage_any_project( self, client, user_token, superuser_token ): """Test that superuser can perform all operations on any project.""" # Create project as regular user create_response = await client.post( "/api/v1/projects", json={"name": "User Project", "slug": "user-project"}, headers={"Authorization": f"Bearer {user_token}"}, ) project_id = create_response.json()["id"] # Superuser can read get_response = await client.get( f"/api/v1/projects/{project_id}", headers={"Authorization": f"Bearer {superuser_token}"}, ) assert get_response.status_code == status.HTTP_200_OK # Superuser can update update_response = await client.patch( f"/api/v1/projects/{project_id}", json={"description": "Updated by admin"}, headers={"Authorization": f"Bearer {superuser_token}"}, ) assert update_response.status_code == status.HTTP_200_OK # Superuser can pause pause_response = await client.post( f"/api/v1/projects/{project_id}/pause", headers={"Authorization": f"Bearer {superuser_token}"}, ) assert pause_response.status_code == status.HTTP_200_OK # Superuser can resume resume_response = await client.post( f"/api/v1/projects/{project_id}/resume", headers={"Authorization": f"Bearer {superuser_token}"}, ) assert resume_response.status_code == status.HTTP_200_OK # Superuser can archive archive_response = await client.delete( f"/api/v1/projects/{project_id}", headers={"Authorization": f"Bearer {superuser_token}"}, ) assert archive_response.status_code == status.HTTP_200_OK