Skip to main content
Glama
test_mcp_server.py12.7 kB
"""Tests for FastMCP integration helpers.""" from __future__ import annotations import os import threading from pathlib import Path from types import SimpleNamespace from typing import Any from unittest.mock import AsyncMock, MagicMock, patch import pytest from igloo_mcp import mcp_server from igloo_mcp.config import Config @pytest.fixture(autouse=True) def reset_globals(): original = ( mcp_server._health_monitor, mcp_server._resource_manager, mcp_server._catalog_service, ) yield ( mcp_server._health_monitor, mcp_server._resource_manager, mcp_server._catalog_service, ) = original class StubService: def __init__(self, cursor: MagicMock | None = None) -> None: self.cursor = cursor def get_query_tag_param(self) -> None: return None def get_connection(self, **_: Any): # type: ignore[override] cursor = self.cursor or MagicMock() class _ConnCtx: def __enter__(self): return None, cursor def __exit__(self, exc_type, exc, tb): return False return _ConnCtx() class CapturingServer: def __init__(self) -> None: self.tools: dict[str, Any] = {} self.resources: dict[str, Any] = {} def tool(self, *, name: str, description: str): def decorator(func): self.tools[name] = func return func return decorator def resource(self, uri: str, **_: Any): def decorator(func): self.resources[uri] = func return func return decorator def _register_with_stub_execute( monkeypatch: pytest.MonkeyPatch, *, execute_side_effect: Exception | dict[str, Any] | None = None, ): """Register igloo_mcp tools with stubbed execute functionality. Note: CLI bridge functionality was removed in v0.3.x. """ server = CapturingServer() service = StubService() config = Config.from_env() original_create = mcp_server.create_service_context context = original_create(existing_config=config) monkeypatch.setattr("igloo_mcp.mcp_server.get_config", lambda: config) monkeypatch.setattr( "igloo_mcp.mcp_server.create_service_context", lambda *, existing_config=None: context, ) execute_mock = AsyncMock() if isinstance(execute_side_effect, Exception): execute_mock.side_effect = execute_side_effect elif execute_side_effect is None: execute_mock.return_value = {"rows": []} else: execute_mock.return_value = execute_side_effect execute_stub = SimpleNamespace(execute=execute_mock) monkeypatch.setattr( "igloo_mcp.mcp_server.ExecuteQueryTool", lambda *args, **kwargs: execute_stub, ) dummy_tool = SimpleNamespace(execute=lambda *_, **__: None) for attr in ( "BuildCatalogTool", "BuildDependencyGraphTool", "ConnectionTestTool", "HealthCheckTool", "GetCatalogSummaryTool", "SearchCatalogTool", "EvolveReportTool", ): monkeypatch.setattr( f"igloo_mcp.mcp_server.{attr}", lambda *args, **kwargs: dummy_tool, ) mcp_server.register_igloo_mcp( server, service, ) return server, execute_mock, context def test_execute_query_sync_applies_overrides(): cursor = MagicMock() cursor.fetchall.return_value = [{"COUNT": 1}] cursor.fetchone.side_effect = [ { "ROLE": "DEFAULT_ROLE", "WAREHOUSE": "DEFAULT_WH", "DATABASE": "DEFAULT_DB", "SCHEMA": "DEFAULT_SCHEMA", } ] cursor.rowcount = 1 service = StubService(cursor) original_state = { "role": "DEFAULT_ROLE", "warehouse": "DEFAULT_WH", "database": "DEFAULT_DB", "schema": "DEFAULT_SCHEMA", } with ( patch( "igloo_mcp.mcp_server.ensure_session_lock", return_value=threading.Lock(), ), patch("igloo_mcp.mcp_server.snapshot_session", return_value=original_state) as snapshot, patch("igloo_mcp.mcp_server.apply_session_context") as apply_ctx, patch("igloo_mcp.mcp_server.restore_session_context") as restore_ctx, ): result = mcp_server._execute_query_sync( service, "SELECT COUNT(*) FROM FOO", { "role": "SYSADMIN", "warehouse": "COMPUTE_WH", "database": "TEST_DB", "schema": "PUBLIC", }, ) assert result["rowcount"] == 1 assert result["rows"] == [{"COUNT": 1}] cursor.execute.assert_any_call("SELECT COUNT(*) FROM FOO") apply_ctx.assert_called_once_with( cursor, { "role": "SYSADMIN", "warehouse": "COMPUTE_WH", "database": "TEST_DB", "schema": "PUBLIC", }, ) restore_ctx.assert_called_once_with(cursor, original_state) snapshot.assert_called_once_with(cursor) # Lineage functionality removed - test removed def test_get_catalog_summary_sync_missing(tmp_path: Path): # Should raise FileNotFoundError instead of returning error object import pytest with pytest.raises(FileNotFoundError) as exc_info: mcp_server._get_catalog_summary_sync(str(tmp_path)) assert "Catalog summary not found" in str(exc_info.value) assert str(tmp_path) in str(exc_info.value) def test_get_catalog_summary_sync_success(tmp_path: Path): summary_file = tmp_path / "catalog_summary.json" summary_file.write_text('{"totals": {"tables": 1}}', encoding="utf-8") class StubCatalog: def __init__(self) -> None: self.called = False def load_summary(self, catalog_dir: str): self.called = True assert catalog_dir == str(tmp_path) return {"totals": {"tables": 1}} original = mcp_server._catalog_service try: stub = StubCatalog() mcp_server._catalog_service = stub # type: ignore[assignment] result = mcp_server._get_catalog_summary_sync(str(tmp_path)) assert result["totals"]["tables"] == 1 assert stub.called is True finally: mcp_server._catalog_service = original def test_register_igloo_mcp_registers_once(): """Test that register_igloo_mcp doesn't duplicate tool registrations.""" class DummyServer: def __init__(self) -> None: self.names: list[str] = [] self.resources: list[str] = [] def tool(self, *, name: str, description: str): def decorator(func): # pragma: no cover - executed by registration self.names.append(name) return func return decorator def resource(self, uri: str, **_: Any): def decorator(func): # pragma: no cover - executed by registration self.resources.append(uri) return func return decorator server = DummyServer() service = StubService() mcp_server.register_igloo_mcp(server, service) assert server.names # ensure tools registered assert server.resources # ensure resource registered # Second call should not duplicate registrations mcp_server.register_igloo_mcp(server, service) assert server.names == list(dict.fromkeys(server.names)) def test_register_igloo_mcp_sets_up_context(monkeypatch): """Test that register_igloo_mcp properly sets up service context. Note: CLI bridge functionality was removed in v0.3.x. """ class DummyServer: def __init__(self) -> None: self.tools = {} self.resources = {} def tool(self, *, name: str, description: str): def decorator(func): self.tools[name] = func return func return decorator def resource(self, uri: str, **_: Any): def decorator(func): self.resources[uri] = func return func return decorator server = DummyServer() service = StubService() ctx = MagicMock() ctx.health_monitor = MagicMock() ctx.resource_manager = MagicMock() def fake_create_service_context(*, existing_config=None): return ctx monkeypatch.setattr("igloo_mcp.mcp_server.create_service_context", fake_create_service_context) mcp_server.register_igloo_mcp(server, service) assert mcp_server._health_monitor is ctx.health_monitor assert mcp_server._resource_manager is ctx.resource_manager @pytest.mark.asyncio async def test_execute_query_tool_handles_timeout(monkeypatch: pytest.MonkeyPatch): """Test that timeout errors are properly propagated.""" server, _, _ = _register_with_stub_execute( monkeypatch, execute_side_effect=TimeoutError("timed out"), ) tool = server.tools["execute_query"] with pytest.raises(TimeoutError) as exc_info: await tool("SELECT 1", timeout_seconds=12, reason="test timeout") assert "timed out" in str(exc_info.value) @pytest.mark.asyncio async def test_execute_query_tool_handles_value_errors(monkeypatch: pytest.MonkeyPatch): """Test that validation errors are properly propagated for non-numeric timeout strings.""" from igloo_mcp.mcp.exceptions import MCPValidationError server, _, _ = _register_with_stub_execute( monkeypatch, execute_side_effect=ValueError("timeout_seconds must be an integer value in seconds."), ) tool = server.tools["execute_query"] with pytest.raises(MCPValidationError) as exc_info: await tool("SELECT 1", timeout_seconds="invalid", reason="test param error") assert "timeout_seconds" in str(exc_info.value) assert "invalid" in str(exc_info.value) @pytest.mark.asyncio async def test_execute_query_tool_accepts_string_timeout( monkeypatch: pytest.MonkeyPatch, ): """Test that numeric string timeouts are accepted and coerced to int (#48).""" server, execute_mock, _ = _register_with_stub_execute(monkeypatch) tool = server.tools["execute_query"] await tool("SELECT 1", timeout_seconds="45", reason="string timeout") # String should be coerced to int assert execute_mock.await_args.kwargs["timeout_seconds"] == 45 @pytest.mark.asyncio async def test_execute_query_tool_handles_runtime_errors( monkeypatch: pytest.MonkeyPatch, ): """Test that runtime errors are properly propagated.""" server, _, _ = _register_with_stub_execute( monkeypatch, execute_side_effect=RuntimeError("bad failure"), ) tool = server.tools["execute_query"] with pytest.raises(RuntimeError) as exc_info: await tool("SELECT 1", reason="test compact error") assert "bad failure" in str(exc_info.value) @pytest.mark.asyncio async def test_execute_query_tool_verbose_errors(monkeypatch: pytest.MonkeyPatch): """Test that verbose errors include full details.""" server, _, _ = _register_with_stub_execute( monkeypatch, execute_side_effect=RuntimeError("detailed failure"), ) tool = server.tools["execute_query"] with pytest.raises(RuntimeError) as exc_info: await tool("SELECT 1", verbose_errors=True, reason="test verbose error") assert "detailed failure" in str(exc_info.value) def test_apply_config_overrides_sets_env(monkeypatch: pytest.MonkeyPatch): args = SimpleNamespace( snowcli_config=None, profile="TEST_PROFILE", warehouse=None, database=None, schema=None, role=None, ) config = Config.from_env() def fake_load_config(config_path=None, cli_overrides=None): assert cli_overrides == {"profile": "TEST_PROFILE"} return config monkeypatch.setattr("igloo_mcp.mcp_server.load_config", fake_load_config) cfg = mcp_server._apply_config_overrides(args) assert cfg is config assert os.environ["SNOWFLAKE_PROFILE"] == config.snowflake.profile def test_apply_config_overrides_raises_on_failure(monkeypatch: pytest.MonkeyPatch): args = SimpleNamespace( snowcli_config=None, profile=None, warehouse=None, database=None, schema=None, role=None, ) def fake_load_config(config_path=None, cli_overrides=None): raise mcp_server.ConfigError("boom") monkeypatch.setattr("igloo_mcp.mcp_server.load_config", fake_load_config) with pytest.raises(SystemExit) as exc_info: mcp_server._apply_config_overrides(args) assert "Failed to load configuration" in str(exc_info.value)

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