From 04c939d4c266454a25913014f1833326db4e4a8a Mon Sep 17 00:00:00 2001 From: Felipe Cardoso Date: Wed, 31 Dec 2025 13:20:09 +0100 Subject: [PATCH] test(sprints): add comprehensive API route tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../tests/api/routes/syndarix/test_sprints.py | 821 ++++++++++++++++++ 1 file changed, 821 insertions(+) create mode 100644 backend/tests/api/routes/syndarix/test_sprints.py diff --git a/backend/tests/api/routes/syndarix/test_sprints.py b/backend/tests/api/routes/syndarix/test_sprints.py new file mode 100644 index 0000000..0b33bf3 --- /dev/null +++ b/backend/tests/api/routes/syndarix/test_sprints.py @@ -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