60e0ffc959
Upgrade checks / Notify on failure (push) Has been cancelled
Upgrade checks / Close issue on success (push) Has been cancelled
Schema Crash Test / Real-world schema crash test (232K schemas) (push) Has been cancelled
Run static analysis / static_analysis (push) Has been cancelled
Tests / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Tests / Tests with lowest-direct dependencies (push) Has been cancelled
Tests / MCP conformance tests (push) Has been cancelled
Tests / Integration tests (push) Has been cancelled
Tests / Package install smoke (push) Has been cancelled
Upgrade checks / Static analysis (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Upgrade checks / Integration tests (push) Has been cancelled
Update MCPServerConfig Schema / update-config-schema (push) Has been cancelled
Update SDK Documentation / update-sdk-docs (push) Has been cancelled
479 lines
14 KiB
Python
479 lines
14 KiB
Python
"""Unit tests for OpenAPI models."""
|
|
|
|
import pytest
|
|
from inline_snapshot import snapshot
|
|
|
|
from fastmcp.utilities.openapi.models import (
|
|
HttpMethod,
|
|
HTTPRoute,
|
|
ParameterInfo,
|
|
ParameterLocation,
|
|
RequestBodyInfo,
|
|
ResponseInfo,
|
|
)
|
|
|
|
|
|
class TestParameterInfo:
|
|
"""Test ParameterInfo model."""
|
|
|
|
def test_basic_parameter_creation(self):
|
|
"""Test creating a basic parameter."""
|
|
param = ParameterInfo(
|
|
name="id",
|
|
location="path",
|
|
required=True,
|
|
schema={"type": "integer"},
|
|
)
|
|
|
|
assert param.name == "id"
|
|
assert param.location == "path"
|
|
assert param.required is True
|
|
assert param.schema_ == {"type": "integer"}
|
|
assert param.description is None
|
|
assert param.explode is None
|
|
assert param.style is None
|
|
|
|
def test_parameter_with_all_fields(self):
|
|
"""Test creating parameter with all optional fields."""
|
|
param = ParameterInfo(
|
|
name="filter",
|
|
location="query",
|
|
required=False,
|
|
schema={"type": "object", "properties": {"name": {"type": "string"}}},
|
|
description="Filter criteria",
|
|
explode=True,
|
|
style="deepObject",
|
|
)
|
|
|
|
assert param.name == "filter"
|
|
assert param.location == "query"
|
|
assert param.required is False
|
|
assert param.description == "Filter criteria"
|
|
assert param.explode is True
|
|
assert param.style == "deepObject"
|
|
|
|
@pytest.mark.parametrize("location", ["path", "query", "header", "cookie"])
|
|
def test_valid_parameter_locations(self, location: ParameterLocation):
|
|
"""Test that all valid parameter locations are accepted."""
|
|
param = ParameterInfo(
|
|
name="test",
|
|
location=location,
|
|
required=False,
|
|
schema={"type": "string"},
|
|
)
|
|
assert param.location == location
|
|
|
|
def test_parameter_defaults(self):
|
|
"""Test parameter default values."""
|
|
param = ParameterInfo(
|
|
name="test",
|
|
location="query",
|
|
schema={"type": "string"},
|
|
)
|
|
|
|
# required should default to False for non-path parameters
|
|
assert param.required is False
|
|
assert param.description is None
|
|
assert param.explode is None
|
|
assert param.style is None
|
|
|
|
def test_parameter_with_empty_schema(self):
|
|
"""Test parameter with empty schema."""
|
|
param = ParameterInfo(
|
|
name="test",
|
|
location="query",
|
|
schema={},
|
|
)
|
|
|
|
assert param.schema_ == {}
|
|
|
|
|
|
class TestRequestBodyInfo:
|
|
"""Test RequestBodyInfo model."""
|
|
|
|
def test_basic_request_body(self):
|
|
"""Test creating a basic request body."""
|
|
request_body = RequestBodyInfo(
|
|
required=True,
|
|
description="User data",
|
|
)
|
|
|
|
assert request_body.required is True
|
|
assert request_body.description == "User data"
|
|
assert request_body.content_schema == {}
|
|
|
|
def test_request_body_with_content_schema(self):
|
|
"""Test request body with content schema."""
|
|
content_schema = {
|
|
"application/json": {
|
|
"type": "object",
|
|
"properties": {
|
|
"name": {"type": "string"},
|
|
"email": {"type": "string"},
|
|
},
|
|
"required": ["name"],
|
|
}
|
|
}
|
|
|
|
request_body = RequestBodyInfo(
|
|
required=True,
|
|
content_schema=content_schema,
|
|
)
|
|
|
|
assert request_body.content_schema == content_schema
|
|
|
|
def test_request_body_defaults(self):
|
|
"""Test request body default values."""
|
|
request_body = RequestBodyInfo()
|
|
|
|
assert request_body.required is False
|
|
assert request_body.description is None
|
|
assert request_body.content_schema == {}
|
|
|
|
def test_request_body_multiple_content_types(self):
|
|
"""Test request body with multiple content types."""
|
|
content_schema = {
|
|
"application/json": {
|
|
"type": "object",
|
|
"properties": {"name": {"type": "string"}},
|
|
},
|
|
"application/xml": {
|
|
"type": "object",
|
|
"properties": {"name": {"type": "string"}},
|
|
},
|
|
}
|
|
|
|
request_body = RequestBodyInfo(content_schema=content_schema)
|
|
|
|
assert len(request_body.content_schema) == 2
|
|
assert "application/json" in request_body.content_schema
|
|
assert "application/xml" in request_body.content_schema
|
|
|
|
|
|
class TestResponseInfo:
|
|
"""Test ResponseInfo model."""
|
|
|
|
def test_basic_response(self):
|
|
"""Test creating a basic response."""
|
|
response = ResponseInfo(description="Success response")
|
|
|
|
assert response.description == "Success response"
|
|
assert response.content_schema == {}
|
|
|
|
def test_response_with_content_schema(self):
|
|
"""Test response with content schema."""
|
|
content_schema = {
|
|
"application/json": {
|
|
"type": "object",
|
|
"properties": {
|
|
"id": {"type": "integer"},
|
|
"message": {"type": "string"},
|
|
},
|
|
}
|
|
}
|
|
|
|
response = ResponseInfo(
|
|
description="User created",
|
|
content_schema=content_schema,
|
|
)
|
|
|
|
assert response.description == "User created"
|
|
assert response.content_schema == content_schema
|
|
|
|
def test_response_required_description(self):
|
|
"""Test that response description is required."""
|
|
# Should not raise an error - description has a default
|
|
response = ResponseInfo()
|
|
assert response.description is None
|
|
|
|
|
|
class TestHTTPRoute:
|
|
"""Test HTTPRoute model."""
|
|
|
|
def test_basic_route_creation(self):
|
|
"""Test creating a basic HTTP route."""
|
|
route = HTTPRoute(
|
|
path="/users/{id}",
|
|
method="GET",
|
|
operation_id="get_user",
|
|
)
|
|
|
|
assert route.path == "/users/{id}"
|
|
assert route.method == "GET"
|
|
assert route.operation_id == "get_user"
|
|
assert route.summary is None
|
|
assert route.description is None
|
|
assert route.tags == []
|
|
assert route.parameters == []
|
|
assert route.request_body is None
|
|
assert route.responses == {}
|
|
|
|
def test_route_with_all_fields(self):
|
|
"""Test creating route with all fields."""
|
|
parameters = [
|
|
ParameterInfo(
|
|
name="id",
|
|
location="path",
|
|
required=True,
|
|
schema={"type": "integer"},
|
|
)
|
|
]
|
|
|
|
request_body = RequestBodyInfo(
|
|
required=True,
|
|
content_schema={
|
|
"application/json": {
|
|
"type": "object",
|
|
"properties": {"name": {"type": "string"}},
|
|
}
|
|
},
|
|
)
|
|
|
|
responses = {
|
|
"200": ResponseInfo(
|
|
description="Success",
|
|
content_schema={
|
|
"application/json": {
|
|
"type": "object",
|
|
"properties": {"id": {"type": "integer"}},
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
route = HTTPRoute(
|
|
path="/users/{id}",
|
|
method="PUT",
|
|
operation_id="update_user",
|
|
summary="Update user",
|
|
description="Update user by ID",
|
|
tags=["users"],
|
|
parameters=parameters,
|
|
request_body=request_body,
|
|
responses=responses,
|
|
request_schemas={"User": {"type": "object"}},
|
|
extensions={"x-custom": "value"},
|
|
)
|
|
|
|
assert route.path == "/users/{id}"
|
|
assert route.method == "PUT"
|
|
assert route.operation_id == "update_user"
|
|
assert route.summary == "Update user"
|
|
assert route.description == "Update user by ID"
|
|
assert route.tags == ["users"]
|
|
assert len(route.parameters) == 1
|
|
assert route.request_body is not None
|
|
assert "200" in route.responses
|
|
assert "User" in route.request_schemas
|
|
assert route.extensions["x-custom"] == "value"
|
|
|
|
def test_route_pre_calculated_fields(self):
|
|
"""Test route with pre-calculated fields."""
|
|
route = HTTPRoute(
|
|
path="/test",
|
|
method="GET",
|
|
operation_id="test",
|
|
flat_param_schema={
|
|
"type": "object",
|
|
"properties": {"id": {"type": "integer"}},
|
|
},
|
|
parameter_map={"id": {"location": "path", "openapi_name": "id"}},
|
|
)
|
|
|
|
assert route.flat_param_schema["type"] == "object"
|
|
assert "id" in route.flat_param_schema["properties"]
|
|
assert "id" in route.parameter_map
|
|
assert route.parameter_map["id"]["location"] == "path"
|
|
|
|
@pytest.mark.parametrize(
|
|
"method", ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]
|
|
)
|
|
def test_valid_http_methods(self, method: HttpMethod):
|
|
"""Test that all valid HTTP methods are accepted."""
|
|
route = HTTPRoute(
|
|
path="/test",
|
|
method=method,
|
|
operation_id="test",
|
|
)
|
|
assert route.method == method
|
|
|
|
def test_route_with_empty_collections(self):
|
|
"""Test route with empty collections."""
|
|
route = HTTPRoute(
|
|
path="/test",
|
|
method="GET",
|
|
operation_id="test",
|
|
tags=[],
|
|
parameters=[],
|
|
responses={},
|
|
request_schemas={},
|
|
extensions={},
|
|
)
|
|
|
|
assert route.tags == []
|
|
assert route.parameters == []
|
|
assert route.responses == {}
|
|
assert route.request_schemas == {}
|
|
assert route.response_schemas == {}
|
|
assert route.extensions == {}
|
|
|
|
def test_route_defaults(self):
|
|
"""Test route default values."""
|
|
route = HTTPRoute(
|
|
path="/test",
|
|
method="GET",
|
|
operation_id="test",
|
|
)
|
|
|
|
assert route.summary is None
|
|
assert route.description is None
|
|
assert route.tags == []
|
|
assert route.parameters == []
|
|
assert route.request_body is None
|
|
assert route.responses == {}
|
|
assert route.request_schemas == {}
|
|
assert route.response_schemas == {}
|
|
assert route.extensions == {}
|
|
assert route.flat_param_schema == {}
|
|
assert route.parameter_map == {}
|
|
|
|
|
|
class TestModelValidation:
|
|
"""Test model validation and error cases."""
|
|
|
|
def test_parameter_info_validation(self):
|
|
"""Test ParameterInfo validation."""
|
|
# Valid parameter
|
|
param = ParameterInfo(
|
|
name="test",
|
|
location="query",
|
|
schema={"type": "string"},
|
|
)
|
|
assert param.name == "test"
|
|
|
|
def test_route_validation(self):
|
|
"""Test HTTPRoute validation."""
|
|
# Valid route
|
|
route = HTTPRoute(
|
|
path="/test",
|
|
method="GET",
|
|
operation_id="test",
|
|
)
|
|
assert route.path == "/test"
|
|
|
|
def test_nested_model_validation(self):
|
|
"""Test validation of nested models."""
|
|
# Create route with nested models
|
|
param = ParameterInfo(
|
|
name="id",
|
|
location="path",
|
|
required=True,
|
|
schema={"type": "integer"},
|
|
)
|
|
|
|
request_body = RequestBodyInfo(required=True)
|
|
|
|
route = HTTPRoute(
|
|
path="/test/{id}",
|
|
method="POST",
|
|
operation_id="test",
|
|
parameters=[param],
|
|
request_body=request_body,
|
|
)
|
|
|
|
assert len(route.parameters) == 1
|
|
assert route.parameters[0].name == "id"
|
|
assert route.request_body is not None
|
|
assert route.request_body.required is True
|
|
|
|
|
|
class TestModelSerialization:
|
|
"""Test model serialization and deserialization."""
|
|
|
|
def test_parameter_info_serialization(self):
|
|
"""Test ParameterInfo serialization."""
|
|
param = ParameterInfo(
|
|
name="filter",
|
|
location="query",
|
|
required=False,
|
|
schema={"type": "object"},
|
|
description="Filter criteria",
|
|
explode=True,
|
|
style="deepObject",
|
|
)
|
|
|
|
# Test model_dump with alias
|
|
data = param.model_dump(by_alias=True)
|
|
|
|
assert data["name"] == "filter"
|
|
assert data["location"] == "query"
|
|
assert data["required"] is False
|
|
assert data["schema"] == {"type": "object"} # Using alias
|
|
assert data["description"] == "Filter criteria"
|
|
assert data["explode"] is True
|
|
assert data["style"] == "deepObject"
|
|
|
|
def test_route_serialization(self):
|
|
"""Test HTTPRoute serialization."""
|
|
param = ParameterInfo(
|
|
name="id",
|
|
location="path",
|
|
required=True,
|
|
schema={"type": "integer"},
|
|
)
|
|
|
|
route = HTTPRoute(
|
|
path="/users/{id}",
|
|
method="GET",
|
|
operation_id="get_user",
|
|
parameters=[param],
|
|
)
|
|
|
|
# Test model_dump
|
|
data = route.model_dump()
|
|
|
|
assert data["path"] == "/users/{id}"
|
|
assert data["method"] == "GET"
|
|
assert data["operation_id"] == "get_user"
|
|
assert len(data["parameters"]) == 1
|
|
assert data["parameters"][0]["name"] == "id"
|
|
|
|
def test_model_reconstruction(self):
|
|
"""Test reconstructing models from serialized data."""
|
|
# Create original parameter
|
|
original_param = ParameterInfo(
|
|
name="test",
|
|
location="query",
|
|
schema={"type": "string"},
|
|
description="Test parameter",
|
|
)
|
|
|
|
# Serialize and reconstruct using by_alias
|
|
data = original_param.model_dump(by_alias=True)
|
|
|
|
data == snapshot(
|
|
{
|
|
"name": "test",
|
|
"location": "query",
|
|
"required": False,
|
|
"schema": {"type": "string"},
|
|
"description": "Test parameter",
|
|
"explode": None,
|
|
"style": None,
|
|
}
|
|
)
|
|
|
|
reconstructed_param = ParameterInfo(
|
|
name=data["name"],
|
|
location=data["location"],
|
|
schema=data["schema"],
|
|
description=data["description"],
|
|
explode=data["explode"],
|
|
style=data["style"],
|
|
)
|
|
|
|
assert reconstructed_param.name == original_param.name
|
|
assert reconstructed_param.location == original_param.location
|
|
assert reconstructed_param.schema_ == original_param.schema_
|
|
assert reconstructed_param.description == original_param.description
|