Skip to content

Commit

Permalink
Ta#68861 [16.0][MIG] test_http_request (#175)
Browse files Browse the repository at this point in the history
Co-authored-by: Lanto Razafindrabe <[email protected]>
  • Loading branch information
abenzbiria and lanto-razafindrabe authored Sep 3, 2024
1 parent fe4d7cb commit 9464678
Show file tree
Hide file tree
Showing 9 changed files with 315 additions and 0 deletions.
1 change: 1 addition & 0 deletions .docker_files/main/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"attachment_minio",
"lang_fr_activated",
"mail_template_default",
"test_http_request",
],
"installable": True,
}
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ COPY admin_light_calendar /mnt/extra-addons/admin_light_calendar
COPY attachment_minio /mnt/extra-addons/attachment_minio
COPY lang_fr_activated /mnt/extra-addons/lang_fr_activated
COPY mail_template_default /mnt/extra-addons/mail_template_default
COPY test_http_request /mnt/extra-addons/test_http_request

COPY .docker_files/main /mnt/extra-addons/main
COPY .docker_files/odoo.conf /etc/odoo
50 changes: 50 additions & 0 deletions test_http_request/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
Test HTTP Request
-----------------
A technical Odoo module to help testing HTTP requests/controllers.

This module does not need to be in the dependencies of other modules that use it.
However, if installed, it will have no impact on a production instance.

Context
-------
Testing HTTP controllers in Odoo is not a trivial task.
Therefore, many developpers do not automate the tests of their controllers.

This modules intends to offer tools to make the task easier.

Usage
-----

Mocking HTTP Requests
~~~~~~~~~~~~~~~~~~~~~
The first tool this module provides is a way to mock Odoo HTTP requests.

In your module, you may use the ``mock_odoo_request`` to simulate the request:

.. code-block:: python
from odoo.addons.test_http_request.common import mock_odoo_request
from odoo.tests.common import TransactionCase
from ..controllers.main import MyController
class TestSomething(TransactionCase):
def test_some_method(self):
with mock_odoo_request(self.env):
result = MyController().do_something()
assert result == ...
The ``mock_odoo_request`` function is a context manager.
Behind the scene, when calling it, the following technical work is done:

* An HTTP session is created.
* An environ is created.
* An httprequest object is added to the werkzeug stack.
* The httprequest object is bound to the given ``Odoo Environment`` object.

Contributors
------------
* Numigi (tm) and all its contributors (https://bit.ly/numigiens)
4 changes: 4 additions & 0 deletions test_http_request/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# © 2023 Numigi (tm) and all its contributors (https://bit.ly/numigiens)
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).

from . import tests
16 changes: 16 additions & 0 deletions test_http_request/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# © 2023 Numigi (tm) and all its contributors (https://bit.ly/numigiens)
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).

{
'name': 'Test HTTP Request',
'version': '16.0.1.0.0',
'author': 'Numigi',
'maintainer': 'Numigi',
'license': 'LGPL-3',
'category': 'Other',
'summary': 'Technical module for testing http requests.',
'depends': [
'base',
],
'installable': True,
}
174 changes: 174 additions & 0 deletions test_http_request/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# © 2023 Numigi (tm) and all its contributors (https://bit.ly/numigiens)
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).

import json
import werkzeug
from contextlib import contextmanager
from io import BytesIO
from odoo.addons.http_routing.models.ir_http import url_for
from odoo.api import Environment
from odoo.http import HttpDispatcher, JsonRPCDispatcher, _request_stack, Session
from odoo.tools import config
from typing import Optional, Union
from odoo.http import FilesystemSessionStore
from werkzeug.datastructures import ImmutableOrderedMultiDict
from werkzeug.test import EnvironBuilder
from werkzeug.urls import url_encode
from werkzeug.wrappers.request import Request


class _MockOdooRequestMixin:

@staticmethod
def redirect(url, code=302):
"""Add the `redirect` method to the request.
This method is added programatically by the module http_routing:
odoo/addons/http_routing/models/ir_http.py (method _dispatch)
"""
return werkzeug.utils.redirect(url_for(url), code)

@property
def website(self):
"""Add the `website` property to the request.
The attribute is added programatically by the module website:
odoo/addons/website/models/ir_http.py (method _add_dispatch_parameters)
The attribute is added as a property so that the request object
can be used even if the module website is not installed.
"""
return self.env['website'].get_current_website()

def __exit__(self, exc_type, exc_value, traceback):
"""Prevent commiting the transaction when exiting the HTTP request.
Since the request uses the same cursor as the test fixture,
the cursor must not be commited when exiting the request.
"""
_request_stack.pop()

def __enter__(self):
"""Push the request to the request stack."""
_request_stack.push(self)
return self


class _MockOdooHttpRequest(_MockOdooRequestMixin, HttpDispatcher):
pass


class _MockOdooJsonRequest(_MockOdooRequestMixin, JsonRPCDispatcher):
pass


def _make_environ_form_data_stream(data: dict) -> BytesIO:
"""Make the form data stream for an Odoo http request.
Odoo uses ImmutableOrderedMultiDict to store url encoded form
data instead of the default ImmutableMultiDict in werkzeug.
The test utility :class:`werkzeug.test.EnvironBuilder` uses
MultiDict to store form data.
This must be ajusted in order reproduce properly the behavior of Odoo.
"""
encoded_data = url_encode(data).encode("ascii")
return BytesIO(encoded_data)


