Skip to main content
Glama
onimsha

Airtable OAuth MCP Server

by onimsha
test_mcp_server.py28.5 kB
"""Unit tests for MCP server.""" from unittest.mock import AsyncMock, MagicMock, patch import pytest from airtable_mcp.mcp.server import AirtableMCPServer class TestAirtableMCPServer: """Test cases for AirtableMCPServer.""" @pytest.fixture def mock_oauth_provider(self): """Create a mock OAuth provider.""" provider = MagicMock() provider.access_token = "test_access_token" provider.ensure_valid_token = AsyncMock(return_value=True) return provider @pytest.fixture def server(self, mock_oauth_provider): """Create a server instance for testing.""" server = AirtableMCPServer() # Mock the OAuth provider on the server instance server.oauth_provider = mock_oauth_provider return server @pytest.mark.asyncio async def test_list_bases_success(self, server): """Test successful list bases tool.""" from airtable_mcp.api.models import AirtableBase, ListBasesResponse # Create mock response with proper Pydantic models mock_base = AirtableBase( id="app123", name="Test Base", permissionLevel="create" ) mock_response = ListBasesResponse(bases=[mock_base]) with ( patch( "mcp_oauth_lib.auth.context.AuthContext.require_access_token", return_value="test_token", ), patch.object(server, "_get_authenticated_client") as mock_get_client, ): mock_client = AsyncMock() mock_client.list_bases.return_value = mock_response mock_get_client.return_value = mock_client # Test the logic by calling the _get_authenticated_client and list_bases # since the tool function itself is not directly accessible client = await server._get_authenticated_client() response = await client.list_bases() # Verify the expected data structure that the tool would return result = [ { "id": base.id, "name": base.name, "permissionLevel": base.permission_level, } for base in response.bases ] assert len(result) == 1 assert result[0]["id"] == "app123" assert result[0]["name"] == "Test Base" assert result[0]["permissionLevel"] == "create" mock_client.list_bases.assert_called_once() @pytest.mark.asyncio async def test_list_tables_success(self, server): """Test successful list tables tool.""" from airtable_mcp.api.models import AirtableTable, BaseSchemaResponse # Create mock response with proper Pydantic models mock_table = AirtableTable( id="tbl123", name="Test Table", primaryFieldId="fld123", fields=[] ) mock_response = BaseSchemaResponse(tables=[mock_table]) with ( patch( "mcp_oauth_lib.auth.context.AuthContext.require_access_token", return_value="test_token", ), patch.object(server, "_get_authenticated_client") as mock_get_client, ): mock_client = AsyncMock() mock_client.get_base_schema.return_value = mock_response mock_get_client.return_value = mock_client # Test the logic by calling the client methods client = await server._get_authenticated_client() schema = await client.get_base_schema("app123") # Verify the expected data structure that the tool would return result = [ { "id": table.id, "name": table.name, } for table in schema.tables ] assert len(result) == 1 assert result[0]["id"] == "tbl123" assert result[0]["name"] == "Test Table" mock_client.get_base_schema.assert_called_once_with("app123") @pytest.mark.asyncio async def test_list_tables_with_detail_level(self, server): """Test list tables tool logic with detail level.""" from airtable_mcp.api.models import ( AirtableField, AirtableTable, BaseSchemaResponse, ) # Create mock response with proper Pydantic models mock_field = AirtableField( id="fld123", name="Primary", type="singleLineText", description="Primary field", ) mock_table = AirtableTable( id="tbl123", name="Test Table", primaryFieldId="fld123", fields=[mock_field], views=[{"id": "viw123", "name": "Grid view", "type": "grid"}], ) mock_response = BaseSchemaResponse(tables=[mock_table]) with ( patch( "mcp_oauth_lib.auth.context.AuthContext.require_access_token", return_value="test_token", ), patch.object(server, "_get_authenticated_client") as mock_get_client, ): mock_client = AsyncMock() mock_client.get_base_schema.return_value = mock_response mock_get_client.return_value = mock_client # Test the logic by calling the client method and format the response client = await server._get_authenticated_client() schema = await client.get_base_schema("app123") # Simulate withFieldInfo detail level processing result = [ { "id": table.id, "name": table.name, "description": table.description, "primaryFieldId": table.primary_field_id, "fields": [ { "id": field.id, "name": field.name, "type": field.type, "description": field.description, "options": field.options, } for field in table.fields ], } for table in schema.tables ] assert len(result) == 1 table = result[0] assert table["id"] == "tbl123" assert "fields" in table assert len(table["fields"]) == 1 mock_client.get_base_schema.assert_called_once_with("app123") @pytest.mark.asyncio async def test_describe_table_success(self, server): """Test successful describe table tool logic.""" from airtable_mcp.api.models import ( AirtableField, AirtableTable, BaseSchemaResponse, ) # Create mock response with proper Pydantic models mock_field = AirtableField( id="fld123", name="Primary", type="singleLineText", description="Primary field", ) mock_table = AirtableTable( id="tbl123", name="Test Table", primaryFieldId="fld123", fields=[mock_field], views=[{"id": "viw123", "name": "Grid view", "type": "grid"}], ) mock_response = BaseSchemaResponse(tables=[mock_table]) with ( patch( "mcp_oauth_lib.auth.context.AuthContext.require_access_token", return_value="test_token", ), patch.object(server, "_get_authenticated_client") as mock_get_client, ): mock_client = AsyncMock() mock_client.get_base_schema.return_value = mock_response mock_get_client.return_value = mock_client # Test the logic by calling the client method and finding the table client = await server._get_authenticated_client() schema = await client.get_base_schema("app123") # Find the specific table (simulating the describe_table logic) table = next( (t for t in schema.tables if t.id == "tbl123" or t.name == "tbl123"), None, ) assert table is not None result = { "id": table.id, "name": table.name, "description": table.description, "primaryFieldId": table.primary_field_id, "fields": [ { "id": field.id, "name": field.name, "type": field.type, "description": field.description, "options": field.options, } for field in table.fields ], "views": table.views, } assert result["id"] == "tbl123" assert result["name"] == "Test Table" assert "fields" in result assert "views" in result mock_client.get_base_schema.assert_called_once_with("app123") @pytest.mark.asyncio async def test_describe_table_not_found(self, server): """Test describe table when table not found.""" from airtable_mcp.api.models import BaseSchemaResponse mock_response = BaseSchemaResponse(tables=[]) with ( patch( "mcp_oauth_lib.auth.context.AuthContext.require_access_token", return_value="test_token", ), patch.object(server, "_get_authenticated_client") as mock_get_client, ): mock_client = AsyncMock() mock_client.get_base_schema.return_value = mock_response mock_get_client.return_value = mock_client # Test the logic by calling the client method and simulating table search client = await server._get_authenticated_client() schema = await client.get_base_schema("app123") # Find the specific table (simulating the describe_table logic) table = next( (t for t in schema.tables if t.id == "tbl123" or t.name == "tbl123"), None, ) # Should not find the table assert table is None # Simulate the exception that would be raised if not table: error_msg = "Table 'tbl123' not found in base 'app123'" assert "not found" in error_msg @pytest.mark.asyncio async def test_server_initialization_with_oauth_disabled(self): """Test server initialization with OAuth endpoints disabled.""" server = AirtableMCPServer(enable_oauth_endpoints=False) assert server.enable_oauth_endpoints is False assert server.oauth_server is None @pytest.mark.asyncio async def test_server_initialization_missing_credentials(self): """Test server initialization with missing OAuth credentials.""" with patch.dict("os.environ", {}, clear=True): server = AirtableMCPServer(enable_oauth_endpoints=True) # Should disable OAuth endpoints when credentials are missing assert server.enable_oauth_endpoints is False assert server.oauth_server is None @pytest.mark.asyncio async def test_oauth_provider_access(self, server): """Test getting OAuth provider instance.""" provider = server._get_oauth_provider() if server.oauth_server: assert provider is not None else: assert provider is None @pytest.mark.asyncio async def test_authentication_client_creation_no_provider(self): """Test authenticated client creation without OAuth provider.""" from airtable_mcp.api.exceptions import AirtableAuthError # Create server without OAuth endpoints server = AirtableMCPServer(enable_oauth_endpoints=False) with patch( "mcp_oauth_lib.auth.context.AuthContext.require_access_token", return_value="test_token", ): with pytest.raises(AirtableAuthError, match="OAuth provider not available"): await server._get_authenticated_client() @pytest.mark.asyncio async def test_client_creation_exception_handling(self, server): """Test authenticated client creation with exception handling.""" from airtable_mcp.api.exceptions import AirtableAuthError with ( patch( "mcp_oauth_lib.auth.context.AuthContext.require_access_token", return_value="test_token", ), patch.object(server, "_get_oauth_provider") as mock_get_provider, patch( "airtable_mcp.mcp.server.AirtableClient", side_effect=Exception("Client creation failed"), ), ): mock_provider = MagicMock() mock_get_provider.return_value = mock_provider with pytest.raises(AirtableAuthError, match="Authentication failed"): await server._get_authenticated_client() @pytest.mark.asyncio async def test_list_records_with_json_fields_parameter(self, server): """Test list records with JSON string fields parameter.""" from airtable_mcp.api.models import AirtableRecord, ListRecordsOptions mock_record = AirtableRecord( id="rec123", fields={"Name": "Test Record"}, createdTime="2025-01-01T00:00:00.000Z", ) with ( patch( "mcp_oauth_lib.auth.context.AuthContext.require_access_token", return_value="test_token", ), patch.object(server, "_get_authenticated_client") as mock_get_client, ): mock_client = AsyncMock() mock_client.list_records.return_value = [mock_record] mock_get_client.return_value = mock_client # Test the JSON fields parameter processing logic client = await server._get_authenticated_client() # Simulate processing JSON string fields like the tool would fields_param = '["Name", "Email"]' # JSON string normalized_fields = None if fields_param is not None: if isinstance(fields_param, str): if fields_param.startswith("[") and fields_param.endswith("]"): try: import json normalized_fields = json.loads(fields_param) except (json.JSONDecodeError, ValueError): normalized_fields = [fields_param] else: normalized_fields = [fields_param] else: normalized_fields = fields_param options = ListRecordsOptions( view=None, sort=None, fields=normalized_fields, ) records = await client.list_records("app123", "tbl123", options) assert len(records) == 1 assert normalized_fields == ["Name", "Email"] @pytest.mark.asyncio async def test_list_records_success(self, server, mock_airtable_response): """Test successful list records tool logic.""" from airtable_mcp.api.models import AirtableRecord, ListRecordsOptions # Create mock records mock_record = AirtableRecord( id="rec123", fields={"Name": "Test Record"}, createdTime="2025-01-01T00:00:00.000Z", ) with ( patch( "mcp_oauth_lib.auth.context.AuthContext.require_access_token", return_value="test_token", ), patch.object(server, "_get_authenticated_client") as mock_get_client, ): mock_client = AsyncMock() mock_client.list_records.return_value = [mock_record] mock_get_client.return_value = mock_client # Test the logic by calling the client method client = await server._get_authenticated_client() options = ListRecordsOptions(view=None, sort=None, fields=None) records = await client.list_records("app123", "tbl123", options) # Format the response like the tool would result = [ { "id": record.id, "fields": record.fields, "createdTime": record.created_time, } for record in records ] assert len(result) == 1 assert result[0]["id"] == "rec123" mock_client.list_records.assert_called_once() @pytest.mark.asyncio async def test_list_records_with_parameters(self, server, mock_airtable_response): """Test list records with all parameters.""" from airtable_mcp.api.models import AirtableRecord, ListRecordsOptions # Create mock records mock_record = AirtableRecord( id="rec123", fields={"Name": "Test Record"}, createdTime="2025-01-01T00:00:00.000Z", ) with ( patch( "mcp_oauth_lib.auth.context.AuthContext.require_access_token", return_value="test_token", ), patch.object(server, "_get_authenticated_client") as mock_get_client, ): mock_client = AsyncMock() mock_client.list_records.return_value = [mock_record] mock_get_client.return_value = mock_client # Test the logic by calling the client method with options client = await server._get_authenticated_client() options = ListRecordsOptions( view="Grid view", sort=[{"field": "Name", "direction": "asc"}], fields=["Name", "Value"], filter_by_formula="{Status} = 'Active'", ) records = await client.list_records("app123", "tbl123", options) assert len(records) == 1 mock_client.list_records.assert_called_once() @pytest.mark.asyncio async def test_get_record_success(self, server): """Test successful get record tool.""" from airtable_mcp.api.models import AirtableRecord mock_record = AirtableRecord( id="rec123", fields={"Name": "Test Record"}, createdTime="2025-01-01T00:00:00.000Z", ) with ( patch( "mcp_oauth_lib.auth.context.AuthContext.require_access_token", return_value="test_token", ), patch.object(server, "_get_authenticated_client") as mock_get_client, ): mock_client = AsyncMock() mock_client.get_record.return_value = mock_record mock_get_client.return_value = mock_client # Test the logic by calling the client methods client = await server._get_authenticated_client() result = await client.get_record("app123", "tbl123", "rec123") assert result.id == "rec123" assert result.fields["Name"] == "Test Record" mock_client.get_record.assert_called_once_with("app123", "tbl123", "rec123") @pytest.mark.asyncio async def test_create_record_success(self, server): """Test successful create record tool logic.""" from airtable_mcp.api.models import AirtableRecord # Create mock record mock_record = AirtableRecord( id="rec456", fields={"Name": "New Record"}, createdTime="2025-01-01T00:00:00.000Z", ) with ( patch( "mcp_oauth_lib.auth.context.AuthContext.require_access_token", return_value="test_token", ), patch.object(server, "_get_authenticated_client") as mock_get_client, ): mock_client = AsyncMock() mock_client.create_records.return_value = [mock_record] mock_get_client.return_value = mock_client # Test the logic by calling the client method client = await server._get_authenticated_client() records = await client.create_records( "app123", "tbl123", [{"fields": {"Name": "New Record"}}], False ) # Format the response like the tool would (single record) record = records[0] result = { "id": record.id, "fields": record.fields, "createdTime": record.created_time, } assert result["id"] == "rec456" assert result["fields"]["Name"] == "New Record" mock_client.create_records.assert_called_once() @pytest.mark.asyncio async def test_create_records_success(self, server): """Test successful create records tool logic.""" from airtable_mcp.api.models import AirtableRecord # Create mock records mock_records = [ AirtableRecord( id="rec456", fields={"Name": "Record 1"}, createdTime="2025-01-01T00:00:00.000Z", ), AirtableRecord( id="rec789", fields={"Name": "Record 2"}, createdTime="2025-01-01T00:00:00.000Z", ), ] with ( patch( "mcp_oauth_lib.auth.context.AuthContext.require_access_token", return_value="test_token", ), patch.object(server, "_get_authenticated_client") as mock_get_client, ): mock_client = AsyncMock() mock_client.create_records.return_value = mock_records mock_get_client.return_value = mock_client # Test the logic by calling the client method client = await server._get_authenticated_client() created_records = await client.create_records( "app123", "tbl123", [ {"fields": {"Name": "Record 1"}}, {"fields": {"Name": "Record 2"}}, ], False, ) # Format the response like the tool would result = [ { "id": record.id, "fields": record.fields, "createdTime": record.created_time, } for record in created_records ] assert len(result) == 2 assert result[0]["id"] == "rec456" mock_client.create_records.assert_called_once() @pytest.mark.asyncio async def test_update_records_success(self, server): """Test successful update records tool logic.""" from airtable_mcp.api.models import AirtableRecord # Create mock record mock_record = AirtableRecord( id="rec123", fields={"Name": "Updated Record"}, createdTime="2025-01-01T00:00:00.000Z", ) with ( patch( "mcp_oauth_lib.auth.context.AuthContext.require_access_token", return_value="test_token", ), patch.object(server, "_get_authenticated_client") as mock_get_client, ): mock_client = AsyncMock() mock_client.update_records.return_value = [mock_record] mock_get_client.return_value = mock_client # Test the logic by calling the client method client = await server._get_authenticated_client() updated_records = await client.update_records( "app123", "tbl123", [ {"id": "rec123", "fields": {"Name": "Updated Record"}}, ], False, ) # Format the response like the tool would result = [ { "id": record.id, "fields": record.fields, "createdTime": record.created_time, } for record in updated_records ] assert len(result) == 1 assert result[0]["fields"]["Name"] == "Updated Record" mock_client.update_records.assert_called_once() @pytest.mark.asyncio async def test_delete_records_success(self, server): """Test successful delete records tool logic.""" with ( patch( "mcp_oauth_lib.auth.context.AuthContext.require_access_token", return_value="test_token", ), patch.object(server, "_get_authenticated_client") as mock_get_client, ): mock_client = AsyncMock() mock_client.delete_records.return_value = ["rec123"] mock_get_client.return_value = mock_client # Test the logic by calling the client method client = await server._get_authenticated_client() deleted_ids = await client.delete_records("app123", "tbl123", ["rec123"]) assert deleted_ids == ["rec123"] mock_client.delete_records.assert_called_once_with( "app123", "tbl123", ["rec123"] ) @pytest.mark.asyncio async def test_search_records_success(self, server, mock_airtable_response): """Test successful search records tool logic.""" from airtable_mcp.api.models import AirtableRecord, ListRecordsOptions # Create mock record mock_record = AirtableRecord( id="rec123", fields={"Name": "Test Record"}, createdTime="2025-01-01T00:00:00.000Z", ) with ( patch( "mcp_oauth_lib.auth.context.AuthContext.require_access_token", return_value="test_token", ), patch.object(server, "_get_authenticated_client") as mock_get_client, ): mock_client = AsyncMock() mock_client.search_records.return_value = [mock_record] mock_get_client.return_value = mock_client # Test the logic by calling the client method client = await server._get_authenticated_client() options = ListRecordsOptions(view="Grid view", fields=["Name"]) records = await client.search_records( "app123", "tbl123", "{Name} = 'Test'", options ) # Format the response like the tool would result = [ { "id": record.id, "fields": record.fields, "createdTime": record.created_time, } for record in records ] assert len(result) == 1 assert result[0]["id"] == "rec123" mock_client.search_records.assert_called_once() @pytest.mark.asyncio async def test_get_authenticated_client_success(self, server, mock_oauth_provider): """Test successful authenticated client creation.""" with ( patch( "mcp_oauth_lib.auth.context.AuthContext.require_access_token", return_value="test_token", ), patch.object( server, "_get_oauth_provider", return_value=mock_oauth_provider ), patch("airtable_mcp.mcp.server.AirtableClient") as mock_client_class, ): mock_oauth_provider.ensure_valid_token = AsyncMock(return_value=True) mock_oauth_provider.access_token = "test_token" client = await server._get_authenticated_client() mock_client_class.assert_called_once() assert client is not None @pytest.mark.asyncio async def test_get_authenticated_client_auth_failure( self, server, mock_oauth_provider ): """Test authenticated client creation with auth failure.""" from starlette.exceptions import HTTPException with patch( "mcp_oauth_lib.auth.context.AuthContext.require_access_token", side_effect=HTTPException(status_code=401, detail="No token"), ): with pytest.raises(HTTPException): await server._get_authenticated_client()

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/onimsha/airtable-mcp-server-oauth'

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