Skip to content

Commit e23684c

Browse files
natifridmanclaude
andcommitted
Add comprehensive pytest unit tests for MCP tools
- Add 97 passing unit tests covering all MCP functionality - Test tools.py: Token handling, all 5 MCP tools, SQL injection prevention - Test database.py: Snowflake queries, pagination, error handling - Test mcp_server.py: Server lifecycle, tool registration, metrics integration - Test config.py: Environment variables, transport modes, Prometheus setup - Test metrics.py: Usage tracking, HTTP server, health endpoints - Add pytest configuration and dependencies to pyproject.toml - Include comprehensive async testing with proper mocking - Cover edge cases, exception handling, and security features 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 7356404 commit e23684c

File tree

8 files changed

+1746
-0
lines changed

8 files changed

+1746
-0
lines changed

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,6 @@ packages = ["src"]
2424
dev-dependencies = [
2525
"flake8>=7.3.0",
2626
"requests>=2.32.4",
27+
"pytest>=8.0.0",
28+
"pytest-asyncio>=0.23.0",
2729
]

pytest.ini

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[tool:pytest]
2+
testpaths = tests
3+
python_files = test_*.py
4+
python_classes = Test*
5+
python_functions = test_*
6+
addopts =
7+
-v
8+
--tb=short
9+
--strict-markers
10+
--disable-warnings
11+
filterwarnings =
12+
ignore::DeprecationWarning
13+
ignore::PendingDeprecationWarning
14+
markers =
15+
asyncio: marks tests as async
16+
slow: marks tests as slow (deselect with '-m "not slow"')
17+
integration: marks tests as integration tests

tests/__init__.py

Whitespace-only changes.

