From 3751379f1e9a4c215fb6eb898b4ccc67659b9ace Mon Sep 17 00:00:00 2001 From: stephanchrst Date: Tue, 10 May 2022 21:51:50 +0700 Subject: initial commit 2 --- addons/website/tests/__init__.py | 22 + addons/website/tests/template_qweb_test.xml | 36 + addons/website/tests/test_attachment.py | 51 + addons/website/tests/test_base_url.py | 65 + addons/website/tests/test_controllers.py | 46 + addons/website/tests/test_converter.py | 81 ++ addons/website/tests/test_crawl.py | 116 ++ addons/website/tests/test_get_current_website.py | 123 ++ addons/website/tests/test_lang_url.py | 62 + addons/website/tests/test_menu.py | 171 +++ addons/website/tests/test_page.py | 252 ++++ addons/website/tests/test_performance.py | 122 ++ addons/website/tests/test_qweb.py | 161 +++ addons/website/tests/test_res_users.py | 60 + addons/website/tests/test_snippets.py | 12 + addons/website/tests/test_theme.py | 14 + addons/website/tests/test_ui.py | 194 +++ addons/website/tests/test_views.py | 1432 ++++++++++++++++++++ .../tests/test_views_inherit_module_update.py | 85 ++ addons/website/tests/test_website_favicon.py | 36 + .../website/tests/test_website_reset_password.py | 77 ++ addons/website/tests/test_website_visitor.py | 326 +++++ 22 files changed, 3544 insertions(+) create mode 100644 addons/website/tests/__init__.py create mode 100644 addons/website/tests/template_qweb_test.xml create mode 100644 addons/website/tests/test_attachment.py create mode 100644 addons/website/tests/test_base_url.py create mode 100644 addons/website/tests/test_controllers.py create mode 100644 addons/website/tests/test_converter.py create mode 100644 addons/website/tests/test_crawl.py create mode 100644 addons/website/tests/test_get_current_website.py create mode 100644 addons/website/tests/test_lang_url.py create mode 100644 addons/website/tests/test_menu.py create mode 100644 addons/website/tests/test_page.py create mode 100644 addons/website/tests/test_performance.py create mode 100644 addons/website/tests/test_qweb.py create mode 100644 addons/website/tests/test_res_users.py create mode 100644 addons/website/tests/test_snippets.py create mode 100644 addons/website/tests/test_theme.py create mode 100644 addons/website/tests/test_ui.py create mode 100644 addons/website/tests/test_views.py create mode 100644 addons/website/tests/test_views_inherit_module_update.py create mode 100644 addons/website/tests/test_website_favicon.py create mode 100644 addons/website/tests/test_website_reset_password.py create mode 100644 addons/website/tests/test_website_visitor.py (limited to 'addons/website/tests') diff --git a/addons/website/tests/__init__.py b/addons/website/tests/__init__.py new file mode 100644 index 00000000..efde81df --- /dev/null +++ b/addons/website/tests/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import test_attachment +from . import test_base_url +from . import test_converter +from . import test_crawl +from . import test_get_current_website +from . import test_lang_url +from . import test_menu +from . import test_page +from . import test_performance +from . import test_qweb +from . import test_res_users +from . import test_theme +from . import test_ui +from . import test_views +from . import test_views_inherit_module_update +from . import test_website_favicon +from . import test_website_reset_password +from . import test_website_visitor +from . import test_controllers +from . import test_snippets diff --git a/addons/website/tests/template_qweb_test.xml b/addons/website/tests/template_qweb_test.xml new file mode 100644 index 00000000..ac7dd4e3 --- /dev/null +++ b/addons/website/tests/template_qweb_test.xml @@ -0,0 +1,36 @@ + + + + + + + \ No newline at end of file diff --git a/addons/website/tests/test_attachment.py b/addons/website/tests/test_attachment.py new file mode 100644 index 00000000..17359eb7 --- /dev/null +++ b/addons/website/tests/test_attachment.py @@ -0,0 +1,51 @@ +import odoo.tests +from odoo.tests.common import HOST +from odoo.tools import config + + +@odoo.tests.common.tagged('post_install', '-at_install') +class TestWebsiteAttachment(odoo.tests.HttpCase): + + def test_01_type_url_301_image(self): + IMD = self.env['ir.model.data'] + IrAttachment = self.env['ir.attachment'] + + img1 = IrAttachment.create({ + 'public': True, + 'name': 's_banner_default_image.jpg', + 'type': 'url', + 'url': '/website/static/src/img/snippets_demo/s_banner.jpg' + }) + + img2 = IrAttachment.create({ + 'public': True, + 'name': 's_banner_default_image.jpg', + 'type': 'url', + 'url': '/web/image/test.an_image_url' + }) + + IMD.create({ + 'name': 'an_image_url', + 'module': 'test', + 'model': img1._name, + 'res_id': img1.id, + }) + + IMD.create({ + 'name': 'an_image_redirect_301', + 'module': 'test', + 'model': img2._name, + 'res_id': img2.id, + }) + + req = self.url_open('/web/image/test.an_image_url') + self.assertEqual(req.status_code, 200) + + base = "http://%s:%s" % (HOST, config['http_port']) + + req = self.opener.get(base + '/web/image/test.an_image_redirect_301', allow_redirects=False) + self.assertEqual(req.status_code, 301) + self.assertEqual(req.headers['Location'], base + '/web/image/test.an_image_url') + + req = self.opener.get(base + '/web/image/test.an_image_redirect_301', allow_redirects=True) + self.assertEqual(req.status_code, 200) diff --git a/addons/website/tests/test_base_url.py b/addons/website/tests/test_base_url.py new file mode 100644 index 00000000..c14a6bc3 --- /dev/null +++ b/addons/website/tests/test_base_url.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from lxml.html import document_fromstring + +import odoo.tests + + +class TestUrlCommon(odoo.tests.HttpCase): + def setUp(self): + super(TestUrlCommon, self).setUp() + self.domain = 'http://' + odoo.tests.HOST + self.website = self.env['website'].create({ + 'name': 'test base url', + 'domain': self.domain, + }) + + lang_fr = self.env['res.lang']._activate_lang('fr_FR') + self.website.language_ids = self.env.ref('base.lang_en') + lang_fr + self.website.default_lang_id = self.env.ref('base.lang_en') + + def _assertCanonical(self, url, canonical_url): + res = self.url_open(url) + canonical_link = document_fromstring(res.content).xpath("/html/head/link[@rel='canonical']") + self.assertEqual(len(canonical_link), 1) + self.assertEqual(canonical_link[0].attrib["href"], canonical_url) + + +@odoo.tests.tagged('-at_install', 'post_install') +class TestBaseUrl(TestUrlCommon): + def test_01_base_url(self): + ICP = self.env['ir.config_parameter'] + icp_base_url = ICP.sudo().get_param('web.base.url') + + # Test URL is correct for the website itself when the domain is set + self.assertEqual(self.website.get_base_url(), self.domain) + + # Test URL is correct for a model without website_id + without_website_id = self.env['ir.attachment'].create({'name': 'test base url'}) + self.assertEqual(without_website_id.get_base_url(), icp_base_url) + + # Test URL is correct for a model with website_id... + with_website_id = self.env['res.partner'].create({'name': 'test base url'}) + + # ...when no website is set on the model + with_website_id.website_id = False + self.assertEqual(with_website_id.get_base_url(), icp_base_url) + + # ...when the website is correctly set + with_website_id.website_id = self.website + self.assertEqual(with_website_id.get_base_url(), self.domain) + + # ...when the set website doesn't have a domain + self.website.domain = False + self.assertEqual(with_website_id.get_base_url(), icp_base_url) + + # Test URL is correct for the website itself when no domain is set + self.assertEqual(self.website.get_base_url(), icp_base_url) + + def test_02_canonical_url(self): + self._assertCanonical('/', self.domain + '/') + self._assertCanonical('/?debug=1', self.domain + '/') + self._assertCanonical('/a-page', self.domain + '/a-page') + self._assertCanonical('/en_US', self.domain + '/') + self._assertCanonical('/fr_FR', self.domain + '/fr/') diff --git a/addons/website/tests/test_controllers.py b/addons/website/tests/test_controllers.py new file mode 100644 index 00000000..9bb167fe --- /dev/null +++ b/addons/website/tests/test_controllers.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import json + +from odoo import tests +from odoo.tools import mute_logger + + +@tests.tagged('post_install', '-at_install') +class TestControllers(tests.HttpCase): + + @mute_logger('odoo.addons.http_routing.models.ir_http', 'odoo.http') + def test_last_created_pages_autocompletion(self): + self.authenticate("admin", "admin") + Page = self.env['website.page'] + last_5_url_edited = [] + base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') + suggested_links_url = base_url + '/website/get_suggested_links' + + for i in range(0, 10): + new_page = Page.create({ + 'name': 'Generic', + 'type': 'qweb', + 'arch': ''' +
content
+ ''', + 'key': "test.generic_view-%d" % i, + 'url': "/generic-%d" % i, + 'is_published': True, + }) + if i % 2 == 0: + # mark as old + new_page._write({'write_date': '2020-01-01'}) + else: + last_5_url_edited.append(new_page.url) + + res = self.opener.post(url=suggested_links_url, json={'params': {'needle': '/'}}) + resp = json.loads(res.content) + assert 'result' in resp + suggested_links = resp['result'] + last_modified_history = next(o for o in suggested_links['others'] if o["title"] == "Last modified pages") + last_modified_values = map(lambda o: o['value'], last_modified_history['values']) + + matching_pages = set(map(lambda o: o['value'], suggested_links['matching_pages'])) + self.assertEqual(set(last_modified_values), set(last_5_url_edited) - matching_pages) diff --git a/addons/website/tests/test_converter.py b/addons/website/tests/test_converter.py new file mode 100644 index 00000000..9a688c74 --- /dev/null +++ b/addons/website/tests/test_converter.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.http_routing.models.ir_http import slugify, unslug +from odoo.tests.common import BaseCase + + +class TestUnslug(BaseCase): + + def test_unslug(self): + tests = { + '': (None, None), + 'foo': (None, None), + 'foo-': (None, None), + '-': (None, None), + 'foo-1': ('foo', 1), + 'foo-bar-1': ('foo-bar', 1), + 'foo--1': ('foo', -1), + '1': (None, 1), + '1-1': ('1', 1), + '--1': (None, None), + 'foo---1': (None, None), + 'foo1': (None, None), + } + + for slug, expected in tests.items(): + self.assertEqual(unslug(slug), expected) + +class TestTitleToSlug(BaseCase): + """ + Those tests should pass with or without python-slugify + See website/models/website.py slugify method + """ + + def test_spaces(self): + self.assertEqual( + "spaces", + slugify(u" spaces ") + ) + + def test_unicode(self): + self.assertEqual( + "heterogeneite", + slugify(u"hétérogénéité") + ) + + def test_underscore(self): + self.assertEqual( + "one-two", + slugify(u"one_two") + ) + + def test_caps(self): + self.assertEqual( + "camelcase", + slugify(u"CamelCase") + ) + + def test_special_chars(self): + self.assertEqual( + "o-d-o-o", + slugify(u"o!#d{|\o/@~o&%^?") + ) + + def test_str_to_unicode(self): + self.assertEqual( + "espana", + slugify("España") + ) + + def test_numbers(self): + self.assertEqual( + "article-1", + slugify(u"Article 1") + ) + + def test_all(self): + self.assertEqual( + "do-you-know-martine-a-la-plage", + slugify(u"Do YOU know 'Martine à la plage' ?") + ) diff --git a/addons/website/tests/test_crawl.py b/addons/website/tests/test_crawl.py new file mode 100644 index 00000000..d210fd9b --- /dev/null +++ b/addons/website/tests/test_crawl.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import logging +import time + +import lxml.html +from werkzeug import urls + +import odoo +import re + +from odoo.addons.base.tests.common import HttpCaseWithUserDemo + +_logger = logging.getLogger(__name__) + + +@odoo.tests.common.tagged('post_install', '-at_install', 'crawl') +class Crawler(HttpCaseWithUserDemo): + """ Test suite crawling an Odoo CMS instance and checking that all + internal links lead to a 200 response. + + If a username and a password are provided, authenticates the user before + starting the crawl + """ + + def setUp(self): + super(Crawler, self).setUp() + + if hasattr(self.env['res.partner'], 'grade_id'): + # Create at least one published parter, so that /partners doesn't + # return a 404 + grade = self.env['res.partner.grade'].create({ + 'name': 'A test grade', + 'website_published': True, + }) + self.env['res.partner'].create({ + 'name': 'A Company for /partners', + 'is_company': True, + 'grade_id': grade.id, + 'website_published': True, + }) + + def crawl(self, url, seen=None, msg=''): + if seen is None: + seen = set() + + url_slug = re.sub(r"[/](([^/=?&]+-)?[0-9]+)([/]|$)", '//', url) + url_slug = re.sub(r"([^/=?&]+)=[^/=?&]+", '\g<1>=param', url_slug) + if url_slug in seen: + return seen + else: + seen.add(url_slug) + + _logger.info("%s %s", msg, url) + r = self.url_open(url, allow_redirects=False) + if r.status_code in (301, 302): + # check local redirect to avoid fetch externals pages + new_url = r.headers.get('Location') + current_url = r.url + if urls.url_parse(new_url).netloc != urls.url_parse(current_url).netloc: + return seen + r = self.url_open(new_url) + + code = r.status_code + self.assertIn(code, range(200, 300), "%s Fetching %s returned error response (%d)" % (msg, url, code)) + + if r.headers['Content-Type'].startswith('text/html'): + doc = lxml.html.fromstring(r.content) + for link in doc.xpath('//a[@href]'): + href = link.get('href') + + parts = urls.url_parse(href) + # href with any fragment removed + href = parts.replace(fragment='').to_url() + + # FIXME: handle relative link (not parts.path.startswith /) + if parts.netloc or \ + not parts.path.startswith('/') or \ + parts.path == '/web' or\ + parts.path.startswith('/web/') or \ + parts.path.startswith('/en_US/') or \ + (parts.scheme and parts.scheme not in ('http', 'https')): + continue + + self.crawl(href, seen, msg) + return seen + + def test_10_crawl_public(self): + t0 = time.time() + t0_sql = self.registry.test_cr.sql_log_count + seen = self.crawl('/', msg='Anonymous Coward') + count = len(seen) + duration = time.time() - t0 + sql = self.registry.test_cr.sql_log_count - t0_sql + _logger.runbot("public crawled %s urls in %.2fs %s queries, %.3fs %.2fq per request, ", count, duration, sql, duration / count, float(sql) / count) + + def test_20_crawl_demo(self): + t0 = time.time() + t0_sql = self.registry.test_cr.sql_log_count + self.authenticate('demo', 'demo') + seen = self.crawl('/', msg='demo') + count = len(seen) + duration = time.time() - t0 + sql = self.registry.test_cr.sql_log_count - t0_sql + _logger.runbot("demo crawled %s urls in %.2fs %s queries, %.3fs %.2fq per request", count, duration, sql, duration / count, float(sql) / count) + + def test_30_crawl_admin(self): + t0 = time.time() + t0_sql = self.registry.test_cr.sql_log_count + self.authenticate('admin', 'admin') + seen = self.crawl('/', msg='admin') + count = len(seen) + duration = time.time() - t0 + sql = self.registry.test_cr.sql_log_count - t0_sql + _logger.runbot("admin crawled %s urls in %.2fs %s queries, %.3fs %.2fq per request", count, duration, sql, duration / count, float(sql) / count) diff --git a/addons/website/tests/test_get_current_website.py b/addons/website/tests/test_get_current_website.py new file mode 100644 index 00000000..da36762a --- /dev/null +++ b/addons/website/tests/test_get_current_website.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests import tagged +from odoo.tests.common import TransactionCase + + +@tagged('post_install', '-at_install') +class TestGetCurrentWebsite(TransactionCase): + + def test_01_get_current_website_id(self): + """Make sure `_get_current_website_id works`.""" + + Website = self.env['website'] + + # clean initial state + website1 = self.env.ref('website.default_website') + website1.domain = '' + website1.country_group_ids = False + + website2 = Website.create({ + 'name': 'My Website 2', + 'domain': '', + 'country_group_ids': False, + }) + + country1 = self.env['res.country'].create({'name': "My Country 1"}) + country2 = self.env['res.country'].create({'name': "My Country 2"}) + country3 = self.env['res.country'].create({'name': "My Country 3"}) + country4 = self.env['res.country'].create({'name': "My Country 4"}) + country5 = self.env['res.country'].create({'name': "My Country 5"}) + + country_group_1_2 = self.env['res.country.group'].create({ + 'name': "My Country Group 1-2", + 'country_ids': [(6, 0, (country1 + country2 + country5).ids)], + }) + country_group_3 = self.env['res.country.group'].create({ + 'name': "My Country Group 3", + 'country_ids': [(6, 0, (country3 + country5).ids)], + }) + + # CASE: no domain, no country: get first + self.assertEqual(Website._get_current_website_id('', False), website1.id) + self.assertEqual(Website._get_current_website_id('', country3.id), website1.id) + + # CASE: no domain, given country: get by country + website1.country_group_ids = country_group_1_2 + website2.country_group_ids = country_group_3 + + self.assertEqual(Website._get_current_website_id('', country1.id), website1.id) + self.assertEqual(Website._get_current_website_id('', country2.id), website1.id) + self.assertEqual(Website._get_current_website_id('', country3.id), website2.id) + + # CASE: no domain, wrong country: get first + self.assertEqual(Website._get_current_website_id('', country4.id), Website.search([]).sorted('country_group_ids')[0].id) + + # CASE: no domain, multiple country: get first + self.assertEqual(Website._get_current_website_id('', country5.id), website1.id) + + # setup domain + website1.domain = 'my-site-1.fr' + website2.domain = 'https://my2ndsite.com:80' + + website1.country_group_ids = False + website2.country_group_ids = False + + # CASE: domain set: get matching domain + self.assertEqual(Website._get_current_website_id('my-site-1.fr', False), website1.id) + + # CASE: domain set: get matching domain (scheme and port supported) + self.assertEqual(Website._get_current_website_id('my-site-1.fr:8069', False), website1.id) + + self.assertEqual(Website._get_current_website_id('my2ndsite.com:80', False), website2.id) + self.assertEqual(Website._get_current_website_id('my2ndsite.com:8069', False), website2.id) + self.assertEqual(Website._get_current_website_id('my2ndsite.com', False), website2.id) + + # CASE: domain set, wrong domain: get first + self.assertEqual(Website._get_current_website_id('test.com', False), website1.id) + + # CASE: subdomain: not supported + self.assertEqual(Website._get_current_website_id('www.my2ndsite.com', False), website1.id) + + # CASE: domain set, given country: get by domain in priority + website1.country_group_ids = country_group_1_2 + website2.country_group_ids = country_group_3 + + self.assertEqual(Website._get_current_website_id('my2ndsite.com', country1.id), website2.id) + self.assertEqual(Website._get_current_website_id('my2ndsite.com', country2.id), website2.id) + self.assertEqual(Website._get_current_website_id('my-site-1.fr', country3.id), website1.id) + + # CASE: domain set, wrong country: get first for domain + self.assertEqual(Website._get_current_website_id('my2ndsite.com', country4.id), website2.id) + + # CASE: domain set, multiple country: get first for domain + website1.domain = website2.domain + self.assertEqual(Website._get_current_website_id('my2ndsite.com', country5.id), website1.id) + + # CASE: overlapping domain: get exact match + website1.domain = 'site-1.com' + website2.domain = 'even-better-site-1.com' + self.assertEqual(Website._get_current_website_id('site-1.com', False), website1.id) + self.assertEqual(Website._get_current_website_id('even-better-site-1.com', False), website2.id) + + # CASE: case insensitive + website1.domain = 'Site-1.com' + website2.domain = 'Even-Better-site-1.com' + self.assertEqual(Website._get_current_website_id('sitE-1.com', False), website1.id) + self.assertEqual(Website._get_current_website_id('even-beTTer-site-1.com', False), website2.id) + + # CASE: same domain, different port + website1.domain = 'site-1.com:80' + website2.domain = 'site-1.com:81' + self.assertEqual(Website._get_current_website_id('site-1.com:80', False), website1.id) + self.assertEqual(Website._get_current_website_id('site-1.com:81', False), website2.id) + self.assertEqual(Website._get_current_website_id('site-1.com:82', False), website1.id) + self.assertEqual(Website._get_current_website_id('site-1.com', False), website1.id) + + def test_02_signup_user_website_id(self): + website = self.env.ref('website.default_website') + website.specific_user_account = True + + user = self.env['res.users'].create({'website_id': website.id, 'login': 'sad@mail.com', 'name': 'Hope Fully'}) + self.assertTrue(user.website_id == user.partner_id.website_id == website) diff --git a/addons/website/tests/test_lang_url.py b/addons/website/tests/test_lang_url.py new file mode 100644 index 00000000..51153ade --- /dev/null +++ b/addons/website/tests/test_lang_url.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.http_routing.models.ir_http import url_lang +from odoo.addons.website.tools import MockRequest +from odoo.tests import HttpCase, tagged + + +@tagged('-at_install', 'post_install') +class TestLangUrl(HttpCase): + def setUp(self): + super(TestLangUrl, self).setUp() + + # Simulate multi lang without loading translations + self.website = self.env.ref('website.default_website') + self.lang_fr = self.env['res.lang']._activate_lang('fr_FR') + self.lang_fr.write({'url_code': 'fr'}) + self.website.language_ids = self.env.ref('base.lang_en') + self.lang_fr + self.website.default_lang_id = self.env.ref('base.lang_en') + + def test_01_url_lang(self): + with MockRequest(self.env, website=self.website): + self.assertEqual(url_lang('', '[lang]'), '/[lang]/hello/', "`[lang]` is used to be replaced in the url_return after installing a language, it should not be replaced or removed.") + + def test_02_url_redirect(self): + url = '/fr_WHATEVER/contactus' + r = self.url_open(url) + self.assertEqual(r.status_code, 200) + self.assertTrue(r.url.endswith('/fr/contactus'), "fr_WHATEVER should be forwarded to 'fr_FR' lang as closest match") + + url = '/fr_FR/contactus' + r = self.url_open(url) + self.assertEqual(r.status_code, 200) + self.assertTrue(r.url.endswith('/fr/contactus'), "lang in url should use url_code ('fr' in this case)") + + def test_03_url_cook_lang_not_available(self): + """ An activated res.lang should not be displayed in the frontend if not a website lang. """ + self.website.language_ids = self.env.ref('base.lang_en') + r = self.url_open('/fr/contactus') + self.assertTrue('lang="en-US"' in r.text, "french should not be displayed as not a frontend lang") + + def test_04_url_cook_lang_not_available(self): + """ `nearest_lang` should filter out lang not available in frontend. + Eg: 1. go in backend in english -> request.context['lang'] = `en_US` + 2. go in frontend, the request.context['lang'] is passed through + `nearest_lang` which should not return english. More then a + misbehavior it will crash in website language selector template. + """ + # 1. Load backend + self.authenticate('admin', 'admin') + r = self.url_open('/web') + self.assertTrue('"lang": "en_US"' in r.text, "ensure english was loaded") + + # 2. Remove en_US from frontend + self.website.language_ids = self.lang_fr + self.website.default_lang_id = self.lang_fr + + # 3. Ensure visiting /contactus do not crash + url = '/contactus' + r = self.url_open(url) + self.assertEqual(r.status_code, 200) + self.assertTrue('lang="fr-FR"' in r.text, "Ensure contactus did not soft crash + loaded in correct lang") diff --git a/addons/website/tests/test_menu.py b/addons/website/tests/test_menu.py new file mode 100644 index 00000000..5a0fe9c4 --- /dev/null +++ b/addons/website/tests/test_menu.py @@ -0,0 +1,171 @@ +# coding: utf-8 + +import json + +from odoo.tests import common + + +class TestMenu(common.TransactionCase): + def setUp(self): + super(TestMenu, self).setUp() + self.nb_website = self.env['website'].search_count([]) + + def test_01_menu_got_duplicated(self): + Menu = self.env['website.menu'] + total_menu_items = Menu.search_count([]) + + self.menu_root = Menu.create({ + 'name': 'Root', + }) + + self.menu_child = Menu.create({ + 'name': 'Child', + 'parent_id': self.menu_root.id, + }) + + self.assertEqual(total_menu_items + self.nb_website * 2, Menu.search_count([]), "Creating a menu without a website_id should create this menu for every website_id") + + def test_02_menu_count(self): + Menu = self.env['website.menu'] + total_menu_items = Menu.search_count([]) + + top_menu = self.env['website'].get_current_website().menu_id + data = [ + { + 'id': 'new-1', + 'parent_id': top_menu.id, + 'name': 'New Menu Specific 1', + 'url': '/new-specific-1', + 'is_mega_menu': False, + }, + { + 'id': 'new-2', + 'parent_id': top_menu.id, + 'name': 'New Menu Specific 2', + 'url': '/new-specific-2', + 'is_mega_menu': False, + } + ] + Menu.save(1, {'data': data, 'to_delete': []}) + + self.assertEqual(total_menu_items + 2, Menu.search_count([]), "Creating 2 new menus should create only 2 menus records") + + def test_03_default_menu_for_new_website(self): + Website = self.env['website'] + Menu = self.env['website.menu'] + total_menu_items = Menu.search_count([]) + + # Simulating website.menu created on module install (blog, shop, forum..) that will be created on default menu tree + default_menu = self.env.ref('website.main_menu') + Menu.create({ + 'name': 'Sub Default Menu', + 'parent_id': default_menu.id, + }) + self.assertEqual(total_menu_items + 1 + self.nb_website, Menu.search_count([]), "Creating a default child menu should create it as such and copy it on every website") + + # Ensure new website got a top menu + total_menus = Menu.search_count([]) + Website.create({'name': 'new website'}) + self.assertEqual(total_menus + 4, Menu.search_count([]), "New website's bootstraping should have duplicate default menu tree (Top/Home/Contactus/Sub Default Menu)") + + def test_04_specific_menu_translation(self): + Translation = self.env['ir.translation'] + Menu = self.env['website.menu'] + existing_menus = Menu.search([]) + + default_menu = self.env.ref('website.main_menu') + template_menu = Menu.create({ + 'parent_id': default_menu.id, + 'name': 'Menu in english', + 'url': 'turlututu', + }) + new_menus = Menu.search([]) - existing_menus + specific1, specific2 = new_menus.with_context(lang='fr_FR') - template_menu + + # create fr_FR translation for template menu + self.env.ref('base.lang_fr').active = True + template_menu.with_context(lang='fr_FR').name = 'Menu en français' + Translation.search([ + ('name', '=', 'website.menu,name'), ('res_id', '=', template_menu.id), + ]).module = 'website' + self.assertEqual(specific1.name, 'Menu in english', + 'Translating template menu does not translate specific menu') + + # have different translation for specific website + specific1.name = 'Menu in french' + + # loading translation add missing specific translation + Translation._load_module_terms(['website'], ['fr_FR']) + Menu.invalidate_cache(['name']) + self.assertEqual(specific1.name, 'Menu in french', + 'Load translation without overwriting keep existing translation') + self.assertEqual(specific2.name, 'Menu en français', + 'Load translation add missing translation from template menu') + + # loading translation with overwrite sync all translations from menu template + Translation._load_module_terms(['website'], ['fr_FR'], overwrite=True) + Menu.invalidate_cache(['name']) + self.assertEqual(specific1.name, 'Menu en français', + 'Load translation with overwriting update existing menu from template') + + def test_05_default_menu_unlink(self): + Menu = self.env['website.menu'] + total_menu_items = Menu.search_count([]) + + default_menu = self.env.ref('website.main_menu') + default_menu.child_id[0].unlink() + self.assertEqual(total_menu_items - 1 - self.nb_website, Menu.search_count([]), "Deleting a default menu item should delete its 'copies' (same URL) from website's menu trees. In this case, the default child menu and its copies on website 1 and website 2") + + +class TestMenuHttp(common.HttpCase): + def test_01_menu_page_m2o(self): + # 1. Create a page with a menu + Menu = self.env['website.menu'] + Page = self.env['website.page'] + page_url = '/page_specific' + page = Page.create({ + 'url': page_url, + 'website_id': 1, + # ir.ui.view properties + 'name': 'Base', + 'type': 'qweb', + 'arch': '
Specific View
', + 'key': 'test.specific_view', + }) + menu = Menu.create({ + 'name': 'Page Specific menu', + 'page_id': page.id, + 'url': page_url, + 'website_id': 1, + }) + + # 2. Edit the menu URL to a 'reserved' URL + data = { + 'id': menu.id, + 'parent_id': menu.parent_id.id, + 'name': menu.name, + 'url': '/website/info', + } + self.authenticate("admin", "admin") + # `Menu.save(1, {'data': [data], 'to_delete': []})` would have been + # ideal but need a full frontend context to generate routing maps, + # router and registry, even MockRequest is not enough + self.url_open('/web/dataset/call_kw', data=json.dumps({ + "params": { + 'model': 'website.menu', + 'method': 'save', + 'args': [1, {'data': [data], 'to_delete': []}], + 'kwargs': {}, + }, + }), headers={"Content-Type": "application/json"}) + + self.assertFalse(menu.page_id, "M2o should have been unset as this is a reserved URL.") + self.assertEqual(menu.url, '/website/info', "Menu URL should have changed.") + self.assertEqual(page.url, page_url, "Page's URL shouldn't have changed.") + + # 3. Edit the menu URL back to the page URL + data['url'] = page_url + Menu.save(1, {'data': [data], 'to_delete': []}) + self.assertEqual(menu.page_id, page, + "M2o should have been set back, as there was a page found with the new URL set on the menu.") + self.assertTrue(page.url == menu.url == page_url) diff --git a/addons/website/tests/test_page.py b/addons/website/tests/test_page.py new file mode 100644 index 00000000..7d7d0928 --- /dev/null +++ b/addons/website/tests/test_page.py @@ -0,0 +1,252 @@ +# coding: utf-8 +from odoo.tests import common, HttpCase, tagged + + +@tagged('-at_install', 'post_install') +class TestPage(common.TransactionCase): + def setUp(self): + super(TestPage, self).setUp() + View = self.env['ir.ui.view'] + Page = self.env['website.page'] + Menu = self.env['website.menu'] + + self.base_view = View.create({ + 'name': 'Base', + 'type': 'qweb', + 'arch': '
content
', + 'key': 'test.base_view', + }) + + self.extension_view = View.create({ + 'name': 'Extension', + 'mode': 'extension', + 'inherit_id': self.base_view.id, + 'arch': '
, extended content
', + 'key': 'test.extension_view', + }) + + self.page_1 = Page.create({ + 'view_id': self.base_view.id, + 'url': '/page_1', + }) + + self.page_1_menu = Menu.create({ + 'name': 'Page 1 menu', + 'page_id': self.page_1.id, + 'website_id': 1, + }) + + def test_copy_page(self): + View = self.env['ir.ui.view'] + Page = self.env['website.page'] + Menu = self.env['website.menu'] + # Specific page + self.specific_view = View.create({ + 'name': 'Base', + 'type': 'qweb', + 'arch': '
Specific View
', + 'key': 'test.specific_view', + }) + self.page_specific = Page.create({ + 'view_id': self.specific_view.id, + 'url': '/page_specific', + 'website_id': 1, + }) + self.page_specific_menu = Menu.create({ + 'name': 'Page Specific menu', + 'page_id': self.page_specific.id, + 'website_id': 1, + }) + total_pages = Page.search_count([]) + total_menus = Menu.search_count([]) + # Copying a specific page should create a new page with an unique URL (suffixed by -X) + Page.clone_page(self.page_specific.id, clone_menu=True) + cloned_page = Page.search([('url', '=', '/page_specific-1')]) + cloned_menu = Menu.search([('url', '=', '/page_specific-1')]) + self.assertEqual(len(cloned_page), 1, "A page with an URL /page_specific-1 should've been created") + self.assertEqual(Page.search_count([]), total_pages + 1, "Should have cloned the page") + # It should also copy its menu with new url/name/page_id (if the page has a menu) + self.assertEqual(len(cloned_menu), 1, "A specific page (with a menu) being cloned should have it's menu also cloned") + self.assertEqual(cloned_menu.page_id, cloned_page, "The new cloned menu and the new cloned page should be linked (m2o)") + self.assertEqual(Menu.search_count([]), total_menus + 1, "Should have cloned the page menu") + Page.clone_page(self.page_specific.id, page_name="about-us", clone_menu=True) + cloned_page_about_us = Page.search([('url', '=', '/about-us')]) + cloned_menu_about_us = Menu.search([('url', '=', '/about-us')]) + self.assertEqual(len(cloned_page_about_us), 1, "A page with an URL /about-us should've been created") + self.assertEqual(len(cloned_menu_about_us), 1, "A specific page (with a menu) being cloned should have it's menu also cloned") + self.assertEqual(cloned_menu_about_us.page_id, cloned_page_about_us, "The new cloned menu and the new cloned page should be linked (m2o)") + # It should also copy its menu with new url/name/page_id (if the page has a menu) + self.assertEqual(Menu.search_count([]), total_menus + 2, "Should have cloned the page menu") + + total_pages = Page.search_count([]) + total_menus = Menu.search_count([]) + + # Copying a generic page should create a specific page with same URL + Page.clone_page(self.page_1.id, clone_menu=True) + cloned_generic_page = Page.search([('url', '=', '/page_1'), ('id', '!=', self.page_1.id), ('website_id', '!=', False)]) + self.assertEqual(len(cloned_generic_page), 1, "A generic page being cloned should create a specific one for the current website") + self.assertEqual(cloned_generic_page.url, self.page_1.url, "The URL of the cloned specific page should be the same as the generic page it has been cloned from") + self.assertEqual(Page.search_count([]), total_pages + 1, "Should have cloned the generic page as a specific page for this website") + self.assertEqual(Menu.search_count([]), total_menus, "It should not create a new menu as the generic page's menu belong to another website") + # Except if the URL already exists for this website (its the case now that we already cloned it once) + Page.clone_page(self.page_1.id, clone_menu=True) + cloned_generic_page_2 = Page.search([('url', '=', '/page_1-1'), ('id', '!=', self.page_1.id)]) + self.assertEqual(len(cloned_generic_page_2), 1, "A generic page being cloned should create a specific page with a new URL if there is already a specific page with that URL") + + def test_cow_page(self): + Menu = self.env['website.menu'] + Page = self.env['website.page'] + View = self.env['ir.ui.view'] + + # backend write, no COW + total_pages = Page.search_count([]) + total_menus = Menu.search_count([]) + total_views = View.search_count([]) + self.page_1.write({'arch': '
modified base content
'}) + self.assertEqual(total_pages, Page.search_count([])) + self.assertEqual(total_menus, Menu.search_count([])) + self.assertEqual(total_views, View.search_count([])) + + # edit through frontend + self.page_1.with_context(website_id=1).write({'arch': '
website 1 content
'}) + + # 1. should have created website-specific copies for: + # - page + # - view x2 (base view + extension view) + # 2. should not have created menu copy as menus are not shared/COW + # 3. and shouldn't have touched original records + self.assertEqual(total_pages + 1, Page.search_count([])) + self.assertEqual(total_menus, Menu.search_count([])) + self.assertEqual(total_views + 2, View.search_count([])) + + self.assertEqual(self.page_1.arch, '
modified base content
') + self.assertEqual(bool(self.page_1.website_id), False) + + new_page = Page.search([('url', '=', '/page_1'), ('id', '!=', self.page_1.id)]) + self.assertEqual(new_page.website_id.id, 1) + self.assertEqual(new_page.view_id.inherit_children_ids[0].website_id.id, 1) + self.assertEqual(new_page.arch, '
website 1 content
') + + def test_cow_extension_view(self): + ''' test cow on extension view itself (like web_editor would do in the frontend) ''' + Menu = self.env['website.menu'] + Page = self.env['website.page'] + View = self.env['ir.ui.view'] + + # nothing special should happen when editing through the backend + total_pages = Page.search_count([]) + total_menus = Menu.search_count([]) + total_views = View.search_count([]) + self.extension_view.write({'arch': '
modified extension content
'}) + self.assertEqual(self.extension_view.arch, '
modified extension content
') + self.assertEqual(total_pages, Page.search_count([])) + self.assertEqual(total_menus, Menu.search_count([])) + self.assertEqual(total_views, View.search_count([])) + + # When editing through the frontend a website-specific copy + # for the extension view should be created. When rendering the + # original website.page on website 1 it will look differently + # due to this new extension view. + self.extension_view.with_context(website_id=1).write({'arch': '
website 1 content
'}) + self.assertEqual(total_pages, Page.search_count([])) + self.assertEqual(total_menus, Menu.search_count([])) + self.assertEqual(total_views + 1, View.search_count([])) + + self.assertEqual(self.extension_view.arch, '
modified extension content
') + self.assertEqual(bool(self.page_1.website_id), False) + + new_view = View.search([('name', '=', 'Extension'), ('website_id', '=', 1)]) + self.assertEqual(new_view.arch, '
website 1 content
') + self.assertEqual(new_view.website_id.id, 1) + + def test_cou_page_backend(self): + Page = self.env['website.page'] + View = self.env['ir.ui.view'] + + # currently the view unlink of website.page can't handle views with inherited views + self.extension_view.unlink() + + self.page_1.unlink() + self.assertEqual(Page.search_count([('url', '=', '/page_1')]), 0) + self.assertEqual(View.search_count([('name', 'in', ('Base', 'Extension'))]), 0) + + def test_cou_page_frontend(self): + Page = self.env['website.page'] + View = self.env['ir.ui.view'] + Website = self.env['website'] + + website2 = self.env['website'].create({ + 'name': 'My Second Website', + 'domain': '', + }) + + # currently the view unlink of website.page can't handle views with inherited views + self.extension_view.unlink() + + website_id = 1 + self.page_1.with_context(website_id=website_id).unlink() + + self.assertEqual(bool(self.base_view.exists()), False) + self.assertEqual(bool(self.page_1.exists()), False) + # Not COU but deleting a page will delete its menu (cascade) + self.assertEqual(bool(self.page_1_menu.exists()), False) + + pages = Page.search([('url', '=', '/page_1')]) + self.assertEqual(len(pages), Website.search_count([]) - 1, "A specific page for every website should have been created, except for the one from where we deleted the generic one.") + self.assertTrue(website_id not in pages.mapped('website_id').ids, "The website from which we deleted the generic page should not have a specific one.") + self.assertTrue(website_id not in View.search([('name', 'in', ('Base', 'Extension'))]).mapped('website_id').ids, "Same for views") + +@tagged('-at_install', 'post_install') +class WithContext(HttpCase): + def setUp(self): + super().setUp() + Page = self.env['website.page'] + View = self.env['ir.ui.view'] + base_view = View.create({ + 'name': 'Base', + 'type': 'qweb', + 'arch': ''' + + I am a generic page + + ''', + 'key': 'test.base_view', + }) + self.page = Page.create({ + 'view_id': base_view.id, + 'url': '/page_1', + 'is_published': True, + }) + + def test_unpublished_page(self): + specific_page = self.page.copy({'website_id': self.env['website'].get_current_website().id}) + specific_page.write({'is_published': False, 'arch': self.page.arch.replace('I am a generic page', 'I am a specific page')}) + + self.authenticate(None, None) + r = self.url_open(specific_page.url) + self.assertEqual(r.status_code, 404, "Restricted users should see a 404 and not the generic one as we unpublished the specific one") + + self.authenticate('admin', 'admin') + r = self.url_open(specific_page.url) + self.assertEqual(r.status_code, 200, "Admin should see the specific unpublished page") + self.assertEqual('I am a specific page' in r.text, True, "Admin should see the specific unpublished page") + + def test_search(self): + dbname = common.get_db_name() + admin_uid = self.env.ref('base.user_admin').id + website = self.env['website'].get_current_website() + + robot = self.xmlrpc_object.execute( + dbname, admin_uid, 'admin', + 'website', 'search_pages', [website.id], 'info' + ) + self.assertIn({'loc': '/website/info'}, robot) + + pages = self.xmlrpc_object.execute( + dbname, admin_uid, 'admin', + 'website', 'search_pages', [website.id], 'page' + ) + self.assertIn( + '/page_1', + [p['loc'] for p in pages], + ) diff --git a/addons/website/tests/test_performance.py b/addons/website/tests/test_performance.py new file mode 100644 index 00000000..ad29f647 --- /dev/null +++ b/addons/website/tests/test_performance.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests.common import HttpCase + +EXTRA_REQUEST = 5 +""" During tests, the query on 'base_registry_signaling, base_cache_signaling' + won't be executed on hot state, but 6 new queries related to the test cursor + will be added: + SAVEPOINT "test_cursor_4" + SAVEPOINT "test_cursor_4" + ROLLBACK TO SAVEPOINT "test_cursor_4" + SAVEPOINT "test_cursor_5" + [.. usual SQL Queries .. ] + SAVEPOINT "test_cursor_5" + ROLLBACK TO SAVEPOINT "test_cursor_5" +""" + + +class UtilPerf(HttpCase): + def _get_url_hot_query(self, url): + url += ('?' not in url and '?' or '') + '&nocache' + + # ensure worker is in hot state + self.url_open(url) + self.url_open(url) + + sql_count = self.registry.test_cr.sql_log_count + self.url_open(url) + return self.registry.test_cr.sql_log_count - sql_count - EXTRA_REQUEST + + +class TestStandardPerformance(UtilPerf): + def test_10_perf_sql_img_controller(self): + self.authenticate('demo', 'demo') + url = '/web/image/res.users/2/image_256' + self.assertEqual(self._get_url_hot_query(url), 7) + + def test_20_perf_sql_img_controller_bis(self): + url = '/web/image/website/1/favicon' + self.assertEqual(self._get_url_hot_query(url), 4) + self.authenticate('portal', 'portal') + self.assertEqual(self._get_url_hot_query(url), 4) + + +class TestWebsitePerformance(UtilPerf): + + def setUp(self): + super().setUp() + self.page, self.menu = self._create_page_with_menu('/sql_page') + + def _create_page_with_menu(self, url): + name = url[1:] + website = self.env['website'].browse(1) + page = self.env['website.page'].create({ + 'url': url, + 'name': name, + 'type': 'qweb', + 'arch': ' \ + \ +
\ + \ + ' % (name, name), + 'key': 'website.page_test_%s' % name, + 'is_published': True, + 'website_id': website.id, + 'track': False, + }) + menu = self.env['website.menu'].create({ + 'name': name, + 'url': url, + 'page_id': page.id, + 'website_id': website.id, + 'parent_id': website.menu_id.id + }) + return (page, menu) + + def test_10_perf_sql_queries_page(self): + # standard untracked website.page + self.assertEqual(self._get_url_hot_query(self.page.url), 11) + self.menu.unlink() + self.assertEqual(self._get_url_hot_query(self.page.url), 13) + + def test_15_perf_sql_queries_page(self): + # standard tracked website.page + self.page.track = True + self.assertEqual(self._get_url_hot_query(self.page.url), 19) + self.menu.unlink() + self.assertEqual(self._get_url_hot_query(self.page.url), 21) + + def test_20_perf_sql_queries_homepage(self): + # homepage "/" has its own controller + self.assertEqual(self._get_url_hot_query('/'), 20) + + def test_30_perf_sql_queries_page_no_layout(self): + # website.page with no call to layout templates + self.page.arch = '
I am a blank page
' + self.assertEqual(self._get_url_hot_query(self.page.url), 9) + + def test_40_perf_sql_queries_page_multi_level_menu(self): + # menu structure should not impact SQL requests + _, menu_a = self._create_page_with_menu('/a') + _, menu_aa = self._create_page_with_menu('/aa') + _, menu_b = self._create_page_with_menu('/b') + _, menu_bb = self._create_page_with_menu('/bb') + _, menu_bbb = self._create_page_with_menu('/bbb') + _, menu_bbbb = self._create_page_with_menu('/bbbb') + _, menu_bbbbb = self._create_page_with_menu('/bbbbb') + self._create_page_with_menu('c') + menu_bbbbb.parent_id = menu_bbbb + menu_bbbb.parent_id = menu_bbb + menu_bbb.parent_id = menu_bb + menu_bb.parent_id = menu_b + menu_aa.parent_id = menu_a + + self.assertEqual(self._get_url_hot_query(self.page.url), 11) + + def test_50_perf_sql_web_content(self): + # assets route /web/content/.. + self.url_open('/') # create assets attachments + assets_url = self.env['ir.attachment'].search([('url', '=like', '/web/content/%/web.assets_common%.js')], limit=1).url + self.assertEqual(self._get_url_hot_query(assets_url), 2) diff --git a/addons/website/tests/test_qweb.py b/addons/website/tests/test_qweb.py new file mode 100644 index 00000000..de87bd81 --- /dev/null +++ b/addons/website/tests/test_qweb.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from lxml import etree +import re + +from odoo import http, tools +from odoo.addons.base.tests.common import TransactionCaseWithUserDemo +from odoo.addons.website.tools import MockRequest +from odoo.modules.module import get_module_resource +from odoo.tests.common import TransactionCase + + +class TestQweb(TransactionCaseWithUserDemo): + def _load(self, module, *args): + tools.convert_file(self.cr, 'website', + get_module_resource(module, *args), + {}, 'init', False, 'test') + + def test_qweb_cdn(self): + self._load('website', 'tests', 'template_qweb_test.xml') + + website = self.env.ref('website.default_website') + website.write({ + "cdn_activated": True, + "cdn_url": "http://test.cdn" + }) + + demo = self.env['res.users'].search([('login', '=', 'demo')])[0] + demo.write({"signature": ''' + span + '''}) + + demo_env = self.env(user=demo) + + html = demo_env['ir.qweb']._render('website.test_template', {"user": demo}, website_id= website.id) + asset_data = etree.HTML(html).xpath('//*[@data-asset-xmlid]')[0] + asset_xmlid = asset_data.attrib.get('data-asset-xmlid') + asset_version = asset_data.attrib.get('data-asset-version') + + html = html.strip().decode('utf8') + html = re.sub(r'\?unique=[^"]+', '', html).encode('utf8') + + attachments = demo_env['ir.attachment'].search([('url', '=like', '/web/content/%-%/website.test_bundle.%')]) + self.assertEqual(len(attachments), 2) + + format_data = { + "js": attachments[0].url, + "css": attachments[1].url, + "user_id": demo.id, + "filename": "Marc%20Demo", + "alt": "Marc Demo", + "asset_xmlid": asset_xmlid, + "asset_version": asset_version, + } + + self.assertEqual(html, (""" + + + + + + + + + + + + + + x + x + xxx +
+ span +
+
%(alt)s
+ +""" % format_data).encode('utf8')) + + +class TestQwebProcessAtt(TransactionCase): + def setUp(self): + super(TestQwebProcessAtt, self).setUp() + self.website = self.env.ref('website.default_website') + self.env['res.lang']._activate_lang('fr_FR') + self.website.language_ids = self.env.ref('base.lang_en') + self.env.ref('base.lang_fr') + self.website.default_lang_id = self.env.ref('base.lang_en') + self.website.cdn_activated = True + self.website.cdn_url = "http://test.cdn" + self.website.cdn_filters = "\n".join(["^(/[a-z]{2}_[A-Z]{2})?/a$", "^(/[a-z]{2})?/a$", "^/b$"]) + + def _test_att(self, url, expect, tag='a', attribute='href'): + self.assertEqual( + self.env['ir.qweb']._post_processing_att(tag, {attribute: url}, {}), + expect + ) + + def test_process_att_no_request(self): + # no request so no URL rewriting + self._test_att('/', {'href': '/'}) + self._test_att('/en/', {'href': '/en/'}) + self._test_att('/fr/', {'href': '/fr/'}) + # no URL rewritting for CDN + self._test_att('/a', {'href': '/a'}) + + def test_process_att_no_website(self): + with MockRequest(self.env): + # no website so URL rewriting + self._test_att('/', {'href': '/'}) + self._test_att('/en/', {'href': '/en/'}) + self._test_att('/fr/', {'href': '/fr/'}) + # no URL rewritting for CDN + self._test_att('/a', {'href': '/a'}) + + def test_process_att_monolang_route(self): + with MockRequest(self.env, website=self.website, multilang=False): + # lang not changed in URL but CDN enabled + self._test_att('/a', {'href': 'http://test.cdn/a'}) + self._test_att('/en/a', {'href': 'http://test.cdn/en/a'}) + self._test_att('/b', {'href': 'http://test.cdn/b'}) + self._test_att('/en/b', {'href': '/en/b'}) + + def test_process_att_no_request_lang(self): + with MockRequest(self.env, website=self.website): + self._test_att('/', {'href': '/'}) + self._test_att('/en/', {'href': '/'}) + self._test_att('/fr/', {'href': '/fr/'}) + + def test_process_att_with_request_lang(self): + with MockRequest(self.env, website=self.website, context={'lang': 'fr_FR'}): + self._test_att('/', {'href': '/fr/'}) + self._test_att('/en/', {'href': '/'}) + self._test_att('/fr/', {'href': '/fr/'}) + + def test_process_att_matching_cdn_and_lang(self): + with MockRequest(self.env, website=self.website): + # lang prefix is added before CDN + self._test_att('/a', {'href': 'http://test.cdn/a'}) + self._test_att('/en/a', {'href': 'http://test.cdn/a'}) + self._test_att('/fr/a', {'href': 'http://test.cdn/fr/a'}) + self._test_att('/b', {'href': 'http://test.cdn/b'}) + self._test_att('/en/b', {'href': 'http://test.cdn/b'}) + self._test_att('/fr/b', {'href': '/fr/b'}) + + def test_process_att_no_route(self): + with MockRequest(self.env, website=self.website, context={'lang': 'fr_FR'}, routing=False): + # default on multilang=True if route is not /{module}/static/ + self._test_att('/web/static/hi', {'href': '/web/static/hi'}) + self._test_att('/my-page', {'href': '/fr/my-page'}) + + def test_process_att_url_crap(self): + with MockRequest(self.env, website=self.website): + match = http.root.get_db_router.return_value.bind.return_value.match + # #{fragment} is stripped from URL when testing route + self._test_att('/x#y?z', {'href': '/x#y?z'}) + match.assert_called_with('/x', method='POST', query_args=None) + + match.reset_calls() + self._test_att('/x?y#z', {'href': '/x?y#z'}) + match.assert_called_with('/x', method='POST', query_args='y') diff --git a/addons/website/tests/test_res_users.py b/addons/website/tests/test_res_users.py new file mode 100644 index 00000000..ef8d97ed --- /dev/null +++ b/addons/website/tests/test_res_users.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from psycopg2 import IntegrityError + +from odoo.tests.common import TransactionCase, new_test_user +from odoo.exceptions import ValidationError +from odoo.service.model import check +from odoo.tools import mute_logger + + +class TestWebsiteResUsers(TransactionCase): + + def setUp(self): + super().setUp() + websites = self.env['website'].create([ + {'name': 'Test Website'}, + {'name': 'Test Website 2'}, + ]) + self.website_1, self.website_2 = websites + + def test_no_website(self): + new_test_user(self.env, login='Pou', website_id=False) + with self.assertRaises(ValidationError): + new_test_user(self.env, login='Pou', website_id=False) + + def test_websites_set_null(self): + user_1 = new_test_user(self.env, login='Pou', website_id=self.website_1.id) + user_2 = new_test_user(self.env, login='Pou', website_id=self.website_2.id) + with self.assertRaises(ValidationError): + (user_1 | user_2).write({'website_id': False}) + + def test_null_and_website(self): + new_test_user(self.env, login='Pou', website_id=self.website_1.id) + new_test_user(self.env, login='Pou', website_id=False) + + def test_change_login(self): + new_test_user(self.env, login='Pou', website_id=self.website_1.id) + user_belle = new_test_user(self.env, login='Belle', website_id=self.website_1.id) + with self.assertRaises(IntegrityError), mute_logger('odoo.sql_db'): + user_belle.login = 'Pou' + + def test_change_login_no_website(self): + new_test_user(self.env, login='Pou', website_id=False) + user_belle = new_test_user(self.env, login='Belle', website_id=False) + with self.assertRaises(ValidationError): + user_belle.login = 'Pou' + + def test_same_website_message(self): + + @check # Check decorator, otherwise translation is not applied + def check_new_test_user(dbname): + new_test_user(self.env(context={'land': 'en_US'}), login='Pou', website_id=self.website_1.id) + + new_test_user(self.env, login='Pou', website_id=self.website_1.id) + + # Should be a ValidationError (with a nice translated error message), + # not an IntegrityError + with self.assertRaises(ValidationError), mute_logger('odoo.sql_db'): + check_new_test_user(self.env.registry._db.dbname) diff --git a/addons/website/tests/test_snippets.py b/addons/website/tests/test_snippets.py new file mode 100644 index 00000000..83f906f5 --- /dev/null +++ b/addons/website/tests/test_snippets.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import odoo +import odoo.tests + + +@odoo.tests.common.tagged('post_install', '-at_install', 'website_snippets') +class TestSnippets(odoo.tests.HttpCase): + + def test_01_empty_parents_autoremove(self): + self.start_tour("/?enable_editor=1", "snippet_empty_parent_autoremove", login='admin') diff --git a/addons/website/tests/test_theme.py b/addons/website/tests/test_theme.py new file mode 100644 index 00000000..845474d6 --- /dev/null +++ b/addons/website/tests/test_theme.py @@ -0,0 +1,14 @@ +# coding: utf-8 +from odoo.tests import common + + +class TestTheme(common.TransactionCase): + + def test_theme_remove_working(self): + """ This test ensure theme can be removed. + Theme removal is also the first step during theme installation. + """ + theme_common_module = self.env['ir.module.module'].search([('name', '=', 'theme_default')]) + website = self.env['website'].get_current_website() + website.theme_id = theme_common_module.id + self.env['ir.module.module']._theme_remove(website) diff --git a/addons/website/tests/test_ui.py b/addons/website/tests/test_ui.py new file mode 100644 index 00000000..1a629bc4 --- /dev/null +++ b/addons/website/tests/test_ui.py @@ -0,0 +1,194 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import odoo +import odoo.tests + + +@odoo.tests.tagged('-at_install', 'post_install') +class TestUiCustomizeTheme(odoo.tests.HttpCase): + def test_01_attachment_website_unlink(self): + ''' Some ir.attachment needs to be unlinked when a website is unlink, + otherwise some flows will just crash. That's the case when 2 website + have their theme color customized. Removing a website will make its + customized attachment generic, thus having 2 attachments with the + same URL available for other websites, leading to singleton errors + (among other). + + But no all attachment should be deleted, eg we don't want to delete + a SO or invoice PDF coming from an ecommerce order. + ''' + Website = self.env['website'] + Page = self.env['website.page'] + Attachment = self.env['ir.attachment'] + + website_default = Website.browse(1) + website_test = Website.create({'name': 'Website Test'}) + + # simulate attachment state when editing 2 theme through customize + custom_url = '/TEST/website/static/src/scss/options/colors/user_theme_color_palette.custom.web.assets_common.scss' + scss_attachment = Attachment.create({ + 'name': custom_url, + 'type': 'binary', + 'mimetype': 'text/scss', + 'datas': '', + 'url': custom_url, + 'website_id': website_default.id + }) + scss_attachment.copy({'website_id': website_test.id}) + + # simulate PDF from ecommerce order + # Note: it will only have its website_id flag if the website has a domain + # equal to the current URL (fallback or get_current_website()) + so_attachment = Attachment.create({ + 'name': 'SO036.pdf', + 'type': 'binary', + 'mimetype': 'application/pdf', + 'datas': '', + 'website_id': website_test.id + }) + + # avoid sql error on page website_id restrict + Page.search([('website_id', '=', website_test.id)]).unlink() + website_test.unlink() + self.assertEqual(Attachment.search_count([('url', '=', custom_url)]), 1, 'Should not left duplicates when deleting a website') + self.assertTrue(so_attachment.exists(), 'Most attachment should not be deleted') + self.assertFalse(so_attachment.website_id, 'Website should be removed') + + +@odoo.tests.tagged('-at_install', 'post_install') +class TestUiHtmlEditor(odoo.tests.HttpCase): + def test_html_editor_multiple_templates(self): + Website = self.env['website'] + View = self.env['ir.ui.view'] + Page = self.env['website.page'] + + self.generic_view = View.create({ + 'name': 'Generic', + 'type': 'qweb', + 'arch': ''' +
content
+ ''', + 'key': 'test.generic_view', + }) + + self.generic_page = Page.create({ + 'view_id': self.generic_view.id, + 'url': '/generic', + }) + + generic_page = Website.viewref('test.generic_view') + # Use an empty page layout with oe_structure id for this test + oe_structure_layout = ''' + + +
+ + + ''' + generic_page.arch = oe_structure_layout + self.start_tour("/", 'html_editor_multiple_templates', login='admin') + self.assertEqual(View.search_count([('key', '=', 'test.generic_view')]), 2, "homepage view should have been COW'd") + self.assertTrue(generic_page.arch == oe_structure_layout, "Generic homepage view should be untouched") + self.assertEqual(len(generic_page.inherit_children_ids.filtered(lambda v: 'oe_structure' in v.name)), 0, "oe_structure view should have been deleted when aboutus was COW") + specific_page = Website.with_context(website_id=1).viewref('test.generic_view') + self.assertTrue(specific_page.arch != oe_structure_layout, "Specific homepage view should have been changed") + self.assertEqual(len(specific_page.inherit_children_ids.filtered(lambda v: 'oe_structure' in v.name)), 1, "oe_structure view should have been created on the specific tree") + + def test_html_editor_scss(self): + self.start_tour("/", 'test_html_editor_scss', login='admin') + +@odoo.tests.tagged('-at_install', 'post_install') +class TestUiTranslate(odoo.tests.HttpCase): + def test_admin_tour_rte_translator(self): + fr_BE = self.env.ref('base.lang_fr_BE') + fr_BE.active = True + self.env.ref('website.default_website').language_ids |= fr_BE + self.start_tour("/", 'rte_translator', login='admin', timeout=120) + + +@odoo.tests.common.tagged('post_install', '-at_install') +class TestUi(odoo.tests.HttpCase): + + def test_01_admin_tour_homepage(self): + self.start_tour("/?enable_editor=1", 'homepage', login='admin') + + def test_02_restricted_editor(self): + self.restricted_editor = self.env['res.users'].create({ + 'name': 'Restricted Editor', + 'login': 'restricted', + 'password': 'restricted', + 'groups_id': [(6, 0, [ + self.ref('base.group_user'), + self.ref('website.group_website_publisher') + ])] + }) + self.start_tour("/", 'restricted_editor', login='restricted') + + def test_03_backend_dashboard(self): + self.start_tour("/", 'backend_dashboard', login='admin') + + def test_04_website_navbar_menu(self): + website = self.env['website'].search([], limit=1) + self.env['website.menu'].create({ + 'name': 'Test Tour Menu', + 'url': '/test-tour-menu', + 'parent_id': website.menu_id.id, + 'sequence': 0, + 'website_id': website.id, + }) + self.start_tour("/", 'website_navbar_menu') + + def test_05_specific_website_editor(self): + website_default = self.env['website'].search([], limit=1) + new_website = self.env['website'].create({'name': 'New Website'}) + website_editor_assets_view = self.env.ref('website.assets_wysiwyg') + self.env['ir.ui.view'].create({ + 'name': 'Editor Extension', + 'type': 'qweb', + 'inherit_id': website_editor_assets_view.id, + 'website_id': new_website.id, + 'arch': """ + + + + """, + }) + self.start_tour("/website/force/%s" % website_default.id, "generic_website_editor", login='admin') + self.start_tour("/website/force/%s" % new_website.id, "specific_website_editor", login='admin') + + def test_06_public_user_editor(self): + website_default = self.env['website'].search([], limit=1) + website_default.homepage_id.arch = """ + + +