Skip to main content
Glama
test_api_completeness_request_id_timing.py12.8 kB
"""Unit tests for API completeness: request_id and timing functionality. Tests the request_id generation, custom request_id passthrough, and timing metrics across catalog and health tools (build_catalog, get_catalog_summary, search_catalog, health). """ from __future__ import annotations import re import tempfile import time from pathlib import Path from unittest.mock import Mock import pytest from igloo_mcp.catalog import CatalogService from igloo_mcp.config import Config from igloo_mcp.mcp.tools.build_catalog import BuildCatalogTool from igloo_mcp.mcp.tools.get_catalog_summary import GetCatalogSummaryTool from igloo_mcp.mcp.tools.health import HealthCheckTool from igloo_mcp.mcp.tools.search_catalog import SearchCatalogTool # UUID4 pattern for validation UUID4_PATTERN = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$") @pytest.fixture def mock_config(): """Create mock config for testing.""" config = Mock(spec=Config) config.snowflake_profile = "test_profile" config.reports_dir = Path(tempfile.mkdtemp()) # Mock nested snowflake config for health tool mock_snowflake = Mock() mock_snowflake.profile = "test_profile" config.snowflake = mock_snowflake return config @pytest.fixture def mock_catalog_service(mock_config): """Create mock catalog service.""" service = Mock(spec=CatalogService) # Mock build result build_result = Mock() build_result.output_dir = "/tmp/test_catalog" build_result.totals = Mock( databases=5, schemas=10, tables=50, views=20, materialized_views=5, dynamic_tables=3, tasks=2, functions=15, procedures=8, columns=500, ) service.build = Mock(return_value=build_result) return service @pytest.fixture def temp_catalog_dir(): """Create temporary catalog directory with test data.""" import json with tempfile.TemporaryDirectory() as tmpdir: # Create catalog.json file with empty catalog structure catalog_data = { "databases": [], "schemas": [], "tables": [], "views": [], "materialized_views": [], "dynamic_tables": [], "tasks": [], "functions": [], "procedures": [], } catalog_path = Path(tmpdir) / "catalog.json" catalog_path.write_text(json.dumps(catalog_data)) yield tmpdir @pytest.fixture def catalog_service_for_summary(mock_config): """Create catalog service for summary tests.""" service = Mock(spec=CatalogService) # Mock get_summary to return a proper summary mock_summary = Mock() mock_summary.databases = 0 mock_summary.schemas = 0 mock_summary.tables = 0 service.get_summary = Mock(return_value=mock_summary) return service class TestBuildCatalogRequestIdTiming: """Test request_id and timing for build_catalog tool.""" @pytest.mark.asyncio async def test_auto_generated_request_id(self, mock_config, mock_catalog_service): """Test that request_id is auto-generated when not provided.""" tool = BuildCatalogTool(mock_config, mock_catalog_service) result = await tool.execute(output_dir="./test_catalog") assert "request_id" in result assert UUID4_PATTERN.match(result["request_id"]) @pytest.mark.asyncio async def test_custom_request_id_passthrough(self, mock_config, mock_catalog_service): """Test that custom request_id is preserved.""" tool = BuildCatalogTool(mock_config, mock_catalog_service) custom_id = "custom-request-123" result = await tool.execute(output_dir="./test_catalog", request_id=custom_id) assert result["request_id"] == custom_id @pytest.mark.asyncio async def test_timing_structure(self, mock_config, mock_catalog_service): """Test timing metrics structure and accuracy.""" tool = BuildCatalogTool(mock_config, mock_catalog_service) start = time.time() result = await tool.execute(output_dir="./test_catalog") elapsed = (time.time() - start) * 1000 assert "timing" in result timing = result["timing"] # Check structure assert "catalog_fetch_ms" in timing assert "total_duration_ms" in timing # Check values are positive assert timing["catalog_fetch_ms"] > 0 assert timing["total_duration_ms"] > 0 # Check total is reasonable (should be close to measured elapsed) assert timing["total_duration_ms"] <= elapsed + 100 # Allow 100ms buffer # Check breakdown makes sense assert timing["catalog_fetch_ms"] <= timing["total_duration_ms"] @pytest.mark.asyncio async def test_warnings_field_present(self, mock_config, mock_catalog_service): """Test that warnings field is always present.""" tool = BuildCatalogTool(mock_config, mock_catalog_service) result = await tool.execute(output_dir="./test_catalog") assert "warnings" in result assert isinstance(result["warnings"], list) assert result["warnings"] == [] # Empty by default class TestGetCatalogSummaryRequestIdTiming: """Test request_id and timing for get_catalog_summary tool.""" @pytest.mark.asyncio async def test_auto_generated_request_id(self, catalog_service_for_summary, temp_catalog_dir): """Test that request_id is auto-generated when not provided.""" tool = GetCatalogSummaryTool(catalog_service_for_summary) result = await tool.execute(catalog_dir=temp_catalog_dir) assert "request_id" in result assert UUID4_PATTERN.match(result["request_id"]) @pytest.mark.asyncio async def test_custom_request_id_passthrough(self, catalog_service_for_summary, temp_catalog_dir): """Test that custom request_id is preserved.""" tool = GetCatalogSummaryTool(catalog_service_for_summary) custom_id = "summary-request-456" result = await tool.execute(catalog_dir=temp_catalog_dir, request_id=custom_id) assert result["request_id"] == custom_id @pytest.mark.asyncio async def test_timing_structure_simple(self, catalog_service_for_summary, temp_catalog_dir): """Test timing metrics for simple operation (total only).""" tool = GetCatalogSummaryTool(catalog_service_for_summary) start = time.time() result = await tool.execute(catalog_dir=temp_catalog_dir) elapsed = (time.time() - start) * 1000 assert "timing" in result timing = result["timing"] # Simple operation: only total_duration_ms assert "total_duration_ms" in timing assert timing["total_duration_ms"] > 0 assert timing["total_duration_ms"] <= elapsed + 100 class TestSearchCatalogRequestIdTiming: """Test request_id and timing for search_catalog tool.""" @pytest.mark.asyncio async def test_auto_generated_request_id(self, temp_catalog_dir): """Test that request_id is auto-generated when not provided.""" tool = SearchCatalogTool() result = await tool.execute(catalog_dir=temp_catalog_dir) assert "request_id" in result assert UUID4_PATTERN.match(result["request_id"]) @pytest.mark.asyncio async def test_custom_request_id_passthrough(self, temp_catalog_dir): """Test that custom request_id is preserved.""" tool = SearchCatalogTool() custom_id = "search-request-789" result = await tool.execute(catalog_dir=temp_catalog_dir, request_id=custom_id) assert result["request_id"] == custom_id @pytest.mark.asyncio async def test_timing_structure_with_breakdown(self, temp_catalog_dir): """Test timing metrics with breakdown for search operation.""" tool = SearchCatalogTool() start = time.time() result = await tool.execute(catalog_dir=temp_catalog_dir) elapsed = (time.time() - start) * 1000 assert "timing" in result timing = result["timing"] # Search operation: breakdown + total assert "search_duration_ms" in timing assert "total_duration_ms" in timing assert timing["search_duration_ms"] > 0 assert timing["total_duration_ms"] > 0 assert timing["total_duration_ms"] <= elapsed + 100 # Breakdown should be <= total assert timing["search_duration_ms"] <= timing["total_duration_ms"] @pytest.mark.asyncio async def test_warnings_field_present(self, temp_catalog_dir): """Test that warnings field is always present.""" tool = SearchCatalogTool() result = await tool.execute(catalog_dir=temp_catalog_dir) assert "warnings" in result assert isinstance(result["warnings"], list) assert result["warnings"] == [] class TestHealthRequestIdTiming: """Test request_id and timing for health tool.""" @pytest.mark.asyncio async def test_auto_generated_request_id(self, mock_config): """Test that request_id is auto-generated when not provided.""" # Create mock snowflake service mock_sf_service = Mock() tool = HealthCheckTool(mock_config, mock_sf_service) # Mock the connection test async def mock_test_connection(): return { "status": "connected", "connected": True, "profile": "test", } tool._test_connection = mock_test_connection # Disable optional checks that require additional mocking result = await tool.execute(include_profile=False, include_cortex=False, include_catalog=False) assert "request_id" in result assert UUID4_PATTERN.match(result["request_id"]) @pytest.mark.asyncio async def test_custom_request_id_passthrough(self, mock_config): """Test that custom request_id is preserved.""" mock_sf_service = Mock() tool = HealthCheckTool(mock_config, mock_sf_service) async def mock_test_connection(): return { "status": "connected", "connected": True, "profile": "test", } tool._test_connection = mock_test_connection custom_id = "health-request-999" result = await tool.execute( request_id=custom_id, include_profile=False, include_cortex=False, include_catalog=False, ) assert result["request_id"] == custom_id @pytest.mark.asyncio async def test_timing_structure_simple(self, mock_config): """Test timing metrics for health check (simple operation).""" mock_sf_service = Mock() tool = HealthCheckTool(mock_config, mock_sf_service) async def mock_test_connection(): return { "status": "connected", "connected": True, "profile": "test", } tool._test_connection = mock_test_connection start = time.time() result = await tool.execute(include_profile=False, include_cortex=False, include_catalog=False) elapsed = (time.time() - start) * 1000 assert "timing" in result timing = result["timing"] # Simple operation: only total_duration_ms assert "total_duration_ms" in timing assert timing["total_duration_ms"] > 0 assert timing["total_duration_ms"] <= elapsed + 100 class TestRequestIdFormat: """Test request_id format validation across all tools.""" @pytest.mark.asyncio async def test_request_id_is_valid_uuid4( self, mock_config, mock_catalog_service, temp_catalog_dir, catalog_service_for_summary ): """Test that auto-generated request_id follows UUID4 format.""" tools = [ BuildCatalogTool(mock_config, mock_catalog_service), GetCatalogSummaryTool(catalog_service_for_summary), SearchCatalogTool(), ] for tool in tools: if isinstance(tool, BuildCatalogTool): result = await tool.execute(output_dir="./test_catalog") else: result = await tool.execute(catalog_dir=temp_catalog_dir) request_id = result["request_id"] # Check UUID4 format assert UUID4_PATTERN.match(request_id), f"{tool.name} request_id {request_id} is not valid UUID4" # Check version field (9th group should start with 4) assert request_id[14] == "4", f"{tool.name} request_id is not UUID version 4" # Check variant (17th char should be 8, 9, a, or b) assert request_id[19] in "89ab", f"{tool.name} request_id is not RFC 4122 variant"

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/Evan-Kim2028/igloo-mcp'

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