""" Unit tests for Issue domain services. Tests business logic in issue services with no external dependencies. """ import pytest from datetime import datetime, timedelta, timezone from domain.issues.models import Issue, Label, IssueState from domain.issues.services import IssueStatusService, IssueValidationService from domain.issues.exceptions import IssueValidationError class TestIssueStatusService: """Test business logic in issue status service.""" @pytest.fixture def service(self): return IssueStatusService() def test_determine_kanban_column_for_closed_issue(self, service): # Arrange issue = Issue( number=1, title="Closed Issue", state=IssueState.CLOSED, labels=[], created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc) ) project_info = {"kanban_columns": ["Todo", "In Progress", "Review", "Done"]} # Act column = service.determine_kanban_column(issue, project_info) # Assert assert column == "Done" @pytest.mark.parametrize("status_label,expected_column", [ ("status:in-progress", "In Progress"), ("status:review", "Review"), ("status:blocked", "Blocked"), ("status:ready", "Ready"), ]) def test_determine_kanban_column_based_on_status_labels(self, service, status_label, expected_column): # Arrange issue = Issue( number=1, title="Test Issue", state=IssueState.OPEN, labels=[Label(status_label)], created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc) ) project_info = {"kanban_columns": ["Todo", "In Progress", "Review", "Blocked", "Ready", "Done"]} # Act column = service.determine_kanban_column(issue, project_info) # Assert assert column == expected_column def test_determine_kanban_column_defaults_to_todo(self, service): # Arrange issue = Issue( number=1, title="New Issue", state=IssueState.OPEN, labels=[Label("bug")], # No status label created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc) ) project_info = {"kanban_columns": ["Todo", "In Progress", "Done"]} # Act column = service.determine_kanban_column(issue, project_info) # Assert assert column == "Todo" @pytest.mark.parametrize("priority_label,expected_level", [ ("priority:low", "Low"), ("priority:medium", "Medium"), ("priority:high", "High"), ("priority:critical", "Critical"), ]) def test_extract_priority_info_with_priority_labels(self, service, priority_label, expected_level): # Arrange issue = Issue( number=1, title="Test", state=IssueState.OPEN, labels=[Label(priority_label)], created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc) ) # Act priority_info = service.extract_priority_info(issue) # Assert assert priority_info["level"] == expected_level assert priority_info["label"] == priority_label def test_extract_priority_info_defaults_to_medium(self, service): # Arrange issue = Issue( number=1, title="Test", state=IssueState.OPEN, labels=[Label("bug")], # No priority label created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc) ) # Act priority_info = service.extract_priority_info(issue) # Assert assert priority_info["level"] == "Medium" assert priority_info["label"] is None def test_extract_state_info_for_open_issue(self, service): # Arrange issue = Issue( number=1, title="Test", state=IssueState.OPEN, labels=[Label("status:in-progress")], created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc) ) # Act state_info = service.extract_state_info(issue) # Assert assert state_info["state"] == "open" assert state_info["state_labels"] == ["status:in-progress"] assert state_info["is_closed"] is False assert state_info["closed_at"] is None def test_extract_state_info_for_closed_issue(self, service): # Arrange closed_at = datetime.now(timezone.utc) issue = Issue( number=1, title="Test", state=IssueState.CLOSED, labels=[], created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc), closed_at=closed_at ) # Act state_info = service.extract_state_info(issue) # Assert assert state_info["state"] == "closed" assert state_info["is_closed"] is True assert state_info["closed_at"] == closed_at.isoformat() def test_calculate_issue_age_days(self, service): # Arrange created_at = datetime.now(timezone.utc) - timedelta(days=5) issue = Issue( number=1, title="Test", state=IssueState.OPEN, labels=[], created_at=created_at, updated_at=datetime.now(timezone.utc) ) # Act age_days = service.calculate_issue_age_days(issue) # Assert assert age_days == 5 def test_is_stale_issue_with_old_open_issue(self, service): # Arrange created_at = datetime.now(timezone.utc) - timedelta(days=45) issue = Issue( number=1, title="Test", state=IssueState.OPEN, labels=[], created_at=created_at, updated_at=datetime.now(timezone.utc) ) # Act is_stale = service.is_stale_issue(issue, stale_threshold_days=30) # Assert assert is_stale is True def test_is_stale_issue_with_recent_open_issue(self, service): # Arrange created_at = datetime.now(timezone.utc) - timedelta(days=15) issue = Issue( number=1, title="Test", state=IssueState.OPEN, labels=[], created_at=created_at, updated_at=datetime.now(timezone.utc) ) # Act is_stale = service.is_stale_issue(issue, stale_threshold_days=30) # Assert assert is_stale is False def test_is_stale_issue_with_closed_issue_never_stale(self, service): # Arrange created_at = datetime.now(timezone.utc) - timedelta(days=100) issue = Issue( number=1, title="Test", state=IssueState.CLOSED, labels=[], created_at=created_at, updated_at=datetime.now(timezone.utc), closed_at=datetime.now(timezone.utc) ) # Act is_stale = service.is_stale_issue(issue, stale_threshold_days=30) # Assert assert is_stale is False class TestIssueValidationService: """Test business logic in issue validation service.""" @pytest.fixture def service(self): return IssueValidationService() def test_validate_issue_creation_with_valid_data(self, service): # Arrange title = "Valid Issue Title" labels = ["bug", "priority:high"] # Act & Assert - Should not raise exception service.validate_issue_creation(title, labels) def test_validate_issue_creation_with_empty_title_raises_error(self, service): # Arrange title = "" labels = ["bug"] # Act & Assert with pytest.raises(IssueValidationError) as exc_info: service.validate_issue_creation(title, labels) assert "Issue title cannot be empty" in str(exc_info.value) assert exc_info.value.field == "title" def test_validate_issue_creation_with_whitespace_only_title_raises_error(self, service): # Arrange title = " " labels = ["bug"] # Act & Assert with pytest.raises(IssueValidationError) as exc_info: service.validate_issue_creation(title, labels) assert "Issue title cannot be empty" in str(exc_info.value) def test_validate_issue_creation_with_too_long_title_raises_error(self, service): # Arrange title = "x" * 256 # Too long labels = ["bug"] # Act & Assert with pytest.raises(IssueValidationError) as exc_info: service.validate_issue_creation(title, labels) assert "Issue title cannot exceed 255 characters" in str(exc_info.value) def test_validate_issue_creation_with_multiple_priority_labels_raises_error(self, service): # Arrange title = "Valid Title" labels = ["bug", "priority:high", "priority:low"] # Act & Assert with pytest.raises(IssueValidationError) as exc_info: service.validate_issue_creation(title, labels) assert "Issue cannot have multiple priority labels" in str(exc_info.value) assert exc_info.value.field == "labels" def test_validate_issue_creation_with_multiple_state_labels_raises_error(self, service): # Arrange title = "Valid Title" labels = ["bug", "status:open", "status:in-progress"] # Act & Assert with pytest.raises(IssueValidationError) as exc_info: service.validate_issue_creation(title, labels) assert "Issue cannot have multiple state labels" in str(exc_info.value) def test_validate_title_update_with_valid_title(self, service): # Arrange new_title = "Updated Title" # Act & Assert - Should not raise exception service.validate_title_update(new_title) def test_validate_label_addition_to_issue_without_conflicts(self, service): # Arrange issue = Issue( number=1, title="Test", state=IssueState.OPEN, labels=[Label("bug")], created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc) ) new_label = "enhancement" # Act & Assert - Should not raise exception service.validate_label_addition(issue, new_label) def test_validate_label_addition_with_duplicate_label_raises_error(self, service): # Arrange issue = Issue( number=1, title="Test", state=IssueState.OPEN, labels=[Label("bug")], created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc) ) new_label = "bug" # Act & Assert with pytest.raises(IssueValidationError) as exc_info: service.validate_label_addition(issue, new_label) assert "Issue already has label 'bug'" in str(exc_info.value) def test_validate_label_addition_with_conflicting_priority_raises_error(self, service): # Arrange issue = Issue( number=1, title="Test", state=IssueState.OPEN, labels=[Label("priority:high")], created_at=datetime.now(timezone.utc), updated_at=datetime.now(timezone.utc) ) new_label = "priority:low" # Act & Assert with pytest.raises(IssueValidationError) as exc_info: service.validate_label_addition(issue, new_label) assert "Issue already has priority label" in str(exc_info.value) assert "Cannot add 'priority:low'" in str(exc_info.value)