summaryrefslogtreecommitdiff
path: root/addons/test_website
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/test_website
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/test_website')
-rw-r--r--addons/test_website/__init__.py5
-rw-r--r--addons/test_website/__manifest__.py26
-rw-r--r--addons/test_website/controllers/__init__.py4
-rw-r--r--addons/test_website/controllers/main.py140
-rw-r--r--addons/test_website/data/test_website_data.xml116
-rw-r--r--addons/test_website/models/__init__.py1
-rw-r--r--addons/test_website/models/model.py14
-rw-r--r--addons/test_website/security/ir.model.access.csv2
-rw-r--r--addons/test_website/static/src/js/test_error.js30
-rw-r--r--addons/test_website/static/tests/tours/custom_snippets.js96
-rw-r--r--addons/test_website/static/tests/tours/error_views.js152
-rw-r--r--addons/test_website/static/tests/tours/json_auth.js26
-rw-r--r--addons/test_website/static/tests/tours/reset_views.js109
-rw-r--r--addons/test_website/tests/__init__.py13
-rw-r--r--addons/test_website/tests/test_controller_args.py31
-rw-r--r--addons/test_website/tests/test_custom_snippet.py13
-rw-r--r--addons/test_website/tests/test_error.py10
-rw-r--r--addons/test_website/tests/test_is_multilang.py71
-rw-r--r--addons/test_website/tests/test_multi_company.py16
-rw-r--r--addons/test_website/tests/test_performance.py10
-rw-r--r--addons/test_website/tests/test_redirect.py162
-rw-r--r--addons/test_website/tests/test_reset_views.py111
-rw-r--r--addons/test_website/tests/test_session.py9
-rw-r--r--addons/test_website/tests/test_views_during_module_operation.py83
-rw-r--r--addons/test_website/views/templates.xml36
25 files changed, 1286 insertions, 0 deletions
diff --git a/addons/test_website/__init__.py b/addons/test_website/__init__.py
new file mode 100644
index 00000000..c48d23bb
--- /dev/null
+++ b/addons/test_website/__init__.py
@@ -0,0 +1,5 @@
+# -*- encoding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import controllers
+from . import models
diff --git a/addons/test_website/__manifest__.py b/addons/test_website/__manifest__.py
new file mode 100644
index 00000000..a8887131
--- /dev/null
+++ b/addons/test_website/__manifest__.py
@@ -0,0 +1,26 @@
+# -*- encoding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+{
+ 'name': 'Website Test',
+ 'version': '1.0',
+ 'category': 'Hidden',
+ 'sequence': 9876,
+ 'summary': 'Website Test, mainly for module install/uninstall tests',
+ 'description': """This module contains tests related to website. Those are
+present in a separate module as we are testing module install/uninstall/upgrade
+and we don't want to reload the website module every time, including it's possible
+dependencies. Neither we want to add in website module some routes, views and
+models which only purpose is to run tests.""",
+ 'depends': [
+ 'website',
+ ],
+ 'data': [
+ 'views/templates.xml',
+ 'data/test_website_data.xml',
+ 'security/ir.model.access.csv',
+ ],
+ 'installable': True,
+ 'application': False,
+ 'license': 'LGPL-3',
+}
diff --git a/addons/test_website/controllers/__init__.py b/addons/test_website/controllers/__init__.py
new file mode 100644
index 00000000..5d4b25db
--- /dev/null
+++ b/addons/test_website/controllers/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import main
diff --git a/addons/test_website/controllers/main.py b/addons/test_website/controllers/main.py
new file mode 100644
index 00000000..63333b81
--- /dev/null
+++ b/addons/test_website/controllers/main.py
@@ -0,0 +1,140 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import json
+import werkzeug
+
+from odoo import http
+from odoo.http import request
+from odoo.addons.portal.controllers.web import Home
+from odoo.exceptions import UserError, ValidationError, AccessError, MissingError, AccessDenied
+
+
+class WebsiteTest(Home):
+
+ @http.route('/test_view', type='http', auth='public', website=True, sitemap=False)
+ def test_view(self, **kwargs):
+ return request.render('test_website.test_view')
+
+ @http.route('/ignore_args/converteronly/<string:a>/', type='http', auth="public", website=True, sitemap=False)
+ def test_ignore_args_converter_only(self, a):
+ return request.make_response(json.dumps(dict(a=a, kw=None)))
+
+ @http.route('/ignore_args/none', type='http', auth="public", website=True, sitemap=False)
+ def test_ignore_args_none(self):
+ return request.make_response(json.dumps(dict(a=None, kw=None)))
+
+ @http.route('/ignore_args/a', type='http', auth="public", website=True, sitemap=False)
+ def test_ignore_args_a(self, a):
+ return request.make_response(json.dumps(dict(a=a, kw=None)))
+
+ @http.route('/ignore_args/kw', type='http', auth="public", website=True, sitemap=False)
+ def test_ignore_args_kw(self, a, **kw):
+ return request.make_response(json.dumps(dict(a=a, kw=kw)))
+
+ @http.route('/ignore_args/converter/<string:a>/', type='http', auth="public", website=True, sitemap=False)
+ def test_ignore_args_converter(self, a, b='youhou', **kw):
+ return request.make_response(json.dumps(dict(a=a, b=b, kw=kw)))
+
+ @http.route('/ignore_args/converter/<string:a>/nokw', type='http', auth="public", website=True, sitemap=False)
+ def test_ignore_args_converter_nokw(self, a, b='youhou'):
+ return request.make_response(json.dumps(dict(a=a, b=b)))
+
+ @http.route('/multi_company_website', type='http', auth="public", website=True, sitemap=False)
+ def test_company_context(self):
+ return request.make_response(json.dumps(request.context.get('allowed_company_ids')))
+
+ @http.route('/test_lang_url/<model("res.country"):country>', type='http', auth='public', website=True, sitemap=False)
+ def test_lang_url(self, **kwargs):
+ return request.render('test_website.test_view')
+
+ # Test Session
+
+ @http.route('/test_get_dbname', type='json', auth='public', website=True, sitemap=False)
+ def test_get_dbname(self, **kwargs):
+ return request.env.cr.dbname
+
+ # Test Error
+
+ @http.route('/test_error_view', type='http', auth='public', website=True, sitemap=False)
+ def test_error_view(self, **kwargs):
+ return request.render('test_website.test_error_view')
+
+ @http.route('/test_user_error_http', type='http', auth='public', website=True, sitemap=False)
+ def test_user_error_http(self, **kwargs):
+ raise UserError("This is a user http test")
+
+ @http.route('/test_user_error_json', type='json', auth='public', website=True, sitemap=False)
+ def test_user_error_json(self, **kwargs):
+ raise UserError("This is a user rpc test")
+
+ @http.route('/test_validation_error_http', type='http', auth='public', website=True, sitemap=False)
+ def test_validation_error_http(self, **kwargs):
+ raise ValidationError("This is a validation http test")
+
+ @http.route('/test_validation_error_json', type='json', auth='public', website=True, sitemap=False)
+ def test_validation_error_json(self, **kwargs):
+ raise ValidationError("This is a validation rpc test")
+
+ @http.route('/test_access_error_json', type='json', auth='public', website=True, sitemap=False)
+ def test_access_error_json(self, **kwargs):
+ raise AccessError("This is an access rpc test")
+
+ @http.route('/test_access_error_http', type='http', auth='public', website=True, sitemap=False)
+ def test_access_error_http(self, **kwargs):
+ raise AccessError("This is an access http test")
+
+ @http.route('/test_missing_error_json', type='json', auth='public', website=True, sitemap=False)
+ def test_missing_error_json(self, **kwargs):
+ raise MissingError("This is a missing rpc test")
+
+ @http.route('/test_missing_error_http', type='http', auth='public', website=True, sitemap=False)
+ def test_missing_error_http(self, **kwargs):
+ raise MissingError("This is a missing http test")
+
+ @http.route('/test_internal_error_json', type='json', auth='public', website=True, sitemap=False)
+ def test_internal_error_json(self, **kwargs):
+ raise werkzeug.exceptions.InternalServerError()
+
+ @http.route('/test_internal_error_http', type='http', auth='public', website=True, sitemap=False)
+ def test_internal_error_http(self, **kwargs):
+ raise werkzeug.exceptions.InternalServerError()
+
+ @http.route('/test_access_denied_json', type='json', auth='public', website=True, sitemap=False)
+ def test_denied_error_json(self, **kwargs):
+ raise AccessDenied("This is an access denied rpc test")
+
+ @http.route('/test_access_denied_http', type='http', auth='public', website=True, sitemap=False)
+ def test_denied_error_http(self, **kwargs):
+ raise AccessDenied("This is an access denied http test")
+
+ @http.route(['/get'], type='http', auth="public", methods=['GET'], website=True, sitemap=False)
+ def get_method(self, **kw):
+ return request.make_response('get')
+
+ @http.route(['/post'], type='http', auth="public", methods=['POST'], website=True, sitemap=False)
+ def post_method(self, **kw):
+ return request.make_response('post')
+
+ @http.route(['/get_post'], type='http', auth="public", methods=['GET', 'POST'], website=True, sitemap=False)
+ def get_post_method(self, **kw):
+ return request.make_response('get_post')
+
+ @http.route(['/get_post_nomultilang'], type='http', auth="public", methods=['GET', 'POST'], website=True, multilang=False, sitemap=False)
+ def get_post_method_no_multilang(self, **kw):
+ return request.make_response('get_post_nomultilang')
+
+ # Test Perfs
+
+ @http.route(['/empty_controller_test'], type='http', auth='public', website=True, multilang=False, sitemap=False)
+ def empty_controller_test(self, **kw):
+ return 'Basic Controller Content'
+
+ # Test Redirects
+ @http.route(['/test_website/country/<model("res.country"):country>'], type='http', auth="public", website=True, sitemap=False)
+ def test_model_converter_country(self, country, **kw):
+ return request.render('test_website.test_redirect_view', {'country': country})
+
+ @http.route(['/test_website/200/<model("test.model"):rec>'], type='http', auth="public", website=True, sitemap=False)
+ def test_model_converter_seoname(self, rec, **kw):
+ return request.make_response('ok')
diff --git a/addons/test_website/data/test_website_data.xml b/addons/test_website/data/test_website_data.xml
new file mode 100644
index 00000000..0f9fc54c
--- /dev/null
+++ b/addons/test_website/data/test_website_data.xml
@@ -0,0 +1,116 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data noupdate="1">
+
+ <record id="test_model_publish" model="ir.rule">
+ <field name="name">Public user: read only website published</field>
+ <field name="model_id" ref="test_website.model_test_model"/>
+ <field name="groups" eval="[(4, ref('base.group_public'))]"/>
+ <field name="domain_force">[('website_published','=', True)]</field>
+ <field name="perm_read" eval="True"/>
+ </record>
+
+
+ <!-- RECORDS FOR RESET VIEWS TESTS -->
+ <record id="test_view" model="ir.ui.view">
+ <field name="name">Test View</field>
+ <field name="type">qweb</field>
+ <field name="key">test_website.test_view</field>
+ <field name="arch" type="xml">
+ <t name="Test View" priority="29" t-name="test_website.test_view">
+ <t t-call="website.layout">
+ <p>Test View</p>
+ <p>placeholder</p>
+ </t>
+ </t>
+ </field>
+ </record>
+ <record id="test_page_view" model="ir.ui.view">
+ <field name="name">Test Page View</field>
+ <field name="type">qweb</field>
+ <field name="key">test_website.test_page_view</field>
+ <field name="arch" type="xml">
+ <t name="Test Page View" priority="29" t-name="test_website.test_page_view">
+ <t t-call="website.layout">
+ <div id="oe_structure_test_website_page" class="oe_structure oe_empty"/>
+ <p>Test Page View</p>
+ <p>placeholder</p>
+ </t>
+ </t>
+ </field>
+ </record>
+ <record id="test_error_view" model="ir.ui.view">
+ <field name="name">Test Error View</field>
+ <field name="type">qweb</field>
+ <field name="key">test_website.test_error_view</field>
+ <field name="arch" type="xml">
+ <t name="Test Error View" t-name="test_website.test_error_view">
+ <t t-call="website.layout">
+ <div class="container">
+ <h1>Test Error View</h1>
+ <div class="row">
+ <ul class="list-group http_error col-6">
+ <li class="list-group-item list-group-item-primary"><h2>http Errors</h2></li>
+ <li class="list-group-item"><a href="/test_user_error_http">http UserError (400)</a></li>
+ <li class="list-group-item"><a href="/test_validation_error_http">http ValidationError (400)</a></li>
+ <li class="list-group-item"><a href="/test_missing_error_http">http MissingError (400)</a></li>
+ <li class="list-group-item"><a href="/test_access_error_http">http AccessError (403)</a></li>
+ <li class="list-group-item"><a href="/test_access_denied_http">http AccessDenied (403)</a></li>
+ <li class="list-group-item"><a href="/test_internal_error_http">http InternalServerError (500)</a></li>
+ <li class="list-group-item"><a href="/test_not_found_http">http NotFound (404)</a></li>
+ </ul>
+ <ul class="list-group rpc_error col-6">
+ <li class="list-group-item list-group-item-primary"><h2>rpc Warnings</h2></li>
+ <li class="list-group-item"><a href="/test_user_error_json">rpc UserError</a></li>
+ <li class="list-group-item"><a href="/test_validation_error_json">rpc ValidationError</a></li>
+ <li class="list-group-item"><a href="/test_missing_error_json">rpc MissingError</a></li>
+ <li class="list-group-item"><a href="/test_access_error_json">rpc AccessError</a></li>
+ <li class="list-group-item"><a href="/test_access_denied_json">rpc AccessDenied</a></li>
+ <li class="list-group-item list-group-item-primary"><h2>rpc Errors</h2></li>
+ <li class="list-group-item"><a href="/test_internal_error_json">rpc InternalServerError</a></li>
+ </ul>
+ </div>
+ </div>
+ </t>
+ </t>
+ </field>
+ </record>
+ <record id="test_page" model="website.page">
+ <field name="is_published">True</field>
+ <field name="url">/test_page_view</field>
+ <field name="view_id" ref="test_page_view"/>
+ <field name="website_indexed" eval="False"/>
+ </record>
+ <record id="test_view_to_be_t_called" model="ir.ui.view">
+ <field name="name">Test View To Be t-called</field>
+ <field name="type">qweb</field>
+ <field name="key">test_website.test_view_to_be_t_called</field>
+ <field name="arch" type="xml">
+ <t name="Test View To Be t-called" priority="29" t-name="test_website.test_view_to_be_t_called">
+ <p>Test View To Be t-called</p>
+ <p>placeholder</p>
+ </t>
+ </field>
+ </record>
+ <template id="test_view_child_broken" inherit_id="test_website.test_view" active="False">
+ <xpath expr="//p[last()]" position="replace">
+ <p>Test View Child Broken</p>
+ <p>placeholder</p>
+ </xpath>
+ </template>
+
+ <!-- RECORDS FOR MODULE OPERATION TESTS -->
+ <template id="update_module_base_view">
+ <div>I am a base view</div>
+ </template>
+
+ <!-- RECORDS FOR REDIRECT TESTS -->
+ <template id="test_redirect_view">
+ <t t-esc="country.name"/>
+ <t t-if="not request.env.user._is_public()" t-esc="'Logged In'"/>
+ <!-- `href` is send through `url_for` for non editor users -->
+ <a href="/test_website/country/andorra-1">I am a link</a>
+ </template>
+
+ </data>
+</odoo>
diff --git a/addons/test_website/models/__init__.py b/addons/test_website/models/__init__.py
new file mode 100644
index 00000000..9186ee3a
--- /dev/null
+++ b/addons/test_website/models/__init__.py
@@ -0,0 +1 @@
+from . import model
diff --git a/addons/test_website/models/model.py b/addons/test_website/models/model.py
new file mode 100644
index 00000000..626e3bec
--- /dev/null
+++ b/addons/test_website/models/model.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models
+
+
+class TestModel(models.Model):
+ """ Add website option in server actions. """
+
+ _name = 'test.model'
+ _inherit = ['website.seo.metadata', 'website.published.mixin']
+ _description = 'Website Model Test'
+
+ name = fields.Char(required=1)
diff --git a/addons/test_website/security/ir.model.access.csv b/addons/test_website/security/ir.model.access.csv
new file mode 100644
index 00000000..681cacee
--- /dev/null
+++ b/addons/test_website/security/ir.model.access.csv
@@ -0,0 +1,2 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_test_model,access_test_model,model_test_model,,1,0,0,0
diff --git a/addons/test_website/static/src/js/test_error.js b/addons/test_website/static/src/js/test_error.js
new file mode 100644
index 00000000..8a0f909d
--- /dev/null
+++ b/addons/test_website/static/src/js/test_error.js
@@ -0,0 +1,30 @@
+odoo.define('website_forum.test_error', function (require) {
+'use strict';
+
+var publicWidget = require('web.public.widget');
+
+publicWidget.registry.testError = publicWidget.Widget.extend({
+ selector: '.rpc_error',
+ events: {
+ 'click a': '_onRpcErrorClick',
+ },
+
+ //----------------------------------------------------------------------
+ // Handlers
+ //----------------------------------------------------------------------
+
+ /**
+ * make a rpc call with the href of the DOM element clicked
+ * @private
+ * @param {Event} ev
+ * @returns {Promise}
+ */
+ _onRpcErrorClick: function (ev) {
+ ev.preventDefault();
+ var $link = $(ev.currentTarget);
+ return this._rpc({
+ route: $link.attr('href'),
+ });
+ }
+});
+});
diff --git a/addons/test_website/static/tests/tours/custom_snippets.js b/addons/test_website/static/tests/tours/custom_snippets.js
new file mode 100644
index 00000000..22f15c9a
--- /dev/null
+++ b/addons/test_website/static/tests/tours/custom_snippets.js
@@ -0,0 +1,96 @@
+odoo.define('test_website.custom_snippets', function (require) {
+'use strict';
+
+var tour = require('web_tour.tour');
+
+/**
+ * The purpose of this tour is to check the custom snippets flow:
+ *
+ * -> go to edit mode
+ * -> drag a banner into page content
+ * -> customize banner (set text)
+ * -> save banner as custom snippet
+ * -> confirm name (remove in master when implicit default name feature is implemented)
+ * -> confirm save & reload (remove in master because reload is not needed anymore)
+ * -> ensure custom snippet is available
+ * -> drag custom snippet
+ * -> ensure block appears as banner
+ * -> ensure block appears as custom banner
+ * -> delete custom snippet
+ * -> confirm delete
+ * -> ensure it was deleted
+ */
+
+tour.register('test_custom_snippet', {
+ url: '/',
+ test: true
+}, [
+ {
+ content: "enter edit mode",
+ trigger: "a[data-action=edit]"
+ },
+ {
+ content: "drop a snippet",
+ trigger: "#oe_snippets .oe_snippet[name='Banner'] .oe_snippet_thumbnail:not(.o_we_already_dragging)",
+ extra_trigger: "body.editor_enable.editor_has_snippets",
+ moveTrigger: ".oe_drop_zone",
+ run: "drag_and_drop #wrap",
+ },
+ {
+ content: "customize snippet",
+ trigger: "#wrapwrap .s_banner h1",
+ run: "text",
+ consumeEvent: "input",
+ },
+ {
+ content: "save custom snippet",
+ trigger: ".snippet-option-SnippetSave we-button",
+ },
+ {
+ content: "confirm save name",
+ trigger: ".modal-dialog button span:contains('Save')",
+ },
+ {
+ content: "confirm save and reload",
+ trigger: ".modal-dialog button span:contains('Save and Reload')",
+ },
+ {
+ content: "ensure custom snippet appeared",
+ trigger: "#oe_snippets .oe_snippet[name='Custom Banner']",
+ run: function() {}, // check
+ },
+ {
+ content: "drop custom snippet",
+ trigger: ".oe_snippet[name='Custom Banner'] .oe_snippet_thumbnail:not(.o_we_already_dragging)",
+ extra_trigger: "body.editor_enable.editor_has_snippets",
+ moveTrigger: ".oe_drop_zone",
+ run: "drag_and_drop #wrap",
+ },
+ {
+ content: "ensure banner section exists",
+ trigger: "#wrap section[data-name='Banner']",
+ run: function() {}, // check
+ },
+ {
+ content: "ensure custom banner section exists",
+ trigger: "#wrap section[data-name='Custom Banner']",
+ run: function() {}, // check
+ },
+ {
+ content: "delete custom snippet",
+ trigger: ".oe_snippet[name='Custom Banner'] we-button.o_delete_btn",
+ extra_trigger: ".oe_snippet[name='Custom Banner'] .oe_snippet_thumbnail:not(.o_we_already_dragging)",
+ },
+ {
+ content: "confirm delete",
+ trigger: ".modal-dialog button:has(span:contains('Yes'))",
+ },
+ {
+ content: "ensure custom snippet disappeared",
+ trigger: "#oe_snippets",
+ extra_trigger: "#oe_snippets:not(:has(.oe_snippet[name='Custom Banner']))",
+ run: function() {}, // check
+ },
+]);
+
+});
diff --git a/addons/test_website/static/tests/tours/error_views.js b/addons/test_website/static/tests/tours/error_views.js
new file mode 100644
index 00000000..0d0d4961
--- /dev/null
+++ b/addons/test_website/static/tests/tours/error_views.js
@@ -0,0 +1,152 @@
+odoo.define('test_website.error_views', function (require) {
+'use strict';
+
+var tour = require('web_tour.tour');
+
+tour.register('test_error_website', {
+ test: true,
+ url: '/test_error_view',
+},
+[
+ // RPC ERROR
+ {
+ content: "trigger rpc user error",
+ trigger: 'a[href="/test_user_error_json"]',
+ }, {
+ content: "rpc user error modal has message",
+ extra_trigger: 'div.toast-body:contains("This is a user rpc test")',
+ trigger: 'button.o_notification_close',
+ }, {
+ content: "trigger rpc access error",
+ trigger: 'a[href="/test_access_error_json"]',
+ }, {
+ content: "rpc access error modal has message",
+ extra_trigger: 'div.toast-body:contains("This is an access rpc test")',
+ trigger: 'button.o_notification_close',
+ }, {
+ content: "trigger validation rpc error",
+ trigger: 'a[href="/test_validation_error_json"]',
+ }, {
+ content: "rpc validation error modal has message",
+ extra_trigger: 'div.toast-body:contains("This is a validation rpc test")',
+ trigger: 'button.o_notification_close',
+ }, {
+ content: "trigger rpc missing error",
+ trigger: 'a[href="/test_missing_error_json"]',
+ }, {
+ content: "rpc missing error modal has message",
+ extra_trigger: 'div.toast-body:contains("This is a missing rpc test")',
+ trigger: 'button.o_notification_close',
+ }, {
+ content: "trigger rpc error 403",
+ trigger: 'a[href="/test_access_denied_json"]',
+ }, {
+ content: "rpc error 403 modal has message",
+ extra_trigger: 'div.toast-body:contains("This is an access denied rpc test")',
+ trigger: 'button.o_notification_close',
+ }, {
+ content: "trigger rpc error 500",
+ trigger: 'a[href="/test_internal_error_json"]',
+ }, {
+ content: "rpc error 500 modal is an ErrorDialog",
+ extra_trigger: 'div.o_dialog_error.modal-body div.alert.alert-warning',
+ trigger: 'button.btn.btn-primary[type="button"]',
+ },
+ // HTTP ERROR
+ {
+ content: "trigger http user error",
+ trigger: 'body',
+ run: function () {
+ window.location.href = window.location.origin + '/test_user_error_http?debug=0';
+ },
+ }, {
+ content: "http user error page has title and message",
+ extra_trigger: 'h1:contains("Something went wrong.")',
+ trigger: 'div.container pre:contains("This is a user http test")',
+ run: function () {
+ window.location.href = window.location.origin + '/test_user_error_http?debug=1';
+ },
+ }, {
+ content: "http user error page debug has title and message open",
+ extra_trigger: 'h1:contains("Something went wrong.")',
+ trigger: 'div#error_main.collapse.show pre:contains("This is a user http test")',
+ run: function () {},
+ }, {
+ content: "http user error page debug has traceback closed",
+ trigger: 'body:has(div#error_traceback.collapse:not(.show) pre#exception_traceback)',
+ run: function () {
+ window.location.href = window.location.origin + '/test_validation_error_http?debug=0';
+ },
+ }, {
+ content: "http validation error page has title and message",
+ extra_trigger: 'h1:contains("Something went wrong.")',
+ trigger: 'div.container pre:contains("This is a validation http test")',
+ run: function () {
+ window.location.href = window.location.origin + '/test_validation_error_http?debug=1';
+ },
+ }, {
+ content: "http validation error page debug has title and message open",
+ extra_trigger: 'h1:contains("Something went wrong.")',
+ trigger: 'div#error_main.collapse.show pre:contains("This is a validation http test")',
+ run: function () {},
+ }, {
+ content: "http validation error page debug has traceback closed",
+ trigger: 'body:has(div#error_traceback.collapse:not(.show) pre#exception_traceback)',
+ run: function () {
+ window.location.href = window.location.origin + '/test_access_error_http?debug=0';
+ },
+ }, {
+ content: "http access error page has title and message",
+ extra_trigger: 'h1:contains("403: Forbidden")',
+ trigger: 'div.container pre:contains("This is an access http test")',
+ run: function () {
+ window.location.href = window.location.origin + '/test_access_error_http?debug=1';
+ },
+ }, {
+ content: "http access error page debug has title and message open",
+ extra_trigger: 'h1:contains("403: Forbidden")',
+ trigger: 'div#error_main.collapse.show pre:contains("This is an access http test")',
+ run: function () {},
+ }, {
+ content: "http access error page debug has traceback closed",
+ trigger: 'body:has(div#error_traceback.collapse:not(.show) pre#exception_traceback)',
+ run: function () {
+ window.location.href = window.location.origin + '/test_missing_error_http?debug=0';
+ },
+ }, {
+ content: "http missing error page has title and message",
+ extra_trigger: 'h1:contains("Something went wrong.")',
+ trigger: 'div.container pre:contains("This is a missing http test")',
+ run: function () {
+ window.location.href = window.location.origin + '/test_missing_error_http?debug=1';
+ },
+ }, {
+ content: "http missing error page debug has title and message open",
+ extra_trigger: 'h1:contains("Something went wrong.")',
+ trigger: 'div#error_main.collapse.show pre:contains("This is a missing http test")',
+ run: function () {},
+ }, {
+ content: "http missing error page debug has traceback closed",
+ trigger: 'body:has(div#error_traceback.collapse:not(.show) pre#exception_traceback)',
+ run: function () {
+ window.location.href = window.location.origin + '/test_access_denied_http?debug=0';
+ },
+ }, {
+ content: "http error 403 page has title but no message",
+ extra_trigger: 'h1:contains("403: Forbidden")',
+ trigger: 'div#wrap:not(:has(pre:contains("This is an access denied http test"))', //See ir_http.py handle_exception, the exception is replaced so there is no message !
+ run: function () {
+ window.location.href = window.location.origin + '/test_access_denied_http?debug=1';
+ },
+ }, {
+ content: "http 403 error page debug has title but no message",
+ extra_trigger: 'h1:contains("403: Forbidden")',
+ trigger: 'div#debug_infos:not(:has(#error_main))',
+ run: function () {},
+ }, {
+ content: "http 403 error page debug has traceback open",
+ trigger: 'body:has(div#error_traceback.collapse.show pre#exception_traceback)',
+ run: function () {},
+ },
+]);
+});
diff --git a/addons/test_website/static/tests/tours/json_auth.js b/addons/test_website/static/tests/tours/json_auth.js
new file mode 100644
index 00000000..ba89bed4
--- /dev/null
+++ b/addons/test_website/static/tests/tours/json_auth.js
@@ -0,0 +1,26 @@
+odoo.define('test_website.json_auth', function (require) {
+'use strict';
+
+var tour = require('web_tour.tour');
+var session = require('web.session')
+
+tour.register('test_json_auth', {
+ test: true,
+}, [{
+ trigger: 'body',
+ run: async function () {
+ await session.rpc('/test_get_dbname').then( function (result){
+ return session.rpc("/web/session/authenticate", {
+ db: result,
+ login: 'admin',
+ password: 'admin'
+ });
+ });
+ window.location.href = window.location.origin;
+ },
+}, {
+ trigger: 'span:contains(Mitchell Admin), span:contains(Administrator)',
+ run: function () {},
+}
+]);
+});
diff --git a/addons/test_website/static/tests/tours/reset_views.js b/addons/test_website/static/tests/tours/reset_views.js
new file mode 100644
index 00000000..cf2853f4
--- /dev/null
+++ b/addons/test_website/static/tests/tours/reset_views.js
@@ -0,0 +1,109 @@
+odoo.define('test_website.reset_views', function (require) {
+'use strict';
+
+var tour = require("web_tour.tour");
+
+var BROKEN_STEP = {
+ // because saving a broken template opens a recovery page with no assets
+ // there's no way for the tour to resume on the new page, and thus no way
+ // to properly wait for the page to be saved & reloaded in order to fix the
+ // race condition of a tour ending on a side-effect (with the possible
+ // exception of somehow telling the harness / browser to do it)
+ trigger: 'body',
+ run: function () {}
+};
+tour.register('test_reset_page_view_complete_flow_part1', {
+ test: true,
+ url: '/test_page_view',
+},
+ [
+ // 1. Edit the page through Edit Mode, it will COW the view
+ {
+ content: "enter edit mode",
+ trigger: "a[data-action=edit]"
+ },
+ {
+ content: "drop a snippet",
+ trigger: "#oe_snippets .oe_snippet:has(.s_cover) .oe_snippet_thumbnail",
+ // id starting by 'oe_structure..' will actually create an inherited view
+ run: "drag_and_drop #oe_structure_test_website_page",
+ },
+ {
+ content: "save the page",
+ extra_trigger: '#oe_structure_test_website_page.o_dirty',
+ trigger: "button[data-action=save]",
+ },
+ // 2. Edit that COW'd view in the HTML editor to break it.
+ {
+ content: "open customize menu",
+ extra_trigger: "body:not(.editor_enable)",
+ trigger: '#customize-menu > a',
+ },
+ {
+ content: "open html editor",
+ trigger: '#html_editor',
+ },
+ {
+ content: "add a broken t-field in page DOM",
+ trigger: 'div.ace_line .ace_xml:contains("placeholder")',
+ run: function () {
+ ace.edit('ace-view-editor').getSession().insert({row: 4, column: 1}, '<t t-field="not.exist"/>\n');
+ },
+ },
+ {
+ content: "save the html editor",
+ extra_trigger: '.ace_content:contains("not.exist")',
+ trigger: ".o_ace_view_editor button[data-action=save]",
+ },
+ BROKEN_STEP
+ ]
+);
+
+tour.register('test_reset_page_view_complete_flow_part2', {
+ test: true,
+ url: '/test_page_view',
+},
+ [
+ {
+ content: "check that the view got fixed",
+ trigger: 'p:containsExact("Test Page View")',
+ run: function () {}, // it's a check
+ },
+ {
+ content: "check that the inherited COW view is still there (created during edit mode)",
+ trigger: '#oe_structure_test_website_page .s_cover',
+ run: function () {}, // it's a check
+ },
+ //4. Now break the inherited view created when dropping a snippet
+ {
+ content: "open customize menu",
+ trigger: '#customize-menu > a',
+ },
+ {
+ content: "open html editor",
+ trigger: '#html_editor',
+ },
+ {
+ content: "select oe_structure view",
+ trigger: '#s2id_ace-view-list', // use select2 version
+ run: function () {
+ var viewId = $('#ace-view-list option:contains("oe_structure_test_website_page")').val();
+ $('#ace-view-list').val(viewId).trigger('change');
+ },
+ },
+ {
+ content: "add a broken t-field in page DOM",
+ trigger: 'div.ace_line .ace_xml:contains("oe_structure_test_website_page")',
+ run: function () {
+ ace.edit('ace-view-editor').getSession().insert({row: 4, column: 1}, '<t t-field="not.exist"/>\n');
+ },
+ },
+ {
+ content: "save the html editor",
+ trigger: ".o_ace_view_editor button[data-action=save]",
+ },
+ BROKEN_STEP
+ ]
+);
+
+});
diff --git a/addons/test_website/tests/__init__.py b/addons/test_website/tests/__init__.py
new file mode 100644
index 00000000..38da4577
--- /dev/null
+++ b/addons/test_website/tests/__init__.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import test_controller_args
+from . import test_custom_snippet
+from . import test_error
+from . import test_is_multilang
+from . import test_multi_company
+from . import test_performance
+from . import test_redirect
+from . import test_reset_views
+from . import test_session
+from . import test_views_during_module_operation
diff --git a/addons/test_website/tests/test_controller_args.py b/addons/test_website/tests/test_controller_args.py
new file mode 100644
index 00000000..fc21c22a
--- /dev/null
+++ b/addons/test_website/tests/test_controller_args.py
@@ -0,0 +1,31 @@
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import odoo.tests
+
+
+@odoo.tests.common.tagged('post_install', '-at_install')
+class TestWebsiteControllerArgs(odoo.tests.HttpCase):
+
+ def test_crawl_args(self):
+ req = self.url_open('/ignore_args/converter/valueA/?b=valueB&c=valueC')
+ self.assertEqual(req.status_code, 200)
+ self.assertEqual(req.json(), {'a': 'valueA', 'b': 'valueB', 'kw': {'c': 'valueC'}})
+
+ req = self.url_open('/ignore_args/converter/valueA/nokw?b=valueB&c=valueC')
+ self.assertEqual(req.status_code, 200)
+ self.assertEqual(req.json(), {'a': 'valueA', 'b': 'valueB'})
+
+ req = self.url_open('/ignore_args/converteronly/valueA/?b=valueB&c=valueC')
+ self.assertEqual(req.status_code, 200)
+ self.assertEqual(req.json(), {'a': 'valueA', 'kw': None})
+
+ req = self.url_open('/ignore_args/none?a=valueA&b=valueB')
+ self.assertEqual(req.status_code, 200)
+ self.assertEqual(req.json(), {'a': None, 'kw': None})
+
+ req = self.url_open('/ignore_args/a?a=valueA&b=valueB')
+ self.assertEqual(req.status_code, 200)
+ self.assertEqual(req.json(), {'a': 'valueA', 'kw': None})
+
+ req = self.url_open('/ignore_args/kw?a=valueA&b=valueB')
+ self.assertEqual(req.status_code, 200)
+ self.assertEqual(req.json(), {'a': 'valueA', 'kw': {'b': 'valueB'}})
diff --git a/addons/test_website/tests/test_custom_snippet.py b/addons/test_website/tests/test_custom_snippet.py
new file mode 100644
index 00000000..c79ed227
--- /dev/null
+++ b/addons/test_website/tests/test_custom_snippet.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import odoo.tests
+from odoo.tools import mute_logger
+
+
+@odoo.tests.common.tagged('post_install', '-at_install')
+class TestCustomSnippet(odoo.tests.HttpCase):
+
+ @mute_logger('odoo.addons.http_routing.models.ir_http', 'odoo.http')
+ def test_01_run_tour(self):
+ self.start_tour("/", 'test_custom_snippet', login="admin")
diff --git a/addons/test_website/tests/test_error.py b/addons/test_website/tests/test_error.py
new file mode 100644
index 00000000..f4c9334e
--- /dev/null
+++ b/addons/test_website/tests/test_error.py
@@ -0,0 +1,10 @@
+import odoo.tests
+from odoo.tools import mute_logger
+
+
+@odoo.tests.common.tagged('post_install', '-at_install')
+class TestWebsiteError(odoo.tests.HttpCase):
+
+ @mute_logger('odoo.addons.http_routing.models.ir_http', 'odoo.http')
+ def test_01_run_test(self):
+ self.start_tour("/test_error_view", 'test_error_website')
diff --git a/addons/test_website/tests/test_is_multilang.py b/addons/test_website/tests/test_is_multilang.py
new file mode 100644
index 00000000..1c7d6180
--- /dev/null
+++ b/addons/test_website/tests/test_is_multilang.py
@@ -0,0 +1,71 @@
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import odoo.tests
+import lxml
+
+
+@odoo.tests.common.tagged('post_install', '-at_install')
+class TestIsMultiLang(odoo.tests.HttpCase):
+
+ def test_01_is_multilang_url(self):
+ website = self.env['website'].search([], limit=1)
+ fr = self.env.ref('base.lang_fr').sudo()
+ en = self.env.ref('base.lang_en').sudo()
+
+ fr.active = True
+ fr_prefix = "/" + fr.iso_code
+
+ website.default_lang_id = en
+ website.language_ids = en + fr
+
+ for data in [None, {'post': True}]: # GET / POST
+ body = lxml.html.fromstring(self.url_open('/fr/multi_url', data=data).content)
+
+ self.assertEqual(fr_prefix + '/get', body.find('./a[@id="get"]').get('href'))
+ self.assertEqual(fr_prefix + '/post', body.find('./form[@id="post"]').get('action'))
+ self.assertEqual(fr_prefix + '/get_post', body.find('./a[@id="get_post"]').get('href'))
+ self.assertEqual('/get_post_nomultilang', body.find('./a[@id="get_post_nomultilang"]').get('href'))
+
+ def test_02_url_lang_code_underscore(self):
+ website = self.env['website'].browse(1)
+ it = self.env.ref('base.lang_it').sudo()
+ en = self.env.ref('base.lang_en').sudo()
+ be = self.env.ref('base.lang_fr_BE').sudo()
+ country1 = self.env['res.country'].create({'name': "My Super Country"})
+
+ it.active = True
+ be.active = True
+ website.domain = 'http://127.0.0.1:8069' # for _is_canonical_url
+ website.default_lang_id = en
+ website.language_ids = en + it + be
+ params = {
+ 'src': country1.name,
+ 'value': country1.name + ' Italia',
+ 'type': 'model',
+ 'name': 'res.country,name',
+ 'res_id': country1.id,
+ 'lang': it.code,
+ 'state': 'translated',
+ }
+ self.env['ir.translation'].create(params)
+ params.update({
+ 'value': country1.name + ' Belgium',
+ 'lang': be.code,
+ })
+ self.env['ir.translation'].create(params)
+ r = self.url_open('/test_lang_url/%s' % country1.id)
+ self.assertEqual(r.status_code, 200)
+ self.assertTrue(r.url.endswith('/test_lang_url/my-super-country-%s' % country1.id))
+
+ r = self.url_open('/%s/test_lang_url/%s' % (it.url_code, country1.id))
+ self.assertEqual(r.status_code, 200)
+ self.assertTrue(r.url.endswith('/%s/test_lang_url/my-super-country-italia-%s' % (it.url_code, country1.id)))
+
+ body = lxml.html.fromstring(r.content)
+ # Note: this test is indirectly testing the `ref=canonical` tag is correctly set,
+ # as it is required in order for `rel=alternate` tags to be inserted in the DOM
+ it_href = body.find('./head/link[@rel="alternate"][@hreflang="it"]').get('href')
+ fr_href = body.find('./head/link[@rel="alternate"][@hreflang="fr"]').get('href')
+ en_href = body.find('./head/link[@rel="alternate"][@hreflang="en"]').get('href')
+ self.assertTrue(it_href.endswith('/%s/test_lang_url/my-super-country-italia-%s' % (it.url_code, country1.id)))
+ self.assertTrue(fr_href.endswith('/%s/test_lang_url/my-super-country-belgium-%s' % (be.url_code, country1.id)))
+ self.assertTrue(en_href.endswith('/test_lang_url/my-super-country-%s' % country1.id))
diff --git a/addons/test_website/tests/test_multi_company.py b/addons/test_website/tests/test_multi_company.py
new file mode 100644
index 00000000..1395466a
--- /dev/null
+++ b/addons/test_website/tests/test_multi_company.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.tests.common import HttpCase, tagged
+
+
+@tagged('post_install', '-at_install')
+class TestMultiCompany(HttpCase):
+
+ def test_company_in_context(self):
+ """ Test website company is set in context """
+ website = self.env.ref('website.default_website')
+ company = self.env['res.company'].create({'name': "Adaa"})
+ website.company_id = company
+ response = self.url_open('/multi_company_website')
+ self.assertEqual(response.json()[0], company.id)
diff --git a/addons/test_website/tests/test_performance.py b/addons/test_website/tests/test_performance.py
new file mode 100644
index 00000000..cd06dfda
--- /dev/null
+++ b/addons/test_website/tests/test_performance.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.addons.website.tests.test_performance import UtilPerf
+
+
+class TestPerformance(UtilPerf):
+ def test_10_perf_sql_website_controller_minimalist(self):
+ url = '/empty_controller_test'
+ self.assertEqual(self._get_url_hot_query(url), 3)
diff --git a/addons/test_website/tests/test_redirect.py b/addons/test_website/tests/test_redirect.py
new file mode 100644
index 00000000..38eebcde
--- /dev/null
+++ b/addons/test_website/tests/test_redirect.py
@@ -0,0 +1,162 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import odoo
+from odoo.tests import HttpCase, tagged
+from odoo.tests.common import HOST
+from odoo.tools import mute_logger
+from odoo.addons.http_routing.models.ir_http import slug
+
+from unittest.mock import patch
+
+
+@tagged('-at_install', 'post_install')
+class TestRedirect(HttpCase):
+
+ def setUp(self):
+ super(TestRedirect, self).setUp()
+
+ self.user_portal = self.env['res.users'].with_context({'no_reset_password': True}).create({
+ 'name': 'Test Website Portal User',
+ 'login': 'portal_user',
+ 'password': 'portal_user',
+ 'email': 'portal_user@mail.com',
+ 'groups_id': [(6, 0, [self.env.ref('base.group_portal').id])]
+ })
+
+ self.base_url = "http://%s:%s" % (HOST, odoo.tools.config['http_port'])
+
+ def test_01_redirect_308_model_converter(self):
+
+ self.env['website.rewrite'].create({
+ 'name': 'Test Website Redirect',
+ 'redirect_type': '308',
+ 'url_from': '/test_website/country/<model("res.country"):country>',
+ 'url_to': '/redirected/country/<model("res.country"):country>',
+ })
+ country_ad = self.env.ref('base.ad')
+
+ """ Ensure 308 redirect with model converter works fine, including:
+ - Correct & working redirect as public user
+ - Correct & working redirect as logged in user
+ - Correct replace of url_for() URLs in DOM
+ """
+ url = '/test_website/country/' + slug(country_ad)
+ redirect_url = url.replace('test_website', 'redirected')
+
+ # [Public User] Open the original url and check redirect OK
+ r = self.url_open(url)
+ self.assertEqual(r.status_code, 200)
+ self.assertTrue(r.url.endswith(redirect_url), "Ensure URL got redirected")
+ self.assertTrue(country_ad.name in r.text, "Ensure the controller returned the expected value")
+ self.assertTrue(redirect_url in r.text, "Ensure the url_for has replaced the href URL in the DOM")
+
+ # [Logged In User] Open the original url and check redirect OK
+ self.authenticate("portal_user", "portal_user")
+ r = self.url_open(url)
+ self.assertEqual(r.status_code, 200)
+ self.assertTrue(r.url.endswith(redirect_url), "Ensure URL got redirected (2)")
+ self.assertTrue('Logged In' in r.text, "Ensure logged in")
+ self.assertTrue(country_ad.name in r.text, "Ensure the controller returned the expected value (2)")
+ self.assertTrue(redirect_url in r.text, "Ensure the url_for has replaced the href URL in the DOM")
+
+ @mute_logger('odoo.addons.http_routing.models.ir_http') # mute 403 warning
+ def test_02_redirect_308_RequestUID(self):
+ self.env['website.rewrite'].create({
+ 'name': 'Test Website Redirect',
+ 'redirect_type': '308',
+ 'url_from': '/test_website/200/<model("test.model"):rec>',
+ 'url_to': '/test_website/308/<model("test.model"):rec>',
+ })
+
+ rec_published = self.env['test.model'].create({'name': 'name', 'website_published': True})
+ rec_unpublished = self.env['test.model'].create({'name': 'name', 'website_published': False})
+
+ WebsiteHttp = odoo.addons.website.models.ir_http.Http
+
+ def _get_error_html(env, code, value):
+ return str(code).split('_')[-1], "CUSTOM %s" % code
+
+ with patch.object(WebsiteHttp, '_get_error_html', _get_error_html):
+ # Patch will avoid to display real 404 page and regenerate assets each time and unlink old one.
+ # And it allow to be sur that exception id handled by handle_exception and return a "managed error" page.
+
+ # published
+ resp = self.url_open("/test_website/200/name-%s" % rec_published.id, allow_redirects=False)
+ self.assertEqual(resp.status_code, 308)
+ self.assertEqual(resp.headers.get('Location'), self.base_url + "/test_website/308/name-%s" % rec_published.id)
+
+ resp = self.url_open("/test_website/308/name-%s" % rec_published.id, allow_redirects=False)
+ self.assertEqual(resp.status_code, 200)
+
+ resp = self.url_open("/test_website/200/xx-%s" % rec_published.id, allow_redirects=False)
+ self.assertEqual(resp.status_code, 308)
+ self.assertEqual(resp.headers.get('Location'), self.base_url + "/test_website/308/xx-%s" % rec_published.id)
+
+ resp = self.url_open("/test_website/308/xx-%s" % rec_published.id, allow_redirects=False)
+ self.assertEqual(resp.status_code, 301)
+ self.assertEqual(resp.headers.get('Location'), self.base_url + "/test_website/308/name-%s" % rec_published.id)
+
+ resp = self.url_open("/test_website/200/xx-%s" % rec_published.id, allow_redirects=True)
+ self.assertEqual(resp.status_code, 200)
+ self.assertEqual(resp.url, self.base_url + "/test_website/308/name-%s" % rec_published.id)
+
+ # unexisting
+ resp = self.url_open("/test_website/200/name-100", allow_redirects=False)
+ self.assertEqual(resp.status_code, 308)
+ self.assertEqual(resp.headers.get('Location'), self.base_url + "/test_website/308/name-100")
+
+ resp = self.url_open("/test_website/308/name-100", allow_redirects=False)
+ self.assertEqual(resp.status_code, 404)
+ self.assertEqual(resp.text, "CUSTOM 404")
+
+ resp = self.url_open("/test_website/200/xx-100", allow_redirects=False)
+ self.assertEqual(resp.status_code, 308)
+ self.assertEqual(resp.headers.get('Location'), self.base_url + "/test_website/308/xx-100")
+
+ resp = self.url_open("/test_website/308/xx-100", allow_redirects=False)
+ self.assertEqual(resp.status_code, 404)
+ self.assertEqual(resp.text, "CUSTOM 404")
+
+ # unpublish
+ resp = self.url_open("/test_website/200/name-%s" % rec_unpublished.id, allow_redirects=False)
+ self.assertEqual(resp.status_code, 308)
+ self.assertEqual(resp.headers.get('Location'), self.base_url + "/test_website/308/name-%s" % rec_unpublished.id)
+
+ resp = self.url_open("/test_website/308/name-%s" % rec_unpublished.id, allow_redirects=False)
+ self.assertEqual(resp.status_code, 403)
+ self.assertEqual(resp.text, "CUSTOM 403")
+
+ resp = self.url_open("/test_website/200/xx-%s" % rec_unpublished.id, allow_redirects=False)
+ self.assertEqual(resp.status_code, 308)
+ self.assertEqual(resp.headers.get('Location'), self.base_url + "/test_website/308/xx-%s" % rec_unpublished.id)
+
+ resp = self.url_open("/test_website/308/xx-%s" % rec_unpublished.id, allow_redirects=False)
+ self.assertEqual(resp.status_code, 403)
+ self.assertEqual(resp.text, "CUSTOM 403")
+
+ # with seo_name as slug
+ rec_published.seo_name = "seo_name"
+ rec_unpublished.seo_name = "seo_name"
+
+ resp = self.url_open("/test_website/200/seo-name-%s" % rec_published.id, allow_redirects=False)
+ self.assertEqual(resp.status_code, 308)
+ self.assertEqual(resp.headers.get('Location'), self.base_url + "/test_website/308/seo-name-%s" % rec_published.id)
+
+ resp = self.url_open("/test_website/308/seo-name-%s" % rec_published.id, allow_redirects=False)
+ self.assertEqual(resp.status_code, 200)
+
+ resp = self.url_open("/test_website/200/xx-%s" % rec_unpublished.id, allow_redirects=False)
+ self.assertEqual(resp.status_code, 308)
+ self.assertEqual(resp.headers.get('Location'), self.base_url + "/test_website/308/xx-%s" % rec_unpublished.id)
+
+ resp = self.url_open("/test_website/308/xx-%s" % rec_unpublished.id, allow_redirects=False)
+ self.assertEqual(resp.status_code, 403)
+ self.assertEqual(resp.text, "CUSTOM 403")
+
+ resp = self.url_open("/test_website/200/xx-100", allow_redirects=False)
+ self.assertEqual(resp.status_code, 308)
+ self.assertEqual(resp.headers.get('Location'), self.base_url + "/test_website/308/xx-100")
+
+ resp = self.url_open("/test_website/308/xx-100", allow_redirects=False)
+ self.assertEqual(resp.status_code, 404)
+ self.assertEqual(resp.text, "CUSTOM 404")
diff --git a/addons/test_website/tests/test_reset_views.py b/addons/test_website/tests/test_reset_views.py
new file mode 100644
index 00000000..a11e8282
--- /dev/null
+++ b/addons/test_website/tests/test_reset_views.py
@@ -0,0 +1,111 @@
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import re
+
+import odoo.tests
+from odoo.tools import mute_logger
+
+
+def break_view(view, fr='<p>placeholder</p>', to='<p t-field="not.exist"/>'):
+ view.arch = view.arch.replace(fr, to)
+
+
+@odoo.tests.common.tagged('post_install', '-at_install')
+class TestWebsiteResetViews(odoo.tests.HttpCase):
+
+ def fix_it(self, page, mode='soft'):
+ self.authenticate("admin", "admin")
+ resp = self.url_open(page)
+ self.assertEqual(resp.status_code, 500, "Waiting 500")
+ self.assertTrue('<button data-mode="soft" class="reset_templates_button' in resp.text)
+ data = {'view_id': self.find_template(resp), 'redirect': page, 'mode': mode}
+ resp = self.url_open('/website/reset_template', data)
+ self.assertEqual(resp.status_code, 200, "Waiting 200")
+
+ def find_template(self, response):
+ find = re.search(r'<input.*type="hidden".*name="view_id".*value="([0-9]+)?"', response.text)
+ return find and find.group(1)
+
+ def setUp(self):
+ super(TestWebsiteResetViews, self).setUp()
+ self.Website = self.env['website']
+ self.View = self.env['ir.ui.view']
+ self.test_view = self.Website.viewref('test_website.test_view')
+
+ @mute_logger('odoo.addons.http_routing.models.ir_http')
+ def test_01_reset_specific_page_view(self):
+ self.test_page_view = self.Website.viewref('test_website.test_page_view')
+ total_views = self.View.search_count([('type', '=', 'qweb')])
+ # Trigger COW then break the QWEB XML on it
+ break_view(self.test_page_view.with_context(website_id=1))
+ self.assertEqual(total_views + 1, self.View.search_count([('type', '=', 'qweb')]), "Missing COW view")
+ self.fix_it('/test_page_view')
+
+ @mute_logger('odoo.addons.http_routing.models.ir_http')
+ def test_02_reset_specific_view_controller(self):
+ total_views = self.View.search_count([('type', '=', 'qweb')])
+ # Trigger COW then break the QWEB XML on it
+ # `t-att-data="not.exist"` will test the case where exception.html contains branding
+ break_view(self.test_view.with_context(website_id=1), to='<p t-att-data="not.exist" />')
+ self.assertEqual(total_views + 1, self.View.search_count([('type', '=', 'qweb')]), "Missing COW view")
+ self.fix_it('/test_view')
+
+ @mute_logger('odoo.addons.http_routing.models.ir_http')
+ def test_03_reset_specific_view_controller_t_called(self):
+ self.test_view_to_be_t_called = self.Website.viewref('test_website.test_view_to_be_t_called')
+
+ total_views = self.View.search_count([('type', '=', 'qweb')])
+ # Trigger COW then break the QWEB XML on it
+ break_view(self.test_view_to_be_t_called.with_context(website_id=1))
+ break_view(self.test_view, to='<t t-call="test_website.test_view_to_be_t_called"/>')
+ self.assertEqual(total_views + 1, self.View.search_count([('type', '=', 'qweb')]), "Missing COW view")
+ self.fix_it('/test_view')
+
+ @mute_logger('odoo.addons.http_routing.models.ir_http')
+ def test_04_reset_specific_view_controller_inherit(self):
+ self.test_view_child_broken = self.Website.viewref('test_website.test_view_child_broken')
+
+ # Activate and break the inherited view
+ self.test_view_child_broken.active = True
+ break_view(self.test_view_child_broken.with_context(website_id=1, load_all_views=True))
+
+ self.fix_it('/test_view')
+
+ # This test work in real life, but not in test mode since we cannot rollback savepoint.
+ # @mute_logger('odoo.addons.http_routing.models.ir_http', 'odoo.addons.website.models.ir_ui_view')
+ # def test_05_reset_specific_view_controller_broken_request(self):
+ # total_views = self.View.search_count([('type', '=', 'qweb')])
+ # # Trigger COW then break the QWEB XML on it
+ # break_view(self.test_view.with_context(website_id=1), to='<t t-esc="request.env[\'website\'].browse(\'a\').name" />')
+ # self.assertEqual(total_views + 1, self.View.search_count([('type', '=', 'qweb')]), "Missing COW view (1)")
+ # self.fix_it('/test_view')
+
+ # also mute ir.ui.view as `get_view_id()` will raise "Could not find view object with xml_id 'not.exist'""
+ @mute_logger('odoo.addons.http_routing.models.ir_http', 'odoo.addons.website.models.ir_ui_view')
+ def test_06_reset_specific_view_controller_inexisting_template(self):
+ total_views = self.View.search_count([('type', '=', 'qweb')])
+ # Trigger COW then break the QWEB XML on it
+ break_view(self.test_view.with_context(website_id=1), to='<t t-call="not.exist"/>')
+ self.assertEqual(total_views + 1, self.View.search_count([('type', '=', 'qweb')]), "Missing COW view (2)")
+ self.fix_it('/test_view')
+
+ @mute_logger('odoo.addons.http_routing.models.ir_http')
+ def test_07_reset_page_view_complete_flow(self):
+ self.start_tour("/", 'test_reset_page_view_complete_flow_part1', login="admin")
+ self.fix_it('/test_page_view')
+ self.start_tour("/", 'test_reset_page_view_complete_flow_part2', login="admin")
+ self.fix_it('/test_page_view')
+
+ @mute_logger('odoo.addons.http_routing.models.ir_http')
+ def test_08_reset_specific_page_view_hard_mode(self):
+ self.test_page_view = self.Website.viewref('test_website.test_page_view')
+ total_views = self.View.search_count([('type', '=', 'qweb')])
+ # Trigger COW then break the QWEB XML on it
+ break_view(self.test_page_view.with_context(website_id=1))
+ # Break it again to have a previous arch different than file arch
+ break_view(self.test_page_view.with_context(website_id=1))
+ self.assertEqual(total_views + 1, self.View.search_count([('type', '=', 'qweb')]), "Missing COW view")
+ with self.assertRaises(AssertionError):
+ # soft reset should not be able to reset the view as previous
+ # version is also broken
+ self.fix_it('/test_page_view')
+ self.fix_it('/test_page_view', 'hard')
diff --git a/addons/test_website/tests/test_session.py b/addons/test_website/tests/test_session.py
new file mode 100644
index 00000000..22975ebd
--- /dev/null
+++ b/addons/test_website/tests/test_session.py
@@ -0,0 +1,9 @@
+import odoo.tests
+from odoo.tools import mute_logger
+
+
+@odoo.tests.common.tagged('post_install', '-at_install')
+class TestWebsiteSession(odoo.tests.HttpCase):
+
+ def test_01_run_test(self):
+ self.start_tour('/', 'test_json_auth')
diff --git a/addons/test_website/tests/test_views_during_module_operation.py b/addons/test_website/tests/test_views_during_module_operation.py
new file mode 100644
index 00000000..c078afc0
--- /dev/null
+++ b/addons/test_website/tests/test_views_during_module_operation.py
@@ -0,0 +1,83 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.tests import standalone
+
+
+@standalone('cow_views')
+def test_01_cow_views_unlink_on_module_update(env):
+ """ Ensure COW views are correctly removed during module update.
+ Not removing the view could lead to traceback:
+ - Having a view A
+ - Having a view B that inherits from a view C
+ - View B t-call view A
+ - COW view B
+ - Delete view A and B from module datas and update it
+ - Rendering view C will crash since it will render child view B that
+ t-call unexisting view A
+ """
+
+ View = env['ir.ui.view']
+ Imd = env['ir.model.data']
+
+ update_module_base_view = env.ref('test_website.update_module_base_view')
+ update_module_view_to_be_t_called = View.create({
+ 'name': 'View to be t-called',
+ 'type': 'qweb',
+ 'arch': '<div>I will be t-called</div>',
+ 'key': 'test_website.update_module_view_to_be_t_called',
+ })
+ update_module_child_view = View.create({
+ 'name': 'Child View',
+ 'mode': 'extension',
+ 'inherit_id': update_module_base_view.id,
+ 'arch': '''
+ <div position="inside">
+ <t t-call="test_website.update_module_view_to_be_t_called"/>
+ </div>
+ ''',
+ 'key': 'test_website.update_module_child_view',
+ })
+
+ # Create IMD so when updating the module the views will be removed (not found in file)
+ Imd.create({
+ 'module': 'test_website',
+ 'name': 'update_module_view_to_be_t_called',
+ 'model': 'ir.ui.view',
+ 'res_id': update_module_view_to_be_t_called.id,
+ })
+ Imd.create({
+ 'module': 'test_website',
+ 'name': 'update_module_child_view',
+ 'model': 'ir.ui.view',
+ 'res_id': update_module_child_view.id,
+ })
+
+ # Trigger COW on child view
+ update_module_child_view.with_context(website_id=1).write({'name': 'Child View (W1)'})
+
+ # Ensure views are correctly setup
+ msg = "View '%s' does not exist!"
+ assert View.search_count([
+ ('type', '=', 'qweb'),
+ ('key', '=', update_module_child_view.key)
+ ]) == 2, msg % update_module_child_view.key
+ assert bool(env.ref(update_module_view_to_be_t_called.key)),\
+ msg % update_module_view_to_be_t_called.key
+ assert bool(env.ref(update_module_base_view.key)), msg % update_module_base_view.key
+
+ # Upgrade the module
+ test_website_module = env['ir.module.module'].search([('name', '=', 'test_website')])
+ test_website_module.button_immediate_upgrade()
+ env.reset() # clear the set of environments
+ env = env() # get an environment that refers to the new registry
+
+ # Ensure generic views got removed
+ view = env.ref('test_website.update_module_view_to_be_t_called', raise_if_not_found=False)
+ assert not view, "Generic view did not get removed!"
+
+ # Ensure specific COW views got removed
+ assert not env['ir.ui.view'].search_count([
+ ('type', '=', 'qweb'),
+ ('key', '=', 'test_website.update_module_child_view'),
+ ]), "Specific COW views did not get removed!"
diff --git a/addons/test_website/views/templates.xml b/addons/test_website/views/templates.xml
new file mode 100644
index 00000000..2bbee7e9
--- /dev/null
+++ b/addons/test_website/views/templates.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <template id="assets_frontend" inherit_id="website.assets_frontend">
+ <xpath expr="//script[last()]" position="after">
+ <script type="text/javascript" src="/test_website/static/src/js/test_error.js"></script>
+ </xpath>
+ </template>
+
+ <template id="assets_tests" name="Test Website Assets Tests" inherit_id="web.assets_tests">
+ <xpath expr="." position="inside">
+ <script type="text/javascript" src="/test_website/static/tests/tours/reset_views.js"></script>
+ <script type="text/javascript" src="/test_website/static/tests/tours/error_views.js"></script>
+ <script type="text/javascript" src="/test_website/static/tests/tours/json_auth.js"></script>
+ <script type="text/javascript" src="/test_website/static/tests/tours/custom_snippets.js"></script>
+ </xpath>
+ </template>
+
+ <record id="multi_url" model="website.page">
+ <field name="name">Multi URL test</field>
+ <field name="url">/multi_url</field>
+ <field name="website_published">False</field>
+ <field name="type">qweb</field>
+ <field name="key">test_website.multi_url</field>
+ <field name="website_published">True</field>
+ <field name="arch" type="xml">
+ <t t-name='multi_url'>
+ <div>
+ <a id='get' href="/get">get</a>
+ <form id='post' action="/post">post</form>>
+ <a id='get_post' href="/get_post">get_post</a>
+ <a id='get_post_nomultilang' href="/get_post_nomultilang">get_post_nomultilang</a>
+ </div>
+ </t>
+ </field>
+ </record>
+</odoo>