pythonasyncioapiasyncfastapiframeworkjsonjson-schemaopenapiopenapi3pydanticpython-typespython3redocreststarletteswaggerswagger-uiuvicornweb
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.
100 lines
2.9 KiB
100 lines
2.9 KiB
from io import BytesIO
|
|
from typing import Annotated, Any
|
|
|
|
import pytest
|
|
from fastapi import Depends, FastAPI, File, Form, status
|
|
from fastapi.exceptions import FastAPIError
|
|
from fastapi.testclient import TestClient
|
|
|
|
from tests._annotated_body_depends_merge_common import (
|
|
BarFilePayload,
|
|
BasePayload,
|
|
FooFilePayload,
|
|
FooPayload,
|
|
)
|
|
|
|
|
|
class TestAnnotatedBodyDependsMergeFile:
|
|
@pytest.mark.parametrize(
|
|
("path", "ann1", "ann2", "model_cls", "expected_ref_suffix"),
|
|
[
|
|
(
|
|
"/file-a",
|
|
File(),
|
|
Depends(FooFilePayload),
|
|
FooFilePayload,
|
|
"FooFilePayload",
|
|
),
|
|
(
|
|
"/file-b",
|
|
Depends(BarFilePayload),
|
|
File(),
|
|
BarFilePayload,
|
|
"BarFilePayload",
|
|
),
|
|
],
|
|
)
|
|
def test_openapi_file_depends_merge(
|
|
self,
|
|
path: str,
|
|
ann1: Any,
|
|
ann2: Any,
|
|
model_cls: type[BasePayload],
|
|
expected_ref_suffix: str,
|
|
) -> None:
|
|
app = FastAPI()
|
|
|
|
@app.post(path)
|
|
def route_file(
|
|
data: Annotated[BasePayload, ann1, ann2],
|
|
) -> None:
|
|
assert isinstance(data, model_cls)
|
|
|
|
client = TestClient(app)
|
|
schema = client.get("/openapi.json").json()
|
|
rb = schema["paths"][path]["post"]["requestBody"]
|
|
content = rb["content"]
|
|
assert "multipart/form-data" in content
|
|
ref = content["multipart/form-data"]["schema"]["$ref"]
|
|
assert ref.endswith(f"/{expected_ref_suffix}")
|
|
|
|
def test_runtime_file_validates_concrete_model(self) -> None:
|
|
app = FastAPI()
|
|
|
|
@app.post("/file-c")
|
|
def route_file(
|
|
data: Annotated[BasePayload, File(), Depends(FooFilePayload)],
|
|
) -> dict[str, str]:
|
|
return {"extra": data.extra_foo, "fn": data.blob.filename or ""}
|
|
|
|
client = TestClient(app)
|
|
r = client.post(
|
|
"/file-c",
|
|
data={"kind": "foo", "extra_foo": "u"},
|
|
files={"blob": ("up.txt", BytesIO(b"xyz"), "text/plain")},
|
|
)
|
|
assert r.status_code == status.HTTP_200_OK
|
|
assert r.json() == {"extra": "u", "fn": "up.txt"}
|
|
|
|
bad = client.post(
|
|
"/file-c",
|
|
data={"kind": "foo"},
|
|
files={"blob": ("up.txt", BytesIO(b"x"), "text/plain")},
|
|
)
|
|
assert bad.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT
|
|
|
|
def test_rejects_file_and_form_together(self) -> None:
|
|
app = FastAPI()
|
|
|
|
with pytest.raises(FastAPIError, match="multiple `Body`"):
|
|
|
|
@app.post("/file-conflict")
|
|
def route_conflict(
|
|
data: Annotated[
|
|
BasePayload,
|
|
File(),
|
|
Form(),
|
|
Depends(FooPayload),
|
|
],
|
|
) -> None:
|
|
pass # pragma: no cover
|
|
|