""" Test suite for Core Domain Models. These tests ensure the domain models (Issue, Label, User, etc.) work correctly, including state management, validation, and business logic. """ import pytest from datetime import datetime, timezone from issue_tracker.core.models import ( Issue, Label, User, Milestone, Comment, IssueState, Priority, IssueType, LabelCategories ) @pytest.mark.unit class TestIssueState: """Test IssueState enumeration.""" def test_from_string_standard_states(self): """Test converting standard strings to IssueState.""" assert IssueState.from_string("open") == IssueState.OPEN assert IssueState.from_string("closed") == IssueState.CLOSED assert IssueState.from_string("in_progress") == IssueState.IN_PROGRESS assert IssueState.from_string("blocked") == IssueState.BLOCKED def test_from_string_variants(self): """Test converting variant strings to IssueState.""" assert IssueState.from_string("in-progress") == IssueState.IN_PROGRESS assert IssueState.from_string("progress") == IssueState.IN_PROGRESS assert IssueState.from_string("OPEN") == IssueState.OPEN def test_from_string_unknown_defaults_to_open(self): """Test unknown state strings default to OPEN.""" assert IssueState.from_string("unknown") == IssueState.OPEN assert IssueState.from_string("") == IssueState.OPEN def test_to_backend_string_gitea(self): """Test converting to Gitea backend string.""" assert IssueState.OPEN.to_backend_string("gitea") == "open" assert IssueState.CLOSED.to_backend_string("gitea") == "closed" assert IssueState.IN_PROGRESS.to_backend_string("gitea") == "open" assert IssueState.BLOCKED.to_backend_string("gitea") == "open" def test_to_backend_string_github(self): """Test converting to GitHub backend string.""" assert IssueState.OPEN.to_backend_string("github") == "open" assert IssueState.CLOSED.to_backend_string("github") == "closed" @pytest.mark.unit class TestPriority: """Test Priority enumeration.""" def test_from_label_with_priority_prefix(self): """Test extracting priority from label.""" assert Priority.from_label("priority:low") == Priority.LOW assert Priority.from_label("priority:medium") == Priority.MEDIUM assert Priority.from_label("priority:high") == Priority.HIGH assert Priority.from_label("priority:critical") == Priority.CRITICAL def test_from_label_without_prefix(self): """Test non-priority labels return None.""" assert Priority.from_label("bug") is None assert Priority.from_label("enhancement") is None def test_from_label_invalid_priority(self): """Test invalid priority returns None.""" assert Priority.from_label("priority:invalid") is None @pytest.mark.unit class TestIssueType: """Test IssueType enumeration.""" def test_from_label_standard_types(self): """Test extracting issue type from label.""" assert IssueType.from_label("bug") == IssueType.BUG assert IssueType.from_label("feature") == IssueType.FEATURE assert IssueType.from_label("enhancement") == IssueType.ENHANCEMENT assert IssueType.from_label("task") == IssueType.TASK assert IssueType.from_label("documentation") == IssueType.DOCUMENTATION assert IssueType.from_label("question") == IssueType.QUESTION def test_from_label_case_insensitive(self): """Test case insensitive type matching.""" assert IssueType.from_label("BUG") == IssueType.BUG assert IssueType.from_label("Bug") == IssueType.BUG def test_from_label_invalid_type(self): """Test invalid type returns None.""" assert IssueType.from_label("invalid") is None @pytest.mark.unit class TestLabel: """Test Label model.""" def test_label_creation(self): """Test creating a label.""" label = Label(name="bug", color="red", description="Bug reports") assert label.name == "bug" assert label.color == "red" assert label.description == "Bug reports" def test_label_category_priority(self): """Test priority label categorization.""" label = Label(name="priority:high") assert label.category == "priority" def test_label_category_status(self): """Test status label categorization.""" label = Label(name="status:in-progress") assert label.category == "status" def test_label_category_type_with_prefix(self): """Test type label with prefix categorization.""" label = Label(name="type:bug") assert label.category == "type" def test_label_category_type_without_prefix(self): """Test type label without prefix categorization.""" label = Label(name="bug") assert label.category == "type" label2 = Label(name="feature") assert label2.category == "type" def test_label_category_other(self): """Test other label categorization.""" label = Label(name="good-first-issue") assert label.category == "other" def test_label_priority_property(self): """Test extracting priority from label.""" label = Label(name="priority:high") assert label.priority == Priority.HIGH label2 = Label(name="bug") assert label2.priority is None def test_label_issue_type_property(self): """Test extracting issue type from label.""" label = Label(name="bug") assert label.issue_type == IssueType.BUG label2 = Label(name="priority:high") assert label2.issue_type is None @pytest.mark.unit class TestUser: """Test User model.""" def test_user_creation(self): """Test creating a user.""" user = User( id="user123", username="alice", display_name="Alice Smith", email="alice@example.com" ) assert user.id == "user123" assert user.username == "alice" assert user.display_name == "Alice Smith" assert user.email == "alice@example.com" @pytest.mark.unit class TestMilestone: """Test Milestone model.""" def test_milestone_creation(self): """Test creating a milestone.""" now = datetime.now(timezone.utc) milestone = Milestone( id="m1", title="v1.0", description="First release", state="open", due_date=now, created_at=now, updated_at=now ) assert milestone.id == "m1" assert milestone.title == "v1.0" assert milestone.state == "open" @pytest.mark.unit class TestComment: """Test Comment model.""" def test_comment_creation(self): """Test creating a comment.""" author = User(id="user1", username="alice") now = datetime.now(timezone.utc) comment = Comment( id="c1", body="Great issue!", author=author, created_at=now ) assert comment.id == "c1" assert comment.body == "Great issue!" assert comment.author.username == "alice" @pytest.mark.unit class TestIssueBasics: """Test basic Issue model functionality.""" def test_issue_creation(self): """Test creating an issue.""" now = datetime.now(timezone.utc) issue = Issue( id="issue1", number=1, title="Test Issue", description="Test description", state=IssueState.OPEN, created_at=now, updated_at=now ) assert issue.id == "issue1" assert issue.number == 1 assert issue.title == "Test Issue" assert issue.state == IssueState.OPEN def test_issue_with_labels(self): """Test creating issue with labels.""" now = datetime.now(timezone.utc) labels = [ Label(name="bug", color="red"), Label(name="priority:high", color="orange") ] issue = Issue( id="issue1", number=1, title="Bug", description="Desc", state=IssueState.OPEN, created_at=now, updated_at=now, labels=labels ) assert len(issue.labels) == 2 def test_issue_with_assignees(self): """Test creating issue with assignees.""" now = datetime.now(timezone.utc) assignees = [User(id="u1", username="alice")] issue = Issue( id="issue1", number=1, title="Task", description="Desc", state=IssueState.OPEN, created_at=now, updated_at=now, assignees=assignees ) assert len(issue.assignees) == 1 assert issue.primary_assignee.username == "alice" def test_primary_assignee_when_none(self): """Test primary_assignee returns None when no assignees.""" now = datetime.now(timezone.utc) issue = Issue( id="issue1", number=1, title="Task", description="Desc", state=IssueState.OPEN, created_at=now, updated_at=now ) assert issue.primary_assignee is None @pytest.mark.unit class TestIssueLabelCategorization: """Test Issue label categorization.""" def test_label_categories_property(self): """Test label_categories property organizes labels.""" now = datetime.now(timezone.utc) labels = [ Label(name="bug"), Label(name="priority:high"), Label(name="status:in-review"), Label(name="good-first-issue") ] issue = Issue( id="issue1", number=1, title="Test", description="Desc", state=IssueState.OPEN, created_at=now, updated_at=now, labels=labels ) categories = issue.label_categories assert len(categories.type_labels) == 1 assert len(categories.priority_labels) == 1 assert len(categories.status_labels) == 1 assert len(categories.other_labels) == 1 def test_issue_priority_property(self): """Test issue.priority extracts priority from labels.""" now = datetime.now(timezone.utc) labels = [Label(name="priority:high"), Label(name="bug")] issue = Issue( id="issue1", number=1, title="Test", description="Desc", state=IssueState.OPEN, created_at=now, updated_at=now, labels=labels ) assert issue.priority == Priority.HIGH def test_issue_priority_none_when_no_priority_label(self): """Test issue.priority is None without priority label.""" now = datetime.now(timezone.utc) labels = [Label(name="bug")] issue = Issue( id="issue1", number=1, title="Test", description="Desc", state=IssueState.OPEN, created_at=now, updated_at=now, labels=labels ) assert issue.priority is None def test_issue_type_property(self): """Test issue.issue_type extracts type from labels.""" now = datetime.now(timezone.utc) labels = [Label(name="bug"), Label(name="priority:high")] issue = Issue( id="issue1", number=1, title="Test", description="Desc", state=IssueState.OPEN, created_at=now, updated_at=now, labels=labels ) assert issue.issue_type == IssueType.BUG def test_cache_invalidation(self): """Test label cache is invalidated when labels change.""" now = datetime.now(timezone.utc) issue = Issue( id="issue1", number=1, title="Test", description="Desc", state=IssueState.OPEN, created_at=now, updated_at=now, labels=[Label(name="bug")] ) # Access to populate cache assert issue.issue_type == IssueType.BUG # Manually modify labels issue.labels = [Label(name="feature")] issue.invalidate_cache() # Should reflect new labels assert issue.issue_type == IssueType.FEATURE @pytest.mark.unit class TestIssueStateTransitions: """Test Issue state transition methods.""" def test_close_issue(self): """Test closing an open issue.""" now = datetime.now(timezone.utc) issue = Issue( id="issue1", number=1, title="Test", description="Desc", state=IssueState.OPEN, created_at=now, updated_at=now ) issue.close() assert issue.state == IssueState.CLOSED assert issue.closed_at is not None def test_close_issue_with_custom_timestamp(self): """Test closing issue with custom timestamp.""" now = datetime.now(timezone.utc) issue = Issue( id="issue1", number=1, title="Test", description="Desc", state=IssueState.OPEN, created_at=now, updated_at=now ) custom_time = datetime(2023, 1, 1, 12, 0, 0, tzinfo=timezone.utc) issue.close(closed_at=custom_time) assert issue.closed_at == custom_time def test_close_already_closed_issue_raises_error(self): """Test closing already closed issue raises error.""" now = datetime.now(timezone.utc) issue = Issue( id="issue1", number=1, title="Test", description="Desc", state=IssueState.CLOSED, created_at=now, updated_at=now, closed_at=now ) with pytest.raises(ValueError, match="already closed"): issue.close() def test_reopen_issue(self): """Test reopening a closed issue.""" now = datetime.now(timezone.utc) issue = Issue( id="issue1", number=1, title="Test", description="Desc", state=IssueState.CLOSED, created_at=now, updated_at=now, closed_at=now ) issue.reopen() assert issue.state == IssueState.OPEN assert issue.closed_at is None def test_reopen_open_issue_raises_error(self): """Test reopening non-closed issue raises error.""" now = datetime.now(timezone.utc) issue = Issue( id="issue1", number=1, title="Test", description="Desc", state=IssueState.OPEN, created_at=now, updated_at=now ) with pytest.raises(ValueError, match="not closed"): issue.reopen() @pytest.mark.unit class TestIssueLabelMethods: """Test Issue label manipulation methods.""" def test_add_label(self): """Test adding a label to issue.""" now = datetime.now(timezone.utc) issue = Issue( id="issue1", number=1, title="Test", description="Desc", state=IssueState.OPEN, created_at=now, updated_at=now, labels=[] ) bug_label = Label(name="bug", color="red") issue.add_label(bug_label) assert len(issue.labels) == 1 assert issue.labels[0].name == "bug" def test_add_duplicate_label_ignored(self): """Test adding duplicate label is ignored.""" now = datetime.now(timezone.utc) bug_label = Label(name="bug", color="red") issue = Issue( id="issue1", number=1, title="Test", description="Desc", state=IssueState.OPEN, created_at=now, updated_at=now, labels=[bug_label] ) issue.add_label(bug_label) assert len(issue.labels) == 1 def test_remove_label(self): """Test removing a label from issue.""" now = datetime.now(timezone.utc) issue = Issue( id="issue1", number=1, title="Test", description="Desc", state=IssueState.OPEN, created_at=now, updated_at=now, labels=[Label(name="bug"), Label(name="feature")] ) result = issue.remove_label("bug") assert result is True assert len(issue.labels) == 1 assert issue.labels[0].name == "feature" def test_remove_nonexistent_label_returns_false(self): """Test removing non-existent label returns False.""" now = datetime.now(timezone.utc) issue = Issue( id="issue1", number=1, title="Test", description="Desc", state=IssueState.OPEN, created_at=now, updated_at=now, labels=[Label(name="bug")] ) result = issue.remove_label("nonexistent") assert result is False assert len(issue.labels) == 1 def test_has_label(self): """Test checking if issue has a label.""" now = datetime.now(timezone.utc) issue = Issue( id="issue1", number=1, title="Test", description="Desc", state=IssueState.OPEN, created_at=now, updated_at=now, labels=[Label(name="bug"), Label(name="priority:high")] ) assert issue.has_label("bug") assert issue.has_label("priority:high") assert not issue.has_label("feature") @pytest.mark.unit class TestIssueAssigneeMethods: """Test Issue assignee manipulation methods.""" def test_add_assignee(self): """Test adding an assignee to issue.""" now = datetime.now(timezone.utc) issue = Issue( id="issue1", number=1, title="Test", description="Desc", state=IssueState.OPEN, created_at=now, updated_at=now, assignees=[] ) user = User(id="u1", username="alice") issue.add_assignee(user) assert len(issue.assignees) == 1 assert issue.assignees[0].username == "alice" def test_add_duplicate_assignee_ignored(self): """Test adding duplicate assignee is ignored.""" now = datetime.now(timezone.utc) user = User(id="u1", username="alice") issue = Issue( id="issue1", number=1, title="Test", description="Desc", state=IssueState.OPEN, created_at=now, updated_at=now, assignees=[user] ) issue.add_assignee(user) assert len(issue.assignees) == 1 def test_remove_assignee(self): """Test removing an assignee from issue.""" now = datetime.now(timezone.utc) user1 = User(id="u1", username="alice") user2 = User(id="u2", username="bob") issue = Issue( id="issue1", number=1, title="Test", description="Desc", state=IssueState.OPEN, created_at=now, updated_at=now, assignees=[user1, user2] ) result = issue.remove_assignee("u1") assert result is True assert len(issue.assignees) == 1 assert issue.assignees[0].username == "bob" def test_remove_nonexistent_assignee_returns_false(self): """Test removing non-existent assignee returns False.""" now = datetime.now(timezone.utc) user = User(id="u1", username="alice") issue = Issue( id="issue1", number=1, title="Test", description="Desc", state=IssueState.OPEN, created_at=now, updated_at=now, assignees=[user] ) result = issue.remove_assignee("nonexistent") assert result is False assert len(issue.assignees) == 1 @pytest.mark.unit class TestIssueCommentMethods: """Test Issue comment methods.""" def test_add_comment(self): """Test adding a comment to issue.""" now = datetime.now(timezone.utc) issue = Issue( id="issue1", number=1, title="Test", description="Desc", state=IssueState.OPEN, created_at=now, updated_at=now, comments=[] ) author = User(id="u1", username="alice") comment = Comment(id="c1", body="Great!", author=author, created_at=now) issue.add_comment(comment) assert len(issue.comments) == 1 assert issue.comments[0].body == "Great!" @pytest.mark.unit class TestIssueSerialization: """Test Issue serialization.""" def test_to_dict(self): """Test converting issue to dictionary.""" now = datetime.now(timezone.utc) labels = [Label(name="bug", color="red")] assignees = [User(id="u1", username="alice", display_name="Alice")] milestone = Milestone(id="m1", title="v1.0") issue = Issue( id="issue1", number=1, title="Test Issue", description="Test desc", state=IssueState.OPEN, created_at=now, updated_at=now, labels=labels, assignees=assignees, milestone=milestone, backend_id="gitea-123", backend_type="gitea" ) issue_dict = issue.to_dict() assert issue_dict['id'] == "issue1" assert issue_dict['number'] == 1 assert issue_dict['title'] == "Test Issue" assert issue_dict['state'] == "open" assert issue_dict['backend_id'] == "gitea-123" assert issue_dict['backend_type'] == "gitea" assert len(issue_dict['labels']) == 1 assert len(issue_dict['assignees']) == 1 assert issue_dict['milestone']['id'] == "m1"