Skip to main content
Glama

MCP Git Server

by MementoRC
test_github_primitives.py26.4 kB
""" Unit tests for GitHub primitive operations. This module provides comprehensive unit tests for the GitHub primitives module, testing all public functions, error conditions, and integration points. Critical for TDD Compliance: These tests define the interface that github_primitives must implement. DO NOT modify these tests to match implementation - the implementation must satisfy these test requirements to prevent LLM compliance issues. """ import os from unittest.mock import AsyncMock, MagicMock, patch import aiohttp import pytest from mcp_server_git.primitives.github_primitives import ( GitHubAPIError, GitHubAuthenticationError, GitHubPrimitiveError, GitHubRateLimitError, build_github_headers, build_github_url, check_repository_access, format_github_error, get_authenticated_user, get_commit_info, get_file_content, get_github_token, get_pull_request_info, get_repository_info, list_repository_contents, make_github_request, parse_github_url, search_repositories, validate_github_token, ) class TestGitHubExceptions: """Test GitHub primitive exception classes.""" def test_github_primitive_error_base(self): """Test GitHubPrimitiveError base exception.""" error = GitHubPrimitiveError("Test error") assert str(error) == "Test error" assert isinstance(error, Exception) def test_github_authentication_error(self): """Test GitHubAuthenticationError exception.""" error = GitHubAuthenticationError("Auth failed") assert str(error) == "Auth failed" assert isinstance(error, GitHubPrimitiveError) def test_github_rate_limit_error(self): """Test GitHubRateLimitError exception.""" error = GitHubRateLimitError("Rate limit exceeded") assert str(error) == "Rate limit exceeded" assert isinstance(error, GitHubPrimitiveError) def test_github_api_error_basic(self): """Test GitHubAPIError without status code.""" error = GitHubAPIError("API error") assert str(error) == "API error" assert error.status_code is None assert isinstance(error, GitHubPrimitiveError) def test_github_api_error_with_status(self): """Test GitHubAPIError with status code.""" error = GitHubAPIError("Not found", status_code=404) assert str(error) == "Not found" assert error.status_code == 404 class TestTokenHandling: """Test GitHub token validation and retrieval.""" def test_get_github_token_found(self): """Test getting GitHub token from environment.""" with patch.dict(os.environ, {"GITHUB_TOKEN": "test_token"}): token = get_github_token() assert token == "test_token" def test_get_github_token_not_found(self): """Test getting GitHub token when not in environment.""" with patch.dict(os.environ, {}, clear=True): token = get_github_token() assert token is None def test_validate_github_token_valid_classic(self): """Test validation of classic personal access token.""" token = "ghp_" + "1" * 36 assert validate_github_token(token) is True def test_validate_github_token_valid_fine_grained(self): """Test validation of fine-grained personal access token.""" token = "github_pat_" + "a" * 82 assert validate_github_token(token) is True def test_validate_github_token_valid_app_installation(self): """Test validation of GitHub App installation token.""" token = "ghs_" + "1" * 36 assert validate_github_token(token) is True def test_validate_github_token_valid_app_user(self): """Test validation of GitHub App user token.""" token = "ghu_" + "1" * 36 assert validate_github_token(token) is True def test_validate_github_token_invalid_format(self): """Test validation of invalid token format.""" assert validate_github_token("invalid_token") is False assert validate_github_token("ghp_short") is False assert validate_github_token("") is False assert validate_github_token(" ") is False def test_validate_github_token_none(self): """Test validation with None token.""" assert validate_github_token(None) is False class TestRequestBuilding: """Test GitHub request building utilities.""" def test_build_github_headers_valid_token(self): """Test building GitHub headers with valid token.""" token = "ghp_" + "1" * 36 headers = build_github_headers(token) assert headers["Authorization"] == f"Bearer {token}" assert headers["Accept"] == "application/vnd.github.v3+json" assert headers["User-Agent"] == "MCP-Git-Server/1.1.0" def test_build_github_headers_invalid_token(self): """Test building GitHub headers with invalid token.""" with pytest.raises(GitHubAuthenticationError): build_github_headers("invalid_token") def test_build_github_url_default_base(self): """Test building GitHub URL with default base.""" url = build_github_url("/repos/owner/repo") assert url == "https://api.github.com/repos/owner/repo" def test_build_github_url_custom_base(self): """Test building GitHub URL with custom base.""" url = build_github_url("/user", "https://api.github.example.com") assert url == "https://api.github.example.com/user" def test_build_github_url_strips_slashes(self): """Test URL building strips leading/trailing slashes properly.""" url = build_github_url("repos/owner/repo/", "https://api.github.com/") assert url == "https://api.github.com/repos/owner/repo" class TestGitHubRequests: """Test GitHub API request functionality.""" @pytest.mark.asyncio async def test_make_github_request_success(self): """Test successful GitHub API request.""" mock_response = MagicMock() mock_response.status = 200 mock_response.json = AsyncMock(return_value={"login": "testuser"}) mock_response.text = AsyncMock(return_value='{"login": "testuser"}') with patch("aiohttp.ClientSession") as mock_session_class: mock_session = AsyncMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=None) # Mock the request method to return an async context manager mock_request_cm = AsyncMock() mock_request_cm.__aenter__ = AsyncMock(return_value=mock_response) mock_request_cm.__aexit__ = AsyncMock(return_value=None) mock_session.request = MagicMock(return_value=mock_request_cm) mock_session_class.return_value = mock_session with patch.dict(os.environ, {"GITHUB_TOKEN": "ghp_" + "1" * 36}): result = await make_github_request("GET", "/user") assert result == {"login": "testuser"} @pytest.mark.asyncio async def test_make_github_request_no_token(self): """Test GitHub request without token.""" with patch.dict(os.environ, {}, clear=True): with pytest.raises(GitHubAuthenticationError): await make_github_request("GET", "/user") @pytest.mark.asyncio async def test_make_github_request_auth_error(self): """Test GitHub request with authentication error.""" mock_response = MagicMock() mock_response.status = 401 mock_response.text = AsyncMock(return_value="Unauthorized") with patch("aiohttp.ClientSession") as mock_session_class: mock_session = AsyncMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=None) # Mock the request method to return an async context manager mock_request_cm = AsyncMock() mock_request_cm.__aenter__ = AsyncMock(return_value=mock_response) mock_request_cm.__aexit__ = AsyncMock(return_value=None) mock_session.request = MagicMock(return_value=mock_request_cm) mock_session_class.return_value = mock_session with patch.dict(os.environ, {"GITHUB_TOKEN": "ghp_" + "1" * 36}): with pytest.raises(GitHubAuthenticationError): await make_github_request("GET", "/user") @pytest.mark.asyncio async def test_make_github_request_rate_limit(self): """Test GitHub request with rate limit error.""" mock_response = MagicMock() mock_response.status = 403 mock_response.text = AsyncMock(return_value="rate limit exceeded") mock_response.headers = {"X-RateLimit-Reset": "1234567890"} with patch("aiohttp.ClientSession") as mock_session_class: mock_session = AsyncMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=None) # Mock the request method to return an async context manager mock_request_cm = AsyncMock() mock_request_cm.__aenter__ = AsyncMock(return_value=mock_response) mock_request_cm.__aexit__ = AsyncMock(return_value=None) mock_session.request = MagicMock(return_value=mock_request_cm) mock_session_class.return_value = mock_session with patch.dict(os.environ, {"GITHUB_TOKEN": "ghp_" + "1" * 36}): with pytest.raises(GitHubRateLimitError): await make_github_request("GET", "/user") @pytest.mark.asyncio async def test_make_github_request_api_error(self): """Test GitHub request with general API error.""" mock_response = MagicMock() mock_response.status = 404 mock_response.text = AsyncMock(return_value="Not Found") with patch("aiohttp.ClientSession") as mock_session_class: mock_session = AsyncMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=None) # Mock the request method to return an async context manager mock_request_cm = AsyncMock() mock_request_cm.__aenter__ = AsyncMock(return_value=mock_response) mock_request_cm.__aexit__ = AsyncMock(return_value=None) mock_session.request = MagicMock(return_value=mock_request_cm) mock_session_class.return_value = mock_session with patch.dict(os.environ, {"GITHUB_TOKEN": "ghp_" + "1" * 36}): with pytest.raises(GitHubAPIError) as exc_info: await make_github_request("GET", "/unknown") assert exc_info.value.status_code == 404 @pytest.mark.asyncio async def test_make_github_request_network_error(self): """Test GitHub request with network error.""" with patch("aiohttp.ClientSession") as mock_session_class: mock_session = AsyncMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=None) mock_session.request = MagicMock( side_effect=aiohttp.ClientError("Network error") ) mock_session_class.return_value = mock_session with patch.dict(os.environ, {"GITHUB_TOKEN": "ghp_" + "1" * 36}): with pytest.raises(GitHubAPIError): await make_github_request("GET", "/user") @pytest.mark.asyncio async def test_make_github_request_with_params(self): """Test GitHub request with URL parameters.""" mock_response = MagicMock() mock_response.status = 200 mock_response.json = AsyncMock(return_value={"items": []}) mock_response.text = AsyncMock(return_value='{"items": []}') with patch("aiohttp.ClientSession") as mock_session_class: mock_session = AsyncMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=None) # Mock the request method to return an async context manager mock_request_cm = AsyncMock() mock_request_cm.__aenter__ = AsyncMock(return_value=mock_response) mock_request_cm.__aexit__ = AsyncMock(return_value=None) mock_session.request = MagicMock(return_value=mock_request_cm) mock_session_class.return_value = mock_session with patch.dict(os.environ, {"GITHUB_TOKEN": "ghp_" + "1" * 36}): result = await make_github_request( "GET", "/search/repositories", params={"q": "test"} ) assert result == {"items": []} @pytest.mark.asyncio async def test_make_github_request_with_json_data(self): """Test GitHub request with JSON data.""" mock_response = MagicMock() mock_response.status = 201 mock_response.json = AsyncMock(return_value={"id": 123, "title": "Test Issue"}) mock_response.text = AsyncMock( return_value='{"id": 123, "title": "Test Issue"}' ) with patch("aiohttp.ClientSession") as mock_session_class: mock_session = AsyncMock() mock_session.__aenter__ = AsyncMock(return_value=mock_session) mock_session.__aexit__ = AsyncMock(return_value=None) # Mock the request method to return an async context manager mock_request_cm = AsyncMock() mock_request_cm.__aenter__ = AsyncMock(return_value=mock_response) mock_request_cm.__aexit__ = AsyncMock(return_value=None) mock_session.request = MagicMock(return_value=mock_request_cm) mock_session_class.return_value = mock_session with patch.dict(os.environ, {"GITHUB_TOKEN": "ghp_" + "1" * 36}): result = await make_github_request( "POST", "/repos/owner/repo/issues", json_data={"title": "Test Issue"}, ) assert result == {"id": 123, "title": "Test Issue"} class TestHighLevelOperations: """Test high-level GitHub API operations.""" @pytest.mark.asyncio async def test_get_authenticated_user(self): """Test getting authenticated user information.""" with patch( "mcp_server_git.primitives.github_primitives.make_github_request" ) as mock_request: mock_request.return_value = {"login": "testuser", "id": 12345} result = await get_authenticated_user() assert result == {"login": "testuser", "id": 12345} mock_request.assert_called_once_with("GET", "/user") @pytest.mark.asyncio async def test_check_repository_access_success(self): """Test checking repository access when access is granted.""" with patch( "mcp_server_git.primitives.github_primitives.make_github_request" ) as mock_request: mock_request.return_value = {"full_name": "owner/repo"} result = await check_repository_access("owner", "repo") assert result is True mock_request.assert_called_once_with("GET", "/repos/owner/repo") @pytest.mark.asyncio async def test_check_repository_access_denied(self): """Test checking repository access when access is denied.""" with patch( "mcp_server_git.primitives.github_primitives.make_github_request" ) as mock_request: mock_request.side_effect = GitHubAPIError("Not found", status_code=404) result = await check_repository_access("owner", "repo") assert result is False @pytest.mark.asyncio async def test_get_repository_info(self): """Test getting repository information.""" repo_data = { "full_name": "owner/repo", "default_branch": "main", "private": False, } with patch( "mcp_server_git.primitives.github_primitives.make_github_request" ) as mock_request: mock_request.return_value = repo_data result = await get_repository_info("owner", "repo") assert result == repo_data mock_request.assert_called_once_with("GET", "/repos/owner/repo") @pytest.mark.asyncio async def test_get_pull_request_info(self): """Test getting pull request information.""" pr_data = { "number": 123, "title": "Test PR", "state": "open", "user": {"login": "author"}, } with patch( "mcp_server_git.primitives.github_primitives.make_github_request" ) as mock_request: mock_request.return_value = pr_data result = await get_pull_request_info("owner", "repo", 123) assert result == pr_data mock_request.assert_called_once_with("GET", "/repos/owner/repo/pulls/123") @pytest.mark.asyncio async def test_get_commit_info(self): """Test getting commit information.""" commit_data = { "sha": "abc123", "commit": {"author": {"name": "Test Author"}, "message": "Test commit"}, } with patch( "mcp_server_git.primitives.github_primitives.make_github_request" ) as mock_request: mock_request.return_value = commit_data result = await get_commit_info("owner", "repo", "abc123") assert result == commit_data mock_request.assert_called_once_with( "GET", "/repos/owner/repo/commits/abc123" ) @pytest.mark.asyncio async def test_list_repository_contents_directory(self): """Test listing repository contents for directory.""" contents_data = [ {"type": "file", "name": "README.md"}, {"type": "dir", "name": "src"}, ] with patch( "mcp_server_git.primitives.github_primitives.make_github_request" ) as mock_request: mock_request.return_value = contents_data result = await list_repository_contents("owner", "repo", "src") assert result == contents_data mock_request.assert_called_once_with( "GET", "/repos/owner/repo/contents/src", params={} ) @pytest.mark.asyncio async def test_list_repository_contents_file(self): """Test listing repository contents for single file.""" file_data = {"type": "file", "name": "README.md", "content": "..."} with patch( "mcp_server_git.primitives.github_primitives.make_github_request" ) as mock_request: mock_request.return_value = file_data result = await list_repository_contents("owner", "repo", "README.md") assert result == [file_data] # Should be wrapped in list @pytest.mark.asyncio async def test_list_repository_contents_with_ref(self): """Test listing repository contents with specific ref.""" contents_data = [{"type": "file", "name": "README.md"}] with patch( "mcp_server_git.primitives.github_primitives.make_github_request" ) as mock_request: mock_request.return_value = contents_data result = await list_repository_contents( "owner", "repo", "src", ref="develop" ) assert result == contents_data mock_request.assert_called_once_with( "GET", "/repos/owner/repo/contents/src", params={"ref": "develop"} ) @pytest.mark.asyncio async def test_get_file_content(self): """Test getting file content.""" file_data = { "type": "file", "name": "README.md", "content": "SGVsbG8gV29ybGQ=", # "Hello World" in base64 } with patch( "mcp_server_git.primitives.github_primitives.make_github_request" ) as mock_request: mock_request.return_value = file_data result = await get_file_content("owner", "repo", "README.md") assert result == file_data mock_request.assert_called_once_with( "GET", "/repos/owner/repo/contents/README.md", params={} ) @pytest.mark.asyncio async def test_search_repositories(self): """Test searching repositories.""" search_data = { "total_count": 1, "items": [{"full_name": "owner/repo", "stargazers_count": 100}], } with patch( "mcp_server_git.primitives.github_primitives.make_github_request" ) as mock_request: mock_request.return_value = search_data result = await search_repositories("language:python") assert result == search_data mock_request.assert_called_once_with( "GET", "/search/repositories", params={ "q": "language:python", "sort": "updated", "order": "desc", "per_page": 30, "page": 1, }, ) @pytest.mark.asyncio async def test_search_repositories_with_custom_params(self): """Test searching repositories with custom parameters.""" search_data = {"total_count": 0, "items": []} with patch( "mcp_server_git.primitives.github_primitives.make_github_request" ) as mock_request: mock_request.return_value = search_data result = await search_repositories( "test", sort="stars", order="asc", per_page=10, page=2 ) assert result == search_data mock_request.assert_called_once_with( "GET", "/search/repositories", params={ "q": "test", "sort": "stars", "order": "asc", "per_page": 10, "page": 2, }, ) @pytest.mark.asyncio async def test_search_repositories_per_page_limit(self): """Test that search repositories respects GitHub's per_page limit.""" search_data = {"total_count": 0, "items": []} with patch( "mcp_server_git.primitives.github_primitives.make_github_request" ) as mock_request: mock_request.return_value = search_data await search_repositories("test", per_page=150) # Above GitHub limit # Should be capped at 100 mock_request.assert_called_once_with( "GET", "/search/repositories", params={ "q": "test", "sort": "updated", "order": "desc", "per_page": 100, "page": 1, }, ) class TestUtilityFunctions: """Test utility functions.""" def test_parse_github_url_https(self): """Test parsing HTTPS GitHub URL.""" url = "https://github.com/owner/repo" result = parse_github_url(url) assert result == {"owner": "owner", "repo": "repo"} def test_parse_github_url_https_with_git(self): """Test parsing HTTPS GitHub URL with .git suffix.""" url = "https://github.com/owner/repo.git" result = parse_github_url(url) assert result == {"owner": "owner", "repo": "repo"} def test_parse_github_url_https_with_slash(self): """Test parsing HTTPS GitHub URL with trailing slash.""" url = "https://github.com/owner/repo/" result = parse_github_url(url) assert result == {"owner": "owner", "repo": "repo"} def test_parse_github_url_ssh(self): """Test parsing SSH GitHub URL.""" url = "git@github.com:owner/repo" result = parse_github_url(url) assert result == {"owner": "owner", "repo": "repo"} def test_parse_github_url_ssh_with_git(self): """Test parsing SSH GitHub URL with .git suffix.""" url = "git@github.com:owner/repo.git" result = parse_github_url(url) assert result == {"owner": "owner", "repo": "repo"} def test_parse_github_url_invalid(self): """Test parsing invalid GitHub URL.""" invalid_urls = [ "https://gitlab.com/owner/repo", "https://github.com/owner", "invalid-url", "", ] for url in invalid_urls: result = parse_github_url(url) assert result is None def test_format_github_error_authentication(self): """Test formatting authentication error.""" error = GitHubAuthenticationError("Invalid token") result = format_github_error(error) assert "🔒 Authentication failed" in result assert "Invalid token" in result def test_format_github_error_rate_limit(self): """Test formatting rate limit error.""" error = GitHubRateLimitError("Rate limit exceeded") result = format_github_error(error) assert "⏱️ Rate limit exceeded" in result def test_format_github_error_api_with_status(self): """Test formatting API error with status code.""" error = GitHubAPIError("Not found", status_code=404) result = format_github_error(error) assert "❌ GitHub API error (HTTP 404)" in result assert "Not found" in result def test_format_github_error_api_without_status(self): """Test formatting API error without status code.""" error = GitHubAPIError("General error") result = format_github_error(error) assert "❌ GitHub API error: General error" in result assert "(HTTP " not in result def test_format_github_error_unexpected(self): """Test formatting unexpected error.""" error = ValueError("Unexpected error") result = format_github_error(error) assert "💥 Unexpected error" in result assert "Unexpected error" in result

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/MementoRC/mcp-git'

If you have feedback or need assistance with the MCP directory API, please join our Discord server