test(sprints): add sprint issues and IDOR prevention tests

- Add TestSprintIssues class (5 tests)
  - List sprint issues (empty/with data)
  - Add issue to sprint
  - Add nonexistent issue to sprint

- Add TestSprintCrossProjectValidation class (3 tests)
  - IDOR prevention for get/update/start through wrong project

Coverage: sprints.py 72% → 76%

🤖 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 14:04:05 +01:00
parent c3b66cccfc
commit ddf9b5fe25

View File

@@ -819,3 +819,303 @@ class TestSprintAuthorization:
)
assert response.status_code == status.HTTP_403_FORBIDDEN
@pytest.mark.asyncio
class TestSprintIssues:
"""Tests for sprint issue management endpoints."""
async def test_list_sprint_issues_empty(self, client, user_token, test_project):
"""Test listing issues in a sprint with no issues."""
project_id = test_project["id"]
start_date = date.today()
end_date = start_date + timedelta(days=14)
# Create sprint
create_response = await client.post(
f"/api/v1/projects/{project_id}/sprints",
json={
"project_id": project_id,
"name": "Empty Sprint",
"number": 1,
"start_date": start_date.isoformat(),
"end_date": end_date.isoformat(),
},
headers={"Authorization": f"Bearer {user_token}"},
)
sprint_id = create_response.json()["id"]
# List issues
response = await client.get(
f"/api/v1/projects/{project_id}/sprints/{sprint_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_sprint_issues_with_data(self, client, user_token, test_project):
"""Test listing issues in a sprint with issues."""
project_id = test_project["id"]
start_date = date.today()
end_date = start_date + timedelta(days=14)
# Create sprint
create_response = await client.post(
f"/api/v1/projects/{project_id}/sprints",
json={
"project_id": project_id,
"name": "Sprint with Issues",
"number": 1,
"start_date": start_date.isoformat(),
"end_date": end_date.isoformat(),
},
headers={"Authorization": f"Bearer {user_token}"},
)
sprint_id = create_response.json()["id"]
# Create issue in the sprint
await client.post(
f"/api/v1/projects/{project_id}/issues",
json={
"project_id": project_id,
"title": "Sprint Issue 1",
"sprint_id": sprint_id,
},
headers={"Authorization": f"Bearer {user_token}"},
)
await client.post(
f"/api/v1/projects/{project_id}/issues",
json={
"project_id": project_id,
"title": "Sprint Issue 2",
"sprint_id": sprint_id,
},
headers={"Authorization": f"Bearer {user_token}"},
)
# List issues
response = await client.get(
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/issues",
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"] == 2
async def test_list_sprint_issues_not_found(self, client, user_token, test_project):
"""Test listing issues in a nonexistent sprint."""
project_id = test_project["id"]
fake_sprint_id = str(uuid.uuid4())
response = await client.get(
f"/api/v1/projects/{project_id}/sprints/{fake_sprint_id}/issues",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
async def test_add_issue_to_sprint(self, client, user_token, test_project):
"""Test adding an issue to a sprint."""
project_id = test_project["id"]
start_date = date.today()
end_date = start_date + timedelta(days=14)
# Create sprint
sprint_response = await client.post(
f"/api/v1/projects/{project_id}/sprints",
json={
"project_id": project_id,
"name": "Target Sprint",
"number": 1,
"start_date": start_date.isoformat(),
"end_date": end_date.isoformat(),
},
headers={"Authorization": f"Bearer {user_token}"},
)
sprint_id = sprint_response.json()["id"]
# Create issue without sprint
issue_response = await client.post(
f"/api/v1/projects/{project_id}/issues",
json={
"project_id": project_id,
"title": "Issue to Add",
},
headers={"Authorization": f"Bearer {user_token}"},
)
issue_id = issue_response.json()["id"]
# Add issue to sprint
response = await client.post(
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/issues",
params={"issue_id": issue_id},
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_200_OK
async def test_add_nonexistent_issue_to_sprint(
self, client, user_token, test_project
):
"""Test adding a nonexistent issue to a sprint."""
project_id = test_project["id"]
start_date = date.today()
end_date = start_date + timedelta(days=14)
# Create sprint
sprint_response = await client.post(
f"/api/v1/projects/{project_id}/sprints",
json={
"project_id": project_id,
"name": "Target Sprint",
"number": 1,
"start_date": start_date.isoformat(),
"end_date": end_date.isoformat(),
},
headers={"Authorization": f"Bearer {user_token}"},
)
sprint_id = sprint_response.json()["id"]
fake_issue_id = str(uuid.uuid4())
# Try to add nonexistent issue
response = await client.post(
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/issues",
params={"issue_id": fake_issue_id},
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
@pytest.mark.asyncio
class TestSprintCrossProjectValidation:
"""Tests for cross-project validation (IDOR prevention)."""
async def test_sprint_not_in_project(self, client, user_token):
"""Test accessing sprint that exists but not in the specified project."""
# Create two projects
project1 = await client.post(
"/api/v1/projects",
json={"name": "Project 1 Sprint", "slug": "project-1-sprint-idor"},
headers={"Authorization": f"Bearer {user_token}"},
)
project2 = await client.post(
"/api/v1/projects",
json={"name": "Project 2 Sprint", "slug": "project-2-sprint-idor"},
headers={"Authorization": f"Bearer {user_token}"},
)
project1_id = project1.json()["id"]
project2_id = project2.json()["id"]
start_date = date.today()
end_date = start_date + timedelta(days=14)
# Create sprint in project1
sprint_response = await client.post(
f"/api/v1/projects/{project1_id}/sprints",
json={
"project_id": project1_id,
"name": "Project 1 Sprint",
"number": 1,
"start_date": start_date.isoformat(),
"end_date": end_date.isoformat(),
},
headers={"Authorization": f"Bearer {user_token}"},
)
sprint_id = sprint_response.json()["id"]
# Try to access sprint via project2 (IDOR attempt)
response = await client.get(
f"/api/v1/projects/{project2_id}/sprints/{sprint_id}",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
async def test_update_sprint_wrong_project(self, client, user_token):
"""Test updating sprint through wrong project."""
# Create two projects
project1 = await client.post(
"/api/v1/projects",
json={"name": "Project A Sprint", "slug": "project-a-sprint-idor"},
headers={"Authorization": f"Bearer {user_token}"},
)
project2 = await client.post(
"/api/v1/projects",
json={"name": "Project B Sprint", "slug": "project-b-sprint-idor"},
headers={"Authorization": f"Bearer {user_token}"},
)
project1_id = project1.json()["id"]
project2_id = project2.json()["id"]
start_date = date.today()
end_date = start_date + timedelta(days=14)
# Create sprint in project1
sprint_response = await client.post(
f"/api/v1/projects/{project1_id}/sprints",
json={
"project_id": project1_id,
"name": "Project A Sprint",
"number": 1,
"start_date": start_date.isoformat(),
"end_date": end_date.isoformat(),
},
headers={"Authorization": f"Bearer {user_token}"},
)
sprint_id = sprint_response.json()["id"]
# Try to update sprint via project2 (IDOR attempt)
response = await client.patch(
f"/api/v1/projects/{project2_id}/sprints/{sprint_id}",
json={"name": "Hacked Name"},
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND
async def test_start_sprint_wrong_project(self, client, user_token):
"""Test starting sprint through wrong project."""
# Create two projects
project1 = await client.post(
"/api/v1/projects",
json={"name": "Project X Sprint", "slug": "project-x-sprint-idor"},
headers={"Authorization": f"Bearer {user_token}"},
)
project2 = await client.post(
"/api/v1/projects",
json={"name": "Project Y Sprint", "slug": "project-y-sprint-idor"},
headers={"Authorization": f"Bearer {user_token}"},
)
project1_id = project1.json()["id"]
project2_id = project2.json()["id"]
start_date = date.today()
end_date = start_date + timedelta(days=14)
# Create sprint in project1
sprint_response = await client.post(
f"/api/v1/projects/{project1_id}/sprints",
json={
"project_id": project1_id,
"name": "Project X Sprint",
"number": 1,
"start_date": start_date.isoformat(),
"end_date": end_date.isoformat(),
},
headers={"Authorization": f"Bearer {user_token}"},
)
sprint_id = sprint_response.json()["id"]
# Try to start sprint via project2 (IDOR attempt)
response = await client.post(
f"/api/v1/projects/{project2_id}/sprints/{sprint_id}/start",
headers={"Authorization": f"Bearer {user_token}"},
)
assert response.status_code == status.HTTP_404_NOT_FOUND