Add group parameter feature
This commit is contained in:
74
tests/test_group.py
Normal file
74
tests/test_group.py
Normal file
@@ -0,0 +1,74 @@
|
||||
import pytest
|
||||
|
||||
from aiohttp_pydantic.injectors import (
|
||||
Group,
|
||||
_get_group_signature,
|
||||
_unpack_group_in_signature,
|
||||
DuplicateNames,
|
||||
)
|
||||
|
||||
|
||||
def test_get_group_signature_with_a2b2():
|
||||
class A(Group):
|
||||
a: int = 1
|
||||
|
||||
class B(Group):
|
||||
b: str = "b"
|
||||
|
||||
class B2(B):
|
||||
b: str = "b2" # Overwrite default value
|
||||
|
||||
class A2(A):
|
||||
a: int # Remove default value
|
||||
|
||||
class A2B2(A2, B2):
|
||||
ab2: float
|
||||
|
||||
assert ({"ab2": float, "a": int, "b": str}, {"b": "b2"}) == _get_group_signature(
|
||||
A2B2
|
||||
)
|
||||
|
||||
|
||||
def test_unpack_group_in_signature():
|
||||
class PaginationGroup(Group):
|
||||
page: int
|
||||
page_size: int = 20
|
||||
|
||||
args = {"pagination": PaginationGroup, "name": str, "age": int}
|
||||
|
||||
default = {"age": 18}
|
||||
|
||||
_unpack_group_in_signature(args, default)
|
||||
|
||||
assert args == {"page": int, "page_size": int, "name": str, "age": int}
|
||||
|
||||
assert default == {"age": 18, "page_size": 20}
|
||||
|
||||
|
||||
def test_unpack_group_in_signature_with_duplicate_error():
|
||||
class PaginationGroup(Group):
|
||||
page: int
|
||||
page_size: int = 20
|
||||
|
||||
args = {"pagination": PaginationGroup, "page": int, "age": int}
|
||||
|
||||
with pytest.raises(DuplicateNames) as e_info:
|
||||
_unpack_group_in_signature(args, {})
|
||||
|
||||
assert e_info.value.group is PaginationGroup
|
||||
assert e_info.value.attr_name == "page"
|
||||
|
||||
|
||||
def test_unpack_group_in_signature_with_parameters_overwrite():
|
||||
class PaginationGroup(Group):
|
||||
page: int = 0
|
||||
page_size: int = 20
|
||||
|
||||
args = {"page": PaginationGroup, "age": int}
|
||||
|
||||
default = {}
|
||||
_unpack_group_in_signature(args, default)
|
||||
|
||||
assert args == {"page": int, "page_size": int, "age": int}
|
||||
|
||||
assert default == {"page": 0, "page_size": 20}
|
||||
@@ -123,18 +123,12 @@ def test_paths_operation_tags():
|
||||
oas = OpenApiSpec3()
|
||||
operation = oas.paths["/users/{petId}"].get
|
||||
assert operation.tags == []
|
||||
operation.tags = ['pets']
|
||||
operation.tags = ["pets"]
|
||||
|
||||
assert oas.spec['paths']['/users/{petId}'] == {
|
||||
'get': {
|
||||
'tags': ['pets']
|
||||
}
|
||||
}
|
||||
assert oas.spec["paths"]["/users/{petId}"] == {"get": {"tags": ["pets"]}}
|
||||
|
||||
operation.tags = []
|
||||
assert oas.spec['paths']['/users/{petId}'] == {
|
||||
'get': {}
|
||||
}
|
||||
assert oas.spec["paths"]["/users/{petId}"] == {"get": {}}
|
||||
|
||||
|
||||
def test_paths_operation_responses():
|
||||
|
||||
@@ -7,6 +7,7 @@ from aiohttp import web
|
||||
from pydantic.main import BaseModel
|
||||
|
||||
from aiohttp_pydantic import PydanticView, oas
|
||||
from aiohttp_pydantic.injectors import Group
|
||||
from aiohttp_pydantic.oas.typing import r200, r201, r204, r404
|
||||
from aiohttp_pydantic.oas.view import generate_oas
|
||||
|
||||
@@ -76,6 +77,24 @@ class ViewResponseReturnASimpleType(PydanticView):
|
||||
return web.json_response()
|
||||
|
||||
|
||||
async def ensure_content_durability(client):
|
||||
"""
|
||||
Reload the page 2 times to ensure that content is always the same
|
||||
note: pydantic can return a cached dict, if a view updates the dict the
|
||||
output will be incoherent
|
||||
"""
|
||||
response_1 = await client.get("/oas/spec")
|
||||
assert response_1.status == 200
|
||||
assert response_1.content_type == "application/json"
|
||||
content_1 = await response_1.json()
|
||||
|
||||
response_2 = await client.get("/oas/spec")
|
||||
content_2 = await response_2.json()
|
||||
assert content_1 == content_2
|
||||
|
||||
return content_2
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def generated_oas(aiohttp_client, loop) -> web.Application:
|
||||
app = web.Application()
|
||||
@@ -84,20 +103,7 @@ async def generated_oas(aiohttp_client, loop) -> web.Application:
|
||||
app.router.add_view("/simple-type", ViewResponseReturnASimpleType)
|
||||
oas.setup(app)
|
||||
|
||||
client = await aiohttp_client(app)
|
||||
response_1 = await client.get("/oas/spec")
|
||||
assert response_1.content_type == "application/json"
|
||||
assert response_1.status == 200
|
||||
content_1 = await response_1.json()
|
||||
|
||||
# Reload the page to ensure that content is always the same
|
||||
# note: pydantic can return a cached dict, if a view updates
|
||||
# the dict the output will be incoherent
|
||||
response_2 = await client.get("/oas/spec")
|
||||
content_2 = await response_2.json()
|
||||
assert content_1 == content_2
|
||||
|
||||
return content_2
|
||||
return await ensure_content_durability(await aiohttp_client(app))
|
||||
|
||||
|
||||
async def test_generated_oas_should_have_components_schemas(generated_oas):
|
||||
@@ -377,3 +383,29 @@ async def test_generated_view_info_as_title():
|
||||
"info": {"title": "test title", "version": "1.0.0"},
|
||||
"openapi": "3.0.0",
|
||||
}
|
||||
|
||||
|
||||
async def test_use_parameters_group_should_not_impact_the_oas(aiohttp_client):
|
||||
class PetCollectionView1(PydanticView):
|
||||
async def get(self, page: int = 1, page_size: int = 20) -> r200[List[Pet]]:
|
||||
return web.json_response()
|
||||
|
||||
class Pagination(Group):
|
||||
page: int = 1
|
||||
page_size: int = 20
|
||||
|
||||
class PetCollectionView2(PydanticView):
|
||||
async def get(self, pagination: Pagination) -> r200[List[Pet]]:
|
||||
return web.json_response()
|
||||
|
||||
app1 = web.Application()
|
||||
app1.router.add_view("/pets", PetCollectionView1)
|
||||
oas.setup(app1)
|
||||
|
||||
app2 = web.Application()
|
||||
app2.router.add_view("/pets", PetCollectionView2)
|
||||
oas.setup(app2)
|
||||
|
||||
assert await ensure_content_durability(
|
||||
await aiohttp_client(app1)
|
||||
) == await ensure_content_durability(await aiohttp_client(app2))
|
||||
|
||||
@@ -5,6 +5,7 @@ from enum import Enum
|
||||
from aiohttp import web
|
||||
|
||||
from aiohttp_pydantic import PydanticView
|
||||
from aiohttp_pydantic.injectors import Group
|
||||
|
||||
|
||||
class JSONEncoder(json.JSONEncoder):
|
||||
@@ -32,6 +33,31 @@ class ViewWithEnumType(PydanticView):
|
||||
return web.json_response({"format": format}, dumps=JSONEncoder().encode)
|
||||
|
||||
|
||||
class Signature(Group):
|
||||
signature_expired: datetime
|
||||
signature_scope: str = "read"
|
||||
|
||||
@property
|
||||
def expired(self) -> datetime:
|
||||
return self.signature_expired
|
||||
|
||||
@property
|
||||
def scope(self) -> str:
|
||||
return self.signature_scope
|
||||
|
||||
|
||||
class ArticleViewWithSignatureGroup(PydanticView):
|
||||
async def get(
|
||||
self,
|
||||
*,
|
||||
signature: Signature,
|
||||
):
|
||||
return web.json_response(
|
||||
{"expired": signature.expired, "scope": signature.scope},
|
||||
dumps=JSONEncoder().encode,
|
||||
)
|
||||
|
||||
|
||||
async def test_get_article_without_required_header_should_return_an_error_message(
|
||||
aiohttp_client, loop
|
||||
):
|
||||
@@ -134,3 +160,21 @@ async def test_correct_value_to_header_defined_with_str_enum(aiohttp_client, loo
|
||||
assert await resp.json() == {"format": "UMT"}
|
||||
assert resp.status == 200
|
||||
assert resp.content_type == "application/json"
|
||||
|
||||
|
||||
async def test_with_signature_group(aiohttp_client, loop):
|
||||
app = web.Application()
|
||||
app.router.add_view("/article", ArticleViewWithSignatureGroup)
|
||||
|
||||
client = await aiohttp_client(app)
|
||||
resp = await client.get(
|
||||
"/article",
|
||||
headers={
|
||||
"signature_expired": "2020-10-04T18:01:00",
|
||||
"signature.scope": "write",
|
||||
},
|
||||
)
|
||||
|
||||
assert await resp.json() == {"expired": "2020-10-04T18:01:00", "scope": "read"}
|
||||
assert resp.status == 200
|
||||
assert resp.content_type == "application/json"
|
||||
|
||||
@@ -3,6 +3,7 @@ from pydantic import Field
|
||||
from aiohttp import web
|
||||
|
||||
from aiohttp_pydantic import PydanticView
|
||||
from aiohttp_pydantic.injectors import Group
|
||||
|
||||
|
||||
class ArticleView(PydanticView):
|
||||
@@ -23,6 +24,34 @@ class ArticleView(PydanticView):
|
||||
)
|
||||
|
||||
|
||||
class Pagination(Group):
|
||||
page_num: int
|
||||
page_size: int = 20
|
||||
|
||||
@property
|
||||
def num(self) -> int:
|
||||
return self.page_num
|
||||
|
||||
@property
|
||||
def size(self) -> int:
|
||||
return self.page_size
|
||||
|
||||
|
||||
class ArticleViewWithPaginationGroup(PydanticView):
|
||||
async def get(
|
||||
self,
|
||||
with_comments: bool,
|
||||
page: Pagination,
|
||||
):
|
||||
return web.json_response(
|
||||
{
|
||||
"with_comments": with_comments,
|
||||
"page_num": page.num,
|
||||
"page_size": page.size,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def test_get_article_without_required_qs_should_return_an_error_message(
|
||||
aiohttp_client, loop
|
||||
):
|
||||
@@ -158,3 +187,69 @@ async def test_get_article_with_one_value_of_tags_must_be_a_list(aiohttp_client,
|
||||
}
|
||||
assert resp.status == 200
|
||||
assert resp.content_type == "application/json"
|
||||
|
||||
|
||||
async def test_get_article_without_required_field_page(aiohttp_client, loop):
|
||||
app = web.Application()
|
||||
app.router.add_view("/article", ArticleViewWithPaginationGroup)
|
||||
|
||||
client = await aiohttp_client(app)
|
||||
|
||||
resp = await client.get("/article", params={"with_comments": 1})
|
||||
assert await resp.json() == [
|
||||
{
|
||||
"in": "query string",
|
||||
"loc": ["page_num"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
assert resp.status == 400
|
||||
assert resp.content_type == "application/json"
|
||||
|
||||
|
||||
async def test_get_article_with_page(aiohttp_client, loop):
|
||||
app = web.Application()
|
||||
app.router.add_view("/article", ArticleViewWithPaginationGroup)
|
||||
|
||||
client = await aiohttp_client(app)
|
||||
|
||||
resp = await client.get("/article", params={"with_comments": 1, "page_num": 2})
|
||||
assert await resp.json() == {"page_num": 2, "page_size": 20, "with_comments": True}
|
||||
assert resp.status == 200
|
||||
assert resp.content_type == "application/json"
|
||||
|
||||
|
||||
async def test_get_article_with_page_and_page_size(aiohttp_client, loop):
|
||||
app = web.Application()
|
||||
app.router.add_view("/article", ArticleViewWithPaginationGroup)
|
||||
|
||||
client = await aiohttp_client(app)
|
||||
|
||||
resp = await client.get(
|
||||
"/article", params={"with_comments": 1, "page_num": 1, "page_size": 10}
|
||||
)
|
||||
assert await resp.json() == {"page_num": 1, "page_size": 10, "with_comments": True}
|
||||
assert resp.status == 200
|
||||
assert resp.content_type == "application/json"
|
||||
|
||||
|
||||
async def test_get_article_with_page_and_wrong_page_size(aiohttp_client, loop):
|
||||
app = web.Application()
|
||||
app.router.add_view("/article", ArticleViewWithPaginationGroup)
|
||||
|
||||
client = await aiohttp_client(app)
|
||||
|
||||
resp = await client.get(
|
||||
"/article", params={"with_comments": 1, "page_num": 1, "page_size": "large"}
|
||||
)
|
||||
assert await resp.json() == [
|
||||
{
|
||||
"in": "query string",
|
||||
"loc": ["page_size"],
|
||||
"msg": "value is not a valid integer",
|
||||
"type": "type_error.integer",
|
||||
}
|
||||
]
|
||||
assert resp.status == 400
|
||||
assert resp.content_type == "application/json"
|
||||
|
||||
Reference in New Issue
Block a user