Previously proposed formating was trying to normalize extra in one part
of the path. This means that no extra needed a placeholder.
The implementation was meant to be more generic and extendible since a
part of the logic has to be in website.
A suggestion was made to make it more restricted but explicite by
keeping the url simple in web/controllers/binary.py but adding a
controller in website to add this extra part.
The base extra direction is now in the extension, as the min part.
Initial urls:
/web/assets/{unique}/[{website_id}/][rtl/]{bundle_name}[.min].{extension}
New urls:
/web/assets/[{website_id}]/{unique}/{bundle_name}[.rtl][.min].{extension}
Managed by two routes:
/web/assets/<string:unique>/<string:filename>
/web/assets/<int:website_id>/<string:unique>/<string:filename>
Where filename is in the format {bundle_name}[.rtl][.min].{extension}
Multiple possibilities where proposed
- /web/assets/website/<int:website_id>/<string:unique>/<string:filename>
More explicit but prefixing by /website was considered
- /website/assets/<int:website_id>/<string:unique>/<string:filename>
This one is a litle painfull to match similar attachement, where
website is ignored.
- /website/<int:website_id>/assets/<string:unique>/<string:filename>
Almost accepted but subjective, and anyway two previous solution breaks
the cdn mecanism and would need a migration
- /web/assets/<int:website_id>/<string:unique>/<string:filename>
Almost accepted but subjective, and anyway two previous solution breaks
the cdn mecanism and would need a migration
This last solution was not ideal to match without unique
/web/assets/%/<string:filename> can match both
/web/assets/123456/<string:filename>
and
/web/assets/1/123456/<string:filename>
Anyway, matching without unique shouldn't be supported for al (even if
it is kind of supported with any right now) but it will work by changing
unique wildcard to a more specific one (_ * 7)
closes odoo/odoo#131353
Related: odoo/enterprise#47313
Related: odoo/design-themes#730
Signed-off-by: Xavier Dollé (xdo) <xdo@odoo.com>
223 lines
11 KiB
Python
223 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
import re
|
|
|
|
import odoo.tests
|
|
|
|
from odoo.tools import config
|
|
|
|
|
|
@odoo.tests.common.tagged('post_install', '-at_install')
|
|
class TestWebsiteAssets(odoo.tests.HttpCase):
|
|
|
|
def test_01_multi_domain_assets_generation(self):
|
|
Website = self.env['website']
|
|
Attachment = self.env['ir.attachment']
|
|
# Simulate single website DBs: make sure other website do not interfer
|
|
# (We can't delete those, constraint will most likely be raised)
|
|
Website.search([]).write({'domain': 'inactive.test'})
|
|
# Don't use HOST, hardcode it so it doesn't get changed one day and make
|
|
# the test useless
|
|
domain_1 = "http://127.0.0.1:%s" % config['http_port']
|
|
domain_2 = "http://localhost:%s" % config['http_port']
|
|
Website.browse(1).domain = domain_1
|
|
|
|
self.authenticate('admin', 'admin')
|
|
self.env['web_editor.assets'].with_context(website_id=1).make_scss_customization(
|
|
'/website/static/src/scss/options/colors/user_color_palette.scss',
|
|
{"o-cc1-bg": "'400'"},
|
|
)
|
|
|
|
def get_last_backend_asset_attach_id():
|
|
return Attachment.search([
|
|
('name', '=', 'web.assets_backend.min.js'),
|
|
], order="id desc", limit=1).id
|
|
|
|
def check_asset():
|
|
self.assertEqual(last_backend_asset_attach_id, get_last_backend_asset_attach_id())
|
|
|
|
last_backend_asset_attach_id = get_last_backend_asset_attach_id()
|
|
|
|
# The first call will generate the assets and populate the cache and
|
|
# take ~100 SQL Queries (~cold state).
|
|
# Any later call to `/web`, regardless of the domain, will take only
|
|
# ~10 SQL Queries (hot state).
|
|
# Without the calls the `check_asset()` (which would raise early and
|
|
# would not call other `url_open()`) and before the fix coming with this
|
|
# test, here is the logs:
|
|
# "GET /web HTTP/1.1" 200 - 222 0.135 3.840 <-- 222 Queries, ~4s
|
|
# "GET /web HTTP/1.1" 200 - 181 0.101 3.692 <-- 181 Queries, ~4s
|
|
# "GET /web HTTP/1.1" 200 - 215 0.121 3.704 <-- 215 Queries, ~4s
|
|
# "GET /web HTTP/1.1" 200 - 181 0.100 3.616 <-- 181 Queries, ~4s
|
|
# After the fix, here is the logs:
|
|
# "GET /web HTTP/1.1" 200 - 101 0.043 0.353 <-- 101 Queries, ~0.3s
|
|
# "GET /web HTTP/1.1" 200 - 11 0.004 0.007 <-- 11 Queries, ~10ms
|
|
# "GET /web HTTP/1.1" 200 - 11 0.003 0.005 <-- 11 Queries, ~10ms
|
|
# "GET /web HTTP/1.1" 200 - 11 0.003 0.008 <-- 11 Queries, ~10ms
|
|
self.url_open(domain_1 + '/web')
|
|
check_asset()
|
|
self.url_open(domain_2 + '/web')
|
|
check_asset()
|
|
self.url_open(domain_1 + '/web')
|
|
check_asset()
|
|
self.url_open(domain_2 + '/web')
|
|
check_asset()
|
|
self.url_open(domain_1 + '/web')
|
|
check_asset()
|
|
|
|
def test_02_t_cache_invalidation(self):
|
|
self.authenticate(None, None)
|
|
page = self.url_open('/').text # add to cache
|
|
public_assets_links = re.findall(r'(/web/assets/\d+/\w{7}/web.assets_frontend\..+)"/>', page)
|
|
self.assertTrue(public_assets_links)
|
|
self.authenticate('admin', 'admin')
|
|
page = self.url_open('/').text
|
|
admin_assets_links = re.findall(r'(/web/assets/\d+/\w{7}/web.assets_frontend\..+)"/>', page)
|
|
self.assertTrue(admin_assets_links)
|
|
|
|
self.assertEqual(public_assets_links, admin_assets_links)
|
|
|
|
snippets = self.env['ir.asset'].search([
|
|
('path', '=like', 'website/static/src/snippets/s_social_media/000.scss'), # arbitrary, a unused css one that doesn't make the page fail when archived.
|
|
('bundle', '=', 'web.assets_frontend'),
|
|
])
|
|
self.assertTrue(snippets)
|
|
write_dates = snippets.mapped('write_date')
|
|
snippets.write({'active': False})
|
|
snippets.flush_recordset()
|
|
self.assertNotEqual(write_dates, snippets.mapped('write_date'))
|
|
|
|
page = self.url_open('/').text
|
|
new_admin_assets_links = re.findall(r'(/web/assets/\d+/\w{7}/web.assets_frontend\..+)"/>', page)
|
|
self.assertTrue(new_admin_assets_links)
|
|
|
|
self.assertEqual(public_assets_links, admin_assets_links)
|
|
self.assertNotEqual(new_admin_assets_links, admin_assets_links, "we expect a change since ir_assets were written")
|
|
|
|
self.authenticate(None, None)
|
|
page = self.url_open('/').text
|
|
|
|
new_public_assets_links = re.findall(r'(/web/assets/\d+/\w{7}/web.assets_frontend\..+)"/>', page)
|
|
self.assertEqual(new_admin_assets_links, new_public_assets_links, "t-cache should have been invalidated for public user too")
|
|
|
|
@odoo.tests.tagged('-at_install', 'post_install')
|
|
class TestWebAssets(odoo.tests.HttpCase):
|
|
def test_assets_url_validation(self):
|
|
website_id = self.env['website'].search([], limit=1, order='id desc').id
|
|
|
|
with odoo.tools.mute_logger('odoo.addons.base.models.ir_asset'):
|
|
self.assertEqual(
|
|
self.url_open(f'/web/assets/{website_id}/debug/hello/web.assets_frontend.css', allow_redirects=False).status_code,
|
|
404,
|
|
"unexpected direction extra",
|
|
)
|
|
self.assertEqual(
|
|
self.url_open(f'/web/assets/{website_id}/debug/web.assets_f_ontend.js', allow_redirects=False).status_code,
|
|
404,
|
|
"bundle name contains `_` and should be escaped wildcard",
|
|
)
|
|
self.assertEqual(
|
|
self.url_open(f'/web/assets/{website_id}/debug/web.assets_frontend.rtl.js', allow_redirects=False).status_code,
|
|
404,
|
|
"js cannot have `rtl` has extra",
|
|
)
|
|
self.assertEqual(
|
|
self.url_open(f'/web/assets/{website_id}/debug/web.assets_frontend.rtl.js', allow_redirects=False).status_code,
|
|
404,
|
|
"js cannot have `rtl` has extra",
|
|
)
|
|
self.assertEqual(
|
|
self.url_open(f'/web/{website_id+1}/assets/debug/web.assets_frontend.css', allow_redirects=False).status_code,
|
|
404,
|
|
"website_id does not exist",
|
|
)
|
|
self.assertEqual(
|
|
self.url_open(f'/web/assets/{website_id}/debug/web.assets_frontend.aa.css', allow_redirects=False).status_code,
|
|
404,
|
|
"invalid direction",
|
|
)
|
|
self.assertEqual(
|
|
self.url_open(f'/web/assets/{website_id}/any/web.assets_frontend.min.rtl.css', allow_redirects=False).status_code,
|
|
404,
|
|
"min and direction inverted",
|
|
)
|
|
self.assertEqual(
|
|
self.url_open(f'/web/assets/{website_id}/any/web.assets_frontend.js', allow_redirects=False).status_code,
|
|
404,
|
|
"missing min in non debug mode",
|
|
)
|
|
|
|
self.assertEqual(
|
|
self.url_open('/web/assets/debug/web.assets_frontend.css', allow_redirects=False).status_code,
|
|
200,
|
|
)
|
|
self.assertEqual(
|
|
self.url_open('/web/assets/debug/web.assets_frontend.js', allow_redirects=False).status_code,
|
|
200,
|
|
)
|
|
self.assertEqual(
|
|
self.url_open('/web/assets/debug/web.assets_frontend.rtl.css', allow_redirects=False).status_code,
|
|
200,
|
|
)
|
|
self.assertEqual(
|
|
self.url_open(f'/web/assets/{website_id}/debug/web.assets_frontend.css', allow_redirects=False).status_code,
|
|
200,
|
|
)
|
|
self.assertEqual(
|
|
self.url_open(f'/web/assets/{website_id}/debug/web.assets_frontend.rtl.css', allow_redirects=False).status_code,
|
|
200,
|
|
)
|
|
self.assertEqual(
|
|
self.url_open(f'/web/assets/{website_id}/debug/web.assets_frontend.js', allow_redirects=False).status_code,
|
|
200,
|
|
)
|
|
self.assertEqual(
|
|
self.url_open(f'/web/assets/{website_id}/any/web.assets_frontend.rtl.min.css', allow_redirects=False).status_code,
|
|
200,
|
|
)
|
|
|
|
self.assertEqual(
|
|
self.url_open(f'/web/assets/{website_id}/any/web.assets_frontend.min.css', allow_redirects=False).status_code,
|
|
200,
|
|
)
|
|
self.assertEqual(
|
|
self.url_open(f'/web/assets/{website_id}/any/web.assets_frontend.min.js', allow_redirects=False).status_code,
|
|
200,
|
|
)
|
|
|
|
# redirect urls
|
|
invalid_version = '1234567'
|
|
self.assertEqual(
|
|
self.url_open(f'/web/assets/{website_id}/{invalid_version}/web.assets_frontend.min.css', allow_redirects=False).headers['location'].split('/assets/')[1],
|
|
self.env['ir.qweb']._get_asset_bundle('web.assets_frontend', assets_params={'website_id': website_id}).get_link('css').split('/assets/')[1],
|
|
)
|
|
|
|
def test_ensure_correct_website_asset(self):
|
|
# when searching for an attachment, if the unique a wildcard, we want to ensute that we don't match a website one when seraching a no website one.
|
|
# this test should also wheck that the clean_attachement does not erase a website_attachement after generating a base attachment
|
|
website_id = self.env['website'].search([], limit=1, order='id desc').id
|
|
unique = self.env['ir.qweb']._get_asset_bundle('web.assets_frontend').get_version('js')
|
|
base_url = self.env['ir.asset']._get_asset_bundle_url('web.assets_frontend.min.js', '%', {})
|
|
base_url_versioned = self.env['ir.asset']._get_asset_bundle_url('web.assets_frontend.min.js', unique, {})
|
|
website_url = self.env['ir.asset']._get_asset_bundle_url('web.assets_frontend.min.js', '%', {'website_id': website_id})
|
|
# we expect the unique to be the same in this case, but there is no garantee
|
|
website_url_versioned = self.env['ir.asset']._get_asset_bundle_url('web.assets_frontend.min.js', unique, {'website_id': website_id})
|
|
|
|
self.env['ir.attachment'].search([('url', '=like', '%web.assets_frontend.min.js')]).unlink()
|
|
|
|
# generate website assets
|
|
self.assertEqual(self.url_open(website_url, allow_redirects=False).status_code, 200)
|
|
self.assertEqual(
|
|
self.env['ir.attachment'].search([('url', '=like', '%web.assets_frontend.min.js')]).mapped('url'),
|
|
[website_url_versioned],
|
|
'Only the website asset is expected to be present',
|
|
)
|
|
|
|
# generate base assets
|
|
self.assertEqual(self.url_open(base_url, allow_redirects=False).status_code, 200)
|
|
self.assertEqual(
|
|
self.env['ir.attachment'].search([('url', '=like', '%web.assets_frontend.min.js')]).mapped('url'),
|
|
[base_url_versioned, website_url_versioned],
|
|
'base asset is expected to be present',
|
|
)
|