generated from coulomb/repo-seed
feat: transform to agent coordination platform with comprehensive documentation
Transform Issue Facade from a universal CLI tool into an agent coordination platform with comprehensive documentation and enhanced capabilities for autonomous coding agents. Major Changes: - Complete README rewrite focusing on agent-driven coordination - New comprehensive documentation (AGENT_INTEGRATION.md, CLAUDE.md, ROADMAP.md) - Capability integration setup with CAPABILITY.yaml and integration scripts - Enhanced Makefile with local development targets for easier workflows Bug Fixes: - Fix schema initialization using executescript() for multi-line SQL support - Disable FTS5 triggers due to compatibility issues (documented for future re-enablement) Features: - Enhanced CLI list command with full parameter passthrough - New examples directory with agent integration patterns - New comprehensive test suite (test_core_models.py, test_local_backend.py) Code Quality: - Remove @cached_property decorators for Label properties (simplification) - Clean up test organization (removed old test_gitea_integration.py) This milestone establishes Issue Facade as a production-ready coordination layer for multi-agent software development, with clear integration paths and comprehensive developer documentation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
602
tests/test_core_models.py
Normal file
602
tests/test_core_models.py
Normal file
@@ -0,0 +1,602 @@
|
||||
"""
|
||||
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"
|
||||
@@ -1,531 +0,0 @@
|
||||
"""
|
||||
Tests for Gitea Issue/Milestone/Label Management Integration
|
||||
|
||||
This test suite provides comprehensive coverage of Gitea API operations for
|
||||
issue tracking, including issues, milestones, and labels.
|
||||
|
||||
The issue-facade capability provides a unified interface to various issue
|
||||
tracking backends, including Gitea. This test suite covers the complete
|
||||
Gitea API integration layer.
|
||||
|
||||
Test Coverage:
|
||||
- **GiteaConfig**: Configuration and API URL generation
|
||||
- **IssuesClient**: Full issue CRUD operations, labels, milestones
|
||||
- **MilestonesClient**: Milestone creation and management
|
||||
- **LabelsClient**: Label operations
|
||||
- **GiteaClient**: Main client facade
|
||||
- **Error Handling**: Error propagation and handling
|
||||
- **Integration Patterns**: API consistency and compatibility
|
||||
|
||||
Current Status: SKIPPED - Tests need updating for issue-facade architecture
|
||||
Related Code: capabilities/issue-facade/issue_tracker/backends/gitea/
|
||||
|
||||
Note: These tests were originally written for a different Gitea client
|
||||
architecture. They need to be updated to work with the issue-facade backend
|
||||
pattern (see test_gitea_backend.py for the current backend tests).
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, MagicMock, patch
|
||||
from datetime import datetime
|
||||
|
||||
# Skip all tests - need to update for issue-facade architecture
|
||||
# These tests are for a different Gitea client pattern than currently implemented
|
||||
pytestmark = pytest.mark.skip(
|
||||
reason="Tests need updating for issue-facade backend architecture. "
|
||||
"See test_gitea_backend.py for current Gitea backend tests."
|
||||
)
|
||||
|
||||
|
||||
class TestGiteaConfig:
|
||||
"""Test GiteaConfig functionality."""
|
||||
|
||||
def test_config_creation(self):
|
||||
"""Test basic config creation."""
|
||||
config = GiteaConfig(
|
||||
gitea_url="https://gitea.example.com",
|
||||
repo_owner="test_owner",
|
||||
repo_name="test_repo",
|
||||
auth_token="test_token"
|
||||
)
|
||||
|
||||
assert config.gitea_url == "https://gitea.example.com"
|
||||
assert config.repo_owner == "test_owner"
|
||||
assert config.repo_name == "test_repo"
|
||||
assert config.auth_token == "test_token"
|
||||
|
||||
def test_api_url_properties(self):
|
||||
"""Test API URL property generation."""
|
||||
config = GiteaConfig(
|
||||
gitea_url="https://gitea.example.com",
|
||||
repo_owner="test_owner",
|
||||
repo_name="test_repo"
|
||||
)
|
||||
|
||||
assert config.base_api_url == "https://gitea.example.com/api/v1"
|
||||
assert config.repo_api_url == "https://gitea.example.com/api/v1/repos/test_owner/test_repo"
|
||||
assert config.issues_api_url == "https://gitea.example.com/api/v1/repos/test_owner/test_repo/issues"
|
||||
|
||||
@patch('gitea.config.subprocess.run')
|
||||
def test_from_git_repository(self, mock_run):
|
||||
"""Test config creation from git repository."""
|
||||
mock_run.return_value = Mock(
|
||||
stdout="https://gitea.example.com/owner/repo.git",
|
||||
returncode=0
|
||||
)
|
||||
|
||||
config = GiteaConfig.from_git_repository()
|
||||
|
||||
assert config.gitea_url == "https://gitea.example.com"
|
||||
assert config.repo_owner == "owner"
|
||||
assert config.repo_name == "repo"
|
||||
|
||||
def test_config_validation(self):
|
||||
"""Test config validation."""
|
||||
# Valid config should not raise
|
||||
config = GiteaConfig(
|
||||
gitea_url="https://gitea.example.com",
|
||||
repo_owner="owner",
|
||||
repo_name="repo"
|
||||
)
|
||||
config.validate() # Should not raise
|
||||
|
||||
# Invalid URL should raise
|
||||
invalid_config = GiteaConfig(
|
||||
gitea_url="invalid-url",
|
||||
repo_owner="owner",
|
||||
repo_name="repo"
|
||||
)
|
||||
with pytest.raises(Exception):
|
||||
invalid_config.validate()
|
||||
|
||||
|
||||
class TestIssuesClient:
|
||||
"""Test IssuesClient functionality."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.mock_api = Mock()
|
||||
self.client = IssuesClient(self.mock_api)
|
||||
|
||||
# Mock issue for responses
|
||||
self.mock_issue = Mock(spec=Issue)
|
||||
self.mock_issue.number = 1
|
||||
self.mock_issue.title = "Test Issue"
|
||||
self.mock_issue.body = "Test body"
|
||||
self.mock_issue.state = "open"
|
||||
self.mock_issue.html_url = "https://gitea.example.com/owner/repo/issues/1"
|
||||
self.mock_issue.created_at = datetime(2023, 1, 1, 12, 0, 0)
|
||||
self.mock_issue.updated_at = datetime(2023, 1, 1, 12, 0, 0)
|
||||
self.mock_issue.assignee = None
|
||||
self.mock_issue.labels = []
|
||||
self.mock_issue.milestone = None
|
||||
|
||||
def test_get_issue(self):
|
||||
"""Test getting a single issue."""
|
||||
self.mock_api.get_issue.return_value = self.mock_issue
|
||||
|
||||
result = self.client.get(1)
|
||||
|
||||
assert result == self.mock_issue
|
||||
self.mock_api.get_issue.assert_called_once_with(1)
|
||||
|
||||
def test_list_issues(self):
|
||||
"""Test listing issues."""
|
||||
self.mock_api.list_issues.return_value = [self.mock_issue]
|
||||
|
||||
result = self.client.list()
|
||||
|
||||
assert result == [self.mock_issue]
|
||||
self.mock_api.list_issues.assert_called_once_with("all", 1, 50)
|
||||
|
||||
def test_list_issues_with_filters(self):
|
||||
"""Test listing issues with filters."""
|
||||
self.mock_api.list_issues.return_value = [self.mock_issue]
|
||||
|
||||
result = self.client.list(state="open", page=2, per_page=25)
|
||||
|
||||
assert result == [self.mock_issue]
|
||||
self.mock_api.list_issues.assert_called_once_with("open", 2, 25)
|
||||
|
||||
def test_create_issue(self):
|
||||
"""Test creating an issue."""
|
||||
self.mock_api.create_issue.return_value = self.mock_issue
|
||||
|
||||
result = self.client.create("Test Title", "Test Body")
|
||||
|
||||
assert result == self.mock_issue
|
||||
self.mock_api.create_issue.assert_called_once()
|
||||
|
||||
def test_create_issue_with_options(self):
|
||||
"""Test creating an issue with optional fields."""
|
||||
self.mock_api.create_issue.return_value = self.mock_issue
|
||||
|
||||
result = self.client.create(
|
||||
"Test Title",
|
||||
"Test Body",
|
||||
assignees=["user1"],
|
||||
milestone=1,
|
||||
labels=["bug", "priority:high"]
|
||||
)
|
||||
|
||||
assert result == self.mock_issue
|
||||
self.mock_api.create_issue.assert_called_once()
|
||||
|
||||
def test_update_issue(self):
|
||||
"""Test updating an issue."""
|
||||
self.mock_api.update_issue.return_value = self.mock_issue
|
||||
|
||||
result = self.client.update(1, title="New Title")
|
||||
|
||||
assert result == self.mock_issue
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_close_issue(self):
|
||||
"""Test closing an issue."""
|
||||
closed_issue = Mock(spec=Issue)
|
||||
closed_issue.state = "closed"
|
||||
self.mock_api.update_issue.return_value = closed_issue
|
||||
|
||||
result = self.client.close(1)
|
||||
|
||||
assert result.state == "closed"
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_reopen_issue(self):
|
||||
"""Test reopening an issue."""
|
||||
opened_issue = Mock(spec=Issue)
|
||||
opened_issue.state = "open"
|
||||
self.mock_api.update_issue.return_value = opened_issue
|
||||
|
||||
result = self.client.reopen(1)
|
||||
|
||||
assert result.state == "open"
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_add_labels(self):
|
||||
"""Test adding labels to an issue."""
|
||||
# Mock getting current issue
|
||||
self.mock_issue.labels = [Mock(name="existing")]
|
||||
self.mock_api.get_issue.return_value = self.mock_issue
|
||||
|
||||
# Mock update result
|
||||
updated_issue = Mock(spec=Issue)
|
||||
updated_issue.labels = [Mock(name="existing"), Mock(name="new")]
|
||||
self.mock_api.update_issue.return_value = updated_issue
|
||||
|
||||
result = self.client.add_labels(1, ["new"])
|
||||
|
||||
assert len(result.labels) == 2
|
||||
self.mock_api.get_issue.assert_called_once_with(1)
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_remove_labels(self):
|
||||
"""Test removing labels from an issue."""
|
||||
# Mock getting current issue
|
||||
label1 = Mock(name="keep")
|
||||
label2 = Mock(name="remove")
|
||||
self.mock_issue.labels = [label1, label2]
|
||||
self.mock_api.get_issue.return_value = self.mock_issue
|
||||
|
||||
# Mock update result
|
||||
updated_issue = Mock(spec=Issue)
|
||||
updated_issue.labels = [label1]
|
||||
self.mock_api.update_issue.return_value = updated_issue
|
||||
|
||||
result = self.client.remove_labels(1, ["remove"])
|
||||
|
||||
assert len(result.labels) == 1
|
||||
self.mock_api.get_issue.assert_called_once_with(1)
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_assign_to_milestone(self):
|
||||
"""Test assigning issue to milestone."""
|
||||
self.mock_api.update_issue.return_value = self.mock_issue
|
||||
|
||||
result = self.client.assign_to_milestone(1, 5)
|
||||
|
||||
assert result == self.mock_issue
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_remove_from_milestone(self):
|
||||
"""Test removing issue from milestone."""
|
||||
self.mock_api.update_issue.return_value = self.mock_issue
|
||||
|
||||
result = self.client.remove_from_milestone(1)
|
||||
|
||||
assert result == self.mock_issue
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_set_labels(self):
|
||||
"""Test replacing all labels on an issue."""
|
||||
self.mock_api.update_issue.return_value = self.mock_issue
|
||||
|
||||
result = self.client.set_labels(1, ["bug", "priority:high"])
|
||||
|
||||
assert result == self.mock_issue
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_update_title(self):
|
||||
"""Test updating only issue title."""
|
||||
self.mock_api.update_issue.return_value = self.mock_issue
|
||||
|
||||
result = self.client.update_title(1, "New Title")
|
||||
|
||||
assert result == self.mock_issue
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_update_body(self):
|
||||
"""Test updating only issue body."""
|
||||
self.mock_api.update_issue.return_value = self.mock_issue
|
||||
|
||||
result = self.client.update_body(1, "New Body")
|
||||
|
||||
assert result == self.mock_issue
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_set_priority(self):
|
||||
"""Test setting issue priority."""
|
||||
# Mock getting current issue
|
||||
self.mock_issue.labels = [Mock(name="bug")]
|
||||
self.mock_api.get_issue.return_value = self.mock_issue
|
||||
self.mock_api.update_issue.return_value = self.mock_issue
|
||||
|
||||
result = self.client.set_priority(1, Priority.HIGH)
|
||||
|
||||
assert result == self.mock_issue
|
||||
self.mock_api.get_issue.assert_called_once_with(1)
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_set_status(self):
|
||||
"""Test setting issue status."""
|
||||
# Mock getting current issue
|
||||
self.mock_issue.labels = [Mock(name="bug")]
|
||||
self.mock_api.get_issue.return_value = self.mock_issue
|
||||
self.mock_api.update_issue.return_value = self.mock_issue
|
||||
|
||||
result = self.client.set_status(1, ProjectState.ACTIVE)
|
||||
|
||||
assert result == self.mock_issue
|
||||
self.mock_api.get_issue.assert_called_once_with(1)
|
||||
self.mock_api.update_issue.assert_called_once()
|
||||
|
||||
def test_to_dict(self):
|
||||
"""Test converting issue to dictionary."""
|
||||
result = self.client.to_dict(self.mock_issue)
|
||||
|
||||
expected_keys = ['number', 'title', 'body', 'state', 'html_url',
|
||||
'created_at', 'updated_at', 'assignee', 'labels', 'milestone']
|
||||
|
||||
assert all(key in result for key in expected_keys)
|
||||
assert result['number'] == 1
|
||||
assert result['title'] == "Test Issue"
|
||||
assert result['state'] == "open"
|
||||
|
||||
|
||||
class TestMilestonesClient:
|
||||
"""Test MilestonesClient functionality."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.mock_api = Mock()
|
||||
self.client = MilestonesClient(self.mock_api)
|
||||
|
||||
self.mock_milestone = Mock(spec=Milestone)
|
||||
self.mock_milestone.id = 1
|
||||
self.mock_milestone.title = "Test Milestone"
|
||||
|
||||
def test_list_milestones(self):
|
||||
"""Test listing milestones."""
|
||||
self.mock_api.list_milestones.return_value = [self.mock_milestone]
|
||||
|
||||
result = self.client.list()
|
||||
|
||||
assert result == [self.mock_milestone]
|
||||
self.mock_api.list_milestones.assert_called_once_with("all")
|
||||
|
||||
def test_list_open_milestones(self):
|
||||
"""Test listing open milestones."""
|
||||
self.mock_api.list_milestones.return_value = [self.mock_milestone]
|
||||
|
||||
result = self.client.list_open()
|
||||
|
||||
assert result == [self.mock_milestone]
|
||||
self.mock_api.list_milestones.assert_called_once_with("open")
|
||||
|
||||
def test_create_milestone(self):
|
||||
"""Test creating a milestone."""
|
||||
self.mock_api.create_milestone.return_value = self.mock_milestone
|
||||
|
||||
result = self.client.create("Test Milestone", "Description")
|
||||
|
||||
assert result == self.mock_milestone
|
||||
self.mock_api.create_milestone.assert_called_once()
|
||||
|
||||
|
||||
class TestLabelsClient:
|
||||
"""Test LabelsClient functionality."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.mock_api = Mock()
|
||||
self.client = LabelsClient(self.mock_api)
|
||||
|
||||
self.mock_label = Mock(spec=Label)
|
||||
self.mock_label.id = 1
|
||||
self.mock_label.name = "bug"
|
||||
|
||||
def test_list_labels(self):
|
||||
"""Test listing labels."""
|
||||
self.mock_api.list_labels.return_value = [self.mock_label]
|
||||
|
||||
result = self.client.list()
|
||||
|
||||
assert result == [self.mock_label]
|
||||
self.mock_api.list_labels.assert_called_once()
|
||||
|
||||
def test_create_label(self):
|
||||
"""Test creating a label."""
|
||||
self.mock_api.create_label.return_value = self.mock_label
|
||||
|
||||
result = self.client.create("bug", "red", "Bug reports")
|
||||
|
||||
assert result == self.mock_label
|
||||
self.mock_api.create_label.assert_called_once()
|
||||
|
||||
|
||||
class TestGiteaClient:
|
||||
"""Test the main GiteaClient facade."""
|
||||
|
||||
@patch('gitea.client.GiteaApiClient')
|
||||
def test_client_initialization(self, mock_api_client):
|
||||
"""Test GiteaClient initialization."""
|
||||
config = GiteaConfig(
|
||||
gitea_url="https://gitea.example.com",
|
||||
repo_owner="test_owner",
|
||||
repo_name="test_repo"
|
||||
)
|
||||
|
||||
client = GiteaClient(config)
|
||||
|
||||
assert isinstance(client.issues, IssuesClient)
|
||||
assert isinstance(client.milestones, MilestonesClient)
|
||||
assert isinstance(client.labels, LabelsClient)
|
||||
mock_api_client.assert_called_once_with(config)
|
||||
|
||||
@patch('gitea.client.GiteaConfig.from_git_repository')
|
||||
@patch('gitea.client.GiteaApiClient')
|
||||
def test_client_auto_config(self, mock_api_client, mock_from_git):
|
||||
"""Test GiteaClient with auto-detected config."""
|
||||
mock_config = Mock()
|
||||
mock_from_git.return_value = mock_config
|
||||
|
||||
client = GiteaClient()
|
||||
|
||||
mock_from_git.assert_called_once()
|
||||
mock_api_client.assert_called_once_with(mock_config)
|
||||
|
||||
|
||||
class TestErrorHandling:
|
||||
"""Test error handling throughout the facade."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Set up test fixtures."""
|
||||
self.mock_api = Mock()
|
||||
self.client = IssuesClient(self.mock_api)
|
||||
|
||||
def test_gitea_error_propagation(self):
|
||||
"""Test that GiteaError is properly propagated."""
|
||||
self.mock_api.get_issue.side_effect = GiteaError("API Error")
|
||||
|
||||
with pytest.raises(GiteaError):
|
||||
self.client.get(1)
|
||||
|
||||
def test_not_found_error_propagation(self):
|
||||
"""Test that GiteaNotFoundError is properly propagated."""
|
||||
self.mock_api.get_issue.side_effect = GiteaNotFoundError("Issue not found")
|
||||
|
||||
with pytest.raises(GiteaNotFoundError):
|
||||
self.client.get(999)
|
||||
|
||||
def test_auth_error_propagation(self):
|
||||
"""Test that GiteaAuthError is properly propagated."""
|
||||
self.mock_api.create_issue.side_effect = GiteaAuthError("Unauthorized")
|
||||
|
||||
with pytest.raises(GiteaAuthError):
|
||||
self.client.create("Title", "Body")
|
||||
|
||||
|
||||
class TestIntegrationPatterns:
|
||||
"""Test integration patterns and best practices."""
|
||||
|
||||
@patch('gitea.client.GiteaApiClient')
|
||||
def test_consistent_interface(self, mock_api_client):
|
||||
"""Test that the facade provides consistent interfaces."""
|
||||
config = GiteaConfig(gitea_url="https://gitea.example.com",
|
||||
repo_owner="owner", repo_name="repo")
|
||||
client = GiteaClient(config)
|
||||
|
||||
# All sub-clients should be available
|
||||
assert hasattr(client, 'issues')
|
||||
assert hasattr(client, 'milestones')
|
||||
assert hasattr(client, 'labels')
|
||||
|
||||
# All should have consistent method patterns
|
||||
assert hasattr(client.issues, 'list')
|
||||
assert hasattr(client.issues, 'get')
|
||||
assert hasattr(client.issues, 'create')
|
||||
assert hasattr(client.issues, 'update')
|
||||
|
||||
assert hasattr(client.milestones, 'list')
|
||||
assert hasattr(client.milestones, 'create')
|
||||
|
||||
assert hasattr(client.labels, 'list')
|
||||
assert hasattr(client.labels, 'create')
|
||||
|
||||
def test_backward_compatibility_dict_conversion(self):
|
||||
"""Test that to_dict provides backward compatibility."""
|
||||
mock_api = Mock()
|
||||
client = IssuesClient(mock_api)
|
||||
|
||||
# Create a mock issue with all expected attributes
|
||||
mock_issue = Mock(spec=Issue)
|
||||
mock_issue.number = 1
|
||||
mock_issue.title = "Test"
|
||||
mock_issue.body = "Body"
|
||||
mock_issue.state = "open"
|
||||
mock_issue.html_url = "https://example.com"
|
||||
mock_issue.created_at = datetime(2023, 1, 1)
|
||||
mock_issue.updated_at = datetime(2023, 1, 1)
|
||||
mock_issue.assignee = None
|
||||
mock_issue.labels = []
|
||||
mock_issue.milestone = None
|
||||
|
||||
result = client.to_dict(mock_issue)
|
||||
|
||||
# Should contain all expected fields for backward compatibility
|
||||
required_fields = ['number', 'title', 'body', 'state', 'html_url',
|
||||
'created_at', 'updated_at', 'assignee', 'labels', 'milestone']
|
||||
|
||||
for field in required_fields:
|
||||
assert field in result, f"Missing required field: {field}"
|
||||
|
||||
def test_label_operations_consistency(self):
|
||||
"""Test that label operations work consistently."""
|
||||
mock_api = Mock()
|
||||
client = IssuesClient(mock_api)
|
||||
|
||||
# Mock issue with labels
|
||||
mock_issue = Mock()
|
||||
mock_issue.labels = [Mock(name="bug"), Mock(name="priority:high")]
|
||||
mock_api.get_issue.return_value = mock_issue
|
||||
mock_api.update_issue.return_value = mock_issue
|
||||
|
||||
# Test all label operations
|
||||
client.add_labels(1, ["new-label"])
|
||||
client.remove_labels(1, ["old-label"])
|
||||
client.set_labels(1, ["label1", "label2"])
|
||||
|
||||
# Should have made appropriate API calls
|
||||
assert mock_api.get_issue.call_count == 2 # add_labels and remove_labels
|
||||
assert mock_api.update_issue.call_count == 3 # all three operations
|
||||
751
tests/test_local_backend.py
Normal file
751
tests/test_local_backend.py
Normal file
@@ -0,0 +1,751 @@
|
||||
"""
|
||||
Test suite for Local SQLite Backend functionality.
|
||||
|
||||
These tests ensure the local backend correctly implements the IssueBackend interface
|
||||
and provides reliable offline issue tracking.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import tempfile
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
from issue_tracker.backends.local.backend import LocalSQLiteBackend
|
||||
from issue_tracker.core.models import Issue, Label, User, Milestone, Comment, IssueState
|
||||
from issue_tracker.core.interfaces import IssueFilter
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestLocalBackendInitialization:
|
||||
"""Test local backend initialization and connection."""
|
||||
|
||||
def test_backend_initialization(self):
|
||||
"""Test backend initializes with correct type and capabilities."""
|
||||
backend = LocalSQLiteBackend()
|
||||
|
||||
assert backend.backend_type == "local"
|
||||
assert backend.capabilities.supports_milestones
|
||||
assert backend.capabilities.supports_assignees
|
||||
assert backend.capabilities.supports_comments
|
||||
assert backend.capabilities.supports_labels
|
||||
assert backend.capabilities.supports_search
|
||||
assert backend.capabilities.supports_bulk_operations
|
||||
assert not backend.capabilities.supports_webhooks
|
||||
assert not backend.capabilities.supports_real_time
|
||||
|
||||
def test_connect_creates_database(self):
|
||||
"""Test connect creates database file and schema."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
db_path = Path(tmpdir) / "test.db"
|
||||
backend = LocalSQLiteBackend()
|
||||
|
||||
backend.connect({'db_path': str(db_path)})
|
||||
|
||||
assert db_path.exists()
|
||||
assert backend.connection is not None
|
||||
assert backend.test_connection()
|
||||
|
||||
backend.disconnect()
|
||||
|
||||
def test_disconnect_closes_connection(self):
|
||||
"""Test disconnect properly closes database connection."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
backend = LocalSQLiteBackend()
|
||||
backend.connect({'db_path': str(Path(tmpdir) / "test.db")})
|
||||
|
||||
backend.disconnect()
|
||||
|
||||
assert backend.connection is None
|
||||
assert not backend.test_connection()
|
||||
|
||||
def test_schema_initialization(self):
|
||||
"""Test database schema is properly initialized."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
backend = LocalSQLiteBackend()
|
||||
backend.connect({'db_path': str(Path(tmpdir) / "test.db")})
|
||||
|
||||
# Verify tables exist
|
||||
cursor = backend.connection.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='table'"
|
||||
)
|
||||
tables = {row[0] for row in cursor.fetchall()}
|
||||
|
||||
expected_tables = {
|
||||
'issues', 'labels', 'users', 'milestones', 'comments',
|
||||
'issue_labels', 'issue_assignees', 'sync_history'
|
||||
}
|
||||
|
||||
assert expected_tables.issubset(tables)
|
||||
|
||||
backend.disconnect()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestIssueCRUD:
|
||||
"""Test issue CRUD operations."""
|
||||
|
||||
@pytest.fixture
|
||||
def backend(self):
|
||||
"""Create a temporary backend for each test."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
backend = LocalSQLiteBackend()
|
||||
backend.connect({'db_path': str(Path(tmpdir) / "test.db")})
|
||||
yield backend
|
||||
backend.disconnect()
|
||||
|
||||
def test_create_issue(self, backend):
|
||||
"""Test creating a new issue."""
|
||||
issue = Issue(
|
||||
id=None,
|
||||
number=0,
|
||||
title="Test Issue",
|
||||
description="Test description",
|
||||
state=IssueState.OPEN,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc)
|
||||
)
|
||||
|
||||
created = backend.create_issue(issue)
|
||||
|
||||
assert created.id is not None
|
||||
assert created.number == 1
|
||||
assert created.title == "Test Issue"
|
||||
assert created.description == "Test description"
|
||||
assert created.state == IssueState.OPEN
|
||||
|
||||
def test_create_multiple_issues_increments_numbers(self, backend):
|
||||
"""Test issue numbers increment correctly."""
|
||||
issue1 = Issue(
|
||||
id=None, number=0, title="Issue 1", description="Desc 1",
|
||||
state=IssueState.OPEN,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc)
|
||||
)
|
||||
issue2 = Issue(
|
||||
id=None, number=0, title="Issue 2", description="Desc 2",
|
||||
state=IssueState.OPEN,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc)
|
||||
)
|
||||
|
||||
created1 = backend.create_issue(issue1)
|
||||
created2 = backend.create_issue(issue2)
|
||||
|
||||
assert created1.number == 1
|
||||
assert created2.number == 2
|
||||
|
||||
def test_get_issue_by_id(self, backend):
|
||||
"""Test retrieving issue by ID."""
|
||||
issue = Issue(
|
||||
id=None, number=0, title="Test", description="Desc",
|
||||
state=IssueState.OPEN,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc)
|
||||
)
|
||||
created = backend.create_issue(issue)
|
||||
|
||||
retrieved = backend.get_issue(created.id)
|
||||
|
||||
assert retrieved is not None
|
||||
assert retrieved.id == created.id
|
||||
assert retrieved.title == "Test"
|
||||
|
||||
def test_get_issue_by_number(self, backend):
|
||||
"""Test retrieving issue by number."""
|
||||
issue = Issue(
|
||||
id=None, number=0, title="Test", description="Desc",
|
||||
state=IssueState.OPEN,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc)
|
||||
)
|
||||
created = backend.create_issue(issue)
|
||||
|
||||
retrieved = backend.get_issue_by_number(created.number)
|
||||
|
||||
assert retrieved is not None
|
||||
assert retrieved.number == created.number
|
||||
assert retrieved.title == "Test"
|
||||
|
||||
def test_get_nonexistent_issue_returns_none(self, backend):
|
||||
"""Test getting non-existent issue returns None."""
|
||||
assert backend.get_issue("nonexistent-id") is None
|
||||
assert backend.get_issue_by_number(999) is None
|
||||
|
||||
def test_update_issue(self, backend):
|
||||
"""Test updating an existing issue."""
|
||||
issue = Issue(
|
||||
id=None, number=0, title="Original", description="Original desc",
|
||||
state=IssueState.OPEN,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc)
|
||||
)
|
||||
created = backend.create_issue(issue)
|
||||
|
||||
created.title = "Updated"
|
||||
created.description = "Updated desc"
|
||||
created.state = IssueState.CLOSED
|
||||
created.closed_at = datetime.now(timezone.utc)
|
||||
|
||||
updated = backend.update_issue(created)
|
||||
|
||||
assert updated.title == "Updated"
|
||||
assert updated.description == "Updated desc"
|
||||
assert updated.state == IssueState.CLOSED
|
||||
|
||||
# Verify changes persisted
|
||||
retrieved = backend.get_issue(created.id)
|
||||
assert retrieved.title == "Updated"
|
||||
|
||||
def test_delete_issue(self, backend):
|
||||
"""Test deleting an issue."""
|
||||
issue = Issue(
|
||||
id=None, number=0, title="To Delete", description="Desc",
|
||||
state=IssueState.OPEN,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc)
|
||||
)
|
||||
created = backend.create_issue(issue)
|
||||
|
||||
result = backend.delete_issue(created.id)
|
||||
|
||||
assert result is True
|
||||
assert backend.get_issue(created.id) is None
|
||||
|
||||
def test_delete_nonexistent_issue_returns_false(self, backend):
|
||||
"""Test deleting non-existent issue returns False."""
|
||||
result = backend.delete_issue("nonexistent-id")
|
||||
assert result is False
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestIssueWithLabels:
|
||||
"""Test issue operations with labels."""
|
||||
|
||||
@pytest.fixture
|
||||
def backend(self):
|
||||
"""Create a temporary backend for each test."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
backend = LocalSQLiteBackend()
|
||||
backend.connect({'db_path': str(Path(tmpdir) / "test.db")})
|
||||
yield backend
|
||||
backend.disconnect()
|
||||
|
||||
def test_create_issue_with_labels(self, backend):
|
||||
"""Test creating issue with labels."""
|
||||
labels = [
|
||||
Label(name="bug", color="red", description="Bug reports"),
|
||||
Label(name="priority:high", color="orange")
|
||||
]
|
||||
|
||||
issue = Issue(
|
||||
id=None, number=0, title="Bug Report", description="Desc",
|
||||
state=IssueState.OPEN,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
labels=labels
|
||||
)
|
||||
|
||||
created = backend.create_issue(issue)
|
||||
retrieved = backend.get_issue(created.id)
|
||||
|
||||
assert len(retrieved.labels) == 2
|
||||
assert any(l.name == "bug" for l in retrieved.labels)
|
||||
assert any(l.name == "priority:high" for l in retrieved.labels)
|
||||
|
||||
def test_update_issue_labels(self, backend):
|
||||
"""Test updating issue labels."""
|
||||
issue = Issue(
|
||||
id=None, number=0, title="Test", description="Desc",
|
||||
state=IssueState.OPEN,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
labels=[Label(name="bug")]
|
||||
)
|
||||
created = backend.create_issue(issue)
|
||||
|
||||
# Update labels
|
||||
created.labels = [
|
||||
Label(name="bug"),
|
||||
Label(name="enhancement"),
|
||||
Label(name="priority:low")
|
||||
]
|
||||
backend.update_issue(created)
|
||||
|
||||
retrieved = backend.get_issue(created.id)
|
||||
assert len(retrieved.labels) == 3
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestIssueWithAssignees:
|
||||
"""Test issue operations with assignees."""
|
||||
|
||||
@pytest.fixture
|
||||
def backend(self):
|
||||
"""Create a temporary backend for each test."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
backend = LocalSQLiteBackend()
|
||||
backend.connect({'db_path': str(Path(tmpdir) / "test.db")})
|
||||
yield backend
|
||||
backend.disconnect()
|
||||
|
||||
def test_create_issue_with_assignees(self, backend):
|
||||
"""Test creating issue with assignees."""
|
||||
assignees = [
|
||||
User(id="user1", username="alice", display_name="Alice"),
|
||||
User(id="user2", username="bob", display_name="Bob")
|
||||
]
|
||||
|
||||
issue = Issue(
|
||||
id=None, number=0, title="Assigned Issue", description="Desc",
|
||||
state=IssueState.OPEN,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
assignees=assignees
|
||||
)
|
||||
|
||||
created = backend.create_issue(issue)
|
||||
retrieved = backend.get_issue(created.id)
|
||||
|
||||
assert len(retrieved.assignees) == 2
|
||||
assert any(u.username == "alice" for u in retrieved.assignees)
|
||||
assert any(u.username == "bob" for u in retrieved.assignees)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestIssueWithMilestone:
|
||||
"""Test issue operations with milestones."""
|
||||
|
||||
@pytest.fixture
|
||||
def backend(self):
|
||||
"""Create a temporary backend for each test."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
backend = LocalSQLiteBackend()
|
||||
backend.connect({'db_path': str(Path(tmpdir) / "test.db")})
|
||||
yield backend
|
||||
backend.disconnect()
|
||||
|
||||
def test_create_issue_with_milestone(self, backend):
|
||||
"""Test creating issue with milestone."""
|
||||
milestone = Milestone(
|
||||
id=None,
|
||||
title="v1.0",
|
||||
description="First release",
|
||||
state="open",
|
||||
due_date=datetime.now(timezone.utc) + timedelta(days=30)
|
||||
)
|
||||
created_milestone = backend.create_milestone(milestone)
|
||||
|
||||
issue = Issue(
|
||||
id=None, number=0, title="Feature", description="Desc",
|
||||
state=IssueState.OPEN,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
milestone=created_milestone
|
||||
)
|
||||
|
||||
created_issue = backend.create_issue(issue)
|
||||
retrieved = backend.get_issue(created_issue.id)
|
||||
|
||||
assert retrieved.milestone is not None
|
||||
assert retrieved.milestone.title == "v1.0"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestListAndFilter:
|
||||
"""Test listing and filtering issues."""
|
||||
|
||||
@pytest.fixture
|
||||
def backend(self):
|
||||
"""Create backend with sample data."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
backend = LocalSQLiteBackend()
|
||||
backend.connect({'db_path': str(Path(tmpdir) / "test.db")})
|
||||
|
||||
# Create sample issues
|
||||
backend.create_issue(Issue(
|
||||
id=None, number=0, title="Open Bug", description="Bug desc",
|
||||
state=IssueState.OPEN,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
labels=[Label(name="bug")]
|
||||
))
|
||||
|
||||
backend.create_issue(Issue(
|
||||
id=None, number=0, title="Closed Feature", description="Feature desc",
|
||||
state=IssueState.CLOSED,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
closed_at=datetime.now(timezone.utc),
|
||||
labels=[Label(name="enhancement")]
|
||||
))
|
||||
|
||||
backend.create_issue(Issue(
|
||||
id=None, number=0, title="In Progress Task", description="Task desc",
|
||||
state=IssueState.IN_PROGRESS,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc)
|
||||
))
|
||||
|
||||
yield backend
|
||||
backend.disconnect()
|
||||
|
||||
def test_list_all_issues(self, backend):
|
||||
"""Test listing all issues."""
|
||||
issues = backend.list_issues()
|
||||
assert len(issues) == 3
|
||||
|
||||
def test_filter_by_state_open(self, backend):
|
||||
"""Test filtering by open state."""
|
||||
filter_criteria = IssueFilter(state="open")
|
||||
issues = backend.list_issues(filter_criteria)
|
||||
assert len(issues) == 1
|
||||
assert issues[0].state == IssueState.OPEN
|
||||
|
||||
def test_filter_by_state_closed(self, backend):
|
||||
"""Test filtering by closed state."""
|
||||
filter_criteria = IssueFilter(state="closed")
|
||||
issues = backend.list_issues(filter_criteria)
|
||||
assert len(issues) == 1
|
||||
assert issues[0].state == IssueState.CLOSED
|
||||
|
||||
def test_search_issues(self, backend):
|
||||
"""Test searching issues by text."""
|
||||
filter_criteria = IssueFilter(search="Bug")
|
||||
issues = backend.list_issues(filter_criteria)
|
||||
assert len(issues) == 1
|
||||
assert "Bug" in issues[0].title
|
||||
|
||||
def test_search_issues_method(self, backend):
|
||||
"""Test search_issues method."""
|
||||
issues = backend.search_issues("Feature")
|
||||
assert len(issues) == 1
|
||||
assert "Feature" in issues[0].title
|
||||
|
||||
def test_filter_with_limit(self, backend):
|
||||
"""Test filtering with limit."""
|
||||
filter_criteria = IssueFilter(limit=2)
|
||||
issues = backend.list_issues(filter_criteria)
|
||||
assert len(issues) == 2
|
||||
|
||||
def test_filter_with_offset(self, backend):
|
||||
"""Test filtering with offset and limit."""
|
||||
filter_criteria = IssueFilter(limit=2, offset=1)
|
||||
issues = backend.list_issues(filter_criteria)
|
||||
assert len(issues) == 2
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestLabelOperations:
|
||||
"""Test label management operations."""
|
||||
|
||||
@pytest.fixture
|
||||
def backend(self):
|
||||
"""Create a temporary backend for each test."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
backend = LocalSQLiteBackend()
|
||||
backend.connect({'db_path': str(Path(tmpdir) / "test.db")})
|
||||
yield backend
|
||||
backend.disconnect()
|
||||
|
||||
def test_create_label(self, backend):
|
||||
"""Test creating a label."""
|
||||
label = Label(name="bug", color="red", description="Bug reports")
|
||||
created = backend.create_label(label)
|
||||
|
||||
assert created.name == "bug"
|
||||
assert created.color == "red"
|
||||
|
||||
def test_get_labels(self, backend):
|
||||
"""Test getting all labels."""
|
||||
backend.create_label(Label(name="bug", color="red"))
|
||||
backend.create_label(Label(name="enhancement", color="blue"))
|
||||
|
||||
labels = backend.get_labels()
|
||||
assert len(labels) == 2
|
||||
|
||||
def test_update_label(self, backend):
|
||||
"""Test updating a label."""
|
||||
label = Label(name="bug", color="red", description="Old desc")
|
||||
backend.create_label(label)
|
||||
|
||||
# Create new label with updated values (Label is frozen/immutable)
|
||||
updated_label = Label(name="bug", color="orange", description="New desc")
|
||||
backend.update_label(updated_label)
|
||||
|
||||
labels = backend.get_labels()
|
||||
bug_label = next(l for l in labels if l.name == "bug")
|
||||
assert bug_label.color == "orange"
|
||||
assert bug_label.description == "New desc"
|
||||
|
||||
def test_delete_label(self, backend):
|
||||
"""Test deleting a label."""
|
||||
backend.create_label(Label(name="bug", color="red"))
|
||||
|
||||
result = backend.delete_label("bug")
|
||||
assert result is True
|
||||
|
||||
labels = backend.get_labels()
|
||||
assert len(labels) == 0
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestUserOperations:
|
||||
"""Test user management operations."""
|
||||
|
||||
@pytest.fixture
|
||||
def backend(self):
|
||||
"""Create a temporary backend for each test."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
backend = LocalSQLiteBackend()
|
||||
backend.connect({'db_path': str(Path(tmpdir) / "test.db")})
|
||||
|
||||
# Create test users
|
||||
backend.connection.execute("""
|
||||
INSERT INTO users (id, username, display_name, email)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""", ("user1", "alice", "Alice Smith", "alice@example.com"))
|
||||
backend.connection.execute("""
|
||||
INSERT INTO users (id, username, display_name, email)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""", ("user2", "bob", "Bob Jones", "bob@example.com"))
|
||||
backend.connection.commit()
|
||||
|
||||
yield backend
|
||||
backend.disconnect()
|
||||
|
||||
def test_get_users(self, backend):
|
||||
"""Test getting all users."""
|
||||
users = backend.get_users()
|
||||
assert len(users) == 2
|
||||
|
||||
def test_get_user_by_id(self, backend):
|
||||
"""Test getting user by ID."""
|
||||
user = backend.get_user("user1")
|
||||
assert user is not None
|
||||
assert user.username == "alice"
|
||||
|
||||
def test_search_users(self, backend):
|
||||
"""Test searching users."""
|
||||
users = backend.search_users("alice")
|
||||
assert len(users) == 1
|
||||
assert users[0].username == "alice"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestMilestoneOperations:
|
||||
"""Test milestone management operations."""
|
||||
|
||||
@pytest.fixture
|
||||
def backend(self):
|
||||
"""Create a temporary backend for each test."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
backend = LocalSQLiteBackend()
|
||||
backend.connect({'db_path': str(Path(tmpdir) / "test.db")})
|
||||
yield backend
|
||||
backend.disconnect()
|
||||
|
||||
def test_create_milestone(self, backend):
|
||||
"""Test creating a milestone."""
|
||||
milestone = Milestone(
|
||||
id=None,
|
||||
title="v1.0",
|
||||
description="First release",
|
||||
state="open",
|
||||
due_date=datetime.now(timezone.utc) + timedelta(days=30)
|
||||
)
|
||||
|
||||
created = backend.create_milestone(milestone)
|
||||
assert created.id is not None
|
||||
assert created.title == "v1.0"
|
||||
|
||||
def test_get_milestones(self, backend):
|
||||
"""Test getting all milestones."""
|
||||
backend.create_milestone(Milestone(id=None, title="v1.0", state="open"))
|
||||
backend.create_milestone(Milestone(id=None, title="v2.0", state="open"))
|
||||
|
||||
milestones = backend.get_milestones()
|
||||
assert len(milestones) == 2
|
||||
|
||||
def test_update_milestone(self, backend):
|
||||
"""Test updating a milestone."""
|
||||
milestone = Milestone(id=None, title="v1.0", description="Old", state="open")
|
||||
created = backend.create_milestone(milestone)
|
||||
|
||||
created.description = "Updated"
|
||||
created.state = "closed"
|
||||
backend.update_milestone(created)
|
||||
|
||||
milestones = backend.get_milestones()
|
||||
updated = next(m for m in milestones if m.id == created.id)
|
||||
assert updated.description == "Updated"
|
||||
assert updated.state == "closed"
|
||||
|
||||
def test_delete_milestone(self, backend):
|
||||
"""Test deleting a milestone."""
|
||||
milestone = Milestone(id=None, title="v1.0", state="open")
|
||||
created = backend.create_milestone(milestone)
|
||||
|
||||
result = backend.delete_milestone(created.id)
|
||||
assert result is True
|
||||
|
||||
milestones = backend.get_milestones()
|
||||
assert len(milestones) == 0
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestCommentOperations:
|
||||
"""Test comment operations."""
|
||||
|
||||
@pytest.fixture
|
||||
def backend(self):
|
||||
"""Create backend with an issue."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
backend = LocalSQLiteBackend()
|
||||
backend.connect({'db_path': str(Path(tmpdir) / "test.db")})
|
||||
|
||||
# Create test issue
|
||||
issue = Issue(
|
||||
id=None, number=0, title="Test", description="Desc",
|
||||
state=IssueState.OPEN,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc)
|
||||
)
|
||||
backend.create_issue(issue)
|
||||
|
||||
yield backend
|
||||
backend.disconnect()
|
||||
|
||||
def test_add_comment(self, backend):
|
||||
"""Test adding a comment to an issue."""
|
||||
issue = backend.get_issue_by_number(1)
|
||||
author = User(id="user1", username="alice")
|
||||
|
||||
comment = Comment(
|
||||
id=None,
|
||||
body="Great issue!",
|
||||
author=author,
|
||||
created_at=datetime.now(timezone.utc)
|
||||
)
|
||||
|
||||
created = backend.add_comment(issue.id, comment)
|
||||
assert created.id is not None
|
||||
assert created.body == "Great issue!"
|
||||
|
||||
def test_get_comments(self, backend):
|
||||
"""Test getting comments for an issue."""
|
||||
issue = backend.get_issue_by_number(1)
|
||||
author = User(id="user1", username="alice")
|
||||
|
||||
comment1 = Comment(
|
||||
id=None, body="First comment", author=author,
|
||||
created_at=datetime.now(timezone.utc)
|
||||
)
|
||||
comment2 = Comment(
|
||||
id=None, body="Second comment", author=author,
|
||||
created_at=datetime.now(timezone.utc)
|
||||
)
|
||||
|
||||
backend.add_comment(issue.id, comment1)
|
||||
backend.add_comment(issue.id, comment2)
|
||||
|
||||
comments = backend.get_comments(issue.id)
|
||||
assert len(comments) == 2
|
||||
assert comments[0].body == "First comment"
|
||||
|
||||
def test_update_comment(self, backend):
|
||||
"""Test updating a comment."""
|
||||
issue = backend.get_issue_by_number(1)
|
||||
author = User(id="user1", username="alice")
|
||||
|
||||
comment = Comment(
|
||||
id=None, body="Original", author=author,
|
||||
created_at=datetime.now(timezone.utc)
|
||||
)
|
||||
created = backend.add_comment(issue.id, comment)
|
||||
|
||||
created.body = "Updated"
|
||||
backend.update_comment(created)
|
||||
|
||||
comments = backend.get_comments(issue.id)
|
||||
assert comments[0].body == "Updated"
|
||||
|
||||
def test_delete_comment(self, backend):
|
||||
"""Test deleting a comment."""
|
||||
issue = backend.get_issue_by_number(1)
|
||||
author = User(id="user1", username="alice")
|
||||
|
||||
comment = Comment(
|
||||
id=None, body="To delete", author=author,
|
||||
created_at=datetime.now(timezone.utc)
|
||||
)
|
||||
created = backend.add_comment(issue.id, comment)
|
||||
|
||||
result = backend.delete_comment(created.id)
|
||||
assert result is True
|
||||
|
||||
comments = backend.get_comments(issue.id)
|
||||
assert len(comments) == 0
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestSyncOperations:
|
||||
"""Test synchronization support."""
|
||||
|
||||
@pytest.fixture
|
||||
def backend(self):
|
||||
"""Create a temporary backend for each test."""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
backend = LocalSQLiteBackend()
|
||||
backend.connect({'db_path': str(Path(tmpdir) / "test.db")})
|
||||
yield backend
|
||||
backend.disconnect()
|
||||
|
||||
def test_finalize_sync_logs_success(self, backend):
|
||||
"""Test successful sync is logged."""
|
||||
backend.finalize_sync(success=True)
|
||||
|
||||
cursor = backend.connection.execute(
|
||||
"SELECT * FROM sync_history WHERE success = 1"
|
||||
)
|
||||
rows = cursor.fetchall()
|
||||
assert len(rows) == 1
|
||||
|
||||
def test_get_issues_modified_since(self, backend):
|
||||
"""Test getting issues modified after a timestamp."""
|
||||
old_time = datetime.now(timezone.utc) - timedelta(hours=2)
|
||||
|
||||
# Create issue before timestamp
|
||||
issue1 = Issue(
|
||||
id=None, number=0, title="Old", description="Desc",
|
||||
state=IssueState.OPEN,
|
||||
created_at=old_time,
|
||||
updated_at=old_time
|
||||
)
|
||||
backend.create_issue(issue1)
|
||||
|
||||
# Create issue after timestamp
|
||||
new_time = datetime.now(timezone.utc)
|
||||
issue2 = Issue(
|
||||
id=None, number=0, title="New", description="Desc",
|
||||
state=IssueState.OPEN,
|
||||
created_at=new_time,
|
||||
updated_at=new_time
|
||||
)
|
||||
backend.create_issue(issue2)
|
||||
|
||||
# Get issues modified since 1 hour ago
|
||||
since_time = datetime.now(timezone.utc) - timedelta(hours=1)
|
||||
modified_issues = backend.get_issues_modified_since(since_time)
|
||||
|
||||
assert len(modified_issues) == 1
|
||||
assert modified_issues[0].title == "New"
|
||||
|
||||
def test_get_sync_conflicts_returns_empty(self, backend):
|
||||
"""Test local backend has no sync conflicts."""
|
||||
conflicts = backend.get_sync_conflicts()
|
||||
assert conflicts == []
|
||||
|
||||
def test_prepare_for_sync(self, backend):
|
||||
"""Test prepare_for_sync doesn't fail."""
|
||||
# Should not raise
|
||||
backend.prepare_for_sync()
|
||||
Reference in New Issue
Block a user