- Add comprehensive gitea facade tests (35 tests covering all functionality) - Remove direct gitea integration tests from tddai/markitect modules - Maintain 100% test coverage while eliminating direct API testing - Achieve 324/324 passing tests confirming no functionality loss - Complete consolidation strategy from GITEA_INTEGRATION_CONSOLIDATION_GAMEPLAN.md 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
510 lines
18 KiB
Python
510 lines
18 KiB
Python
"""
|
|
Comprehensive tests for the Gitea facade/integration layer.
|
|
|
|
This test suite covers all Gitea API operations through the facade pattern,
|
|
ensuring the gitea.client module provides reliable, well-tested functionality
|
|
for the rest of the application.
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import Mock, MagicMock, patch
|
|
from datetime import datetime
|
|
|
|
from gitea.client import GiteaClient, IssuesClient, MilestonesClient, LabelsClient
|
|
from gitea.config import GiteaConfig
|
|
from gitea.models import Issue, Milestone, Label, ProjectState, Priority
|
|
from gitea.exceptions import GiteaError, GiteaNotFoundError, GiteaAuthError
|
|
|
|
|
|
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 |