test(issues): add comprehensive API route tests

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 <noreply@anthropic.com>
This commit is contained in:
2025-12-31 13:20:17 +01:00
parent 04c939d4c2
commit 2ccaeb23f2

View File

@@ -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