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.

314 lines
9.5 KiB

from http import HTTPStatus
from typing import Annotated, Any
import pytest
from fastapi import (
Body,
Depends,
FastAPI,
File,
Form,
Header,
Query,
Security,
status,
)
from fastapi.exceptions import FastAPIError
from fastapi.security import HTTPBearer
from fastapi.testclient import TestClient
from tests._annotated_body_depends_merge_common import (
BarPayload,
BasePayload,
FooPayload,
openapi_request_body_schema_ref,
)
_MERGED_BODY_DEFAULT = FooPayload(kind="foo", extra_foo="default")
class TestAnnotatedBodyDependsMergeBody:
@pytest.mark.parametrize(
(
"path",
"ann1",
"ann2",
"model_cls",
"expected_ref_suffix",
"assert_no_query_params",
"payload",
),
[
pytest.param(
"/a",
Body(),
Depends(FooPayload),
FooPayload,
"FooPayload",
True,
{"kind": "foo", "extra_foo": "hit"},
),
pytest.param(
"/b",
Depends(BarPayload),
Body(),
BarPayload,
"BarPayload",
False,
{"kind": "bar", "extra_bar": "hit"},
),
],
)
def test_openapi_json_body_depends_merge(
self,
path: str,
ann1: Any,
ann2: Any,
model_cls: type[BasePayload],
expected_ref_suffix: str,
assert_no_query_params: bool,
payload: dict[str, str],
) -> None:
app = FastAPI()
@app.post(path)
def route(
data: Annotated[BasePayload, ann1, ann2],
) -> None:
assert isinstance(data, model_cls)
client = TestClient(app)
schema = client.get("/openapi.json").json()
post = schema["paths"][path]["post"]
if assert_no_query_params:
assert post.get("parameters") in (None, [])
ref = openapi_request_body_schema_ref(
schema, path=path, method="post", content_type="application/json"
)
assert ref.endswith(f"/{expected_ref_suffix}")
ok = client.post(path, json=payload)
assert ok.status_code == status.HTTP_200_OK
def test_runtime_json_validates_concrete_model(self) -> None:
app = FastAPI()
@app.post("/c")
def route(
data: Annotated[BasePayload, Body(), Depends(FooPayload)],
) -> dict[str, str]:
return {"extra": data.extra_foo}
client = TestClient(app)
extra = "ricksanchez"
payload = {"kind": "foo", "extra_foo": extra}
expected_json = {"extra": extra}
r = client.post("/c", json=payload)
assert r.status_code == status.HTTP_200_OK
assert r.json() == expected_json
bad = client.post("/c", json={"kind": "foo"})
# not status.*: Starlette confused HTTP_422_UNPROCESSABLE_CONTENT HTTP_422_UNPROCESSABLE_ENTITY
assert bad.status_code == HTTPStatus.UNPROCESSABLE_ENTITY
def test_merge_depends_empty_falls_back_to_declared_model(self) -> None:
app = FastAPI()
@app.post("/depends-empty")
def route(
data: Annotated[FooPayload, Body(), Depends()],
) -> dict[str, str]:
return {"extra": data.extra_foo}
client = TestClient(app)
extra = "foobar"
payload = {"kind": "foo", "extra_foo": extra}
expected_json = {"extra": extra}
r = client.post("/depends-empty", json=payload)
assert r.status_code == status.HTTP_200_OK
assert r.json() == expected_json
def test_merged_body_parameter_signature_default(self) -> None:
app = FastAPI()
@app.post("/merged-default")
def route(
data: Annotated[
BasePayload, Body(), Depends(FooPayload)
] = _MERGED_BODY_DEFAULT,
) -> dict[str, str]:
return {"extra": data.extra_foo}
client = TestClient(app)
empty = client.post("/merged-default")
assert empty.status_code == status.HTTP_200_OK
expected_default = {"extra": "default"}
assert empty.json() == expected_default
override_extra = "hello world"
override_payload = {"kind": "foo", "extra_foo": override_extra}
expected_override = {"extra": override_extra}
overridden = client.post("/merged-default", json=override_payload)
assert overridden.status_code == status.HTTP_200_OK
assert overridden.json() == expected_override
def test_put_patch_json_body_depends_openapi(self) -> None:
app = FastAPI()
path = "/items/{item_id}"
@app.put(path)
def route_put(
item_id: str,
data: Annotated[BasePayload, Body(), Depends(FooPayload)],
) -> None:
assert isinstance(data, FooPayload)
@app.patch(path)
def route_patch(
item_id: str,
data: Annotated[BasePayload, Depends(FooPayload), Body()],
) -> None:
assert isinstance(data, FooPayload)
client = TestClient(app)
schema = client.get("/openapi.json").json()
for method in ("put", "patch"):
ref = openapi_request_body_schema_ref(
schema, path=path, method=method, content_type="application/json"
)
assert ref.endswith("/FooPayload")
put_extra = "fizz"
put_payload = {"kind": "foo", "extra_foo": put_extra}
r = client.put("/items/1", json=put_payload)
assert r.status_code == status.HTTP_200_OK
patch_extra = "buzz"
patch_payload = {"kind": "foo", "extra_foo": patch_extra}
r2 = client.patch("/items/1", json=patch_payload)
assert r2.status_code == status.HTTP_200_OK
def test_rejects_body_with_callable_depends(self) -> None:
app = FastAPI()
def not_a_model() -> None:
pass # pragma: no cover
with pytest.raises(FastAPIError, match="Pydantic model class"):
@app.post("/d")
def route_d(
data: Annotated[BasePayload, Body(), Depends(not_a_model)],
) -> None:
pass # pragma: no cover
def test_rejects_multiple_depends_with_body(self) -> None:
app = FastAPI()
with pytest.raises(FastAPIError, match="multiple `Depends`"):
@app.post("/e")
def route_e(
data: Annotated[
BasePayload,
Body(),
Depends(FooPayload),
Depends(BarPayload),
],
) -> None:
pass # pragma: no cover
def test_rejects_body_and_form_together(self) -> None:
app = FastAPI()
with pytest.raises(FastAPIError, match="multiple `Body`"):
@app.post("/conflict")
def route_conflict(
data: Annotated[
BasePayload,
Body(),
Form(),
Depends(FooPayload),
],
) -> None:
pass # pragma: no cover
@pytest.mark.parametrize(
"shape_marker",
[
pytest.param(Body(), id="body"),
pytest.param(Form(), id="form"),
pytest.param(File(), id="file"),
],
)
def test_rejects_query_with_body_like_in_same_annotated(
self, shape_marker: Any
) -> None:
app = FastAPI()
with pytest.raises(FastAPIError, match="Cannot combine `Query`"):
@app.post("/query-shape")
def route_bad(
data: Annotated[
BasePayload,
shape_marker,
Query(),
Depends(FooPayload),
],
) -> None:
pass # pragma: no cover
def test_rejects_multiple_query_with_depends(self) -> None:
app = FastAPI()
with pytest.raises(FastAPIError, match="multiple `Query`"):
@app.post("/multi-query")
def route_multi_query(
data: Annotated[
BasePayload,
Query(),
Query(),
Depends(FooPayload),
],
) -> None:
pass # pragma: no cover
def test_rejects_body_like_with_other_param_in_same_annotated(self) -> None:
app = FastAPI()
with pytest.raises(FastAPIError, match="other parameter types"):
@app.post("/body-plus-header")
def route_body_header(
data: Annotated[
BasePayload,
Body(),
Header(),
Depends(FooPayload),
],
) -> None:
pass # pragma: no cover
def test_rejects_security_with_body_like_merge(self) -> None:
app = FastAPI()
bearer = HTTPBearer(auto_error=False)
with pytest.raises(FastAPIError, match="`Security`"):
@app.post("/body-security")
def route_body_security(
data: Annotated[BasePayload, Body(), Security(bearer)],
) -> None:
pass # pragma: no cover
def test_rejects_merge_on_path_parameter(self) -> None:
app = FastAPI()
with pytest.raises(FastAPIError, match="path parameter"):
@app.post("/path/{data}")
def route_path(
data: Annotated[BasePayload, Body(), Depends(FooPayload)],
) -> None: ...