-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Ta#68861 [16.0][MIG] test_http_request (#175)
Co-authored-by: Lanto Razafindrabe <[email protected]>
- Loading branch information
1 parent
fe4d7cb
commit 9464678
Showing
9 changed files
with
315 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |