[ADD] test_http: Core HTTP test suite

Backport from the master-httpocalypse-juc test suite in order to observe
the perf of the master branch and to compare them with the httpocalypse.

closes odoo/odoo#84156

Task: 2571224
Signed-off-by: Raphael Collet <rco@odoo.com>
This commit is contained in:
Julien Castiaux
2022-02-08 14:53:48 +00:00
parent c7de4ff68c
commit ce12c52a5d
12 changed files with 757 additions and 1 deletions
+2
View File
@@ -0,0 +1,2 @@
from . import controllers
from . import models
+16
View File
@@ -0,0 +1,16 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Test HTTP',
'version': '1.0',
'category': 'Hidden/Tests',
'description': """A module to test HTTP""",
'depends': ['base', 'web', 'web_tour'],
'installable': True,
'auto_install': False,
'data': [
'data.xml',
'ir.model.access.csv',
'views.xml'
],
'license': 'LGPL-3',
}
+111
View File
@@ -0,0 +1,111 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
import logging
from odoo import http
from odoo.exceptions import UserError
from odoo.http import request
_logger = logging.getLogger(__name__)
CT_JSON = {'Content-Type': 'application/json'}
WSGI_SAFE_KEYS = {'PATH_INFO', 'QUERY_STRING', 'RAW_URI', 'SCRIPT_NAME', 'wsgi.url_scheme'}
class TestHttp(http.Controller):
# =====================================================
# Greeting
# =====================================================
@http.route(['/test_http/greeting', '/test_http/greeting-none'], type='http', auth='none')
def greeting_none(self):
return "Tek'ma'te"
@http.route('/test_http/greeting-public', type='http', auth='public')
def greeting_public(self):
assert request.env.user, "ORM should be initialized"
return "Tek'ma'te"
@http.route('/test_http/greeting-user', type='http', auth='user')
def greeting_user(self):
assert request.env.user, "ORM should be initialized"
return "Tek'ma'te"
@http.route('/test_http/wsgi_environ', type='http', auth='none')
def wsgi_environ(self):
environ = {
key: val for key, val in request.httprequest.environ.items()
if (key.startswith('HTTP_') # headers
or key.startswith('REMOTE_')
or key.startswith('REQUEST_')
or key.startswith('SERVER_')
or key.startswith('werkzeug.proxy_fix.')
or key in WSGI_SAFE_KEYS)
}
return request.make_response(
json.dumps(environ, indent=4),
headers=list(CT_JSON.items())
)
# =====================================================
# Echo-Reply
# =====================================================
@http.route('/test_http/echo-http-get', type='http', auth='none', methods=['GET'])
def echo_http_get(self, **kwargs):
return str(kwargs)
@http.route('/test_http/echo-http-post', type='http', auth='none', methods=['POST'], csrf=False)
def echo_http_post(self, **kwargs):
return str(kwargs)
@http.route('/test_http/echo-http-csrf', type='http', auth='none', methods=['POST'], csrf=True)
def echo_http_csrf(self, **kwargs):
return str(kwargs)
@http.route('/test_http/echo-json', type='json', auth='none', methods=['POST'], csrf=False)
def echo_json(self, **kwargs):
return kwargs
@http.route('/test_http/echo-json-context', type='json', auth='user', methods=['POST'], csrf=False)
def echo_json_context(self, **kwargs):
return request.env.context
# =====================================================
# Models
# =====================================================
@http.route('/test_http/<model("test_http.galaxy"):galaxy>', auth='public')
def galaxy(self, galaxy):
if not galaxy.exists():
raise UserError('The Ancients did not settle there.')
return http.request.render('test_http.tmpl_galaxy', {
'galaxy': galaxy,
'stargates': http.request.env['test_http.stargate'].search([
('galaxy_id', '=', galaxy.id)
]),
})
@http.route('/test_http/<model("test_http.galaxy"):galaxy>/<model("test_http.stargate"):gate>', auth='user')
def stargate(self, galaxy, gate):
if not gate.exists():
raise UserError("The goa'uld destroyed the gate")
return http.request.render('test_http.tmpl_stargate', {
'gate': gate
})
# =====================================================
# Cors
# =====================================================
@http.route('/test_http/cors_http_default', type='http', auth='none', cors='*')
def cors_http(self):
return "Hello"
@http.route('/test_http/cors_http_methods', type='http', auth='none', methods=['GET', 'PUT'], cors='*')
def cors_http_verbs(self, **kwargs):
return "Hello"
@http.route('/test_http/cors_json', type='json', auth='none', cors='*')
def cors_json(self, **kwargs):
return {}
+43
View File
@@ -0,0 +1,43 @@
<odoo>
<data>
<record id="milky_way" model="test_http.galaxy">
<field name="name">Milky Way</field>
</record>
<record id="pegasus" model="test_http.galaxy">
<field name="name">Pegasus</field>
</record>
<record id="earth" model="test_http.stargate">
<field name="name">Earth</field>
<field name="address">sq5Abt</field>
<field name="galaxy_id" ref="test_http.milky_way"/>
</record>
<record id="abydos" model="test_http.stargate">
<field name="name">Abydos</field>
<field name="address">r7fwcu</field>
<field name="galaxy_id" ref="test_http.milky_way"/>
</record>
<record id="dakara" model="test_http.stargate">
<field name="name">Dakara</field>
<field name="address">gs38x4</field>
<field name="galaxy_id" ref="test_http.milky_way"/>
</record>
<record id="lantea" model="test_http.stargate">
<field name="name">Lantea</field>
<field name="address">u6iz7d</field>
<field name="galaxy_id" ref="test_http.pegasus"/>
<field name="has_galaxy_crystal" eval="True"/>
</record>
<record id="athos" model="test_http.stargate">
<field name="name">Athos</field>
<field name="address">mfp2rt</field>
<field name="galaxy_id" ref="test_http.pegasus"/>
</record>
</data>
</odoo>
@@ -0,0 +1,6 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_test_http_stargate_portal,test_http.stargate,model_test_http_stargate,base.group_public,1,0,0,0
access_test_http_stargate_user,test_http.stargate,model_test_http_stargate,base.group_user,1,1,1,1
access_test_http_galaxy_portal,test_http.galaxy,model_test_http_galaxy,base.group_public,1,0,0,0
access_test_http_galaxy_user,test_http.galaxy,model_test_http_galaxy,base.group_user,1,1,0,0
access_test_http_galaxy_system,test_http.galaxy,model_test_http_galaxy,base.group_system,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_test_http_stargate_portal test_http.stargate model_test_http_stargate base.group_public 1 0 0 0
3 access_test_http_stargate_user test_http.stargate model_test_http_stargate base.group_user 1 1 1 1
4 access_test_http_galaxy_portal test_http.galaxy model_test_http_galaxy base.group_public 1 0 0 0
5 access_test_http_galaxy_user test_http.galaxy model_test_http_galaxy base.group_user 1 1 0 0
6 access_test_http_galaxy_system test_http.galaxy model_test_http_galaxy base.group_system 1 1 1 1
+57
View File
@@ -0,0 +1,57 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields, api
MILKY_WAY_REGIONS = ['P3X', 'P4X', 'P2X', 'P5C']
PEGASUS_REGIONS = ['M4R', 'P3Y', 'M6R']
class Stargate(models.Model):
_name = 'test_http.stargate'
_description = 'Stargate'
name = fields.Char(required=True, store=True, compute='_compute_name', readonly=False, help="The stargate/planet common name.")
address = fields.Char(required=True, help="The stargate's 6-glyphs address used by other stargates to dial this one.")
sgc_designation = fields.Char(store=True, compute='_compute_sgc_designation', help="The SGC designation name of this stargate.")
galaxy_id = fields.Many2one('test_http.galaxy', required=True, help="The galaxy where this stargate is.")
has_galaxy_crystal = fields.Boolean(store=True, compute='_compute_has_galaxy_crystal', readonly=False, help="Whether this stargate can dial other galaxies.")
_sql_constraints = [
('address_length', 'CHECK(LENGTH(address) = 6)', "Local addresses have 6 glyphs"),
]
@api.depends('galaxy_id')
def _compute_has_galaxy_crystal(self):
milky_way = self.env.ref('test_http.milky_way')
for gate in self:
gate.has_galaxy_crystal = gate.galaxy_id == milky_way
@api.depends('sgc_designation')
def _compute_name(self):
for gate in self:
if not gate.name:
gate.name = gate.sgc_designation
@api.depends('address')
def _compute_sgc_designation(self):
""" Forge a sgc designation that look like a real one. """
for gate in self:
if gate.galaxy_id.name not in ('Milky Way', 'Pegasus'):
gate.sgc_designation = False
continue
region_part = (
PEGASUS_REGIONS[gate.id % len(PEGASUS_REGIONS)]
if gate.galaxy_id.name == 'Pegasus'
else MILKY_WAY_REGIONS[gate.id % len(MILKY_WAY_REGIONS)]
)
local_part = str(int.from_bytes(gate.address.encode(), 'big'))[:3]
gate.sgc_designation = f'{region_part}-{local_part}'
class Galaxy(models.Model):
_name = 'test_http.galaxy'
_description = 'Galaxy'
name = fields.Char(required=True, help='The galaxy common name.')
Binary file not shown.

