# tests/crud/syndarix/test_sprint_crud.py """ Tests for Sprint CRUD operations. """ import uuid from datetime import date, timedelta import pytest from app.crud.syndarix import sprint as sprint_crud from app.models.syndarix import SprintStatus from app.schemas.syndarix import SprintCreate, SprintUpdate class TestSprintCreate: """Tests for sprint creation.""" @pytest.mark.asyncio async def test_create_sprint_success(self, async_test_db, test_project_crud): """Test successfully creating a sprint.""" _test_engine, AsyncTestingSessionLocal = async_test_db today = date.today() async with AsyncTestingSessionLocal() as session: sprint_data = SprintCreate( project_id=test_project_crud.id, name="Sprint 1", number=1, goal="Complete initial setup", start_date=today, end_date=today + timedelta(days=14), status=SprintStatus.PLANNED, planned_points=21, ) result = await sprint_crud.create(session, obj_in=sprint_data) assert result.id is not None assert result.name == "Sprint 1" assert result.number == 1 assert result.goal == "Complete initial setup" assert result.status == SprintStatus.PLANNED assert result.planned_points == 21 @pytest.mark.asyncio async def test_create_sprint_minimal(self, async_test_db, test_project_crud): """Test creating sprint with minimal fields.""" _test_engine, AsyncTestingSessionLocal = async_test_db today = date.today() async with AsyncTestingSessionLocal() as session: sprint_data = SprintCreate( project_id=test_project_crud.id, name="Minimal Sprint", number=1, start_date=today, end_date=today + timedelta(days=14), ) result = await sprint_crud.create(session, obj_in=sprint_data) assert result.name == "Minimal Sprint" assert result.status == SprintStatus.PLANNED # Default assert result.goal is None assert result.planned_points is None class TestSprintRead: """Tests for sprint read operations.""" @pytest.mark.asyncio async def test_get_sprint_by_id(self, async_test_db, test_sprint_crud): """Test getting sprint by ID.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: result = await sprint_crud.get(session, id=str(test_sprint_crud.id)) assert result is not None assert result.id == test_sprint_crud.id @pytest.mark.asyncio async def test_get_sprint_by_id_not_found(self, async_test_db): """Test getting non-existent sprint returns None.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: result = await sprint_crud.get(session, id=str(uuid.uuid4())) assert result is None @pytest.mark.asyncio async def test_get_with_details(self, async_test_db, test_sprint_crud): """Test getting sprint with related details.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: result = await sprint_crud.get_with_details( session, sprint_id=test_sprint_crud.id, ) assert result is not None assert result["sprint"].id == test_sprint_crud.id assert result["project_name"] is not None assert "issue_count" in result assert "open_issues" in result assert "completed_issues" in result class TestSprintUpdate: """Tests for sprint update operations.""" @pytest.mark.asyncio async def test_update_sprint_basic_fields(self, async_test_db, test_sprint_crud): """Test updating basic sprint fields.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: sprint = await sprint_crud.get(session, id=str(test_sprint_crud.id)) update_data = SprintUpdate( name="Updated Sprint Name", goal="Updated goal", ) result = await sprint_crud.update(session, db_obj=sprint, obj_in=update_data) assert result.name == "Updated Sprint Name" assert result.goal == "Updated goal" @pytest.mark.asyncio async def test_update_sprint_dates(self, async_test_db, test_sprint_crud): """Test updating sprint dates.""" _test_engine, AsyncTestingSessionLocal = async_test_db today = date.today() async with AsyncTestingSessionLocal() as session: sprint = await sprint_crud.get(session, id=str(test_sprint_crud.id)) update_data = SprintUpdate( start_date=today + timedelta(days=1), end_date=today + timedelta(days=21), ) result = await sprint_crud.update(session, db_obj=sprint, obj_in=update_data) assert result.start_date == today + timedelta(days=1) assert result.end_date == today + timedelta(days=21) class TestSprintLifecycle: """Tests for sprint lifecycle operations.""" @pytest.mark.asyncio async def test_start_sprint(self, async_test_db, test_sprint_crud): """Test starting a planned sprint.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: result = await sprint_crud.start_sprint( session, sprint_id=test_sprint_crud.id, ) assert result is not None assert result.status == SprintStatus.ACTIVE @pytest.mark.asyncio async def test_start_sprint_with_custom_date(self, async_test_db, test_project_crud): """Test starting sprint with custom start date.""" _test_engine, AsyncTestingSessionLocal = async_test_db today = date.today() # Create a planned sprint async with AsyncTestingSessionLocal() as session: sprint_data = SprintCreate( project_id=test_project_crud.id, name="Start Date Sprint", number=10, start_date=today, end_date=today + timedelta(days=14), status=SprintStatus.PLANNED, ) created = await sprint_crud.create(session, obj_in=sprint_data) sprint_id = created.id # Start with custom date new_start = today + timedelta(days=2) async with AsyncTestingSessionLocal() as session: result = await sprint_crud.start_sprint( session, sprint_id=sprint_id, start_date=new_start, ) assert result.status == SprintStatus.ACTIVE assert result.start_date == new_start @pytest.mark.asyncio async def test_start_sprint_already_active_fails(self, async_test_db, test_project_crud): """Test starting an already active sprint raises ValueError.""" _test_engine, AsyncTestingSessionLocal = async_test_db today = date.today() # Create and start a sprint async with AsyncTestingSessionLocal() as session: sprint_data = SprintCreate( project_id=test_project_crud.id, name="Already Active Sprint", number=20, start_date=today, end_date=today + timedelta(days=14), status=SprintStatus.ACTIVE, ) created = await sprint_crud.create(session, obj_in=sprint_data) sprint_id = created.id # Try to start again async with AsyncTestingSessionLocal() as session: with pytest.raises(ValueError) as exc_info: await sprint_crud.start_sprint(session, sprint_id=sprint_id) assert "cannot start sprint" in str(exc_info.value).lower() @pytest.mark.asyncio async def test_complete_sprint(self, async_test_db, test_project_crud): """Test completing an active sprint.""" _test_engine, AsyncTestingSessionLocal = async_test_db today = date.today() # Create an active sprint async with AsyncTestingSessionLocal() as session: sprint_data = SprintCreate( project_id=test_project_crud.id, name="Complete Me Sprint", number=30, start_date=today - timedelta(days=14), end_date=today, status=SprintStatus.ACTIVE, planned_points=21, ) created = await sprint_crud.create(session, obj_in=sprint_data) sprint_id = created.id # Complete async with AsyncTestingSessionLocal() as session: result = await sprint_crud.complete_sprint(session, sprint_id=sprint_id) assert result is not None assert result.status == SprintStatus.COMPLETED @pytest.mark.asyncio async def test_complete_planned_sprint_fails(self, async_test_db, test_project_crud): """Test completing a planned sprint raises ValueError.""" _test_engine, AsyncTestingSessionLocal = async_test_db today = date.today() async with AsyncTestingSessionLocal() as session: sprint_data = SprintCreate( project_id=test_project_crud.id, name="Planned Sprint", number=40, start_date=today, end_date=today + timedelta(days=14), status=SprintStatus.PLANNED, ) created = await sprint_crud.create(session, obj_in=sprint_data) sprint_id = created.id async with AsyncTestingSessionLocal() as session: with pytest.raises(ValueError) as exc_info: await sprint_crud.complete_sprint(session, sprint_id=sprint_id) assert "cannot complete sprint" in str(exc_info.value).lower() @pytest.mark.asyncio async def test_cancel_sprint(self, async_test_db, test_project_crud): """Test cancelling a sprint.""" _test_engine, AsyncTestingSessionLocal = async_test_db today = date.today() async with AsyncTestingSessionLocal() as session: sprint_data = SprintCreate( project_id=test_project_crud.id, name="Cancel Me Sprint", number=50, start_date=today, end_date=today + timedelta(days=14), status=SprintStatus.ACTIVE, ) created = await sprint_crud.create(session, obj_in=sprint_data) sprint_id = created.id async with AsyncTestingSessionLocal() as session: result = await sprint_crud.cancel_sprint(session, sprint_id=sprint_id) assert result is not None assert result.status == SprintStatus.CANCELLED @pytest.mark.asyncio async def test_cancel_completed_sprint_fails(self, async_test_db, test_project_crud): """Test cancelling a completed sprint raises ValueError.""" _test_engine, AsyncTestingSessionLocal = async_test_db today = date.today() async with AsyncTestingSessionLocal() as session: sprint_data = SprintCreate( project_id=test_project_crud.id, name="Completed Sprint", number=60, start_date=today - timedelta(days=14), end_date=today, status=SprintStatus.COMPLETED, ) created = await sprint_crud.create(session, obj_in=sprint_data) sprint_id = created.id async with AsyncTestingSessionLocal() as session: with pytest.raises(ValueError) as exc_info: await sprint_crud.cancel_sprint(session, sprint_id=sprint_id) assert "cannot cancel sprint" in str(exc_info.value).lower() class TestSprintByProject: """Tests for getting sprints by project.""" @pytest.mark.asyncio async def test_get_by_project(self, async_test_db, test_project_crud, test_sprint_crud): """Test getting sprints by project.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: sprints, total = await sprint_crud.get_by_project( session, project_id=test_project_crud.id, ) assert total >= 1 assert all(s.project_id == test_project_crud.id for s in sprints) @pytest.mark.asyncio async def test_get_by_project_with_status(self, async_test_db, test_project_crud): """Test filtering sprints by status.""" _test_engine, AsyncTestingSessionLocal = async_test_db today = date.today() # Create sprints with different statuses async with AsyncTestingSessionLocal() as session: planned_sprint = SprintCreate( project_id=test_project_crud.id, name="Planned Filter Sprint", number=70, start_date=today, end_date=today + timedelta(days=14), status=SprintStatus.PLANNED, ) await sprint_crud.create(session, obj_in=planned_sprint) active_sprint = SprintCreate( project_id=test_project_crud.id, name="Active Filter Sprint", number=71, start_date=today, end_date=today + timedelta(days=14), status=SprintStatus.ACTIVE, ) await sprint_crud.create(session, obj_in=active_sprint) async with AsyncTestingSessionLocal() as session: sprints, _ = await sprint_crud.get_by_project( session, project_id=test_project_crud.id, status=SprintStatus.ACTIVE, ) assert all(s.status == SprintStatus.ACTIVE for s in sprints) class TestSprintActiveSprint: """Tests for active sprint operations.""" @pytest.mark.asyncio async def test_get_active_sprint(self, async_test_db, test_project_crud): """Test getting active sprint for a project.""" _test_engine, AsyncTestingSessionLocal = async_test_db today = date.today() # Create an active sprint async with AsyncTestingSessionLocal() as session: sprint_data = SprintCreate( project_id=test_project_crud.id, name="Active Sprint", number=80, start_date=today, end_date=today + timedelta(days=14), status=SprintStatus.ACTIVE, ) await sprint_crud.create(session, obj_in=sprint_data) async with AsyncTestingSessionLocal() as session: result = await sprint_crud.get_active_sprint( session, project_id=test_project_crud.id, ) assert result is not None assert result.status == SprintStatus.ACTIVE @pytest.mark.asyncio async def test_get_active_sprint_none(self, async_test_db, test_project_crud): """Test getting active sprint when none exists.""" _test_engine, AsyncTestingSessionLocal = async_test_db # Note: test_sprint_crud has PLANNED status by default async with AsyncTestingSessionLocal() as session: result = await sprint_crud.get_active_sprint( session, project_id=test_project_crud.id, ) # May or may not be None depending on other tests if result is not None: assert result.status == SprintStatus.ACTIVE class TestSprintNextNumber: """Tests for getting next sprint number.""" @pytest.mark.asyncio async def test_get_next_sprint_number(self, async_test_db, test_project_crud): """Test getting next sprint number.""" _test_engine, AsyncTestingSessionLocal = async_test_db today = date.today() # Create sprints with numbers async with AsyncTestingSessionLocal() as session: for i in range(1, 4): sprint_data = SprintCreate( project_id=test_project_crud.id, name=f"Number Sprint {i}", number=i, start_date=today, end_date=today + timedelta(days=14), ) await sprint_crud.create(session, obj_in=sprint_data) async with AsyncTestingSessionLocal() as session: next_number = await sprint_crud.get_next_sprint_number( session, project_id=test_project_crud.id, ) assert next_number >= 4 class TestSprintVelocity: """Tests for sprint velocity operations.""" @pytest.mark.asyncio async def test_get_velocity(self, async_test_db, test_project_crud): """Test getting velocity data for completed sprints.""" _test_engine, AsyncTestingSessionLocal = async_test_db today = date.today() # Create completed sprints with points async with AsyncTestingSessionLocal() as session: for i in range(1, 4): sprint_data = SprintCreate( project_id=test_project_crud.id, name=f"Velocity Sprint {i}", number=100 + i, start_date=today - timedelta(days=14 * i), end_date=today - timedelta(days=14 * (i - 1)), status=SprintStatus.COMPLETED, planned_points=20, completed_points=15 + i, ) await sprint_crud.create(session, obj_in=sprint_data) async with AsyncTestingSessionLocal() as session: velocity_data = await sprint_crud.get_velocity( session, project_id=test_project_crud.id, limit=5, ) assert len(velocity_data) >= 1 for data in velocity_data: assert "sprint_number" in data assert "sprint_name" in data assert "planned_points" in data assert "completed_points" in data assert "velocity" in data class TestSprintWithIssueCounts: """Tests for getting sprints with issue counts.""" @pytest.mark.asyncio async def test_get_sprints_with_issue_counts(self, async_test_db, test_project_crud, test_sprint_crud): """Test getting sprints with issue counts.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: results, total = await sprint_crud.get_sprints_with_issue_counts( session, project_id=test_project_crud.id, ) assert total >= 1 for result in results: assert "sprint" in result assert "issue_count" in result assert "open_issues" in result assert "completed_issues" in result