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.

208 lines
6.2 KiB

"""
Tests for Annotated types with ForwardRef support.
Tests for issue #13056: Annotated types with ForwardRef cause errors.
"""
from typing import Annotated, Optional
import pytest
from fastapi import Body, FastAPI, Header, Path, Query
from fastapi.testclient import TestClient
from pydantic import BaseModel
class UserModel(BaseModel):
"""User model for testing ForwardRef."""
name: str = "test_user"
age: int = 25
class Item(BaseModel):
"""Item model for testing ForwardRef."""
title: str = "test_item"
description: Optional[str] = None
def test_annotated_forwardref_query_param():
"""Test Annotated with ForwardRef in query parameter."""
app = FastAPI()
@app.get("/users")
async def get_user(user: Annotated["UserModel", Query()]):
return {"user": user}
client = TestClient(app)
response = client.get("/users?name=John&age=30")
assert response.status_code == 200
assert response.json() == {"user": {"name": "John", "age": 30}}
def test_annotated_forwardref_query_param_default():
"""Test Annotated with ForwardRef in query parameter with default."""
app = FastAPI()
@app.get("/users-default")
async def get_user_default(user: Annotated["UserModel", Query()] = UserModel()):
return {"user": user}
client = TestClient(app)
response = client.get("/users-default")
assert response.status_code == 200
assert response.json() == {"user": {"name": "test_user", "age": 25}}
def test_annotated_forwardref_optional():
"""Test Annotated with ForwardRef in Optional parameter."""
app = FastAPI()
@app.get("/items")
async def get_item(item: Annotated[Optional["Item"], Query()] = None):
return {"item": item}
client = TestClient(app)
# Test with no parameter
response = client.get("/items")
assert response.status_code == 200
assert response.json() == {"item": None}
def test_annotated_forwardref_path_param():
"""Test Annotated with ForwardRef in path parameter."""
app = FastAPI()
@app.get("/users/{user_id}")
async def get_user_by_id(user_id: Annotated["int", Path()]):
return {"user_id": user_id}
client = TestClient(app)
response = client.get("/users/123")
assert response.status_code == 200
assert response.json() == {"user_id": 123}
def test_annotated_forwardref_header():
"""Test Annotated with ForwardRef in header parameter."""
app = FastAPI()
@app.get("/headers")
async def get_headers(x_token: Annotated["str", Header()]):
return {"x_token": x_token}
client = TestClient(app)
response = client.get("/headers", headers={"x-token": "test-token"})
assert response.status_code == 200
assert response.json() == {"x_token": "test-token"}
def test_annotated_forwardref_body():
"""Test Annotated with ForwardRef in request body."""
app = FastAPI()
@app.post("/items")
async def create_item(item: Annotated["Item", Body()]):
return {"item": item}
client = TestClient(app)
response = client.post(
"/items", json={"title": "Test Item", "description": "A test item"}
)
assert response.status_code == 200
assert response.json() == {
"item": {"title": "Test Item", "description": "A test item"}
}
def test_annotated_forwardref_multiple_annotations():
"""Test Annotated with ForwardRef and multiple metadata items."""
app = FastAPI()
@app.get("/multi")
async def get_multi(
param: Annotated["UserModel", "some metadata", Query(), "more metadata"],
):
return {"param": param}
client = TestClient(app)
response = client.get("/multi?name=John&age=30")
assert response.status_code == 200
assert response.json() == {"param": {"name": "John", "age": 30}}
def test_annotated_forwardref_openapi_schema():
"""Test that OpenAPI schema is generated correctly with ForwardRef."""
app = FastAPI()
@app.get("/users")
async def get_user(user: Annotated["UserModel", Query()]):
return {"user": user}
client = TestClient(app)
response = client.get("/openapi.json")
assert response.status_code == 200
openapi_schema = response.json()
# Check that the endpoint is in the schema
assert "/users" in openapi_schema["paths"]
# Check that the parameter schema is generated
endpoint_schema = openapi_schema["paths"]["/users"]["get"]
assert "parameters" in endpoint_schema
# Check that the UserModel schema is in the components
assert "components" in openapi_schema
assert "schemas" in openapi_schema["components"]
def test_annotated_forwardref_get_type_hints():
"""Test that get_type_hints works correctly with Annotated ForwardRef."""
from typing import get_type_hints
def test_func(param: Annotated["UserModel", Query()]):
pass
# This should work without errors (it will resolve UserModel since it's defined at module level)
hints = get_type_hints(test_func)
assert "param" in hints
# get_type_hints resolves the ForwardRef to the actual type
assert hints["param"] == UserModel
def test_annotated_forwardref_with_dependencies():
"""Test Annotated with ForwardRef in dependency chain."""
app = FastAPI()
def get_current_user(user: Annotated["UserModel", Query()]) -> "UserModel":
return user
@app.get("/protected")
async def get_protected(current_user: Annotated[UserModel, Query()]):
return {"user": current_user}
client = TestClient(app)
response = client.get("/protected?name=Alice&age=28")
assert response.status_code == 200
assert response.json() == {"user": {"name": "Alice", "age": 28}}
def test_annotated_forwardref_complex_types():
"""Test Annotated with ForwardRef for complex type annotations."""
app = FastAPI()
@app.get("/search")
async def search(items: Annotated[list["Item"], Query()] = None):
return {"items": items or []}
client = TestClient(app)
# This tests that complex types with ForwardRef don't break the system
response = client.get("/search")
assert response.status_code in [
200,
422,
] # May be 422 if the type check is too strict
if __name__ == "__main__":
pytest.main([__file__, "-v"])