You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

186 lines
6.1 KiB

"""
Test that routes with multiple methods get unique operation IDs.
Related to issue #13175
"""
import warnings
from fastapi import FastAPI
from fastapi.testclient import TestClient
def test_multiple_methods_unique_operation_ids():
"""Test that a route with multiple methods generates unique operation IDs"""
app = FastAPI()
@app.api_route("/clear", methods=["POST", "DELETE"])
def clear():
return {"message": "cleared"}
client = TestClient(app)
# Test that the endpoints work
response = client.post("/clear")
assert response.status_code == 200
assert response.json() == {"message": "cleared"}
response = client.delete("/clear")
assert response.status_code == 200
assert response.json() == {"message": "cleared"}
# Capture warnings
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
# Get OpenAPI schema which triggers operation ID generation
openapi_schema = app.openapi()
# Check that no duplicate operation ID warning was raised
duplicate_warnings = [
warning for warning in w if "Duplicate Operation ID" in str(warning.message)
]
assert len(duplicate_warnings) == 0, (
f"Found {len(duplicate_warnings)} duplicate operation ID warnings: "
f"{[str(w.message) for w in duplicate_warnings]}"
)
# Verify both methods are in the schema with different operation IDs
assert "/clear" in openapi_schema["paths"]
clear_path = openapi_schema["paths"]["/clear"]
# Both POST and DELETE should be present
assert "post" in clear_path
assert "delete" in clear_path
# Operation IDs should be different
post_op_id = clear_path["post"]["operationId"]
delete_op_id = clear_path["delete"]["operationId"]
assert post_op_id != delete_op_id, (
f"Operation IDs should be different: POST={post_op_id}, DELETE={delete_op_id}"
)
def test_multiple_routes_with_multiple_methods():
"""Test multiple routes each with multiple methods"""
app = FastAPI()
@app.api_route("/clear", methods=["POST", "DELETE"])
def clear():
return {"message": "cleared"}
@app.api_route("/reset", methods=["POST", "PUT"])
def reset():
return {"message": "reset"}
client = TestClient(app)
# Test that all endpoints work
assert client.post("/clear").status_code == 200
assert client.delete("/clear").status_code == 200
assert client.post("/reset").status_code == 200
assert client.put("/reset").status_code == 200
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
openapi_schema = app.openapi()
duplicate_warnings = [
warning for warning in w if "Duplicate Operation ID" in str(warning.message)
]
assert len(duplicate_warnings) == 0
# Verify all operation IDs are unique
operation_ids = set()
for path_data in openapi_schema["paths"].values():
for method_data in path_data.values():
if isinstance(method_data, dict) and "operationId" in method_data:
op_id = method_data["operationId"]
assert op_id not in operation_ids, f"Duplicate operation ID: {op_id}"
operation_ids.add(op_id)
def test_single_method_route_unchanged():
"""Test that routes with single methods still work as before"""
app = FastAPI()
@app.get("/items")
def get_items():
return {"items": []}
client = TestClient(app)
# Test that the endpoint works
response = client.get("/items")
assert response.status_code == 200
assert response.json() == {"items": []}
openapi_schema = app.openapi()
# Should have the expected structure
assert "/items" in openapi_schema["paths"]
assert "get" in openapi_schema["paths"]["/items"]
assert "operationId" in openapi_schema["paths"]["/items"]["get"]
def test_add_api_route_with_multiple_methods():
"""Test using add_api_route directly with multiple methods"""
app = FastAPI()
def clear_handler():
return {"message": "cleared"}
app.add_api_route("/clear", clear_handler, methods=["POST", "DELETE"])
client = TestClient(app)
# Test that both methods work
assert client.post("/clear").status_code == 200
assert client.delete("/clear").status_code == 200
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
openapi_schema = app.openapi()
duplicate_warnings = [
warning for warning in w if "Duplicate Operation ID" in str(warning.message)
]
assert len(duplicate_warnings) == 0
clear_path = openapi_schema["paths"]["/clear"]
assert clear_path["post"]["operationId"] != clear_path["delete"]["operationId"]
def test_explicit_operation_id_with_multiple_methods():
"""Test that explicit operation_id is respected and not modified"""
app = FastAPI()
@app.api_route("/clear", methods=["POST", "DELETE"], operation_id="custom_clear")
def clear():
return {"message": "cleared"}
client = TestClient(app)
# Test that both methods work
assert client.post("/clear").status_code == 200
assert client.delete("/clear").status_code == 200
# When explicit operation_id is provided, it's used as-is which may cause
# duplicate operation ID warnings - this is expected behavior
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
openapi_schema = app.openapi()
# If user provides explicit operation_id with multiple methods,
# they'll get a duplicate warning - this is intentional
duplicate_warnings = [
warning for warning in w if "Duplicate Operation ID" in str(warning.message)
]
# We expect a duplicate warning because the user explicitly set the same
# operation_id for multiple methods
assert len(duplicate_warnings) > 0
clear_path = openapi_schema["paths"]["/clear"]
# Both methods use the same explicit operation_id
assert clear_path["post"]["operationId"] == "custom_clear"
assert clear_path["delete"]["operationId"] == "custom_clear"