After

Width:  |  Height:  |  Size: 814 B

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Creator: CorelDRAW -->
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="63.509mm" height="63.509mm" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
viewBox="0 0 63.509 63.509">
<defs>
<style type="text/css">
<![CDATA[
.fil1 {fill:none}
.fil0 {fill:#1F1A17}
]]>
</style>
</defs>
<g id="Layer_x0020_1">
<metadata id="CorelCorpID_0Corel-Layer"/>
<g id="_150956456">
<path id="_151090152" class="fil0" d="M31.6671 7.4461c4.8933,0 8.861,3.9677 8.861,8.8598 0,4.8932 -3.9677,8.8609 -8.861,8.8609 -4.892,0 -8.8597,-3.9677 -8.8597,-8.8609 0,-4.8921 3.9677,-8.8598 8.8597,-8.8598zm0.0006 4.3597c2.4855,0 4.5007,2.0151 4.5007,4.5007 0,2.4855 -2.0152,4.5006 -4.5007,4.5006 -2.4855,0 -4.5006,-2.0151 -4.5006,-4.5006 0,-2.4856 2.0151,-4.5007 4.5006,-4.5007z"/>
<path id="_150956480" class="fil0" d="M44.2053 53.9278c-2.4381,4.0631 -2.4381,4.0631 -2.4381,4.0631l0 -0.0656 12.9247 0 -23.0242 -31.2605c-23.0047,31.2605 -23.0047,31.2605 -23.0047,31.2605l12.9071 0 0 0.0656c0,0 0,0 -2.4381,-4.0631l-0.0049 0.0067c3.1437,-4.2692 7.2299,-9.8181 12.5406,-17.0292l0 0c5.3108,7.2111 9.3982,12.7594 12.5425,17.0286l-0.0049 -0.0061z"/>
</g>
<rect class="fil1" x="-0.0864984" y="0.170202" width="63.509" height="63.509"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+1
View File
@@ -0,0 +1 @@
from . import test_http
+466
View File
@@ -0,0 +1,466 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
import unittest
from html.parser import HTMLParser
from unittest.mock import patch
from urllib.parse import urlparse
from socket import gethostbyname
import odoo
from odoo.http import WebRequest
from odoo.tests import tagged
from odoo.tests.common import HOST, HttpCase, new_test_user
from odoo.tools import config, file_open, mute_logger
from odoo.tools.func import lazy_property
from odoo.addons.test_http.controllers import CT_JSON
# pylint: disable=W0223(abstract-method)
class HtmlTokenizer(HTMLParser):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.tokens = []
@classmethod
def _attrs_to_str(cls, attrs):
out = []
for key, value in attrs:
out.append(f"{key}={value!r}" if value else key)
return " ".join(out)
def handle_starttag(self, tag, attrs):
self.tokens.append(f"<{tag} {self._attrs_to_str(attrs)}>")
def handle_endtag(self, tag):
self.tokens.append(f"</{tag}>")
def handle_startendtag(self, tag, attrs):
# HTML5 <img> instead of XHTML <img/>
self.handle_starttag(tag, attrs)
def handle_data(self, data):
data = data.strip()
if data:
self.tokens.append(data)
@classmethod
def tokenize(cls, source_str):
"""
Parse the source html into a list of tokens. Only tags and
tags data are conserved, other elements such as comments are
discarded.
"""
tokenizer = cls()
tokenizer.feed(source_str)
return tokenizer.tokens
class TestHttpBase(HttpCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.addClassCleanup(lazy_property.reset_all, odoo.http.root)
cls.classPatch(odoo.conf, 'server_wide_modules', ['base', 'web', 'test_http'])
lazy_property.reset_all(odoo.http.root)
def db_url_open(self, url, *args, allow_redirects=False, **kwargs):
return self.url_open(url, *args, allow_redirects=allow_redirects, **kwargs)
def nodb_url_open(self, url, *args, allow_redirects=False, **kwargs):
with patch('odoo.http.db_list') as db_list, \
patch('odoo.http.db_filter') as db_filter, \
patch('odoo.http.db_monodb') as db_monodb:
db_list.return_value = []
db_filter.return_value = []
db_monodb.return_value = None
return self.url_open(url, *args, allow_redirects=allow_redirects, **kwargs)
@tagged('post_install', '-at_install')
class TestHttpGreeting(TestHttpBase):
def test_greeting0_matrix(self):
new_test_user(self.env, 'jackoneill', context={'lang': 'en_US'})
test_matrix = [
# path, database, login, expected_code, expected_re_pattern
('/test_http/greeting', False, None, 200, r"Tek'ma'te"),
('/test_http/greeting', True, None, 200, r"Tek'ma'te"),
('/test_http/greeting', True, 'public', 200, r"Tek'ma'te"),
('/test_http/greeting', True, 'jackoneill', 200, r"Tek'ma'te"),
('/test_http/greeting-none', False, None, 200, r"Tek'ma'te"),
('/test_http/greeting-none', True, None, 200, r"Tek'ma'te"),
('/test_http/greeting-none', True, 'public', 200, r"Tek'ma'te"),
('/test_http/greeting-none', True, 'jackoneill', 200, r"Tek'ma'te"),
('/test_http/greeting-public', False, None, 404, r"Not Found"),
('/test_http/greeting-public', True, None, 200, r"Tek'ma'te"),
('/test_http/greeting-public', True, 'public', 200, r"Tek'ma'te"),
('/test_http/greeting-public', True, 'jackoneill', 200, r"Tek'ma'te"),
('/test_http/greeting-user', False, None, 404, r"Not Found"),
('/test_http/greeting-user', True, None, 303, r".*/web/login.*"),
('/test_http/greeting-user', True, 'public', 303, r".*/web/login.*"),
('/test_http/greeting-user', True, 'jackoneill', 200, r"Tek'ma'te"),
]
for path, withdb, login, expected_code, expected_pattern in test_matrix:
with self.subTest(path=path, withdb=withdb, login=login):
if withdb:
if login == 'public':
self.authenticate(None, None)
elif login:
self.authenticate(login, login)
res = self.db_url_open(path, allow_redirects=False)
else:
res = self.nodb_url_open(path, allow_redirects=False)
self.assertEqual(res.status_code, expected_code)
self.assertRegex(res.text, expected_pattern)
if withdb and login:
self.logout(keep_db=False)
def test_greeting1_headers_nodb(self):
res = self.nodb_url_open('/test_http/greeting')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers.get('Content-Type'), 'text/html; charset=utf-8')
self.assertEqual(res.text, "Tek'ma'te")
def test_greeting2_headers_db(self):
new_test_user(self.env, 'jackoneill', context={'lang': 'en_US'})
self.authenticate('jackoneill', 'jackoneill')
res = self.db_url_open('/test_http/greeting')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers.get('Content-Type'), 'text/html; charset=utf-8')
self.assertEqual(res.text, "Tek'ma'te")
@tagged('post_install', '-at_install')
class TestHttpStatic(TestHttpBase):
def test_static0_png_image(self):
res = self.nodb_url_open("/test_http/static/src/img/gizeh.png")
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers.get('Content-Length'), '814')
self.assertEqual(res.headers.get('Content-Type'), 'image/png')
cache_control = set(res.headers.get('Cache-Control', '').split(', '))
self.assertEqual(cache_control, {'public', 'max-age=604800'}) # one week
with file_open('test_http/static/src/img/gizeh.png', 'rb') as file:
self.assertEqual(res.content, file.read())
def test_static1_svg_image(self):
res = self.nodb_url_open("/test_http/static/src/img/gizeh.svg")
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers.get('Content-Length'), '1529')
self.assertEqual(res.headers.get('Content-Type'), 'image/svg+xml')
cache_control = set(res.headers.get('Cache-Control', '').split(', '))
self.assertEqual(cache_control, {'public', 'max-age=604800'}) # one week
with file_open('test_http/static/src/img/gizeh.svg', 'rb') as file:
self.assertEqual(res.content, file.read())
def test_static2_not_found(self):
res = self.nodb_url_open("/test_http/static/i-dont-exist")
self.assertEqual(res.status_code, 404)
def test_static3_attachment(self):
with file_open('test_http/static/src/img/gizeh.svg', 'rb') as file:
content = file.read()
attachment = self.env['ir.attachment'].create({
'name': 'point_of_origin.svg',
'type': 'binary',
'raw': content,
'res_model': 'test_http.stargate',
'res_id': self.ref('test_http.earth'),
})
attachment['url'] = f'/test_http/{attachment["checksum"]}'
res = self.db_url_open(attachment['url'])
self.assertEqual(res.headers.get('Content-Length'), '1529')
self.assertEqual(res.headers.get('Content-Type'), 'image/svg+xml; charset=utf-8')
self.assertEqual(res.headers.get('Content-Security-Policy'), "default-src 'none'")
self.assertEqual(res.content, content)
@tagged('post_install', '-at_install')
class TestHttpEchoReplyHttpNoDB(TestHttpBase):
def test_echohttp0_get_qs_nodb(self):
res = self.nodb_url_open('/test_http/echo-http-get?race=Asgard')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.text, "{'race': 'Asgard'}")
def test_echohttp1_get_form_nodb(self):
res = self.nodb_url_open('/test_http/echo-http-get', data={'commander': 'Thor'})
self.assertEqual(res.status_code, 405)
def test_echohttp2_post_qs_nodb(self):
res = self.nodb_url_open('/test_http/echo-http-post?race=Asgard')
self.assertEqual(res.status_code, 405)
def test_echohttp3_post_qs_form_nodb(self):
res = self.nodb_url_open('/test_http/echo-http-post?race=Asgard', data={'commander': 'Thor'})
self.assertEqual(res.status_code, 200)
self.assertEqual(res.text, "{'race': 'Asgard', 'commander': 'Thor'}")
@mute_logger('odoo.http')
def test_echohttp4_post_json_nodb(self):
res = self.nodb_url_open('/test_http/echo-http-post', data='{}', headers=CT_JSON)
self.assertIn("Bad Request", res.text)
@unittest.skip("Broken in master, fixed in httpocalypse.")
def test_echohttp5_post_csrf(self):
res = self.nodb_url_open('/test_http/echo-http-csrf?race=Asgard', data={'commander': 'Thor'})
self.assertEqual(res.status_code, 303)
self.assertEqual(urlparse(res.headers.get('Location', '')).path, '/web/database/selector')
@tagged('post_install', '-at_install')
class TestHttpEchoReplyJsonNoDB(TestHttpBase):
def test_echojson0_qs_json_nodb(self):
payload = json.dumps({
'jsonrpc': '2.0',
'id': 1234,
'params': {
'commander': 'Thor',
},
})
res = self.nodb_url_open("/test_http/echo-json?race=Asgard", data=payload, headers=CT_JSON)
self.assertEqual(res.status_code, 200)
self.assertEqual(res.text, '{"jsonrpc": "2.0", "id": 1234, "result": {"commander": "Thor"}}')
def test_echojson1_http_get_nodb(self):
res = self.nodb_url_open('/test_http/echo-json') # GET
self.assertEqual(res.status_code, 405)
@mute_logger('odoo.http')
def test_echojson2_http_post_nodb(self):
res = self.nodb_url_open('/test_http/echo-json', data={'race': 'Asgard'}) # POST
self.assertIn("Bad Request", res.text)
@tagged('post_install', '-at_install')
class TestHttpEchoReplyHttpWithDB(TestHttpBase):
def setUp(self):
super().setUp()
self.jackoneill = new_test_user(self.env, 'jackoneill', context={'lang': 'en_US'})
self.authenticate('jackoneill', 'jackoneill')
def test_echohttp0_get_qs_db(self):
res = self.db_url_open('/test_http/echo-http-get?race=Asgard')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.text, "{'race': 'Asgard'}")
def test_echohttp1_get_form_db(self):
res = self.db_url_open('/test_http/echo-http-get', data={'commander': 'Thor'})
self.assertEqual(res.status_code, 405)
def test_echohttp2_post_qs_db(self):
res = self.db_url_open('/test_http/echo-http-post?race=Asgard')
self.assertEqual(res.status_code, 405)
def test_echohttp3_post_qs_form_db(self):
res = self.db_url_open('/test_http/echo-http-post?race=Asgard', data={'commander': 'Thor'})
self.assertEqual(res.status_code, 200)
self.assertEqual(res.text, "{'race': 'Asgard', 'commander': 'Thor'}")
@mute_logger('odoo.http')
def test_echohttp4_post_json_db(self):
res = self.db_url_open('/test_http/echo-http-post', data='{}', headers=CT_JSON)
self.assertIn("Bad Request", res.text)
@mute_logger('odoo.http')
def test_echohttp5_post_no_csrf(self):
res = self.db_url_open('/test_http/echo-http-csrf?race=Asgard', data={'commander': 'Thor'})
self.assertEqual(res.status_code, 400)
self.assertIn("Session expired (invalid CSRF token)", res.text)
@mute_logger('odoo.http')
def test_echohttp6_post_bad_csrf(self):
res = self.db_url_open('/test_http/echo-http-csrf?race=Asgard', data={'commander': 'Thor', 'csrf_token': 'bad token'})
self.assertEqual(res.status_code, 400)
self.assertIn("Session expired (invalid CSRF token)", res.text)
@mute_logger('odoo.http')
def test_echohttp7_post_good_csrf(self):
res = self.db_url_open('/test_http/echo-http-csrf?race=Asgard', data={'commander': 'Thor', 'csrf_token': WebRequest.csrf_token(self)})
self.assertEqual(res.status_code, 200)
self.assertEqual(res.text, "{'race': 'Asgard', 'commander': 'Thor'}")
@tagged('post_install', '-at_install')
class TestHttpEchoReplyJsonWithDB(TestHttpBase):
def setUp(self):
super().setUp()
self.jackoneill = new_test_user(self.env, 'jackoneill', context={'lang': 'en_US'})
self.authenticate('jackoneill', 'jackoneill')
def test_echojson0_qs_json_db(self):
payload = json.dumps({
'jsonrpc': '2.0',
'id': 1234,
'params': {
'commander': 'Thor',
},
})
res = self.db_url_open('/test_http/echo-json?race=Asgard', data=payload, headers=CT_JSON)
self.assertEqual(res.status_code, 200)
self.assertEqual(res.text, '{"jsonrpc": "2.0", "id": 1234, "result": {"commander": "Thor"}}')
def test_echojson1_http_get_db(self):
res = self.db_url_open('/test_http/echo-json') # GET
self.assertEqual(res.status_code, 405)
@mute_logger('odoo.http')
def test_echojson2_http_post_db(self):
res = self.db_url_open('/test_http/echo-json', data={'race': 'Asgard'}) # POST
self.assertIn("Bad Request", res.text)
def test_echojson3_context_db(self):
payload = json.dumps({
"jsonrpc": "2.0",
"id": 0,
"params": {
"context": {
"name": "Thor"
},
"race": "Asgard",
},
})
res = self.db_url_open("/test_http/echo-json-context", data=payload, headers=CT_JSON)
self.assertEqual(res.status_code, 200)
self.assertEqual(res.text, '{"jsonrpc": "2.0", "id": 0, "result": {"name": "Thor"}}')
@tagged('post_install', '-at_install')
class TestHttpModels(TestHttpBase):
def setUp(self):
super().setUp()
self.jackoneill = new_test_user(self.env, 'jackoneill', context={'lang': 'en_US'})
self.authenticate('jackoneill', 'jackoneill')
def test_models0_galaxy_ok(self):
milky_way = self.env.ref('test_http.milky_way')
res = self.url_open(f"/test_http/{milky_way.id}")
self.assertEqual(res.status_code, 200)
self.assertEqual(
HtmlTokenizer.tokenize(res.text),
HtmlTokenizer.tokenize('''\
<p>Milky Way</p>
<ul>
<li><a href="/test_http/1/1">Earth (P4X-126)</a></li>
<li><a href="/test_http/1/2">Abydos (P2X-125)</a></li>
<li><a href="/test_http/1/3">Dakara (P5C-113)</a></li>
</ul>
''')
)
@unittest.skip('500 without website, 404 with website, 400 in httpocalypse. Cmon master...')
def test_models1_galaxy_ko(self):
res = self.url_open("/test_http/404") # unknown galaxy
self.assertEqual(res.status_code, 400)
self.assertIn('The Ancients did not settle there.', res.text)
def test_models2_stargate_ok(self):
milky_way = self.env.ref('test_http.milky_way')
earth = self.env.ref('test_http.earth')
res = self.url_open(f'/test_http/{milky_way.id}/{earth.id}')
self.assertEqual(res.status_code, 200)
self.assertEqual(
HtmlTokenizer.tokenize(res.text),
HtmlTokenizer.tokenize('''\
<dl>
<dt>name</dt><dd>Earth</dd>
<dt>address</dt><dd>sq5Abt</dd>
<dt>sgc_designation</dt><dd>P4X-126</dd>
</dl>
''')
)
@unittest.skip('500 without website, 404 with website, 400 in httpocalypse. Cmon master...')
def test_models3_stargate_ko(self):
milky_way = self.env.ref('test_http.milky_way')
res = self.url_open(f'/test_http/{milky_way.id}/9999') # unknown gate
self.assertEqual(res.status_code, 400)
self.assertIn("The goa'uld destroyed the gate", res.text)
@tagged('post_install', '-at_install')
class TestHttpMisc(TestHttpBase):
@unittest.skip('In master it is in the http_routing override')
def test_misc0_redirect(self):
res = self.nodb_url_open('/test_http//greeting')
self.assertEqual(res.status_code, 301)
self.assertEqual(urlparse(res.headers.get('Location', '')).path, '/test_http/greeting')
def test_misc1_reverse_proxy(self):
# client <-> reverse-proxy <-> odoo
client_ip = '127.0.0.16'
reverseproxy_ip = gethostbyname(HOST)
host = 'mycompany.odoo.com'
headers = {
'Host': '',
'X-Forwarded-For': client_ip,
'X-Forwarded-Host': host,
'X-Forwarded-Proto': 'https'
}
# Don't trust client-sent forwarded headers
with patch.object(config, 'options', {**config.options, 'proxy_mode': False}):
res = self.nodb_url_open('/test_http/wsgi_environ', headers=headers)
self.assertEqual(res.status_code, 200)
self.assertEqual(res.json()['REMOTE_ADDR'], reverseproxy_ip)
self.assertEqual(res.json()['HTTP_HOST'], '')
# Trust proxy-sent forwarded headers
with patch.object(config, 'options', {**config.options, 'proxy_mode': True}):
res = self.nodb_url_open('/test_http/wsgi_environ', headers=headers)
self.assertEqual(res.status_code, 200)
self.assertEqual(res.json()['REMOTE_ADDR'], client_ip)
self.assertEqual(res.json()['HTTP_HOST'], host)
@tagged('post_install', '-at_install')
class TestHttpCors(TestHttpBase):
def test_cors0_http_default(self):
res_opt = self.opener.options(f'{self.base_url()}/test_http/cors_http_default', timeout=10, allow_redirects=False)
self.assertIn(res_opt.status_code, (200, 204))
self.assertEqual(res_opt.headers.get('Access-Control-Allow-Origin'), '*')
self.assertEqual(res_opt.headers.get('Access-Control-Allow-Methods'), 'GET, POST')
self.assertEqual(res_opt.headers.get('Access-Control-Max-Age'), '86400') # one day
self.assertEqual(res_opt.headers.get('Access-Control-Allow-Headers'), 'Origin, X-Requested-With, Content-Type, Accept, Authorization')
res_get = self.url_open('/test_http/cors_http_default')
self.assertEqual(res_get.status_code, 200)
self.assertEqual(res_get.headers.get('Access-Control-Allow-Origin'), '*')
self.assertEqual(res_get.headers.get('Access-Control-Allow-Methods'), 'GET, POST')
@unittest.skip("Broken in master, fixed in httpocalypse.")
def test_cors1_http_methods(self):
res_opt = self.opener.options(f'{self.base_url()}/test_http/cors_http_methods', timeout=10, allow_redirects=False)
self.assertIn(res_opt.status_code, (200, 204))
self.assertEqual(res_opt.headers.get('Access-Control-Allow-Origin'), '*')
self.assertEqual(res_opt.headers.get('Access-Control-Allow-Methods'), 'GET, PUT')
self.assertEqual(res_opt.headers.get('Access-Control-Max-Age'), '86400') # one day
self.assertEqual(res_opt.headers.get('Access-Control-Allow-Headers'), 'Origin, X-Requested-With, Content-Type, Accept, Authorization')
res_post = self.url_open('/test_http/cors_http_methods')
self.assertEqual(res_post.status_code, 200)
self.assertEqual(res_post.headers.get('Access-Control-Allow-Origin'), '*')
self.assertEqual(res_post.headers.get('Access-Control-Allow-Methods'), 'GET, PUT')
def test_cors2_json(self):
res_opt = self.opener.options(f'{self.base_url()}/test_http/cors_json', timeout=10, allow_redirects=False)
self.assertIn(res_opt.status_code, (200, 204), res_opt.text)
self.assertEqual(res_opt.headers.get('Access-Control-Allow-Origin'), '*')
self.assertEqual(res_opt.headers.get('Access-Control-Allow-Methods'), 'POST')
self.assertEqual(res_opt.headers.get('Access-Control-Max-Age'), '86400') # one day
self.assertEqual(res_opt.headers.get('Access-Control-Allow-Headers'), 'Origin, X-Requested-With, Content-Type, Accept, Authorization')
res_post = self.url_open('/test_http/cors_json', data=json.dumps({'params': {}}), headers=CT_JSON)
self.assertEqual(res_post.status_code, 200)
self.assertEqual(res_post.headers.get('Access-Control-Allow-Origin'), '*')
self.assertEqual(res_post.headers.get('Access-Control-Allow-Methods'), 'POST')
+25
View File
@@ -0,0 +1,25 @@
<odoo>
<data>
<template id="tmpl_galaxy">
<p><t t-out="galaxy.name"/></p>
<ul>
<li t-foreach="stargates" t-as="gate">
<a t-attf-href="/test_http/#{ galaxy.id }/#{ gate.id }">
<t t-out="gate.name"/> (<t t-out="gate.sgc_designation"/>)
</a>
</li>
</ul>
</template>
<template id="tmpl_stargate">
<dl>
<t t-if="gate.galaxy_id.id != 1">
<dt>galaxy</dt><dd><t t-out="gate.galaxy_id.name"/></dd>
</t>
<t t-foreach="('name', 'address', 'sgc_designation')" t-as="field">
<dt><t t-out="field"/></dt><dd><t t-out="gate[field]"/></dd>
</t>
</dl>
</template>
</data>
</odoo>
+8 -1
View File
@@ -417,6 +417,13 @@ class BaseCase(unittest.TestCase, metaclass=MetaCase):
patcher.start()
self.addCleanup(patcher.stop)
@classmethod
def classPatch(cls, obj, key, val):
""" Do the patch ``setattr(obj, key, val)``, and prepare cleanup. """
patcher = patch.object(obj, key, val) # this is unittest.mock.patch
patcher.start()
cls.addClassCleanup(patcher.stop)
@contextmanager
def with_user(self, login):
""" Change user for a given test, like with self.with_user() ... """
@@ -1556,7 +1563,7 @@ class HttpCase(TransactionCase):
odoo.tools.misc.dumpstacks()
def logout(self, keep_db=True):
self.session.logout(keep_db=True)
self.session.logout(keep_db=keep_db)
odoo.http.root.session_store.save(self.session)
def authenticate(self, user, password):