tests/test_config.py

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import pytest
2+
from unittest.mock import patch
3+
import sys
4+
import os
5+
6+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../src'))
7+
8+
9+
class TestConfig:
10+
"""Test cases for configuration module"""
11+
12+
@patch.dict('os.environ', {
13+
'MCP_TRANSPORT': 'http',
14+
'SNOWFLAKE_BASE_URL': 'https://test.snowflake.com',
15+
'SNOWFLAKE_DATABASE': 'TEST_DB',
16+
'SNOWFLAKE_SCHEMA': 'TEST_SCHEMA',
17+
'SNOWFLAKE_WAREHOUSE': 'TEST_WH',
18+
'INTERNAL_GATEWAY': 'true',
19+
'ENABLE_METRICS': 'true',
20+
'METRICS_PORT': '9090'
21+
})
22+
def test_config_from_environment(self):
23+
"""Test configuration loading from environment variables"""
24+
# Re-import to get fresh config with mocked environment
25+
import importlib
26+
import config
27+
importlib.reload(config)
28+
29+
assert config.MCP_TRANSPORT == 'http'
30+
assert config.SNOWFLAKE_BASE_URL == 'https://test.snowflake.com'
31+
assert config.SNOWFLAKE_DATABASE == 'TEST_DB'
32+
assert config.SNOWFLAKE_SCHEMA == 'TEST_SCHEMA'
33+
assert config.SNOWFLAKE_WAREHOUSE == 'TEST_WH'
34+
assert config.INTERNAL_GATEWAY == 'true'
35+
assert config.ENABLE_METRICS is True
36+
assert config.METRICS_PORT == 9090
37+
38+
@patch.dict('os.environ', {}, clear=True)
39+
def test_config_defaults(self):
40+
"""Test configuration defaults when environment variables are not set"""
41+
import importlib
42+
import config
43+
importlib.reload(config)
44+
45+
assert config.MCP_TRANSPORT == 'stdio' # Default
46+
assert config.SNOWFLAKE_WAREHOUSE == 'DEFAULT' # Default
47+
assert config.INTERNAL_GATEWAY == 'false' # Default
48+
assert config.ENABLE_METRICS is False # Default
49+
assert config.METRICS_PORT == 8000 # Default
50+
51+
# These should be None when not set
52+
assert config.SNOWFLAKE_BASE_URL is None
53+
assert config.SNOWFLAKE_DATABASE is None
54+
assert config.SNOWFLAKE_SCHEMA is None
55+
56+
@patch.dict('os.environ', {'MCP_TRANSPORT': 'stdio', 'SNOWFLAKE_TOKEN': 'test_token'})
57+
def test_stdio_transport_token_handling(self):
58+
"""Test token handling for stdio transport"""
59+
import importlib
60+
import config
61+
importlib.reload(config)
62+
63+
assert config.MCP_TRANSPORT == 'stdio'
64+
assert config.SNOWFLAKE_TOKEN == 'test_token'
65+
66+
@patch.dict('os.environ', {'MCP_TRANSPORT': 'http', 'SNOWFLAKE_TOKEN': 'test_token'})
67+
def test_non_stdio_transport_token_handling(self):
68+
"""Test token handling for non-stdio transport"""
69+
import importlib
70+
import config
71+
importlib.reload(config)
72+
73+
assert config.MCP_TRANSPORT == 'http'
74+
assert config.SNOWFLAKE_TOKEN is None # Should be None for non-stdio
75+
76+
@patch.dict('os.environ', {'ENABLE_METRICS': 'false'})
77+
def test_metrics_disabled(self):
78+
"""Test metrics configuration when disabled"""
79+
import importlib
80+
import config
81+
importlib.reload(config)
82+
83+
assert config.ENABLE_METRICS is False
84+
85+
@patch.dict('os.environ', {'ENABLE_METRICS': 'true'})
86+
def test_metrics_enabled(self):
87+
"""Test metrics configuration when enabled"""
88+
import importlib
89+
import config
90+
importlib.reload(config)
91+
92+
assert config.ENABLE_METRICS is True
93+
94+
@patch.dict('os.environ', {'ENABLE_METRICS': 'TRUE'})
95+
def test_metrics_case_insensitive(self):
96+
"""Test that metrics configuration is case insensitive"""
97+
import importlib
98+
import config
99+
importlib.reload(config)
100+
101+
assert config.ENABLE_METRICS is True
102+
103+
@patch.dict('os.environ', {'ENABLE_METRICS': 'yes'})
104+
def test_metrics_non_true_value(self):
105+
"""Test that non-'true' values disable metrics"""
106+
import importlib
107+
import config
108+
importlib.reload(config)
109+
110+
assert config.ENABLE_METRICS is False
111+
112+
@patch.dict('os.environ', {'METRICS_PORT': 'not_a_number'})
113+
def test_invalid_metrics_port(self):
114+
"""Test handling of invalid metrics port"""
115+
import importlib
116+
117+
with pytest.raises(ValueError):
118+
import config
119+
importlib.reload(config)
120+
121+
def test_prometheus_availability_check(self):
122+
"""Test Prometheus availability detection"""
123+
import importlib
124+
import config
125+
importlib.reload(config)
126+
127+
# PROMETHEUS_AVAILABLE should be True or False
128+
assert isinstance(config.PROMETHEUS_AVAILABLE, bool)
129+
130+
@patch('config.logging.basicConfig')
131+
def test_logging_configuration(self, mock_basicconfig):
132+
"""Test that logging is configured"""
133+
import importlib
134+
import config
135+
importlib.reload(config)
136+
137+
# Verify logging.basicConfig was called
138+
mock_basicconfig.assert_called_once()
139+
140+
# Check the configuration parameters
141+
call_args = mock_basicconfig.call_args
142+
assert call_args[1]['level'] == config.logging.INFO
143+
assert 'format' in call_args[1]
144+
assert 'handlers' in call_args[1]
145+
146+
@patch.dict('os.environ', {'INTERNAL_GATEWAY': 'FALSE'})
147+
def test_internal_gateway_case_insensitive(self):
148+
"""Test that internal gateway configuration is case insensitive"""
149+
import importlib
150+
import config
151+
importlib.reload(config)
152+
153+
assert config.INTERNAL_GATEWAY == 'FALSE' # Should preserve original case
154+
155+
@patch.dict('os.environ', {
156+
'SNOWFLAKE_BASE_URL': '',
157+
'SNOWFLAKE_DATABASE': '',
158+
'SNOWFLAKE_SCHEMA': ''
159+
})
160+
def test_empty_string_environment_variables(self):
161+
"""Test handling of empty string environment variables"""
162+
import importlib
163+
import config
164+
importlib.reload(config)
165+
166+
# Empty strings should be treated as None/empty
167+
assert config.SNOWFLAKE_BASE_URL == ''
168+
assert config.SNOWFLAKE_DATABASE == ''
169+
assert config.SNOWFLAKE_SCHEMA == ''
170+
171+
def test_config_constants_immutability(self):
172+
"""Test that configuration values are set as expected"""
173+
import config
174+
175+
# Test that we can access the configuration values
176+
# (immutability would be tested by trying to modify them)
177+
assert hasattr(config, 'MCP_TRANSPORT')
178+
assert hasattr(config, 'SNOWFLAKE_BASE_URL')
179+
assert hasattr(config, 'SNOWFLAKE_DATABASE')
180+
assert hasattr(config, 'SNOWFLAKE_SCHEMA')
181+
assert hasattr(config, 'SNOWFLAKE_WAREHOUSE')
182+
assert hasattr(config, 'INTERNAL_GATEWAY')
183+
assert hasattr(config, 'SNOWFLAKE_TOKEN')
184+
assert hasattr(config, 'ENABLE_METRICS')
185+
assert hasattr(config, 'METRICS_PORT')
186+
assert hasattr(config, 'PROMETHEUS_AVAILABLE')
187+
188+
def test_prometheus_import_error(self):
189+
"""Test handling when prometheus_client import fails"""
190+
# Test the import check logic directly
191+
import sys
192+
import importlib
193+
194+
# Temporarily remove prometheus_client from modules if it exists
195+
prometheus_module = sys.modules.pop('prometheus_client', None)
196+
197+
try:
198+
# Mock the import to fail
199+
with patch.dict('sys.modules', {'prometheus_client': None}):
200+
# Reload config to trigger the import check
201+
import config
202+
importlib.reload(config)
203+
204+
# Should detect that prometheus is not available
205+
assert config.PROMETHEUS_AVAILABLE is False
206+
finally:
207+
# Restore the module if it was there
208+
if prometheus_module is not None:
209+
sys.modules['prometheus_client'] = prometheus_module

0 commit comments

Comments
 (0)