perf: optimize webapp asset delivery, split admin webapp bundle
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
import gzip
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
@@ -447,6 +448,38 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
||||
"subscription_webapp.min.22222222.js",
|
||||
)
|
||||
|
||||
def test_resolve_webapp_admin_asset_names_prefer_latest_minified_builds(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
asset_dir = Path(tmpdir)
|
||||
(asset_dir / "subscription_webapp_admin.js").write_text(
|
||||
"console.log('admin fallback');", encoding="utf-8"
|
||||
)
|
||||
(asset_dir / "subscription_webapp_admin.css").write_text(
|
||||
".admin{color:red}", encoding="utf-8"
|
||||
)
|
||||
old_js = asset_dir / "subscription_webapp_admin.min.11111111.js"
|
||||
new_js = asset_dir / "subscription_webapp_admin.min.22222222.js"
|
||||
old_css = asset_dir / "subscription_webapp_admin.11111111.css"
|
||||
new_css = asset_dir / "subscription_webapp_admin.22222222.css"
|
||||
old_js.write_text("console.log('old');", encoding="utf-8")
|
||||
new_js.write_text("console.log('new');", encoding="utf-8")
|
||||
old_css.write_text(".old{}", encoding="utf-8")
|
||||
new_css.write_text(".new{}", encoding="utf-8")
|
||||
os.utime(old_js, (1, 1))
|
||||
os.utime(old_css, (1, 1))
|
||||
os.utime(new_js, (2, 2))
|
||||
os.utime(new_css, (2, 2))
|
||||
|
||||
with patch.object(webapp_assets, "ASSET_DIR", asset_dir):
|
||||
self.assertEqual(
|
||||
subscription_webapp._resolve_webapp_admin_js_asset_name(),
|
||||
"subscription_webapp_admin.min.22222222.js",
|
||||
)
|
||||
self.assertEqual(
|
||||
subscription_webapp._resolve_webapp_admin_css_asset_name(),
|
||||
"subscription_webapp_admin.22222222.css",
|
||||
)
|
||||
|
||||
async def test_js_asset_route_sets_immutable_cache_control_for_minified_asset(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
asset_dir = Path(tmpdir)
|
||||
@@ -466,6 +499,71 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
||||
)
|
||||
self.assertEqual(response.text, "console.log('minified');")
|
||||
|
||||
async def test_admin_js_asset_route_serves_admin_bundle(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
asset_dir = Path(tmpdir)
|
||||
minified_asset = asset_dir / "subscription_webapp_admin.min.abcdef12.js"
|
||||
minified_asset.write_text("console.log('admin');", encoding="utf-8")
|
||||
|
||||
request = SimpleNamespace(
|
||||
app={"settings": SimpleNamespace(WEBAPP_ENABLED=True)},
|
||||
match_info={"asset_hash": "abcdef12"},
|
||||
)
|
||||
|
||||
with patch.object(webapp_assets, "ASSET_DIR", asset_dir):
|
||||
response = await subscription_webapp.admin_js_asset_route(request)
|
||||
|
||||
self.assertEqual(
|
||||
response.headers["Cache-Control"], "public, max-age=31536000, immutable"
|
||||
)
|
||||
self.assertEqual(response.text, "console.log('admin');")
|
||||
|
||||
async def test_js_asset_route_prefers_precompressed_brotli_asset(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
asset_dir = Path(tmpdir)
|
||||
minified_asset = asset_dir / "subscription_webapp.min.abcdef12.js"
|
||||
minified_asset.write_text("console.log('minified');", encoding="utf-8")
|
||||
(asset_dir / "subscription_webapp.min.abcdef12.js.br").write_bytes(b"br-body")
|
||||
|
||||
request = SimpleNamespace(
|
||||
app={"settings": SimpleNamespace(WEBAPP_ENABLED=True)},
|
||||
match_info={"asset_hash": "abcdef12"},
|
||||
headers={"Accept-Encoding": "gzip, br"},
|
||||
)
|
||||
|
||||
with patch.object(webapp_assets, "ASSET_DIR", asset_dir):
|
||||
response = await subscription_webapp.js_asset_route(request)
|
||||
|
||||
self.assertEqual(response.body, b"br-body")
|
||||
self.assertEqual(response.headers["Content-Encoding"], "br")
|
||||
self.assertEqual(response.headers["Vary"], "Accept-Encoding")
|
||||
self.assertEqual(
|
||||
response.headers["Cache-Control"], "public, max-age=31536000, immutable"
|
||||
)
|
||||
|
||||
async def test_css_asset_route_falls_back_to_precompressed_gzip_asset(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
asset_dir = Path(tmpdir)
|
||||
css_asset = asset_dir / "subscription_webapp.abcdef12.css"
|
||||
css_asset.write_text(".app{color:red}", encoding="utf-8")
|
||||
(asset_dir / "subscription_webapp.abcdef12.css.gz").write_bytes(b"gz-body")
|
||||
|
||||
request = SimpleNamespace(
|
||||
app={"settings": SimpleNamespace(WEBAPP_ENABLED=True)},
|
||||
match_info={"asset_hash": "abcdef12"},
|
||||
headers={"Accept-Encoding": "gzip"},
|
||||
)
|
||||
|
||||
with patch.object(webapp_assets, "ASSET_DIR", asset_dir):
|
||||
response = await subscription_webapp.css_asset_route(request)
|
||||
|
||||
self.assertEqual(response.body, b"gz-body")
|
||||
self.assertEqual(response.headers["Content-Encoding"], "gzip")
|
||||
self.assertEqual(response.headers["Vary"], "Accept-Encoding")
|
||||
self.assertEqual(
|
||||
response.headers["Cache-Control"], "public, max-age=31536000, immutable"
|
||||
)
|
||||
|
||||
async def test_theme_css_asset_route_serves_file_from_configured_directory(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
themes_dir = Path(tmpdir)
|
||||
@@ -487,8 +585,68 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
self.assertEqual(response.content_type, "text/css")
|
||||
self.assertEqual(response.headers["Cache-Control"], "no-cache")
|
||||
self.assertIn("ETag", response.headers)
|
||||
self.assertIn("--bg: red", response.text)
|
||||
|
||||
async def test_theme_css_asset_route_returns_not_modified_for_matching_etag(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
themes_dir = Path(tmpdir)
|
||||
(themes_dir / "custom").mkdir()
|
||||
(themes_dir / "custom" / "theme.css").write_text(
|
||||
".theme-key-custom { --bg: red; }", encoding="utf-8"
|
||||
)
|
||||
request = SimpleNamespace(
|
||||
app={
|
||||
"settings": SimpleNamespace(
|
||||
WEBAPP_ENABLED=True,
|
||||
WEBAPP_THEMES_DIR=str(themes_dir),
|
||||
)
|
||||
},
|
||||
match_info={"path": "custom/theme.css"},
|
||||
headers={},
|
||||
)
|
||||
|
||||
response = await subscription_webapp.theme_css_asset_route(request)
|
||||
etag = response.headers["ETag"]
|
||||
cached_request = SimpleNamespace(
|
||||
app=request.app,
|
||||
match_info=request.match_info,
|
||||
headers={"If-None-Match": etag},
|
||||
)
|
||||
|
||||
cached_response = await subscription_webapp.theme_css_asset_route(cached_request)
|
||||
|
||||
self.assertEqual(cached_response.status, 304)
|
||||
self.assertEqual(cached_response.headers["ETag"], etag)
|
||||
self.assertEqual(cached_response.headers["Cache-Control"], "no-cache")
|
||||
|
||||
async def test_theme_css_asset_route_serves_cached_gzip_when_accepted(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
themes_dir = Path(tmpdir)
|
||||
(themes_dir / "custom").mkdir()
|
||||
(themes_dir / "custom" / "theme.css").write_text(
|
||||
".theme-key-custom { --bg: red; }\n", encoding="utf-8"
|
||||
)
|
||||
request = SimpleNamespace(
|
||||
app={
|
||||
"settings": SimpleNamespace(
|
||||
WEBAPP_ENABLED=True,
|
||||
WEBAPP_THEMES_DIR=str(themes_dir),
|
||||
)
|
||||
},
|
||||
match_info={"path": "custom/theme.css"},
|
||||
headers={"Accept-Encoding": "gzip"},
|
||||
)
|
||||
|
||||
response = await subscription_webapp.theme_css_asset_route(request)
|
||||
|
||||
self.assertEqual(response.headers["Content-Encoding"], "gzip")
|
||||
self.assertEqual(response.headers["Vary"], "Accept-Encoding")
|
||||
self.assertEqual(
|
||||
gzip.decompress(response.body).decode("utf-8"),
|
||||
".theme-key-custom { --bg: red; }\n",
|
||||
)
|
||||
|
||||
async def test_theme_css_asset_route_serves_default_theme_asset_from_theme_folder(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
request = SimpleNamespace(
|
||||
@@ -542,8 +700,41 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
self.assertEqual(response.content_type, "image/png")
|
||||
self.assertEqual(response.headers["Cache-Control"], "public, max-age=3600")
|
||||
self.assertIn("ETag", response.headers)
|
||||
self.assertEqual(response.body, b"png-bytes")
|
||||
|
||||
async def test_theme_asset_route_returns_not_modified_for_matching_etag(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
themes_dir = Path(tmpdir)
|
||||
(themes_dir / "custom" / "icons").mkdir(parents=True)
|
||||
(themes_dir / "custom" / "icons" / "save.png").write_bytes(b"png-bytes")
|
||||
request = SimpleNamespace(
|
||||
app={
|
||||
"settings": SimpleNamespace(
|
||||
WEBAPP_ENABLED=True,
|
||||
WEBAPP_THEMES_DIR=str(themes_dir),
|
||||
)
|
||||
},
|
||||
match_info={"path": "custom/icons/save.png"},
|
||||
query={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
response = await subscription_webapp.theme_asset_route(request)
|
||||
etag = response.headers["ETag"]
|
||||
cached_request = SimpleNamespace(
|
||||
app=request.app,
|
||||
match_info=request.match_info,
|
||||
query={},
|
||||
headers={"If-None-Match": etag},
|
||||
)
|
||||
|
||||
cached_response = await subscription_webapp.theme_asset_route(cached_request)
|
||||
|
||||
self.assertEqual(cached_response.status, 304)
|
||||
self.assertEqual(cached_response.headers["ETag"], etag)
|
||||
self.assertEqual(cached_response.headers["Cache-Control"], "public, max-age=3600")
|
||||
|
||||
async def test_theme_asset_route_uses_immutable_cache_for_versioned_assets(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
themes_dir = Path(tmpdir)
|
||||
|
||||
@@ -63,9 +63,12 @@ class WebAppRouteContractTests(unittest.TestCase):
|
||||
("GET", "/webapp-uploaded-logo/{filename}"): "webapp_uploaded_logo_route",
|
||||
("GET", "/webapp-emoji/{codepoints}/512.{ext}"): "webapp_animated_emoji_route",
|
||||
("GET", "/subscription_webapp.css"): "css_asset_route",
|
||||
("GET", "/subscription_webapp_admin.css"): "admin_css_asset_route",
|
||||
("GET", "/webapp-theme-css/{path}"): "theme_css_asset_route",
|
||||
("GET", "/subscription_webapp.min.{asset_hash}.js"): "js_asset_route",
|
||||
("GET", "/subscription_webapp.js"): "js_asset_route",
|
||||
("GET", "/subscription_webapp_admin.min.{asset_hash}.js"): "admin_js_asset_route",
|
||||
("GET", "/subscription_webapp_admin.js"): "admin_js_asset_route",
|
||||
("POST", "/api/auth/telegram/nonce"): "telegram_oauth_nonce_route",
|
||||
("POST", "/api/auth/token"): "auth_token_route",
|
||||
("POST", "/api/auth/email/request"): "email_auth_request_route",
|
||||
|
||||
Reference in New Issue
Block a user