import json import os import tempfile import unittest from datetime import datetime, timezone from pathlib import Path from types import SimpleNamespace from unittest.mock import patch from bot.app.web import subscription_webapp from config.settings import Settings class WebAppAssetTests(unittest.IsolatedAsyncioTestCase): def test_serialize_plans_prefers_tariffs_config_over_legacy_packages(self): with tempfile.TemporaryDirectory() as tmpdir: path = Path(tmpdir) / "tariffs.json" path.write_text( json.dumps( { "default_tariff": "standard", "tariffs": [ { "key": "standard", "names": {"en": "Standard"}, "descriptions": {"en": "100 GB monthly"}, "squad_uuids": ["uuid"], "billing_model": "period", "monthly_gb": 100, "hwid_device_limit": 5, "hwid_device_packages": { "rub": [{"count": 1, "price": 99}], "stars": [{"count": 1, "price": 2500}], }, "prices_rub": {"1": 150}, "prices_stars": {"1": 0}, "enabled_periods": [1], "enabled": True, }, { "key": "traffic", "names": {"en": "Traffic"}, "descriptions": {"en": "Pay as you go"}, "squad_uuids": ["uuid"], "billing_model": "traffic", "traffic_packages": { "rub": [{"gb": 50, "price": 799}], "stars": [{"gb": 50, "price": 2500}], }, "enabled": True, }, ], } ), encoding="utf-8", ) settings = Settings( _env_file=None, BOT_TOKEN="token", POSTGRES_USER="app_user", POSTGRES_PASSWORD="app_password", TARIFFS_CONFIG_PATH=str(path), TRAFFIC_PACKAGES="10:199", ) plans = subscription_webapp._serialize_plans(settings, "en") self.assertEqual([plan["tariff_key"] for plan in plans], ["standard", "traffic"]) self.assertEqual(plans[0]["sale_mode"], "subscription") self.assertEqual(plans[0]["months"], 1) self.assertEqual(plans[0]["hwid_device_limit"], 5) self.assertEqual(plans[0]["hwid_device_packages"][0]["device_count"], 1) self.assertEqual(plans[1]["sale_mode"], "traffic_package") self.assertEqual(plans[1]["traffic_gb"], 50.0) self.assertEqual(plans[1]["stars_price"], 2500) def test_subscription_template_does_not_block_on_telegram_sdk(self): html = subscription_webapp.TEMPLATE_PATH.read_text(encoding="utf-8") self.assertNotIn("https://telegram.org/js/telegram-web-app.js", html) self.assertNotIn("https://fonts.googleapis.com", html) self.assertLess(html.index("/subscription_webapp.css"), html.index("WEBAPP_JS_SCRIPT")) def test_https_webapp_logo_uses_same_origin_proxy(self): settings = SimpleNamespace(WEBAPP_LOGO_URL="https://cdn.example.com/logo.png") self.assertEqual(subscription_webapp._resolve_webapp_logo_url(settings), "/webapp-logo") def test_telegram_avatar_url_uses_same_origin_account_route(self): avatar = SimpleNamespace( user_id=123, image_bytes=b"avatar", updated_at=datetime(2026, 5, 6, 12, 0, tzinfo=timezone.utc), ) self.assertEqual( subscription_webapp._telegram_avatar_url(avatar), f"/api/account/avatar?v={int(avatar.updated_at.timestamp())}", ) def test_select_compact_telegram_photo_size_prefers_small_suitable_photo(self): small = SimpleNamespace(width=80, height=80, file_size=5000) medium = SimpleNamespace(width=160, height=160, file_size=12000) large = SimpleNamespace(width=640, height=640, file_size=90000) self.assertIs( subscription_webapp._select_compact_telegram_photo_size([small, large, medium]), medium, ) def test_serialize_plans_uses_traffic_packages_in_traffic_mode(self): settings = Settings( _env_file=None, BOT_TOKEN="token", POSTGRES_USER="app_user", POSTGRES_PASSWORD="app_password", TARIFFS_CONFIG_PATH="missing-tariffs.json", TRAFFIC_PACKAGES="10:199,50:799", STARS_TRAFFIC_PACKAGES="50:2500", ) plans = subscription_webapp._serialize_plans(settings, "en") self.assertEqual([plan["traffic_gb"] for plan in plans], [10.0, 50.0]) self.assertEqual(plans[0]["sale_mode"], "traffic") self.assertEqual(plans[0]["price"], 199.0) self.assertEqual(plans[1]["stars_price"], 2500) def test_serialize_payment_methods_respects_runtime_provider_toggles(self): settings = Settings( _env_file=None, BOT_TOKEN="token", POSTGRES_USER="app_user", POSTGRES_PASSWORD="app_password", TARIFFS_CONFIG_PATH="missing-tariffs.json", CRYPTOPAY_ENABLED=False, FREEKASSA_ENABLED=False, SEVERPAY_ENABLED=False, YOOKASSA_ENABLED=False, PLATEGA_ENABLED=False, STARS_ENABLED=False, ) configured_service = SimpleNamespace(configured=True) app = { "cryptopay_service": configured_service, "freekassa_service": configured_service, "severpay_service": configured_service, "yookassa_service": configured_service, "platega_service": configured_service, } methods = subscription_webapp._serialize_payment_methods(settings, app) self.assertEqual(methods, []) def test_serialize_plans_includes_stars_only_subscription_options(self): settings = Settings( _env_file=None, BOT_TOKEN="token", POSTGRES_USER="app_user", POSTGRES_PASSWORD="app_password", YOOKASSA_ENABLED=False, CRYPTOPAY_ENABLED=False, TARIFFS_CONFIG_PATH="missing-tariffs.json", RUB_PRICE_1_MONTH=None, STARS_PRICE_1_MONTH=250, ) plans = subscription_webapp._serialize_plans(settings, "en") self.assertEqual(len(plans), 1) self.assertEqual(plans[0]["months"], 1) self.assertEqual(plans[0]["price"], 0.0) self.assertEqual(plans[0]["stars_price"], 250) def test_resolve_webapp_js_asset_name_prefers_latest_minified_build(self): with tempfile.TemporaryDirectory() as tmpdir: asset_dir = Path(tmpdir) (asset_dir / "subscription_webapp.js").write_text( "console.log('fallback');", encoding="utf-8" ) old_asset = asset_dir / "subscription_webapp.min.11111111.js" new_asset = asset_dir / "subscription_webapp.min.22222222.js" old_asset.write_text("console.log('old');", encoding="utf-8") new_asset.write_text("console.log('new');", encoding="utf-8") os.utime(old_asset, (1, 1)) os.utime(new_asset, (2, 2)) with patch.object(subscription_webapp, "ASSET_DIR", asset_dir): self.assertEqual( subscription_webapp._resolve_webapp_js_asset_name(), "subscription_webapp.min.22222222.js", ) async def test_js_asset_route_sets_immutable_cache_control_for_minified_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") request = SimpleNamespace( app={"settings": SimpleNamespace(WEBAPP_ENABLED=True)}, match_info={"asset_hash": "abcdef12"}, ) with patch.object(subscription_webapp, "ASSET_DIR", asset_dir): response = await subscription_webapp.js_asset_route(request) self.assertEqual( response.headers["Cache-Control"], "public, max-age=31536000, immutable" ) self.assertEqual(response.text, "console.log('minified');")