Skip to content

Commit dba9456

Browse files
Copilotpontemonti
andcommitted
Add comprehensive tests for custom MCP server URL support
Co-authored-by: pontemonti <7850950+pontemonti@users.noreply.github.com>
1 parent 7468ea0 commit dba9456

2 files changed

Lines changed: 240 additions & 0 deletions

File tree

tests/tooling/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
"""Tests for tooling components."""
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
"""Unit tests for MCP Server Configuration Service."""
5+
6+
import json
7+
import os
8+
from pathlib import Path
9+
from typing import Dict, Any
10+
from unittest.mock import Mock, patch, AsyncMock, MagicMock
11+
import pytest
12+
import aiohttp
13+
14+
from microsoft_agents_a365.tooling.services.mcp_tool_server_configuration_service import (
15+
McpToolServerConfigurationService,
16+
)
17+
from microsoft_agents_a365.tooling.models import MCPServerConfig
18+
19+
20+
class TestMCPServerConfig:
21+
"""Tests for MCPServerConfig model."""
22+
23+
def test_mcp_server_config_with_custom_url(self):
24+
"""Test that MCPServerConfig can be created with a custom URL."""
25+
config = MCPServerConfig(
26+
mcp_server_name="TestServer",
27+
mcp_server_unique_name="test_server",
28+
url="https://custom.mcp.server/endpoint",
29+
)
30+
31+
assert config.mcp_server_name == "TestServer"
32+
assert config.mcp_server_unique_name == "test_server"
33+
assert config.url == "https://custom.mcp.server/endpoint"
34+
35+
def test_mcp_server_config_without_custom_url(self):
36+
"""Test that MCPServerConfig works without a custom URL."""
37+
config = MCPServerConfig(
38+
mcp_server_name="TestServer",
39+
mcp_server_unique_name="test_server",
40+
)
41+
42+
assert config.mcp_server_name == "TestServer"
43+
assert config.mcp_server_unique_name == "test_server"
44+
assert config.url is None
45+
46+
def test_mcp_server_config_validation(self):
47+
"""Test that MCPServerConfig validates required fields."""
48+
with pytest.raises(ValueError, match="mcp_server_name cannot be empty"):
49+
MCPServerConfig(mcp_server_name="", mcp_server_unique_name="test")
50+
51+
with pytest.raises(ValueError, match="mcp_server_unique_name cannot be empty"):
52+
MCPServerConfig(mcp_server_name="test", mcp_server_unique_name="")
53+
54+
55+
class TestMcpToolServerConfigurationService:
56+
"""Tests for McpToolServerConfigurationService."""
57+
58+
@pytest.fixture
59+
def service(self):
60+
"""Create a service instance for testing."""
61+
return McpToolServerConfigurationService()
62+
63+
@pytest.fixture
64+
def mock_manifest_data(self) -> Dict[str, Any]:
65+
"""Create mock manifest data."""
66+
return {
67+
"mcpServers": [
68+
{
69+
"mcpServerName": "TestServer1",
70+
"mcpServerUniqueName": "test_server_1",
71+
},
72+
{
73+
"mcpServerName": "TestServer2",
74+
"mcpServerUniqueName": "test_server_2",
75+
"mcpServerUrl": "https://custom.server.com/mcp",
76+
},
77+
]
78+
}
79+
80+
def test_extract_server_url_from_manifest(self, service):
81+
"""Test extracting custom URL from manifest element."""
82+
# Test with mcpServerUrl field
83+
element = {"mcpServerUrl": "https://custom.url.com"}
84+
url = service._extract_server_url(element)
85+
assert url == "https://custom.url.com"
86+
87+
# Test with url field
88+
element = {"url": "https://another.url.com"}
89+
url = service._extract_server_url(element)
90+
assert url == "https://another.url.com"
91+
92+
# Test with no URL
93+
element = {}
94+
url = service._extract_server_url(element)
95+
assert url is None
96+
97+
def test_parse_manifest_server_config_with_custom_url(self, service):
98+
"""Test parsing manifest config with custom URL."""
99+
server_element = {
100+
"mcpServerName": "CustomServer",
101+
"mcpServerUniqueName": "custom_server",
102+
"mcpServerUrl": "https://my.custom.server/mcp",
103+
}
104+
105+
config = service._parse_manifest_server_config(server_element)
106+
107+
assert config is not None
108+
assert config.mcp_server_name == "CustomServer"
109+
assert config.mcp_server_unique_name == "custom_server"
110+
assert config.url == "https://my.custom.server/mcp"
111+
112+
@patch("microsoft_agents_a365.tooling.services.mcp_tool_server_configuration_service.build_mcp_server_url")
113+
def test_parse_manifest_server_config_without_custom_url(self, mock_build_url, service):
114+
"""Test parsing manifest config without custom URL constructs URL."""
115+
mock_build_url.return_value = "https://default.server/agents/servers/test_server"
116+
117+
server_element = {
118+
"mcpServerName": "DefaultServer",
119+
"mcpServerUniqueName": "test_server",
120+
}
121+
122+
config = service._parse_manifest_server_config(server_element)
123+
124+
assert config is not None
125+
assert config.mcp_server_name == "DefaultServer"
126+
# When no custom URL, the built URL goes into mcp_server_unique_name
127+
assert config.mcp_server_unique_name == "https://default.server/agents/servers/test_server"
128+
mock_build_url.assert_called_once_with("test_server")
129+
130+
def test_parse_gateway_server_config_with_custom_url(self, service):
131+
"""Test parsing gateway config with custom URL."""
132+
server_element = {
133+
"mcpServerName": "GatewayServer",
134+
"mcpServerUniqueName": "gateway_server_endpoint",
135+
"url": "https://gateway.custom.url/mcp",
136+
}
137+
138+
config = service._parse_gateway_server_config(server_element)
139+
140+
assert config is not None
141+
assert config.mcp_server_name == "GatewayServer"
142+
assert config.mcp_server_unique_name == "gateway_server_endpoint"
143+
assert config.url == "https://gateway.custom.url/mcp"
144+
145+
def test_parse_gateway_server_config_without_custom_url(self, service):
146+
"""Test parsing gateway config without custom URL."""
147+
server_element = {
148+
"mcpServerName": "GatewayServer",
149+
"mcpServerUniqueName": "https://gateway.default/endpoint",
150+
}
151+
152+
config = service._parse_gateway_server_config(server_element)
153+
154+
assert config is not None
155+
assert config.mcp_server_name == "GatewayServer"
156+
assert config.mcp_server_unique_name == "https://gateway.default/endpoint"
157+
assert config.url is None
158+
159+
@patch.dict(os.environ, {"ENVIRONMENT": "Development"})
160+
def test_is_development_scenario(self, service):
161+
"""Test development scenario detection."""
162+
assert service._is_development_scenario() is True
163+
164+
@patch.dict(os.environ, {"ENVIRONMENT": "Production"})
165+
def test_is_production_scenario(self, service):
166+
"""Test production scenario detection."""
167+
assert service._is_development_scenario() is False
168+
169+
@patch.object(McpToolServerConfigurationService, "_load_servers_from_manifest")
170+
@patch.dict(os.environ, {"ENVIRONMENT": "Development"})
171+
@pytest.mark.asyncio
172+
async def test_list_tool_servers_development(self, mock_load_manifest, service):
173+
"""Test listing servers in development mode."""
174+
mock_servers = [
175+
MCPServerConfig(
176+
mcp_server_name="DevServer",
177+
mcp_server_unique_name="dev_server",
178+
url="https://dev.server/mcp",
179+
)
180+
]
181+
mock_load_manifest.return_value = mock_servers
182+
183+
servers = await service.list_tool_servers(
184+
agentic_app_id="test-app-id", auth_token="test-token"
185+
)
186+
187+
assert servers == mock_servers
188+
mock_load_manifest.assert_called_once()
189+
190+
@patch("microsoft_agents_a365.tooling.services.mcp_tool_server_configuration_service.get_tooling_gateway_for_digital_worker")
191+
@patch.dict(os.environ, {"ENVIRONMENT": "Production"})
192+
@pytest.mark.asyncio
193+
async def test_list_tool_servers_production_with_custom_url(self, mock_gateway_url, service):
194+
"""Test listing servers in production mode with custom URL."""
195+
mock_gateway_url.return_value = "https://gateway.test/agents/test-app-id/mcpServers"
196+
197+
# Mock aiohttp response
198+
mock_response_data = {
199+
"mcpServers": [
200+
{
201+
"mcpServerName": "ProdServer",
202+
"mcpServerUniqueName": "prod_server",
203+
"url": "https://prod.custom.url/mcp",
204+
}
205+
]
206+
}
207+
208+
with patch("aiohttp.ClientSession") as mock_session_class:
209+
# Create proper async context managers
210+
mock_response = MagicMock()
211+
mock_response.status = 200
212+
mock_response.text = AsyncMock(return_value=json.dumps(mock_response_data))
213+
214+
# Create async context manager for response
215+
mock_response_cm = MagicMock()
216+
mock_response_cm.__aenter__ = AsyncMock(return_value=mock_response)
217+
mock_response_cm.__aexit__ = AsyncMock(return_value=None)
218+
219+
# Create async context manager for session
220+
mock_session = MagicMock()
221+
mock_session.get = MagicMock(return_value=mock_response_cm)
222+
223+
mock_session_cm = MagicMock()
224+
mock_session_cm.__aenter__ = AsyncMock(return_value=mock_session)
225+
mock_session_cm.__aexit__ = AsyncMock(return_value=None)
226+
227+
mock_session_class.return_value = mock_session_cm
228+
229+
servers = await service.list_tool_servers(
230+
agentic_app_id="test-app-id", auth_token="test-token"
231+
)
232+
233+
assert len(servers) == 1
234+
assert servers[0].mcp_server_name == "ProdServer"
235+
assert servers[0].mcp_server_unique_name == "prod_server"
236+
assert servers[0].url == "https://prod.custom.url/mcp"

0 commit comments

Comments
 (0)