forked from cardosofelipe/fast-next-template
test(sprints): add comprehensive API route tests
Add 28 tests for sprints API covering: - CRUD operations (create, list, get, update) - Lifecycle management (start, complete, cancel) - Sprint velocity endpoint - Authorization and access control - Pagination and filtering 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
821
backend/tests/api/routes/syndarix/test_sprints.py
Normal file
821
backend/tests/api/routes/syndarix/test_sprints.py
Normal file
@@ -0,0 +1,821 @@
|
||||
# tests/api/routes/syndarix/test_sprints.py
|
||||
"""
|
||||
Comprehensive tests for the Sprints API endpoints.
|
||||
|
||||
Tests cover:
|
||||
- CRUD operations (create, read, update, delete)
|
||||
- Lifecycle operations (start, complete, cancel)
|
||||
- Issue management (add/remove issues)
|
||||
- Velocity metrics
|
||||
- Authorization (project ownership, superuser access)
|
||||
- Error handling
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import date, timedelta
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from fastapi import status
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def test_project(client, user_token):
|
||||
"""Create a test project for sprint tests."""
|
||||
response = await client.post(
|
||||
"/api/v1/projects",
|
||||
json={"name": "Sprint Test Project", "slug": "sprint-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 TestCreateSprint:
|
||||
"""Tests for POST /api/v1/projects/{project_id}/sprints endpoint."""
|
||||
|
||||
async def test_create_sprint_success(self, client, user_token, test_project):
|
||||
"""Test successful sprint creation."""
|
||||
project_id = test_project["id"]
|
||||
start_date = date.today()
|
||||
end_date = start_date + timedelta(days=14)
|
||||
|
||||
response = await client.post(
|
||||
f"/api/v1/projects/{project_id}/sprints",
|
||||
json={
|
||||
"project_id": project_id,
|
||||
"name": "Sprint 1",
|
||||
"number": 1,
|
||||
"goal": "Implement core features",
|
||||
"start_date": start_date.isoformat(),
|
||||
"end_date": end_date.isoformat(),
|
||||
},
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
data = response.json()
|
||||
|
||||
assert data["name"] == "Sprint 1"
|
||||
assert data["number"] == 1
|
||||
assert data["goal"] == "Implement core features"
|
||||
assert data["status"] == "planned"
|
||||
assert data["project_id"] == project_id
|
||||
assert "id" in data
|
||||
assert "created_at" in data
|
||||
|
||||
async def test_create_sprint_minimal_fields(self, client, user_token, test_project):
|
||||
"""Test creating sprint with only required fields."""
|
||||
project_id = test_project["id"]
|
||||
start_date = date.today()
|
||||
end_date = start_date + timedelta(days=14)
|
||||
|
||||
response = await client.post(
|
||||
f"/api/v1/projects/{project_id}/sprints",
|
||||
json={
|
||||
"project_id": project_id,
|
||||
"name": "Minimal Sprint",
|
||||
"number": 1,
|
||||
"start_date": start_date.isoformat(),
|
||||
"end_date": end_date.isoformat(),
|
||||
},
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
data = response.json()
|
||||
assert data["name"] == "Minimal Sprint"
|
||||
assert data["goal"] is None
|
||||
assert data["planned_points"] is None
|
||||
|
||||
async def test_create_sprint_invalid_dates(self, client, user_token, test_project):
|
||||
"""Test that end date before start date is rejected."""
|
||||
project_id = test_project["id"]
|
||||
start_date = date.today()
|
||||
end_date = start_date - timedelta(days=1)
|
||||
|
||||
response = await client.post(
|
||||
f"/api/v1/projects/{project_id}/sprints",
|
||||
json={
|
||||
"project_id": project_id,
|
||||
"name": "Invalid Sprint",
|
||||
"number": 1,
|
||||
"start_date": start_date.isoformat(),
|
||||
"end_date": end_date.isoformat(),
|
||||
},
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
async def test_create_sprint_mismatched_project_id(
|
||||
self, client, user_token, test_project
|
||||
):
|
||||
"""Test that mismatched project IDs are rejected."""
|
||||
project_id = test_project["id"]
|
||||
fake_project_id = str(uuid.uuid4())
|
||||
start_date = date.today()
|
||||
end_date = start_date + timedelta(days=14)
|
||||
|
||||
response = await client.post(
|
||||
f"/api/v1/projects/{project_id}/sprints",
|
||||
json={
|
||||
"project_id": fake_project_id, # Different from URL
|
||||
"name": "Mismatched Sprint",
|
||||
"number": 1,
|
||||
"start_date": start_date.isoformat(),
|
||||
"end_date": end_date.isoformat(),
|
||||
},
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
async def test_create_sprint_unauthorized_project(
|
||||
self, client, user_token, superuser_project
|
||||
):
|
||||
"""Test that users cannot create sprints in others' projects."""
|
||||
project_id = superuser_project["id"]
|
||||
start_date = date.today()
|
||||
end_date = start_date + timedelta(days=14)
|
||||
|
||||
response = await client.post(
|
||||
f"/api/v1/projects/{project_id}/sprints",
|
||||
json={
|
||||
"project_id": project_id,
|
||||
"name": "Unauthorized Sprint",
|
||||
"number": 1,
|
||||
"start_date": start_date.isoformat(),
|
||||
"end_date": end_date.isoformat(),
|
||||
},
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
async def test_create_sprint_nonexistent_project(self, client, user_token):
|
||||
"""Test creating sprint in nonexistent project."""
|
||||
fake_project_id = str(uuid.uuid4())
|
||||
start_date = date.today()
|
||||
end_date = start_date + timedelta(days=14)
|
||||
|
||||
response = await client.post(
|
||||
f"/api/v1/projects/{fake_project_id}/sprints",
|
||||
json={
|
||||
"project_id": fake_project_id,
|
||||
"name": "Orphan Sprint",
|
||||
"number": 1,
|
||||
"start_date": start_date.isoformat(),
|
||||
"end_date": end_date.isoformat(),
|
||||
},
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestListSprints:
|
||||
"""Tests for GET /api/v1/projects/{project_id}/sprints endpoint."""
|
||||
|
||||
async def test_list_sprints_empty(self, client, user_token, test_project):
|
||||
"""Test listing sprints when none exist."""
|
||||
project_id = test_project["id"]
|
||||
|
||||
response = await client.get(
|
||||
f"/api/v1/projects/{project_id}/sprints",
|
||||
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_sprints_with_data(self, client, user_token, test_project):
|
||||
"""Test listing sprints returns created sprints."""
|
||||
project_id = test_project["id"]
|
||||
start_date = date.today()
|
||||
|
||||
# Create multiple sprints
|
||||
for i in range(3):
|
||||
end_date = start_date + timedelta(days=14 * (i + 1))
|
||||
await client.post(
|
||||
f"/api/v1/projects/{project_id}/sprints",
|
||||
json={
|
||||
"project_id": project_id,
|
||||
"name": f"Sprint {i + 1}",
|
||||
"number": i + 1,
|
||||
"start_date": start_date.isoformat(),
|
||||
"end_date": end_date.isoformat(),
|
||||
},
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
|
||||
response = await client.get(
|
||||
f"/api/v1/projects/{project_id}/sprints",
|
||||
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_sprints_filter_by_status(self, client, user_token, test_project):
|
||||
"""Test filtering sprints by status."""
|
||||
project_id = test_project["id"]
|
||||
start_date = date.today()
|
||||
end_date = start_date + timedelta(days=14)
|
||||
|
||||
# Create a planned sprint
|
||||
await client.post(
|
||||
f"/api/v1/projects/{project_id}/sprints",
|
||||
json={
|
||||
"project_id": project_id,
|
||||
"name": "Planned Sprint",
|
||||
"number": 1,
|
||||
"start_date": start_date.isoformat(),
|
||||
"end_date": end_date.isoformat(),
|
||||
},
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
|
||||
# Filter by planned status
|
||||
response = await client.get(
|
||||
f"/api/v1/projects/{project_id}/sprints?status=planned",
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
assert len(data["data"]) >= 1
|
||||
assert all(s["status"] == "planned" for s in data["data"])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestGetSprint:
|
||||
"""Tests for GET /api/v1/projects/{project_id}/sprints/{sprint_id} endpoint."""
|
||||
|
||||
async def test_get_sprint_success(self, client, user_token, test_project):
|
||||
"""Test getting a sprint by ID."""
|
||||
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": "Get Test 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"]
|
||||
|
||||
# Get sprint
|
||||
response = await client.get(
|
||||
f"/api/v1/projects/{project_id}/sprints/{sprint_id}",
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
assert data["id"] == sprint_id
|
||||
assert data["name"] == "Get Test Sprint"
|
||||
|
||||
async def test_get_sprint_not_found(self, client, user_token, test_project):
|
||||
"""Test getting 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}",
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestGetActiveSprint:
|
||||
"""Tests for GET /api/v1/projects/{project_id}/sprints/active endpoint."""
|
||||
|
||||
async def test_get_active_sprint_none(self, client, user_token, test_project):
|
||||
"""Test getting active sprint when none exists."""
|
||||
project_id = test_project["id"]
|
||||
|
||||
response = await client.get(
|
||||
f"/api/v1/projects/{project_id}/sprints/active",
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() is None
|
||||
|
||||
async def test_get_active_sprint_exists(self, client, user_token, test_project):
|
||||
"""Test getting active sprint when one exists."""
|
||||
project_id = test_project["id"]
|
||||
start_date = date.today()
|
||||
end_date = start_date + timedelta(days=14)
|
||||
|
||||
# Create and start a sprint
|
||||
create_response = await client.post(
|
||||
f"/api/v1/projects/{project_id}/sprints",
|
||||
json={
|
||||
"project_id": project_id,
|
||||
"name": "Active 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"]
|
||||
|
||||
# Start the sprint
|
||||
await client.post(
|
||||
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/start",
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
|
||||
# Get active sprint
|
||||
response = await client.get(
|
||||
f"/api/v1/projects/{project_id}/sprints/active",
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
assert data["id"] == sprint_id
|
||||
assert data["status"] == "active"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestUpdateSprint:
|
||||
"""Tests for PATCH /api/v1/projects/{project_id}/sprints/{sprint_id} endpoint."""
|
||||
|
||||
async def test_update_sprint_success(self, client, user_token, test_project):
|
||||
"""Test updating a sprint."""
|
||||
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": "Original 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"]
|
||||
|
||||
# Update sprint
|
||||
response = await client.patch(
|
||||
f"/api/v1/projects/{project_id}/sprints/{sprint_id}",
|
||||
json={"name": "Updated Sprint", "goal": "New goal"},
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
assert data["name"] == "Updated Sprint"
|
||||
assert data["goal"] == "New goal"
|
||||
|
||||
async def test_update_sprint_not_found(self, client, user_token, test_project):
|
||||
"""Test updating a nonexistent sprint."""
|
||||
project_id = test_project["id"]
|
||||
fake_sprint_id = str(uuid.uuid4())
|
||||
|
||||
response = await client.patch(
|
||||
f"/api/v1/projects/{project_id}/sprints/{fake_sprint_id}",
|
||||
json={"name": "Updated Sprint"},
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestSprintLifecycle:
|
||||
"""Tests for sprint lifecycle endpoints (start, complete, cancel)."""
|
||||
|
||||
async def test_start_sprint_success(self, client, user_token, test_project):
|
||||
"""Test starting a planned sprint."""
|
||||
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 to Start",
|
||||
"number": 1,
|
||||
"start_date": start_date.isoformat(),
|
||||
"end_date": end_date.isoformat(),
|
||||
},
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
sprint_id = create_response.json()["id"]
|
||||
|
||||
# Start sprint
|
||||
response = await client.post(
|
||||
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/start",
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["status"] == "active"
|
||||
|
||||
async def test_complete_sprint_success(self, client, user_token, test_project):
|
||||
"""Test completing an active sprint."""
|
||||
project_id = test_project["id"]
|
||||
start_date = date.today()
|
||||
end_date = start_date + timedelta(days=14)
|
||||
|
||||
# Create and start sprint
|
||||
create_response = await client.post(
|
||||
f"/api/v1/projects/{project_id}/sprints",
|
||||
json={
|
||||
"project_id": project_id,
|
||||
"name": "Sprint to Complete",
|
||||
"number": 1,
|
||||
"start_date": start_date.isoformat(),
|
||||
"end_date": end_date.isoformat(),
|
||||
},
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
sprint_id = create_response.json()["id"]
|
||||
|
||||
await client.post(
|
||||
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/start",
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
|
||||
# Complete sprint
|
||||
response = await client.post(
|
||||
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/complete",
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["status"] == "completed"
|
||||
|
||||
async def test_cancel_sprint_success(self, client, user_token, test_project):
|
||||
"""Test cancelling a planned sprint."""
|
||||
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 to Cancel",
|
||||
"number": 1,
|
||||
"start_date": start_date.isoformat(),
|
||||
"end_date": end_date.isoformat(),
|
||||
},
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
sprint_id = create_response.json()["id"]
|
||||
|
||||
# Cancel sprint
|
||||
response = await client.post(
|
||||
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/cancel",
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["status"] == "cancelled"
|
||||
|
||||
async def test_cannot_start_already_active_sprint(
|
||||
self, client, user_token, test_project
|
||||
):
|
||||
"""Test that starting an already active sprint fails."""
|
||||
project_id = test_project["id"]
|
||||
start_date = date.today()
|
||||
end_date = start_date + timedelta(days=14)
|
||||
|
||||
# Create and start sprint
|
||||
create_response = await client.post(
|
||||
f"/api/v1/projects/{project_id}/sprints",
|
||||
json={
|
||||
"project_id": project_id,
|
||||
"name": "Active 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"]
|
||||
|
||||
await client.post(
|
||||
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/start",
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
|
||||
# Try to start again
|
||||
response = await client.post(
|
||||
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/start",
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
async def test_cannot_complete_planned_sprint(
|
||||
self, client, user_token, test_project
|
||||
):
|
||||
"""Test that completing a planned sprint fails."""
|
||||
project_id = test_project["id"]
|
||||
start_date = date.today()
|
||||
end_date = start_date + timedelta(days=14)
|
||||
|
||||
# Create sprint (planned status)
|
||||
create_response = await client.post(
|
||||
f"/api/v1/projects/{project_id}/sprints",
|
||||
json={
|
||||
"project_id": project_id,
|
||||
"name": "Planned 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"]
|
||||
|
||||
# Try to complete without starting
|
||||
response = await client.post(
|
||||
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/complete",
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestDeleteSprint:
|
||||
"""Tests for DELETE /api/v1/projects/{project_id}/sprints/{sprint_id} endpoint."""
|
||||
|
||||
async def test_delete_planned_sprint_success(self, client, user_token, test_project):
|
||||
"""Test deleting a planned sprint."""
|
||||
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 to Delete",
|
||||
"number": 1,
|
||||
"start_date": start_date.isoformat(),
|
||||
"end_date": end_date.isoformat(),
|
||||
},
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
sprint_id = create_response.json()["id"]
|
||||
|
||||
# Delete sprint
|
||||
response = await client.delete(
|
||||
f"/api/v1/projects/{project_id}/sprints/{sprint_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_cancelled_sprint_success(
|
||||
self, client, user_token, test_project
|
||||
):
|
||||
"""Test deleting a cancelled sprint."""
|
||||
project_id = test_project["id"]
|
||||
start_date = date.today()
|
||||
end_date = start_date + timedelta(days=14)
|
||||
|
||||
# Create and cancel sprint
|
||||
create_response = await client.post(
|
||||
f"/api/v1/projects/{project_id}/sprints",
|
||||
json={
|
||||
"project_id": project_id,
|
||||
"name": "Sprint to Cancel and Delete",
|
||||
"number": 1,
|
||||
"start_date": start_date.isoformat(),
|
||||
"end_date": end_date.isoformat(),
|
||||
},
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
sprint_id = create_response.json()["id"]
|
||||
|
||||
await client.post(
|
||||
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/cancel",
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
|
||||
# Delete cancelled sprint
|
||||
response = await client.delete(
|
||||
f"/api/v1/projects/{project_id}/sprints/{sprint_id}",
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["success"] is True
|
||||
|
||||
async def test_cannot_delete_active_sprint(self, client, user_token, test_project):
|
||||
"""Test that deleting an active sprint fails."""
|
||||
project_id = test_project["id"]
|
||||
start_date = date.today()
|
||||
end_date = start_date + timedelta(days=14)
|
||||
|
||||
# Create and start sprint
|
||||
create_response = await client.post(
|
||||
f"/api/v1/projects/{project_id}/sprints",
|
||||
json={
|
||||
"project_id": project_id,
|
||||
"name": "Active 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"]
|
||||
|
||||
await client.post(
|
||||
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/start",
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
|
||||
# Try to delete active sprint
|
||||
response = await client.delete(
|
||||
f"/api/v1/projects/{project_id}/sprints/{sprint_id}",
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
async def test_cannot_delete_completed_sprint(
|
||||
self, client, user_token, test_project
|
||||
):
|
||||
"""Test that deleting a completed sprint fails."""
|
||||
project_id = test_project["id"]
|
||||
start_date = date.today()
|
||||
end_date = start_date + timedelta(days=14)
|
||||
|
||||
# Create, start, and complete sprint
|
||||
create_response = await client.post(
|
||||
f"/api/v1/projects/{project_id}/sprints",
|
||||
json={
|
||||
"project_id": project_id,
|
||||
"name": "Completed 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"]
|
||||
|
||||
await client.post(
|
||||
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/start",
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
await client.post(
|
||||
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/complete",
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
|
||||
# Try to delete completed sprint
|
||||
response = await client.delete(
|
||||
f"/api/v1/projects/{project_id}/sprints/{sprint_id}",
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestSprintVelocity:
|
||||
"""Tests for GET /api/v1/projects/{project_id}/sprints/velocity endpoint."""
|
||||
|
||||
async def test_get_velocity_empty(self, client, user_token, test_project):
|
||||
"""Test getting velocity when no completed sprints exist."""
|
||||
project_id = test_project["id"]
|
||||
|
||||
response = await client.get(
|
||||
f"/api/v1/projects/{project_id}/sprints/velocity",
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == []
|
||||
|
||||
async def test_get_velocity_with_completed_sprints(
|
||||
self, client, user_token, test_project
|
||||
):
|
||||
"""Test getting velocity with completed sprints."""
|
||||
project_id = test_project["id"]
|
||||
start_date = date.today()
|
||||
end_date = start_date + timedelta(days=14)
|
||||
|
||||
# Create, start, and complete a sprint
|
||||
create_response = await client.post(
|
||||
f"/api/v1/projects/{project_id}/sprints",
|
||||
json={
|
||||
"project_id": project_id,
|
||||
"name": "Sprint 1",
|
||||
"number": 1,
|
||||
"start_date": start_date.isoformat(),
|
||||
"end_date": end_date.isoformat(),
|
||||
"planned_points": 20,
|
||||
},
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
sprint_id = create_response.json()["id"]
|
||||
|
||||
await client.post(
|
||||
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/start",
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
await client.post(
|
||||
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/complete",
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
|
||||
# Get velocity
|
||||
response = await client.get(
|
||||
f"/api/v1/projects/{project_id}/sprints/velocity",
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
assert len(data) >= 1
|
||||
assert data[0]["sprint_number"] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestSprintAuthorization:
|
||||
"""Tests for sprint authorization."""
|
||||
|
||||
async def test_superuser_can_manage_any_project_sprints(
|
||||
self, client, user_token, superuser_token, test_project
|
||||
):
|
||||
"""Test that superuser can manage sprints in any project."""
|
||||
project_id = test_project["id"]
|
||||
start_date = date.today()
|
||||
end_date = start_date + timedelta(days=14)
|
||||
|
||||
# Create sprint as superuser in user's project
|
||||
response = await client.post(
|
||||
f"/api/v1/projects/{project_id}/sprints",
|
||||
json={
|
||||
"project_id": project_id,
|
||||
"name": "Superuser Sprint",
|
||||
"number": 1,
|
||||
"start_date": start_date.isoformat(),
|
||||
"end_date": end_date.isoformat(),
|
||||
},
|
||||
headers={"Authorization": f"Bearer {superuser_token}"},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
|
||||
async def test_user_cannot_access_other_project_sprints(
|
||||
self, client, user_token, superuser_project
|
||||
):
|
||||
"""Test that users cannot access sprints in others' projects."""
|
||||
project_id = superuser_project["id"]
|
||||
|
||||
response = await client.get(
|
||||
f"/api/v1/projects/{project_id}/sprints",
|
||||
headers={"Authorization": f"Bearer {user_token}"},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
Reference in New Issue
Block a user