| """ |
| Coverage expansion tests for integration services. |
| |
| Tests cover critical code paths in: |
| - integrations/airtable_service.py: Airtable database operations |
| - integrations/salesforce_enhanced_api.py: Salesforce CRM operations |
| - integrations/discord_routes.py: Discord messaging operations |
| |
| Target: Cover critical paths (happy path + error paths) to increase coverage. |
| Uses extensive mocking to avoid external API dependencies. |
| """ |
|
|
| import pytest |
| from unittest.mock import Mock, patch, MagicMock, AsyncMock |
| from datetime import datetime |
| import httpx |
|
|
| from integrations.airtable_service import AirtableService |
|
|
|
|
| class TestAirtableServiceCoverage: |
| """Coverage expansion for AirtableService class.""" |
|
|
| @pytest.fixture |
| def airtable_service(self): |
| """Get Airtable service instance.""" |
| return AirtableService(tenant_id="default") |
|
|
| |
| def test_airtable_service_init(self, airtable_service): |
| """Airtable service initializes correctly.""" |
| assert airtable_service.tenant_id == "default" |
| assert airtable_service.base_url == "https://api.airtable.com/v0" |
| assert airtable_service.client is not None |
|
|
| def test_airtable_service_with_config(self): |
| """Initialize with custom config.""" |
| config = {"api_key": "test-key-123"} |
| service = AirtableService(tenant_id="test", config=config) |
| assert service.api_key == "test-key-123" |
|
|
| |
| @pytest.mark.asyncio |
| async def test_get_bases_success(self, airtable_service): |
| """Successfully list Airtable bases.""" |
| mock_response = MagicMock() |
| mock_response.status_code = 200 |
| mock_response.json.return_value = { |
| "bases": [ |
| {"id": "base-1", "name": "Base 1"}, |
| {"id": "base-2", "name": "Base 2"} |
| ] |
| } |
|
|
| with patch.object(airtable_service.client, 'get', AsyncMock(return_value=mock_response)): |
| bases = await airtable_service.get_bases() |
|
|
| assert len(bases) == 2 |
| assert bases[0]["id"] == "base-1" |
|
|
| @pytest.mark.asyncio |
| async def test_get_bases_with_token(self, airtable_service): |
| """List bases with custom token.""" |
| mock_response = MagicMock() |
| mock_response.status_code = 200 |
| mock_response.json.return_value = {"bases": []} |
|
|
| with patch.object(airtable_service.client, 'get', AsyncMock(return_value=mock_response)): |
| bases = await airtable_service.get_bases(token="custom-token") |
|
|
| assert isinstance(bases, list) |
|
|
| @pytest.mark.asyncio |
| async def test_get_bases_api_error(self, airtable_service): |
| """Handle API errors gracefully.""" |
| mock_response = MagicMock() |
| mock_response.raise_for_status.side_effect = Exception("API Error") |
|
|
| with patch.object(airtable_service.client, 'get', AsyncMock(return_value=mock_response)): |
| bases = await airtable_service.get_bases() |
|
|
| assert bases == [] |
|
|
| |
| @pytest.mark.asyncio |
| async def test_get_tables_success(self, airtable_service): |
| """Successfully list tables in a base.""" |
| mock_response = MagicMock() |
| mock_response.status_code = 200 |
| mock_response.json.return_value = { |
| "tables": [ |
| {"id": "table-1", "name": "Table 1"}, |
| {"id": "table-2", "name": "Table 2"} |
| ] |
| } |
|
|
| with patch.object(airtable_service.client, 'get', AsyncMock(return_value=mock_response)): |
| tables = await airtable_service.get_tables("base-123") |
|
|
| assert len(tables) == 2 |
| assert tables[0]["id"] == "table-1" |
|
|
| @pytest.mark.asyncio |
| async def test_get_tables_not_found(self, airtable_service): |
| """Handle base not found gracefully.""" |
| mock_response = MagicMock() |
| mock_response.raise_for_status.side_effect = Exception("Base not found") |
|
|
| with patch.object(airtable_service.client, 'get', AsyncMock(return_value=mock_response)): |
| tables = await airtable_service.get_tables("nonexistent-base") |
|
|
| assert tables == [] |
|
|
| |
| @pytest.mark.asyncio |
| async def test_list_records_success(self, airtable_service): |
| """Successfully list records from table.""" |
| mock_response = MagicMock() |
| mock_response.status_code = 200 |
| mock_response.json.return_value = { |
| "records": [ |
| {"id": "rec-1", "fields": {"Name": "John"}}, |
| {"id": "rec-2", "fields": {"Name": "Jane"}} |
| ] |
| } |
|
|
| with patch.object(airtable_service.client, 'get', AsyncMock(return_value=mock_response)): |
| records = await airtable_service.list_records( |
| base_id="base-123", |
| table_name="Table 1" |
| ) |
|
|
| assert len(records) == 2 |
|
|
| @pytest.mark.asyncio |
| async def test_list_records_with_filter(self, airtable_service): |
| """List records with filter formula.""" |
| mock_response = MagicMock() |
| mock_response.status_code = 200 |
| mock_response.json.return_value = {"records": []} |
|
|
| with patch.object(airtable_service.client, 'get', AsyncMock(return_value=mock_response)): |
| records = await airtable_service.list_records( |
| base_id="base-123", |
| table_name="Table 1", |
| filter_formula="{Status} = 'Active'" |
| ) |
|
|
| assert isinstance(records, list) |
|
|
| @pytest.mark.asyncio |
| async def test_list_records_no_api_key(self, airtable_service): |
| """Reject request without API key.""" |
| airtable_service.api_key = None |
|
|
| with pytest.raises(Exception) as exc_info: |
| await airtable_service.list_records("base-123", "Table 1") |
|
|
| assert "authenticated" in str(exc_info.value).lower() |
|
|
| |
| @pytest.mark.asyncio |
| async def test_create_record_success(self, airtable_service): |
| """Successfully create record.""" |
| mock_response = MagicMock() |
| mock_response.status_code = 200 |
| mock_response.json.return_value = { |
| "id": "rec-123", |
| "fields": {"Name": "New Record"} |
| } |
|
|
| with patch.object(airtable_service.client, 'post', AsyncMock(return_value=mock_response)): |
| record = await airtable_service.create_record( |
| base_id="base-123", |
| table_name="Table 1", |
| fields={"Name": "New Record"} |
| ) |
|
|
| assert record["id"] == "rec-123" |
|
|
| |
| @pytest.mark.asyncio |
| async def test_update_record_success(self, airtable_service): |
| """Successfully update record.""" |
| mock_response = MagicMock() |
| mock_response.status_code = 200 |
| mock_response.json.return_value = { |
| "id": "rec-123", |
| "fields": {"Name": "Updated"} |
| } |
|
|
| with patch.object(airtable_service.client, 'patch', AsyncMock(return_value=mock_response)): |
| record = await airtable_service.update_record( |
| base_id="base-123", |
| table_name="Table 1", |
| record_id="rec-123", |
| fields={"Name": "Updated"} |
| ) |
|
|
| assert record["fields"]["Name"] == "Updated" |
|
|
| |
| @pytest.mark.asyncio |
| async def test_delete_record_success(self, airtable_service): |
| """Successfully delete record.""" |
| mock_response = MagicMock() |
| mock_response.status_code = 200 |
| mock_response.json.return_value = {"deleted": True, "id": "rec-123"} |
|
|
| with patch.object(airtable_service.client, 'delete', AsyncMock(return_value=mock_response)): |
| result = await airtable_service.delete_record( |
| base_id="base-123", |
| table_name="Table 1", |
| record_id="rec-123" |
| ) |
|
|
| assert result["deleted"] == True |
|
|
| |
| @pytest.mark.asyncio |
| async def test_close_client(self, airtable_service): |
| """Successfully close HTTP client.""" |
| airtable_service.client.aclose = AsyncMock() |
| await airtable_service.close() |
| airtable_service.client.aclose.assert_called_once() |
|
|
|
|
| class TestSalesforceServiceCoverage: |
| """Coverage expansion for Salesforce service.""" |
|
|
| @pytest.fixture |
| def mock_sf_service(self): |
| """Get mock Salesforce service.""" |
| |
| with patch('integrations.salesforce_enhanced_api.SalesforceEnhancedAPI') as mock_class: |
| mock_instance = MagicMock() |
| mock_class.return_value = mock_instance |
| yield mock_instance |
|
|
| |
| @pytest.mark.asyncio |
| async def test_salesforce_authenticate_success(self, mock_sf_service): |
| """Successfully authenticate to Salesforce.""" |
| mock_sf_service.authenticate = AsyncMock(return_value={"access_token": "token-123"}) |
|
|
| result = await mock_sf_service.authenticate( |
| username="test@example.com", |
| password="password", |
| security_token="token" |
| ) |
|
|
| assert result["access_token"] == "token-123" |
|
|
| |
| @pytest.mark.asyncio |
| async def test_salesforce_query_success(self, mock_sf_service): |
| """Successfully query Salesforce records.""" |
| mock_sf_service.query = AsyncMock(return_value={ |
| "totalSize": 2, |
| "records": [ |
| {"Id": "001-1", "Name": "Account 1"}, |
| {"Id": "001-2", "Name": "Account 2"} |
| ] |
| }) |
|
|
| result = await mock_sf_service.query("SELECT Id, Name FROM Account") |
|
|
| assert result["totalSize"] == 2 |
| assert len(result["records"]) == 2 |
|
|
| |
| @pytest.mark.asyncio |
| async def test_salesforce_create_success(self, mock_sf_service): |
| """Successfully create Salesforce record.""" |
| mock_sf_service.create = AsyncMock(return_value={ |
| "id": "001-123", |
| "success": True |
| }) |
|
|
| result = await mock_sf_service.create("Account", {"Name": "Test Account"}) |
|
|
| assert result["success"] == True |
| assert result["id"] == "001-123" |
|
|
| |
| @pytest.mark.asyncio |
| async def test_salesforce_update_success(self, mock_sf_service): |
| """Successfully update Salesforce record.""" |
| mock_sf_service.update = AsyncMock(return_value={"success": True}) |
|
|
| result = await mock_sf_service.update("Account", "001-123", {"Name": "Updated"}) |
|
|
| assert result["success"] == True |
|
|
| |
| @pytest.mark.asyncio |
| async def test_salesforce_delete_success(self, mock_sf_service): |
| """Successfully delete Salesforce record.""" |
| mock_sf_service.delete = AsyncMock(return_value={"success": True}) |
|
|
| result = await mock_sf_service.delete("Account", "001-123") |
|
|
| assert result["success"] == True |
|
|
|
|
| class TestDiscordRoutesCoverage: |
| """Coverage expansion for Discord routes.""" |
|
|
| @pytest.fixture |
| def test_client(self): |
| """Get FastAPI test client.""" |
| from fastapi.testclient import TestClient |
| from main import app |
| return TestClient(app) |
|
|
| |
| @patch('integrations.discord_routes.send_discord_webhook') |
| def test_send_discord_webhook_success(self, mock_webhook, test_client): |
| """Successfully send Discord webhook.""" |
| mock_webhook.return_value = {"success": True} |
|
|
| response = test_client.post( |
| "/api/integrations/discord/webhook", |
| json={ |
| "webhook_url": "https://discord.com/api/webhooks/123", |
| "content": "Test message", |
| "username": "Test Bot" |
| } |
| ) |
|
|
| |
| assert response.status_code in [200, 401, 422] |
|
|
| |
| def test_discord_webhook_missing_content(self, test_client): |
| """Discord webhook without content returns validation error.""" |
| response = test_client.post( |
| "/api/integrations/discord/webhook", |
| json={ |
| "webhook_url": "https://discord.com/api/webhooks/123" |
| |
| } |
| ) |
|
|
| assert response.status_code in [200, 401, 422] |
|
|
| def test_discord_webhook_invalid_url(self, test_client): |
| """Discord webhook with invalid URL.""" |
| response = test_client.post( |
| "/api/integrations/discord/webhook", |
| json={ |
| "webhook_url": "not-a-url", |
| "content": "Test" |
| } |
| ) |
|
|
| assert response.status_code in [200, 401, 422] |
|
|
| |
| @patch('integrations.discord_routes.get_current_user') |
| def test_discord_auth_required(self, mock_get_user, test_client): |
| """Discord endpoints require authentication.""" |
| mock_get_user.side_effect = Exception("Not authenticated") |
|
|
| response = test_client.post( |
| "/api/integrations/discord/webhook", |
| json={"webhook_url": "https://discord.com/api/webhooks/123", "content": "Test"} |
| ) |
|
|
| |
| assert response.status_code in [401, 403, 422] |
|
|
|
|
| class TestIntegrationServicesErrorHandling: |
| """Coverage expansion for integration service error handling.""" |
|
|
| |
| @pytest.mark.asyncio |
| async def test_airtable_network_error(self): |
| """Handle network errors gracefully.""" |
| service = AirtableService(tenant_id="default") |
|
|
| mock_response = MagicMock() |
| mock_response.raise_for_status.side_effect = httpx.NetworkError("Connection failed") |
|
|
| with patch.object(service.client, 'get', AsyncMock(return_value=mock_response)): |
| bases = await service.get_bases() |
|
|
| assert bases == [] |
|
|
| |
| @pytest.mark.asyncio |
| async def test_airtable_timeout_error(self): |
| """Handle timeout errors gracefully.""" |
| service = AirtableService(tenant_id="default") |
|
|
| mock_response = MagicMock() |
| mock_response.raise_for_status.side_effect = httpx.TimeoutException("Request timeout") |
|
|
| with patch.object(service.client, 'get', AsyncMock(return_value=mock_response)): |
| bases = await service.get_bases() |
|
|
| assert bases == [] |
|
|
| |
| @pytest.mark.asyncio |
| async def test_airtable_invalid_json(self): |
| """Handle invalid JSON responses.""" |
| service = AirtableService(tenant_id="default") |
|
|
| mock_response = MagicMock() |
| mock_response.status_code = 200 |
| mock_response.json.side_effect = Exception("Invalid JSON") |
|
|
| with patch.object(service.client, 'get', AsyncMock(return_value=mock_response)): |
| bases = await service.get_bases() |
|
|
| assert bases == [] |
|
|
| |
| @pytest.mark.asyncio |
| async def test_airtable_rate_limited(self): |
| """Handle rate limiting errors.""" |
| service = AirtableService(tenant_id="default") |
|
|
| mock_response = MagicMock() |
| mock_response.status_code = 429 |
| mock_response.raise_for_status.side_effect = httpx.HTTPStatusError( |
| "Rate limited", request=MagicMock(), response=mock_response |
| ) |
|
|
| with patch.object(service.client, 'get', AsyncMock(return_value=mock_response)): |
| bases = await service.get_bases() |
|
|
| assert bases == [] |
|
|
| |
| @pytest.mark.asyncio |
| async def test_airtable_auth_error(self): |
| """Handle authentication errors.""" |
| service = AirtableService(tenant_id="default") |
|
|
| mock_response = MagicMock() |
| mock_response.status_code = 401 |
| mock_response.raise_for_status.side_effect = httpx.HTTPStatusError( |
| "Unauthorized", request=MagicMock(), response=mock_response |
| ) |
|
|
| with patch.object(service.client, 'get', AsyncMock(return_value=mock_response)): |
| bases = await service.get_bases() |
|
|
| assert bases == [] |
|
|