2 changed files with 288 additions and 10 deletions
@ -0,0 +1,280 @@ |
|||||
|
from typing import Annotated |
||||
|
|
||||
|
import pytest |
||||
|
from dirty_equals import IsOneOf |
||||
|
from fastapi import FastAPI, Query |
||||
|
from fastapi.testclient import TestClient |
||||
|
from pydantic import BaseModel |
||||
|
|
||||
|
app = FastAPI() |
||||
|
|
||||
|
# ===================================================================================== |
||||
|
# Without aliases which exercise the "Wildcard" capture behavior |
||||
|
|
||||
|
|
||||
|
@app.get("/required-dict-str") |
||||
|
async def read_required_dict_str(p: Annotated[dict[str, str], Query()]): |
||||
|
return {"p": p} |
||||
|
|
||||
|
|
||||
|
class QueryModelRequiredDictStr(BaseModel): |
||||
|
p: dict[str, str] |
||||
|
|
||||
|
|
||||
|
@app.get("/model-required-dict-str") |
||||
|
def read_model_required_dict_str(p: Annotated[QueryModelRequiredDictStr, Query()]): |
||||
|
return {"p": p.p} |
||||
|
|
||||
|
|
||||
|
@pytest.mark.parametrize( |
||||
|
"path", |
||||
|
["/required-dict-str", "/model-required-dict-str"], |
||||
|
) |
||||
|
def test_required_dict_str_schema(path: str): |
||||
|
assert app.openapi()["paths"][path]["get"]["parameters"] == [ |
||||
|
{ |
||||
|
"required": True, |
||||
|
"schema": { |
||||
|
"title": "P", |
||||
|
"type": "object", |
||||
|
"additionalProperties": {"type": "string"}, |
||||
|
}, |
||||
|
"name": "p", |
||||
|
"in": "query", |
||||
|
} |
||||
|
] |
||||
|
|
||||
|
|
||||
|
@pytest.mark.parametrize( |
||||
|
"path", |
||||
|
["/required-dict-str", "/model-required-dict-str"], |
||||
|
) |
||||
|
def test_required_dict_str_missing(path: str): |
||||
|
client = TestClient(app) |
||||
|
response = client.get(path) |
||||
|
assert response.status_code == 422 |
||||
|
assert response.json() == { |
||||
|
"detail": [ |
||||
|
{ |
||||
|
"type": "missing", |
||||
|
"loc": ["query", "p"], |
||||
|
"msg": "Field required", |
||||
|
"input": IsOneOf(None, {}), |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
|
||||
|
|
||||
|
@pytest.mark.parametrize( |
||||
|
"path", |
||||
|
["/required-dict-str", "/model-required-dict-str"], |
||||
|
) |
||||
|
def test_required_dict_str(path: str): |
||||
|
client = TestClient(app) |
||||
|
response = client.get(f"{path}?foo=bar&baz=qux") |
||||
|
assert response.status_code == 200 |
||||
|
assert response.json() == {"p": {"foo": "bar", "baz": "qux"}} |
||||
|
|
||||
|
# ===================================================================================== |
||||
|
# With union types |
||||
|
|
||||
|
@app.get("/required-dict-union") |
||||
|
async def read_required_dict_union(p: Annotated[dict[str, str] | dict[str, int], Query()]): |
||||
|
return {"p": p} |
||||
|
|
||||
|
|
||||
|
class QueryModelRequiredDictUnion(BaseModel): |
||||
|
p: dict[str, str] | dict[str, int] |
||||
|
|
||||
|
|
||||
|
@app.get("/model-required-dict-union") |
||||
|
def read_model_required_dict_union(p: Annotated[QueryModelRequiredDictUnion, Query()]): |
||||
|
return {"p": p.p} |
||||
|
|
||||
|
@pytest.mark.parametrize( |
||||
|
"path", |
||||
|
["/required-dict-union", "/model-required-dict-union"], |
||||
|
) |
||||
|
def test_required_dict_union_schema(path: str): |
||||
|
assert app.openapi()["paths"][path]["get"]["parameters"] == [ |
||||
|
{ |
||||
|
"required": True, |
||||
|
"schema": { |
||||
|
"title": "P", |
||||
|
"anyOf": [ |
||||
|
{ |
||||
|
"type": "object", |
||||
|
"additionalProperties": {"type": "string"}, |
||||
|
}, |
||||
|
{ |
||||
|
"type": "object", |
||||
|
"additionalProperties": {"type": "integer"}, |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
"name": "p", |
||||
|
"in": "query", |
||||
|
} |
||||
|
] |
||||
|
|
||||
|
@pytest.mark.parametrize( |
||||
|
"path", |
||||
|
["/required-dict-union", "/model-required-dict-union"], |
||||
|
) |
||||
|
def test_required_dict_union_missing(path: str): |
||||
|
client = TestClient(app) |
||||
|
response = client.get(path) |
||||
|
assert response.status_code == 422 |
||||
|
assert response.json() == { |
||||
|
"detail": [ |
||||
|
{ |
||||
|
"type": "missing", |
||||
|
"loc": ["query", "p"], |
||||
|
"msg": "Field required", |
||||
|
"input": IsOneOf(None, {}), |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
|
||||
|
@pytest.mark.parametrize( |
||||
|
"path", |
||||
|
["/required-dict-union", "/model-required-dict-union"], |
||||
|
) |
||||
|
def test_required_dict_union(path: str): |
||||
|
client = TestClient(app) |
||||
|
response = client.get(f"{path}?foo=bar&baz=42") |
||||
|
assert response.status_code == 200 |
||||
|
assert response.json() == {"p": {"foo": "bar", "baz": "42"}} |
||||
|
|
||||
|
@app.get("/required-dict-of-union") |
||||
|
async def read_required_dict_of_union(p: Annotated[dict[str, int | bool], Query()]): |
||||
|
return {"p": p} |
||||
|
|
||||
|
|
||||
|
class QueryModelRequiredDictOfUnion(BaseModel): |
||||
|
p: dict[str, int | bool] |
||||
|
|
||||
|
|
||||
|
@app.get("/model-required-dict-of-union") |
||||
|
def read_model_required_dict_of_union( |
||||
|
p: Annotated[QueryModelRequiredDictOfUnion, Query()], |
||||
|
): |
||||
|
return {"p": p.p} |
||||
|
|
||||
|
@pytest.mark.parametrize( |
||||
|
"path", |
||||
|
["/required-dict-of-union", "/model-required-dict-of-union"], |
||||
|
) |
||||
|
def test_required_dict_of_union_schema(path: str): |
||||
|
assert app.openapi()["paths"][path]["get"]["parameters"] == [ |
||||
|
{ |
||||
|
"required": True, |
||||
|
"schema": { |
||||
|
"title": "P", |
||||
|
"type": "object", |
||||
|
"additionalProperties": { |
||||
|
"anyOf": [ |
||||
|
{"type": "integer"}, |
||||
|
{"type": "boolean"}, |
||||
|
] |
||||
|
}, |
||||
|
}, |
||||
|
"name": "p", |
||||
|
"in": "query", |
||||
|
} |
||||
|
] |
||||
|
|
||||
|
@pytest.mark.parametrize( |
||||
|
"path", |
||||
|
["/required-dict-of-union", "/model-required-dict-of-union"], |
||||
|
) |
||||
|
def test_required_dict_of_union_missing(path: str): |
||||
|
client = TestClient(app) |
||||
|
response = client.get(path) |
||||
|
assert response.status_code == 422 |
||||
|
assert response.json() == { |
||||
|
"detail": [ |
||||
|
{ |
||||
|
"type": "missing", |
||||
|
"loc": ["query", "p"], |
||||
|
"msg": "Field required", |
||||
|
"input": IsOneOf(None, {}), |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
|
||||
|
|
||||
|
@pytest.mark.parametrize( |
||||
|
"path", |
||||
|
["/required-dict-of-union", "/model-required-dict-of-union"], |
||||
|
) |
||||
|
def test_required_dict_of_union(path: str): |
||||
|
client = TestClient(app) |
||||
|
# Testing the "Wildcard" capture behavior for dicts |
||||
|
response = client.get(f"{path}?foo=True&baz=42") |
||||
|
assert response.status_code == 200 |
||||
|
assert response.json() == {"p": {"foo": True, "baz": 42}} |
||||
|
|
||||
|
@app.get("/required-dict-of-list") |
||||
|
async def read_required_dict_of_list(p: Annotated[dict[str, list[int]], Query()]): |
||||
|
return {"p": p} |
||||
|
|
||||
|
class QueryModelRequiredDictOfList(BaseModel): |
||||
|
p: dict[str, list[int]] |
||||
|
|
||||
|
@app.get("/model-required-dict-of-list") |
||||
|
def read_model_required_dict_of_list( |
||||
|
p: Annotated[QueryModelRequiredDictOfList, Query()], |
||||
|
): |
||||
|
return {"p": p.p} |
||||
|
|
||||
|
@pytest.mark.parametrize( |
||||
|
"path", |
||||
|
["/required-dict-of-list", "/model-required-dict-of-list"], |
||||
|
) |
||||
|
def test_required_dict_of_list_schema(path: str): |
||||
|
assert app.openapi()["paths"][path]["get"]["parameters"] == [ |
||||
|
{ |
||||
|
"required": True, |
||||
|
"schema": { |
||||
|
"title": "P", |
||||
|
"type": "object", |
||||
|
"additionalProperties": { |
||||
|
"type": "array", |
||||
|
"items": {"type": "integer"}, |
||||
|
}, |
||||
|
}, |
||||
|
"name": "p", |
||||
|
"in": "query", |
||||
|
} |
||||
|
] |
||||
|
|
||||
|
@pytest.mark.parametrize( |
||||
|
"path", |
||||
|
["/required-dict-of-list", "/model-required-dict-of-list"], |
||||
|
) |
||||
|
def test_required_dict_of_list_missing(path: str): |
||||
|
client = TestClient(app) |
||||
|
response = client.get(path) |
||||
|
assert response.status_code == 422 |
||||
|
assert response.json() == { |
||||
|
"detail": [ |
||||
|
{ |
||||
|
"type": "missing", |
||||
|
"loc": ["query", "p"], |
||||
|
"msg": "Field required", |
||||
|
"input": IsOneOf(None, {}), |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
|
||||
|
@pytest.mark.parametrize( |
||||
|
"path", |
||||
|
["/required-dict-of-list", "/model-required-dict-of-list"], |
||||
|
) |
||||
|
def test_required_dict_of_list(path: str): |
||||
|
client = TestClient(app) |
||||
|
# Testing the "Wildcard" capture behavior for dicts with list values |
||||
|
response = client.get(f"{path}?foo=1&foo=2&baz=3") |
||||
|
assert response.status_code == 200 |
||||
|
assert response.json() == {"p": {"foo": [1, 2], "baz": [3]}} |
||||
Loading…
Reference in new issue