feat: separate webapp favicon configuration in appearance admin panel section

This commit is contained in:
3252a8
2026-05-15 19:27:33 +03:00
parent e9ad259957
commit 2a1ff8e45e
18 changed files with 667 additions and 21 deletions
+6
View File
@@ -39,6 +39,9 @@ class SettingsTests(unittest.TestCase):
WEBAPP_LOGO_USE_EMOJI=True,
WEBAPP_LOGO_EMOJI="🔥",
WEBAPP_LOGO_EMOJI_FONT="twemoji",
WEBAPP_FAVICON_USE_CUSTOM=True,
WEBAPP_FAVICON_URL="https://cdn.example.com/favicon.png",
WEBAPP_LOGO_FAVICON_URL="/webapp-favicon/abcdef1234567890/icon-180.png",
)
self.assertEqual(settings.WEBAPP_PRIMARY_COLOR, "#00fe7a")
@@ -46,6 +49,9 @@ class SettingsTests(unittest.TestCase):
self.assertFalse(settings.WEBAPP_LOGO_USE_EMOJI)
self.assertEqual(settings.WEBAPP_LOGO_EMOJI, "🫥")
self.assertEqual(settings.WEBAPP_LOGO_EMOJI_FONT, "system")
self.assertFalse(settings.WEBAPP_FAVICON_USE_CUSTOM)
self.assertIsNone(settings.WEBAPP_FAVICON_URL)
self.assertIsNone(settings.WEBAPP_LOGO_FAVICON_URL)
def test_tariffs_config_missing_uses_legacy_fallback(self):
settings = Settings(
+59
View File
@@ -1,4 +1,5 @@
import asyncio
import io
import json
import os
import tempfile
@@ -8,7 +9,10 @@ from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
from PIL import Image
from bot.app.web import subscription_webapp
from bot.app.web.admin_api_impl import themes as admin_themes
from bot.app.web.webapp import assets as webapp_assets
from config.settings import Settings
from config.webapp_themes_config import builtin_webapp_themes_config
@@ -110,6 +114,61 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
self.assertEqual(subscription_webapp._resolve_webapp_logo_url(settings), "")
def test_custom_webapp_favicon_takes_precedence(self):
settings = SimpleNamespace(
WEBAPP_FAVICON_USE_CUSTOM=True,
WEBAPP_FAVICON_URL="/webapp-favicon/abcdef1234567890/icon-180.png",
WEBAPP_LOGO_FAVICON_URL="/webapp-favicon/1111111111111111/icon-180.png",
)
self.assertEqual(
subscription_webapp._resolve_webapp_favicon_url(settings, "/logo.png"),
"/webapp-favicon/abcdef1234567890/icon-180.png",
)
def test_logo_generated_favicon_is_used_when_custom_disabled(self):
settings = SimpleNamespace(
WEBAPP_FAVICON_USE_CUSTOM=False,
WEBAPP_FAVICON_URL="/webapp-favicon/abcdef1234567890/icon-180.png",
WEBAPP_LOGO_FAVICON_URL="/webapp-favicon/1111111111111111/icon-180.png",
)
self.assertEqual(
subscription_webapp._resolve_webapp_favicon_url(settings, "/logo.png"),
"/webapp-favicon/1111111111111111/icon-180.png",
)
def test_logo_generated_favicon_is_not_used_without_logo(self):
settings = SimpleNamespace(
WEBAPP_FAVICON_USE_CUSTOM=False,
WEBAPP_FAVICON_URL="/webapp-favicon/abcdef1234567890/icon-180.png",
WEBAPP_LOGO_FAVICON_URL="/webapp-favicon/1111111111111111/icon-180.png",
)
self.assertEqual(subscription_webapp._resolve_webapp_favicon_url(settings, ""), "")
def test_favicon_head_markup_includes_touch_icon(self):
markup = subscription_webapp._favicon_head_markup(
"/webapp-favicon/abcdef1234567890/icon-180.png"
)
self.assertIn('rel="apple-touch-icon"', markup)
self.assertIn("/webapp-favicon/abcdef1234567890/icon-32.png", markup)
def test_favicon_set_generation_writes_common_icon_sizes(self):
buffer = io.BytesIO()
Image.new("RGBA", (2, 2), (0, 254, 122, 255)).save(buffer, format="PNG")
png_body = buffer.getvalue()
with tempfile.TemporaryDirectory() as tmpdir:
with patch.object(admin_themes, "WEBAPP_FAVICON_DIR", Path(tmpdir)):
payload = admin_themes._write_favicon_set(png_body, "image/png", "icon.png")
self.assertRegex(payload["favicon_url"], r"^/webapp-favicon/[0-9a-f]{16}/icon-180\.png$")
digest = payload["favicon_url"].split("/")[2]
self.assertTrue((Path(tmpdir) / digest / "icon-32.png").exists())
self.assertTrue((Path(tmpdir) / digest / "apple-touch-icon.png").exists())
self.assertTrue((Path(tmpdir) / digest / "favicon.ico").exists())
def test_initial_theme_head_markup_includes_css_and_tokens(self):
cfg = builtin_webapp_themes_config("#123456")
theme = cfg.theme_by_key("light")
+14
View File
@@ -143,6 +143,7 @@ class WebAppRouteContractTests(unittest.TestCase):
("GET", "/api/admin/themes"): "admin_themes_get_route",
("PUT", "/api/admin/themes"): "admin_themes_save_route",
("POST", "/api/admin/appearance/logo"): "admin_appearance_logo_upload_route",
("POST", "/api/admin/appearance/favicon"): "admin_appearance_favicon_upload_route",
("GET", "/api/admin/panel/internal-squads"): "admin_panel_internal_squads_route",
}
@@ -167,6 +168,19 @@ class WebAppRouteContractTests(unittest.TestCase):
self.assertEqual(match_info.handler.__name__, "index_route")
def test_webapp_favicon_asset_route_is_registered(self):
app = web.Application()
subscription_webapp.setup_subscription_webapp_routes(app)
request = make_mocked_request(
"GET",
"/webapp-favicon/abcdef1234567890/icon-180.png",
app=app,
)
match_info = asyncio.run(app.router.resolve(request))
self.assertEqual(match_info.handler.__name__, "webapp_favicon_route")
class AdminApiAuthContractTests(unittest.IsolatedAsyncioTestCase):
def _settings(self):