Skip to content

Commit 73b6ac4

Browse files
authored
Merge pull request #60 from natifridman/tests
Add comprehensive pytest unit tests for MCP tools
2 parents 7356404 + a7c3638 commit 73b6ac4

File tree

12 files changed

+2273
-368
lines changed

12 files changed

+2273
-368
lines changed

.github/workflows/python-lint.yaml

Lines changed: 0 additions & 27 deletions
This file was deleted.

.github/workflows/test.yml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: Tests
2+
3+
on:
4+
pull_request:
5+
branches: [ main ]
6+
7+
jobs:
8+
lint:
9+
name: Code Linting
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- name: Install uv
16+
uses: astral-sh/setup-uv@v3
17+
with:
18+
version: "latest"
19+
20+
- name: Set up Python
21+
run: uv python install 3.11
22+
23+
- name: Install dependencies
24+
run: uv sync --dev
25+
26+
- name: Run flake8
27+
run: uv run flake8 src/ --max-line-length=120 --ignore=E501,W503
28+
29+
test:
30+
name: Tests & Coverage
31+
runs-on: ubuntu-latest
32+
needs: lint
33+
34+
steps:
35+
- uses: actions/checkout@v4
36+
37+
- name: Install uv
38+
uses: astral-sh/setup-uv@v3
39+
with:
40+
version: "latest"
41+
42+
- name: Set up Python
43+
run: uv python install 3.11
44+
45+
- name: Install dependencies
46+
run: uv sync --dev
47+
48+
- name: Run tests with coverage
49+
run: |
50+
uv add --dev pytest-cov
51+
uv run pytest tests/ --cov=src --cov-report=xml --cov-report=term -v --tb=short
52+
53+
- name: Upload coverage to Codecov
54+
uses: codecov/codecov-action@v4
55+
with:
56+
file: ./coverage.xml
57+
flags: unittests
58+
name: codecov-umbrella
59+
fail_ci_if_error: false

codecov.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
coverage:
2+
status:
3+
project: # add everything under here, more options at https://docs.codecov.com/docs/commit-status
4+
default:
5+
# basic
6+
target: auto # default
7+
threshold: 0%
8+
base: auto

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)