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.
1499 lines
55 KiB
1499 lines
55 KiB
import sys
|
|
from typing import Any, List, Union
|
|
|
|
from tests.utils import pydantic_snapshot, skip_module_if_py_gte_314
|
|
|
|
if sys.version_info >= (3, 14):
|
|
skip_module_if_py_gte_314()
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi._compat.v1 import BaseModel
|
|
from fastapi.testclient import TestClient
|
|
from inline_snapshot import snapshot
|
|
from pydantic import BaseModel as NewBaseModel
|
|
|
|
|
|
class SubItem(BaseModel):
|
|
name: str
|
|
|
|
|
|
class Item(BaseModel):
|
|
title: str
|
|
size: int
|
|
description: Union[str, None] = None
|
|
sub: SubItem
|
|
multi: List[SubItem] = []
|
|
|
|
|
|
class NewSubItem(NewBaseModel):
|
|
new_sub_name: str
|
|
|
|
|
|
class NewItem(NewBaseModel):
|
|
new_title: str
|
|
new_size: int
|
|
new_description: Union[str, None] = None
|
|
new_sub: NewSubItem
|
|
new_multi: List[NewSubItem] = []
|
|
|
|
|
|
app = FastAPI()
|
|
|
|
|
|
@app.post("/v1-to-v2/item")
|
|
def handle_v1_item_to_v2(data: Item) -> NewItem:
|
|
return NewItem(
|
|
new_title=data.title,
|
|
new_size=data.size,
|
|
new_description=data.description,
|
|
new_sub=NewSubItem(new_sub_name=data.sub.name),
|
|
new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi],
|
|
)
|
|
|
|
|
|
@app.post("/v1-to-v2/item-filter", response_model=NewItem)
|
|
def handle_v1_item_to_v2_filter(data: Item) -> Any:
|
|
result = {
|
|
"new_title": data.title,
|
|
"new_size": data.size,
|
|
"new_description": data.description,
|
|
"new_sub": {"new_sub_name": data.sub.name, "new_sub_secret": "sub_hidden"},
|
|
"new_multi": [
|
|
{"new_sub_name": s.name, "new_sub_secret": "sub_hidden"} for s in data.multi
|
|
],
|
|
"secret": "hidden_v1_to_v2",
|
|
}
|
|
return result
|
|
|
|
|
|
@app.post("/v2-to-v1/item")
|
|
def handle_v2_item_to_v1(data: NewItem) -> Item:
|
|
return Item(
|
|
title=data.new_title,
|
|
size=data.new_size,
|
|
description=data.new_description,
|
|
sub=SubItem(name=data.new_sub.new_sub_name),
|
|
multi=[SubItem(name=s.new_sub_name) for s in data.new_multi],
|
|
)
|
|
|
|
|
|
@app.post("/v2-to-v1/item-filter", response_model=Item)
|
|
def handle_v2_item_to_v1_filter(data: NewItem) -> Any:
|
|
result = {
|
|
"title": data.new_title,
|
|
"size": data.new_size,
|
|
"description": data.new_description,
|
|
"sub": {"name": data.new_sub.new_sub_name, "sub_secret": "sub_hidden"},
|
|
"multi": [
|
|
{"name": s.new_sub_name, "sub_secret": "sub_hidden"} for s in data.new_multi
|
|
],
|
|
"secret": "hidden_v2_to_v1",
|
|
}
|
|
return result
|
|
|
|
|
|
@app.post("/v1-to-v2/item-to-list")
|
|
def handle_v1_item_to_v2_list(data: Item) -> List[NewItem]:
|
|
converted = NewItem(
|
|
new_title=data.title,
|
|
new_size=data.size,
|
|
new_description=data.description,
|
|
new_sub=NewSubItem(new_sub_name=data.sub.name),
|
|
new_multi=[NewSubItem(new_sub_name=s.name) for s in data.multi],
|
|
)
|
|
return [converted, converted]
|
|
|
|
|
|
@app.post("/v1-to-v2/list-to-list")
|
|
def handle_v1_list_to_v2_list(data: List[Item]) -> List[NewItem]:
|
|
result = []
|
|
for item in data:
|
|
result.append(
|
|
NewItem(
|
|
new_title=item.title,
|
|
new_size=item.size,
|
|
new_description=item.description,
|
|
new_sub=NewSubItem(new_sub_name=item.sub.name),
|
|
new_multi=[NewSubItem(new_sub_name=s.name) for s in item.multi],
|
|
)
|
|
)
|
|
return result
|
|
|
|
|
|
@app.post("/v1-to-v2/list-to-list-filter", response_model=List[NewItem])
|
|
def handle_v1_list_to_v2_list_filter(data: List[Item]) -> Any:
|
|
result = []
|
|
for item in data:
|
|
converted = {
|
|
"new_title": item.title,
|
|
"new_size": item.size,
|
|
"new_description": item.description,
|
|
"new_sub": {"new_sub_name": item.sub.name, "new_sub_secret": "sub_hidden"},
|
|
"new_multi": [
|
|
{"new_sub_name": s.name, "new_sub_secret": "sub_hidden"}
|
|
for s in item.multi
|
|
],
|
|
"secret": "hidden_v2_to_v1",
|
|
}
|
|
result.append(converted)
|
|
return result
|
|
|
|
|
|
@app.post("/v1-to-v2/list-to-item")
|
|
def handle_v1_list_to_v2_item(data: List[Item]) -> NewItem:
|
|
if data:
|
|
item = data[0]
|
|
return NewItem(
|
|
new_title=item.title,
|
|
new_size=item.size,
|
|
new_description=item.description,
|
|
new_sub=NewSubItem(new_sub_name=item.sub.name),
|
|
new_multi=[NewSubItem(new_sub_name=s.name) for s in item.multi],
|
|
)
|
|
return NewItem(new_title="", new_size=0, new_sub=NewSubItem(new_sub_name=""))
|
|
|
|
|
|
@app.post("/v2-to-v1/item-to-list")
|
|
def handle_v2_item_to_v1_list(data: NewItem) -> List[Item]:
|
|
converted = Item(
|
|
title=data.new_title,
|
|
size=data.new_size,
|
|
description=data.new_description,
|
|
sub=SubItem(name=data.new_sub.new_sub_name),
|
|
multi=[SubItem(name=s.new_sub_name) for s in data.new_multi],
|
|
)
|
|
return [converted, converted]
|
|
|
|
|
|
@app.post("/v2-to-v1/list-to-list")
|
|
def handle_v2_list_to_v1_list(data: List[NewItem]) -> List[Item]:
|
|
result = []
|
|
for item in data:
|
|
result.append(
|
|
Item(
|
|
title=item.new_title,
|
|
size=item.new_size,
|
|
description=item.new_description,
|
|
sub=SubItem(name=item.new_sub.new_sub_name),
|
|
multi=[SubItem(name=s.new_sub_name) for s in item.new_multi],
|
|
)
|
|
)
|
|
return result
|
|
|
|
|
|
@app.post("/v2-to-v1/list-to-list-filter", response_model=List[Item])
|
|
def handle_v2_list_to_v1_list_filter(data: List[NewItem]) -> Any:
|
|
result = []
|
|
for item in data:
|
|
converted = {
|
|
"title": item.new_title,
|
|
"size": item.new_size,
|
|
"description": item.new_description,
|
|
"sub": {"name": item.new_sub.new_sub_name, "sub_secret": "sub_hidden"},
|
|
"multi": [
|
|
{"name": s.new_sub_name, "sub_secret": "sub_hidden"}
|
|
for s in item.new_multi
|
|
],
|
|
"secret": "hidden_v2_to_v1",
|
|
}
|
|
result.append(converted)
|
|
return result
|
|
|
|
|
|
@app.post("/v2-to-v1/list-to-item")
|
|
def handle_v2_list_to_v1_item(data: List[NewItem]) -> Item:
|
|
if data:
|
|
item = data[0]
|
|
return Item(
|
|
title=item.new_title,
|
|
size=item.new_size,
|
|
description=item.new_description,
|
|
sub=SubItem(name=item.new_sub.new_sub_name),
|
|
multi=[SubItem(name=s.new_sub_name) for s in item.new_multi],
|
|
)
|
|
return Item(title="", size=0, sub=SubItem(name=""))
|
|
|
|
|
|
client = TestClient(app)
|
|
|
|
|
|
def test_v1_to_v2_item():
|
|
response = client.post(
|
|
"/v1-to-v2/item",
|
|
json={
|
|
"title": "Old Item",
|
|
"size": 100,
|
|
"description": "V1 description",
|
|
"sub": {"name": "V1 Sub"},
|
|
"multi": [{"name": "M1"}, {"name": "M2"}],
|
|
},
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
assert response.json() == {
|
|
"new_title": "Old Item",
|
|
"new_size": 100,
|
|
"new_description": "V1 description",
|
|
"new_sub": {"new_sub_name": "V1 Sub"},
|
|
"new_multi": [{"new_sub_name": "M1"}, {"new_sub_name": "M2"}],
|
|
}
|
|
|
|
|
|
def test_v1_to_v2_item_minimal():
|
|
response = client.post(
|
|
"/v1-to-v2/item",
|
|
json={"title": "Minimal", "size": 50, "sub": {"name": "MinSub"}},
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
assert response.json() == {
|
|
"new_title": "Minimal",
|
|
"new_size": 50,
|
|
"new_description": None,
|
|
"new_sub": {"new_sub_name": "MinSub"},
|
|
"new_multi": [],
|
|
}
|
|
|
|
|
|
def test_v1_to_v2_item_filter():
|
|
response = client.post(
|
|
"/v1-to-v2/item-filter",
|
|
json={
|
|
"title": "Filtered Item",
|
|
"size": 50,
|
|
"sub": {"name": "Sub"},
|
|
"multi": [{"name": "Multi1"}],
|
|
},
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
result = response.json()
|
|
assert result == snapshot(
|
|
{
|
|
"new_title": "Filtered Item",
|
|
"new_size": 50,
|
|
"new_description": None,
|
|
"new_sub": {"new_sub_name": "Sub"},
|
|
"new_multi": [{"new_sub_name": "Multi1"}],
|
|
}
|
|
)
|
|
# Verify secret fields are filtered out
|
|
assert "secret" not in result
|
|
assert "new_sub_secret" not in result["new_sub"]
|
|
assert "new_sub_secret" not in result["new_multi"][0]
|
|
|
|
|
|
def test_v2_to_v1_item():
|
|
response = client.post(
|
|
"/v2-to-v1/item",
|
|
json={
|
|
"new_title": "New Item",
|
|
"new_size": 200,
|
|
"new_description": "V2 description",
|
|
"new_sub": {"new_sub_name": "V2 Sub"},
|
|
"new_multi": [{"new_sub_name": "N1"}, {"new_sub_name": "N2"}],
|
|
},
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
assert response.json() == {
|
|
"title": "New Item",
|
|
"size": 200,
|
|
"description": "V2 description",
|
|
"sub": {"name": "V2 Sub"},
|
|
"multi": [{"name": "N1"}, {"name": "N2"}],
|
|
}
|
|
|
|
|
|
def test_v2_to_v1_item_minimal():
|
|
response = client.post(
|
|
"/v2-to-v1/item",
|
|
json={
|
|
"new_title": "MinimalNew",
|
|
"new_size": 75,
|
|
"new_sub": {"new_sub_name": "MinNewSub"},
|
|
},
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
assert response.json() == {
|
|
"title": "MinimalNew",
|
|
"size": 75,
|
|
"description": None,
|
|
"sub": {"name": "MinNewSub"},
|
|
"multi": [],
|
|
}
|
|
|
|
|
|
def test_v2_to_v1_item_filter():
|
|
response = client.post(
|
|
"/v2-to-v1/item-filter",
|
|
json={
|
|
"new_title": "Filtered New",
|
|
"new_size": 75,
|
|
"new_sub": {"new_sub_name": "NewSub"},
|
|
"new_multi": [],
|
|
},
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
result = response.json()
|
|
assert result == snapshot(
|
|
{
|
|
"title": "Filtered New",
|
|
"size": 75,
|
|
"description": None,
|
|
"sub": {"name": "NewSub"},
|
|
"multi": [],
|
|
}
|
|
)
|
|
# Verify secret fields are filtered out
|
|
assert "secret" not in result
|
|
assert "sub_secret" not in result["sub"]
|
|
|
|
|
|
def test_v1_item_to_v2_list():
|
|
response = client.post(
|
|
"/v1-to-v2/item-to-list",
|
|
json={
|
|
"title": "Single to List",
|
|
"size": 150,
|
|
"description": "Convert to list",
|
|
"sub": {"name": "Sub1"},
|
|
"multi": [],
|
|
},
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
result = response.json()
|
|
assert result == [
|
|
{
|
|
"new_title": "Single to List",
|
|
"new_size": 150,
|
|
"new_description": "Convert to list",
|
|
"new_sub": {"new_sub_name": "Sub1"},
|
|
"new_multi": [],
|
|
},
|
|
{
|
|
"new_title": "Single to List",
|
|
"new_size": 150,
|
|
"new_description": "Convert to list",
|
|
"new_sub": {"new_sub_name": "Sub1"},
|
|
"new_multi": [],
|
|
},
|
|
]
|
|
|
|
|
|
def test_v1_list_to_v2_list():
|
|
response = client.post(
|
|
"/v1-to-v2/list-to-list",
|
|
json=[
|
|
{"title": "Item1", "size": 10, "sub": {"name": "Sub1"}},
|
|
{
|
|
"title": "Item2",
|
|
"size": 20,
|
|
"description": "Second item",
|
|
"sub": {"name": "Sub2"},
|
|
"multi": [{"name": "M1"}, {"name": "M2"}],
|
|
},
|
|
{"title": "Item3", "size": 30, "sub": {"name": "Sub3"}},
|
|
],
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
assert response.json() == [
|
|
{
|
|
"new_title": "Item1",
|
|
"new_size": 10,
|
|
"new_description": None,
|
|
"new_sub": {"new_sub_name": "Sub1"},
|
|
"new_multi": [],
|
|
},
|
|
{
|
|
"new_title": "Item2",
|
|
"new_size": 20,
|
|
"new_description": "Second item",
|
|
"new_sub": {"new_sub_name": "Sub2"},
|
|
"new_multi": [{"new_sub_name": "M1"}, {"new_sub_name": "M2"}],
|
|
},
|
|
{
|
|
"new_title": "Item3",
|
|
"new_size": 30,
|
|
"new_description": None,
|
|
"new_sub": {"new_sub_name": "Sub3"},
|
|
"new_multi": [],
|
|
},
|
|
]
|
|
|
|
|
|
def test_v1_list_to_v2_list_filter():
|
|
response = client.post(
|
|
"/v1-to-v2/list-to-list-filter",
|
|
json=[{"title": "FilterMe", "size": 30, "sub": {"name": "SubF"}}],
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
result = response.json()
|
|
assert result == snapshot(
|
|
[
|
|
{
|
|
"new_title": "FilterMe",
|
|
"new_size": 30,
|
|
"new_description": None,
|
|
"new_sub": {"new_sub_name": "SubF"},
|
|
"new_multi": [],
|
|
}
|
|
]
|
|
)
|
|
# Verify secret fields are filtered out
|
|
assert "secret" not in result[0]
|
|
assert "new_sub_secret" not in result[0]["new_sub"]
|
|
|
|
|
|
def test_v1_list_to_v2_item():
|
|
response = client.post(
|
|
"/v1-to-v2/list-to-item",
|
|
json=[
|
|
{"title": "First", "size": 100, "sub": {"name": "FirstSub"}},
|
|
{"title": "Second", "size": 200, "sub": {"name": "SecondSub"}},
|
|
],
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
assert response.json() == {
|
|
"new_title": "First",
|
|
"new_size": 100,
|
|
"new_description": None,
|
|
"new_sub": {"new_sub_name": "FirstSub"},
|
|
"new_multi": [],
|
|
}
|
|
|
|
|
|
def test_v1_list_to_v2_item_empty():
|
|
response = client.post("/v1-to-v2/list-to-item", json=[])
|
|
assert response.status_code == 200, response.text
|
|
assert response.json() == {
|
|
"new_title": "",
|
|
"new_size": 0,
|
|
"new_description": None,
|
|
"new_sub": {"new_sub_name": ""},
|
|
"new_multi": [],
|
|
}
|
|
|
|
|
|
def test_v2_item_to_v1_list():
|
|
response = client.post(
|
|
"/v2-to-v1/item-to-list",
|
|
json={
|
|
"new_title": "Single New",
|
|
"new_size": 250,
|
|
"new_description": "New to list",
|
|
"new_sub": {"new_sub_name": "NewSub"},
|
|
"new_multi": [],
|
|
},
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
assert response.json() == [
|
|
{
|
|
"title": "Single New",
|
|
"size": 250,
|
|
"description": "New to list",
|
|
"sub": {"name": "NewSub"},
|
|
"multi": [],
|
|
},
|
|
{
|
|
"title": "Single New",
|
|
"size": 250,
|
|
"description": "New to list",
|
|
"sub": {"name": "NewSub"},
|
|
"multi": [],
|
|
},
|
|
]
|
|
|
|
|
|
def test_v2_list_to_v1_list():
|
|
response = client.post(
|
|
"/v2-to-v1/list-to-list",
|
|
json=[
|
|
{"new_title": "New1", "new_size": 15, "new_sub": {"new_sub_name": "NS1"}},
|
|
{
|
|
"new_title": "New2",
|
|
"new_size": 25,
|
|
"new_description": "Second new",
|
|
"new_sub": {"new_sub_name": "NS2"},
|
|
"new_multi": [{"new_sub_name": "NM1"}],
|
|
},
|
|
],
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
assert response.json() == [
|
|
{
|
|
"title": "New1",
|
|
"size": 15,
|
|
"description": None,
|
|
"sub": {"name": "NS1"},
|
|
"multi": [],
|
|
},
|
|
{
|
|
"title": "New2",
|
|
"size": 25,
|
|
"description": "Second new",
|
|
"sub": {"name": "NS2"},
|
|
"multi": [{"name": "NM1"}],
|
|
},
|
|
]
|
|
|
|
|
|
def test_v2_list_to_v1_list_filter():
|
|
response = client.post(
|
|
"/v2-to-v1/list-to-list-filter",
|
|
json=[
|
|
{
|
|
"new_title": "FilterNew",
|
|
"new_size": 35,
|
|
"new_sub": {"new_sub_name": "NSF"},
|
|
}
|
|
],
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
result = response.json()
|
|
assert result == snapshot(
|
|
[
|
|
{
|
|
"title": "FilterNew",
|
|
"size": 35,
|
|
"description": None,
|
|
"sub": {"name": "NSF"},
|
|
"multi": [],
|
|
}
|
|
]
|
|
)
|
|
# Verify secret fields are filtered out
|
|
assert "secret" not in result[0]
|
|
assert "sub_secret" not in result[0]["sub"]
|
|
|
|
|
|
def test_v2_list_to_v1_item():
|
|
response = client.post(
|
|
"/v2-to-v1/list-to-item",
|
|
json=[
|
|
{
|
|
"new_title": "FirstNew",
|
|
"new_size": 300,
|
|
"new_sub": {"new_sub_name": "FNS"},
|
|
},
|
|
{
|
|
"new_title": "SecondNew",
|
|
"new_size": 400,
|
|
"new_sub": {"new_sub_name": "SNS"},
|
|
},
|
|
],
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
assert response.json() == {
|
|
"title": "FirstNew",
|
|
"size": 300,
|
|
"description": None,
|
|
"sub": {"name": "FNS"},
|
|
"multi": [],
|
|
}
|
|
|
|
|
|
def test_v2_list_to_v1_item_empty():
|
|
response = client.post("/v2-to-v1/list-to-item", json=[])
|
|
assert response.status_code == 200, response.text
|
|
assert response.json() == {
|
|
"title": "",
|
|
"size": 0,
|
|
"description": None,
|
|
"sub": {"name": ""},
|
|
"multi": [],
|
|
}
|
|
|
|
|
|
def test_v1_to_v2_validation_error():
|
|
response = client.post("/v1-to-v2/item", json={"title": "Missing fields"})
|
|
assert response.status_code == 422, response.text
|
|
assert response.json() == snapshot(
|
|
{
|
|
"detail": [
|
|
{
|
|
"loc": ["body", "size"],
|
|
"msg": "field required",
|
|
"type": "value_error.missing",
|
|
},
|
|
{
|
|
"loc": ["body", "sub"],
|
|
"msg": "field required",
|
|
"type": "value_error.missing",
|
|
},
|
|
]
|
|
}
|
|
)
|
|
|
|
|
|
def test_v1_to_v2_nested_validation_error():
|
|
response = client.post(
|
|
"/v1-to-v2/item",
|
|
json={"title": "Bad sub", "size": 100, "sub": {"wrong_field": "value"}},
|
|
)
|
|
assert response.status_code == 422, response.text
|
|
assert response.json() == snapshot(
|
|
{
|
|
"detail": [
|
|
{
|
|
"loc": ["body", "sub", "name"],
|
|
"msg": "field required",
|
|
"type": "value_error.missing",
|
|
}
|
|
]
|
|
}
|
|
)
|
|
|
|
|
|
def test_v1_to_v2_type_validation_error():
|
|
response = client.post(
|
|
"/v1-to-v2/item",
|
|
json={"title": "Bad type", "size": "not_a_number", "sub": {"name": "Sub"}},
|
|
)
|
|
assert response.status_code == 422, response.text
|
|
assert response.json() == snapshot(
|
|
{
|
|
"detail": [
|
|
{
|
|
"loc": ["body", "size"],
|
|
"msg": "value is not a valid integer",
|
|
"type": "type_error.integer",
|
|
}
|
|
]
|
|
}
|
|
)
|
|
|
|
|
|
def test_v2_to_v1_validation_error():
|
|
response = client.post(
|
|
"/v2-to-v1/item",
|
|
json={"new_title": "Missing fields"},
|
|
)
|
|
assert response.status_code == 422, response.text
|
|
assert response.json() == snapshot(
|
|
{
|
|
"detail": pydantic_snapshot(
|
|
v2=snapshot(
|
|
[
|
|
{
|
|
"type": "missing",
|
|
"loc": ["body", "new_size"],
|
|
"msg": "Field required",
|
|
"input": {"new_title": "Missing fields"},
|
|
},
|
|
{
|
|
"type": "missing",
|
|
"loc": ["body", "new_sub"],
|
|
"msg": "Field required",
|
|
"input": {"new_title": "Missing fields"},
|
|
},
|
|
]
|
|
),
|
|
v1=snapshot(
|
|
[
|
|
{
|
|
"loc": ["body", "new_size"],
|
|
"msg": "field required",
|
|
"type": "value_error.missing",
|
|
},
|
|
{
|
|
"loc": ["body", "new_sub"],
|
|
"msg": "field required",
|
|
"type": "value_error.missing",
|
|
},
|
|
]
|
|
),
|
|
)
|
|
}
|
|
)
|
|
|
|
|
|
def test_v2_to_v1_nested_validation_error():
|
|
response = client.post(
|
|
"/v2-to-v1/item",
|
|
json={
|
|
"new_title": "Bad sub",
|
|
"new_size": 200,
|
|
"new_sub": {"wrong_field": "value"},
|
|
},
|
|
)
|
|
assert response.status_code == 422, response.text
|
|
assert response.json() == snapshot(
|
|
{
|
|
"detail": [
|
|
pydantic_snapshot(
|
|
v2=snapshot(
|
|
{
|
|
"type": "missing",
|
|
"loc": ["body", "new_sub", "new_sub_name"],
|
|
"msg": "Field required",
|
|
"input": {"wrong_field": "value"},
|
|
}
|
|
),
|
|
v1=snapshot(
|
|
{
|
|
"loc": ["body", "new_sub", "new_sub_name"],
|
|
"msg": "field required",
|
|
"type": "value_error.missing",
|
|
}
|
|
),
|
|
)
|
|
]
|
|
}
|
|
)
|
|
|
|
|
|
def test_v1_list_validation_error():
|
|
response = client.post(
|
|
"/v1-to-v2/list-to-list",
|
|
json=[
|
|
{"title": "Valid", "size": 10, "sub": {"name": "S"}},
|
|
{"title": "Invalid"},
|
|
],
|
|
)
|
|
assert response.status_code == 422, response.text
|
|
assert response.json() == snapshot(
|
|
{
|
|
"detail": [
|
|
{
|
|
"loc": ["body", 1, "size"],
|
|
"msg": "field required",
|
|
"type": "value_error.missing",
|
|
},
|
|
{
|
|
"loc": ["body", 1, "sub"],
|
|
"msg": "field required",
|
|
"type": "value_error.missing",
|
|
},
|
|
]
|
|
}
|
|
)
|
|
|
|
|
|
def test_v2_list_validation_error():
|
|
response = client.post(
|
|
"/v2-to-v1/list-to-list",
|
|
json=[
|
|
{"new_title": "Valid", "new_size": 10, "new_sub": {"new_sub_name": "NS"}},
|
|
{"new_title": "Invalid"},
|
|
],
|
|
)
|
|
assert response.status_code == 422, response.text
|
|
assert response.json() == snapshot(
|
|
{
|
|
"detail": pydantic_snapshot(
|
|
v2=snapshot(
|
|
[
|
|
{
|
|
"type": "missing",
|
|
"loc": ["body", 1, "new_size"],
|
|
"msg": "Field required",
|
|
"input": {"new_title": "Invalid"},
|
|
},
|
|
{
|
|
"type": "missing",
|
|
"loc": ["body", 1, "new_sub"],
|
|
"msg": "Field required",
|
|
"input": {"new_title": "Invalid"},
|
|
},
|
|
]
|
|
),
|
|
v1=snapshot(
|
|
[
|
|
{
|
|
"loc": ["body", 1, "new_size"],
|
|
"msg": "field required",
|
|
"type": "value_error.missing",
|
|
},
|
|
{
|
|
"loc": ["body", 1, "new_sub"],
|
|
"msg": "field required",
|
|
"type": "value_error.missing",
|
|
},
|
|
]
|
|
),
|
|
)
|
|
}
|
|
)
|
|
|
|
|
|
def test_invalid_list_structure_v1():
|
|
response = client.post(
|
|
"/v1-to-v2/list-to-list",
|
|
json={"title": "Not a list", "size": 100, "sub": {"name": "Sub"}},
|
|
)
|
|
assert response.status_code == 422, response.text
|
|
assert response.json() == snapshot(
|
|
{
|
|
"detail": [
|
|
{
|
|
"loc": ["body"],
|
|
"msg": "value is not a valid list",
|
|
"type": "type_error.list",
|
|
}
|
|
]
|
|
}
|
|
)
|
|
|
|
|
|
def test_invalid_list_structure_v2():
|
|
response = client.post(
|
|
"/v2-to-v1/list-to-list",
|
|
json={
|
|
"new_title": "Not a list",
|
|
"new_size": 100,
|
|
"new_sub": {"new_sub_name": "Sub"},
|
|
},
|
|
)
|
|
assert response.status_code == 422, response.text
|
|
assert response.json() == snapshot(
|
|
{
|
|
"detail": pydantic_snapshot(
|
|
v2=snapshot(
|
|
[
|
|
{
|
|
"type": "list_type",
|
|
"loc": ["body"],
|
|
"msg": "Input should be a valid list",
|
|
"input": {
|
|
"new_title": "Not a list",
|
|
"new_size": 100,
|
|
"new_sub": {"new_sub_name": "Sub"},
|
|
},
|
|
}
|
|
]
|
|
),
|
|
v1=snapshot(
|
|
[
|
|
{
|
|
"loc": ["body"],
|
|
"msg": "value is not a valid list",
|
|
"type": "type_error.list",
|
|
}
|
|
]
|
|
),
|
|
)
|
|
}
|
|
)
|
|
|
|
|
|
def test_openapi_schema():
|
|
response = client.get("/openapi.json")
|
|
assert response.status_code == 200, response.text
|
|
assert response.json() == snapshot(
|
|
{
|
|
"openapi": "3.1.0",
|
|
"info": {"title": "FastAPI", "version": "0.1.0"},
|
|
"paths": {
|
|
"/v1-to-v2/item": {
|
|
"post": {
|
|
"summary": "Handle V1 Item To V2",
|
|
"operationId": "handle_v1_item_to_v2_v1_to_v2_item_post",
|
|
"requestBody": {
|
|
"content": {
|
|
"application/json": {
|
|
"schema": pydantic_snapshot(
|
|
v2=snapshot(
|
|
{
|
|
"allOf": [
|
|
{
|
|
"$ref": "#/components/schemas/Item"
|
|
}
|
|
],
|
|
"title": "Data",
|
|
}
|
|
),
|
|
v1=snapshot(
|
|
{"$ref": "#/components/schemas/Item"}
|
|
),
|
|
)
|
|
}
|
|
},
|
|
"required": True,
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "Successful Response",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"$ref": "#/components/schemas/NewItem"
|
|
}
|
|
}
|
|
},
|
|
},
|
|
"422": {
|
|
"description": "Validation Error",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"$ref": "#/components/schemas/HTTPValidationError"
|
|
}
|
|
}
|
|
},
|
|
},
|
|
},
|
|
}
|
|
},
|
|
"/v1-to-v2/item-filter": {
|
|
"post": {
|
|
"summary": "Handle V1 Item To V2 Filter",
|
|
"operationId": "handle_v1_item_to_v2_filter_v1_to_v2_item_filter_post",
|
|
"requestBody": {
|
|
"content": {
|
|
"application/json": {
|
|
"schema": pydantic_snapshot(
|
|
v2=snapshot(
|
|
{
|
|
"allOf": [
|
|
{
|
|
"$ref": "#/components/schemas/Item"
|
|
}
|
|
],
|
|
"title": "Data",
|
|
}
|
|
),
|
|
v1=snapshot(
|
|
{"$ref": "#/components/schemas/Item"}
|
|
),
|
|
)
|
|
}
|
|
},
|
|
"required": True,
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "Successful Response",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"$ref": "#/components/schemas/NewItem"
|
|
}
|
|
}
|
|
},
|
|
},
|
|
"422": {
|
|
"description": "Validation Error",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"$ref": "#/components/schemas/HTTPValidationError"
|
|
}
|
|
}
|
|
},
|
|
},
|
|
},
|
|
}
|
|
},
|
|
"/v2-to-v1/item": {
|
|
"post": {
|
|
"summary": "Handle V2 Item To V1",
|
|
"operationId": "handle_v2_item_to_v1_v2_to_v1_item_post",
|
|
"requestBody": {
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {"$ref": "#/components/schemas/NewItem"}
|
|
}
|
|
},
|
|
"required": True,
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "Successful Response",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {"$ref": "#/components/schemas/Item"}
|
|
}
|
|
},
|
|
},
|
|
"422": {
|
|
"description": "Validation Error",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"$ref": "#/components/schemas/HTTPValidationError"
|
|
}
|
|
}
|
|
},
|
|
},
|
|
},
|
|
}
|
|
},
|
|
"/v2-to-v1/item-filter": {
|
|
"post": {
|
|
"summary": "Handle V2 Item To V1 Filter",
|
|
"operationId": "handle_v2_item_to_v1_filter_v2_to_v1_item_filter_post",
|
|
"requestBody": {
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {"$ref": "#/components/schemas/NewItem"}
|
|
}
|
|
},
|
|
"required": True,
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "Successful Response",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {"$ref": "#/components/schemas/Item"}
|
|
}
|
|
},
|
|
},
|
|
"422": {
|
|
"description": "Validation Error",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"$ref": "#/components/schemas/HTTPValidationError"
|
|
}
|
|
}
|
|
},
|
|
},
|
|
},
|
|
}
|
|
},
|
|
"/v1-to-v2/item-to-list": {
|
|
"post": {
|
|
"summary": "Handle V1 Item To V2 List",
|
|
"operationId": "handle_v1_item_to_v2_list_v1_to_v2_item_to_list_post",
|
|
"requestBody": {
|
|
"content": {
|
|
"application/json": {
|
|
"schema": pydantic_snapshot(
|
|
v2=snapshot(
|
|
{
|
|
"allOf": [
|
|
{
|
|
"$ref": "#/components/schemas/Item"
|
|
}
|
|
],
|
|
"title": "Data",
|
|
}
|
|
),
|
|
v1=snapshot(
|
|
{"$ref": "#/components/schemas/Item"}
|
|
),
|
|
)
|
|
}
|
|
},
|
|
"required": True,
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "Successful Response",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"items": {
|
|
"$ref": "#/components/schemas/NewItem"
|
|
},
|
|
"type": "array",
|
|
"title": "Response Handle V1 Item To V2 List V1 To V2 Item To List Post",
|
|
}
|
|
}
|
|
},
|
|
},
|
|
"422": {
|
|
"description": "Validation Error",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"$ref": "#/components/schemas/HTTPValidationError"
|
|
}
|
|
}
|
|
},
|
|
},
|
|
},
|
|
}
|
|
},
|
|
"/v1-to-v2/list-to-list": {
|
|
"post": {
|
|
"summary": "Handle V1 List To V2 List",
|
|
"operationId": "handle_v1_list_to_v2_list_v1_to_v2_list_to_list_post",
|
|
"requestBody": {
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"items": {"$ref": "#/components/schemas/Item"},
|
|
"type": "array",
|
|
"title": "Data",
|
|
}
|
|
}
|
|
},
|
|
"required": True,
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "Successful Response",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"items": {
|
|
"$ref": "#/components/schemas/NewItem"
|
|
},
|
|
"type": "array",
|
|
"title": "Response Handle V1 List To V2 List V1 To V2 List To List Post",
|
|
}
|
|
}
|
|
},
|
|
},
|
|
"422": {
|
|
"description": "Validation Error",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"$ref": "#/components/schemas/HTTPValidationError"
|
|
}
|
|
}
|
|
},
|
|
},
|
|
},
|
|
}
|
|
},
|
|
"/v1-to-v2/list-to-list-filter": {
|
|
"post": {
|
|
"summary": "Handle V1 List To V2 List Filter",
|
|
"operationId": "handle_v1_list_to_v2_list_filter_v1_to_v2_list_to_list_filter_post",
|
|
"requestBody": {
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"items": {"$ref": "#/components/schemas/Item"},
|
|
"type": "array",
|
|
"title": "Data",
|
|
}
|
|
}
|
|
},
|
|
"required": True,
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "Successful Response",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"items": {
|
|
"$ref": "#/components/schemas/NewItem"
|
|
},
|
|
"type": "array",
|
|
"title": "Response Handle V1 List To V2 List Filter V1 To V2 List To List Filter Post",
|
|
}
|
|
}
|
|
},
|
|
},
|
|
"422": {
|
|
"description": "Validation Error",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"$ref": "#/components/schemas/HTTPValidationError"
|
|
}
|
|
}
|
|
},
|
|
},
|
|
},
|
|
}
|
|
},
|
|
"/v1-to-v2/list-to-item": {
|
|
"post": {
|
|
"summary": "Handle V1 List To V2 Item",
|
|
"operationId": "handle_v1_list_to_v2_item_v1_to_v2_list_to_item_post",
|
|
"requestBody": {
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"items": {"$ref": "#/components/schemas/Item"},
|
|
"type": "array",
|
|
"title": "Data",
|
|
}
|
|
}
|
|
},
|
|
"required": True,
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "Successful Response",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"$ref": "#/components/schemas/NewItem"
|
|
}
|
|
}
|
|
},
|
|
},
|
|
"422": {
|
|
"description": "Validation Error",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"$ref": "#/components/schemas/HTTPValidationError"
|
|
}
|
|
}
|
|
},
|
|
},
|
|
},
|
|
}
|
|
},
|
|
"/v2-to-v1/item-to-list": {
|
|
"post": {
|
|
"summary": "Handle V2 Item To V1 List",
|
|
"operationId": "handle_v2_item_to_v1_list_v2_to_v1_item_to_list_post",
|
|
"requestBody": {
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {"$ref": "#/components/schemas/NewItem"}
|
|
}
|
|
},
|
|
"required": True,
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "Successful Response",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"items": {
|
|
"$ref": "#/components/schemas/Item"
|
|
},
|
|
"type": "array",
|
|
"title": "Response Handle V2 Item To V1 List V2 To V1 Item To List Post",
|
|
}
|
|
}
|
|
},
|
|
},
|
|
"422": {
|
|
"description": "Validation Error",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"$ref": "#/components/schemas/HTTPValidationError"
|
|
}
|
|
}
|
|
},
|
|
},
|
|
},
|
|
}
|
|
},
|
|
"/v2-to-v1/list-to-list": {
|
|
"post": {
|
|
"summary": "Handle V2 List To V1 List",
|
|
"operationId": "handle_v2_list_to_v1_list_v2_to_v1_list_to_list_post",
|
|
"requestBody": {
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"items": {
|
|
"$ref": "#/components/schemas/NewItem"
|
|
},
|
|
"type": "array",
|
|
"title": "Data",
|
|
}
|
|
}
|
|
},
|
|
"required": True,
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "Successful Response",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"items": {
|
|
"$ref": "#/components/schemas/Item"
|
|
},
|
|
"type": "array",
|
|
"title": "Response Handle V2 List To V1 List V2 To V1 List To List Post",
|
|
}
|
|
}
|
|
},
|
|
},
|
|
"422": {
|
|
"description": "Validation Error",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"$ref": "#/components/schemas/HTTPValidationError"
|
|
}
|
|
}
|
|
},
|
|
},
|
|
},
|
|
}
|
|
},
|
|
"/v2-to-v1/list-to-list-filter": {
|
|
"post": {
|
|
"summary": "Handle V2 List To V1 List Filter",
|
|
"operationId": "handle_v2_list_to_v1_list_filter_v2_to_v1_list_to_list_filter_post",
|
|
"requestBody": {
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"items": {
|
|
"$ref": "#/components/schemas/NewItem"
|
|
},
|
|
"type": "array",
|
|
"title": "Data",
|
|
}
|
|
}
|
|
},
|
|
"required": True,
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "Successful Response",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"items": {
|
|
"$ref": "#/components/schemas/Item"
|
|
},
|
|
"type": "array",
|
|
"title": "Response Handle V2 List To V1 List Filter V2 To V1 List To List Filter Post",
|
|
}
|
|
}
|
|
},
|
|
},
|
|
"422": {
|
|
"description": "Validation Error",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"$ref": "#/components/schemas/HTTPValidationError"
|
|
}
|
|
}
|
|
},
|
|
},
|
|
},
|
|
}
|
|
},
|
|
"/v2-to-v1/list-to-item": {
|
|
"post": {
|
|
"summary": "Handle V2 List To V1 Item",
|
|
"operationId": "handle_v2_list_to_v1_item_v2_to_v1_list_to_item_post",
|
|
"requestBody": {
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"items": {
|
|
"$ref": "#/components/schemas/NewItem"
|
|
},
|
|
"type": "array",
|
|
"title": "Data",
|
|
}
|
|
}
|
|
},
|
|
"required": True,
|
|
},
|
|
"responses": {
|
|
"200": {
|
|
"description": "Successful Response",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {"$ref": "#/components/schemas/Item"}
|
|
}
|
|
},
|
|
},
|
|
"422": {
|
|
"description": "Validation Error",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"$ref": "#/components/schemas/HTTPValidationError"
|
|
}
|
|
}
|
|
},
|
|
},
|
|
},
|
|
}
|
|
},
|
|
},
|
|
"components": {
|
|
"schemas": {
|
|
"HTTPValidationError": {
|
|
"properties": {
|
|
"detail": {
|
|
"items": {
|
|
"$ref": "#/components/schemas/ValidationError"
|
|
},
|
|
"type": "array",
|
|
"title": "Detail",
|
|
}
|
|
},
|
|
"type": "object",
|
|
"title": "HTTPValidationError",
|
|
},
|
|
"Item": {
|
|
"properties": {
|
|
"title": {"type": "string", "title": "Title"},
|
|
"size": {"type": "integer", "title": "Size"},
|
|
"description": {"type": "string", "title": "Description"},
|
|
"sub": {"$ref": "#/components/schemas/SubItem"},
|
|
"multi": {
|
|
"items": {"$ref": "#/components/schemas/SubItem"},
|
|
"type": "array",
|
|
"title": "Multi",
|
|
"default": [],
|
|
},
|
|
},
|
|
"type": "object",
|
|
"required": ["title", "size", "sub"],
|
|
"title": "Item",
|
|
},
|
|
"NewItem": {
|
|
"properties": {
|
|
"new_title": {"type": "string", "title": "New Title"},
|
|
"new_size": {"type": "integer", "title": "New Size"},
|
|
"new_description": pydantic_snapshot(
|
|
v2=snapshot(
|
|
{
|
|
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
"title": "New Description",
|
|
}
|
|
),
|
|
v1=snapshot(
|
|
{"type": "string", "title": "New Description"}
|
|
),
|
|
),
|
|
"new_sub": {"$ref": "#/components/schemas/NewSubItem"},
|
|
"new_multi": {
|
|
"items": {"$ref": "#/components/schemas/NewSubItem"},
|
|
"type": "array",
|
|
"title": "New Multi",
|
|
"default": [],
|
|
},
|
|
},
|
|
"type": "object",
|
|
"required": ["new_title", "new_size", "new_sub"],
|
|
"title": "NewItem",
|
|
},
|
|
"NewSubItem": {
|
|
"properties": {
|
|
"new_sub_name": {"type": "string", "title": "New Sub Name"}
|
|
},
|
|
"type": "object",
|
|
"required": ["new_sub_name"],
|
|
"title": "NewSubItem",
|
|
},
|
|
"SubItem": {
|
|
"properties": {"name": {"type": "string", "title": "Name"}},
|
|
"type": "object",
|
|
"required": ["name"],
|
|
"title": "SubItem",
|
|
},
|
|
"ValidationError": {
|
|
"properties": {
|
|
"loc": {
|
|
"items": {
|
|
"anyOf": [{"type": "string"}, {"type": "integer"}]
|
|
},
|
|
"type": "array",
|
|
"title": "Location",
|
|
},
|
|
"msg": {"type": "string", "title": "Message"},
|
|
"type": {"type": "string", "title": "Error Type"},
|
|
},
|
|
"type": "object",
|
|
"required": ["loc", "msg", "type"],
|
|
"title": "ValidationError",
|
|
},
|
|
}
|
|
},
|
|
}
|
|
)
|
|
|