Files
fastapi/tests/test_request_params/test_default_validation.py
T

249 lines
7.4 KiB
Python

from typing import Annotated
from unittest.mock import Mock
import pytest
from fastapi import Body, Cookie, FastAPI, Form, Header, Query
from fastapi.testclient import TestClient
from pydantic import BaseModel, BeforeValidator, Field
def double(value):
return 0 if value is None else int(value) * 2
ValidatedInt = Annotated[int | None, BeforeValidator(double)]
@pytest.mark.parametrize("parameter", [Query, Header, Cookie, Form, Body])
@pytest.mark.parametrize("path", ["/parameter", "/model"])
def test_missing_value_uses_default_without_validation(parameter, path):
app = FastAPI()
field = Body(embed=True) if parameter is Body else parameter()
@app.post("/parameter")
def read_parameter(value: Annotated[ValidatedInt, field] = 10):
return value
class Model(BaseModel):
value: ValidatedInt = 10
@app.post("/model")
def read_model(value: Annotated[Model, parameter()]):
return value.value
client = TestClient(app)
if parameter is Body:
response = client.post(path, json={})
else:
response = client.post(path)
assert response.status_code == 200
assert response.json() == 10
@pytest.mark.parametrize("parameter", [Query, Header, Cookie, Form, Body])
@pytest.mark.parametrize("path", ["/parameter", "/model"])
def test_missing_value_uses_none_default_without_validation(parameter, path):
app = FastAPI()
field = Body(embed=True) if parameter is Body else parameter()
@app.post("/parameter")
def read_parameter(value: Annotated[ValidatedInt, field] = None):
return value
class Model(BaseModel):
value: ValidatedInt = None
@app.post("/model")
def read_model(value: Annotated[Model, parameter()]):
return value.value
client = TestClient(app)
if parameter is Body:
response = client.post(path, json={})
else:
response = client.post(path)
assert response.status_code == 200
assert response.json() is None
@pytest.mark.parametrize("parameter", [Query, Header, Cookie, Form, Body])
@pytest.mark.parametrize("path", ["/parameter", "/model"])
def test_explicit_value_equal_to_default_is_validated(parameter, path):
app = FastAPI()
field = Body(embed=True) if parameter is Body else parameter()
@app.post("/parameter")
def read_parameter(value: Annotated[ValidatedInt, field] = 10):
return value
class Model(BaseModel):
value: ValidatedInt = 10
@app.post("/model")
def read_model(value: Annotated[Model, parameter()]):
return value.value
client = TestClient(app)
if parameter is Query:
response = client.post(path, params={"value": "10"})
elif parameter is Header:
response = client.post(path, headers={"value": "10"})
elif parameter is Cookie:
response = client.post(path, headers={"cookie": "value=10"})
elif parameter is Form:
response = client.post(path, data={"value": "10"})
else:
response = client.post(path, json={"value": 10})
assert response.status_code == 200
assert response.json() == 20
@pytest.mark.parametrize("default", [None, 10])
@pytest.mark.parametrize(
"path",
[
pytest.param(
"/parameter",
marks=pytest.mark.xfail(
reason="Explicit null is treated as missing for individual Body parameters",
raises=AssertionError,
),
),
"/model",
],
)
def test_explicit_null_is_validated(default, path):
app = FastAPI()
field = Body(embed=True)
@app.post("/parameter")
def read_parameter(value: Annotated[ValidatedInt, field] = default):
return value
class Model(BaseModel):
value: ValidatedInt = default
@app.post("/model")
def read_model(value: Annotated[Model, Body()]):
return value.value
client = TestClient(app)
response = client.post(path, json={"value": None})
assert response.status_code == 200
assert response.json() == 0
@pytest.mark.parametrize("default", [None, 10])
@pytest.mark.xfail(
reason="Explicit null-body is treated as missing", raises=AssertionError
)
def test_explicit_null_body_is_validated(default):
app = FastAPI()
@app.post("/")
def read_body(value: Annotated[ValidatedInt, Body()] = default):
return value
client = TestClient(app)
response = client.post(
"/", content="null", headers={"content-type": "application/json"}
)
assert response.status_code == 200
assert response.json() == 0
@pytest.mark.parametrize("parameter", [Query, Header, Cookie, Form, Body])
@pytest.mark.parametrize("default,expected", [(None, 0), (10, 20)])
def test_model_can_validate_default(parameter, default, expected):
app = FastAPI()
class Model(BaseModel):
value: Annotated[ValidatedInt, Field(validate_default=True)] = default
@app.post("/")
def read_model(value: Annotated[Model, parameter()]):
return value.value
client = TestClient(app)
if parameter is Body:
response = client.post("/", json={})
else:
response = client.post("/")
assert response.status_code == 200
assert response.json() == expected
@pytest.mark.parametrize("parameter", [Query, Header, Cookie, Form, Body])
@pytest.mark.parametrize("default,expected", [(None, 0), (10, 20)])
def test_parameter_can_validate_default(parameter, default, expected):
app = FastAPI()
field = (
Body(embed=True, validate_default=True)
if parameter is Body
else parameter(validate_default=True)
)
@app.post("/")
def read_parameter(value: Annotated[ValidatedInt, field] = default):
return value
client = TestClient(app)
if parameter is Body:
response = client.post("/", json={})
else:
response = client.post("/")
assert response.status_code == 200
assert response.json() == expected
@pytest.mark.parametrize("parameter", [Query, Header, Cookie, Form, Body])
@pytest.mark.parametrize("default,expected", [(None, 0), (10, 20)])
def test_parameter_validates_default_factory_once(parameter, default, expected):
app = FastAPI()
default_factory = Mock(return_value=default)
field = (
Body(embed=True, default_factory=default_factory, validate_default=True)
if parameter is Body
else parameter(default_factory=default_factory, validate_default=True)
)
@app.post("/")
def read_parameter(value: Annotated[ValidatedInt, field]):
return value
default_factory.assert_not_called()
client = TestClient(app)
if parameter is Body:
response = client.post("/", json={})
else:
response = client.post("/")
assert response.status_code == 200
assert response.json() == expected
default_factory.assert_called_once_with()
def test_parameter_rejects_invalid_default():
app = FastAPI()
@app.get("/")
def read_parameter(value: Annotated[int, Query(gt=0, validate_default=True)] = -1):
return value
client = TestClient(app)
response = client.get("/")
assert response.status_code == 422
assert response.json() == {
"detail": [
{
"type": "greater_than",
"loc": ["query", "value"],
"msg": "Input should be greater than 0",
"input": -1,
"ctx": {"gt": 0},
}
]
}
response = client.get("/", params={"value": "1"})
assert response.status_code == 200
assert response.json() == 1