Files
syndarix/backend/tests/api/routes/syndarix/test_issues.py
Felipe Cardoso 664415111a test(backend): add comprehensive tests for OAuth and agent endpoints
- Added tests for OAuth provider admin and consent endpoints covering edge cases.
- Extended agent-related tests to handle incorrect project associations and lifecycle state transitions.
- Introduced tests for sprint status transitions and validation checks.
- Improved multiline formatting consistency across all test functions.
2026-01-03 01:44:11 +01:00

996 lines
34 KiB
Python

# 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
@pytest.mark.asyncio
class TestIssueAssignment:
"""Tests for issue assignment endpoints."""
async def test_assign_issue_to_human(self, client, user_token, test_project):
"""Test assigning an issue to a human."""
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": "Issue to Assign",
},
headers={"Authorization": f"Bearer {user_token}"},
)
issue_id = create_response.json()["id"]
# Assign to human
response = await client.post(
f"/api/v1/projects/{project_id}/issues/{issue_id}/assign",
json={"human_assignee": "john.doe@example.com"},
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["human_assignee"] == "john.doe@example.com"
async def test_unassign_issue(self, client, user_token, test_project):
"""Test unassigning an issue."""
project_id = test_project["id"]
# Create issue and assign
create_response = await client.post(
f"/api/v1/projects/{project_id}/issues",
json={
"project_id": project_id,
"title": "Issue to Unassign",
},
headers={"Authorization": f"Bearer {user_token}"},
)
issue_id = create_response.json()["id"]
await client.post(
f"/api/v1/projects/{project_id}/issues/{issue_id}/assign",
json={"human_assignee": "john.doe@example.com"},
headers={"Authorization": f"Bearer {user_token}"},
)
# Unassign
response = await client.delete(
f"/api/v1/projects/{project_id}/issues/{issue_id}/assignment",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
# After unassign, assigned_agent_id should be None
# Note: human_assignee may or may not be cleared depending on implementation
assert data["assigned_agent_id"] is None
async def test_assign_issue_not_found(self, client, user_token, test_project):
"""Test assigning a nonexistent issue."""
project_id = test_project["id"]
fake_issue_id = str(uuid.uuid4())
response = await client.post(
f"/api/v1/projects/{project_id}/issues/{fake_issue_id}/assign",
json={"human_assignee": "john.doe@example.com"},
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
async def test_unassign_issue_not_found(self, client, user_token, test_project):
"""Test unassigning 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}/assignment",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
async def test_assign_issue_clears_assignment(
self, client, user_token, test_project
):
"""Test that assigning to null clears both assignments."""
project_id = test_project["id"]
# Create issue and assign
create_response = await client.post(
f"/api/v1/projects/{project_id}/issues",
json={
"project_id": project_id,
"title": "Issue to Clear",
},
headers={"Authorization": f"Bearer {user_token}"},
)
issue_id = create_response.json()["id"]
await client.post(
f"/api/v1/projects/{project_id}/issues/{issue_id}/assign",
json={"human_assignee": "john.doe@example.com"},
headers={"Authorization": f"Bearer {user_token}"},
)
# Clear assignment by sending empty object
response = await client.post(
f"/api/v1/projects/{project_id}/issues/{issue_id}/assign",
json={},
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_200_OK
@pytest.mark.asyncio
class TestIssueSync:
"""Tests for issue sync endpoint."""
async def test_sync_issue_no_tracker(self, client, user_token, test_project):
"""Test syncing an issue without external tracker."""
project_id = test_project["id"]
# Create issue without external tracker
create_response = await client.post(
f"/api/v1/projects/{project_id}/issues",
json={
"project_id": project_id,
"title": "Issue without Tracker",
},
headers={"Authorization": f"Bearer {user_token}"},
)
issue_id = create_response.json()["id"]
# Try to sync
response = await client.post(
f"/api/v1/projects/{project_id}/issues/{issue_id}/sync",
headers={"Authorization": f"Bearer {user_token}"},
)
# Should fail because no external tracker configured
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
async def test_sync_issue_not_found(self, client, user_token, test_project):
"""Test syncing a nonexistent issue."""
project_id = test_project["id"]
fake_issue_id = str(uuid.uuid4())
response = await client.post(
f"/api/v1/projects/{project_id}/issues/{fake_issue_id}/sync",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
@pytest.mark.asyncio
class TestIssueCrossProjectValidation:
"""Tests for cross-project validation (IDOR prevention)."""
async def test_issue_not_in_project(self, client, user_token):
"""Test accessing issue that exists but not in the specified project."""
# Create two projects
project1 = await client.post(
"/api/v1/projects",
json={"name": "Project 1", "slug": "project-1-idor"},
headers={"Authorization": f"Bearer {user_token}"},
)
project2 = await client.post(
"/api/v1/projects",
json={"name": "Project 2", "slug": "project-2-idor"},
headers={"Authorization": f"Bearer {user_token}"},
)
project1_id = project1.json()["id"]
project2_id = project2.json()["id"]
# Create issue in project1
issue_response = await client.post(
f"/api/v1/projects/{project1_id}/issues",
json={
"project_id": project1_id,
"title": "Project 1 Issue",
},
headers={"Authorization": f"Bearer {user_token}"},
)
issue_id = issue_response.json()["id"]
# Try to access issue via project2 (IDOR attempt)
response = await client.get(
f"/api/v1/projects/{project2_id}/issues/{issue_id}",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
async def test_update_issue_wrong_project(self, client, user_token):
"""Test updating issue through wrong project."""
# Create two projects
project1 = await client.post(
"/api/v1/projects",
json={"name": "Project A", "slug": "project-a-idor"},
headers={"Authorization": f"Bearer {user_token}"},
)
project2 = await client.post(
"/api/v1/projects",
json={"name": "Project B", "slug": "project-b-idor"},
headers={"Authorization": f"Bearer {user_token}"},
)
project1_id = project1.json()["id"]
project2_id = project2.json()["id"]
# Create issue in project1
issue_response = await client.post(
f"/api/v1/projects/{project1_id}/issues",
json={
"project_id": project1_id,
"title": "Project A Issue",
},
headers={"Authorization": f"Bearer {user_token}"},
)
issue_id = issue_response.json()["id"]
# Try to update issue via project2 (IDOR attempt)
response = await client.patch(
f"/api/v1/projects/{project2_id}/issues/{issue_id}",
json={"title": "Hacked Title"},
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
async def test_delete_issue_wrong_project(self, client, user_token):
"""Test deleting issue through wrong project."""
# Create two projects
project1 = await client.post(
"/api/v1/projects",
json={"name": "Project X", "slug": "project-x-idor"},
headers={"Authorization": f"Bearer {user_token}"},
)
project2 = await client.post(
"/api/v1/projects",
json={"name": "Project Y", "slug": "project-y-idor"},
headers={"Authorization": f"Bearer {user_token}"},
)
project1_id = project1.json()["id"]
project2_id = project2.json()["id"]
# Create issue in project1
issue_response = await client.post(
f"/api/v1/projects/{project1_id}/issues",
json={
"project_id": project1_id,
"title": "Project X Issue",
},
headers={"Authorization": f"Bearer {user_token}"},
)
issue_id = issue_response.json()["id"]
# Try to delete issue via project2 (IDOR attempt)
response = await client.delete(
f"/api/v1/projects/{project2_id}/issues/{issue_id}",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
@pytest.mark.asyncio
class TestIssueValidation:
"""Tests for issue validation during create/update."""
async def test_create_issue_invalid_priority(
self, client, user_token, test_project
):
"""Test creating issue with invalid priority."""
project_id = test_project["id"]
response = await client.post(
f"/api/v1/projects/{project_id}/issues",
json={
"project_id": project_id,
"title": "Issue with Invalid Priority",
"priority": "invalid_priority",
},
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
async def test_create_issue_invalid_status(self, client, user_token, test_project):
"""Test creating issue with invalid status."""
project_id = test_project["id"]
response = await client.post(
f"/api/v1/projects/{project_id}/issues",
json={
"project_id": project_id,
"title": "Issue with Invalid Status",
"status": "invalid_status",
},
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
async def test_update_issue_invalid_priority(
self, client, user_token, test_project
):
"""Test updating issue with invalid priority."""
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": "Issue to Update",
},
headers={"Authorization": f"Bearer {user_token}"},
)
issue_id = create_response.json()["id"]
# Update with invalid priority
response = await client.patch(
f"/api/v1/projects/{project_id}/issues/{issue_id}",
json={"priority": "invalid_priority"},
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
async def test_create_issue_with_nonexistent_sprint(
self, client, user_token, test_project
):
"""Test creating issue with nonexistent sprint ID."""
project_id = test_project["id"]
fake_sprint_id = str(uuid.uuid4())
response = await client.post(
f"/api/v1/projects/{project_id}/issues",
json={
"project_id": project_id,
"title": "Issue with Fake Sprint",
"sprint_id": fake_sprint_id,
},
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
async def test_create_issue_with_nonexistent_agent(
self, client, user_token, test_project
):
"""Test creating issue with nonexistent agent ID."""
project_id = test_project["id"]
fake_agent_id = str(uuid.uuid4())
response = await client.post(
f"/api/v1/projects/{project_id}/issues",
json={
"project_id": project_id,
"title": "Issue with Fake Agent",
"assigned_agent_id": fake_agent_id,
},
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND