mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-24 06:24:18 +02:00
Merge pull request #1035 from jlowin/add-openapi-compatibility-tests
Add comprehensive OpenAPI 3.0 vs 3.1 compatibility tests
This commit is contained in:
commit
cdb7791346
1 changed files with 294 additions and 4 deletions
|
|
@ -7,6 +7,7 @@ from pydantic.networks import AnyUrl
|
|||
from fastmcp import FastMCP
|
||||
from fastmcp.client import Client
|
||||
from fastmcp.server.openapi import FastMCPOpenAPI
|
||||
from fastmcp.utilities.openapi import parse_openapi_to_http_routes
|
||||
|
||||
from .conftest import GET_ROUTE_MAPS
|
||||
|
||||
|
|
@ -80,8 +81,6 @@ class TestOpenAPI30Compatibility:
|
|||
],
|
||||
)
|
||||
elif request.url.path == "/products" and request.method == "POST":
|
||||
import json
|
||||
|
||||
data = json.loads(request.content)
|
||||
return httpx.Response(
|
||||
201, json={"id": "p3", "name": data["name"], "price": data["price"]}
|
||||
|
|
@ -257,8 +256,6 @@ class TestOpenAPI31Compatibility:
|
|||
],
|
||||
)
|
||||
elif request.url.path == "/orders" and request.method == "POST":
|
||||
import json
|
||||
|
||||
data = json.loads(request.content)
|
||||
return httpx.Response(
|
||||
201,
|
||||
|
|
@ -369,3 +366,296 @@ class TestOpenAPI31Compatibility:
|
|||
assert result.data["id"] == "o3"
|
||||
assert result.data["customer"] == "Charlie"
|
||||
assert result.data["items"] == ["item4", "item5"]
|
||||
|
||||
|
||||
class TestOpenAPIVersionDifferences:
|
||||
"""Test specific differences between OpenAPI 3.0 and 3.1 that can cause compatibility issues."""
|
||||
|
||||
def test_openapi_30_exclusive_maximum_boolean_format(self):
|
||||
"""Test OpenAPI 3.0 format with boolean exclusiveMaximum (reproduces GitHub issue #1021)."""
|
||||
spec_with_exclusive_max = {
|
||||
"openapi": "3.0.0",
|
||||
"info": {"title": "Loan API", "version": "1.0.0"},
|
||||
"paths": {
|
||||
"/loans": {
|
||||
"post": {
|
||||
"operationId": "createLoan",
|
||||
"summary": "Create a loan",
|
||||
"requestBody": {
|
||||
"required": True,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/LoanDetails"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
"responses": {"201": {"description": "Loan created"}},
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"LoanDetails": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"amount": {"type": "number", "minimum": 0},
|
||||
"interest_rate": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 100,
|
||||
"exclusiveMaximum": True, # OpenAPI 3.0 boolean format
|
||||
},
|
||||
},
|
||||
"required": ["amount", "interest_rate"],
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
# This should not raise a ValidationError
|
||||
routes = parse_openapi_to_http_routes(spec_with_exclusive_max)
|
||||
assert len(routes) == 1
|
||||
assert routes[0].operation_id == "createLoan"
|
||||
|
||||
def test_openapi_31_exclusive_maximum_numeric_format(self):
|
||||
"""Test OpenAPI 3.1 format with numeric exclusiveMaximum."""
|
||||
spec_with_exclusive_max = {
|
||||
"openapi": "3.1.0",
|
||||
"info": {"title": "Loan API", "version": "1.0.0"},
|
||||
"paths": {
|
||||
"/loans": {
|
||||
"post": {
|
||||
"operationId": "createLoan",
|
||||
"summary": "Create a loan",
|
||||
"requestBody": {
|
||||
"required": True,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/LoanDetails"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
"responses": {"201": {"description": "Loan created"}},
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"LoanDetails": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"amount": {"type": "number", "minimum": 0},
|
||||
"interest_rate": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"exclusiveMaximum": 100, # OpenAPI 3.1 numeric format
|
||||
},
|
||||
},
|
||||
"required": ["amount", "interest_rate"],
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
# This should not raise a ValidationError
|
||||
routes = parse_openapi_to_http_routes(spec_with_exclusive_max)
|
||||
assert len(routes) == 1
|
||||
assert routes[0].operation_id == "createLoan"
|
||||
|
||||
def test_openapi_30_nullable_format(self):
|
||||
"""Test OpenAPI 3.0 nullable format."""
|
||||
spec_with_nullable = {
|
||||
"openapi": "3.0.0",
|
||||
"info": {"title": "User API", "version": "1.0.0"},
|
||||
"paths": {
|
||||
"/users": {
|
||||
"post": {
|
||||
"operationId": "createUser",
|
||||
"summary": "Create a user",
|
||||
"requestBody": {
|
||||
"required": True,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"nullable": True, # OpenAPI 3.0 nullable format
|
||||
},
|
||||
},
|
||||
"required": ["name"],
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
"responses": {"201": {"description": "User created"}},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
# This should not raise a ValidationError
|
||||
routes = parse_openapi_to_http_routes(spec_with_nullable)
|
||||
assert len(routes) == 1
|
||||
assert routes[0].operation_id == "createUser"
|
||||
|
||||
def test_openapi_31_type_array_format(self):
|
||||
"""Test OpenAPI 3.1 type array format for nullable values."""
|
||||
spec_with_type_array = {
|
||||
"openapi": "3.1.0",
|
||||
"info": {"title": "User API", "version": "1.0.0"},
|
||||
"paths": {
|
||||
"/users": {
|
||||
"post": {
|
||||
"operationId": "createUser",
|
||||
"summary": "Create a user",
|
||||
"requestBody": {
|
||||
"required": True,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"email": {
|
||||
"type": [
|
||||
"string",
|
||||
"null",
|
||||
], # OpenAPI 3.1 type array format
|
||||
},
|
||||
},
|
||||
"required": ["name"],
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
"responses": {"201": {"description": "User created"}},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
# This should not raise a ValidationError
|
||||
routes = parse_openapi_to_http_routes(spec_with_type_array)
|
||||
assert len(routes) == 1
|
||||
assert routes[0].operation_id == "createUser"
|
||||
|
||||
def test_openapi_30_with_defs_and_exclusive_maximum(self):
|
||||
"""Test OpenAPI 3.0 with $defs and exclusiveMaximum (complex case from GitHub issue #1021)."""
|
||||
spec_with_defs = {
|
||||
"openapi": "3.0.0",
|
||||
"info": {"title": "Complex Loan API", "version": "1.0.0"},
|
||||
"paths": {
|
||||
"/loans": {
|
||||
"post": {
|
||||
"operationId": "createComplexLoan",
|
||||
"summary": "Create a complex loan",
|
||||
"requestBody": {
|
||||
"required": True,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"loanDetails": {
|
||||
"$ref": "#/components/schemas/LoanDetails"
|
||||
}
|
||||
},
|
||||
"required": ["loanDetails"],
|
||||
"$defs": {
|
||||
"LoanDetails": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"interest_rate": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 100,
|
||||
"exclusiveMaximum": True, # This should trigger the issue
|
||||
},
|
||||
},
|
||||
"required": ["interest_rate"],
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
"responses": {"201": {"description": "Complex loan created"}},
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"LoanDetails": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"interest_rate": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 100,
|
||||
"exclusiveMaximum": True,
|
||||
},
|
||||
},
|
||||
"required": ["interest_rate"],
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
# This should not raise a ValidationError (GitHub issue #1021 should be fixed)
|
||||
routes = parse_openapi_to_http_routes(spec_with_defs)
|
||||
assert len(routes) == 1
|
||||
assert routes[0].operation_id == "createComplexLoan"
|
||||
|
||||
def test_openapi_30_edge_case_with_multiple_exclusive_constraints(self):
|
||||
"""Test edge case with multiple exclusive constraints that might trigger validation issues."""
|
||||
spec_edge_case = {
|
||||
"openapi": "3.0.0",
|
||||
"info": {"title": "Edge Case API", "version": "1.0.0"},
|
||||
"paths": {
|
||||
"/validate": {
|
||||
"post": {
|
||||
"operationId": "validateData",
|
||||
"summary": "Validate data with edge case constraints",
|
||||
"requestBody": {
|
||||
"required": True,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"percentage": {
|
||||
"type": "number",
|
||||
"minimum": 0,
|
||||
"maximum": 100,
|
||||
"exclusiveMaximum": True,
|
||||
"exclusiveMinimum": True, # Both exclusive constraints
|
||||
},
|
||||
"rating": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 10,
|
||||
"exclusiveMaximum": True,
|
||||
},
|
||||
},
|
||||
"required": ["percentage", "rating"],
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
"responses": {"200": {"description": "Data validated"}},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
# This might trigger validation issues with multiple exclusive constraints
|
||||
routes = parse_openapi_to_http_routes(spec_edge_case)
|
||||
assert len(routes) == 1
|
||||
assert routes[0].operation_id == "validateData"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue