mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-24 06:24:18 +02:00
Update tests
This commit is contained in:
parent
2cce73a133
commit
20dd0047c7
3 changed files with 261 additions and 100 deletions
|
|
@ -231,14 +231,60 @@ class OpenAPITool(Tool):
|
|||
path = path.replace(f"{{{param_name}}}", str(param_value))
|
||||
|
||||
# Prepare query parameters - filter out None and empty strings
|
||||
query_params = {
|
||||
p.name: kwargs.get(p.name)
|
||||
for p in self._route.parameters
|
||||
if p.location == "query"
|
||||
and p.name in kwargs
|
||||
and kwargs.get(p.name) is not None
|
||||
and kwargs.get(p.name) != ""
|
||||
}
|
||||
query_params = {}
|
||||
for p in self._route.parameters:
|
||||
if (
|
||||
p.location == "query"
|
||||
and p.name in kwargs
|
||||
and kwargs.get(p.name) is not None
|
||||
and kwargs.get(p.name) != ""
|
||||
):
|
||||
param_value = kwargs.get(p.name)
|
||||
|
||||
# Format array query parameters as comma-separated strings
|
||||
# following OpenAPI form style (default for query parameters)
|
||||
if isinstance(param_value, list) and p.schema_.get("type") == "array":
|
||||
# Get explode parameter from schema, default is True for query parameters
|
||||
# If explode is True, the array is serialized as separate parameters
|
||||
# If explode is False, the array is serialized as a comma-separated string
|
||||
explode = p.schema_.get("explode", True)
|
||||
|
||||
if explode:
|
||||
# When explode=True, we pass the array directly, which HTTPX will serialize
|
||||
# as multiple parameters with the same name
|
||||
query_params[p.name] = param_value
|
||||
else:
|
||||
# For arrays of simple types (strings, numbers, etc.), join with commas
|
||||
if all(
|
||||
isinstance(item, str | int | float | bool)
|
||||
for item in param_value
|
||||
):
|
||||
query_params[p.name] = ",".join(str(v) for v in param_value)
|
||||
else:
|
||||
# For complex types, try to create a simpler representation
|
||||
try:
|
||||
# Try to create a simple string representation
|
||||
formatted_parts = []
|
||||
for item in param_value:
|
||||
if isinstance(item, dict):
|
||||
# For objects, serialize key-value pairs
|
||||
item_parts = []
|
||||
for k, v in item.items():
|
||||
item_parts.append(f"{k}:{v}")
|
||||
formatted_parts.append(".".join(item_parts))
|
||||
else:
|
||||
formatted_parts.append(str(item))
|
||||
|
||||
query_params[p.name] = ",".join(formatted_parts)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Failed to format complex array query parameter '{p.name}': {e}"
|
||||
)
|
||||
# Fallback to string representation
|
||||
query_params[p.name] = param_value
|
||||
else:
|
||||
# Non-array parameters are passed as is
|
||||
query_params[p.name] = param_value
|
||||
|
||||
# Prepare headers - fix typing by ensuring all values are strings
|
||||
headers = {}
|
||||
|
|
|
|||
|
|
@ -1,90 +0,0 @@
|
|||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from fastmcp.server.openapi import OpenAPITool
|
||||
from fastmcp.utilities.openapi import HTTPRoute, ParameterInfo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_client():
|
||||
"""Create a mock httpx.AsyncClient."""
|
||||
client = AsyncMock(spec=httpx.AsyncClient)
|
||||
# Set up a mock response
|
||||
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
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_array_path_parameter_handling(mock_client):
|
||||
"""Test how array path parameters are handled."""
|
||||
# Create a simple route with array path parameter
|
||||
route = HTTPRoute(
|
||||
path="/select/{days}",
|
||||
method="PUT",
|
||||
operation_id="test-operation",
|
||||
parameters=[
|
||||
ParameterInfo(
|
||||
name="days",
|
||||
location="path",
|
||||
required=True,
|
||||
schema={
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"monday",
|
||||
"tuesday",
|
||||
"wednesday",
|
||||
"thursday",
|
||||
"friday",
|
||||
"saturday",
|
||||
"sunday",
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
# Create the tool
|
||||
tool = OpenAPITool(
|
||||
client=mock_client,
|
||||
route=route,
|
||||
name="test-operation",
|
||||
description="Test operation",
|
||||
parameters={},
|
||||
)
|
||||
|
||||
# Test with a single value
|
||||
await tool._execute_request(days=["monday"])
|
||||
|
||||
# Check that the path parameter is formatted correctly
|
||||
# This is where the bug is: it should be '/select/monday' not '/select/[\'monday\']'
|
||||
mock_client.request.assert_called_with(
|
||||
method="PUT",
|
||||
url="/select/monday", # This is the expected format
|
||||
params={},
|
||||
headers={},
|
||||
json=None,
|
||||
timeout=None,
|
||||
)
|
||||
mock_client.request.reset_mock()
|
||||
|
||||
# Test with multiple values
|
||||
await tool._execute_request(days=["monday", "tuesday"])
|
||||
|
||||
# Check that the path parameter is formatted correctly
|
||||
# It should be '/select/monday,tuesday' not '/select/[\'monday\', \'tuesday\']'
|
||||
mock_client.request.assert_called_with(
|
||||
method="PUT",
|
||||
url="/select/monday,tuesday", # This is the expected format
|
||||
params={},
|
||||
headers={},
|
||||
json=None,
|
||||
timeout=None,
|
||||
)
|
||||
|
|
@ -1,10 +1,12 @@
|
|||
from typing import Annotated, Literal
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from fastapi import FastAPI, Query
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.openapi import OpenAPITool
|
||||
from fastmcp import Client, FastMCP
|
||||
from fastmcp.server.openapi import OpenAPITool, RouteMap, RouteType
|
||||
from fastmcp.utilities.openapi import HTTPRoute, ParameterInfo
|
||||
|
||||
|
||||
|
|
@ -258,3 +260,206 @@ async def test_complex_nested_array_path_parameter(mock_client):
|
|||
assert "active" in called_url, "The URL should contain filter values"
|
||||
assert "}" not in called_url, "The URL should not contain Python object syntax"
|
||||
assert "{" not in called_url, "The URL should not contain Python object syntax"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_array_query_param_with_fastapi():
|
||||
"""Test array query parameters using FastAPI and FastMCP.from_fastapi integration."""
|
||||
# Create a FastAPI app with a route that has an array query parameter
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/select")
|
||||
async def select_days(
|
||||
days: Annotated[
|
||||
list[
|
||||
Literal[
|
||||
"monday",
|
||||
"tuesday",
|
||||
"wednesday",
|
||||
"thursday",
|
||||
"friday",
|
||||
"saturday",
|
||||
"sunday",
|
||||
]
|
||||
],
|
||||
Query(explode=True),
|
||||
],
|
||||
): # Using explode=True to get days=monday&days=tuesday format
|
||||
return {"selected": days}
|
||||
|
||||
# Create a FastMCP server from the FastAPI app
|
||||
mcp = FastMCP.from_fastapi(
|
||||
app,
|
||||
route_maps=[
|
||||
RouteMap(methods=["GET"], pattern=r".*", route_type=RouteType.TOOL)
|
||||
],
|
||||
)
|
||||
|
||||
# Test with the client
|
||||
async with Client(mcp) as client:
|
||||
# Get the actual tool name first
|
||||
tools = await client.list_tools()
|
||||
tool_names = [tool.name for tool in tools]
|
||||
assert len(tool_names) == 1, (
|
||||
f"Expected one tool, got {len(tool_names)}: {tool_names}"
|
||||
)
|
||||
tool_name = tool_names[0]
|
||||
|
||||
# Single day
|
||||
result = await client.call_tool(tool_name, {"days": ["monday"]})
|
||||
# Client returns TextContent objects, so parse the JSON
|
||||
assert len(result) == 1
|
||||
assert result[0].type == "text"
|
||||
import json
|
||||
|
||||
result_data = json.loads(result[0].text)
|
||||
assert result_data == {"selected": ["monday"]}
|
||||
|
||||
# Multiple days
|
||||
result = await client.call_tool(tool_name, {"days": ["monday", "tuesday"]})
|
||||
assert len(result) == 1
|
||||
assert result[0].type == "text"
|
||||
result_data = json.loads(result[0].text)
|
||||
assert result_data == {"selected": ["monday", "tuesday"]}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_array_query_parameter_format(mock_client):
|
||||
"""Test that array query parameters are formatted as comma-separated values when explode=False."""
|
||||
# Create a route with array query parameter
|
||||
route = HTTPRoute(
|
||||
path="/select",
|
||||
method="GET",
|
||||
operation_id="test-operation",
|
||||
parameters=[
|
||||
ParameterInfo(
|
||||
name="days",
|
||||
location="query", # This is a query parameter
|
||||
required=True,
|
||||
schema={
|
||||
"type": "array",
|
||||
"explode": False, # Set explode=False to test comma-separated formatting
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"monday",
|
||||
"tuesday",
|
||||
"wednesday",
|
||||
"thursday",
|
||||
"friday",
|
||||
"saturday",
|
||||
"sunday",
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
# Create the tool
|
||||
tool = OpenAPITool(
|
||||
client=mock_client,
|
||||
route=route,
|
||||
name="test-operation",
|
||||
description="Test operation",
|
||||
parameters={},
|
||||
)
|
||||
|
||||
# Test with a single value
|
||||
await tool._execute_request(days=["monday"])
|
||||
|
||||
# Check that the query parameter is formatted correctly
|
||||
mock_client.request.assert_called_with(
|
||||
method="GET",
|
||||
url="/select",
|
||||
params={"days": "monday"}, # Should be formatted as a string, not a list
|
||||
headers={},
|
||||
json=None,
|
||||
timeout=None,
|
||||
)
|
||||
mock_client.request.reset_mock()
|
||||
|
||||
# Test with multiple values
|
||||
await tool._execute_request(days=["monday", "tuesday"])
|
||||
|
||||
# Check that the query parameter is formatted correctly
|
||||
# It should be 'days=monday,tuesday' not 'days=["monday","tuesday"]'
|
||||
mock_client.request.assert_called_with(
|
||||
method="GET",
|
||||
url="/select",
|
||||
params={"days": "monday,tuesday"}, # Should be comma-separated
|
||||
headers={},
|
||||
json=None,
|
||||
timeout=None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_array_query_parameter_exploded_format(mock_client):
|
||||
"""Test that array query parameters are formatted as separate parameters when explode=True."""
|
||||
# Create a route with array query parameter with explode=True (default)
|
||||
route = HTTPRoute(
|
||||
path="/select-exploded",
|
||||
method="GET",
|
||||
operation_id="test-exploded-operation",
|
||||
parameters=[
|
||||
ParameterInfo(
|
||||
name="days",
|
||||
location="query", # This is a query parameter
|
||||
required=True,
|
||||
schema={
|
||||
"type": "array",
|
||||
"explode": True, # Set explode=True for separate parameter serialization
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"monday",
|
||||
"tuesday",
|
||||
"wednesday",
|
||||
"thursday",
|
||||
"friday",
|
||||
"saturday",
|
||||
"sunday",
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
# Create the tool
|
||||
tool = OpenAPITool(
|
||||
client=mock_client,
|
||||
route=route,
|
||||
name="test-exploded-operation",
|
||||
description="Test operation with exploded arrays",
|
||||
parameters={},
|
||||
)
|
||||
|
||||
# Test with a single value
|
||||
await tool._execute_request(days=["monday"])
|
||||
|
||||
# Check that the query parameter is formatted correctly
|
||||
mock_client.request.assert_called_with(
|
||||
method="GET",
|
||||
url="/select-exploded",
|
||||
params={"days": ["monday"]}, # Should be passed as a list for explode=True
|
||||
headers={},
|
||||
json=None,
|
||||
timeout=None,
|
||||
)
|
||||
mock_client.request.reset_mock()
|
||||
|
||||
# Test with multiple values
|
||||
await tool._execute_request(days=["monday", "tuesday"])
|
||||
|
||||
# Check that the query parameter is formatted correctly
|
||||
# It should be passed as an array, which httpx will serialize as days=monday&days=tuesday
|
||||
mock_client.request.assert_called_with(
|
||||
method="GET",
|
||||
url="/select-exploded",
|
||||
params={"days": ["monday", "tuesday"]}, # Should be passed as a list
|
||||
headers={},
|
||||
json=None,
|
||||
timeout=None,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue