""" Unit tests for Project domain models. Tests pure business logic with no external dependencies. """ import pytest from datetime import datetime, timedelta, timezone from domain.projects.models import Project, Milestone, ProjectState from domain.projects.exceptions import MilestoneError class TestMilestone: """Test Milestone entity.""" def test_milestone_creation(self): # Arrange due_date = datetime.now(timezone.utc) + timedelta(days=30) # Act milestone = Milestone( id=1, title="Version 1.0", description="First release", due_date=due_date, state="open", open_issues=5, closed_issues=3 ) # Assert assert milestone.id == 1 assert milestone.title == "Version 1.0" assert milestone.description == "First release" assert milestone.due_date == due_date assert milestone.state == "open" assert milestone.open_issues == 5 assert milestone.closed_issues == 3 def test_completion_percentage_calculation(self): # Arrange milestone = Milestone( id=1, title="Test", description=None, due_date=None, state="open", open_issues=2, closed_issues=8 ) # Act percentage = milestone.completion_percentage # Assert assert percentage == 80.0 # 8/(2+8) * 100 def test_completion_percentage_with_no_issues(self): # Arrange milestone = Milestone( id=1, title="Test", description=None, due_date=None, state="open", open_issues=0, closed_issues=0 ) # Act percentage = milestone.completion_percentage # Assert assert percentage == 0.0 def test_total_issues_property(self): # Arrange milestone = Milestone( id=1, title="Test", description=None, due_date=None, state="open", open_issues=3, closed_issues=7 ) # Act & Assert assert milestone.total_issues == 10 def test_is_overdue_with_past_due_date(self): # Arrange past_date = datetime.now(timezone.utc) - timedelta(days=1) milestone = Milestone( id=1, title="Test", description=None, due_date=past_date, state="open", open_issues=1, closed_issues=0 ) # Act & Assert assert milestone.is_overdue() is True def test_is_overdue_with_future_due_date(self): # Arrange future_date = datetime.now(timezone.utc) + timedelta(days=1) milestone = Milestone( id=1, title="Test", description=None, due_date=future_date, state="open", open_issues=1, closed_issues=0 ) # Act & Assert assert milestone.is_overdue() is False def test_is_overdue_with_no_due_date(self): # Arrange milestone = Milestone( id=1, title="Test", description=None, due_date=None, state="open", open_issues=1, closed_issues=0 ) # Act & Assert assert milestone.is_overdue() is False def test_is_overdue_with_closed_milestone(self): # Arrange past_date = datetime.now(timezone.utc) - timedelta(days=1) milestone = Milestone( id=1, title="Test", description=None, due_date=past_date, state="closed", open_issues=0, closed_issues=5 ) # Act & Assert assert milestone.is_overdue() is False def test_is_completed_with_closed_state(self): # Arrange milestone = Milestone( id=1, title="Test", description=None, due_date=None, state="closed", open_issues=0, closed_issues=5 ) # Act & Assert assert milestone.is_completed() is True def test_is_completed_with_100_percent_completion(self): # Arrange milestone = Milestone( id=1, title="Test", description=None, due_date=None, state="open", open_issues=0, closed_issues=5 ) # Act & Assert assert milestone.is_completed() is True def test_is_completed_with_partial_completion(self): # Arrange milestone = Milestone( id=1, title="Test", description=None, due_date=None, state="open", open_issues=2, closed_issues=3 ) # Act & Assert assert milestone.is_completed() is False def test_add_issue_increments_open_count(self): # Arrange milestone = Milestone( id=1, title="Test", description=None, due_date=None, state="open", open_issues=3, closed_issues=2 ) # Act milestone.add_issue() # Assert assert milestone.open_issues == 4 assert milestone.closed_issues == 2 def test_close_issue_moves_from_open_to_closed(self): # Arrange milestone = Milestone( id=1, title="Test", description=None, due_date=None, state="open", open_issues=3, closed_issues=2 ) # Act milestone.close_issue() # Assert assert milestone.open_issues == 2 assert milestone.closed_issues == 3 def test_close_issue_with_no_open_issues_raises_error(self): # Arrange milestone = Milestone( id=1, title="Test", description=None, due_date=None, state="open", open_issues=0, closed_issues=5 ) # Act & Assert with pytest.raises(MilestoneError) as exc_info: milestone.close_issue() assert "no open issues" in str(exc_info.value) assert exc_info.value.milestone_id == 1 def test_reopen_issue_moves_from_closed_to_open(self): # Arrange milestone = Milestone( id=1, title="Test", description=None, due_date=None, state="open", open_issues=2, closed_issues=3 ) # Act milestone.reopen_issue() # Assert assert milestone.open_issues == 3 assert milestone.closed_issues == 2 def test_reopen_issue_with_no_closed_issues_raises_error(self): # Arrange milestone = Milestone( id=1, title="Test", description=None, due_date=None, state="open", open_issues=5, closed_issues=0 ) # Act & Assert with pytest.raises(MilestoneError) as exc_info: milestone.reopen_issue() assert "no closed issues" in str(exc_info.value) assert exc_info.value.milestone_id == 1 class TestProject: """Test Project aggregate root.""" def test_project_creation(self): # Arrange created_at = datetime.now(timezone.utc) updated_at = datetime.now(timezone.utc) milestones = [ Milestone(1, "M1", None, None, "open", 2, 1), Milestone(2, "M2", None, None, "closed", 0, 3) ] # Act project = Project( name="Test Project", description="A test project", state=ProjectState.ACTIVE, milestones=milestones, kanban_columns=["Todo", "In Progress", "Done"], created_at=created_at, updated_at=updated_at ) # Assert assert project.name == "Test Project" assert project.description == "A test project" assert project.state == ProjectState.ACTIVE assert len(project.milestones) == 2 assert project.kanban_columns == ["Todo", "In Progress", "Done"] def test_get_active_milestones(self): # Arrange milestones = [ Milestone(1, "M1", None, None, "open", 2, 1), Milestone(2, "M2", None, None, "closed", 0, 3), Milestone(3, "M3", None, None, "open", 1, 0) ] project = Project( name="Test", description="", state=ProjectState.ACTIVE, milestones=milestones, kanban_columns=[], created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc) ) # Act active_milestones = project.get_active_milestones() # Assert assert len(active_milestones) == 2 assert all(m.state == "open" for m in active_milestones) def test_get_completed_milestones(self): # Arrange milestones = [ Milestone(1, "M1", None, None, "open", 2, 1), # Not completed Milestone(2, "M2", None, None, "closed", 0, 3), # Completed (closed) Milestone(3, "M3", None, None, "open", 0, 5) # Completed (100%) ] project = Project( name="Test", description="", state=ProjectState.ACTIVE, milestones=milestones, kanban_columns=[], created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc) ) # Act completed_milestones = project.get_completed_milestones() # Assert assert len(completed_milestones) == 2 def test_get_overdue_milestones(self): # Arrange past_date = datetime.now(timezone.utc) - timedelta(days=1) future_date = datetime.now(timezone.utc) + timedelta(days=1) milestones = [ Milestone(1, "M1", None, past_date, "open", 2, 1), # Overdue Milestone(2, "M2", None, future_date, "open", 1, 0), # Not overdue Milestone(3, "M3", None, None, "open", 1, 0) # No due date ] project = Project( name="Test", description="", state=ProjectState.ACTIVE, milestones=milestones, kanban_columns=[], created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc) ) # Act overdue_milestones = project.get_overdue_milestones() # Assert assert len(overdue_milestones) == 1 assert overdue_milestones[0].id == 1 def test_calculate_overall_progress(self): # Arrange milestones = [ Milestone(1, "M1", None, None, "open", 1, 4), # 80% complete Milestone(2, "M2", None, None, "open", 3, 2) # 40% complete ] project = Project( name="Test", description="", state=ProjectState.ACTIVE, milestones=milestones, kanban_columns=[], created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc) ) # Act progress = project.calculate_overall_progress() # Assert assert progress == 60.0 # (80 + 40) / 2 def test_calculate_overall_progress_with_no_milestones(self): # Arrange project = Project( name="Test", description="", state=ProjectState.ACTIVE, milestones=[], kanban_columns=[], created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc) ) # Act progress = project.calculate_overall_progress() # Assert assert progress == 0.0 def test_get_total_issues(self): # Arrange milestones = [ Milestone(1, "M1", None, None, "open", 2, 3), # 5 total Milestone(2, "M2", None, None, "open", 1, 4) # 5 total ] project = Project( name="Test", description="", state=ProjectState.ACTIVE, milestones=milestones, kanban_columns=[], created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc) ) # Act & Assert assert project.get_total_issues() == 10 assert project.get_total_open_issues() == 3 assert project.get_total_closed_issues() == 7 def test_archive_project(self): # Arrange project = Project( name="Test", description="", state=ProjectState.ACTIVE, milestones=[], kanban_columns=[], created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc) ) # Act project.archive() # Assert assert project.state == ProjectState.ARCHIVED assert project.archived_at is not None def test_activate_project(self): # Arrange project = Project( name="Test", description="", state=ProjectState.ARCHIVED, milestones=[], kanban_columns=[], created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc), archived_at=datetime.now(timezone.utc) ) # Act project.activate() # Assert assert project.state == ProjectState.ACTIVE assert project.archived_at is None def test_add_milestone(self): # Arrange project = Project( name="Test", description="", state=ProjectState.ACTIVE, milestones=[], kanban_columns=[], created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc) ) milestone = Milestone(1, "New Milestone", None, None, "open", 0, 0) # Act project.add_milestone(milestone) # Assert assert len(project.milestones) == 1 assert project.milestones[0] == milestone def test_add_duplicate_milestone_raises_error(self): # Arrange milestone1 = Milestone(1, "Milestone 1", None, None, "open", 0, 0) milestone2 = Milestone(1, "Milestone 2", None, None, "open", 0, 0) # Same ID project = Project( name="Test", description="", state=ProjectState.ACTIVE, milestones=[milestone1], kanban_columns=[], created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc) ) # Act & Assert with pytest.raises(ValueError, match="Milestone with ID 1 already exists"): project.add_milestone(milestone2) def test_remove_milestone(self): # Arrange milestone = Milestone(1, "Milestone", None, None, "open", 0, 0) project = Project( name="Test", description="", state=ProjectState.ACTIVE, milestones=[milestone], kanban_columns=[], created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc) ) # Act project.remove_milestone(1) # Assert assert len(project.milestones) == 0 def test_remove_nonexistent_milestone_raises_error(self): # Arrange project = Project( name="Test", description="", state=ProjectState.ACTIVE, milestones=[], kanban_columns=[], created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc) ) # Act & Assert with pytest.raises(ValueError, match="Milestone with ID 999 not found"): project.remove_milestone(999) def test_get_milestone(self): # Arrange milestone = Milestone(1, "Milestone", None, None, "open", 0, 0) project = Project( name="Test", description="", state=ProjectState.ACTIVE, milestones=[milestone], kanban_columns=[], created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc) ) # Act found_milestone = project.get_milestone(1) # Assert assert found_milestone == milestone def test_get_nonexistent_milestone_returns_none(self): # Arrange project = Project( name="Test", description="", state=ProjectState.ACTIVE, milestones=[], kanban_columns=[], created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc) ) # Act found_milestone = project.get_milestone(999) # Assert assert found_milestone is None