- Created complete domain layer with pure business logic - Implemented Issue domain models with 48 passing tests - Implemented Project domain models with 31 passing tests - Added domain services for complex business operations - Established clean separation between domain, application, and infrastructure - All 250 tests passing with no breaking changes 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
368 lines
11 KiB
Python
368 lines
11 KiB
Python
"""
|
|
Unit tests for Issue domain services.
|
|
|
|
Tests business logic in issue services with no external dependencies.
|
|
"""
|
|
|
|
import pytest
|
|
from datetime import datetime, timedelta
|
|
|
|
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.utcnow(),
|
|
updated_at=datetime.utcnow()
|
|
)
|
|
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.utcnow(),
|
|
updated_at=datetime.utcnow()
|
|
)
|
|
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.utcnow(),
|
|
updated_at=datetime.utcnow()
|
|
)
|
|
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.utcnow(),
|
|
updated_at=datetime.utcnow()
|
|
)
|
|
|
|
# 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.utcnow(),
|
|
updated_at=datetime.utcnow()
|
|
)
|
|
|
|
# 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.utcnow(),
|
|
updated_at=datetime.utcnow()
|
|
)
|
|
|
|
# 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.utcnow()
|
|
issue = Issue(
|
|
number=1,
|
|
title="Test",
|
|
state=IssueState.CLOSED,
|
|
labels=[],
|
|
created_at=datetime.utcnow(),
|
|
updated_at=datetime.utcnow(),
|
|
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.utcnow() - timedelta(days=5)
|
|
issue = Issue(
|
|
number=1,
|
|
title="Test",
|
|
state=IssueState.OPEN,
|
|
labels=[],
|
|
created_at=created_at,
|
|
updated_at=datetime.utcnow()
|
|
)
|
|
|
|
# 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.utcnow() - timedelta(days=45)
|
|
issue = Issue(
|
|
number=1,
|
|
title="Test",
|
|
state=IssueState.OPEN,
|
|
labels=[],
|
|
created_at=created_at,
|
|
updated_at=datetime.utcnow()
|
|
)
|
|
|
|
# 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.utcnow() - timedelta(days=15)
|
|
issue = Issue(
|
|
number=1,
|
|
title="Test",
|
|
state=IssueState.OPEN,
|
|
labels=[],
|
|
created_at=created_at,
|
|
updated_at=datetime.utcnow()
|
|
)
|
|
|
|
# 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.utcnow() - timedelta(days=100)
|
|
issue = Issue(
|
|
number=1,
|
|
title="Test",
|
|
state=IssueState.CLOSED,
|
|
labels=[],
|
|
created_at=created_at,
|
|
updated_at=datetime.utcnow(),
|
|
closed_at=datetime.utcnow()
|
|
)
|
|
|
|
# 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.utcnow(),
|
|
updated_at=datetime.utcnow()
|
|
)
|
|
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.utcnow(),
|
|
updated_at=datetime.utcnow()
|
|
)
|
|
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.utcnow(),
|
|
updated_at=datetime.utcnow()
|
|
)
|
|
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) |