"""Tests for handling parameter name collisions between different OpenAPI parameter locations.""" from unittest.mock import AsyncMock, MagicMock import httpx import pytest from fastmcp.server.openapi import OpenAPITool from fastmcp.utilities.openapi import HTTPRoute, ParameterInfo, RequestBodyInfo @pytest.fixture def mock_client(): """Create a mock httpx.AsyncClient.""" client = AsyncMock(spec=httpx.AsyncClient) mock_response = MagicMock() mock_response.json.return_value = {"result": "success"} mock_response.raise_for_status.return_value = None client.request.return_value = mock_response return client class TestParameterCollisions: """Test parameter name collisions between path/query/header and body parameters.""" async def test_path_body_collision_current_broken_behavior(self, mock_client): """ Demonstrates the current broken behavior when a parameter exists in both path and body. This test should FAIL with the current implementation. """ # Create route with collision: id in both path and body route = HTTPRoute( path="/users/{id}", method="PUT", operation_id="update_user", parameters=[ ParameterInfo( name="id", location="path", required=True, schema={"type": "integer"}, ) ], request_body=RequestBodyInfo( content_schema={ "application/json": { "type": "object", "properties": { "id": {"type": "integer", "description": "User ID"}, "name": {"type": "string", "description": "User name"}, "email": {"type": "string", "description": "User email"}, }, "required": ["id", "name"], } } ), ) # Create tool with current implementation tool = OpenAPITool( client=mock_client, route=route, name="update_user", description="Update user", parameters={}, # Schema would be generated by _combine_schemas ) # This call should work but currently fails because body 'id' is excluded arguments = {"id": 123, "name": "John Doe", "email": "john@example.com"} await tool.run(arguments) # Check what was actually sent call_args = mock_client.request.call_args assert call_args is not None # Current broken behavior: id goes to path but is excluded from body # This means the body is missing the required 'id' field assert call_args[1]["url"] == "/users/123" # Path parameter works # This assertion will FAIL with current implementation because 'id' is excluded from body expected_body = {"id": 123, "name": "John Doe", "email": "john@example.com"} assert call_args[1]["json"] == expected_body, ( "Body should include 'id' parameter" ) async def test_path_body_collision_with_suffixing(self, mock_client): """ Test the desired behavior with parameter suffixing. This test should PASS after implementing the fix. """ # Create route with collision: id in both path and body route = HTTPRoute( path="/users/{id}", method="PUT", operation_id="update_user", parameters=[ ParameterInfo( name="id", location="path", required=True, schema={"type": "integer"}, ) ], request_body=RequestBodyInfo( content_schema={ "application/json": { "type": "object", "properties": { "id": {"type": "integer", "description": "User ID"}, "name": {"type": "string", "description": "User name"}, "email": {"type": "string", "description": "User email"}, }, "required": ["id", "name"], } } ), ) # Tool should be created with suffixed schema tool = OpenAPITool( client=mock_client, route=route, name="update_user", description="Update user", parameters={}, # Schema would include id__path and id ) # LLM would call with suffixed parameters arguments = { "id__path": 123, # Goes to path parameter "id": 123, # Goes to body parameter "name": "John Doe", "email": "john@example.com", } await tool.run(arguments) # Verify correct request was made call_args = mock_client.request.call_args assert call_args is not None # Path parameter should be populated from id__path assert call_args[1]["url"] == "/users/123" # Body should include id (from unsuffixed parameter) expected_body = {"id": 123, "name": "John Doe", "email": "john@example.com"} assert call_args[1]["json"] == expected_body async def test_query_body_collision_with_suffixing(self, mock_client): """Test parameter collision between query and body parameters.""" route = HTTPRoute( path="/search", method="POST", operation_id="search_users", parameters=[ ParameterInfo( name="limit", location="query", required=False, schema={"type": "integer", "default": 10}, ) ], request_body=RequestBodyInfo( content_schema={ "application/json": { "type": "object", "properties": { "limit": { "type": "integer", "description": "Max results in response", }, "query": {"type": "string", "description": "Search query"}, }, "required": ["query"], } } ), ) tool = OpenAPITool( client=mock_client, route=route, name="search_users", description="Search users", parameters={}, ) # LLM call with suffixed parameters arguments = { "limit__query": 5, # Goes to query parameter "limit": 100, # Goes to body parameter "query": "john", } await tool.run(arguments) call_args = mock_client.request.call_args assert call_args is not None # Query parameter from limit__query assert call_args[1]["params"] == {"limit": 5} # Body includes limit from unsuffixed parameter expected_body = {"limit": 100, "query": "john"} assert call_args[1]["json"] == expected_body async def test_no_collisions_unchanged_behavior(self, mock_client): """Test that parameters with no collisions keep original names.""" route = HTTPRoute( path="/users/{user_id}", method="POST", operation_id="create_user", parameters=[ ParameterInfo( name="user_id", location="path", required=True, schema={"type": "integer"}, ) ], request_body=RequestBodyInfo( content_schema={ "application/json": { "type": "object", "properties": { "name": {"type": "string"}, "email": {"type": "string"}, }, "required": ["name"], } } ), ) tool = OpenAPITool( client=mock_client, route=route, name="create_user", description="Create user", parameters={}, ) # No collisions, so original parameter names should work arguments = { "user_id": 123, # Path parameter (no suffix needed) "name": "John", # Body parameter "email": "john@example.com", } await tool.run(arguments) call_args = mock_client.request.call_args assert call_args is not None assert call_args[1]["url"] == "/users/123" expected_body = {"name": "John", "email": "john@example.com"} assert call_args[1]["json"] == expected_body