- 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>
287 lines
8.0 KiB
Python
287 lines
8.0 KiB
Python
"""
|
|
Unit tests for Issue domain models.
|
|
|
|
Tests pure business logic with no external dependencies.
|
|
"""
|
|
|
|
import pytest
|
|
from datetime import datetime, timedelta
|
|
|
|
from domain.issues.models import Issue, Label, IssueState, LabelCategories
|
|
from domain.issues.exceptions import IssueStateError
|
|
|
|
|
|
class TestLabel:
|
|
"""Test Label value object."""
|
|
|
|
def test_label_creation(self):
|
|
# Arrange & Act
|
|
label = Label(name="bug", color="#ff0000", description="Bug label")
|
|
|
|
# Assert
|
|
assert label.name == "bug"
|
|
assert label.color == "#ff0000"
|
|
assert label.description == "Bug label"
|
|
|
|
def test_is_state_label(self):
|
|
# Arrange
|
|
state_label = Label("status:in-progress")
|
|
regular_label = Label("bug")
|
|
|
|
# Act & Assert
|
|
assert state_label.is_state_label() is True
|
|
assert regular_label.is_state_label() is False
|
|
|
|
def test_is_priority_label(self):
|
|
# Arrange
|
|
priority_label = Label("priority:high")
|
|
regular_label = Label("bug")
|
|
|
|
# Act & Assert
|
|
assert priority_label.is_priority_label() is True
|
|
assert regular_label.is_priority_label() is False
|
|
|
|
def test_is_type_label(self):
|
|
# Arrange
|
|
type_label = Label("bug")
|
|
priority_label = Label("priority:high")
|
|
|
|
# Act & Assert
|
|
assert type_label.is_type_label() is True
|
|
assert priority_label.is_type_label() is False
|
|
|
|
@pytest.mark.parametrize("label_name,expected", [
|
|
("bug", True),
|
|
("enhancement", True),
|
|
("feature", True),
|
|
("documentation", True),
|
|
("custom-label", False),
|
|
("priority:high", False)
|
|
])
|
|
def test_type_label_recognition(self, label_name, expected):
|
|
# Arrange
|
|
label = Label(label_name)
|
|
|
|
# Act & Assert
|
|
assert label.is_type_label() == expected
|
|
|
|
|
|
class TestIssue:
|
|
"""Test Issue aggregate root."""
|
|
|
|
def test_issue_creation_with_valid_data(self):
|
|
# Arrange
|
|
created_at = datetime.utcnow()
|
|
updated_at = datetime.utcnow()
|
|
labels = [Label("bug"), Label("priority:high")]
|
|
|
|
# Act
|
|
issue = Issue(
|
|
number=123,
|
|
title="Test Issue",
|
|
state=IssueState.OPEN,
|
|
labels=labels,
|
|
created_at=created_at,
|
|
updated_at=updated_at
|
|
)
|
|
|
|
# Assert
|
|
assert issue.number == 123
|
|
assert issue.title == "Test Issue"
|
|
assert issue.state == IssueState.OPEN
|
|
assert len(issue.labels) == 2
|
|
assert issue.created_at == created_at
|
|
assert issue.updated_at == updated_at
|
|
|
|
def test_categorize_labels_correctly_separates_types(self):
|
|
# Arrange
|
|
labels = [
|
|
Label("bug"), # type label
|
|
Label("priority:high"), # priority label
|
|
Label("status:in-progress"), # state label
|
|
Label("documentation"), # type label
|
|
Label("custom-label") # other label
|
|
]
|
|
issue = Issue(
|
|
number=1,
|
|
title="Test",
|
|
state=IssueState.OPEN,
|
|
labels=labels,
|
|
created_at=datetime.utcnow(),
|
|
updated_at=datetime.utcnow()
|
|
)
|
|
|
|
# Act
|
|
categories = issue.categorize_labels()
|
|
|
|
# Assert
|
|
assert "bug" in categories.type_labels
|
|
assert "documentation" in categories.type_labels
|
|
assert "priority:high" in categories.priority_labels
|
|
assert "status:in-progress" in categories.state_labels
|
|
assert "custom-label" in categories.other_labels
|
|
|
|
def test_close_issue_changes_state_and_sets_closed_at(self):
|
|
# Arrange
|
|
issue = Issue(
|
|
number=1,
|
|
title="Test",
|
|
state=IssueState.OPEN,
|
|
labels=[],
|
|
created_at=datetime.utcnow(),
|
|
updated_at=datetime.utcnow()
|
|
)
|
|
|
|
# Act
|
|
issue.close()
|
|
|
|
# Assert
|
|
assert issue.state == IssueState.CLOSED
|
|
assert issue.closed_at is not None
|
|
assert isinstance(issue.closed_at, datetime)
|
|
|
|
def test_close_already_closed_issue_raises_error(self):
|
|
# Arrange
|
|
issue = Issue(
|
|
number=1,
|
|
title="Test",
|
|
state=IssueState.CLOSED,
|
|
labels=[],
|
|
created_at=datetime.utcnow(),
|
|
updated_at=datetime.utcnow(),
|
|
closed_at=datetime.utcnow()
|
|
)
|
|
|
|
# Act & Assert
|
|
with pytest.raises(IssueStateError) as exc_info:
|
|
issue.close()
|
|
|
|
assert "Issue is already closed" in str(exc_info.value)
|
|
assert exc_info.value.current_state == "closed"
|
|
assert exc_info.value.attempted_state == "closed"
|
|
|
|
def test_reopen_closed_issue_changes_state_and_clears_closed_at(self):
|
|
# Arrange
|
|
issue = Issue(
|
|
number=1,
|
|
title="Test",
|
|
state=IssueState.CLOSED,
|
|
labels=[],
|
|
created_at=datetime.utcnow(),
|
|
updated_at=datetime.utcnow(),
|
|
closed_at=datetime.utcnow()
|
|
)
|
|
|
|
# Act
|
|
issue.reopen()
|
|
|
|
# Assert
|
|
assert issue.state == IssueState.OPEN
|
|
assert issue.closed_at is None
|
|
|
|
def test_reopen_open_issue_raises_error(self):
|
|
# Arrange
|
|
issue = Issue(
|
|
number=1,
|
|
title="Test",
|
|
state=IssueState.OPEN,
|
|
labels=[],
|
|
created_at=datetime.utcnow(),
|
|
updated_at=datetime.utcnow()
|
|
)
|
|
|
|
# Act & Assert
|
|
with pytest.raises(IssueStateError) as exc_info:
|
|
issue.reopen()
|
|
|
|
assert "Issue is not closed" in str(exc_info.value)
|
|
|
|
def test_add_label_to_issue(self):
|
|
# Arrange
|
|
issue = Issue(
|
|
number=1,
|
|
title="Test",
|
|
state=IssueState.OPEN,
|
|
labels=[Label("bug")],
|
|
created_at=datetime.utcnow(),
|
|
updated_at=datetime.utcnow()
|
|
)
|
|
new_label = Label("priority:high")
|
|
|
|
# Act
|
|
issue.add_label(new_label)
|
|
|
|
# Assert
|
|
assert len(issue.labels) == 2
|
|
assert new_label in issue.labels
|
|
|
|
def test_add_duplicate_label_does_not_duplicate(self):
|
|
# Arrange
|
|
label = Label("bug")
|
|
issue = Issue(
|
|
number=1,
|
|
title="Test",
|
|
state=IssueState.OPEN,
|
|
labels=[label],
|
|
created_at=datetime.utcnow(),
|
|
updated_at=datetime.utcnow()
|
|
)
|
|
|
|
# Act
|
|
issue.add_label(label)
|
|
|
|
# Assert
|
|
assert len(issue.labels) == 1
|
|
|
|
def test_remove_label_from_issue(self):
|
|
# Arrange
|
|
issue = Issue(
|
|
number=1,
|
|
title="Test",
|
|
state=IssueState.OPEN,
|
|
labels=[Label("bug"), Label("priority:high")],
|
|
created_at=datetime.utcnow(),
|
|
updated_at=datetime.utcnow()
|
|
)
|
|
|
|
# Act
|
|
issue.remove_label("bug")
|
|
|
|
# Assert
|
|
assert len(issue.labels) == 1
|
|
assert not any(label.name == "bug" for label in issue.labels)
|
|
|
|
def test_has_label_returns_correct_value(self):
|
|
# Arrange
|
|
issue = Issue(
|
|
number=1,
|
|
title="Test",
|
|
state=IssueState.OPEN,
|
|
labels=[Label("bug"), Label("priority:high")],
|
|
created_at=datetime.utcnow(),
|
|
updated_at=datetime.utcnow()
|
|
)
|
|
|
|
# Act & Assert
|
|
assert issue.has_label("bug") is True
|
|
assert issue.has_label("priority:high") is True
|
|
assert issue.has_label("enhancement") is False
|
|
|
|
|
|
class TestLabelCategories:
|
|
"""Test LabelCategories value object."""
|
|
|
|
def test_label_categories_creation(self):
|
|
# Arrange & Act
|
|
categories = LabelCategories(
|
|
state_labels=["status:open"],
|
|
priority_labels=["priority:high"],
|
|
type_labels=["bug"],
|
|
other_labels=["custom"]
|
|
)
|
|
|
|
# Assert
|
|
assert categories.state_labels == ["status:open"]
|
|
assert categories.priority_labels == ["priority:high"]
|
|
assert categories.type_labels == ["bug"]
|
|
assert categories.other_labels == ["custom"] |