Skip to content

Commit 391f2b9

Browse files
author
LittleCoinCoin
committed
test: add comprehensive MCPServerConfig model validation tests
Implement 14 regression tests validating consolidated MCPServerConfig Pydantic model with both local and remote server configurations. Test coverage: - Local server validation (command, args, env fields) - Remote server validation (url, headers fields) - Cross-field validation preventing both command and URL - Field combination validation (args with command, headers with URL) - Empty/whitespace command validation with proper error messages - URL format validation requiring http:// or https:// protocols - Future extension field rejection (timeout, retry_attempts, ssl_verify) - Serialization roundtrip testing with model_dump/model_dump_json - JSON compatibility validation for API integration - Minimal configuration testing for both server types All tests use Pydantic v2 methods (model_dump, model_dump_json) and follow organizational testing standards with wobble framework integration. Validates elimination of redundant HostServerConfig class per v2 requirements.
1 parent 688b4ed commit 391f2b9

File tree

1 file changed

+239
-0
lines changed

1 file changed

+239
-0
lines changed
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
"""
2+
Test suite for consolidated MCPServerConfig Pydantic model.
3+
4+
This module tests the consolidated MCPServerConfig model that supports
5+
both local and remote server configurations with proper validation.
6+
"""
7+
8+
import unittest
9+
import sys
10+
from pathlib import Path
11+
12+
# Add the parent directory to the path to import wobble
13+
sys.path.insert(0, str(Path(__file__).parent.parent))
14+
15+
try:
16+
from wobble.decorators import regression_test, integration_test
17+
except ImportError:
18+
# Fallback decorators if wobble is not available
19+
def regression_test(func):
20+
return func
21+
22+
def integration_test(scope="component"):
23+
def decorator(func):
24+
return func
25+
return decorator
26+
27+
from test_data_utils import MCPHostConfigTestDataLoader
28+
from hatch.mcp_host_config.models import MCPServerConfig
29+
from pydantic import ValidationError
30+
31+
32+
class TestMCPServerConfigModels(unittest.TestCase):
33+
"""Test suite for consolidated MCPServerConfig Pydantic model."""
34+
35+
def setUp(self):
36+
"""Set up test environment."""
37+
self.test_data_loader = MCPHostConfigTestDataLoader()
38+
39+
@regression_test
40+
def test_mcp_server_config_local_server_validation_success(self):
41+
"""Test successful local server configuration validation."""
42+
config_data = self.test_data_loader.load_mcp_server_config("local")
43+
config = MCPServerConfig(**config_data)
44+
45+
self.assertEqual(config.command, "python")
46+
self.assertEqual(len(config.args), 3)
47+
self.assertEqual(config.env["API_KEY"], "test")
48+
self.assertTrue(config.is_local_server)
49+
self.assertFalse(config.is_remote_server)
50+
51+
@regression_test
52+
def test_mcp_server_config_remote_server_validation_success(self):
53+
"""Test successful remote server configuration validation."""
54+
config_data = self.test_data_loader.load_mcp_server_config("remote")
55+
config = MCPServerConfig(**config_data)
56+
57+
self.assertEqual(config.url, "https://api.example.com/mcp")
58+
self.assertEqual(config.headers["Authorization"], "Bearer token")
59+
self.assertFalse(config.is_local_server)
60+
self.assertTrue(config.is_remote_server)
61+
62+
@regression_test
63+
def test_mcp_server_config_validation_fails_both_command_and_url(self):
64+
"""Test validation fails when both command and URL are provided."""
65+
config_data = {
66+
"command": "python",
67+
"args": ["server.py"],
68+
"url": "https://example.com/mcp" # Invalid: both command and URL
69+
}
70+
71+
with self.assertRaises(ValidationError) as context:
72+
MCPServerConfig(**config_data)
73+
74+
self.assertIn("Cannot specify both 'command' and 'url'", str(context.exception))
75+
76+
@regression_test
77+
def test_mcp_server_config_validation_fails_neither_command_nor_url(self):
78+
"""Test validation fails when neither command nor URL are provided."""
79+
config_data = {
80+
"env": {"TEST": "value"}
81+
# Missing both command and url
82+
}
83+
84+
with self.assertRaises(ValidationError) as context:
85+
MCPServerConfig(**config_data)
86+
87+
self.assertIn("Either 'command' (local server) or 'url' (remote server) must be provided",
88+
str(context.exception))
89+
90+
@regression_test
91+
def test_mcp_server_config_validation_args_without_command_fails(self):
92+
"""Test validation fails when args provided without command."""
93+
config_data = {
94+
"url": "https://example.com/mcp",
95+
"args": ["--flag"] # Invalid: args without command
96+
}
97+
98+
with self.assertRaises(ValidationError) as context:
99+
MCPServerConfig(**config_data)
100+
101+
self.assertIn("'args' can only be specified with 'command'", str(context.exception))
102+
103+
@regression_test
104+
def test_mcp_server_config_validation_headers_without_url_fails(self):
105+
"""Test validation fails when headers provided without URL."""
106+
config_data = {
107+
"command": "python",
108+
"headers": {"Authorization": "Bearer token"} # Invalid: headers without URL
109+
}
110+
111+
with self.assertRaises(ValidationError) as context:
112+
MCPServerConfig(**config_data)
113+
114+
self.assertIn("'headers' can only be specified with 'url'", str(context.exception))
115+
116+
@regression_test
117+
def test_mcp_server_config_url_format_validation(self):
118+
"""Test URL format validation."""
119+
invalid_urls = ["ftp://example.com", "example.com", "not-a-url"]
120+
121+
for invalid_url in invalid_urls:
122+
with self.assertRaises(ValidationError):
123+
MCPServerConfig(url=invalid_url)
124+
125+
@regression_test
126+
def test_mcp_server_config_no_future_extension_fields(self):
127+
"""Test that future extension fields are not present."""
128+
# These fields should not be accepted (removed in v2)
129+
config_data = {
130+
"command": "python",
131+
"timeout": 30, # Should be rejected
132+
"retry_attempts": 3, # Should be rejected
133+
"ssl_verify": True # Should be rejected
134+
}
135+
136+
with self.assertRaises(ValidationError) as context:
137+
MCPServerConfig(**config_data)
138+
139+
# Should fail due to extra fields being forbidden
140+
self.assertIn("Extra inputs are not permitted", str(context.exception))
141+
142+
@regression_test
143+
def test_mcp_server_config_command_empty_validation(self):
144+
"""Test validation fails for empty command."""
145+
config_data = {
146+
"command": " ", # Empty/whitespace command
147+
"args": ["server.py"]
148+
}
149+
150+
with self.assertRaises(ValidationError) as context:
151+
MCPServerConfig(**config_data)
152+
153+
self.assertIn("Command cannot be empty", str(context.exception))
154+
155+
@regression_test
156+
def test_mcp_server_config_command_strip_whitespace(self):
157+
"""Test command whitespace is stripped."""
158+
config_data = {
159+
"command": " python ",
160+
"args": ["server.py"]
161+
}
162+
163+
config = MCPServerConfig(**config_data)
164+
self.assertEqual(config.command, "python")
165+
166+
@regression_test
167+
def test_mcp_server_config_minimal_local_server(self):
168+
"""Test minimal local server configuration."""
169+
config_data = self.test_data_loader.load_mcp_server_config("local_minimal")
170+
config = MCPServerConfig(**config_data)
171+
172+
self.assertEqual(config.command, "python")
173+
self.assertEqual(config.args, ["minimal_server.py"])
174+
self.assertIsNone(config.env)
175+
self.assertTrue(config.is_local_server)
176+
self.assertFalse(config.is_remote_server)
177+
178+
@regression_test
179+
def test_mcp_server_config_minimal_remote_server(self):
180+
"""Test minimal remote server configuration."""
181+
config_data = self.test_data_loader.load_mcp_server_config("remote_minimal")
182+
config = MCPServerConfig(**config_data)
183+
184+
self.assertEqual(config.url, "https://minimal.example.com/mcp")
185+
self.assertIsNone(config.headers)
186+
self.assertFalse(config.is_local_server)
187+
self.assertTrue(config.is_remote_server)
188+
189+
@regression_test
190+
def test_mcp_server_config_serialization_roundtrip(self):
191+
"""Test serialization and deserialization roundtrip."""
192+
# Test local server
193+
local_config_data = self.test_data_loader.load_mcp_server_config("local")
194+
local_config = MCPServerConfig(**local_config_data)
195+
196+
# Serialize and deserialize
197+
serialized = local_config.model_dump()
198+
roundtrip_config = MCPServerConfig(**serialized)
199+
200+
self.assertEqual(local_config.command, roundtrip_config.command)
201+
self.assertEqual(local_config.args, roundtrip_config.args)
202+
self.assertEqual(local_config.env, roundtrip_config.env)
203+
self.assertEqual(local_config.is_local_server, roundtrip_config.is_local_server)
204+
205+
# Test remote server
206+
remote_config_data = self.test_data_loader.load_mcp_server_config("remote")
207+
remote_config = MCPServerConfig(**remote_config_data)
208+
209+
# Serialize and deserialize
210+
serialized = remote_config.model_dump()
211+
roundtrip_config = MCPServerConfig(**serialized)
212+
213+
self.assertEqual(remote_config.url, roundtrip_config.url)
214+
self.assertEqual(remote_config.headers, roundtrip_config.headers)
215+
self.assertEqual(remote_config.is_remote_server, roundtrip_config.is_remote_server)
216+
217+
@regression_test
218+
def test_mcp_server_config_json_serialization(self):
219+
"""Test JSON serialization compatibility."""
220+
import json
221+
222+
config_data = self.test_data_loader.load_mcp_server_config("local")
223+
config = MCPServerConfig(**config_data)
224+
225+
# Test JSON serialization
226+
json_str = config.model_dump_json()
227+
self.assertIsInstance(json_str, str)
228+
229+
# Test JSON deserialization
230+
parsed_data = json.loads(json_str)
231+
roundtrip_config = MCPServerConfig(**parsed_data)
232+
233+
self.assertEqual(config.command, roundtrip_config.command)
234+
self.assertEqual(config.args, roundtrip_config.args)
235+
self.assertEqual(config.env, roundtrip_config.env)
236+
237+
238+
if __name__ == '__main__':
239+
unittest.main()

0 commit comments

Comments
 (0)