Browse Source
When a route is created with multiple HTTP methods using add_api_route or api_route, the operation ID was the same for all methods, causing duplicate operation ID warnings in OpenAPI schema generation. Changes: - Modified get_openapi_operation_metadata in fastapi/openapi/utils.py - When a route has multiple methods and no explicit operation_id, append the actual HTTP method to ensure unique IDs - Added comprehensive tests in test_duplicate_operation_id.py Fixes #13175pull/14869/head
2 changed files with 135 additions and 0 deletions
@ -0,0 +1,122 @@ |
|||||
|
""" |
||||
|
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"} |
||||
|
|
||||
|
# 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: " |
||||
|
f"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"} |
||||
|
|
||||
|
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": []} |
||||
|
|
||||
|
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"]) |
||||
|
|
||||
|
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"] |
||||
Loading…
Reference in new issue