diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/test_website | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/test_website')
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> |
