Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ta#68861 [16.0][MIG] test_http_request #175

Merged
merged 25 commits into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
56a6c36
[16.0][MIG] base_extended_security
lanto-razafindrabe Jul 4, 2024
617466e
resolve conflict
abenzbiria Aug 16, 2024
b1d888f
resolve conflict
abenzbiria Aug 26, 2024
546d995
[16.0][MIG] test_http_request
abenzbiria Aug 26, 2024
2f1c1ef
[16.0][MIG] test_http_request
abenzbiria Aug 26, 2024
90c2fb3
[16.0][MIG] test_http_request
abenzbiria Aug 26, 2024
4ee4d9f
[16.0][MIG] test_http_request
abenzbiria Aug 26, 2024
b357192
[16.0][MIG] test_http_request
abenzbiria Aug 26, 2024
ddcf971
[16.0][MIG] test_http_request
abenzbiria Aug 26, 2024
6b5e1fc
[16.0][MIG] test_http_request
abenzbiria Aug 26, 2024
48c41fe
[16.0][MIG] test_http_request
abenzbiria Aug 26, 2024
fb213fa
[16.0][MIG] test_http_request
abenzbiria Aug 26, 2024
2d5cc64
[16.0][MIG] test_http_request
abenzbiria Aug 26, 2024
450760b
[16.0][MIG] test_http_request
abenzbiria Aug 27, 2024
12bdc5b
[16.0][MIG] test_http_request
abenzbiria Aug 27, 2024
8085912
[16.0][MIG] test_http_request
abenzbiria Aug 27, 2024
12efe68
[16.0][MIG] test_http_request
abenzbiria Aug 27, 2024
acfc8a0
[16.0][MIG] test_http_request
abenzbiria Aug 27, 2024
6635cdf
[16.0][MIG] test_http_request
abenzbiria Aug 27, 2024
091e263
[16.0][MIG] test_http_request
abenzbiria Aug 27, 2024
0e9da45
[16.0][MIG] test_http_request
abenzbiria Aug 27, 2024
8ca814b
TESTS
abenzbiria Sep 3, 2024
5183856
[16.0][MIG] test_http_request
abenzbiria Sep 3, 2024
b11d335
[16.0][MIG] test_http_request
abenzbiria Sep 3, 2024
16b40aa
[16.0][MIG] test_http_request
abenzbiria Sep 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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).

{

Check warning on line 4 in test_http_request/__manifest__.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

test_http_request/__manifest__.py#L4

Statement seems to have no effect
'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)
Loading