-
Notifications
You must be signed in to change notification settings - Fork 117
Expand file tree
/
Copy pathtest_router.py
More file actions
294 lines (219 loc) · 11.2 KB
/
Copy pathtest_router.py
File metadata and controls
294 lines (219 loc) · 11.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
"""
Tests for the router module
"""
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from mcp import InitializeResult
from mcp.types import ListToolsResult, ServerCapabilities, Tool, ToolsCapability
from mcpm.core.router.client_connection import ServerConnection
from mcpm.router.router import MCPRouter
from mcpm.router.router_config import RouterConfig
from mcpm.schemas.server_config import RemoteServerConfig
@pytest.fixture
def mock_server_connection():
"""Create a mock server connection for testing"""
mock_conn = MagicMock(spec=ServerConnection)
mock_conn.healthy.return_value = True
mock_conn.request_for_shutdown = AsyncMock()
# Create valid ServerCapabilities with ToolsCapability
tools_capability = ToolsCapability(listChanged=False)
capabilities = ServerCapabilities(
prompts=None, resources=None, tools=tools_capability, logging=None, experimental={}
)
# Mock session initialized response
mock_conn.session_initialized_response = InitializeResult(
protocolVersion="1.0", capabilities=capabilities, serverInfo={"name": "test-server", "version": "1.0.0"}
)
# Mock session
mock_session = AsyncMock()
# Create a valid tool with proper inputSchema structure
mock_tool = Tool(name="test-tool", description="A test tool", inputSchema={"type": "object", "properties": {}})
# Create a ListToolsResult to be returned directly
tools_result = ListToolsResult(tools=[mock_tool])
mock_session.list_tools = AsyncMock(return_value=tools_result)
# If you have prompts/resources, mock them similarly:
mock_session.list_prompts = AsyncMock(return_value=MagicMock(prompts=[]))
mock_session.list_resources = AsyncMock(return_value=MagicMock(resources=[]))
mock_session.list_resource_templates = AsyncMock(return_value=MagicMock(resourceTemplates=[]))
mock_conn.session = mock_session
return mock_conn
@pytest.mark.asyncio
async def test_router_init():
"""Test initializing the router"""
# Test with default values
router = MCPRouter()
assert router.profile_manager is not None
assert router.watcher is None
assert router.router_config is not None
assert router.router_config.strict is False
# Test with custom values
config = RouterConfig(api_key="test-api-key", strict=True)
router = MCPRouter(
reload_server=True,
router_config=config,
)
assert router.watcher is not None
assert router.router_config == config
assert router.router_config.api_key == "test-api-key"
assert router.router_config.strict is True
@pytest.mark.asyncio
async def test_add_server(mock_server_connection):
"""Test adding a server to the router"""
router = MCPRouter()
# Mock get_active_servers to return all server IDs
def mock_get_active_servers(_profile):
return list(router.server_sessions.keys())
# Patch the _patch_handler_func method to use our mock
with patch.object(router, "_patch_handler_func", wraps=router._patch_handler_func) as mock_patch_handler:
mock_patch_handler.return_value.get_target_servers = mock_get_active_servers
server_config = RemoteServerConfig(name="test-server", url="http://localhost:8080/sse")
with patch("mcpm.core.router.router.ServerConnection", return_value=mock_server_connection):
await router.add_server("test-server", server_config)
# Verify server was added
assert "test-server" in router.server_sessions
assert router.server_sessions["test-server"] == mock_server_connection
# Verify capabilities were stored
assert "test-server" in router.capabilities_mapping
# Verify tool was stored
assert "test-tool" in router.tools_mapping
assert router.capabilities_to_server_id["tools"]["test-tool"] == "test-server"
# Test adding duplicate server
with pytest.raises(ValueError):
await router.add_server("test-server", server_config)
@pytest.mark.asyncio
async def test_add_server_unhealthy():
"""Test adding an unhealthy server"""
router = MCPRouter()
server_config = RemoteServerConfig(name="unhealthy-server", url="http://localhost:8080/sse")
mock_conn = MagicMock(spec=ServerConnection)
mock_conn.healthy.return_value = False
with patch("mcpm.core.router.router.ServerConnection", return_value=mock_conn):
with pytest.raises(ValueError, match="Failed to connect to server unhealthy-server"):
await router.add_server("unhealthy-server", server_config)
@pytest.mark.asyncio
async def test_remove_server():
"""Test removing a server from the router"""
router = MCPRouter()
# Setup mock server session with an awaitable request_for_shutdown
mock_session = AsyncMock()
mock_session.close = AsyncMock()
mock_server = MagicMock(spec=ServerConnection)
mock_server.session = mock_session
mock_server.request_for_shutdown = AsyncMock()
# Mock server and capabilities
router.server_sessions = {"test-server": mock_server}
router.capabilities_mapping = {"test-server": {"tools": True}}
router.capabilities_to_server_id = {"tools": {"test-tool": "test-server"}}
router.tools_mapping = {"test-tool": MagicMock()}
# Remove server
await router.remove_server("test-server")
# Verify server was removed
assert "test-server" not in router.server_sessions
assert "test-server" not in router.capabilities_mapping
assert "test-tool" not in router.capabilities_to_server_id["tools"]
assert "test-tool" not in router.tools_mapping
# Verify request_for_shutdown was called
mock_server.request_for_shutdown.assert_called_once()
# Test removing non-existent server
with pytest.raises(ValueError, match="Server with ID non-existent does not exist"):
await router.remove_server("non-existent")
@pytest.mark.asyncio
async def test_update_servers(mock_server_connection):
"""Test updating servers based on configuration"""
router = MCPRouter()
# Mock get_active_servers to return all server IDs
def mock_get_active_servers(_profile):
return list(router.server_sessions.keys())
# Patch the _patch_handler_func method to use our mock
with patch.object(router, "_patch_handler_func", wraps=router._patch_handler_func) as mock_patch_handler:
mock_patch_handler.return_value.get_target_servers = mock_get_active_servers
# Setup initial servers with awaitable request_for_shutdown
mock_old_server = MagicMock(spec=ServerConnection)
mock_old_server.session = AsyncMock()
mock_old_server.request_for_shutdown = AsyncMock()
router.server_sessions = {"old-server": mock_old_server}
# Initialize capabilities_mapping for the old server
router.capabilities_mapping = {"old-server": {"tools": True}}
# Configure new servers
server_configs = [RemoteServerConfig(name="test-server", url="http://localhost:8080/sse")]
with patch("mcpm.core.router.router.ServerConnection", return_value=mock_server_connection):
await router.update_servers(server_configs)
# Verify old server was removed
assert "old-server" not in router.server_sessions
mock_old_server.request_for_shutdown.assert_called_once()
# Verify new server was added
assert "test-server" in router.server_sessions
# Test with empty configs - should not change anything
router.server_sessions = {"test-server": mock_server_connection}
await router.update_servers([])
assert "test-server" in router.server_sessions
@pytest.mark.asyncio
async def test_update_servers_error_handling():
"""Test error handling during server updates"""
router = MCPRouter()
# Setup initial servers with awaitable request_for_shutdown
mock_old_server = MagicMock(spec=ServerConnection)
mock_old_server.session = AsyncMock()
mock_old_server.request_for_shutdown = AsyncMock()
router.server_sessions = {"old-server": mock_old_server}
# Initialize capabilities_mapping for the old server
router.capabilities_mapping = {"old-server": {"tools": True}}
# Configure new servers
server_configs = [RemoteServerConfig(name="test-server", url="http://localhost:8080/sse")]
# Mock add_server to raise exception
with patch.object(router, "add_server", side_effect=Exception("Test error")):
# Should not raise exception
await router.update_servers(server_configs)
# Old server should still be removed
assert "old-server" not in router.server_sessions
mock_old_server.request_for_shutdown.assert_called_once()
# New server should not be added
assert "test-server" not in router.server_sessions
@pytest.mark.asyncio
async def test_router_sse_transport_no_api_key():
"""Test RouterSseTransport with no API key (authentication disabled)"""
from mcpm.router.transport import RouterSseTransport
# Create a RouterSseTransport with no API key
transport = RouterSseTransport("/messages/", api_key=None)
# Create a mock scope
mock_scope = {"type": "http"}
# Test _validate_api_key method directly
assert transport._validate_api_key(mock_scope, api_key=None)
assert transport._validate_api_key(mock_scope, api_key="any-key")
# Test with various API key values - all should be allowed
assert transport._validate_api_key(mock_scope, api_key="test-key")
assert transport._validate_api_key(mock_scope, api_key="invalid-key")
assert transport._validate_api_key(mock_scope, api_key="")
@pytest.mark.asyncio
async def test_router_sse_transport_with_api_key():
"""Test RouterSseTransport with API key (authentication enabled)"""
from mcpm.router.transport import RouterSseTransport
# Create a RouterSseTransport with an API key
transport = RouterSseTransport("/messages/", api_key="correct-api-key")
# Create a mock scope
mock_scope = {"type": "http"}
# Test _validate_api_key method directly
# With the correct API key
assert transport._validate_api_key(mock_scope, api_key="correct-api-key")
# With an incorrect API key
assert not transport._validate_api_key(mock_scope, api_key="wrong-api-key")
# With no API key
assert not transport._validate_api_key(mock_scope, api_key=None)
# Test with empty string
assert not transport._validate_api_key(mock_scope, api_key="")
@pytest.mark.asyncio
async def test_get_sse_server_app_with_api_key():
with patch("mcpm.router.router.RouterSseTransport") as mock_transport:
router = MCPRouter(router_config=RouterConfig(auth_enabled=True, api_key="test-api-key"))
await router.get_sse_server_app()
mock_transport.assert_called_once()
call_kwargs = mock_transport.call_args[1]
assert call_kwargs.get("api_key") == "test-api-key"
@pytest.mark.asyncio
async def test_get_sse_server_app_without_api_key():
with patch("mcpm.router.router.RouterSseTransport") as mock_transport:
router = MCPRouter(router_config=RouterConfig(auth_enabled=False, api_key="custom-secret"))
await router.get_sse_server_app()
mock_transport.assert_called_once()
call_kwargs = mock_transport.call_args[1]
assert call_kwargs.get("api_key") is None