feat: custom themes polishing
This commit is contained in:
@@ -28,6 +28,25 @@ class SettingsTests(unittest.TestCase):
|
||||
self.assertTrue(settings.WEBHOOK_SECRET_TOKEN)
|
||||
self.assertEqual(settings.WEBAPP_SESSION_TTL_SECONDS, 86400)
|
||||
|
||||
def test_deprecated_webapp_appearance_env_values_are_ignored(self):
|
||||
settings = Settings(
|
||||
_env_file=None,
|
||||
BOT_TOKEN="token",
|
||||
POSTGRES_USER="app_user",
|
||||
POSTGRES_PASSWORD="app_password",
|
||||
WEBAPP_PRIMARY_COLOR="#ff0000",
|
||||
WEBAPP_LOGO_URL="https://cdn.example.com/logo.png",
|
||||
WEBAPP_LOGO_USE_EMOJI=True,
|
||||
WEBAPP_LOGO_EMOJI="🔥",
|
||||
WEBAPP_LOGO_EMOJI_FONT="twemoji",
|
||||
)
|
||||
|
||||
self.assertEqual(settings.WEBAPP_PRIMARY_COLOR, "#00fe7a")
|
||||
self.assertIsNone(settings.WEBAPP_LOGO_URL)
|
||||
self.assertFalse(settings.WEBAPP_LOGO_USE_EMOJI)
|
||||
self.assertEqual(settings.WEBAPP_LOGO_EMOJI, "🫥")
|
||||
self.assertEqual(settings.WEBAPP_LOGO_EMOJI_FONT, "system")
|
||||
|
||||
def test_tariffs_config_missing_uses_legacy_fallback(self):
|
||||
settings = Settings(
|
||||
_env_file=None,
|
||||
|
||||
@@ -11,6 +11,7 @@ from unittest.mock import patch
|
||||
from bot.app.web import subscription_webapp
|
||||
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
|
||||
|
||||
|
||||
class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
||||
@@ -91,6 +92,36 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
||||
r"^/webapp-logo\?v=[0-9a-f]{12}$",
|
||||
)
|
||||
|
||||
def test_uploaded_webapp_logo_url_is_served_directly(self):
|
||||
settings = SimpleNamespace(
|
||||
WEBAPP_LOGO_URL="/webapp-uploaded-logo/logo-abcdef1234567890.png"
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
subscription_webapp._resolve_webapp_logo_url(settings),
|
||||
"/webapp-uploaded-logo/logo-abcdef1234567890.png",
|
||||
)
|
||||
|
||||
def test_webapp_logo_is_hidden_when_emoji_logo_is_enabled(self):
|
||||
settings = SimpleNamespace(
|
||||
WEBAPP_LOGO_USE_EMOJI=True,
|
||||
WEBAPP_LOGO_URL="/webapp-uploaded-logo/logo-abcdef1234567890.png",
|
||||
)
|
||||
|
||||
self.assertEqual(subscription_webapp._resolve_webapp_logo_url(settings), "")
|
||||
|
||||
def test_initial_theme_head_markup_includes_css_and_tokens(self):
|
||||
cfg = builtin_webapp_themes_config("#123456")
|
||||
theme = cfg.theme_by_key("light")
|
||||
request = SimpleNamespace(get=lambda key, default="": "nonce-value")
|
||||
|
||||
markup = subscription_webapp._initial_theme_head_markup(request, theme, "#123456")
|
||||
|
||||
self.assertIn("/webapp-theme-css/light/style.css", markup)
|
||||
self.assertIn('nonce="nonce-value"', markup)
|
||||
self.assertIn("--accent:#123456", markup)
|
||||
self.assertIn("color-scheme:light", markup)
|
||||
|
||||
def test_animated_emoji_asset_path_uses_same_origin_route(self):
|
||||
self.assertEqual(
|
||||
subscription_webapp._webapp_animated_emoji_asset_path("🤩"),
|
||||
@@ -364,7 +395,7 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
||||
logo_url = "https://cdn.example.com/logo.png"
|
||||
logo = (b"cached-logo", "image/png")
|
||||
app = {
|
||||
"settings": SimpleNamespace(WEBAPP_LOGO_URL=logo_url),
|
||||
"settings": SimpleNamespace(WEBAPP_LOGO_URL=logo_url, WEBAPP_LOGO_USE_EMOJI=False),
|
||||
"webapp_logo_cache": None,
|
||||
"webapp_logo_cache_lock": asyncio.Lock(),
|
||||
}
|
||||
@@ -403,6 +434,7 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
app = {
|
||||
"settings": SimpleNamespace(
|
||||
WEBAPP_LOGO_USE_EMOJI=True,
|
||||
WEBAPP_LOGO_EMOJI="🤩",
|
||||
WEBAPP_LOGO_EMOJI_FONT="noto-color-animated",
|
||||
),
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import asyncio
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from aiohttp import web
|
||||
from aiohttp.test_utils import make_mocked_request
|
||||
|
||||
from bot.app.web import admin_api, subscription_webapp
|
||||
from bot.app.web.admin_api_impl import auth as admin_auth_routes
|
||||
@@ -57,6 +59,7 @@ class WebAppRouteContractTests(unittest.TestCase):
|
||||
("GET", "/auth/telegram/callback"): "telegram_oauth_callback_route",
|
||||
("GET", "/health"): "health_route",
|
||||
("GET", "/webapp-logo"): "webapp_logo_route",
|
||||
("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", "/webapp-theme-css/{path}"): "theme_css_asset_route",
|
||||
@@ -139,12 +142,31 @@ class WebAppRouteContractTests(unittest.TestCase):
|
||||
("PUT", "/api/admin/tariffs"): "admin_tariffs_save_route",
|
||||
("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",
|
||||
("GET", "/api/admin/panel/internal-squads"): "admin_panel_internal_squads_route",
|
||||
}
|
||||
|
||||
for key, handler_name in expected.items():
|
||||
self.assertEqual(routes.get(key), handler_name, key)
|
||||
|
||||
def test_admin_themes_page_route_is_not_registered(self):
|
||||
app = web.Application()
|
||||
subscription_webapp.setup_subscription_webapp_routes(app)
|
||||
|
||||
request = make_mocked_request("GET", "/admin/themes", app=app)
|
||||
match_info = asyncio.run(app.router.resolve(request))
|
||||
|
||||
self.assertEqual(match_info.http_exception.status, 404)
|
||||
|
||||
def test_admin_appearance_page_route_is_registered(self):
|
||||
app = web.Application()
|
||||
subscription_webapp.setup_subscription_webapp_routes(app)
|
||||
|
||||
request = make_mocked_request("GET", "/admin/appearance", app=app)
|
||||
match_info = asyncio.run(app.router.resolve(request))
|
||||
|
||||
self.assertEqual(match_info.handler.__name__, "index_route")
|
||||
|
||||
|
||||
class AdminApiAuthContractTests(unittest.IsolatedAsyncioTestCase):
|
||||
def _settings(self):
|
||||
|
||||
@@ -279,6 +279,35 @@ class WebappThemesConfigTests(unittest.TestCase):
|
||||
self.assertTrue(win95["use_in_admin"])
|
||||
self.assertNotIn("accent", win95["tokens"])
|
||||
|
||||
def test_theme_accent_is_normalized_to_hex(self):
|
||||
cfg = WebappThemesConfig(
|
||||
default_theme="custom",
|
||||
themes=[
|
||||
{
|
||||
"key": "custom",
|
||||
"enabled": True,
|
||||
"default": True,
|
||||
"tokens": {"color_scheme": "dark", "accent": "0F8"},
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
self.assertEqual(cfg.theme_by_key("custom").tokens.accent, "#00ff88")
|
||||
|
||||
def test_theme_accent_rejects_non_hex_values(self):
|
||||
with self.assertRaises(ValueError):
|
||||
WebappThemesConfig(
|
||||
default_theme="custom",
|
||||
themes=[
|
||||
{
|
||||
"key": "custom",
|
||||
"enabled": True,
|
||||
"default": True,
|
||||
"tokens": {"color_scheme": "dark", "accent": "lime"},
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
def test_public_payload_keeps_admin_usage_flag(self):
|
||||
cfg = WebappThemesConfig(
|
||||
default_theme="custom",
|
||||
|
||||
Reference in New Issue
Block a user