From 2ccaeb23f2afaca84ade9e8f90b31adbf0ffd58d Mon Sep 17 00:00:00 2001 From: Felipe Cardoso Date: Wed, 31 Dec 2025 13:20:17 +0100 Subject: [PATCH] test(issues): add comprehensive API route tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 24 tests for issues API covering: - CRUD operations (create, list, get, update, delete) - Status and priority filtering - Search functionality - Issue statistics - Authorization and access control 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../tests/api/routes/syndarix/test_issues.py | 615 ++++++++++++++++++ 1 file changed, 615 insertions(+) create mode 100644 backend/tests/api/routes/syndarix/test_issues.py diff --git a/backend/tests/api/routes/syndarix/test_issues.py b/backend/tests/api/routes/syndarix/test_issues.py new file mode 100644 index 0000000..3189419 --- /dev/null +++ b/backend/tests/api/routes/syndarix/test_issues.py @@ -0,0 +1,615 @@ +# 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