# tests/api/routes/syndarix/test_issues.py """ Comprehensive tests for the Issues API endpoints. Tests cover: - CRUD operations (create, read, update, delete) - Issue filtering and search - Issue assignment - Issue statistics - Authorization checks """ import uuid import pytest import pytest_asyncio from fastapi import status @pytest_asyncio.fixture async def test_project(client, user_token): """Create a test project for issue tests.""" response = await client.post( "/api/v1/projects", json={"name": "Issue Test Project", "slug": "issue-test-project"}, headers={"Authorization": f"Bearer {user_token}"}, ) return response.json() @pytest_asyncio.fixture async def superuser_project(client, superuser_token): """Create a project owned by superuser.""" response = await client.post( "/api/v1/projects", json={"name": "Superuser Project", "slug": "superuser-project"}, headers={"Authorization": f"Bearer {superuser_token}"}, ) return response.json() @pytest.mark.asyncio class TestCreateIssue: """Tests for POST /api/v1/projects/{project_id}/issues endpoint.""" async def test_create_issue_success(self, client, user_token, test_project): """Test successful issue creation.""" project_id = test_project["id"] response = await client.post( f"/api/v1/projects/{project_id}/issues", json={ "project_id": project_id, "title": "Test Issue", "body": "This is a test issue description", "priority": "medium", }, headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_201_CREATED data = response.json() assert data["title"] == "Test Issue" assert data["body"] == "This is a test issue description" assert data["priority"] == "medium" assert data["status"] == "open" assert data["project_id"] == project_id assert "id" in data assert "created_at" in data async def test_create_issue_minimal_fields(self, client, user_token, test_project): """Test creating issue with only required fields.""" project_id = test_project["id"] response = await client.post( f"/api/v1/projects/{project_id}/issues", json={ "project_id": project_id, "title": "Minimal Issue", }, headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_201_CREATED data = response.json() assert data["title"] == "Minimal Issue" assert data["body"] == "" # Body defaults to empty string assert data["status"] == "open" async def test_create_issue_with_labels(self, client, user_token, test_project): """Test creating issue with labels.""" project_id = test_project["id"] response = await client.post( f"/api/v1/projects/{project_id}/issues", json={ "project_id": project_id, "title": "Labeled Issue", "labels": ["bug", "urgent", "frontend"], }, headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_201_CREATED data = response.json() assert "bug" in data["labels"] assert "urgent" in data["labels"] assert "frontend" in data["labels"] async def test_create_issue_with_story_points(self, client, user_token, test_project): """Test creating issue with story points.""" project_id = test_project["id"] response = await client.post( f"/api/v1/projects/{project_id}/issues", json={ "project_id": project_id, "title": "Story Points Issue", "story_points": 5, }, headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_201_CREATED data = response.json() assert data["story_points"] == 5 async def test_create_issue_unauthorized_project( self, client, user_token, superuser_project ): """Test that users cannot create issues in others' projects.""" project_id = superuser_project["id"] response = await client.post( f"/api/v1/projects/{project_id}/issues", json={ "project_id": project_id, "title": "Unauthorized Issue", }, headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_403_FORBIDDEN async def test_create_issue_nonexistent_project(self, client, user_token): """Test creating issue in nonexistent project.""" fake_project_id = str(uuid.uuid4()) response = await client.post( f"/api/v1/projects/{fake_project_id}/issues", json={ "project_id": fake_project_id, "title": "Orphan Issue", }, headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_404_NOT_FOUND @pytest.mark.asyncio class TestListIssues: """Tests for GET /api/v1/projects/{project_id}/issues endpoint.""" async def test_list_issues_empty(self, client, user_token, test_project): """Test listing issues when none exist.""" project_id = test_project["id"] response = await client.get( f"/api/v1/projects/{project_id}/issues", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK data = response.json() assert data["data"] == [] assert data["pagination"]["total"] == 0 async def test_list_issues_with_data(self, client, user_token, test_project): """Test listing issues returns created issues.""" project_id = test_project["id"] # Create multiple issues for i in range(3): await client.post( f"/api/v1/projects/{project_id}/issues", json={ "project_id": project_id, "title": f"Issue {i + 1}", }, headers={"Authorization": f"Bearer {user_token}"}, ) response = await client.get( f"/api/v1/projects/{project_id}/issues", 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_issues_filter_by_status(self, client, user_token, test_project): """Test filtering issues by status.""" project_id = test_project["id"] # Create issues with different statuses await client.post( f"/api/v1/projects/{project_id}/issues", json={ "project_id": project_id, "title": "Open Issue", "status": "open", }, headers={"Authorization": f"Bearer {user_token}"}, ) await client.post( f"/api/v1/projects/{project_id}/issues", json={ "project_id": project_id, "title": "Closed Issue", "status": "closed", }, headers={"Authorization": f"Bearer {user_token}"}, ) # Filter by open response = await client.get( f"/api/v1/projects/{project_id}/issues?status=open", 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"] == "open" async def test_list_issues_filter_by_priority(self, client, user_token, test_project): """Test filtering issues by priority.""" project_id = test_project["id"] # Create issues with different priorities await client.post( f"/api/v1/projects/{project_id}/issues", json={ "project_id": project_id, "title": "High Priority Issue", "priority": "high", }, headers={"Authorization": f"Bearer {user_token}"}, ) await client.post( f"/api/v1/projects/{project_id}/issues", json={ "project_id": project_id, "title": "Low Priority Issue", "priority": "low", }, headers={"Authorization": f"Bearer {user_token}"}, ) # Filter by high priority response = await client.get( f"/api/v1/projects/{project_id}/issues?priority=high", 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]["priority"] == "high" async def test_list_issues_search(self, client, user_token, test_project): """Test searching issues by title/body.""" project_id = test_project["id"] await client.post( f"/api/v1/projects/{project_id}/issues", json={ "project_id": project_id, "title": "Authentication Bug", "body": "Users cannot login", }, headers={"Authorization": f"Bearer {user_token}"}, ) await client.post( f"/api/v1/projects/{project_id}/issues", json={ "project_id": project_id, "title": "UI Enhancement", "body": "Improve dashboard layout", }, headers={"Authorization": f"Bearer {user_token}"}, ) # Search for authentication response = await client.get( f"/api/v1/projects/{project_id}/issues?search=authentication", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK data = response.json() assert len(data["data"]) == 1 assert "Authentication" in data["data"][0]["title"] async def test_list_issues_pagination(self, client, user_token, test_project): """Test pagination works correctly.""" project_id = test_project["id"] # Create 5 issues for i in range(5): await client.post( f"/api/v1/projects/{project_id}/issues", json={ "project_id": project_id, "title": f"Issue {i + 1}", }, headers={"Authorization": f"Bearer {user_token}"}, ) # Get first page (2 items) response = await client.get( f"/api/v1/projects/{project_id}/issues?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 @pytest.mark.asyncio class TestGetIssue: """Tests for GET /api/v1/projects/{project_id}/issues/{issue_id} endpoint.""" async def test_get_issue_success(self, client, user_token, test_project): """Test getting an issue by ID.""" project_id = test_project["id"] # Create issue create_response = await client.post( f"/api/v1/projects/{project_id}/issues", json={ "project_id": project_id, "title": "Get Test Issue", "body": "Test description", }, headers={"Authorization": f"Bearer {user_token}"}, ) issue_id = create_response.json()["id"] # Get issue response = await client.get( f"/api/v1/projects/{project_id}/issues/{issue_id}", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK data = response.json() assert data["id"] == issue_id assert data["title"] == "Get Test Issue" async def test_get_issue_not_found(self, client, user_token, test_project): """Test getting a nonexistent issue.""" project_id = test_project["id"] fake_issue_id = str(uuid.uuid4()) response = await client.get( f"/api/v1/projects/{project_id}/issues/{fake_issue_id}", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_404_NOT_FOUND @pytest.mark.asyncio class TestUpdateIssue: """Tests for PATCH /api/v1/projects/{project_id}/issues/{issue_id} endpoint.""" async def test_update_issue_success(self, client, user_token, test_project): """Test updating an issue.""" project_id = test_project["id"] # Create issue create_response = await client.post( f"/api/v1/projects/{project_id}/issues", json={ "project_id": project_id, "title": "Original Title", }, headers={"Authorization": f"Bearer {user_token}"}, ) issue_id = create_response.json()["id"] # Update issue response = await client.patch( f"/api/v1/projects/{project_id}/issues/{issue_id}", json={"title": "Updated Title", "body": "New description"}, headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK data = response.json() assert data["title"] == "Updated Title" assert data["body"] == "New description" async def test_update_issue_status(self, client, user_token, test_project): """Test updating issue status.""" project_id = test_project["id"] # Create issue create_response = await client.post( f"/api/v1/projects/{project_id}/issues", json={ "project_id": project_id, "title": "Status Test Issue", }, headers={"Authorization": f"Bearer {user_token}"}, ) issue_id = create_response.json()["id"] # Update status to in_progress response = await client.patch( f"/api/v1/projects/{project_id}/issues/{issue_id}", json={"status": "in_progress"}, headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK assert response.json()["status"] == "in_progress" async def test_update_issue_priority(self, client, user_token, test_project): """Test updating issue priority.""" project_id = test_project["id"] # Create issue with low priority create_response = await client.post( f"/api/v1/projects/{project_id}/issues", json={ "project_id": project_id, "title": "Priority Test Issue", "priority": "low", }, headers={"Authorization": f"Bearer {user_token}"}, ) issue_id = create_response.json()["id"] # Update to critical response = await client.patch( f"/api/v1/projects/{project_id}/issues/{issue_id}", json={"priority": "critical"}, headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK assert response.json()["priority"] == "critical" async def test_update_issue_not_found(self, client, user_token, test_project): """Test updating a nonexistent issue.""" project_id = test_project["id"] fake_issue_id = str(uuid.uuid4()) response = await client.patch( f"/api/v1/projects/{project_id}/issues/{fake_issue_id}", json={"title": "Updated Title"}, headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_404_NOT_FOUND @pytest.mark.asyncio class TestDeleteIssue: """Tests for DELETE /api/v1/projects/{project_id}/issues/{issue_id} endpoint.""" async def test_delete_issue_success(self, client, user_token, test_project): """Test deleting an issue.""" project_id = test_project["id"] # Create issue create_response = await client.post( f"/api/v1/projects/{project_id}/issues", json={ "project_id": project_id, "title": "Delete Test Issue", }, headers={"Authorization": f"Bearer {user_token}"}, ) issue_id = create_response.json()["id"] # Delete issue response = await client.delete( f"/api/v1/projects/{project_id}/issues/{issue_id}", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK assert response.json()["success"] is True async def test_delete_issue_not_found(self, client, user_token, test_project): """Test deleting a nonexistent issue.""" project_id = test_project["id"] fake_issue_id = str(uuid.uuid4()) response = await client.delete( f"/api/v1/projects/{project_id}/issues/{fake_issue_id}", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_404_NOT_FOUND @pytest.mark.asyncio class TestIssueStats: """Tests for GET /api/v1/projects/{project_id}/issues/stats endpoint.""" async def test_get_issue_stats_empty(self, client, user_token, test_project): """Test getting stats when no issues exist.""" project_id = test_project["id"] response = await client.get( f"/api/v1/projects/{project_id}/issues/stats", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK data = response.json() assert data["total"] == 0 assert data["open"] == 0 assert data["in_progress"] == 0 assert data["in_review"] == 0 assert data["blocked"] == 0 assert data["closed"] == 0 async def test_get_issue_stats_with_data(self, client, user_token, test_project): """Test getting stats with issues.""" project_id = test_project["id"] # Create issues with different statuses and priorities await client.post( f"/api/v1/projects/{project_id}/issues", json={ "project_id": project_id, "title": "Open High Issue", "status": "open", "priority": "high", "story_points": 5, }, headers={"Authorization": f"Bearer {user_token}"}, ) await client.post( f"/api/v1/projects/{project_id}/issues", json={ "project_id": project_id, "title": "Closed Low Issue", "status": "closed", "priority": "low", "story_points": 3, }, headers={"Authorization": f"Bearer {user_token}"}, ) response = await client.get( f"/api/v1/projects/{project_id}/issues/stats", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK data = response.json() assert data["total"] == 2 assert data["open"] == 1 assert data["closed"] == 1 assert data["by_priority"]["high"] == 1 assert data["by_priority"]["low"] == 1 @pytest.mark.asyncio class TestIssueAuthorization: """Tests for issue authorization.""" async def test_superuser_can_manage_any_project_issues( self, client, user_token, superuser_token, test_project ): """Test that superuser can manage issues in any project.""" project_id = test_project["id"] # Create issue as superuser in user's project response = await client.post( f"/api/v1/projects/{project_id}/issues", json={ "project_id": project_id, "title": "Superuser Issue", }, headers={"Authorization": f"Bearer {superuser_token}"}, ) assert response.status_code == status.HTTP_201_CREATED async def test_user_cannot_access_other_project_issues( self, client, user_token, superuser_project ): """Test that users cannot access issues in others' projects.""" project_id = superuser_project["id"] response = await client.get( f"/api/v1/projects/{project_id}/issues", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_403_FORBIDDEN