""" Tests for git provider implementations. """ from unittest.mock import MagicMock import pytest from exceptions import APIError, AuthenticationError from models import MergeStrategy, PRState from providers.gitea import GiteaProvider class TestBaseProvider: """Tests for BaseProvider interface.""" def test_parse_repo_url_https(self, mock_gitea_provider): """Test parsing HTTPS repo URL.""" # The mock needs parse_repo_url to work provider = GiteaProvider(base_url="https://gitea.test.com", token="test-token") owner, repo = provider.parse_repo_url("https://gitea.test.com/owner/repo.git") assert owner == "owner" assert repo == "repo" def test_parse_repo_url_https_no_git(self): """Test parsing HTTPS URL without .git suffix.""" provider = GiteaProvider(base_url="https://gitea.test.com", token="test-token") owner, repo = provider.parse_repo_url("https://gitea.test.com/owner/repo") assert owner == "owner" assert repo == "repo" def test_parse_repo_url_ssh(self): """Test parsing SSH repo URL.""" provider = GiteaProvider(base_url="https://gitea.test.com", token="test-token") owner, repo = provider.parse_repo_url("git@gitea.test.com:owner/repo.git") assert owner == "owner" assert repo == "repo" def test_parse_repo_url_invalid(self): """Test error on invalid URL.""" provider = GiteaProvider(base_url="https://gitea.test.com", token="test-token") with pytest.raises(ValueError, match="Unable to parse"): provider.parse_repo_url("invalid-url") class TestGiteaProvider: """Tests for GiteaProvider.""" @pytest.mark.asyncio async def test_is_connected(self, gitea_provider, mock_httpx_client): """Test connection check.""" mock_httpx_client.request.return_value.json = MagicMock( return_value={"login": "test-user"} ) result = await gitea_provider.is_connected() assert result is True @pytest.mark.asyncio async def test_is_connected_no_token(self, test_settings): """Test connection fails without token.""" provider = GiteaProvider( base_url="https://gitea.test.com", token="", settings=test_settings, ) result = await provider.is_connected() assert result is False await provider.close() @pytest.mark.asyncio async def test_get_authenticated_user(self, gitea_provider, mock_httpx_client): """Test getting authenticated user.""" mock_httpx_client.request.return_value.json = MagicMock( return_value={"login": "test-user"} ) user = await gitea_provider.get_authenticated_user() assert user == "test-user" @pytest.mark.asyncio async def test_get_repo_info(self, gitea_provider, mock_httpx_client): """Test getting repository info.""" mock_httpx_client.request.return_value.json = MagicMock( return_value={ "name": "repo", "full_name": "owner/repo", "default_branch": "main", } ) result = await gitea_provider.get_repo_info("owner", "repo") assert result["name"] == "repo" assert result["default_branch"] == "main" @pytest.mark.asyncio async def test_get_default_branch(self, gitea_provider, mock_httpx_client): """Test getting default branch.""" mock_httpx_client.request.return_value.json = MagicMock( return_value={"default_branch": "develop"} ) branch = await gitea_provider.get_default_branch("owner", "repo") assert branch == "develop" class TestGiteaPROperations: """Tests for Gitea PR operations.""" @pytest.mark.asyncio async def test_create_pr(self, gitea_provider, mock_httpx_client): """Test creating a pull request.""" mock_httpx_client.request.return_value.json = MagicMock( return_value={ "number": 42, "html_url": "https://gitea.test.com/owner/repo/pull/42", } ) result = await gitea_provider.create_pr( owner="owner", repo="repo", title="Test PR", body="Test body", source_branch="feature", target_branch="main", ) assert result.success is True assert result.pr_number == 42 assert result.pr_url == "https://gitea.test.com/owner/repo/pull/42" @pytest.mark.asyncio async def test_create_pr_with_options(self, gitea_provider, mock_httpx_client): """Test creating PR with labels, assignees, reviewers.""" # Use side_effect for multiple API calls: # 1. POST create PR # 2. GET labels (for "enhancement") - in add_labels -> _get_or_create_label # 3. POST add labels to PR - in add_labels # 4. GET issue to return labels - in add_labels # 5. PATCH add assignees # 6. POST request reviewers mock_responses = [ { "number": 43, "html_url": "https://gitea.test.com/owner/repo/pull/43", }, # Create PR [{"id": 1, "name": "enhancement"}], # GET labels (found) {}, # POST add labels to PR {"labels": [{"name": "enhancement"}]}, # GET issue to return current labels {}, # PATCH add assignees {}, # POST request reviewers ] mock_httpx_client.request.return_value.json = MagicMock( side_effect=mock_responses ) result = await gitea_provider.create_pr( owner="owner", repo="repo", title="Test PR", body="Test body", source_branch="feature", target_branch="main", labels=["enhancement"], assignees=["user1"], reviewers=["reviewer1"], ) assert result.success is True @pytest.mark.asyncio async def test_get_pr(self, gitea_provider, mock_httpx_client, sample_pr_data): """Test getting a pull request.""" mock_httpx_client.request.return_value.json = MagicMock( return_value=sample_pr_data ) result = await gitea_provider.get_pr("owner", "repo", 42) assert result.success is True assert result.pr["number"] == 42 assert result.pr["title"] == "Test PR" @pytest.mark.asyncio async def test_get_pr_not_found(self, gitea_provider, mock_httpx_client): """Test getting non-existent PR.""" mock_httpx_client.request.return_value.status_code = 404 mock_httpx_client.request.return_value.json = MagicMock(return_value=None) result = await gitea_provider.get_pr("owner", "repo", 999) assert result.success is False @pytest.mark.asyncio async def test_list_prs(self, gitea_provider, mock_httpx_client, sample_pr_data): """Test listing pull requests.""" mock_httpx_client.request.return_value.json = MagicMock( return_value=[sample_pr_data, sample_pr_data] ) result = await gitea_provider.list_prs("owner", "repo") assert result.success is True assert len(result.pull_requests) == 2 @pytest.mark.asyncio async def test_list_prs_with_state_filter( self, gitea_provider, mock_httpx_client, sample_pr_data ): """Test listing PRs with state filter.""" mock_httpx_client.request.return_value.json = MagicMock( return_value=[sample_pr_data] ) result = await gitea_provider.list_prs("owner", "repo", state=PRState.OPEN) assert result.success is True @pytest.mark.asyncio async def test_merge_pr(self, gitea_provider, mock_httpx_client): """Test merging a pull request.""" # First call returns merge result mock_httpx_client.request.return_value.json = MagicMock( return_value={"sha": "merge-commit-sha"} ) result = await gitea_provider.merge_pr( "owner", "repo", 42, merge_strategy=MergeStrategy.SQUASH, ) assert result.success is True assert result.merge_commit_sha == "merge-commit-sha" @pytest.mark.asyncio async def test_update_pr(self, gitea_provider, mock_httpx_client, sample_pr_data): """Test updating a pull request.""" mock_httpx_client.request.return_value.json = MagicMock( return_value=sample_pr_data ) result = await gitea_provider.update_pr( "owner", "repo", 42, title="Updated Title", body="Updated body", ) assert result.success is True @pytest.mark.asyncio async def test_close_pr(self, gitea_provider, mock_httpx_client, sample_pr_data): """Test closing a pull request.""" sample_pr_data["state"] = "closed" mock_httpx_client.request.return_value.json = MagicMock( return_value=sample_pr_data ) result = await gitea_provider.close_pr("owner", "repo", 42) assert result.success is True class TestGiteaBranchOperations: """Tests for Gitea branch operations.""" @pytest.mark.asyncio async def test_get_branch(self, gitea_provider, mock_httpx_client): """Test getting branch info.""" mock_httpx_client.request.return_value.json = MagicMock( return_value={ "name": "main", "commit": {"sha": "abc123"}, } ) result = await gitea_provider.get_branch("owner", "repo", "main") assert result["name"] == "main" @pytest.mark.asyncio async def test_delete_remote_branch(self, gitea_provider, mock_httpx_client): """Test deleting a remote branch.""" mock_httpx_client.request.return_value.status_code = 204 result = await gitea_provider.delete_remote_branch( "owner", "repo", "old-branch" ) assert result is True class TestGiteaCommentOperations: """Tests for Gitea comment operations.""" @pytest.mark.asyncio async def test_add_pr_comment(self, gitea_provider, mock_httpx_client): """Test adding a comment to a PR.""" mock_httpx_client.request.return_value.json = MagicMock( return_value={"id": 1, "body": "Test comment"} ) result = await gitea_provider.add_pr_comment( "owner", "repo", 42, "Test comment" ) assert result["body"] == "Test comment" @pytest.mark.asyncio async def test_list_pr_comments(self, gitea_provider, mock_httpx_client): """Test listing PR comments.""" mock_httpx_client.request.return_value.json = MagicMock( return_value=[ {"id": 1, "body": "Comment 1"}, {"id": 2, "body": "Comment 2"}, ] ) result = await gitea_provider.list_pr_comments("owner", "repo", 42) assert len(result) == 2 class TestGiteaLabelOperations: """Tests for Gitea label operations.""" @pytest.mark.asyncio async def test_add_labels(self, gitea_provider, mock_httpx_client): """Test adding labels to a PR.""" # Use side_effect to return different values for different calls # 1. GET labels (for "bug") - returns existing labels # 2. POST to create "bug" label # 3. GET labels (for "urgent") # 4. POST to create "urgent" label # 5. POST labels to PR # 6. GET issue to return final labels mock_responses = [ [{"id": 1, "name": "existing"}], # GET labels (bug not found) {"id": 2, "name": "bug"}, # POST create bug [ {"id": 1, "name": "existing"}, {"id": 2, "name": "bug"}, ], # GET labels (urgent not found) {"id": 3, "name": "urgent"}, # POST create urgent {}, # POST add labels to PR {"labels": [{"name": "bug"}, {"name": "urgent"}]}, # GET issue ] mock_httpx_client.request.return_value.json = MagicMock( side_effect=mock_responses ) result = await gitea_provider.add_labels("owner", "repo", 42, ["bug", "urgent"]) # Should return updated label list assert isinstance(result, list) @pytest.mark.asyncio async def test_remove_label(self, gitea_provider, mock_httpx_client): """Test removing a label from a PR.""" # Use side_effect for multiple calls # 1. GET labels to find the label ID # 2. DELETE the label from the PR # 3. GET issue to return remaining labels mock_responses = [ [{"id": 1, "name": "bug"}], # GET labels {}, # DELETE label {"labels": []}, # GET issue ] mock_httpx_client.request.return_value.json = MagicMock( side_effect=mock_responses ) result = await gitea_provider.remove_label("owner", "repo", 42, "bug") assert isinstance(result, list) class TestGiteaReviewerOperations: """Tests for Gitea reviewer operations.""" @pytest.mark.asyncio async def test_request_review(self, gitea_provider, mock_httpx_client): """Test requesting review from users.""" mock_httpx_client.request.return_value.json = MagicMock(return_value={}) result = await gitea_provider.request_review( "owner", "repo", 42, ["reviewer1", "reviewer2"] ) assert result == ["reviewer1", "reviewer2"] class TestGiteaErrorHandling: """Tests for error handling in Gitea provider.""" @pytest.mark.asyncio async def test_authentication_error(self, gitea_provider, mock_httpx_client): """Test handling authentication errors.""" mock_httpx_client.request.return_value.status_code = 401 with pytest.raises(AuthenticationError): await gitea_provider._request("GET", "/user") @pytest.mark.asyncio async def test_permission_denied(self, gitea_provider, mock_httpx_client): """Test handling permission denied errors.""" mock_httpx_client.request.return_value.status_code = 403 with pytest.raises(AuthenticationError, match="Insufficient permissions"): await gitea_provider._request("GET", "/protected") @pytest.mark.asyncio async def test_api_error(self, gitea_provider, mock_httpx_client): """Test handling general API errors.""" mock_httpx_client.request.return_value.status_code = 500 mock_httpx_client.request.return_value.text = "Internal Server Error" mock_httpx_client.request.return_value.json = MagicMock( return_value={"message": "Server error"} ) with pytest.raises(APIError): await gitea_provider._request("GET", "/error") class TestGiteaPRParsing: """Tests for PR data parsing.""" def test_parse_pr_open(self, gitea_provider, sample_pr_data): """Test parsing open PR.""" pr_info = gitea_provider._parse_pr(sample_pr_data) assert pr_info.number == 42 assert pr_info.state == PRState.OPEN assert pr_info.title == "Test PR" assert pr_info.source_branch == "feature-branch" assert pr_info.target_branch == "main" def test_parse_pr_merged(self, gitea_provider, sample_pr_data): """Test parsing merged PR.""" sample_pr_data["merged"] = True sample_pr_data["merged_at"] = "2024-01-16T10:00:00Z" pr_info = gitea_provider._parse_pr(sample_pr_data) assert pr_info.state == PRState.MERGED def test_parse_pr_closed(self, gitea_provider, sample_pr_data): """Test parsing closed PR.""" sample_pr_data["state"] = "closed" sample_pr_data["closed_at"] = "2024-01-16T10:00:00Z" pr_info = gitea_provider._parse_pr(sample_pr_data) assert pr_info.state == PRState.CLOSED def test_parse_datetime_iso(self, gitea_provider): """Test parsing ISO datetime strings.""" dt = gitea_provider._parse_datetime("2024-01-15T10:30:00Z") assert dt.year == 2024 assert dt.month == 1 assert dt.day == 15 def test_parse_datetime_none(self, gitea_provider): """Test parsing None datetime returns now.""" dt = gitea_provider._parse_datetime(None) assert dt is not None assert dt.tzinfo is not None