1 changed files with 200 additions and 0 deletions
@ -0,0 +1,200 @@ |
|||||
|
from typing import Annotated |
||||
|
|
||||
|
from fastapi import Cookie, FastAPI, Header, Query |
||||
|
from fastapi.testclient import TestClient |
||||
|
from pydantic import BaseModel |
||||
|
|
||||
|
app = FastAPI() |
||||
|
|
||||
|
|
||||
|
class QueryModel(BaseModel): |
||||
|
limit: int |
||||
|
q: str |
||||
|
|
||||
|
|
||||
|
class HeaderModel(BaseModel): |
||||
|
x_token: str |
||||
|
x_trace: list[str] = [] |
||||
|
|
||||
|
|
||||
|
class CookieModel(BaseModel): |
||||
|
session_id: str |
||||
|
tracker: str |
||||
|
|
||||
|
|
||||
|
@app.get("/query/only") |
||||
|
def read_query_only(m: Annotated[QueryModel, Query()]): |
||||
|
return {"m": m.model_dump()} |
||||
|
|
||||
|
|
||||
|
@app.get("/query/mixed") |
||||
|
def read_query_mixed( |
||||
|
m: Annotated[QueryModel, Query()], |
||||
|
extra: Annotated[int, Query()], |
||||
|
): |
||||
|
return {"m": m.model_dump(), "extra": extra} |
||||
|
|
||||
|
|
||||
|
@app.get("/header/only") |
||||
|
def read_header_only(h: Annotated[HeaderModel, Header()]): |
||||
|
return {"h": h.model_dump()} |
||||
|
|
||||
|
|
||||
|
@app.get("/header/mixed") |
||||
|
def read_header_mixed( |
||||
|
h: Annotated[HeaderModel, Header()], |
||||
|
extra: Annotated[str, Header()], |
||||
|
): |
||||
|
return {"h": h.model_dump(), "extra": extra} |
||||
|
|
||||
|
|
||||
|
@app.get("/header/mixed-no-convert") |
||||
|
def read_header_mixed_no_convert( |
||||
|
h: Annotated[HeaderModel, Header(convert_underscores=False)], |
||||
|
extra: Annotated[str, Header()], |
||||
|
): |
||||
|
return {"h": h.model_dump(), "extra": extra} |
||||
|
|
||||
|
|
||||
|
@app.get("/cookie/only") |
||||
|
def read_cookie_only(c: Annotated[CookieModel, Cookie()]): |
||||
|
return {"c": c.model_dump()} |
||||
|
|
||||
|
|
||||
|
@app.get("/cookie/mixed") |
||||
|
def read_cookie_mixed( |
||||
|
c: Annotated[CookieModel, Cookie()], |
||||
|
extra: Annotated[str, Cookie()], |
||||
|
): |
||||
|
return {"c": c.model_dump(), "extra": extra} |
||||
|
|
||||
|
|
||||
|
client = TestClient(app) |
||||
|
|
||||
|
|
||||
|
def get_parameter_names(path: str) -> list[str]: |
||||
|
return [ |
||||
|
param["name"] for param in app.openapi()["paths"][path]["get"]["parameters"] |
||||
|
] |
||||
|
|
||||
|
|
||||
|
def test_query_model_only_still_flattens(): |
||||
|
response = client.get("/query/only", params={"limit": 10, "q": "hello"}) |
||||
|
assert response.status_code == 200 |
||||
|
assert response.json() == {"m": {"limit": 10, "q": "hello"}} |
||||
|
|
||||
|
|
||||
|
def test_query_model_with_sibling_param(): |
||||
|
response = client.get( |
||||
|
"/query/mixed", params={"limit": 10, "q": "hello", "extra": 5} |
||||
|
) |
||||
|
assert response.status_code == 200 |
||||
|
assert response.json() == {"m": {"limit": 10, "q": "hello"}, "extra": 5} |
||||
|
|
||||
|
|
||||
|
def test_query_model_with_sibling_param_missing_fields(): |
||||
|
response = client.get("/query/mixed", params={"extra": 5}) |
||||
|
assert response.status_code == 422 |
||||
|
assert [error["loc"] for error in response.json()["detail"]] == [ |
||||
|
["query", "limit"], |
||||
|
["query", "q"], |
||||
|
] |
||||
|
|
||||
|
|
||||
|
def test_query_model_with_sibling_param_openapi(): |
||||
|
parameter_names = get_parameter_names("/query/mixed") |
||||
|
assert parameter_names == ["limit", "q", "extra"] |
||||
|
assert "m" not in parameter_names |
||||
|
|
||||
|
|
||||
|
def test_header_model_only_still_flattens(): |
||||
|
response = client.get( |
||||
|
"/header/only", |
||||
|
headers=[("x-token", "abc"), ("x-trace", "one"), ("x-trace", "two")], |
||||
|
) |
||||
|
assert response.status_code == 200 |
||||
|
assert response.json() == {"h": {"x_token": "abc", "x_trace": ["one", "two"]}} |
||||
|
|
||||
|
|
||||
|
def test_header_model_with_sibling_param(): |
||||
|
response = client.get( |
||||
|
"/header/mixed", |
||||
|
headers=[ |
||||
|
("x-token", "abc"), |
||||
|
("x-trace", "one"), |
||||
|
("x-trace", "two"), |
||||
|
("extra", "value"), |
||||
|
], |
||||
|
) |
||||
|
assert response.status_code == 200 |
||||
|
assert response.json() == { |
||||
|
"h": {"x_token": "abc", "x_trace": ["one", "two"]}, |
||||
|
"extra": "value", |
||||
|
} |
||||
|
|
||||
|
|
||||
|
def test_header_model_with_sibling_param_openapi(): |
||||
|
parameter_names = get_parameter_names("/header/mixed") |
||||
|
assert parameter_names == ["x-token", "x-trace", "extra"] |
||||
|
assert "h" not in parameter_names |
||||
|
|
||||
|
|
||||
|
def test_header_model_with_sibling_param_convert_underscores_false(): |
||||
|
response = client.get( |
||||
|
"/header/mixed-no-convert", |
||||
|
headers=[ |
||||
|
("x_token", "abc"), |
||||
|
("x_trace", "one"), |
||||
|
("x_trace", "two"), |
||||
|
("extra", "value"), |
||||
|
], |
||||
|
) |
||||
|
assert response.status_code == 200 |
||||
|
assert response.json() == { |
||||
|
"h": {"x_token": "abc", "x_trace": ["one", "two"]}, |
||||
|
"extra": "value", |
||||
|
} |
||||
|
parameter_names = get_parameter_names("/header/mixed-no-convert") |
||||
|
assert parameter_names == ["x_token", "x_trace", "extra"] |
||||
|
|
||||
|
|
||||
|
def test_cookie_model_only_still_flattens(): |
||||
|
with client as c: |
||||
|
c.cookies.clear() |
||||
|
c.cookies.set("session_id", "123") |
||||
|
c.cookies.set("tracker", "abc") |
||||
|
response = c.get("/cookie/only") |
||||
|
assert response.status_code == 200 |
||||
|
assert response.json() == {"c": {"session_id": "123", "tracker": "abc"}} |
||||
|
|
||||
|
|
||||
|
def test_cookie_model_with_sibling_param(): |
||||
|
with client as c: |
||||
|
c.cookies.clear() |
||||
|
c.cookies.set("session_id", "123") |
||||
|
c.cookies.set("tracker", "abc") |
||||
|
c.cookies.set("extra", "hello") |
||||
|
response = c.get("/cookie/mixed") |
||||
|
assert response.status_code == 200 |
||||
|
assert response.json() == { |
||||
|
"c": {"session_id": "123", "tracker": "abc"}, |
||||
|
"extra": "hello", |
||||
|
} |
||||
|
|
||||
|
|
||||
|
def test_cookie_model_with_sibling_param_missing_fields(): |
||||
|
with client as c: |
||||
|
c.cookies.clear() |
||||
|
c.cookies.set("extra", "hello") |
||||
|
response = c.get("/cookie/mixed") |
||||
|
assert response.status_code == 422 |
||||
|
assert [error["loc"] for error in response.json()["detail"]] == [ |
||||
|
["cookie", "session_id"], |
||||
|
["cookie", "tracker"], |
||||
|
] |
||||
|
|
||||
|
|
||||
|
def test_cookie_model_with_sibling_param_openapi(): |
||||
|
parameter_names = get_parameter_names("/cookie/mixed") |
||||
|
assert parameter_names == ["session_id", "tracker", "extra"] |
||||
|
assert "c" not in parameter_names |
||||
Loading…
Reference in new issue