refactor: drop test classes (#1924)
This commit is contained in:
parent
7b728a243e
commit
d77292fb06
12 changed files with 1611 additions and 1562 deletions
|
|
@ -38,11 +38,8 @@ def bypass_server_output_validation():
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
class TestClientOutputSchemaValidation:
|
|
||||||
"""Test client-side validation of structured output from tools"""
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_tool_structured_output_client_side_validation_basemodel(self):
|
async def test_tool_structured_output_client_side_validation_basemodel():
|
||||||
"""Test that client validates structured content against schema for BaseModel outputs"""
|
"""Test that client validates structured content against schema for BaseModel outputs"""
|
||||||
# Create a malicious low-level server that returns invalid structured content
|
# Create a malicious low-level server that returns invalid structured content
|
||||||
server = Server("test-server")
|
server = Server("test-server")
|
||||||
|
|
@ -81,8 +78,9 @@ class TestClientOutputSchemaValidation:
|
||||||
# Verify it's a validation error
|
# Verify it's a validation error
|
||||||
assert "Invalid structured content returned by tool get_user" in str(exc_info.value)
|
assert "Invalid structured content returned by tool get_user" in str(exc_info.value)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_tool_structured_output_client_side_validation_primitive(self):
|
async def test_tool_structured_output_client_side_validation_primitive():
|
||||||
"""Test that client validates structured content for primitive outputs"""
|
"""Test that client validates structured content for primitive outputs"""
|
||||||
server = Server("test-server")
|
server = Server("test-server")
|
||||||
|
|
||||||
|
|
@ -117,8 +115,9 @@ class TestClientOutputSchemaValidation:
|
||||||
await client.call_tool("calculate", {})
|
await client.call_tool("calculate", {})
|
||||||
assert "Invalid structured content returned by tool calculate" in str(exc_info.value)
|
assert "Invalid structured content returned by tool calculate" in str(exc_info.value)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_tool_structured_output_client_side_validation_dict_typed(self):
|
async def test_tool_structured_output_client_side_validation_dict_typed():
|
||||||
"""Test that client validates dict[str, T] structured content"""
|
"""Test that client validates dict[str, T] structured content"""
|
||||||
server = Server("test-server")
|
server = Server("test-server")
|
||||||
|
|
||||||
|
|
@ -148,8 +147,9 @@ class TestClientOutputSchemaValidation:
|
||||||
await client.call_tool("get_scores", {})
|
await client.call_tool("get_scores", {})
|
||||||
assert "Invalid structured content returned by tool get_scores" in str(exc_info.value)
|
assert "Invalid structured content returned by tool get_scores" in str(exc_info.value)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_tool_structured_output_client_side_validation_missing_required(self):
|
async def test_tool_structured_output_client_side_validation_missing_required():
|
||||||
"""Test that client validates missing required fields"""
|
"""Test that client validates missing required fields"""
|
||||||
server = Server("test-server")
|
server = Server("test-server")
|
||||||
|
|
||||||
|
|
@ -183,8 +183,9 @@ class TestClientOutputSchemaValidation:
|
||||||
await client.call_tool("get_person", {})
|
await client.call_tool("get_person", {})
|
||||||
assert "Invalid structured content returned by tool get_person" in str(exc_info.value)
|
assert "Invalid structured content returned by tool get_person" in str(exc_info.value)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_tool_not_listed_warning(self, caplog: pytest.LogCaptureFixture):
|
async def test_tool_not_listed_warning(caplog: pytest.LogCaptureFixture):
|
||||||
"""Test that client logs warning when tool is not in list_tools but has output_schema"""
|
"""Test that client logs warning when tool is not in list_tools but has output_schema"""
|
||||||
server = Server("test-server")
|
server = Server("test-server")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,16 +25,15 @@ def mock_exit_stack():
|
||||||
return mock.MagicMock(spec=contextlib.AsyncExitStack)
|
return mock.MagicMock(spec=contextlib.AsyncExitStack)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
def test_client_session_group_init():
|
||||||
class TestClientSessionGroup:
|
|
||||||
def test_init(self):
|
|
||||||
mcp_session_group = ClientSessionGroup()
|
mcp_session_group = ClientSessionGroup()
|
||||||
assert not mcp_session_group._tools
|
assert not mcp_session_group._tools
|
||||||
assert not mcp_session_group._resources
|
assert not mcp_session_group._resources
|
||||||
assert not mcp_session_group._prompts
|
assert not mcp_session_group._prompts
|
||||||
assert not mcp_session_group._tool_to_session
|
assert not mcp_session_group._tool_to_session
|
||||||
|
|
||||||
def test_component_properties(self):
|
|
||||||
|
def test_client_session_group_component_properties():
|
||||||
# --- Mock Dependencies ---
|
# --- Mock Dependencies ---
|
||||||
mock_prompt = mock.Mock()
|
mock_prompt = mock.Mock()
|
||||||
mock_resource = mock.Mock()
|
mock_resource = mock.Mock()
|
||||||
|
|
@ -51,7 +50,9 @@ class TestClientSessionGroup:
|
||||||
assert mcp_session_group.resources == {"my_resource": mock_resource}
|
assert mcp_session_group.resources == {"my_resource": mock_resource}
|
||||||
assert mcp_session_group.tools == {"my_tool": mock_tool}
|
assert mcp_session_group.tools == {"my_tool": mock_tool}
|
||||||
|
|
||||||
async def test_call_tool(self):
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_client_session_group_call_tool():
|
||||||
# --- Mock Dependencies ---
|
# --- Mock Dependencies ---
|
||||||
mock_session = mock.AsyncMock()
|
mock_session = mock.AsyncMock()
|
||||||
|
|
||||||
|
|
@ -84,7 +85,9 @@ class TestClientSessionGroup:
|
||||||
meta=None,
|
meta=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def test_connect_to_server(self, mock_exit_stack: contextlib.AsyncExitStack):
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_client_session_group_connect_to_server(mock_exit_stack: contextlib.AsyncExitStack):
|
||||||
"""Test connecting to a server and aggregating components."""
|
"""Test connecting to a server and aggregating components."""
|
||||||
# --- Mock Dependencies ---
|
# --- Mock Dependencies ---
|
||||||
mock_server_info = mock.Mock(spec=types.Implementation)
|
mock_server_info = mock.Mock(spec=types.Implementation)
|
||||||
|
|
@ -121,7 +124,9 @@ class TestClientSessionGroup:
|
||||||
mock_session.list_resources.assert_awaited_once()
|
mock_session.list_resources.assert_awaited_once()
|
||||||
mock_session.list_prompts.assert_awaited_once()
|
mock_session.list_prompts.assert_awaited_once()
|
||||||
|
|
||||||
async def test_connect_to_server_with_name_hook(self, mock_exit_stack: contextlib.AsyncExitStack):
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_client_session_group_connect_to_server_with_name_hook(mock_exit_stack: contextlib.AsyncExitStack):
|
||||||
"""Test connecting with a component name hook."""
|
"""Test connecting with a component name hook."""
|
||||||
# --- Mock Dependencies ---
|
# --- Mock Dependencies ---
|
||||||
mock_server_info = mock.Mock(spec=types.Implementation)
|
mock_server_info = mock.Mock(spec=types.Implementation)
|
||||||
|
|
@ -150,7 +155,9 @@ class TestClientSessionGroup:
|
||||||
assert group.tools[expected_tool_name] == mock_tool
|
assert group.tools[expected_tool_name] == mock_tool
|
||||||
assert group._tool_to_session[expected_tool_name] == mock_session
|
assert group._tool_to_session[expected_tool_name] == mock_session
|
||||||
|
|
||||||
async def test_disconnect_from_server(self): # No mock arguments needed
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_client_session_group_disconnect_from_server():
|
||||||
"""Test disconnecting from a server."""
|
"""Test disconnecting from a server."""
|
||||||
# --- Test Setup ---
|
# --- Test Setup ---
|
||||||
group = ClientSessionGroup()
|
group = ClientSessionGroup()
|
||||||
|
|
@ -213,7 +220,11 @@ class TestClientSessionGroup:
|
||||||
assert "res1" not in group._resources
|
assert "res1" not in group._resources
|
||||||
assert "prm1" not in group._prompts
|
assert "prm1" not in group._prompts
|
||||||
|
|
||||||
async def test_connect_to_server_duplicate_tool_raises_error(self, mock_exit_stack: contextlib.AsyncExitStack):
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_client_session_group_connect_to_server_duplicate_tool_raises_error(
|
||||||
|
mock_exit_stack: contextlib.AsyncExitStack,
|
||||||
|
):
|
||||||
"""Test McpError raised when connecting a server with a dup name."""
|
"""Test McpError raised when connecting a server with a dup name."""
|
||||||
# --- Setup Pre-existing State ---
|
# --- Setup Pre-existing State ---
|
||||||
group = ClientSessionGroup(exit_stack=mock_exit_stack)
|
group = ClientSessionGroup(exit_stack=mock_exit_stack)
|
||||||
|
|
@ -257,14 +268,17 @@ class TestClientSessionGroup:
|
||||||
assert len(group._tools) == 1 # Should still only have the original
|
assert len(group._tools) == 1 # Should still only have the original
|
||||||
assert group._tools[existing_tool_name] is not duplicate_tool # Ensure it's the original mock
|
assert group._tools[existing_tool_name] is not duplicate_tool # Ensure it's the original mock
|
||||||
|
|
||||||
# No patching needed here
|
|
||||||
async def test_disconnect_non_existent_server(self):
|
@pytest.mark.anyio
|
||||||
|
async def test_client_session_group_disconnect_non_existent_server():
|
||||||
"""Test disconnecting a server that isn't connected."""
|
"""Test disconnecting a server that isn't connected."""
|
||||||
session = mock.Mock(spec=mcp.ClientSession)
|
session = mock.Mock(spec=mcp.ClientSession)
|
||||||
group = ClientSessionGroup()
|
group = ClientSessionGroup()
|
||||||
with pytest.raises(McpError):
|
with pytest.raises(McpError):
|
||||||
await group.disconnect_from_server(session)
|
await group.disconnect_from_server(session)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"server_params_instance, client_type_name, patch_target_for_client_func",
|
"server_params_instance, client_type_name, patch_target_for_client_func",
|
||||||
[
|
[
|
||||||
|
|
@ -285,8 +299,7 @@ class TestClientSessionGroup:
|
||||||
), # url, headers, timeout, sse_read_timeout, terminate_on_close
|
), # url, headers, timeout, sse_read_timeout, terminate_on_close
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_establish_session_parameterized(
|
async def test_client_session_group_establish_session_parameterized(
|
||||||
self,
|
|
||||||
server_params_instance: StdioServerParameters | SseServerParameters | StreamableHttpParameters,
|
server_params_instance: StdioServerParameters | SseServerParameters | StreamableHttpParameters,
|
||||||
client_type_name: str, # Just for clarity or conditional logic if needed
|
client_type_name: str, # Just for clarity or conditional logic if needed
|
||||||
patch_target_for_client_func: str,
|
patch_target_for_client_func: str,
|
||||||
|
|
|
||||||
|
|
@ -45,10 +45,7 @@ def valid_access_token() -> AccessToken:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
class TestAuthContextMiddleware:
|
async def test_auth_context_middleware_with_authenticated_user(valid_access_token: AccessToken):
|
||||||
"""Tests for the AuthContextMiddleware class."""
|
|
||||||
|
|
||||||
async def test_with_authenticated_user(self, valid_access_token: AccessToken):
|
|
||||||
"""Test middleware with an authenticated user in scope."""
|
"""Test middleware with an authenticated user in scope."""
|
||||||
app = MockApp()
|
app = MockApp()
|
||||||
middleware = AuthContextMiddleware(app)
|
middleware = AuthContextMiddleware(app)
|
||||||
|
|
@ -85,7 +82,9 @@ class TestAuthContextMiddleware:
|
||||||
assert auth_context_var.get() is None
|
assert auth_context_var.get() is None
|
||||||
assert get_access_token() is None
|
assert get_access_token() is None
|
||||||
|
|
||||||
async def test_with_no_user(self):
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_auth_context_middleware_with_no_user():
|
||||||
"""Test middleware with no user in scope."""
|
"""Test middleware with no user in scope."""
|
||||||
app = MockApp()
|
app = MockApp()
|
||||||
middleware = AuthContextMiddleware(app)
|
middleware = AuthContextMiddleware(app)
|
||||||
|
|
|
||||||
|
|
@ -83,9 +83,8 @@ async def registered_client(client: httpx.AsyncClient) -> dict[str, Any]:
|
||||||
return client_info
|
return client_info
|
||||||
|
|
||||||
|
|
||||||
class TestRegistrationErrorHandling:
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_registration_error_handling(self, client: httpx.AsyncClient, oauth_provider: MockOAuthProvider):
|
async def test_registration_error_handling(client: httpx.AsyncClient, oauth_provider: MockOAuthProvider):
|
||||||
# Mock the register_client method to raise a registration error
|
# Mock the register_client method to raise a registration error
|
||||||
with unittest.mock.patch.object(
|
with unittest.mock.patch.object(
|
||||||
oauth_provider,
|
oauth_provider,
|
||||||
|
|
@ -117,10 +116,8 @@ class TestRegistrationErrorHandling:
|
||||||
assert data["error_description"] == "The redirect URI is invalid"
|
assert data["error_description"] == "The redirect URI is invalid"
|
||||||
|
|
||||||
|
|
||||||
class TestAuthorizeErrorHandling:
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_authorize_error_handling(
|
async def test_authorize_error_handling(
|
||||||
self,
|
|
||||||
client: httpx.AsyncClient,
|
client: httpx.AsyncClient,
|
||||||
oauth_provider: MockOAuthProvider,
|
oauth_provider: MockOAuthProvider,
|
||||||
registered_client: dict[str, Any],
|
registered_client: dict[str, Any],
|
||||||
|
|
@ -160,10 +157,8 @@ class TestAuthorizeErrorHandling:
|
||||||
assert query_params["state"][0] == "test_state"
|
assert query_params["state"][0] == "test_state"
|
||||||
|
|
||||||
|
|
||||||
class TestTokenErrorHandling:
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_token_error_handling_auth_code(
|
async def test_token_error_handling_auth_code(
|
||||||
self,
|
|
||||||
client: httpx.AsyncClient,
|
client: httpx.AsyncClient,
|
||||||
oauth_provider: MockOAuthProvider,
|
oauth_provider: MockOAuthProvider,
|
||||||
registered_client: dict[str, Any],
|
registered_client: dict[str, Any],
|
||||||
|
|
@ -220,9 +215,9 @@ class TestTokenErrorHandling:
|
||||||
assert data["error"] == "invalid_grant"
|
assert data["error"] == "invalid_grant"
|
||||||
assert data["error_description"] == "The authorization code is invalid"
|
assert data["error_description"] == "The authorization code is invalid"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_token_error_handling_refresh_token(
|
async def test_token_error_handling_refresh_token(
|
||||||
self,
|
|
||||||
client: httpx.AsyncClient,
|
client: httpx.AsyncClient,
|
||||||
oauth_provider: MockOAuthProvider,
|
oauth_provider: MockOAuthProvider,
|
||||||
registered_client: dict[str, Any],
|
registered_client: dict[str, Any],
|
||||||
|
|
|
||||||
|
|
@ -105,28 +105,31 @@ async def test_metadata_endpoint_without_path(root_resource_client: httpx.AsyncC
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestMetadataUrlConstruction:
|
# Tests for URL construction utility function
|
||||||
"""Test URL construction utility function."""
|
|
||||||
|
|
||||||
def test_url_without_path(self):
|
|
||||||
|
def test_metadata_url_construction_url_without_path():
|
||||||
"""Test URL construction for resource without path component."""
|
"""Test URL construction for resource without path component."""
|
||||||
resource_url = AnyHttpUrl("https://example.com")
|
resource_url = AnyHttpUrl("https://example.com")
|
||||||
result = build_resource_metadata_url(resource_url)
|
result = build_resource_metadata_url(resource_url)
|
||||||
assert str(result) == "https://example.com/.well-known/oauth-protected-resource"
|
assert str(result) == "https://example.com/.well-known/oauth-protected-resource"
|
||||||
|
|
||||||
def test_url_with_path_component(self):
|
|
||||||
|
def test_metadata_url_construction_url_with_path_component():
|
||||||
"""Test URL construction for resource with path component."""
|
"""Test URL construction for resource with path component."""
|
||||||
resource_url = AnyHttpUrl("https://example.com/mcp")
|
resource_url = AnyHttpUrl("https://example.com/mcp")
|
||||||
result = build_resource_metadata_url(resource_url)
|
result = build_resource_metadata_url(resource_url)
|
||||||
assert str(result) == "https://example.com/.well-known/oauth-protected-resource/mcp"
|
assert str(result) == "https://example.com/.well-known/oauth-protected-resource/mcp"
|
||||||
|
|
||||||
def test_url_with_trailing_slash_only(self):
|
|
||||||
|
def test_metadata_url_construction_url_with_trailing_slash_only():
|
||||||
"""Test URL construction for resource with trailing slash only."""
|
"""Test URL construction for resource with trailing slash only."""
|
||||||
resource_url = AnyHttpUrl("https://example.com/")
|
resource_url = AnyHttpUrl("https://example.com/")
|
||||||
result = build_resource_metadata_url(resource_url)
|
result = build_resource_metadata_url(resource_url)
|
||||||
# Trailing slash should be treated as empty path
|
# Trailing slash should be treated as empty path
|
||||||
assert str(result) == "https://example.com/.well-known/oauth-protected-resource"
|
assert str(result) == "https://example.com/.well-known/oauth-protected-resource"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"resource_url,expected_url",
|
"resource_url,expected_url",
|
||||||
[
|
[
|
||||||
|
|
@ -136,16 +139,16 @@ class TestMetadataUrlConstruction:
|
||||||
("http://localhost:8001/mcp", "http://localhost:8001/.well-known/oauth-protected-resource/mcp"),
|
("http://localhost:8001/mcp", "http://localhost:8001/.well-known/oauth-protected-resource/mcp"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_various_resource_configurations(self, resource_url: str, expected_url: str):
|
def test_metadata_url_construction_various_resource_configurations(resource_url: str, expected_url: str):
|
||||||
"""Test URL construction with various resource configurations."""
|
"""Test URL construction with various resource configurations."""
|
||||||
result = build_resource_metadata_url(AnyHttpUrl(resource_url))
|
result = build_resource_metadata_url(AnyHttpUrl(resource_url))
|
||||||
assert str(result) == expected_url
|
assert str(result) == expected_url
|
||||||
|
|
||||||
|
|
||||||
class TestRouteConsistency:
|
# Tests for consistency between URL generation and route registration
|
||||||
"""Test consistency between URL generation and route registration."""
|
|
||||||
|
|
||||||
def test_route_path_matches_metadata_url(self):
|
|
||||||
|
def test_route_consistency_route_path_matches_metadata_url():
|
||||||
"""Test that route path matches the generated metadata URL."""
|
"""Test that route path matches the generated metadata URL."""
|
||||||
resource_url = AnyHttpUrl("https://example.com/mcp")
|
resource_url = AnyHttpUrl("https://example.com/mcp")
|
||||||
|
|
||||||
|
|
@ -165,6 +168,7 @@ class TestRouteConsistency:
|
||||||
assert len(routes) == 1
|
assert len(routes) == 1
|
||||||
assert routes[0].path == metadata_path
|
assert routes[0].path == metadata_path
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"resource_url,expected_path",
|
"resource_url,expected_path",
|
||||||
[
|
[
|
||||||
|
|
@ -173,7 +177,7 @@ class TestRouteConsistency:
|
||||||
("https://example.com/mcp", "/.well-known/oauth-protected-resource/mcp"),
|
("https://example.com/mcp", "/.well-known/oauth-protected-resource/mcp"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_consistent_paths_for_various_resources(self, resource_url: str, expected_path: str):
|
def test_route_consistency_consistent_paths_for_various_resources(resource_url: str, expected_path: str):
|
||||||
"""Test that URL generation and route creation are consistent."""
|
"""Test that URL generation and route creation are consistent."""
|
||||||
resource_url_obj = AnyHttpUrl(resource_url)
|
resource_url_obj = AnyHttpUrl(resource_url)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,15 @@
|
||||||
from mcp.server.auth.provider import construct_redirect_uri
|
from mcp.server.auth.provider import construct_redirect_uri
|
||||||
|
|
||||||
|
|
||||||
class TestConstructRedirectUri:
|
def test_construct_redirect_uri_no_existing_params():
|
||||||
"""Tests for the construct_redirect_uri function."""
|
|
||||||
|
|
||||||
def test_construct_redirect_uri_no_existing_params(self):
|
|
||||||
"""Test construct_redirect_uri with no existing query parameters."""
|
"""Test construct_redirect_uri with no existing query parameters."""
|
||||||
base_uri = "http://localhost:8000/callback"
|
base_uri = "http://localhost:8000/callback"
|
||||||
result = construct_redirect_uri(base_uri, code="auth_code", state="test_state")
|
result = construct_redirect_uri(base_uri, code="auth_code", state="test_state")
|
||||||
|
|
||||||
assert "http://localhost:8000/callback?code=auth_code&state=test_state" == result
|
assert "http://localhost:8000/callback?code=auth_code&state=test_state" == result
|
||||||
|
|
||||||
def test_construct_redirect_uri_with_existing_params(self):
|
|
||||||
|
def test_construct_redirect_uri_with_existing_params():
|
||||||
"""Test construct_redirect_uri with existing query parameters (regression test for #1279)."""
|
"""Test construct_redirect_uri with existing query parameters (regression test for #1279)."""
|
||||||
base_uri = "http://localhost:8000/callback?session_id=1234"
|
base_uri = "http://localhost:8000/callback?session_id=1234"
|
||||||
result = construct_redirect_uri(base_uri, code="auth_code", state="test_state")
|
result = construct_redirect_uri(base_uri, code="auth_code", state="test_state")
|
||||||
|
|
@ -24,7 +22,8 @@ class TestConstructRedirectUri:
|
||||||
assert "state=test_state" in result
|
assert "state=test_state" in result
|
||||||
assert result.startswith("http://localhost:8000/callback?")
|
assert result.startswith("http://localhost:8000/callback?")
|
||||||
|
|
||||||
def test_construct_redirect_uri_multiple_existing_params(self):
|
|
||||||
|
def test_construct_redirect_uri_multiple_existing_params():
|
||||||
"""Test construct_redirect_uri with multiple existing query parameters."""
|
"""Test construct_redirect_uri with multiple existing query parameters."""
|
||||||
base_uri = "http://localhost:8000/callback?session_id=1234&user=test"
|
base_uri = "http://localhost:8000/callback?session_id=1234&user=test"
|
||||||
result = construct_redirect_uri(base_uri, code="auth_code")
|
result = construct_redirect_uri(base_uri, code="auth_code")
|
||||||
|
|
@ -33,7 +32,8 @@ class TestConstructRedirectUri:
|
||||||
assert "user=test" in result
|
assert "user=test" in result
|
||||||
assert "code=auth_code" in result
|
assert "code=auth_code" in result
|
||||||
|
|
||||||
def test_construct_redirect_uri_with_none_values(self):
|
|
||||||
|
def test_construct_redirect_uri_with_none_values():
|
||||||
"""Test construct_redirect_uri filters out None values."""
|
"""Test construct_redirect_uri filters out None values."""
|
||||||
base_uri = "http://localhost:8000/callback"
|
base_uri = "http://localhost:8000/callback"
|
||||||
result = construct_redirect_uri(base_uri, code="auth_code", state=None)
|
result = construct_redirect_uri(base_uri, code="auth_code", state=None)
|
||||||
|
|
@ -41,14 +41,16 @@ class TestConstructRedirectUri:
|
||||||
assert result == "http://localhost:8000/callback?code=auth_code"
|
assert result == "http://localhost:8000/callback?code=auth_code"
|
||||||
assert "state" not in result
|
assert "state" not in result
|
||||||
|
|
||||||
def test_construct_redirect_uri_empty_params(self):
|
|
||||||
|
def test_construct_redirect_uri_empty_params():
|
||||||
"""Test construct_redirect_uri with no additional parameters."""
|
"""Test construct_redirect_uri with no additional parameters."""
|
||||||
base_uri = "http://localhost:8000/callback?existing=param"
|
base_uri = "http://localhost:8000/callback?existing=param"
|
||||||
result = construct_redirect_uri(base_uri)
|
result = construct_redirect_uri(base_uri)
|
||||||
|
|
||||||
assert result == "http://localhost:8000/callback?existing=param"
|
assert result == "http://localhost:8000/callback?existing=param"
|
||||||
|
|
||||||
def test_construct_redirect_uri_duplicate_param_names(self):
|
|
||||||
|
def test_construct_redirect_uri_duplicate_param_names():
|
||||||
"""Test construct_redirect_uri when adding param that already exists."""
|
"""Test construct_redirect_uri when adding param that already exists."""
|
||||||
base_uri = "http://localhost:8000/callback?code=existing"
|
base_uri = "http://localhost:8000/callback?code=existing"
|
||||||
result = construct_redirect_uri(base_uri, code="new_code")
|
result = construct_redirect_uri(base_uri, code="new_code")
|
||||||
|
|
@ -57,7 +59,8 @@ class TestConstructRedirectUri:
|
||||||
assert "code=existing" in result
|
assert "code=existing" in result
|
||||||
assert "code=new_code" in result
|
assert "code=new_code" in result
|
||||||
|
|
||||||
def test_construct_redirect_uri_multivalued_existing_params(self):
|
|
||||||
|
def test_construct_redirect_uri_multivalued_existing_params():
|
||||||
"""Test construct_redirect_uri with existing multi-valued parameters."""
|
"""Test construct_redirect_uri with existing multi-valued parameters."""
|
||||||
base_uri = "http://localhost:8000/callback?scope=read&scope=write"
|
base_uri = "http://localhost:8000/callback?scope=read&scope=write"
|
||||||
result = construct_redirect_uri(base_uri, code="auth_code")
|
result = construct_redirect_uri(base_uri, code="auth_code")
|
||||||
|
|
@ -66,7 +69,8 @@ class TestConstructRedirectUri:
|
||||||
assert "scope=write" in result
|
assert "scope=write" in result
|
||||||
assert "code=auth_code" in result
|
assert "code=auth_code" in result
|
||||||
|
|
||||||
def test_construct_redirect_uri_encoded_values(self):
|
|
||||||
|
def test_construct_redirect_uri_encoded_values():
|
||||||
"""Test construct_redirect_uri handles URL encoding properly."""
|
"""Test construct_redirect_uri handles URL encoding properly."""
|
||||||
base_uri = "http://localhost:8000/callback"
|
base_uri = "http://localhost:8000/callback"
|
||||||
result = construct_redirect_uri(base_uri, state="test state with spaces")
|
result = construct_redirect_uri(base_uri, state="test state with spaces")
|
||||||
|
|
|
||||||
|
|
@ -10,17 +10,14 @@ by the low-level server to package resource content before sending it over the M
|
||||||
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
||||||
|
|
||||||
|
|
||||||
class TestReadResourceContentsMetadata:
|
def test_read_resource_contents_with_metadata():
|
||||||
"""Test ReadResourceContents meta field.
|
"""Test that ReadResourceContents accepts meta parameter.
|
||||||
|
|
||||||
ReadResourceContents is an internal helper type used by the low-level MCP server.
|
ReadResourceContents is an internal helper type used by the low-level MCP server.
|
||||||
When a resource is read, the server creates a ReadResourceContents instance that
|
When a resource is read, the server creates a ReadResourceContents instance that
|
||||||
contains the content, mime type, and now metadata. The low-level server then
|
contains the content, mime type, and now metadata. The low-level server then
|
||||||
extracts the meta field and includes it in the protocol response as _meta.
|
extracts the meta field and includes it in the protocol response as _meta.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def test_read_resource_contents_with_metadata(self):
|
|
||||||
"""Test that ReadResourceContents accepts meta parameter."""
|
|
||||||
# Bridge between Resource.meta and MCP protocol _meta field (helper_types.py:11)
|
# Bridge between Resource.meta and MCP protocol _meta field (helper_types.py:11)
|
||||||
metadata = {"version": "1.0", "cached": True}
|
metadata = {"version": "1.0", "cached": True}
|
||||||
|
|
||||||
|
|
@ -35,7 +32,8 @@ class TestReadResourceContentsMetadata:
|
||||||
assert contents.meta["version"] == "1.0"
|
assert contents.meta["version"] == "1.0"
|
||||||
assert contents.meta["cached"] is True
|
assert contents.meta["cached"] is True
|
||||||
|
|
||||||
def test_read_resource_contents_without_metadata(self):
|
|
||||||
|
def test_read_resource_contents_without_metadata():
|
||||||
"""Test that ReadResourceContents meta defaults to None."""
|
"""Test that ReadResourceContents meta defaults to None."""
|
||||||
# Ensures backward compatibility - meta defaults to None, _meta omitted from protocol (helper_types.py:11)
|
# Ensures backward compatibility - meta defaults to None, _meta omitted from protocol (helper_types.py:11)
|
||||||
contents = ReadResourceContents(
|
contents = ReadResourceContents(
|
||||||
|
|
@ -45,7 +43,8 @@ class TestReadResourceContentsMetadata:
|
||||||
|
|
||||||
assert contents.meta is None
|
assert contents.meta is None
|
||||||
|
|
||||||
def test_read_resource_contents_with_bytes(self):
|
|
||||||
|
def test_read_resource_contents_with_bytes():
|
||||||
"""Test that ReadResourceContents works with bytes content and meta."""
|
"""Test that ReadResourceContents works with bytes content and meta."""
|
||||||
# Verifies meta works with both str and bytes content (binary resources like images, PDFs)
|
# Verifies meta works with both str and bytes content (binary resources like images, PDFs)
|
||||||
metadata = {"encoding": "utf-8"}
|
metadata = {"encoding": "utf-8"}
|
||||||
|
|
|
||||||
|
|
@ -20,65 +20,71 @@ from mcp.types import (
|
||||||
ToolUseContent,
|
ToolUseContent,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Tests for check_sampling_tools_capability function
|
||||||
|
|
||||||
class TestCheckSamplingToolsCapability:
|
|
||||||
"""Tests for check_sampling_tools_capability function."""
|
|
||||||
|
|
||||||
def test_returns_false_when_caps_none(self) -> None:
|
def test_check_sampling_tools_capability_returns_false_when_caps_none() -> None:
|
||||||
"""Returns False when client_caps is None."""
|
"""Returns False when client_caps is None."""
|
||||||
assert check_sampling_tools_capability(None) is False
|
assert check_sampling_tools_capability(None) is False
|
||||||
|
|
||||||
def test_returns_false_when_sampling_none(self) -> None:
|
|
||||||
|
def test_check_sampling_tools_capability_returns_false_when_sampling_none() -> None:
|
||||||
"""Returns False when client_caps.sampling is None."""
|
"""Returns False when client_caps.sampling is None."""
|
||||||
caps = ClientCapabilities()
|
caps = ClientCapabilities()
|
||||||
assert check_sampling_tools_capability(caps) is False
|
assert check_sampling_tools_capability(caps) is False
|
||||||
|
|
||||||
def test_returns_false_when_tools_none(self) -> None:
|
|
||||||
|
def test_check_sampling_tools_capability_returns_false_when_tools_none() -> None:
|
||||||
"""Returns False when client_caps.sampling.tools is None."""
|
"""Returns False when client_caps.sampling.tools is None."""
|
||||||
caps = ClientCapabilities(sampling=SamplingCapability())
|
caps = ClientCapabilities(sampling=SamplingCapability())
|
||||||
assert check_sampling_tools_capability(caps) is False
|
assert check_sampling_tools_capability(caps) is False
|
||||||
|
|
||||||
def test_returns_true_when_tools_present(self) -> None:
|
|
||||||
|
def test_check_sampling_tools_capability_returns_true_when_tools_present() -> None:
|
||||||
"""Returns True when sampling.tools is present."""
|
"""Returns True when sampling.tools is present."""
|
||||||
caps = ClientCapabilities(sampling=SamplingCapability(tools=SamplingToolsCapability()))
|
caps = ClientCapabilities(sampling=SamplingCapability(tools=SamplingToolsCapability()))
|
||||||
assert check_sampling_tools_capability(caps) is True
|
assert check_sampling_tools_capability(caps) is True
|
||||||
|
|
||||||
|
|
||||||
class TestValidateSamplingTools:
|
# Tests for validate_sampling_tools function
|
||||||
"""Tests for validate_sampling_tools function."""
|
|
||||||
|
|
||||||
def test_no_error_when_tools_none(self) -> None:
|
|
||||||
|
def test_validate_sampling_tools_no_error_when_tools_none() -> None:
|
||||||
"""No error when tools and tool_choice are None."""
|
"""No error when tools and tool_choice are None."""
|
||||||
validate_sampling_tools(None, None, None) # Should not raise
|
validate_sampling_tools(None, None, None) # Should not raise
|
||||||
|
|
||||||
def test_raises_when_tools_provided_but_no_capability(self) -> None:
|
|
||||||
|
def test_validate_sampling_tools_raises_when_tools_provided_but_no_capability() -> None:
|
||||||
"""Raises McpError when tools provided but client doesn't support."""
|
"""Raises McpError when tools provided but client doesn't support."""
|
||||||
tool = Tool(name="test", input_schema={"type": "object"})
|
tool = Tool(name="test", input_schema={"type": "object"})
|
||||||
with pytest.raises(McpError) as exc_info:
|
with pytest.raises(McpError) as exc_info:
|
||||||
validate_sampling_tools(None, [tool], None)
|
validate_sampling_tools(None, [tool], None)
|
||||||
assert "sampling tools capability" in str(exc_info.value)
|
assert "sampling tools capability" in str(exc_info.value)
|
||||||
|
|
||||||
def test_raises_when_tool_choice_provided_but_no_capability(self) -> None:
|
|
||||||
|
def test_validate_sampling_tools_raises_when_tool_choice_provided_but_no_capability() -> None:
|
||||||
"""Raises McpError when tool_choice provided but client doesn't support."""
|
"""Raises McpError when tool_choice provided but client doesn't support."""
|
||||||
with pytest.raises(McpError) as exc_info:
|
with pytest.raises(McpError) as exc_info:
|
||||||
validate_sampling_tools(None, None, ToolChoice(mode="auto"))
|
validate_sampling_tools(None, None, ToolChoice(mode="auto"))
|
||||||
assert "sampling tools capability" in str(exc_info.value)
|
assert "sampling tools capability" in str(exc_info.value)
|
||||||
|
|
||||||
def test_no_error_when_capability_present(self) -> None:
|
|
||||||
|
def test_validate_sampling_tools_no_error_when_capability_present() -> None:
|
||||||
"""No error when client has sampling.tools capability."""
|
"""No error when client has sampling.tools capability."""
|
||||||
caps = ClientCapabilities(sampling=SamplingCapability(tools=SamplingToolsCapability()))
|
caps = ClientCapabilities(sampling=SamplingCapability(tools=SamplingToolsCapability()))
|
||||||
tool = Tool(name="test", input_schema={"type": "object"})
|
tool = Tool(name="test", input_schema={"type": "object"})
|
||||||
validate_sampling_tools(caps, [tool], ToolChoice(mode="auto")) # Should not raise
|
validate_sampling_tools(caps, [tool], ToolChoice(mode="auto")) # Should not raise
|
||||||
|
|
||||||
|
|
||||||
class TestValidateToolUseResultMessages:
|
# Tests for validate_tool_use_result_messages function
|
||||||
"""Tests for validate_tool_use_result_messages function."""
|
|
||||||
|
|
||||||
def test_no_error_for_empty_messages(self) -> None:
|
|
||||||
|
def test_validate_tool_use_result_messages_no_error_for_empty_messages() -> None:
|
||||||
"""No error when messages list is empty."""
|
"""No error when messages list is empty."""
|
||||||
validate_tool_use_result_messages([]) # Should not raise
|
validate_tool_use_result_messages([]) # Should not raise
|
||||||
|
|
||||||
def test_no_error_for_simple_text_messages(self) -> None:
|
|
||||||
|
def test_validate_tool_use_result_messages_no_error_for_simple_text_messages() -> None:
|
||||||
"""No error for simple text messages."""
|
"""No error for simple text messages."""
|
||||||
messages = [
|
messages = [
|
||||||
SamplingMessage(role="user", content=TextContent(type="text", text="Hello")),
|
SamplingMessage(role="user", content=TextContent(type="text", text="Hello")),
|
||||||
|
|
@ -86,7 +92,8 @@ class TestValidateToolUseResultMessages:
|
||||||
]
|
]
|
||||||
validate_tool_use_result_messages(messages) # Should not raise
|
validate_tool_use_result_messages(messages) # Should not raise
|
||||||
|
|
||||||
def test_raises_when_tool_result_mixed_with_other_content(self) -> None:
|
|
||||||
|
def test_validate_tool_use_result_messages_raises_when_tool_result_mixed_with_other_content() -> None:
|
||||||
"""Raises when tool_result is mixed with other content types."""
|
"""Raises when tool_result is mixed with other content types."""
|
||||||
messages = [
|
messages = [
|
||||||
SamplingMessage(
|
SamplingMessage(
|
||||||
|
|
@ -100,7 +107,8 @@ class TestValidateToolUseResultMessages:
|
||||||
with pytest.raises(ValueError, match="only tool_result content"):
|
with pytest.raises(ValueError, match="only tool_result content"):
|
||||||
validate_tool_use_result_messages(messages)
|
validate_tool_use_result_messages(messages)
|
||||||
|
|
||||||
def test_raises_when_tool_result_without_previous_tool_use(self) -> None:
|
|
||||||
|
def test_validate_tool_use_result_messages_raises_when_tool_result_without_previous_tool_use() -> None:
|
||||||
"""Raises when tool_result appears without preceding tool_use."""
|
"""Raises when tool_result appears without preceding tool_use."""
|
||||||
messages = [
|
messages = [
|
||||||
SamplingMessage(
|
SamplingMessage(
|
||||||
|
|
@ -111,7 +119,8 @@ class TestValidateToolUseResultMessages:
|
||||||
with pytest.raises(ValueError, match="previous message containing tool_use"):
|
with pytest.raises(ValueError, match="previous message containing tool_use"):
|
||||||
validate_tool_use_result_messages(messages)
|
validate_tool_use_result_messages(messages)
|
||||||
|
|
||||||
def test_raises_when_tool_result_ids_dont_match_tool_use(self) -> None:
|
|
||||||
|
def test_validate_tool_use_result_messages_raises_when_tool_result_ids_dont_match_tool_use() -> None:
|
||||||
"""Raises when tool_result IDs don't match tool_use IDs."""
|
"""Raises when tool_result IDs don't match tool_use IDs."""
|
||||||
messages = [
|
messages = [
|
||||||
SamplingMessage(
|
SamplingMessage(
|
||||||
|
|
@ -126,7 +135,8 @@ class TestValidateToolUseResultMessages:
|
||||||
with pytest.raises(ValueError, match="do not match"):
|
with pytest.raises(ValueError, match="do not match"):
|
||||||
validate_tool_use_result_messages(messages)
|
validate_tool_use_result_messages(messages)
|
||||||
|
|
||||||
def test_no_error_when_tool_result_matches_tool_use(self) -> None:
|
|
||||||
|
def test_validate_tool_use_result_messages_no_error_when_tool_result_matches_tool_use() -> None:
|
||||||
"""No error when tool_result IDs match tool_use IDs."""
|
"""No error when tool_result IDs match tool_use IDs."""
|
||||||
messages = [
|
messages = [
|
||||||
SamplingMessage(
|
SamplingMessage(
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,7 @@
|
||||||
from mcp.shared.auth import OAuthMetadata
|
from mcp.shared.auth import OAuthMetadata
|
||||||
|
|
||||||
|
|
||||||
class TestOAuthMetadata:
|
def test_oauth():
|
||||||
"""Tests for OAuthMetadata parsing."""
|
|
||||||
|
|
||||||
def test_oauth(self):
|
|
||||||
"""Should not throw when parsing OAuth metadata."""
|
"""Should not throw when parsing OAuth metadata."""
|
||||||
OAuthMetadata.model_validate(
|
OAuthMetadata.model_validate(
|
||||||
{
|
{
|
||||||
|
|
@ -19,7 +16,8 @@ class TestOAuthMetadata:
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_oidc(self):
|
|
||||||
|
def test_oidc():
|
||||||
"""Should not throw when parsing OIDC metadata."""
|
"""Should not throw when parsing OIDC metadata."""
|
||||||
OAuthMetadata.model_validate(
|
OAuthMetadata.model_validate(
|
||||||
{
|
{
|
||||||
|
|
@ -38,7 +36,8 @@ class TestOAuthMetadata:
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_oauth_with_jarm(self):
|
|
||||||
|
def test_oauth_with_jarm():
|
||||||
"""Should not throw when parsing OAuth metadata that includes JARM response modes."""
|
"""Should not throw when parsing OAuth metadata that includes JARM response modes."""
|
||||||
OAuthMetadata.model_validate(
|
OAuthMetadata.model_validate(
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -4,70 +4,77 @@ from pydantic import HttpUrl
|
||||||
|
|
||||||
from mcp.shared.auth_utils import check_resource_allowed, resource_url_from_server_url
|
from mcp.shared.auth_utils import check_resource_allowed, resource_url_from_server_url
|
||||||
|
|
||||||
|
# Tests for resource_url_from_server_url function
|
||||||
|
|
||||||
class TestResourceUrlFromServerUrl:
|
|
||||||
"""Tests for resource_url_from_server_url function."""
|
|
||||||
|
|
||||||
def test_removes_fragment(self):
|
def test_resource_url_from_server_url_removes_fragment():
|
||||||
"""Fragment should be removed per RFC 8707."""
|
"""Fragment should be removed per RFC 8707."""
|
||||||
assert resource_url_from_server_url("https://example.com/path#fragment") == "https://example.com/path"
|
assert resource_url_from_server_url("https://example.com/path#fragment") == "https://example.com/path"
|
||||||
assert resource_url_from_server_url("https://example.com/#fragment") == "https://example.com/"
|
assert resource_url_from_server_url("https://example.com/#fragment") == "https://example.com/"
|
||||||
|
|
||||||
def test_preserves_path(self):
|
|
||||||
|
def test_resource_url_from_server_url_preserves_path():
|
||||||
"""Path should be preserved."""
|
"""Path should be preserved."""
|
||||||
assert (
|
assert (
|
||||||
resource_url_from_server_url("https://example.com/path/to/resource")
|
resource_url_from_server_url("https://example.com/path/to/resource") == "https://example.com/path/to/resource"
|
||||||
== "https://example.com/path/to/resource"
|
|
||||||
)
|
)
|
||||||
assert resource_url_from_server_url("https://example.com/") == "https://example.com/"
|
assert resource_url_from_server_url("https://example.com/") == "https://example.com/"
|
||||||
assert resource_url_from_server_url("https://example.com") == "https://example.com"
|
assert resource_url_from_server_url("https://example.com") == "https://example.com"
|
||||||
|
|
||||||
def test_preserves_query(self):
|
|
||||||
|
def test_resource_url_from_server_url_preserves_query():
|
||||||
"""Query parameters should be preserved."""
|
"""Query parameters should be preserved."""
|
||||||
assert resource_url_from_server_url("https://example.com/path?foo=bar") == "https://example.com/path?foo=bar"
|
assert resource_url_from_server_url("https://example.com/path?foo=bar") == "https://example.com/path?foo=bar"
|
||||||
assert resource_url_from_server_url("https://example.com/?key=value") == "https://example.com/?key=value"
|
assert resource_url_from_server_url("https://example.com/?key=value") == "https://example.com/?key=value"
|
||||||
|
|
||||||
def test_preserves_port(self):
|
|
||||||
|
def test_resource_url_from_server_url_preserves_port():
|
||||||
"""Non-default ports should be preserved."""
|
"""Non-default ports should be preserved."""
|
||||||
assert resource_url_from_server_url("https://example.com:8443/path") == "https://example.com:8443/path"
|
assert resource_url_from_server_url("https://example.com:8443/path") == "https://example.com:8443/path"
|
||||||
assert resource_url_from_server_url("http://example.com:8080/") == "http://example.com:8080/"
|
assert resource_url_from_server_url("http://example.com:8080/") == "http://example.com:8080/"
|
||||||
|
|
||||||
def test_lowercase_scheme_and_host(self):
|
|
||||||
|
def test_resource_url_from_server_url_lowercase_scheme_and_host():
|
||||||
"""Scheme and host should be lowercase for canonical form."""
|
"""Scheme and host should be lowercase for canonical form."""
|
||||||
assert resource_url_from_server_url("HTTPS://EXAMPLE.COM/path") == "https://example.com/path"
|
assert resource_url_from_server_url("HTTPS://EXAMPLE.COM/path") == "https://example.com/path"
|
||||||
assert resource_url_from_server_url("Http://Example.Com:8080/") == "http://example.com:8080/"
|
assert resource_url_from_server_url("Http://Example.Com:8080/") == "http://example.com:8080/"
|
||||||
|
|
||||||
def test_handles_pydantic_urls(self):
|
|
||||||
|
def test_resource_url_from_server_url_handles_pydantic_urls():
|
||||||
"""Should handle Pydantic URL types."""
|
"""Should handle Pydantic URL types."""
|
||||||
url = HttpUrl("https://example.com/path")
|
url = HttpUrl("https://example.com/path")
|
||||||
assert resource_url_from_server_url(url) == "https://example.com/path"
|
assert resource_url_from_server_url(url) == "https://example.com/path"
|
||||||
|
|
||||||
|
|
||||||
class TestCheckResourceAllowed:
|
# Tests for check_resource_allowed function
|
||||||
"""Tests for check_resource_allowed function."""
|
|
||||||
|
|
||||||
def test_identical_urls(self):
|
|
||||||
|
def test_check_resource_allowed_identical_urls():
|
||||||
"""Identical URLs should match."""
|
"""Identical URLs should match."""
|
||||||
assert check_resource_allowed("https://example.com/path", "https://example.com/path") is True
|
assert check_resource_allowed("https://example.com/path", "https://example.com/path") is True
|
||||||
assert check_resource_allowed("https://example.com/", "https://example.com/") is True
|
assert check_resource_allowed("https://example.com/", "https://example.com/") is True
|
||||||
assert check_resource_allowed("https://example.com", "https://example.com") is True
|
assert check_resource_allowed("https://example.com", "https://example.com") is True
|
||||||
|
|
||||||
def test_different_schemes(self):
|
|
||||||
|
def test_check_resource_allowed_different_schemes():
|
||||||
"""Different schemes should not match."""
|
"""Different schemes should not match."""
|
||||||
assert check_resource_allowed("https://example.com/path", "http://example.com/path") is False
|
assert check_resource_allowed("https://example.com/path", "http://example.com/path") is False
|
||||||
assert check_resource_allowed("http://example.com/", "https://example.com/") is False
|
assert check_resource_allowed("http://example.com/", "https://example.com/") is False
|
||||||
|
|
||||||
def test_different_domains(self):
|
|
||||||
|
def test_check_resource_allowed_different_domains():
|
||||||
"""Different domains should not match."""
|
"""Different domains should not match."""
|
||||||
assert check_resource_allowed("https://example.com/path", "https://example.org/path") is False
|
assert check_resource_allowed("https://example.com/path", "https://example.org/path") is False
|
||||||
assert check_resource_allowed("https://sub.example.com/", "https://example.com/") is False
|
assert check_resource_allowed("https://sub.example.com/", "https://example.com/") is False
|
||||||
|
|
||||||
def test_different_ports(self):
|
|
||||||
|
def test_check_resource_allowed_different_ports():
|
||||||
"""Different ports should not match."""
|
"""Different ports should not match."""
|
||||||
assert check_resource_allowed("https://example.com:8443/path", "https://example.com/path") is False
|
assert check_resource_allowed("https://example.com:8443/path", "https://example.com/path") is False
|
||||||
assert check_resource_allowed("https://example.com:8080/", "https://example.com:8443/") is False
|
assert check_resource_allowed("https://example.com:8080/", "https://example.com:8443/") is False
|
||||||
|
|
||||||
def test_hierarchical_matching(self):
|
|
||||||
|
def test_check_resource_allowed_hierarchical_matching():
|
||||||
"""Child paths should match parent paths."""
|
"""Child paths should match parent paths."""
|
||||||
# Parent resource allows child resources
|
# Parent resource allows child resources
|
||||||
assert check_resource_allowed("https://example.com/api/v1/users", "https://example.com/api") is True
|
assert check_resource_allowed("https://example.com/api/v1/users", "https://example.com/api") is True
|
||||||
|
|
@ -81,7 +88,8 @@ class TestCheckResourceAllowed:
|
||||||
assert check_resource_allowed("https://example.com/api", "https://example.com/api/v1") is False
|
assert check_resource_allowed("https://example.com/api", "https://example.com/api/v1") is False
|
||||||
assert check_resource_allowed("https://example.com/", "https://example.com/api") is False
|
assert check_resource_allowed("https://example.com/", "https://example.com/api") is False
|
||||||
|
|
||||||
def test_path_boundary_matching(self):
|
|
||||||
|
def test_check_resource_allowed_path_boundary_matching():
|
||||||
"""Path matching should respect boundaries."""
|
"""Path matching should respect boundaries."""
|
||||||
# Should not match partial path segments
|
# Should not match partial path segments
|
||||||
assert check_resource_allowed("https://example.com/apiextra", "https://example.com/api") is False
|
assert check_resource_allowed("https://example.com/apiextra", "https://example.com/api") is False
|
||||||
|
|
@ -91,7 +99,8 @@ class TestCheckResourceAllowed:
|
||||||
assert check_resource_allowed("https://example.com/api/", "https://example.com/api") is True
|
assert check_resource_allowed("https://example.com/api/", "https://example.com/api") is True
|
||||||
assert check_resource_allowed("https://example.com/api/v1", "https://example.com/api/") is True
|
assert check_resource_allowed("https://example.com/api/v1", "https://example.com/api/") is True
|
||||||
|
|
||||||
def test_trailing_slash_handling(self):
|
|
||||||
|
def test_check_resource_allowed_trailing_slash_handling():
|
||||||
"""Trailing slashes should be handled correctly."""
|
"""Trailing slashes should be handled correctly."""
|
||||||
# With and without trailing slashes
|
# With and without trailing slashes
|
||||||
assert check_resource_allowed("https://example.com/api/", "https://example.com/api") is True
|
assert check_resource_allowed("https://example.com/api/", "https://example.com/api") is True
|
||||||
|
|
@ -99,13 +108,15 @@ class TestCheckResourceAllowed:
|
||||||
assert check_resource_allowed("https://example.com/api/v1", "https://example.com/api") is True
|
assert check_resource_allowed("https://example.com/api/v1", "https://example.com/api") is True
|
||||||
assert check_resource_allowed("https://example.com/api/v1", "https://example.com/api/") is True
|
assert check_resource_allowed("https://example.com/api/v1", "https://example.com/api/") is True
|
||||||
|
|
||||||
def test_case_insensitive_origin(self):
|
|
||||||
|
def test_check_resource_allowed_case_insensitive_origin():
|
||||||
"""Origin comparison should be case-insensitive."""
|
"""Origin comparison should be case-insensitive."""
|
||||||
assert check_resource_allowed("https://EXAMPLE.COM/path", "https://example.com/path") is True
|
assert check_resource_allowed("https://EXAMPLE.COM/path", "https://example.com/path") is True
|
||||||
assert check_resource_allowed("HTTPS://example.com/path", "https://example.com/path") is True
|
assert check_resource_allowed("HTTPS://example.com/path", "https://example.com/path") is True
|
||||||
assert check_resource_allowed("https://Example.Com:8080/api", "https://example.com:8080/api") is True
|
assert check_resource_allowed("https://Example.Com:8080/api", "https://example.com:8080/api") is True
|
||||||
|
|
||||||
def test_empty_paths(self):
|
|
||||||
|
def test_check_resource_allowed_empty_paths():
|
||||||
"""Empty paths should be handled correctly."""
|
"""Empty paths should be handled correctly."""
|
||||||
assert check_resource_allowed("https://example.com", "https://example.com") is True
|
assert check_resource_allowed("https://example.com", "https://example.com") is True
|
||||||
assert check_resource_allowed("https://example.com/", "https://example.com") is True
|
assert check_resource_allowed("https://example.com/", "https://example.com") is True
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,7 @@ from mcp.shared.exceptions import McpError, UrlElicitationRequiredError
|
||||||
from mcp.types import URL_ELICITATION_REQUIRED, ElicitRequestURLParams, ErrorData
|
from mcp.types import URL_ELICITATION_REQUIRED, ElicitRequestURLParams, ErrorData
|
||||||
|
|
||||||
|
|
||||||
class TestUrlElicitationRequiredError:
|
def test_url_elicitation_required_error_create_with_single_elicitation() -> None:
|
||||||
"""Tests for UrlElicitationRequiredError exception class."""
|
|
||||||
|
|
||||||
def test_create_with_single_elicitation(self) -> None:
|
|
||||||
"""Test creating error with a single elicitation."""
|
"""Test creating error with a single elicitation."""
|
||||||
elicitation = ElicitRequestURLParams(
|
elicitation = ElicitRequestURLParams(
|
||||||
mode="url",
|
mode="url",
|
||||||
|
|
@ -24,7 +21,8 @@ class TestUrlElicitationRequiredError:
|
||||||
assert len(error.elicitations) == 1
|
assert len(error.elicitations) == 1
|
||||||
assert error.elicitations[0].elicitation_id == "test-123"
|
assert error.elicitations[0].elicitation_id == "test-123"
|
||||||
|
|
||||||
def test_create_with_multiple_elicitations(self) -> None:
|
|
||||||
|
def test_url_elicitation_required_error_create_with_multiple_elicitations() -> None:
|
||||||
"""Test creating error with multiple elicitations uses plural message."""
|
"""Test creating error with multiple elicitations uses plural message."""
|
||||||
elicitations = [
|
elicitations = [
|
||||||
ElicitRequestURLParams(
|
ElicitRequestURLParams(
|
||||||
|
|
@ -45,7 +43,8 @@ class TestUrlElicitationRequiredError:
|
||||||
assert error.error.message == "URL elicitations required" # Plural
|
assert error.error.message == "URL elicitations required" # Plural
|
||||||
assert len(error.elicitations) == 2
|
assert len(error.elicitations) == 2
|
||||||
|
|
||||||
def test_custom_message(self) -> None:
|
|
||||||
|
def test_url_elicitation_required_error_custom_message() -> None:
|
||||||
"""Test creating error with a custom message."""
|
"""Test creating error with a custom message."""
|
||||||
elicitation = ElicitRequestURLParams(
|
elicitation = ElicitRequestURLParams(
|
||||||
mode="url",
|
mode="url",
|
||||||
|
|
@ -57,7 +56,8 @@ class TestUrlElicitationRequiredError:
|
||||||
|
|
||||||
assert error.error.message == "Custom message"
|
assert error.error.message == "Custom message"
|
||||||
|
|
||||||
def test_from_error_data(self) -> None:
|
|
||||||
|
def test_url_elicitation_required_error_from_error_data() -> None:
|
||||||
"""Test reconstructing error from ErrorData."""
|
"""Test reconstructing error from ErrorData."""
|
||||||
error_data = ErrorData(
|
error_data = ErrorData(
|
||||||
code=URL_ELICITATION_REQUIRED,
|
code=URL_ELICITATION_REQUIRED,
|
||||||
|
|
@ -80,7 +80,8 @@ class TestUrlElicitationRequiredError:
|
||||||
assert error.elicitations[0].elicitation_id == "test-123"
|
assert error.elicitations[0].elicitation_id == "test-123"
|
||||||
assert error.elicitations[0].url == "https://example.com/auth"
|
assert error.elicitations[0].url == "https://example.com/auth"
|
||||||
|
|
||||||
def test_from_error_data_wrong_code(self) -> None:
|
|
||||||
|
def test_url_elicitation_required_error_from_error_data_wrong_code() -> None:
|
||||||
"""Test that from_error raises ValueError for wrong error code."""
|
"""Test that from_error raises ValueError for wrong error code."""
|
||||||
error_data = ErrorData(
|
error_data = ErrorData(
|
||||||
code=-32600, # Wrong code
|
code=-32600, # Wrong code
|
||||||
|
|
@ -91,7 +92,8 @@ class TestUrlElicitationRequiredError:
|
||||||
with pytest.raises(ValueError, match="Expected error code"):
|
with pytest.raises(ValueError, match="Expected error code"):
|
||||||
UrlElicitationRequiredError.from_error(error_data)
|
UrlElicitationRequiredError.from_error(error_data)
|
||||||
|
|
||||||
def test_serialization_roundtrip(self) -> None:
|
|
||||||
|
def test_url_elicitation_required_error_serialization_roundtrip() -> None:
|
||||||
"""Test that error can be serialized and reconstructed."""
|
"""Test that error can be serialized and reconstructed."""
|
||||||
original = UrlElicitationRequiredError(
|
original = UrlElicitationRequiredError(
|
||||||
[
|
[
|
||||||
|
|
@ -114,7 +116,8 @@ class TestUrlElicitationRequiredError:
|
||||||
assert reconstructed.elicitations[0].url == original.elicitations[0].url
|
assert reconstructed.elicitations[0].url == original.elicitations[0].url
|
||||||
assert reconstructed.elicitations[0].message == original.elicitations[0].message
|
assert reconstructed.elicitations[0].message == original.elicitations[0].message
|
||||||
|
|
||||||
def test_error_data_contains_elicitations(self) -> None:
|
|
||||||
|
def test_url_elicitation_required_error_data_contains_elicitations() -> None:
|
||||||
"""Test that error data contains properly serialized elicitations."""
|
"""Test that error data contains properly serialized elicitations."""
|
||||||
elicitation = ElicitRequestURLParams(
|
elicitation = ElicitRequestURLParams(
|
||||||
mode="url",
|
mode="url",
|
||||||
|
|
@ -132,7 +135,8 @@ class TestUrlElicitationRequiredError:
|
||||||
assert elicit_data["url"] == "https://example.com/oauth"
|
assert elicit_data["url"] == "https://example.com/oauth"
|
||||||
assert elicit_data["elicitationId"] == "oauth-flow-1"
|
assert elicit_data["elicitationId"] == "oauth-flow-1"
|
||||||
|
|
||||||
def test_inherits_from_mcp_error(self) -> None:
|
|
||||||
|
def test_url_elicitation_required_error_inherits_from_mcp_error() -> None:
|
||||||
"""Test that UrlElicitationRequiredError inherits from McpError."""
|
"""Test that UrlElicitationRequiredError inherits from McpError."""
|
||||||
elicitation = ElicitRequestURLParams(
|
elicitation = ElicitRequestURLParams(
|
||||||
mode="url",
|
mode="url",
|
||||||
|
|
@ -145,7 +149,8 @@ class TestUrlElicitationRequiredError:
|
||||||
assert isinstance(error, McpError)
|
assert isinstance(error, McpError)
|
||||||
assert isinstance(error, Exception)
|
assert isinstance(error, Exception)
|
||||||
|
|
||||||
def test_exception_message(self) -> None:
|
|
||||||
|
def test_url_elicitation_required_error_exception_message() -> None:
|
||||||
"""Test that exception message is set correctly."""
|
"""Test that exception message is set correctly."""
|
||||||
elicitation = ElicitRequestURLParams(
|
elicitation = ElicitRequestURLParams(
|
||||||
mode="url",
|
mode="url",
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,8 @@ from mcp.shared.tool_name_validation import (
|
||||||
validate_tool_name,
|
validate_tool_name,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Tests for validate_tool_name function - valid names
|
||||||
|
|
||||||
class TestValidateToolName:
|
|
||||||
"""Tests for validate_tool_name function."""
|
|
||||||
|
|
||||||
class TestValidNames:
|
|
||||||
"""Test cases for valid tool names."""
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"tool_name",
|
"tool_name",
|
||||||
|
|
@ -38,27 +34,30 @@ class TestValidateToolName:
|
||||||
"max_length_128",
|
"max_length_128",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_accepts_valid_names(self, tool_name: str) -> None:
|
def test_validate_tool_name_accepts_valid_names(tool_name: str) -> None:
|
||||||
"""Valid tool names should pass validation with no warnings."""
|
"""Valid tool names should pass validation with no warnings."""
|
||||||
result = validate_tool_name(tool_name)
|
result = validate_tool_name(tool_name)
|
||||||
assert result.is_valid is True
|
assert result.is_valid is True
|
||||||
assert result.warnings == []
|
assert result.warnings == []
|
||||||
|
|
||||||
class TestInvalidNames:
|
|
||||||
"""Test cases for invalid tool names."""
|
|
||||||
|
|
||||||
def test_rejects_empty_name(self) -> None:
|
# Tests for validate_tool_name function - invalid names
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_tool_name_rejects_empty_name() -> None:
|
||||||
"""Empty names should be rejected."""
|
"""Empty names should be rejected."""
|
||||||
result = validate_tool_name("")
|
result = validate_tool_name("")
|
||||||
assert result.is_valid is False
|
assert result.is_valid is False
|
||||||
assert "Tool name cannot be empty" in result.warnings
|
assert "Tool name cannot be empty" in result.warnings
|
||||||
|
|
||||||
def test_rejects_name_exceeding_max_length(self) -> None:
|
|
||||||
|
def test_validate_tool_name_rejects_name_exceeding_max_length() -> None:
|
||||||
"""Names exceeding 128 characters should be rejected."""
|
"""Names exceeding 128 characters should be rejected."""
|
||||||
result = validate_tool_name("a" * 129)
|
result = validate_tool_name("a" * 129)
|
||||||
assert result.is_valid is False
|
assert result.is_valid is False
|
||||||
assert any("exceeds maximum length of 128 characters (current: 129)" in w for w in result.warnings)
|
assert any("exceeds maximum length of 128 characters (current: 129)" in w for w in result.warnings)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"tool_name,expected_char",
|
"tool_name,expected_char",
|
||||||
[
|
[
|
||||||
|
|
@ -74,13 +73,14 @@ class TestValidateToolName:
|
||||||
"with_at_symbol",
|
"with_at_symbol",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_rejects_invalid_characters(self, tool_name: str, expected_char: str) -> None:
|
def test_validate_tool_name_rejects_invalid_characters(tool_name: str, expected_char: str) -> None:
|
||||||
"""Names with invalid characters should be rejected."""
|
"""Names with invalid characters should be rejected."""
|
||||||
result = validate_tool_name(tool_name)
|
result = validate_tool_name(tool_name)
|
||||||
assert result.is_valid is False
|
assert result.is_valid is False
|
||||||
assert any("invalid characters" in w and expected_char in w for w in result.warnings)
|
assert any("invalid characters" in w and expected_char in w for w in result.warnings)
|
||||||
|
|
||||||
def test_rejects_multiple_invalid_chars(self) -> None:
|
|
||||||
|
def test_validate_tool_name_rejects_multiple_invalid_chars() -> None:
|
||||||
"""Names with multiple invalid chars should list all of them."""
|
"""Names with multiple invalid chars should list all of them."""
|
||||||
result = validate_tool_name("user name@domain,com")
|
result = validate_tool_name("user name@domain,com")
|
||||||
assert result.is_valid is False
|
assert result.is_valid is False
|
||||||
|
|
@ -89,43 +89,48 @@ class TestValidateToolName:
|
||||||
assert "'@'" in warning
|
assert "'@'" in warning
|
||||||
assert "','" in warning
|
assert "','" in warning
|
||||||
|
|
||||||
def test_rejects_unicode_characters(self) -> None:
|
|
||||||
|
def test_validate_tool_name_rejects_unicode_characters() -> None:
|
||||||
"""Names with unicode characters should be rejected."""
|
"""Names with unicode characters should be rejected."""
|
||||||
result = validate_tool_name("user-\u00f1ame") # n with tilde
|
result = validate_tool_name("user-\u00f1ame") # n with tilde
|
||||||
assert result.is_valid is False
|
assert result.is_valid is False
|
||||||
|
|
||||||
class TestWarningsForProblematicPatterns:
|
|
||||||
"""Test cases for valid names that generate warnings."""
|
|
||||||
|
|
||||||
def test_warns_on_leading_dash(self) -> None:
|
# Tests for validate_tool_name function - warnings for problematic patterns
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_tool_name_warns_on_leading_dash() -> None:
|
||||||
"""Names starting with dash should generate warning but be valid."""
|
"""Names starting with dash should generate warning but be valid."""
|
||||||
result = validate_tool_name("-get-user")
|
result = validate_tool_name("-get-user")
|
||||||
assert result.is_valid is True
|
assert result.is_valid is True
|
||||||
assert any("starts or ends with a dash" in w for w in result.warnings)
|
assert any("starts or ends with a dash" in w for w in result.warnings)
|
||||||
|
|
||||||
def test_warns_on_trailing_dash(self) -> None:
|
|
||||||
|
def test_validate_tool_name_warns_on_trailing_dash() -> None:
|
||||||
"""Names ending with dash should generate warning but be valid."""
|
"""Names ending with dash should generate warning but be valid."""
|
||||||
result = validate_tool_name("get-user-")
|
result = validate_tool_name("get-user-")
|
||||||
assert result.is_valid is True
|
assert result.is_valid is True
|
||||||
assert any("starts or ends with a dash" in w for w in result.warnings)
|
assert any("starts or ends with a dash" in w for w in result.warnings)
|
||||||
|
|
||||||
def test_warns_on_leading_dot(self) -> None:
|
|
||||||
|
def test_validate_tool_name_warns_on_leading_dot() -> None:
|
||||||
"""Names starting with dot should generate warning but be valid."""
|
"""Names starting with dot should generate warning but be valid."""
|
||||||
result = validate_tool_name(".get.user")
|
result = validate_tool_name(".get.user")
|
||||||
assert result.is_valid is True
|
assert result.is_valid is True
|
||||||
assert any("starts or ends with a dot" in w for w in result.warnings)
|
assert any("starts or ends with a dot" in w for w in result.warnings)
|
||||||
|
|
||||||
def test_warns_on_trailing_dot(self) -> None:
|
|
||||||
|
def test_validate_tool_name_warns_on_trailing_dot() -> None:
|
||||||
"""Names ending with dot should generate warning but be valid."""
|
"""Names ending with dot should generate warning but be valid."""
|
||||||
result = validate_tool_name("get.user.")
|
result = validate_tool_name("get.user.")
|
||||||
assert result.is_valid is True
|
assert result.is_valid is True
|
||||||
assert any("starts or ends with a dot" in w for w in result.warnings)
|
assert any("starts or ends with a dot" in w for w in result.warnings)
|
||||||
|
|
||||||
|
|
||||||
class TestIssueToolNameWarning:
|
# Tests for issue_tool_name_warning function
|
||||||
"""Tests for issue_tool_name_warning function."""
|
|
||||||
|
|
||||||
def test_logs_warnings(self, caplog: pytest.LogCaptureFixture) -> None:
|
|
||||||
|
def test_issue_tool_name_warning_logs_warnings(caplog: pytest.LogCaptureFixture) -> None:
|
||||||
"""Warnings should be logged at WARNING level."""
|
"""Warnings should be logged at WARNING level."""
|
||||||
warnings = ["Warning 1", "Warning 2"]
|
warnings = ["Warning 1", "Warning 2"]
|
||||||
with caplog.at_level(logging.WARNING):
|
with caplog.at_level(logging.WARNING):
|
||||||
|
|
@ -137,7 +142,8 @@ class TestIssueToolNameWarning:
|
||||||
assert "Tool registration will proceed" in caplog.text
|
assert "Tool registration will proceed" in caplog.text
|
||||||
assert "SEP-986" in caplog.text
|
assert "SEP-986" in caplog.text
|
||||||
|
|
||||||
def test_no_logging_for_empty_warnings(self, caplog: pytest.LogCaptureFixture) -> None:
|
|
||||||
|
def test_issue_tool_name_warning_no_logging_for_empty_warnings(caplog: pytest.LogCaptureFixture) -> None:
|
||||||
"""Empty warnings list should not produce any log output."""
|
"""Empty warnings list should not produce any log output."""
|
||||||
with caplog.at_level(logging.WARNING):
|
with caplog.at_level(logging.WARNING):
|
||||||
issue_tool_name_warning("test-tool", [])
|
issue_tool_name_warning("test-tool", [])
|
||||||
|
|
@ -145,27 +151,30 @@ class TestIssueToolNameWarning:
|
||||||
assert caplog.text == ""
|
assert caplog.text == ""
|
||||||
|
|
||||||
|
|
||||||
class TestValidateAndWarnToolName:
|
# Tests for validate_and_warn_tool_name function
|
||||||
"""Tests for validate_and_warn_tool_name function."""
|
|
||||||
|
|
||||||
def test_returns_true_for_valid_name(self) -> None:
|
|
||||||
|
def test_validate_and_warn_tool_name_returns_true_for_valid_name() -> None:
|
||||||
"""Valid names should return True."""
|
"""Valid names should return True."""
|
||||||
assert validate_and_warn_tool_name("valid-tool-name") is True
|
assert validate_and_warn_tool_name("valid-tool-name") is True
|
||||||
|
|
||||||
def test_returns_false_for_invalid_name(self) -> None:
|
|
||||||
|
def test_validate_and_warn_tool_name_returns_false_for_invalid_name() -> None:
|
||||||
"""Invalid names should return False."""
|
"""Invalid names should return False."""
|
||||||
assert validate_and_warn_tool_name("") is False
|
assert validate_and_warn_tool_name("") is False
|
||||||
assert validate_and_warn_tool_name("a" * 129) is False
|
assert validate_and_warn_tool_name("a" * 129) is False
|
||||||
assert validate_and_warn_tool_name("invalid name") is False
|
assert validate_and_warn_tool_name("invalid name") is False
|
||||||
|
|
||||||
def test_logs_warnings_for_invalid_name(self, caplog: pytest.LogCaptureFixture) -> None:
|
|
||||||
|
def test_validate_and_warn_tool_name_logs_warnings_for_invalid_name(caplog: pytest.LogCaptureFixture) -> None:
|
||||||
"""Invalid names should trigger warning logs."""
|
"""Invalid names should trigger warning logs."""
|
||||||
with caplog.at_level(logging.WARNING):
|
with caplog.at_level(logging.WARNING):
|
||||||
validate_and_warn_tool_name("invalid name")
|
validate_and_warn_tool_name("invalid name")
|
||||||
|
|
||||||
assert "Tool name validation warning" in caplog.text
|
assert "Tool name validation warning" in caplog.text
|
||||||
|
|
||||||
def test_no_warnings_for_clean_valid_name(self, caplog: pytest.LogCaptureFixture) -> None:
|
|
||||||
|
def test_validate_and_warn_tool_name_no_warnings_for_clean_valid_name(caplog: pytest.LogCaptureFixture) -> None:
|
||||||
"""Clean valid names should not produce any log output."""
|
"""Clean valid names should not produce any log output."""
|
||||||
with caplog.at_level(logging.WARNING):
|
with caplog.at_level(logging.WARNING):
|
||||||
result = validate_and_warn_tool_name("clean-tool-name")
|
result = validate_and_warn_tool_name("clean-tool-name")
|
||||||
|
|
@ -174,8 +183,8 @@ class TestValidateAndWarnToolName:
|
||||||
assert caplog.text == ""
|
assert caplog.text == ""
|
||||||
|
|
||||||
|
|
||||||
class TestEdgeCases:
|
# Tests for edge cases
|
||||||
"""Test edge cases and robustness."""
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"tool_name,is_valid,expected_warning_fragment",
|
"tool_name,is_valid,expected_warning_fragment",
|
||||||
|
|
@ -192,7 +201,7 @@ class TestEdgeCases:
|
||||||
"mixed_valid_invalid",
|
"mixed_valid_invalid",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_edge_cases(self, tool_name: str, is_valid: bool, expected_warning_fragment: str) -> None:
|
def test_edge_cases(tool_name: str, is_valid: bool, expected_warning_fragment: str) -> None:
|
||||||
"""Various edge cases should be handled correctly."""
|
"""Various edge cases should be handled correctly."""
|
||||||
result = validate_tool_name(tool_name)
|
result = validate_tool_name(tool_name)
|
||||||
assert result.is_valid is is_valid
|
assert result.is_valid is is_valid
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue