forked from cardosofelipe/fast-next-template
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user