def _make_environ(
method: str = 'POST',
headers: Optional[dict] = None,
data: Optional[dict] = None,
routing_type: str = 'http',
):
"""Make an environ for the given request parameters."""
assert routing_type in ('http', 'json')
environ_builder = EnvironBuilder(
method=method,
data=json.dumps(data or {}) if routing_type == 'json' else data,
headers=headers,
content_type=(
'application/json' if routing_type == 'json' else
'application/x-www-form-urlencoded'
)
)
environ = environ_builder.get_environ()

if routing_type == 'http' and data:
environ['wsgi.input'] = _make_environ_form_data_stream(data)
return environ


def _set_request_storage_class(httprequest: Request):
"""Set the data structure used to store form data.
This is done in the method Root.dispatch of odoo/http.py.
"""
httprequest.parameter_storage_class = ImmutableOrderedMultiDict


def _make_werkzeug_request(environ: dict) -> Request:
"""Make a werkzeug request from the given environ."""
httprequest = Request(environ)
_set_request_storage_class(httprequest)
return httprequest


def _make_filesystem_session(env: Environment) -> Session:
session_store = FilesystemSessionStore(
config.session_dir, session_class=Session, renew_missing=True)
session = session_store.new()
session.db = env.cr.dbname
session.uid = env.uid
session.context = env.context
return session


def _make_odoo_request(
werkzeug_request: Request, env: Environment, routing_type: str
) -> Union[_MockOdooHttpRequest, _MockOdooJsonRequest]:
"""Make an Odoo request from the given werkzeug request."""
odoo_request_cls = (
_MockOdooJsonRequest if routing_type == 'json' else
_MockOdooHttpRequest
)
odoo_request = odoo_request_cls(werkzeug_request)
odoo_request._env = env
odoo_request._cr = env.cr
odoo_request._uid = env.uid
odoo_request._context = env.context
return odoo_request


@contextmanager
def mock_odoo_request(
env: Environment,
method: str = 'POST',
headers: Optional[dict] = None,
data: Optional[dict] = None,
routing_type: str = 'http',
):
"""Mock an Odoo HTTP request.
This methods builds an HTTPRequest object and adds it to
the local stack of the application.
The Odoo environment of the test fixture is injected to the
request so that objects created in the fixture are available
for controllers.
:param env: the odoo environment to bind with the request.
:param method: the HTTP method called during the request.
:param headers: the request headers.
:param data: an optional dict to be serialized as json or url-encoded data.
:param routing_type: whether to use an http (x-www-form-urlencoded) or json request.
"""
environ = _make_environ(method, headers, data, routing_type)
werkzeug_request = _make_werkzeug_request(environ)
werkzeug_request.session = _make_filesystem_session(env)
odoo_request = _make_odoo_request(werkzeug_request, env, routing_type)

with odoo_request:
yield odoo_request
Binary file added test_http_request/static/description/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions test_http_request/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# © 2023 Numigi (tm) and all its contributors (https://bit.ly/numigiens)
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).

from . import test_mock_http_request
65 changes: 65 additions & 0 deletions test_http_request/tests/test_mock_http_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# © 2023 Numigi (tm) and all its contributors (https://bit.ly/numigiens)
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).

import json
from collections import OrderedDict
from ddt import data, ddt, unpack
from odoo.http import request
from odoo.tests import common
from werkzeug.datastructures import ImmutableMultiDict
from werkzeug.urls import url_encode
from ..common import mock_odoo_request


@ddt
class TestMockHttpRequest(common.TransactionCase):

def setUp(self):
super().setUp()
self.data = OrderedDict([
('firstname', 'John'),
('lastname', 'Doe'),
])

def test_env_propagated_to_request(self):
with mock_odoo_request(self.env, data=self.data):
assert request._env == self.env

def test_method_propagated_to_request(self):
method = 'PATCH'
with mock_odoo_request(self.env, data=self.data, method=method):
assert request.request.method == method

def test_headers_propagated_to_request(self):
header_key = 'Some-Header'
header_value = 'some value'
headers = {
header_key: header_value,
}
with mock_odoo_request(self.env, data=self.data, headers=headers):
assert request.request.headers[header_key] == header_value

@data(
('http', 'application/x-www-form-urlencoded'),
('json', 'application/json'),
)
@unpack
def test_content_type(self, routing_type, content_type):
with mock_odoo_request(self.env, data=self.data, routing_type=routing_type):
assert request.request.content_type == content_type

def test_if_http_routing__data_contained_in_request_form(self):
with mock_odoo_request(self.env, data=self.data):
assert request.request.form == ImmutableMultiDict(self.data)
assert not request.request.data

def test_if_json_routing__data_contained_in_request_data(self):
json_data = json.dumps(self.data).encode()
with mock_odoo_request(self.env, data=self.data, routing_type='json'):
assert not request.request.form
assert request.request.data == json_data

def test_form_data_propagated_with_correct_key_order(self):
data = OrderedDict([(str(i), str(i)) for i in range(10)])
with mock_odoo_request(self.env, data=data):
assert url_encode(request.request.form) == url_encode(data)

0 comments on commit 9464678

Please sign in to comment.