diff --git a/tests/test_gitea_integration.py b/tests/test_gitea_integration.py new file mode 100644 index 0000000..b1d21a7 --- /dev/null +++ b/tests/test_gitea_integration.py @@ -0,0 +1,531 @@ +""" +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 \ No newline at end of file