oca-technical/odoo-bringout-oca-rest-framework-extendable_fastapi/extendable_fastapi/tests/test_generic_extendable.py
2025-08-29 15:43:03 +02:00

208 lines
8.2 KiB
Python

# Copyright 2023 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from requests import Response
from odoo.tests.common import tagged
from fastapi.exceptions import ResponseValidationError
from .common import FastAPITransactionCase
from .routers import demo_pydantic_router
from .schemas import PrivateCustomer, PrivateUser, User
@tagged("post_install", "-at_install")
class TestUser(FastAPITransactionCase):
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
def test_app_components(self):
with self._create_test_client(router=demo_pydantic_router) as test_client:
to_openapi = test_client.app.openapi()
# Check post input and output types
self.assertEqual(
to_openapi["paths"]["/post_user"]["post"]["requestBody"]["content"][
"application/json"
]["schema"]["$ref"],
"#/components/schemas/User",
)
self.assertEqual(
to_openapi["paths"]["/post_user"]["post"]["responses"]["200"][
"content"
]["application/json"]["schema"]["$ref"],
"#/components/schemas/UserSearchResponse",
)
self.assertEqual(
to_openapi["paths"]["/post_private_user"]["post"]["requestBody"][
"content"
]["application/json"]["schema"]["$ref"],
"#/components/schemas/PrivateUser",
)
self.assertEqual(
to_openapi["paths"]["/post_private_user"]["post"]["responses"]["200"][
"content"
]["application/json"]["schema"]["$ref"],
"#/components/schemas/User",
)
self.assertEqual(
to_openapi["paths"]["/post_private_user_generic"]["post"][
"requestBody"
]["content"]["application/json"]["schema"]["$ref"],
"#/components/schemas/PrivateUser",
)
self.assertEqual(
to_openapi["paths"]["/post_private_user_generic"]["post"]["responses"][
"200"
]["content"]["application/json"]["schema"]["$ref"],
"#/components/schemas/UserSearchResponse",
)
# Check Pydantic model extension
self.assertEqual(
set(to_openapi["components"]["schemas"]["User"]["properties"].keys()),
{"name", "address"},
)
self.assertEqual(
set(
to_openapi["components"]["schemas"]["PrivateUser"][
"properties"
].keys()
),
{"name", "address", "password"},
)
self.assertEqual(
to_openapi["components"]["schemas"]["UserSearchResponse"]["properties"][
"items"
]["items"]["$ref"],
"#/components/schemas/User",
)
def test_post_user(self):
name = "Jean Dupont"
address = "Rue du Puits 12, 4000 Liège"
pydantic_data = User(name=name, address=address)
# Assert that class was correctly extended
self.assertTrue(pydantic_data.address)
with self._create_test_client(router=demo_pydantic_router) as test_client:
response: Response = test_client.post(
"/post_user", content=pydantic_data.model_dump_json()
)
self.assertEqual(response.status_code, 200)
res = response.json()
self.assertEqual(res["total"], 1)
user = res["items"][0]
self.assertEqual(user["name"], name)
self.assertEqual(user["address"], address)
self.assertFalse("password" in user.keys())
def test_post_private_user(self):
"""
/post_private_user return attributes from User, but not PrivateUser
Security check: this method should never return attributes from
derived type PrivateUser, even thought a PrivateUser object
is given as input.
"""
name = "Jean Dupont"
address = "Rue du Puits 12, 4000 Liège"
password = "dummy123"
pydantic_data = PrivateUser(name=name, address=address, password=password)
# Assert that class was correctly extended
self.assertTrue(pydantic_data.address)
self.assertTrue(pydantic_data.password)
with self._create_test_client(router=demo_pydantic_router) as test_client:
response: Response = test_client.post(
"/post_private_user", content=pydantic_data.model_dump_json()
)
self.assertEqual(response.status_code, 200)
user = response.json()
self.assertEqual(user["name"], name)
self.assertEqual(user["address"], address)
# Private attrs were not returned
self.assertFalse("password" in user.keys())
def test_post_private_user_generic(self):
"""
/post_private_user_generic return attributes from User, but not PrivateUser
Security check: this method should never return attributes from
derived type PrivateUser, even thought a PrivateUser object
is given as input.
This test is specifically made to test this assertion with generics.
"""
name = "Jean Dupont"
address = "Rue du Puits 12, 4000 Liège"
password = "dummy123"
pydantic_data = PrivateUser(name=name, address=address, password=password)
# Assert that class was correctly extended
self.assertTrue(pydantic_data.address)
self.assertTrue(pydantic_data.password)
with self._create_test_client(router=demo_pydantic_router) as test_client:
response: Response = test_client.post(
"/post_private_user_generic", content=pydantic_data.model_dump_json()
)
self.assertEqual(response.status_code, 200)
res = response.json()
self.assertEqual(res["total"], 1)
user = res["items"][0]
self.assertEqual(user["name"], name)
self.assertEqual(user["address"], address)
# Private attrs were not returned
self.assertFalse("password" in user.keys())
def test_get_user_failed_no_address(self):
"""
Try to get a specific user but having no address
-> Error because address is a required field on User (extended) class
:return:
"""
user = self.env["res.users"].create(
{
"name": "Michel Dupont",
"login": "michel",
}
)
with self._create_test_client(
router=demo_pydantic_router
) as test_client, self.assertRaises(ResponseValidationError):
test_client.get(f"/{user.id}")
def test_get_user_failed_no_pwd(self):
"""
Try to get a specific user having an address but no password.
-> No error because return type is User, not PrivateUser
:return:
"""
user = self.env["res.users"].create(
{
"name": "Michel Dupont",
"login": "michel",
"street": "Rue du Moulin",
}
)
self.assertFalse(user.password)
with self._create_test_client(router=demo_pydantic_router) as test_client:
response: Response = test_client.get(f"/private/{user.id}")
self.assertEqual(response.status_code, 200)
def test_extra_forbid_response_fails(self):
"""
If adding extra="forbid" to the User model, we cannot write
a router with a response type = User and returning PrivateUser
in the code
"""
name = "Jean Dupont"
address = "Rue du Puits 12, 4000 Liège"
password = "dummy123"
pydantic_data = PrivateCustomer(name=name, address=address, password=password)
with self.assertRaises(ResponseValidationError), self._create_test_client(
router=demo_pydantic_router
) as test_client:
test_client.post(
"/post_private_customer", content=pydantic_data.model_dump_json()
)