Skip to main content
Glama
vitalune

Personal Knowledge Assistant

by vitalune
test_api_integrations.py30.9 kB
""" Unit tests for API Integrations Tests all API clients with comprehensive mocking to avoid external API calls. """ import pytest import asyncio from datetime import datetime, timedelta from unittest.mock import AsyncMock, MagicMock, patch from typing import Dict, Any, List from src.integrations.client_manager import APIClientManager, ClientConfig, ServiceStatus from src.integrations.gmail_client import GmailClient from src.integrations.drive_client import DriveClient from src.integrations.twitter_client import TwitterClient from src.integrations.linkedin_client import LinkedInClient from src.config.auth import AuthProvider class TestAPIClientManager: """Test the central API client manager""" @pytest.fixture async def client_manager(self, test_settings, mock_auth_manager): """Create a test client manager""" with patch('src.integrations.client_manager.get_auth_manager', return_value=mock_auth_manager): manager = APIClientManager() await manager.initialize() yield manager await manager.shutdown() @pytest.mark.asyncio async def test_client_manager_initialization(self, client_manager): """Test client manager initializes properly""" assert client_manager is not None assert client_manager._clients == {} assert client_manager._client_configs == {} assert client_manager._service_status == {} def test_configure_service(self, client_manager): """Test service configuration""" client_manager.configure_service( service="gmail", client_id="test_client_id", client_secret="test_client_secret", scopes=["https://www.googleapis.com/auth/gmail.readonly"] ) assert "gmail" in client_manager._client_configs config = client_manager._client_configs["gmail"] assert config.client_id == "test_client_id" assert config.client_secret == "test_client_secret" assert config.scopes == ["https://www.googleapis.com/auth/gmail.readonly"] assert config.enabled is True @pytest.mark.asyncio async def test_create_gmail_client(self, client_manager): """Test Gmail client creation""" client_manager.configure_service( service="gmail", client_id="test_client_id", client_secret="test_client_secret", scopes=["https://www.googleapis.com/auth/gmail.readonly"] ) with patch('src.integrations.client_manager.GmailClient') as mock_gmail_class: mock_client = AsyncMock() mock_gmail_class.return_value = mock_client client = await client_manager.create_client("gmail") assert client is not None assert client == mock_client assert "gmail" in client_manager._clients assert "gmail" in client_manager._service_status @pytest.mark.asyncio async def test_create_drive_client(self, client_manager): """Test Drive client creation""" client_manager.configure_service( service="drive", client_id="test_client_id", client_secret="test_client_secret", scopes=["https://www.googleapis.com/auth/drive.readonly"] ) with patch('src.integrations.client_manager.DriveClient') as mock_drive_class: mock_client = AsyncMock() mock_drive_class.return_value = mock_client client = await client_manager.create_client("drive") assert client is not None assert client == mock_client assert "drive" in client_manager._clients @pytest.mark.asyncio async def test_create_twitter_client(self, client_manager): """Test Twitter client creation""" client_manager.configure_service( service="twitter", client_id="test_client_id", client_secret="test_client_secret", scopes=["tweet.read", "tweet.write"] ) with patch('src.integrations.client_manager.TwitterClient') as mock_twitter_class: mock_client = AsyncMock() mock_twitter_class.return_value = mock_client client = await client_manager.create_client("twitter") assert client is not None assert client == mock_client assert "twitter" in client_manager._clients @pytest.mark.asyncio async def test_create_linkedin_client(self, client_manager): """Test LinkedIn client creation""" client_manager.configure_service( service="linkedin", client_id="test_client_id", client_secret="test_client_secret", scopes=["r_liteprofile", "w_member_social"] ) with patch('src.integrations.client_manager.LinkedInClient') as mock_linkedin_class: mock_client = AsyncMock() mock_linkedin_class.return_value = mock_client client = await client_manager.create_client("linkedin") assert client is not None assert client == mock_client assert "linkedin" in client_manager._clients @pytest.mark.asyncio async def test_get_client_creates_if_not_exists(self, client_manager): """Test that get_client creates client if it doesn't exist""" client_manager.configure_service( service="gmail", client_id="test_client_id", client_secret="test_client_secret", scopes=["https://www.googleapis.com/auth/gmail.readonly"] ) with patch('src.integrations.client_manager.GmailClient') as mock_gmail_class: mock_client = AsyncMock() mock_gmail_class.return_value = mock_client # First call should create the client client1 = await client_manager.get_client("gmail") # Second call should return the same client client2 = await client_manager.get_client("gmail") assert client1 == client2 assert client1 == mock_client # Should only create once mock_gmail_class.assert_called_once() @pytest.mark.asyncio async def test_authentication_flow(self, client_manager, mock_auth_manager): """Test authentication flow""" client_manager.configure_service( service="gmail", client_id="test_client_id", client_secret="test_client_secret" ) mock_client = AsyncMock() mock_client.authenticate.side_effect = Exception("Please visit this URL to authorize: https://auth.url") with patch('src.integrations.client_manager.GmailClient', return_value=mock_client): auth_url = await client_manager.authenticate_service("gmail") assert auth_url == "https://auth.url" mock_client.authenticate.assert_called_once() @pytest.mark.asyncio async def test_oauth_callback_handling(self, client_manager): """Test OAuth callback handling""" client_manager.configure_service( service="gmail", client_id="test_client_id", client_secret="test_client_secret" ) mock_client = AsyncMock() mock_client.handle_oauth_callback.return_value = True with patch('src.integrations.client_manager.GmailClient', return_value=mock_client): success = await client_manager.handle_oauth_callback( service="gmail", authorization_code="test_code", state="test_state" ) assert success is True mock_client.handle_oauth_callback.assert_called_once_with("test_code", "test_state") # Check that service status was updated assert "gmail" in client_manager._service_status status = client_manager._service_status["gmail"] assert status.authenticated is True assert status.healthy is True @pytest.mark.asyncio async def test_check_authentication(self, client_manager, mock_auth_manager): """Test authentication checking""" mock_auth_manager.list_tokens.return_value = [ {"status": "valid", "provider": "gmail", "expires_at": datetime.now() + timedelta(hours=1)} ] is_authenticated = await client_manager.check_authentication("gmail") assert is_authenticated is True mock_auth_manager.list_tokens.assert_called_once() @pytest.mark.asyncio async def test_refresh_tokens(self, client_manager): """Test token refresh functionality""" client_manager.configure_service( service="gmail", client_id="test_client_id", client_secret="test_client_secret" ) mock_client = AsyncMock() mock_client.refresh_token.return_value = True with patch('src.integrations.client_manager.GmailClient', return_value=mock_client): client_manager._clients["gmail"] = mock_client results = await client_manager.refresh_tokens("gmail") assert results == {"gmail": True} mock_client.refresh_token.assert_called_once() @pytest.mark.asyncio async def test_get_service_status(self, client_manager): """Test service status retrieval""" # Add some mock status status = ServiceStatus( service_name="gmail", authenticated=True, healthy=True, last_check=datetime.now() ) client_manager._service_status["gmail"] = status # Get specific service status single_status = await client_manager.get_service_status("gmail") assert "gmail" in single_status assert single_status["gmail"] == status # Get all service statuses all_statuses = await client_manager.get_service_status() assert "gmail" in all_statuses assert all_statuses["gmail"] == status @pytest.mark.asyncio async def test_get_overall_health(self, client_manager, mock_auth_manager): """Test overall health status""" # Mock status for multiple services statuses = { "gmail": ServiceStatus("gmail", True, True, datetime.now()), "drive": ServiceStatus("drive", True, False, datetime.now(), "Connection timeout"), "twitter": ServiceStatus("twitter", False, False, datetime.now(), "Not authenticated") } client_manager._service_status = statuses with patch.object(client_manager.rate_limiter, 'get_all_status', return_value={}): with patch.object(client_manager.client_registry, 'get_all_health_status', return_value={}): health = await client_manager.get_overall_health() assert health["services"]["total"] == 3 assert health["services"]["healthy"] == 1 assert health["services"]["authenticated"] == 2 assert health["services"]["unhealthy"] == 2 assert health["overall_health"] == "degraded" @pytest.mark.asyncio async def test_bulk_export(self, client_manager, temp_dir): """Test bulk data export functionality""" client_manager.configure_service( service="gmail", client_id="test_client_id", client_secret="test_client_secret" ) mock_client = AsyncMock() mock_client.export_data.return_value = 150 # Mock export count with patch('src.integrations.client_manager.GmailClient', return_value=mock_client): client_manager._clients["gmail"] = mock_client with patch.object(client_manager, 'check_authentication', return_value=True): results = await client_manager.bulk_export( output_directory=temp_dir, services=["gmail"], max_items=1000 ) assert results == {"gmail": 150} mock_client.export_data.assert_called_once() @pytest.mark.asyncio async def test_data_sync(self, client_manager): """Test data synchronization between services""" # Configure both services for service in ["twitter", "linkedin"]: client_manager.configure_service( service=service, client_id="test_client_id", client_secret="test_client_secret" ) mock_twitter_client = AsyncMock() mock_twitter_client.get_home_timeline.return_value = { 'tweets': [ MagicMock(id='123', text='Test tweet content') ] } mock_linkedin_client = AsyncMock() mock_linkedin_client.create_post.return_value = {'id': 'linkedin_post_123'} with patch('src.integrations.client_manager.TwitterClient', return_value=mock_twitter_client): with patch('src.integrations.client_manager.LinkedInClient', return_value=mock_linkedin_client): client_manager._clients["twitter"] = mock_twitter_client client_manager._clients["linkedin"] = mock_linkedin_client with patch.object(client_manager, 'check_authentication', return_value=True): synced_count = await client_manager.sync_data( source_service="twitter", target_service="linkedin", data_type="posts", max_items=10 ) assert synced_count == 1 mock_twitter_client.get_home_timeline.assert_called_once() mock_linkedin_client.create_post.assert_called_once() class TestGmailClientMocking: """Test Gmail client with comprehensive mocking""" @pytest.fixture def mock_gmail_client(self): """Create a mock Gmail client""" with patch('src.integrations.gmail_client.build') as mock_build: mock_service = MagicMock() mock_build.return_value = mock_service client = GmailClient( client_id="test_client_id", client_secret="test_client_secret", scopes=["https://www.googleapis.com/auth/gmail.readonly"] ) client._service = mock_service client._authenticated = True return client, mock_service @pytest.mark.asyncio async def test_gmail_search_messages(self, mock_gmail_client): """Test Gmail message search functionality""" client, mock_service = mock_gmail_client # Mock the API response mock_service.users().messages().list().execute.return_value = { 'messages': [ {'id': '123', 'threadId': 'thread1'}, {'id': '456', 'threadId': 'thread2'} ] } mock_service.users().messages().get().execute.side_effect = [ { 'id': '123', 'snippet': 'Test message 1', 'payload': { 'headers': [ {'name': 'Subject', 'value': 'Test Subject 1'}, {'name': 'From', 'value': 'sender1@example.com'}, {'name': 'To', 'value': 'recipient@example.com'} ] }, 'internalDate': '1640995200000' # Unix timestamp in milliseconds }, { 'id': '456', 'snippet': 'Test message 2', 'payload': { 'headers': [ {'name': 'Subject', 'value': 'Test Subject 2'}, {'name': 'From', 'value': 'sender2@example.com'}, {'name': 'To', 'value': 'recipient@example.com'} ] }, 'internalDate': '1640995260000' } ] messages = await client.search_messages(query="test query", max_results=10) assert len(messages) == 2 assert messages[0]['subject'] == 'Test Subject 1' assert messages[1]['subject'] == 'Test Subject 2' # Verify API calls mock_service.users().messages().list.assert_called_once() assert mock_service.users().messages().get().execute.call_count == 2 @pytest.mark.asyncio async def test_gmail_send_message(self, mock_gmail_client): """Test Gmail message sending functionality""" client, mock_service = mock_gmail_client mock_service.users().messages().send().execute.return_value = { 'id': 'sent_message_123', 'labelIds': ['SENT'] } result = await client.send_message( to=["recipient@example.com"], subject="Test Subject", body="Test message body" ) assert result['id'] == 'sent_message_123' mock_service.users().messages().send.assert_called_once() @pytest.mark.asyncio async def test_gmail_get_labels(self, mock_gmail_client): """Test Gmail labels retrieval""" client, mock_service = mock_gmail_client mock_service.users().labels().list().execute.return_value = { 'labels': [ {'id': 'INBOX', 'name': 'INBOX', 'type': 'system'}, {'id': 'SENT', 'name': 'SENT', 'type': 'system'}, {'id': 'custom_label_1', 'name': 'Work', 'type': 'user'} ] } labels = await client.get_labels() assert len(labels) == 3 assert any(label['name'] == 'INBOX' for label in labels) assert any(label['name'] == 'Work' for label in labels) class TestDriveClientMocking: """Test Drive client with comprehensive mocking""" @pytest.fixture def mock_drive_client(self): """Create a mock Drive client""" with patch('src.integrations.drive_client.build') as mock_build: mock_service = MagicMock() mock_build.return_value = mock_service client = DriveClient( client_id="test_client_id", client_secret="test_client_secret", scopes=["https://www.googleapis.com/auth/drive.readonly"] ) client._service = mock_service client._authenticated = True return client, mock_service @pytest.mark.asyncio async def test_drive_search_files(self, mock_drive_client): """Test Drive file search functionality""" client, mock_service = mock_drive_client mock_service.files().list().execute.return_value = { 'files': [ { 'id': 'file1', 'name': 'Document1.pdf', 'mimeType': 'application/pdf', 'size': '1024000', 'modifiedTime': '2023-01-01T12:00:00.000Z' }, { 'id': 'file2', 'name': 'Spreadsheet1.xlsx', 'mimeType': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'size': '512000', 'modifiedTime': '2023-01-02T12:00:00.000Z' } ] } files = await client.search_files(query="test query", max_results=10) assert len(files) == 2 assert files[0]['name'] == 'Document1.pdf' assert files[1]['name'] == 'Spreadsheet1.xlsx' mock_service.files().list.assert_called_once() @pytest.mark.asyncio async def test_drive_get_file_content(self, mock_drive_client): """Test Drive file content retrieval""" client, mock_service = mock_drive_client mock_service.files().get().execute.return_value = { 'id': 'file1', 'name': 'test.txt', 'mimeType': 'text/plain' } mock_service.files().get_media().execute.return_value = b"Test file content" content = await client.get_file_content("file1") assert content == "Test file content" mock_service.files().get.assert_called_once() mock_service.files().get_media.assert_called_once() class TestTwitterClientMocking: """Test Twitter client with comprehensive mocking""" @pytest.fixture def mock_twitter_client(self): """Create a mock Twitter client""" client = TwitterClient( client_id="test_client_id", client_secret="test_client_secret", scopes=["tweet.read", "tweet.write"] ) client._authenticated = True return client @pytest.mark.asyncio async def test_twitter_search_tweets(self, mock_twitter_client): """Test Twitter tweet search functionality""" client = mock_twitter_client with patch.object(client, '_make_request') as mock_request: mock_request.return_value = { 'data': [ { 'id': '123456789', 'text': 'This is a test tweet #testing', 'created_at': '2023-01-01T12:00:00.000Z', 'author_id': 'user123', 'public_metrics': { 'like_count': 10, 'retweet_count': 5, 'reply_count': 2 } } ], 'meta': { 'result_count': 1 } } tweets = await client.search_tweets(query="test query", max_results=10) assert len(tweets) == 1 assert tweets[0]['text'] == 'This is a test tweet #testing' assert tweets[0]['public_metrics']['like_count'] == 10 @pytest.mark.asyncio async def test_twitter_post_tweet(self, mock_twitter_client): """Test Twitter tweet posting functionality""" client = mock_twitter_client with patch.object(client, '_make_request') as mock_request: mock_request.return_value = { 'data': { 'id': '987654321', 'text': 'Posted test tweet' } } result = await client.post_tweet(text="Posted test tweet") assert result['id'] == '987654321' assert result['text'] == 'Posted test tweet' @pytest.mark.asyncio async def test_twitter_get_user_timeline(self, mock_twitter_client): """Test Twitter user timeline retrieval""" client = mock_twitter_client with patch.object(client, '_make_request') as mock_request: mock_request.return_value = { 'data': [ { 'id': '111', 'text': 'First tweet', 'created_at': '2023-01-01T12:00:00.000Z' }, { 'id': '222', 'text': 'Second tweet', 'created_at': '2023-01-01T13:00:00.000Z' } ] } timeline = await client.get_home_timeline(max_results=10) assert len(timeline['tweets']) == 2 assert timeline['tweets'][0]['text'] == 'First tweet' class TestLinkedInClientMocking: """Test LinkedIn client with comprehensive mocking""" @pytest.fixture def mock_linkedin_client(self): """Create a mock LinkedIn client""" client = LinkedInClient( client_id="test_client_id", client_secret="test_client_secret", scopes=["r_liteprofile", "w_member_social"] ) client._authenticated = True return client @pytest.mark.asyncio async def test_linkedin_create_post(self, mock_linkedin_client): """Test LinkedIn post creation functionality""" client = mock_linkedin_client with patch.object(client, '_make_request') as mock_request: mock_request.return_value = { 'id': 'linkedin_post_123' } result = await client.create_post( text="Test LinkedIn post", visibility="PUBLIC" ) assert result['id'] == 'linkedin_post_123' @pytest.mark.asyncio async def test_linkedin_get_profile(self, mock_linkedin_client): """Test LinkedIn profile retrieval""" client = mock_linkedin_client with patch.object(client, '_make_request') as mock_request: mock_request.return_value = { 'id': 'user123', 'firstName': {'localized': {'en_US': 'John'}}, 'lastName': {'localized': {'en_US': 'Doe'}}, 'profilePicture': { 'displayImage': 'urn:li:digitalmediaAsset:image123' } } profile = await client.get_profile() assert profile['id'] == 'user123' assert profile['firstName']['localized']['en_US'] == 'John' class TestAPIClientErrorHandling: """Test error handling across all API clients""" @pytest.mark.asyncio async def test_authentication_error_handling(self): """Test handling of authentication errors""" client = GmailClient( client_id="invalid_client_id", client_secret="invalid_client_secret", scopes=["https://www.googleapis.com/auth/gmail.readonly"] ) with patch('src.integrations.gmail_client.build') as mock_build: mock_build.side_effect = Exception("Authentication failed") with pytest.raises(Exception) as exc_info: await client.authenticate("http://localhost:8080/callback") assert "Authentication failed" in str(exc_info.value) @pytest.mark.asyncio async def test_rate_limit_handling(self, mock_gmail_client): """Test handling of rate limit errors""" client, mock_service = mock_gmail_client # Mock rate limit error from googleapiclient.errors import HttpError error_response = MagicMock() error_response.status = 429 error_response.reason = "Rate Limit Exceeded" mock_service.users().messages().list().execute.side_effect = HttpError( resp=error_response, content=b'{"error": {"code": 429, "message": "Rate limit exceeded"}}' ) with pytest.raises(Exception) as exc_info: await client.search_messages(query="test", max_results=10) # Should handle rate limit appropriately assert "429" in str(exc_info.value) or "rate limit" in str(exc_info.value).lower() @pytest.mark.asyncio async def test_network_error_handling(self, mock_twitter_client): """Test handling of network errors""" client = mock_twitter_client with patch.object(client, '_make_request') as mock_request: mock_request.side_effect = ConnectionError("Network unreachable") with pytest.raises(ConnectionError): await client.search_tweets(query="test", max_results=10) @pytest.mark.asyncio async def test_invalid_response_handling(self, mock_drive_client): """Test handling of invalid API responses""" client, mock_service = mock_drive_client # Mock invalid response structure mock_service.files().list().execute.return_value = { 'invalid_key': 'invalid_data' # Missing 'files' key } files = await client.search_files(query="test", max_results=10) # Should handle gracefully and return empty list or appropriate default assert isinstance(files, list) class TestAPIClientPerformance: """Test performance characteristics of API clients""" @pytest.mark.asyncio async def test_concurrent_api_calls(self, mock_gmail_client): """Test concurrent API calls performance""" client, mock_service = mock_gmail_client # Mock fast responses mock_service.users().messages().list().execute.return_value = { 'messages': [{'id': '123', 'threadId': 'thread1'}] } mock_service.users().messages().get().execute.return_value = { 'id': '123', 'snippet': 'Test message', 'payload': {'headers': []} } import time start_time = time.time() # Make multiple concurrent calls tasks = [ client.search_messages(query=f"query_{i}", max_results=1) for i in range(5) ] results = await asyncio.gather(*tasks) execution_time = time.time() - start_time # Should complete all calls assert len(results) == 5 for result in results: assert isinstance(result, list) # Should be reasonably fast (allowing for mocking overhead) assert execution_time < 5.0 # 5 seconds is very generous for mocked calls @pytest.mark.asyncio async def test_client_resource_cleanup(self, mock_client_manager): """Test that clients properly clean up resources""" manager = APIClientManager() await manager.initialize() # Add some mock clients manager._clients = { 'gmail': MagicMock(), 'drive': MagicMock(), 'twitter': MagicMock() } # Shutdown should clean up all clients await manager.shutdown() # Verify cleanup tasks were initiated assert manager._health_check_task.cancelled() assert manager._token_refresh_task.cancelled()

Latest Blog Posts

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/vitalune/Nexus-MCP'

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