Initial commit: OCA Technical packages (595 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:03 +02:00
commit 2cc02aac6e
24950 changed files with 2318079 additions and 0 deletions

View file

@ -0,0 +1,2 @@
from . import test_fastapi
from . import test_fastapi_demo

View file

@ -0,0 +1,164 @@
# Copyright 2023 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL).
import logging
from contextlib import contextmanager
from functools import partial
from typing import Any, Callable, Dict
from starlette import status
from starlette.requests import Request
from starlette.responses import JSONResponse, Response
from odoo.api import Environment
from odoo.tests import tagged
from odoo.tests.common import TransactionCase
from odoo.addons.base.models.res_partner import Partner
from odoo.addons.base.models.res_users import Users
from fastapi import APIRouter, FastAPI
from fastapi.testclient import TestClient
from ..context import odoo_env_ctx
from ..dependencies import (
authenticated_partner_impl,
optionally_authenticated_partner_impl,
)
from ..error_handlers import convert_exception_to_status_body
_logger = logging.getLogger(__name__)
def default_exception_handler(request: Request, exc: Exception) -> Response:
"""
Default exception handler that returns a response with the exception details.
"""
status_code, body = convert_exception_to_status_body(exc)
if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
# In testing we want to see the exception details of 500 errors
_logger.error("[%d] Error occurred: %s", exc_info=exc)
return JSONResponse(
status_code=status_code,
content=body,
)
@tagged("post_install", "-at_install")
class FastAPITransactionCase(TransactionCase):
"""
This class is a base class for FastAPI tests.
It defines default values for the attributes used to create the test client.
The default values can be overridden by setting the corresponding class attributes.
Default attributes are:
- default_fastapi_app: the FastAPI app to use to create the test client
- default_fastapi_router: the FastAPI router to use to create the test client
- default_fastapi_odoo_env: the Odoo environment that will be used to run
the endpoint implementation
- default_fastapi_running_user: the user that will be used to run the endpoint
implementation
- default_fastapi_authenticated_partner: the partner that will be used to run
to build the authenticated_partner and authenticated_partner_env dependencies
- default_fastapi_dependency_overrides: a dict of dependency overrides that will
be applied to the app when creating the test client
The test client is created by calling the _create_test_client method. When
calling this method, the default values are used unless they are overridden by
passing the corresponding arguments.
Even if you can provide a default value for the default_fastapi_app and
default_fastapi_router attributes, you should always provide only one of them.
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
cls.default_fastapi_app: FastAPI | None = None
cls.default_fastapi_router: APIRouter | None = None
cls.default_fastapi_odoo_env: Environment = cls.env
cls.default_fastapi_running_user: Users | None = None
cls.default_fastapi_authenticated_partner: Partner | None = None
cls.default_fastapi_dependency_overrides: Dict[
Callable[..., Any], Callable[..., Any]
] = {}
@contextmanager
def _create_test_client(
self,
app: FastAPI | None = None,
router: APIRouter | None = None,
user: Users | None = None,
partner: Partner | None = None,
env: Environment = None,
dependency_overrides: Dict[Callable[..., Any], Callable[..., Any]] = None,
raise_server_exceptions: bool = True,
testclient_kwargs=None,
):
"""
Create a test client for the given app or router.
This method is a context manager that yields the test client. It
ensures that the Odoo environment is properly set up when running
the endpoint implementation, and cleaned up after the test client is
closed.
Pay attention to the **'raise_server_exceptions'** argument. It's
default value is **True**. This means that if the endpoint implementation
raises an exception, the test client will raise it. That also means
that if you app includes specific exception handlers, they will not
be called. If you want to test your exception handlers, you should
set this argument to **False**. In this case, the test client will
not raise the exception, but will return it in the response and the
exception handlers will be called.
"""
env = env or self.default_fastapi_odoo_env
user = user or self.default_fastapi_running_user
dependencies = self.default_fastapi_dependency_overrides.copy()
if dependency_overrides:
dependencies.update(dependency_overrides)
if user:
env = env(user=user)
partner = (
partner
or self.default_fastapi_authenticated_partner
or self.env["res.partner"]
)
if partner and authenticated_partner_impl in dependencies:
raise ValueError(
"You cannot provide an override for the authenticated_partner_impl "
"dependency when creating a test client with a partner."
)
if partner or authenticated_partner_impl not in dependencies:
dependencies[authenticated_partner_impl] = partial(lambda a: a, partner)
if partner and optionally_authenticated_partner_impl in dependencies:
raise ValueError(
"You cannot provide an override for the optionally_authenticated_partner_impl "
"dependency when creating a test client with a partner."
)
if partner or optionally_authenticated_partner_impl not in dependencies:
dependencies[optionally_authenticated_partner_impl] = partial(
lambda a: a, partner
)
app = app or self.default_fastapi_app or FastAPI()
router = router or self.default_fastapi_router
if router:
app.include_router(router)
app.dependency_overrides = dependencies
if not raise_server_exceptions:
# Handle exceptions as in FastAPIDispatcher
app.exception_handlers.setdefault(Exception, default_exception_handler)
ctx_token = odoo_env_ctx.set(env)
testclient_kwargs = testclient_kwargs or {}
try:
yield TestClient(
app,
raise_server_exceptions=raise_server_exceptions,
**testclient_kwargs
)
finally:
odoo_env_ctx.reset(ctx_token)

View file

@ -0,0 +1,201 @@
# Copyright 2022 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL).
import os
import unittest
from contextlib import contextmanager
from odoo import sql_db
from odoo.tests.common import HttpCase
from odoo.tools import mute_logger
from fastapi import status
from ..schemas import DemoExceptionType
@unittest.skipIf(os.getenv("SKIP_HTTP_CASE"), "EndpointHttpCase skipped")
class FastAPIHttpCase(HttpCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.fastapi_demo_app = cls.env.ref("fastapi.fastapi_endpoint_demo")
cls.fastapi_multi_demo_app = cls.env.ref(
"fastapi.fastapi_endpoint_multislash_demo"
)
cls.fastapi_apps = cls.fastapi_demo_app + cls.fastapi_multi_demo_app
cls.fastapi_apps._handle_registry_sync()
lang = (
cls.env["res.lang"]
.with_context(active_test=False)
.search([("code", "=", "fr_BE")])
)
lang.active = True
@contextmanager
def _mocked_commit(self):
with unittest.mock.patch.object(
sql_db.TestCursor, "commit", return_value=None
) as mocked_commit:
yield mocked_commit
def _assert_expected_lang(self, accept_language, expected_lang):
route = "/fastapi_demo/demo/lang"
response = self.url_open(route, headers={"Accept-language": accept_language})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, expected_lang)
def test_call(self):
route = "/fastapi_demo/demo/"
response = self.url_open(route)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, b'{"Hello":"World"}')
def test_lang(self):
self._assert_expected_lang("fr,en;q=0.7,en-GB;q=0.3", b'"fr_BE"')
self._assert_expected_lang("en,fr;q=0.7,en-GB;q=0.3", b'"en_US"')
self._assert_expected_lang("fr-FR,en;q=0.7,en-GB;q=0.3", b'"fr_BE"')
self._assert_expected_lang("fr-FR;q=0.1,en;q=1.0,en-GB;q=0.8", b'"en_US"')
def test_retrying(self):
"""Test that the retrying mechanism is working as expected with the
FastAPI endpoints.
"""
nbr_retries = 3
route = f"/fastapi_demo/demo/retrying?nbr_retries={nbr_retries}"
response = self.url_open(route, timeout=20)
self.assertEqual(response.status_code, 200)
self.assertEqual(int(response.content), nbr_retries)
def test_retrying_post(self):
"""Test that the retrying mechanism is working as expected with the
FastAPI endpoints in case of POST request with a file.
"""
nbr_retries = 3
route = f"/fastapi_demo/demo/retrying?nbr_retries={nbr_retries}"
response = self.url_open(
route, timeout=20, files={"file": ("test.txt", b"test")}
)
self.assertEqual(response.status_code, 200)
self.assertDictEqual(response.json(), {"retries": nbr_retries, "file": "test"})
@mute_logger("odoo.http")
def assert_exception_processed(
self,
exception_type: DemoExceptionType,
error_message: str,
expected_message: str,
expected_status_code: int,
) -> None:
with self._mocked_commit() as mocked_commit:
route = (
"/fastapi_demo/demo/exception?"
f"exception_type={exception_type.value}&error_message={error_message}"
)
response = self.url_open(route, timeout=200)
mocked_commit.assert_not_called()
self.assertDictEqual(
response.json(),
{
"detail": expected_message,
},
)
self.assertEqual(response.status_code, expected_status_code)
def test_user_error(self) -> None:
self.assert_exception_processed(
exception_type=DemoExceptionType.user_error,
error_message="test",
expected_message="test",
expected_status_code=status.HTTP_400_BAD_REQUEST,
)
def test_validation_error(self) -> None:
self.assert_exception_processed(
exception_type=DemoExceptionType.validation_error,
error_message="test",
expected_message="test",
expected_status_code=status.HTTP_400_BAD_REQUEST,
)
def test_bare_exception(self) -> None:
self.assert_exception_processed(
exception_type=DemoExceptionType.bare_exception,
error_message="test",
expected_message="Internal Server Error",
expected_status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
def test_access_error(self) -> None:
self.assert_exception_processed(
exception_type=DemoExceptionType.access_error,
error_message="test",
expected_message="AccessError",
expected_status_code=status.HTTP_403_FORBIDDEN,
)
def test_missing_error(self) -> None:
self.assert_exception_processed(
exception_type=DemoExceptionType.missing_error,
error_message="test",
expected_message="MissingError",
expected_status_code=status.HTTP_404_NOT_FOUND,
)
def test_http_exception(self) -> None:
self.assert_exception_processed(
exception_type=DemoExceptionType.http_exception,
error_message="test",
expected_message="test",
expected_status_code=status.HTTP_409_CONFLICT,
)
@mute_logger("odoo.http")
def test_request_validation_error(self) -> None:
with self._mocked_commit() as mocked_commit:
route = "/fastapi_demo/demo/exception?exception_type=BAD&error_message="
response = self.url_open(route, timeout=200)
mocked_commit.assert_not_called()
self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY)
def test_no_commit_on_exception(self) -> None:
# this test check that the way we mock the cursor is working as expected
# and that the transaction is rolled back in case of exception.
with self._mocked_commit() as mocked_commit:
url = "/fastapi_demo/demo"
response = self.url_open(url, timeout=600)
self.assertEqual(response.status_code, 200)
mocked_commit.assert_called_once()
self.assert_exception_processed(
exception_type=DemoExceptionType.http_exception,
error_message="test",
expected_message="test",
expected_status_code=status.HTTP_409_CONFLICT,
)
def test_url_matching(self):
# Test the URL mathing method on the endpoint
paths = ["/fastapi", "/fastapi_demo", "/fastapi/v1"]
EndPoint = self.env["fastapi.endpoint"]
self.assertEqual(
EndPoint._find_first_matching_url_path(paths, "/fastapi_demo/test"),
"/fastapi_demo",
)
self.assertEqual(
EndPoint._find_first_matching_url_path(paths, "/fastapi/test"), "/fastapi"
)
self.assertEqual(
EndPoint._find_first_matching_url_path(paths, "/fastapi/v2/test"),
"/fastapi",
)
self.assertEqual(
EndPoint._find_first_matching_url_path(paths, "/fastapi/v1/test"),
"/fastapi/v1",
)
def test_multi_slash(self):
route = "/fastapi/demo-multi/demo/"
response = self.url_open(route, timeout=20)
self.assertEqual(response.status_code, 200)
self.assertIn(self.fastapi_multi_demo_app.root_path, str(response.url))

View file

@ -0,0 +1,111 @@
# Copyright 2022 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/LGPL).
from functools import partial
from requests import Response
from odoo.exceptions import UserError
from odoo.tools.misc import mute_logger
from fastapi import status
from ..dependencies import fastapi_endpoint
from ..routers import demo_router
from ..schemas import DemoEndpointAppInfo, DemoExceptionType
from .common import FastAPITransactionCase
class FastAPIDemoCase(FastAPITransactionCase):
"""The fastapi lib comes with a useful testclient that let's you
easily test your endpoints. Moreover, the dependency overrides functionality
allows you to provide specific implementation for part of the code to avoid
to rely on some tricky http stuff for example: authentication
This test class is an example on how you can test your own code
"""
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
cls.default_fastapi_router = demo_router
cls.default_fastapi_running_user = cls.env.ref("fastapi.my_demo_app_user")
cls.default_fastapi_authenticated_partner = cls.env["res.partner"].create(
{"name": "FastAPI Demo"}
)
def test_hello_world(self) -> None:
with self._create_test_client() as test_client:
response: Response = test_client.get("/demo/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertDictEqual(response.json(), {"Hello": "World"})
def test_who_ami(self) -> None:
with self._create_test_client() as test_client:
response: Response = test_client.get("/demo/who_ami")
self.assertEqual(response.status_code, status.HTTP_200_OK)
partner = self.default_fastapi_authenticated_partner
self.assertDictEqual(
response.json(),
{
"name": partner.name,
"display_name": partner.display_name,
},
)
def test_endpoint_info(self) -> None:
demo_app = self.env.ref("fastapi.fastapi_endpoint_demo")
with self._create_test_client(
dependency_overrides={fastapi_endpoint: partial(lambda a: a, demo_app)}
) as test_client:
response: Response = test_client.get("/demo/endpoint_app_info")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertDictEqual(
response.json(),
DemoEndpointAppInfo.model_validate(demo_app).model_dump(by_alias=True),
)
def test_exception_raised(self) -> None:
with self.assertRaisesRegex(UserError, "User Error"):
with self._create_test_client() as test_client:
test_client.get(
"/demo/exception",
params={
"exception_type": DemoExceptionType.user_error.value,
"error_message": "User Error",
},
)
with self.assertRaisesRegex(NotImplementedError, "Bare Exception"):
with self._create_test_client() as test_client:
test_client.get(
"/demo/exception",
params={
"exception_type": DemoExceptionType.bare_exception.value,
"error_message": "Bare Exception",
},
)
@mute_logger("odoo.addons.fastapi.tests.common")
def test_exception_not_raised(self) -> None:
with self._create_test_client(raise_server_exceptions=False) as test_client:
response: Response = test_client.get(
"/demo/exception",
params={
"exception_type": DemoExceptionType.user_error.value,
"error_message": "User Error",
},
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertDictEqual(response.json(), {"detail": "User Error"})
with self._create_test_client(raise_server_exceptions=False) as test_client:
response: Response = test_client.get(
"/demo/exception",
params={
"exception_type": DemoExceptionType.bare_exception.value,
"error_message": "Bare Exception",
},
)
self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR)
self.assertDictEqual(response.json(), {"detail": "Internal Server Error"})