""" Project and Agent E2E Workflow Tests. Tests complete project management workflows with real PostgreSQL: - Project CRUD and lifecycle management - Agent spawning and lifecycle - Issue management within projects - Sprint planning and execution Usage: make test-e2e # Run all E2E tests """ from datetime import date, timedelta from uuid import uuid4 import pytest pytestmark = [ pytest.mark.e2e, pytest.mark.postgres, pytest.mark.asyncio, ] class TestProjectCRUDWorkflows: """Test complete project CRUD workflows.""" async def test_create_project_workflow(self, e2e_client): """Test creating a project as authenticated user.""" # Register and login email = f"project-owner-{uuid4().hex[:8]}@example.com" password = "SecurePass123!" await e2e_client.post( "/api/v1/auth/register", json={ "email": email, "password": password, "first_name": "Project", "last_name": "Owner", }, ) login_resp = await e2e_client.post( "/api/v1/auth/login", json={"email": email, "password": password}, ) tokens = login_resp.json() # Create project project_slug = f"test-project-{uuid4().hex[:8]}" create_resp = await e2e_client.post( "/api/v1/projects", headers={"Authorization": f"Bearer {tokens['access_token']}"}, json={ "name": "E2E Test Project", "slug": project_slug, "description": "A project for E2E testing", "autonomy_level": "milestone", }, ) assert create_resp.status_code == 201, f"Failed: {create_resp.text}" project = create_resp.json() assert project["name"] == "E2E Test Project" assert project["slug"] == project_slug assert project["status"] == "active" assert project["agent_count"] == 0 assert project["issue_count"] == 0 async def test_list_projects_only_shows_owned(self, e2e_client): """Test that users only see their own projects.""" # Create two users with projects users = [] for i in range(2): email = f"user-{i}-{uuid4().hex[:8]}@example.com" password = "SecurePass123!" await e2e_client.post( "/api/v1/auth/register", json={ "email": email, "password": password, "first_name": f"User{i}", "last_name": "Test", }, ) login_resp = await e2e_client.post( "/api/v1/auth/login", json={"email": email, "password": password}, ) tokens = login_resp.json() # Each user creates their own project project_slug = f"user{i}-project-{uuid4().hex[:8]}" await e2e_client.post( "/api/v1/projects", headers={"Authorization": f"Bearer {tokens['access_token']}"}, json={ "name": f"User {i} Project", "slug": project_slug, }, ) users.append({"email": email, "tokens": tokens, "slug": project_slug}) # User 0 should only see their project list_resp = await e2e_client.get( "/api/v1/projects", headers={"Authorization": f"Bearer {users[0]['tokens']['access_token']}"}, ) assert list_resp.status_code == 200 data = list_resp.json() slugs = [p["slug"] for p in data["data"]] assert users[0]["slug"] in slugs assert users[1]["slug"] not in slugs async def test_project_lifecycle_pause_resume(self, e2e_client): """Test pausing and resuming a project.""" # Setup user and project email = f"lifecycle-{uuid4().hex[:8]}@example.com" password = "SecurePass123!" await e2e_client.post( "/api/v1/auth/register", json={ "email": email, "password": password, "first_name": "Lifecycle", "last_name": "Test", }, ) login_resp = await e2e_client.post( "/api/v1/auth/login", json={"email": email, "password": password}, ) tokens = login_resp.json() project_slug = f"lifecycle-project-{uuid4().hex[:8]}" create_resp = await e2e_client.post( "/api/v1/projects", headers={"Authorization": f"Bearer {tokens['access_token']}"}, json={"name": "Lifecycle Project", "slug": project_slug}, ) project = create_resp.json() project_id = project["id"] # Pause the project pause_resp = await e2e_client.post( f"/api/v1/projects/{project_id}/pause", headers={"Authorization": f"Bearer {tokens['access_token']}"}, ) assert pause_resp.status_code == 200 assert pause_resp.json()["status"] == "paused" # Resume the project resume_resp = await e2e_client.post( f"/api/v1/projects/{project_id}/resume", headers={"Authorization": f"Bearer {tokens['access_token']}"}, ) assert resume_resp.status_code == 200 assert resume_resp.json()["status"] == "active" async def test_project_archive(self, e2e_client): """Test archiving a project (soft delete).""" # Setup user and project email = f"archive-{uuid4().hex[:8]}@example.com" password = "SecurePass123!" await e2e_client.post( "/api/v1/auth/register", json={ "email": email, "password": password, "first_name": "Archive", "last_name": "Test", }, ) login_resp = await e2e_client.post( "/api/v1/auth/login", json={"email": email, "password": password}, ) tokens = login_resp.json() project_slug = f"archive-project-{uuid4().hex[:8]}" create_resp = await e2e_client.post( "/api/v1/projects", headers={"Authorization": f"Bearer {tokens['access_token']}"}, json={"name": "Archive Project", "slug": project_slug}, ) project = create_resp.json() project_id = project["id"] # Archive the project archive_resp = await e2e_client.delete( f"/api/v1/projects/{project_id}", headers={"Authorization": f"Bearer {tokens['access_token']}"}, ) assert archive_resp.status_code == 200 assert archive_resp.json()["success"] is True # Verify project is archived get_resp = await e2e_client.get( f"/api/v1/projects/{project_id}", headers={"Authorization": f"Bearer {tokens['access_token']}"}, ) assert get_resp.status_code == 200 assert get_resp.json()["status"] == "archived" class TestIssueWorkflows: """Test issue management workflows within projects.""" async def test_create_and_list_issues(self, e2e_client): """Test creating and listing issues in a project.""" # Setup user and project email = f"issue-test-{uuid4().hex[:8]}@example.com" password = "SecurePass123!" await e2e_client.post( "/api/v1/auth/register", json={ "email": email, "password": password, "first_name": "Issue", "last_name": "Tester", }, ) login_resp = await e2e_client.post( "/api/v1/auth/login", json={"email": email, "password": password}, ) tokens = login_resp.json() project_slug = f"issue-project-{uuid4().hex[:8]}" create_resp = await e2e_client.post( "/api/v1/projects", headers={"Authorization": f"Bearer {tokens['access_token']}"}, json={"name": "Issue Test Project", "slug": project_slug}, ) project = create_resp.json() project_id = project["id"] # Create multiple issues issues = [] for i in range(3): issue_resp = await e2e_client.post( f"/api/v1/projects/{project_id}/issues", headers={"Authorization": f"Bearer {tokens['access_token']}"}, json={ "project_id": project_id, "title": f"Test Issue {i + 1}", "body": f"Description for issue {i + 1}", "priority": ["low", "medium", "high"][i], }, ) assert issue_resp.status_code == 201, f"Failed: {issue_resp.text}" issues.append(issue_resp.json()) # List issues list_resp = await e2e_client.get( f"/api/v1/projects/{project_id}/issues", headers={"Authorization": f"Bearer {tokens['access_token']}"}, ) assert list_resp.status_code == 200 data = list_resp.json() assert data["pagination"]["total"] == 3 async def test_issue_status_transitions(self, e2e_client): """Test issue status workflow transitions.""" # Setup user and project email = f"status-test-{uuid4().hex[:8]}@example.com" password = "SecurePass123!" await e2e_client.post( "/api/v1/auth/register", json={ "email": email, "password": password, "first_name": "Status", "last_name": "Tester", }, ) login_resp = await e2e_client.post( "/api/v1/auth/login", json={"email": email, "password": password}, ) tokens = login_resp.json() project_slug = f"status-project-{uuid4().hex[:8]}" create_resp = await e2e_client.post( "/api/v1/projects", headers={"Authorization": f"Bearer {tokens['access_token']}"}, json={"name": "Status Test Project", "slug": project_slug}, ) project = create_resp.json() project_id = project["id"] # Create issue issue_resp = await e2e_client.post( f"/api/v1/projects/{project_id}/issues", headers={"Authorization": f"Bearer {tokens['access_token']}"}, json={ "project_id": project_id, "title": "Status Workflow Issue", "body": "Testing status transitions", }, ) issue = issue_resp.json() issue_id = issue["id"] assert issue["status"] == "open" # Transition through statuses for new_status in ["in_progress", "in_review", "closed"]: update_resp = await e2e_client.patch( f"/api/v1/projects/{project_id}/issues/{issue_id}", headers={"Authorization": f"Bearer {tokens['access_token']}"}, json={"status": new_status}, ) assert update_resp.status_code == 200, f"Failed: {update_resp.text}" assert update_resp.json()["status"] == new_status async def test_issue_filtering(self, e2e_client): """Test issue filtering by status and priority.""" # Setup user and project email = f"filter-test-{uuid4().hex[:8]}@example.com" password = "SecurePass123!" await e2e_client.post( "/api/v1/auth/register", json={ "email": email, "password": password, "first_name": "Filter", "last_name": "Tester", }, ) login_resp = await e2e_client.post( "/api/v1/auth/login", json={"email": email, "password": password}, ) tokens = login_resp.json() project_slug = f"filter-project-{uuid4().hex[:8]}" create_resp = await e2e_client.post( "/api/v1/projects", headers={"Authorization": f"Bearer {tokens['access_token']}"}, json={"name": "Filter Test Project", "slug": project_slug}, ) project = create_resp.json() project_id = project["id"] # Create issues with different priorities for priority in ["low", "medium", "high"]: await e2e_client.post( f"/api/v1/projects/{project_id}/issues", headers={"Authorization": f"Bearer {tokens['access_token']}"}, json={ "project_id": project_id, "title": f"{priority.title()} Priority Issue", "priority": priority, }, ) # Filter by high priority filter_resp = await e2e_client.get( f"/api/v1/projects/{project_id}/issues", headers={"Authorization": f"Bearer {tokens['access_token']}"}, params={"priority": "high"}, ) assert filter_resp.status_code == 200 data = filter_resp.json() assert data["pagination"]["total"] == 1 assert data["data"][0]["priority"] == "high" class TestSprintWorkflows: """Test sprint planning and execution workflows.""" async def test_sprint_lifecycle(self, e2e_client): """Test complete sprint lifecycle: plan -> start -> complete.""" # Setup user and project email = f"sprint-test-{uuid4().hex[:8]}@example.com" password = "SecurePass123!" await e2e_client.post( "/api/v1/auth/register", json={ "email": email, "password": password, "first_name": "Sprint", "last_name": "Tester", }, ) login_resp = await e2e_client.post( "/api/v1/auth/login", json={"email": email, "password": password}, ) tokens = login_resp.json() project_slug = f"sprint-project-{uuid4().hex[:8]}" create_resp = await e2e_client.post( "/api/v1/projects", headers={"Authorization": f"Bearer {tokens['access_token']}"}, json={"name": "Sprint Test Project", "slug": project_slug}, ) project = create_resp.json() project_id = project["id"] # Create sprint today = date.today() sprint_resp = await e2e_client.post( f"/api/v1/projects/{project_id}/sprints", headers={"Authorization": f"Bearer {tokens['access_token']}"}, json={ "project_id": project_id, "name": "Sprint 1", "number": 1, "goal": "Complete initial features", "start_date": today.isoformat(), "end_date": (today + timedelta(days=14)).isoformat(), }, ) assert sprint_resp.status_code == 201, f"Failed: {sprint_resp.text}" sprint = sprint_resp.json() sprint_id = sprint["id"] assert sprint["status"] == "planned" # Start sprint start_resp = await e2e_client.post( f"/api/v1/projects/{project_id}/sprints/{sprint_id}/start", headers={"Authorization": f"Bearer {tokens['access_token']}"}, ) assert start_resp.status_code == 200, f"Failed: {start_resp.text}" assert start_resp.json()["status"] == "active" # Complete sprint complete_resp = await e2e_client.post( f"/api/v1/projects/{project_id}/sprints/{sprint_id}/complete", headers={"Authorization": f"Bearer {tokens['access_token']}"}, ) assert complete_resp.status_code == 200, f"Failed: {complete_resp.text}" assert complete_resp.json()["status"] == "completed" async def test_add_issues_to_sprint(self, e2e_client): """Test adding issues to a sprint.""" # Setup user and project email = f"sprint-issues-{uuid4().hex[:8]}@example.com" password = "SecurePass123!" await e2e_client.post( "/api/v1/auth/register", json={ "email": email, "password": password, "first_name": "SprintIssues", "last_name": "Tester", }, ) login_resp = await e2e_client.post( "/api/v1/auth/login", json={"email": email, "password": password}, ) tokens = login_resp.json() project_slug = f"sprint-issues-project-{uuid4().hex[:8]}" create_resp = await e2e_client.post( "/api/v1/projects", headers={"Authorization": f"Bearer {tokens['access_token']}"}, json={"name": "Sprint Issues Project", "slug": project_slug}, ) project = create_resp.json() project_id = project["id"] # Create sprint today = date.today() sprint_resp = await e2e_client.post( f"/api/v1/projects/{project_id}/sprints", headers={"Authorization": f"Bearer {tokens['access_token']}"}, json={ "project_id": project_id, "name": "Sprint 1", "number": 1, "start_date": today.isoformat(), "end_date": (today + timedelta(days=14)).isoformat(), }, ) assert sprint_resp.status_code == 201, f"Failed: {sprint_resp.text}" sprint = sprint_resp.json() sprint_id = sprint["id"] # Create issue issue_resp = await e2e_client.post( f"/api/v1/projects/{project_id}/issues", headers={"Authorization": f"Bearer {tokens['access_token']}"}, json={ "project_id": project_id, "title": "Sprint Issue", "story_points": 5, }, ) issue = issue_resp.json() issue_id = issue["id"] # Add issue to sprint add_resp = await e2e_client.post( f"/api/v1/projects/{project_id}/sprints/{sprint_id}/issues", headers={"Authorization": f"Bearer {tokens['access_token']}"}, params={"issue_id": issue_id}, ) assert add_resp.status_code == 200, f"Failed: {add_resp.text}" # Verify issue is in sprint issue_check = await e2e_client.get( f"/api/v1/projects/{project_id}/issues/{issue_id}", headers={"Authorization": f"Bearer {tokens['access_token']}"}, ) assert issue_check.json()["sprint_id"] == sprint_id class TestCrossEntityValidation: """Test validation across related entities.""" async def test_cannot_access_other_users_project(self, e2e_client): """Test that users cannot access projects they don't own.""" # Create two users owner_email = f"owner-{uuid4().hex[:8]}@example.com" other_email = f"other-{uuid4().hex[:8]}@example.com" password = "SecurePass123!" # Register owner await e2e_client.post( "/api/v1/auth/register", json={ "email": owner_email, "password": password, "first_name": "Owner", "last_name": "User", }, ) owner_tokens = ( await e2e_client.post( "/api/v1/auth/login", json={"email": owner_email, "password": password}, ) ).json() # Register other user await e2e_client.post( "/api/v1/auth/register", json={ "email": other_email, "password": password, "first_name": "Other", "last_name": "User", }, ) other_tokens = ( await e2e_client.post( "/api/v1/auth/login", json={"email": other_email, "password": password}, ) ).json() # Owner creates project project_slug = f"private-project-{uuid4().hex[:8]}" create_resp = await e2e_client.post( "/api/v1/projects", headers={"Authorization": f"Bearer {owner_tokens['access_token']}"}, json={"name": "Private Project", "slug": project_slug}, ) project = create_resp.json() project_id = project["id"] # Other user tries to access access_resp = await e2e_client.get( f"/api/v1/projects/{project_id}", headers={"Authorization": f"Bearer {other_tokens['access_token']}"}, ) assert access_resp.status_code == 403 async def test_duplicate_project_slug_rejected(self, e2e_client): """Test that duplicate project slugs are rejected.""" email = f"dup-test-{uuid4().hex[:8]}@example.com" password = "SecurePass123!" await e2e_client.post( "/api/v1/auth/register", json={ "email": email, "password": password, "first_name": "Dup", "last_name": "Tester", }, ) tokens = ( await e2e_client.post( "/api/v1/auth/login", json={"email": email, "password": password}, ) ).json() slug = f"unique-slug-{uuid4().hex[:8]}" # First creation should succeed resp1 = await e2e_client.post( "/api/v1/projects", headers={"Authorization": f"Bearer {tokens['access_token']}"}, json={"name": "First Project", "slug": slug}, ) assert resp1.status_code == 201 # Second creation with same slug should fail resp2 = await e2e_client.post( "/api/v1/projects", headers={"Authorization": f"Bearer {tokens['access_token']}"}, json={"name": "Second Project", "slug": slug}, ) assert resp2.status_code == 409 # Conflict class TestIssueStats: """Test issue statistics endpoints.""" async def test_issue_stats_aggregation(self, e2e_client): """Test that issue stats are correctly aggregated.""" email = f"stats-test-{uuid4().hex[:8]}@example.com" password = "SecurePass123!" await e2e_client.post( "/api/v1/auth/register", json={ "email": email, "password": password, "first_name": "Stats", "last_name": "Tester", }, ) tokens = ( await e2e_client.post( "/api/v1/auth/login", json={"email": email, "password": password}, ) ).json() project_slug = f"stats-project-{uuid4().hex[:8]}" create_resp = await e2e_client.post( "/api/v1/projects", headers={"Authorization": f"Bearer {tokens['access_token']}"}, json={"name": "Stats Project", "slug": project_slug}, ) project = create_resp.json() project_id = project["id"] # Create issues with different priorities and story points await e2e_client.post( f"/api/v1/projects/{project_id}/issues", headers={"Authorization": f"Bearer {tokens['access_token']}"}, json={ "project_id": project_id, "title": "High Priority", "priority": "high", "story_points": 8, }, ) await e2e_client.post( f"/api/v1/projects/{project_id}/issues", headers={"Authorization": f"Bearer {tokens['access_token']}"}, json={ "project_id": project_id, "title": "Low Priority", "priority": "low", "story_points": 2, }, ) # Get stats stats_resp = await e2e_client.get( f"/api/v1/projects/{project_id}/issues/stats", headers={"Authorization": f"Bearer {tokens['access_token']}"}, ) assert stats_resp.status_code == 200 stats = stats_resp.json() assert stats["total"] == 2 assert stats["total_story_points"] == 10