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:
Jeremiah Lowin 2025-07-03 21:11:45 -04:00 committed by GitHub
commit cdb7791346
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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"