""" Tests for GitHub provider implementation. """ from unittest.mock import MagicMock import pytest from exceptions import APIError, AuthenticationError from models import MergeStrategy, PRState from providers.github import GitHubProvider class TestGitHubProviderBasics: """Tests for GitHubProvider basic operations.""" def test_provider_name(self): """Test provider name is github.""" provider = GitHubProvider(token="test-token") assert provider.name == "github" def test_parse_repo_url_https(self): """Test parsing HTTPS repo URL.""" provider = GitHubProvider(token="test-token") owner, repo = provider.parse_repo_url("https://github.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 = GitHubProvider(token="test-token") owner, repo = provider.parse_repo_url("https://github.com/owner/repo") assert owner == "owner" assert repo == "repo" def test_parse_repo_url_ssh(self): """Test parsing SSH repo URL.""" provider = GitHubProvider(token="test-token") owner, repo = provider.parse_repo_url("git@github.com:owner/repo.git") assert owner == "owner" assert repo == "repo" def test_parse_repo_url_invalid(self): """Test error on invalid URL.""" provider = GitHubProvider(token="test-token") with pytest.raises(ValueError, match="Unable to parse"): provider.parse_repo_url("invalid-url") @pytest.fixture def mock_github_httpx_client(): """Create a mock httpx client for GitHub provider tests.""" from unittest.mock import AsyncMock mock_response = AsyncMock() mock_response.status_code = 200 mock_response.json = MagicMock(return_value={}) mock_response.text = "" mock_client = AsyncMock() mock_client.request = AsyncMock(return_value=mock_response) mock_client.get = AsyncMock(return_value=mock_response) mock_client.post = AsyncMock(return_value=mock_response) mock_client.patch = AsyncMock(return_value=mock_response) mock_client.put = AsyncMock(return_value=mock_response) mock_client.delete = AsyncMock(return_value=mock_response) return mock_client @pytest.fixture async def github_provider(test_settings, mock_github_httpx_client): """Create a GitHubProvider with mocked HTTP client.""" provider = GitHubProvider( token=test_settings.github_token, settings=test_settings, ) provider._client = mock_github_httpx_client yield provider await provider.close() @pytest.fixture def github_pr_data(): """Sample PR data from GitHub API.""" return { "number": 42, "title": "Test PR", "body": "This is a test pull request", "state": "open", "head": {"ref": "feature-branch"}, "base": {"ref": "main"}, "user": {"login": "test-user"}, "created_at": "2024-01-15T10:00:00Z", "updated_at": "2024-01-15T12:00:00Z", "merged_at": None, "closed_at": None, "html_url": "https://github.com/owner/repo/pull/42", "labels": [{"name": "enhancement"}], "assignees": [{"login": "assignee1"}], "requested_reviewers": [{"login": "reviewer1"}], "mergeable": True, "draft": False, } class TestGitHubProviderConnection: """Tests for GitHub provider connection.""" @pytest.mark.asyncio async def test_is_connected(self, github_provider, mock_github_httpx_client): """Test connection check.""" mock_github_httpx_client.request.return_value.json = MagicMock( return_value={"login": "test-user"} ) result = await github_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 = GitHubProvider( 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, github_provider, mock_github_httpx_client): """Test getting authenticated user.""" mock_github_httpx_client.request.return_value.json = MagicMock( return_value={"login": "test-user"} ) user = await github_provider.get_authenticated_user() assert user == "test-user" class TestGitHubProviderRepoOperations: """Tests for GitHub repository operations.""" @pytest.mark.asyncio async def test_get_repo_info(self, github_provider, mock_github_httpx_client): """Test getting repository info.""" mock_github_httpx_client.request.return_value.json = MagicMock( return_value={ "name": "repo", "full_name": "owner/repo", "default_branch": "main", } ) result = await github_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, github_provider, mock_github_httpx_client): """Test getting default branch.""" mock_github_httpx_client.request.return_value.json = MagicMock( return_value={"default_branch": "develop"} ) branch = await github_provider.get_default_branch("owner", "repo") assert branch == "develop" class TestGitHubPROperations: """Tests for GitHub PR operations.""" @pytest.mark.asyncio async def test_create_pr(self, github_provider, mock_github_httpx_client): """Test creating a pull request.""" mock_github_httpx_client.request.return_value.json = MagicMock( return_value={ "number": 42, "html_url": "https://github.com/owner/repo/pull/42", } ) result = await github_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://github.com/owner/repo/pull/42" @pytest.mark.asyncio async def test_create_pr_with_draft(self, github_provider, mock_github_httpx_client): """Test creating a draft PR.""" mock_github_httpx_client.request.return_value.json = MagicMock( return_value={ "number": 43, "html_url": "https://github.com/owner/repo/pull/43", } ) result = await github_provider.create_pr( owner="owner", repo="repo", title="Draft PR", body="Draft body", source_branch="feature", target_branch="main", draft=True, ) assert result.success is True assert result.pr_number == 43 @pytest.mark.asyncio async def test_create_pr_with_options(self, github_provider, mock_github_httpx_client): """Test creating PR with labels, assignees, reviewers.""" mock_responses = [ {"number": 44, "html_url": "https://github.com/owner/repo/pull/44"}, # Create PR [{"name": "enhancement"}], # POST add labels {}, # POST add assignees {}, # POST request reviewers ] mock_github_httpx_client.request.return_value.json = MagicMock(side_effect=mock_responses) result = await github_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, github_provider, mock_github_httpx_client, github_pr_data): """Test getting a pull request.""" mock_github_httpx_client.request.return_value.json = MagicMock( return_value=github_pr_data ) result = await github_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, github_provider, mock_github_httpx_client): """Test getting non-existent PR.""" mock_github_httpx_client.request.return_value.status_code = 404 mock_github_httpx_client.request.return_value.json = MagicMock(return_value=None) result = await github_provider.get_pr("owner", "repo", 999) assert result.success is False @pytest.mark.asyncio async def test_list_prs(self, github_provider, mock_github_httpx_client, github_pr_data): """Test listing pull requests.""" mock_github_httpx_client.request.return_value.json = MagicMock( return_value=[github_pr_data, github_pr_data] ) result = await github_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, github_provider, mock_github_httpx_client, github_pr_data): """Test listing PRs with state filter.""" mock_github_httpx_client.request.return_value.json = MagicMock( return_value=[github_pr_data] ) result = await github_provider.list_prs( "owner", "repo", state=PRState.OPEN ) assert result.success is True @pytest.mark.asyncio async def test_merge_pr(self, github_provider, mock_github_httpx_client, github_pr_data): """Test merging a pull request.""" # Merge returns sha, then get_pr returns the PR data, then delete branch mock_responses = [ {"sha": "merge-commit-sha", "merged": True}, # PUT merge github_pr_data, # GET PR for branch info None, # DELETE branch ] mock_github_httpx_client.request.return_value.json = MagicMock( side_effect=mock_responses ) result = await github_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_merge_pr_rebase(self, github_provider, mock_github_httpx_client, github_pr_data): """Test merging with rebase strategy.""" mock_responses = [ {"sha": "rebase-commit-sha", "merged": True}, # PUT merge github_pr_data, # GET PR for branch info None, # DELETE branch ] mock_github_httpx_client.request.return_value.json = MagicMock( side_effect=mock_responses ) result = await github_provider.merge_pr( "owner", "repo", 42, merge_strategy=MergeStrategy.REBASE, ) assert result.success is True @pytest.mark.asyncio async def test_update_pr(self, github_provider, mock_github_httpx_client, github_pr_data): """Test updating a pull request.""" mock_github_httpx_client.request.return_value.json = MagicMock( return_value=github_pr_data ) result = await github_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, github_provider, mock_github_httpx_client, github_pr_data): """Test closing a pull request.""" github_pr_data["state"] = "closed" mock_github_httpx_client.request.return_value.json = MagicMock( return_value=github_pr_data ) result = await github_provider.close_pr("owner", "repo", 42) assert result.success is True class TestGitHubBranchOperations: """Tests for GitHub branch operations.""" @pytest.mark.asyncio async def test_get_branch(self, github_provider, mock_github_httpx_client): """Test getting branch info.""" mock_github_httpx_client.request.return_value.json = MagicMock( return_value={ "name": "main", "commit": {"sha": "abc123"}, } ) result = await github_provider.get_branch("owner", "repo", "main") assert result["name"] == "main" @pytest.mark.asyncio async def test_delete_remote_branch(self, github_provider, mock_github_httpx_client): """Test deleting a remote branch.""" mock_github_httpx_client.request.return_value.status_code = 204 result = await github_provider.delete_remote_branch("owner", "repo", "old-branch") assert result is True class TestGitHubCommentOperations: """Tests for GitHub comment operations.""" @pytest.mark.asyncio async def test_add_pr_comment(self, github_provider, mock_github_httpx_client): """Test adding a comment to a PR.""" mock_github_httpx_client.request.return_value.json = MagicMock( return_value={"id": 1, "body": "Test comment"} ) result = await github_provider.add_pr_comment( "owner", "repo", 42, "Test comment" ) assert result["body"] == "Test comment" @pytest.mark.asyncio async def test_list_pr_comments(self, github_provider, mock_github_httpx_client): """Test listing PR comments.""" mock_github_httpx_client.request.return_value.json = MagicMock( return_value=[ {"id": 1, "body": "Comment 1"}, {"id": 2, "body": "Comment 2"}, ] ) result = await github_provider.list_pr_comments("owner", "repo", 42) assert len(result) == 2 class TestGitHubLabelOperations: """Tests for GitHub label operations.""" @pytest.mark.asyncio async def test_add_labels(self, github_provider, mock_github_httpx_client): """Test adding labels to a PR.""" mock_github_httpx_client.request.return_value.json = MagicMock( return_value=[{"name": "bug"}, {"name": "urgent"}] ) result = await github_provider.add_labels( "owner", "repo", 42, ["bug", "urgent"] ) assert "bug" in result assert "urgent" in result @pytest.mark.asyncio async def test_remove_label(self, github_provider, mock_github_httpx_client): """Test removing a label from a PR.""" mock_responses = [ None, # DELETE label {"labels": []}, # GET issue ] mock_github_httpx_client.request.return_value.json = MagicMock(side_effect=mock_responses) result = await github_provider.remove_label( "owner", "repo", 42, "bug" ) assert isinstance(result, list) class TestGitHubReviewerOperations: """Tests for GitHub reviewer operations.""" @pytest.mark.asyncio async def test_request_review(self, github_provider, mock_github_httpx_client): """Test requesting review from users.""" mock_github_httpx_client.request.return_value.json = MagicMock(return_value={}) result = await github_provider.request_review( "owner", "repo", 42, ["reviewer1", "reviewer2"] ) assert result == ["reviewer1", "reviewer2"] class TestGitHubErrorHandling: """Tests for error handling in GitHub provider.""" @pytest.mark.asyncio async def test_authentication_error(self, github_provider, mock_github_httpx_client): """Test handling authentication errors.""" mock_github_httpx_client.request.return_value.status_code = 401 with pytest.raises(AuthenticationError): await github_provider._request("GET", "/user") @pytest.mark.asyncio async def test_permission_denied(self, github_provider, mock_github_httpx_client): """Test handling permission denied errors.""" mock_github_httpx_client.request.return_value.status_code = 403 mock_github_httpx_client.request.return_value.text = "Permission denied" with pytest.raises(AuthenticationError, match="Insufficient permissions"): await github_provider._request("GET", "/protected") @pytest.mark.asyncio async def test_rate_limit_error(self, github_provider, mock_github_httpx_client): """Test handling rate limit errors.""" mock_github_httpx_client.request.return_value.status_code = 403 mock_github_httpx_client.request.return_value.text = "API rate limit exceeded" with pytest.raises(APIError, match="rate limit"): await github_provider._request("GET", "/user") @pytest.mark.asyncio async def test_api_error(self, github_provider, mock_github_httpx_client): """Test handling general API errors.""" mock_github_httpx_client.request.return_value.status_code = 500 mock_github_httpx_client.request.return_value.text = "Internal Server Error" mock_github_httpx_client.request.return_value.json = MagicMock( return_value={"message": "Server error"} ) with pytest.raises(APIError): await github_provider._request("GET", "/error") class TestGitHubPRParsing: """Tests for PR data parsing.""" def test_parse_pr_open(self, github_provider, github_pr_data): """Test parsing open PR.""" pr_info = github_provider._parse_pr(github_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, github_provider, github_pr_data): """Test parsing merged PR.""" github_pr_data["merged_at"] = "2024-01-16T10:00:00Z" pr_info = github_provider._parse_pr(github_pr_data) assert pr_info.state == PRState.MERGED def test_parse_pr_closed(self, github_provider, github_pr_data): """Test parsing closed PR.""" github_pr_data["state"] = "closed" github_pr_data["closed_at"] = "2024-01-16T10:00:00Z" pr_info = github_provider._parse_pr(github_pr_data) assert pr_info.state == PRState.CLOSED def test_parse_pr_draft(self, github_provider, github_pr_data): """Test parsing draft PR.""" github_pr_data["draft"] = True pr_info = github_provider._parse_pr(github_pr_data) assert pr_info.draft is True def test_parse_datetime_iso(self, github_provider): """Test parsing ISO datetime strings.""" dt = github_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, github_provider): """Test parsing None datetime returns now.""" dt = github_provider._parse_datetime(None) assert dt is not None assert dt.tzinfo is not None def test_parse_pr_with_null_body(self, github_provider, github_pr_data): """Test parsing PR with null body.""" github_pr_data["body"] = None pr_info = github_provider._parse_pr(github_pr_data) assert pr_info.body == ""