# tests/models/syndarix/test_sprint.py """ Unit tests for the Sprint model. """ import uuid from datetime import date, datetime, timedelta import pytest from app.models.syndarix import ( Project, Sprint, SprintStatus, ) class TestSprintModel: """Tests for Sprint model creation and fields.""" def test_create_sprint_with_required_fields(self, db_session): """Test creating a sprint with only required fields.""" project = Project( id=uuid.uuid4(), name="Sprint Project", slug="sprint-project", ) db_session.add(project) db_session.commit() today = date.today() sprint = Sprint( id=uuid.uuid4(), project_id=project.id, name="Sprint 1", number=1, start_date=today, end_date=today + timedelta(days=14), ) db_session.add(sprint) db_session.commit() retrieved = db_session.query(Sprint).filter_by(name="Sprint 1").first() assert retrieved is not None assert retrieved.name == "Sprint 1" assert retrieved.number == 1 assert retrieved.start_date == today assert retrieved.end_date == today + timedelta(days=14) assert retrieved.status == SprintStatus.PLANNED # Default assert retrieved.goal is None assert retrieved.planned_points is None assert retrieved.velocity is None def test_create_sprint_with_all_fields(self, db_session): """Test creating a sprint with all optional fields.""" project = Project( id=uuid.uuid4(), name="Full Sprint Project", slug="full-sprint-project", ) db_session.add(project) db_session.commit() today = date.today() sprint_id = uuid.uuid4() sprint = Sprint( id=sprint_id, project_id=project.id, name="Full Sprint", number=5, goal="Complete all authentication features", start_date=today, end_date=today + timedelta(days=14), status=SprintStatus.ACTIVE, planned_points=34, velocity=21, ) db_session.add(sprint) db_session.commit() retrieved = db_session.query(Sprint).filter_by(id=sprint_id).first() assert retrieved.name == "Full Sprint" assert retrieved.number == 5 assert retrieved.goal == "Complete all authentication features" assert retrieved.status == SprintStatus.ACTIVE assert retrieved.planned_points == 34 assert retrieved.velocity == 21 def test_sprint_timestamps(self, db_session): """Test that timestamps are automatically set.""" project = Project( id=uuid.uuid4(), name="Timestamp Sprint Project", slug="timestamp-sprint-project", ) db_session.add(project) db_session.commit() today = date.today() sprint = Sprint( id=uuid.uuid4(), project_id=project.id, name="Timestamp Sprint", number=1, start_date=today, end_date=today + timedelta(days=14), ) db_session.add(sprint) db_session.commit() assert isinstance(sprint.created_at, datetime) assert isinstance(sprint.updated_at, datetime) def test_sprint_string_representation(self, db_session): """Test the string representation of a sprint.""" project = Project( id=uuid.uuid4(), name="Repr Sprint Project", slug="repr-sprint-project" ) db_session.add(project) db_session.commit() today = date.today() sprint = Sprint( id=uuid.uuid4(), project_id=project.id, name="Sprint Alpha", number=3, start_date=today, end_date=today + timedelta(days=14), status=SprintStatus.ACTIVE, ) repr_str = repr(sprint) assert "Sprint Alpha" in repr_str assert "#3" in repr_str assert str(project.id) in repr_str assert "active" in repr_str class TestSprintStatus: """Tests for Sprint status field.""" def test_all_sprint_statuses(self, db_session): """Test that all sprint statuses can be stored.""" project = Project( id=uuid.uuid4(), name="Status Sprint Project", slug="status-sprint-project" ) db_session.add(project) db_session.commit() today = date.today() for idx, status in enumerate(SprintStatus): sprint = Sprint( id=uuid.uuid4(), project_id=project.id, name=f"Sprint {status.value}", number=idx + 1, start_date=today, end_date=today + timedelta(days=14), status=status, ) db_session.add(sprint) db_session.commit() retrieved = db_session.query(Sprint).filter_by(id=sprint.id).first() assert retrieved.status == status class TestSprintLifecycle: """Tests for Sprint lifecycle operations.""" def test_start_sprint(self, db_session): """Test starting a planned sprint.""" project = Project( id=uuid.uuid4(), name="Start Sprint Project", slug="start-sprint-project" ) db_session.add(project) db_session.commit() today = date.today() sprint = Sprint( id=uuid.uuid4(), project_id=project.id, name="Sprint to Start", number=1, start_date=today, end_date=today + timedelta(days=14), status=SprintStatus.PLANNED, ) db_session.add(sprint) db_session.commit() # Start the sprint sprint.status = SprintStatus.ACTIVE sprint.planned_points = 21 db_session.commit() retrieved = db_session.query(Sprint).filter_by(name="Sprint to Start").first() assert retrieved.status == SprintStatus.ACTIVE assert retrieved.planned_points == 21 def test_complete_sprint(self, db_session): """Test completing an active sprint.""" project = Project( id=uuid.uuid4(), name="Complete Sprint Project", slug="complete-sprint-project", ) db_session.add(project) db_session.commit() today = date.today() sprint = Sprint( id=uuid.uuid4(), project_id=project.id, name="Sprint to Complete", number=1, start_date=today - timedelta(days=14), end_date=today, status=SprintStatus.ACTIVE, planned_points=21, ) db_session.add(sprint) db_session.commit() # Complete the sprint sprint.status = SprintStatus.COMPLETED sprint.velocity = 18 db_session.commit() retrieved = ( db_session.query(Sprint).filter_by(name="Sprint to Complete").first() ) assert retrieved.status == SprintStatus.COMPLETED assert retrieved.velocity == 18 def test_cancel_sprint(self, db_session): """Test cancelling a sprint.""" project = Project( id=uuid.uuid4(), name="Cancel Sprint Project", slug="cancel-sprint-project" ) db_session.add(project) db_session.commit() today = date.today() sprint = Sprint( id=uuid.uuid4(), project_id=project.id, name="Sprint to Cancel", number=1, start_date=today, end_date=today + timedelta(days=14), status=SprintStatus.ACTIVE, planned_points=21, ) db_session.add(sprint) db_session.commit() # Cancel the sprint sprint.status = SprintStatus.CANCELLED db_session.commit() retrieved = db_session.query(Sprint).filter_by(name="Sprint to Cancel").first() assert retrieved.status == SprintStatus.CANCELLED class TestSprintDates: """Tests for Sprint date fields.""" def test_sprint_date_range(self, db_session): """Test storing sprint date range.""" project = Project( id=uuid.uuid4(), name="Date Range Project", slug="date-range-project" ) db_session.add(project) db_session.commit() start = date(2024, 1, 1) end = date(2024, 1, 14) sprint = Sprint( id=uuid.uuid4(), project_id=project.id, name="Date Range Sprint", number=1, start_date=start, end_date=end, ) db_session.add(sprint) db_session.commit() retrieved = db_session.query(Sprint).filter_by(name="Date Range Sprint").first() assert retrieved.start_date == start assert retrieved.end_date == end def test_one_day_sprint(self, db_session): """Test creating a one-day sprint.""" project = Project( id=uuid.uuid4(), name="One Day Project", slug="one-day-project" ) db_session.add(project) db_session.commit() today = date.today() sprint = Sprint( id=uuid.uuid4(), project_id=project.id, name="One Day Sprint", number=1, start_date=today, end_date=today, # Same day ) db_session.add(sprint) db_session.commit() retrieved = db_session.query(Sprint).filter_by(name="One Day Sprint").first() assert retrieved.start_date == retrieved.end_date def test_long_sprint(self, db_session): """Test creating a long sprint (e.g., 4 weeks).""" project = Project( id=uuid.uuid4(), name="Long Sprint Project", slug="long-sprint-project" ) db_session.add(project) db_session.commit() today = date.today() sprint = Sprint( id=uuid.uuid4(), project_id=project.id, name="Long Sprint", number=1, start_date=today, end_date=today + timedelta(days=28), # 4 weeks ) db_session.add(sprint) db_session.commit() retrieved = db_session.query(Sprint).filter_by(name="Long Sprint").first() delta = retrieved.end_date - retrieved.start_date assert delta.days == 28 class TestSprintPoints: """Tests for Sprint story points fields.""" def test_sprint_with_zero_points(self, db_session): """Test sprint with zero planned points.""" project = Project( id=uuid.uuid4(), name="Zero Points Project", slug="zero-points-project" ) db_session.add(project) db_session.commit() today = date.today() sprint = Sprint( id=uuid.uuid4(), project_id=project.id, name="Zero Points Sprint", number=1, start_date=today, end_date=today + timedelta(days=14), planned_points=0, velocity=0, ) db_session.add(sprint) db_session.commit() retrieved = ( db_session.query(Sprint).filter_by(name="Zero Points Sprint").first() ) assert retrieved.planned_points == 0 assert retrieved.velocity == 0 def test_sprint_velocity_calculation(self, db_session): """Test that we can calculate velocity from points.""" project = Project( id=uuid.uuid4(), name="Velocity Project", slug="velocity-project" ) db_session.add(project) db_session.commit() today = date.today() sprint = Sprint( id=uuid.uuid4(), project_id=project.id, name="Velocity Sprint", number=1, start_date=today, end_date=today + timedelta(days=14), status=SprintStatus.COMPLETED, planned_points=21, velocity=18, ) db_session.add(sprint) db_session.commit() retrieved = db_session.query(Sprint).filter_by(name="Velocity Sprint").first() # Calculate completion ratio from velocity completion_ratio = retrieved.velocity / retrieved.planned_points assert completion_ratio == pytest.approx(18 / 21, rel=0.01) def test_sprint_overdelivery(self, db_session): """Test sprint where completed > planned (stretch goals).""" project = Project( id=uuid.uuid4(), name="Overdelivery Project", slug="overdelivery-project" ) db_session.add(project) db_session.commit() today = date.today() sprint = Sprint( id=uuid.uuid4(), project_id=project.id, name="Overdelivery Sprint", number=1, start_date=today, end_date=today + timedelta(days=14), status=SprintStatus.COMPLETED, planned_points=20, velocity=25, # Completed more than planned ) db_session.add(sprint) db_session.commit() retrieved = ( db_session.query(Sprint).filter_by(name="Overdelivery Sprint").first() ) assert retrieved.velocity > retrieved.planned_points class TestSprintNumber: """Tests for Sprint number field.""" def test_sequential_sprint_numbers(self, db_session): """Test creating sprints with sequential numbers.""" project = Project( id=uuid.uuid4(), name="Sequential Project", slug="sequential-project" ) db_session.add(project) db_session.commit() today = date.today() for i in range(1, 6): sprint = Sprint( id=uuid.uuid4(), project_id=project.id, name=f"Sprint {i}", number=i, start_date=today + timedelta(days=(i - 1) * 14), end_date=today + timedelta(days=i * 14 - 1), ) db_session.add(sprint) db_session.commit() sprints = ( db_session.query(Sprint) .filter_by(project_id=project.id) .order_by(Sprint.number) .all() ) assert len(sprints) == 5 for i, sprint in enumerate(sprints, 1): assert sprint.number == i def test_large_sprint_number(self, db_session): """Test sprint with large number (e.g., long-running project).""" project = Project( id=uuid.uuid4(), name="Large Number Project", slug="large-number-project" ) db_session.add(project) db_session.commit() today = date.today() sprint = Sprint( id=uuid.uuid4(), project_id=project.id, name="Sprint 100", number=100, start_date=today, end_date=today + timedelta(days=14), ) db_session.add(sprint) db_session.commit() retrieved = db_session.query(Sprint).filter_by(name="Sprint 100").first() assert retrieved.number == 100 class TestSprintUpdate: """Tests for Sprint update operations.""" def test_update_sprint_goal(self, db_session): """Test updating sprint goal.""" project = Project( id=uuid.uuid4(), name="Update Goal Project", slug="update-goal-project" ) db_session.add(project) db_session.commit() today = date.today() sprint = Sprint( id=uuid.uuid4(), project_id=project.id, name="Update Goal Sprint", number=1, start_date=today, end_date=today + timedelta(days=14), goal="Original goal", ) db_session.add(sprint) db_session.commit() original_created_at = sprint.created_at sprint.goal = "Updated goal with more detail" db_session.commit() retrieved = ( db_session.query(Sprint).filter_by(name="Update Goal Sprint").first() ) assert retrieved.goal == "Updated goal with more detail" assert retrieved.created_at == original_created_at assert retrieved.updated_at > original_created_at def test_update_sprint_dates(self, db_session): """Test updating sprint dates.""" project = Project( id=uuid.uuid4(), name="Update Dates Project", slug="update-dates-project" ) db_session.add(project) db_session.commit() today = date.today() sprint = Sprint( id=uuid.uuid4(), project_id=project.id, name="Update Dates Sprint", number=1, start_date=today, end_date=today + timedelta(days=14), ) db_session.add(sprint) db_session.commit() # Extend sprint by a week sprint.end_date = today + timedelta(days=21) db_session.commit() retrieved = ( db_session.query(Sprint).filter_by(name="Update Dates Sprint").first() ) delta = retrieved.end_date - retrieved.start_date assert delta.days == 21