feat: install instruction inside web app
This commit is contained in:
@@ -22,6 +22,14 @@ SUBSCRIPTION_PURCHASE_DESCRIPTION_SETTINGS = (
|
||||
"SUBSCRIPTION_PURCHASE_DESCRIPTION_EN",
|
||||
)
|
||||
|
||||
SUBSCRIPTION_GUIDE_SETTINGS = (
|
||||
"SUBSCRIPTION_GUIDES_ENABLED",
|
||||
"SUBSCRIPTION_PAGE_CONFIG_PANEL_ENABLED",
|
||||
"SUBSCRIPTION_PAGE_CONFIG_JSON_OVERRIDE_ENABLED",
|
||||
"SUBSCRIPTION_PAGE_CONFIG_PATH",
|
||||
"SUBSCRIPTION_PAGE_CONFIG_JSON",
|
||||
)
|
||||
|
||||
|
||||
def _manifest_by_key() -> dict[str, dict]:
|
||||
return {item["key"]: item for item in manifest_payload()}
|
||||
@@ -68,3 +76,20 @@ def test_subscription_purchase_description_settings_i18n_keys_exist():
|
||||
assert field["section"] == "pricing"
|
||||
assert field["i18n_label_key"] in messages
|
||||
assert field["i18n_description_key"] in messages
|
||||
|
||||
|
||||
def test_subscription_guide_settings_i18n_keys_exist():
|
||||
manifest = _manifest_by_key()
|
||||
|
||||
assert manifest["SUBSCRIPTION_GUIDES_ENABLED"]["section"] == "subscription_guides"
|
||||
assert manifest["SUBSCRIPTION_GUIDES_ENABLED"]["section_order"] == 10
|
||||
assert manifest["SUBSCRIPTION_PAGE_CONFIG_JSON"]["type"] == "json"
|
||||
|
||||
for language in ("ru", "en"):
|
||||
messages = _locale(language)
|
||||
|
||||
assert "admin_settings_section_subscription_guides" in messages
|
||||
for setting_key in SUBSCRIPTION_GUIDE_SETTINGS:
|
||||
field = manifest[setting_key]
|
||||
assert field["i18n_label_key"] in messages
|
||||
assert field["i18n_description_key"] in messages
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from bot.app.web.admin_api_impl import webapp_runtime
|
||||
|
||||
|
||||
class AdminWebappRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_refresh_resets_settings_cache_and_invalidates_user_payloads(self):
|
||||
settings = SimpleNamespace()
|
||||
request = SimpleNamespace(
|
||||
app={
|
||||
"settings": settings,
|
||||
"webapp_settings_cache": {"ts": 123.0, "data": {"stale": True}},
|
||||
"subscription_guides_config_cache": {
|
||||
"fingerprint": ("stale",),
|
||||
"status": {"enabled": True},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
webapp_runtime,
|
||||
"invalidate_all_webapp_user_payloads",
|
||||
AsyncMock(),
|
||||
) as invalidate_mock:
|
||||
await webapp_runtime.refresh_webapp_runtime_after_settings_change(
|
||||
request,
|
||||
updates={"SUBSCRIPTION_GUIDES_ENABLED": True},
|
||||
deletes=[],
|
||||
)
|
||||
|
||||
self.assertEqual(request.app["webapp_settings_cache"], {"ts": 0.0, "data": {}})
|
||||
self.assertEqual(
|
||||
request.app["subscription_guides_config_cache"],
|
||||
{"fingerprint": None, "status": None},
|
||||
)
|
||||
invalidate_mock.assert_awaited_once_with(settings, include_devices=False)
|
||||
|
||||
async def test_refresh_clears_logo_cache_for_appearance_settings(self):
|
||||
settings = SimpleNamespace()
|
||||
request = SimpleNamespace(
|
||||
app={
|
||||
"settings": settings,
|
||||
"webapp_settings_cache": {"ts": 123.0, "data": {"stale": True}},
|
||||
"webapp_logo_cache": ("url", b"body", "image/png"),
|
||||
}
|
||||
)
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
webapp_runtime,
|
||||
"invalidate_all_webapp_user_payloads",
|
||||
AsyncMock(),
|
||||
),
|
||||
patch(
|
||||
"bot.app.web.admin_api_impl.themes.prune_unused_appearance_assets"
|
||||
) as prune_mock,
|
||||
):
|
||||
await webapp_runtime.refresh_webapp_runtime_after_settings_change(
|
||||
request,
|
||||
updates={"WEBAPP_LOGO_URL": "/webapp-uploaded-logo/logo.png"},
|
||||
deletes=[],
|
||||
)
|
||||
|
||||
self.assertIsNone(request.app["webapp_logo_cache"])
|
||||
prune_mock.assert_called_once_with(settings)
|
||||
@@ -79,6 +79,52 @@ class PanelApiServiceLoggingTests(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
self.assertEqual(service._request.await_count, 3)
|
||||
|
||||
async def test_get_subscription_page_config_by_short_uuid_uses_panel_endpoint(self):
|
||||
service = self._make_service()
|
||||
panel_payload = {"config": {"version": "1"}}
|
||||
service._request = AsyncMock(return_value={"response": panel_payload})
|
||||
|
||||
result = await service.get_subscription_page_config_by_short_uuid(
|
||||
"short-uuid",
|
||||
request_headers={"user-agent": "Mozilla/5.0"},
|
||||
)
|
||||
|
||||
self.assertEqual(result, panel_payload)
|
||||
service._request.assert_awaited_once_with(
|
||||
"GET",
|
||||
"/subscriptions/subpage-config/short-uuid",
|
||||
json={"requestHeaders": {"user-agent": "Mozilla/5.0"}},
|
||||
log_full_response=False,
|
||||
)
|
||||
|
||||
async def test_get_subscription_page_config_list_uses_panel_endpoint(self):
|
||||
service = self._make_service()
|
||||
panel_payload = {"configs": [{"uuid": "default"}]}
|
||||
service._request = AsyncMock(return_value={"response": panel_payload})
|
||||
|
||||
result = await service.get_subscription_page_config_list()
|
||||
|
||||
self.assertEqual(result, panel_payload)
|
||||
service._request.assert_awaited_once_with(
|
||||
"GET",
|
||||
"/subscription-page-configs",
|
||||
log_full_response=False,
|
||||
)
|
||||
|
||||
async def test_get_subscription_page_config_by_uuid_uses_panel_endpoint(self):
|
||||
service = self._make_service()
|
||||
panel_payload = {"uuid": "default", "config": {"version": "1"}}
|
||||
service._request = AsyncMock(return_value={"response": panel_payload})
|
||||
|
||||
result = await service.get_subscription_page_config_by_uuid("default")
|
||||
|
||||
self.assertEqual(result, panel_payload)
|
||||
service._request.assert_awaited_once_with(
|
||||
"GET",
|
||||
"/subscription-page-configs/default",
|
||||
log_full_response=False,
|
||||
)
|
||||
|
||||
async def test_get_all_panel_users_uses_singleflight_cache_and_update_invalidates(self):
|
||||
service = self._make_service()
|
||||
get_calls = 0
|
||||
|
||||
@@ -32,6 +32,23 @@ class SettingsTests(unittest.TestCase):
|
||||
self.assertTrue(settings.WEBHOOK_SECRET_TOKEN)
|
||||
self.assertEqual(settings.WEBAPP_SESSION_TTL_SECONDS, 86400)
|
||||
|
||||
def test_subscription_guides_defaults_are_enabled(self):
|
||||
settings = Settings(
|
||||
_env_file=None,
|
||||
BOT_TOKEN="token",
|
||||
POSTGRES_USER="app_user",
|
||||
POSTGRES_PASSWORD="app_password",
|
||||
)
|
||||
|
||||
self.assertTrue(settings.SUBSCRIPTION_GUIDES_ENABLED)
|
||||
self.assertTrue(settings.SUBSCRIPTION_PAGE_CONFIG_PANEL_ENABLED)
|
||||
self.assertFalse(settings.SUBSCRIPTION_PAGE_CONFIG_JSON_OVERRIDE_ENABLED)
|
||||
self.assertEqual(
|
||||
settings.SUBSCRIPTION_PAGE_CONFIG_PATH,
|
||||
"data/subpage-config/multiapp.json",
|
||||
)
|
||||
self.assertEqual(settings.SUBSCRIPTION_PAGE_CONFIG_JSON, "")
|
||||
|
||||
def test_deprecated_webapp_appearance_env_values_are_ignored(self):
|
||||
settings = Settings(
|
||||
_env_file=None,
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from config.subscription_guides_config import (
|
||||
SubscriptionGuidesConfigError,
|
||||
default_subscription_guides_config_text,
|
||||
extract_subscription_guides_config_from_panel,
|
||||
load_subscription_guides_config,
|
||||
panel_subscription_page_allowed,
|
||||
subscription_guides_admin_config_json,
|
||||
validate_panel_subscription_guides_config,
|
||||
validate_subscription_guides_config,
|
||||
validate_subscription_guides_config_text,
|
||||
)
|
||||
|
||||
BASE_TRANSLATION_KEYS = (
|
||||
"active",
|
||||
"bandwidth",
|
||||
"connectionKeysHeader",
|
||||
"copyLink",
|
||||
"expired",
|
||||
"expires",
|
||||
"expiresIn",
|
||||
"getLink",
|
||||
"inactive",
|
||||
"indefinitely",
|
||||
"installationGuideHeader",
|
||||
"linkCopied",
|
||||
"linkCopiedToClipboard",
|
||||
"name",
|
||||
"scanQrCode",
|
||||
"scanQrCodeDescription",
|
||||
"scanToImport",
|
||||
"status",
|
||||
"unknown",
|
||||
)
|
||||
|
||||
|
||||
def _localized(text):
|
||||
return {"ru": text, "en": text}
|
||||
|
||||
|
||||
def _config(app_name="Streisand"):
|
||||
return {
|
||||
"version": "1",
|
||||
"locales": ["ru", "en"],
|
||||
"brandingSettings": {
|
||||
"title": "Demo",
|
||||
"logoUrl": "https://example.com/logo.svg",
|
||||
"supportUrl": "https://t.me/support",
|
||||
},
|
||||
"uiConfig": {
|
||||
"subscriptionInfoBlockType": "collapsed",
|
||||
"installationGuidesBlockType": "cards",
|
||||
},
|
||||
"baseSettings": {
|
||||
"metaTitle": "Subscription",
|
||||
"metaDescription": "Subscription",
|
||||
"showConnectionKeys": False,
|
||||
"hideGetLinkButton": False,
|
||||
},
|
||||
"baseTranslations": {key: _localized(key) for key in BASE_TRANSLATION_KEYS},
|
||||
"svgLibrary": {
|
||||
"App": '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"></svg>',
|
||||
"Copy": '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"></svg>',
|
||||
"Download": '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"></svg>',
|
||||
"Phone": '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"></svg>',
|
||||
},
|
||||
"platforms": {
|
||||
"ios": {
|
||||
"displayName": "iOS",
|
||||
"svgIconKey": "Phone",
|
||||
"apps": [
|
||||
{
|
||||
"name": app_name,
|
||||
"svgIconKey": "App",
|
||||
"featured": True,
|
||||
"blocks": [
|
||||
{
|
||||
"svgIconKey": "Download",
|
||||
"svgIconColor": "green",
|
||||
"title": _localized("Install app"),
|
||||
"description": _localized("Install and import the subscription."),
|
||||
"buttons": [
|
||||
{
|
||||
"type": "external",
|
||||
"link": "https://apps.apple.com/app/example",
|
||||
"text": _localized("Open store"),
|
||||
"svgIconKey": "Download",
|
||||
},
|
||||
{
|
||||
"type": "copyButton",
|
||||
"link": "{{SUBSCRIPTION_LINK}}",
|
||||
"text": _localized("Copy link"),
|
||||
"svgIconKey": "Copy",
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_valid_multiapp_like_config_is_normalized():
|
||||
config = validate_subscription_guides_config(_config())
|
||||
|
||||
assert config["version"] == "1"
|
||||
assert config["locales"] == ["ru", "en"]
|
||||
assert config["platforms"]["ios"]["apps"][0]["name"] == "Streisand"
|
||||
|
||||
|
||||
def test_bundled_default_multiapp_config_is_valid():
|
||||
config = validate_subscription_guides_config_text(default_subscription_guides_config_text())
|
||||
|
||||
assert set(config["platforms"]) == {
|
||||
"android",
|
||||
"androidTV",
|
||||
"appleTV",
|
||||
"ios",
|
||||
"linux",
|
||||
"macos",
|
||||
"windows",
|
||||
}
|
||||
assert config["platforms"]["ios"]["displayName"]["ru"] == "iOS"
|
||||
|
||||
|
||||
def test_missing_locale_string_is_rejected():
|
||||
config = _config()
|
||||
del config["platforms"]["ios"]["apps"][0]["blocks"][0]["title"]["en"]
|
||||
|
||||
with pytest.raises(SubscriptionGuidesConfigError, match="title.en"):
|
||||
validate_subscription_guides_config(config)
|
||||
|
||||
|
||||
def test_bad_platform_is_rejected():
|
||||
config = _config()
|
||||
config["platforms"]["bsd"] = config["platforms"].pop("ios")
|
||||
|
||||
with pytest.raises(SubscriptionGuidesConfigError, match="Unsupported platform"):
|
||||
validate_subscription_guides_config(config)
|
||||
|
||||
|
||||
def test_missing_svg_key_is_rejected():
|
||||
config = _config()
|
||||
config["platforms"]["ios"]["svgIconKey"] = "Missing"
|
||||
|
||||
with pytest.raises(SubscriptionGuidesConfigError, match="missing svgLibrary key"):
|
||||
validate_subscription_guides_config(config)
|
||||
|
||||
|
||||
def test_unsafe_svg_is_rejected():
|
||||
config = _config()
|
||||
config["svgLibrary"]["App"] = '<svg viewBox="0 0 24 24" onload="alert(1)"></svg>'
|
||||
|
||||
with pytest.raises(SubscriptionGuidesConfigError, match="unsafe SVG"):
|
||||
validate_subscription_guides_config(config)
|
||||
|
||||
|
||||
def test_unsafe_external_link_is_rejected():
|
||||
config = _config()
|
||||
config["platforms"]["ios"]["apps"][0]["blocks"][0]["buttons"][0][
|
||||
"link"
|
||||
] = "javascript:alert(1)"
|
||||
|
||||
with pytest.raises(SubscriptionGuidesConfigError, match="unsafe URL scheme"):
|
||||
validate_subscription_guides_config(config)
|
||||
|
||||
|
||||
def test_external_custom_scheme_is_allowed_for_multiapp_compatibility():
|
||||
config = _config()
|
||||
config["platforms"]["ios"]["apps"][0]["blocks"][0]["buttons"][0][
|
||||
"link"
|
||||
] = "streisand://import/demo"
|
||||
|
||||
validated = validate_subscription_guides_config(config)
|
||||
|
||||
assert (
|
||||
validated["platforms"]["ios"]["apps"][0]["blocks"][0]["buttons"][0]["link"]
|
||||
== "streisand://import/demo"
|
||||
)
|
||||
|
||||
|
||||
def test_panel_response_config_wrapper_is_supported():
|
||||
payload = {"response": {"config": json.dumps(_config(app_name="Panel App"))}}
|
||||
|
||||
validated = validate_panel_subscription_guides_config(payload)
|
||||
|
||||
assert validated["platforms"]["ios"]["apps"][0]["name"] == "Panel App"
|
||||
|
||||
|
||||
def test_panel_response_direct_v1_config_is_supported():
|
||||
payload = {"response": _config(app_name="Panel Direct App")}
|
||||
|
||||
extracted = extract_subscription_guides_config_from_panel(payload)
|
||||
validated = validate_panel_subscription_guides_config(payload)
|
||||
|
||||
assert extracted["version"] == "1"
|
||||
assert validated["platforms"]["ios"]["apps"][0]["name"] == "Panel Direct App"
|
||||
|
||||
|
||||
def test_panel_response_without_v1_config_is_rejected():
|
||||
with pytest.raises(SubscriptionGuidesConfigError, match="does not contain"):
|
||||
validate_panel_subscription_guides_config({"response": {"config": {"version": "2"}}})
|
||||
|
||||
|
||||
def test_panel_response_with_allowed_default_uses_bundled_config():
|
||||
validated = validate_panel_subscription_guides_config(
|
||||
{"response": {"subpageConfigUuid": None, "webpageAllowed": True}},
|
||||
allow_default_when_missing=True,
|
||||
)
|
||||
|
||||
assert panel_subscription_page_allowed({"response": {"webpageAllowed": True}})
|
||||
assert validated["version"] == "1"
|
||||
assert set(validated["platforms"]) >= {"ios", "android", "windows"}
|
||||
|
||||
|
||||
def test_admin_json_overrides_file_path(tmp_path):
|
||||
file_config = _config(app_name="File App")
|
||||
json_config = _config(app_name="JSON App")
|
||||
config_path = tmp_path / "multiapp.json"
|
||||
config_path.write_text(json.dumps(file_config), encoding="utf-8")
|
||||
settings = SimpleNamespace(
|
||||
SUBSCRIPTION_PAGE_CONFIG_PATH=str(config_path),
|
||||
SUBSCRIPTION_PAGE_CONFIG_JSON_OVERRIDE_ENABLED=True,
|
||||
SUBSCRIPTION_PAGE_CONFIG_JSON=json.dumps(json_config),
|
||||
)
|
||||
|
||||
loaded, source = load_subscription_guides_config(settings)
|
||||
|
||||
assert source == "admin_json"
|
||||
assert loaded["platforms"]["ios"]["apps"][0]["name"] == "JSON App"
|
||||
|
||||
|
||||
def test_admin_json_is_ignored_when_override_switch_is_disabled(tmp_path):
|
||||
file_config = _config(app_name="File App")
|
||||
json_config = _config(app_name="JSON App")
|
||||
config_path = tmp_path / "multiapp.json"
|
||||
config_path.write_text(json.dumps(file_config), encoding="utf-8")
|
||||
settings = SimpleNamespace(
|
||||
SUBSCRIPTION_PAGE_CONFIG_PATH=str(config_path),
|
||||
SUBSCRIPTION_PAGE_CONFIG_JSON_OVERRIDE_ENABLED=False,
|
||||
SUBSCRIPTION_PAGE_CONFIG_JSON=json.dumps(json_config),
|
||||
)
|
||||
|
||||
loaded, source = load_subscription_guides_config(settings)
|
||||
|
||||
assert source == "file"
|
||||
assert loaded["platforms"]["ios"]["apps"][0]["name"] == "File App"
|
||||
|
||||
|
||||
def test_file_path_is_used_when_admin_json_is_empty(tmp_path):
|
||||
file_config = _config(app_name="File App")
|
||||
config_path = tmp_path / "multiapp.json"
|
||||
config_path.write_text(json.dumps(file_config), encoding="utf-8")
|
||||
settings = SimpleNamespace(
|
||||
SUBSCRIPTION_PAGE_CONFIG_PATH=str(config_path),
|
||||
SUBSCRIPTION_PAGE_CONFIG_JSON="",
|
||||
)
|
||||
|
||||
loaded, source = load_subscription_guides_config(settings)
|
||||
|
||||
assert source == "file"
|
||||
assert loaded["platforms"]["ios"]["apps"][0]["name"] == "File App"
|
||||
|
||||
|
||||
def test_missing_file_path_is_not_created_implicitly(tmp_path):
|
||||
config_path = tmp_path / "subpage-config" / "multiapp.json"
|
||||
settings = SimpleNamespace(
|
||||
SUBSCRIPTION_PAGE_CONFIG_PATH=str(config_path),
|
||||
SUBSCRIPTION_PAGE_CONFIG_JSON="",
|
||||
)
|
||||
|
||||
with pytest.raises(SubscriptionGuidesConfigError, match="does not exist"):
|
||||
load_subscription_guides_config(settings)
|
||||
|
||||
assert not config_path.exists()
|
||||
|
||||
|
||||
def test_admin_json_editor_is_empty_when_override_is_empty(tmp_path):
|
||||
config_path = tmp_path / "subpage-config" / "multiapp.json"
|
||||
settings = SimpleNamespace(
|
||||
SUBSCRIPTION_PAGE_CONFIG_PATH=str(config_path),
|
||||
SUBSCRIPTION_PAGE_CONFIG_JSON="",
|
||||
)
|
||||
|
||||
raw, source = subscription_guides_admin_config_json(settings)
|
||||
|
||||
assert source == "empty"
|
||||
assert raw == ""
|
||||
assert not config_path.exists()
|
||||
|
||||
|
||||
def test_admin_json_editor_keeps_admin_override_without_creating_file(tmp_path):
|
||||
config_path = tmp_path / "subpage-config" / "multiapp.json"
|
||||
override = json.dumps(_config(app_name="JSON App"))
|
||||
settings = SimpleNamespace(
|
||||
SUBSCRIPTION_PAGE_CONFIG_PATH=str(config_path),
|
||||
SUBSCRIPTION_PAGE_CONFIG_JSON=override,
|
||||
)
|
||||
|
||||
raw, source = subscription_guides_admin_config_json(settings)
|
||||
|
||||
assert not config_path.exists()
|
||||
assert source == "admin_json"
|
||||
assert json.loads(raw)["platforms"]["ios"]["apps"][0]["name"] == "JSON App"
|
||||
@@ -0,0 +1,208 @@
|
||||
import asyncio
|
||||
import json
|
||||
import unittest
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from bot.app.web import subscription_webapp as guides
|
||||
from config.subscription_guides_config import default_subscription_guides_config_text
|
||||
|
||||
|
||||
class _AsyncSessionFactory:
|
||||
def __call__(self):
|
||||
return self
|
||||
|
||||
async def __aenter__(self):
|
||||
return object()
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
return False
|
||||
|
||||
|
||||
class SubscriptionGuidesRouteTests(unittest.IsolatedAsyncioTestCase):
|
||||
def _request(self, settings, panel_service, match_info=None):
|
||||
return SimpleNamespace(
|
||||
app={
|
||||
"settings": settings,
|
||||
"async_session_factory": _AsyncSessionFactory(),
|
||||
"panel_service": panel_service,
|
||||
"subscription_guides_config_cache": {"fingerprint": None, "status": None},
|
||||
"subscription_guides_config_lock": asyncio.Lock(),
|
||||
},
|
||||
match_info=match_info or {},
|
||||
headers={"User-Agent": "Mozilla/5.0", "Host": "app.example.test"},
|
||||
host="app.example.test",
|
||||
scheme="https",
|
||||
)
|
||||
|
||||
def _settings(self, **overrides):
|
||||
values = {
|
||||
"SUBSCRIPTION_GUIDES_ENABLED": True,
|
||||
"SUBSCRIPTION_PAGE_CONFIG_PANEL_ENABLED": True,
|
||||
"SUBSCRIPTION_PAGE_CONFIG_JSON_OVERRIDE_ENABLED": False,
|
||||
"SUBSCRIPTION_PAGE_CONFIG_JSON": "",
|
||||
"SUBSCRIPTION_PAGE_CONFIG_PATH": "data/subpage-config/multiapp.json",
|
||||
"SUBSCRIPTION_MINI_APP_URL": "https://app.example.test",
|
||||
"CRYPT4_ENABLED": False,
|
||||
"CRYPT4_REDIRECT_URL": "",
|
||||
"CRYPT4_LINK_CACHE_TTL_SECONDS": 3600,
|
||||
}
|
||||
values.update(overrides)
|
||||
return SimpleNamespace(**values)
|
||||
|
||||
def _auth_patch(self):
|
||||
return patch.dict(
|
||||
guides.subscription_guides_route.__globals__,
|
||||
{"_require_user_id": lambda _: 42},
|
||||
)
|
||||
|
||||
async def test_uses_panel_config_when_admin_json_is_empty(self):
|
||||
default_uuid = "00000000-0000-0000-0000-000000000000"
|
||||
panel_service = SimpleNamespace(
|
||||
get_subscription_page_config_list=AsyncMock(
|
||||
return_value={"configs": [{"uuid": default_uuid, "viewPosition": 1}]}
|
||||
),
|
||||
get_subscription_page_config_by_uuid=AsyncMock(
|
||||
return_value={
|
||||
"uuid": default_uuid,
|
||||
"config": json.loads(default_subscription_guides_config_text()),
|
||||
}
|
||||
)
|
||||
)
|
||||
request = self._request(self._settings(), panel_service)
|
||||
|
||||
with self._auth_patch():
|
||||
response = await guides.subscription_guides_route(request)
|
||||
|
||||
body = json.loads(response.text)
|
||||
self.assertTrue(body["enabled"])
|
||||
self.assertEqual(body["source"], "panel")
|
||||
self.assertEqual(body["config"]["version"], "1")
|
||||
panel_service.get_subscription_page_config_list.assert_awaited_once()
|
||||
panel_service.get_subscription_page_config_by_uuid.assert_awaited_once_with(default_uuid)
|
||||
|
||||
async def test_admin_json_override_takes_priority_over_panel(self):
|
||||
admin_config = json.loads(default_subscription_guides_config_text())
|
||||
panel_service = SimpleNamespace(get_subscription_page_config_by_uuid=AsyncMock())
|
||||
request = self._request(
|
||||
self._settings(
|
||||
SUBSCRIPTION_PAGE_CONFIG_JSON_OVERRIDE_ENABLED=True,
|
||||
SUBSCRIPTION_PAGE_CONFIG_JSON=json.dumps(admin_config),
|
||||
),
|
||||
panel_service,
|
||||
)
|
||||
|
||||
with self._auth_patch():
|
||||
response = await guides.subscription_guides_route(request)
|
||||
|
||||
body = json.loads(response.text)
|
||||
self.assertTrue(body["enabled"])
|
||||
self.assertEqual(body["source"], "admin_json")
|
||||
panel_service.get_subscription_page_config_by_uuid.assert_not_called()
|
||||
|
||||
async def test_admin_json_is_ignored_until_override_switch_is_enabled(self):
|
||||
default_uuid = "00000000-0000-0000-0000-000000000000"
|
||||
admin_config = json.loads(default_subscription_guides_config_text())
|
||||
panel_service = SimpleNamespace(
|
||||
get_subscription_page_config_list=AsyncMock(
|
||||
return_value={"configs": [{"uuid": default_uuid, "viewPosition": 1}]}
|
||||
),
|
||||
get_subscription_page_config_by_uuid=AsyncMock(
|
||||
return_value={
|
||||
"uuid": default_uuid,
|
||||
"config": json.loads(default_subscription_guides_config_text()),
|
||||
}
|
||||
)
|
||||
)
|
||||
request = self._request(
|
||||
self._settings(SUBSCRIPTION_PAGE_CONFIG_JSON=json.dumps(admin_config)),
|
||||
panel_service,
|
||||
)
|
||||
|
||||
with self._auth_patch():
|
||||
response = await guides.subscription_guides_route(request)
|
||||
|
||||
body = json.loads(response.text)
|
||||
self.assertTrue(body["enabled"])
|
||||
self.assertEqual(body["source"], "panel")
|
||||
panel_service.get_subscription_page_config_by_uuid.assert_awaited_once_with(default_uuid)
|
||||
|
||||
async def test_panel_config_is_cached_for_multiple_users(self):
|
||||
default_uuid = "00000000-0000-0000-0000-000000000000"
|
||||
panel_config = json.loads(default_subscription_guides_config_text())
|
||||
panel_config["platforms"]["windows"]["apps"][0]["name"] = "Throne"
|
||||
panel_service = SimpleNamespace(
|
||||
get_subscription_page_config_list=AsyncMock(
|
||||
return_value={"configs": [{"uuid": default_uuid, "viewPosition": 1}]}
|
||||
),
|
||||
get_subscription_page_config_by_uuid=AsyncMock(
|
||||
return_value={"uuid": default_uuid, "config": panel_config}
|
||||
),
|
||||
)
|
||||
request = self._request(self._settings(), panel_service)
|
||||
|
||||
with self._auth_patch():
|
||||
response = await guides.subscription_guides_route(request)
|
||||
second_response = await guides.subscription_guides_route(request)
|
||||
|
||||
body = json.loads(response.text)
|
||||
second_body = json.loads(second_response.text)
|
||||
self.assertTrue(body["enabled"])
|
||||
self.assertTrue(second_body["enabled"])
|
||||
self.assertEqual(body["source"], "panel")
|
||||
self.assertEqual(body["config"]["version"], "1")
|
||||
self.assertIn("windows", body["config"]["platforms"])
|
||||
windows_apps = [app["name"] for app in body["config"]["platforms"]["windows"]["apps"]]
|
||||
self.assertIn("Throne", windows_apps)
|
||||
panel_service.get_subscription_page_config_list.assert_awaited_once()
|
||||
panel_service.get_subscription_page_config_by_uuid.assert_awaited_once_with(default_uuid)
|
||||
|
||||
async def test_public_route_returns_shared_config_and_subscription_payload(self):
|
||||
default_uuid = "00000000-0000-0000-0000-000000000000"
|
||||
panel_config = json.loads(default_subscription_guides_config_text())
|
||||
panel_service = SimpleNamespace(
|
||||
get_subscription_page_config_list=AsyncMock(
|
||||
return_value={"configs": [{"uuid": default_uuid, "viewPosition": 1}]}
|
||||
),
|
||||
get_subscription_page_config_by_uuid=AsyncMock(
|
||||
return_value={"uuid": default_uuid, "config": panel_config}
|
||||
),
|
||||
get_user_by_uuid=AsyncMock(
|
||||
return_value={
|
||||
"shortUuid": "share-short",
|
||||
"subscriptionUrl": "https://sb.example.test/share-short",
|
||||
"username": "demo",
|
||||
}
|
||||
),
|
||||
)
|
||||
request = self._request(
|
||||
self._settings(SUBSCRIPTION_MINI_APP_URL="https://app.example.test/app"),
|
||||
panel_service,
|
||||
match_info={"short_uuid": "share-short"},
|
||||
)
|
||||
local_sub = SimpleNamespace(
|
||||
panel_user_uuid="panel-user",
|
||||
is_active=True,
|
||||
end_date=datetime.now(timezone.utc) + timedelta(days=3),
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
guides.subscription_dal,
|
||||
"get_subscription_by_panel_subscription_uuid",
|
||||
AsyncMock(return_value=local_sub),
|
||||
):
|
||||
response = await guides.public_subscription_guides_route(request)
|
||||
|
||||
body = json.loads(response.text)
|
||||
self.assertTrue(body["enabled"])
|
||||
self.assertEqual(body["subscription"]["config_link"], "https://sb.example.test/share-short")
|
||||
self.assertEqual(
|
||||
body["subscription"]["share_url"],
|
||||
"https://app.example.test/install/share/share-short",
|
||||
)
|
||||
panel_service.get_user_by_uuid.assert_awaited_once_with("panel-user")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -3,7 +3,7 @@ from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
import bot.app.web.subscription_webapp # noqa: F401
|
||||
from bot.app.web.webapp import common as common_module
|
||||
from bot.app.web.webapp import cache_helpers
|
||||
|
||||
|
||||
class WebappRedisCacheInvalidationTests(unittest.IsolatedAsyncioTestCase):
|
||||
@@ -14,8 +14,8 @@ class WebappRedisCacheInvalidationTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def fake_delete(_settings, *keys):
|
||||
deleted.extend(keys)
|
||||
|
||||
with patch.object(common_module, "cache_delete", fake_delete):
|
||||
await common_module._invalidate_webapp_user_caches(
|
||||
with patch.object(cache_helpers, "cache_delete", fake_delete):
|
||||
await cache_helpers.invalidate_webapp_user_caches(
|
||||
settings,
|
||||
42,
|
||||
"42",
|
||||
@@ -33,6 +33,25 @@ class WebappRedisCacheInvalidationTests(unittest.IsolatedAsyncioTestCase):
|
||||
],
|
||||
)
|
||||
|
||||
async def test_invalidate_all_webapp_user_payloads_deletes_namespace_patterns(self):
|
||||
settings = SimpleNamespace(REDIS_URL="redis://redis:6379/0", REDIS_KEY_PREFIX="shop")
|
||||
patterns = []
|
||||
|
||||
async def fake_delete_pattern(_settings, pattern):
|
||||
patterns.append(pattern)
|
||||
return 0
|
||||
|
||||
with patch.object(cache_helpers, "cache_delete_pattern", fake_delete_pattern):
|
||||
await cache_helpers.invalidate_all_webapp_user_payloads(settings, include_devices=True)
|
||||
|
||||
self.assertEqual(
|
||||
patterns,
|
||||
[
|
||||
"shop:cache:webapp:me:*",
|
||||
"shop:cache:webapp:devices:*",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -50,6 +50,8 @@ class WebAppRouteContractTests(unittest.TestCase):
|
||||
("GET", "/"): "index_route",
|
||||
("GET", "/login/password"): "index_route",
|
||||
("GET", "/home"): "index_route",
|
||||
("GET", "/install"): "index_route",
|
||||
("GET", "/install/share/{short_uuid}"): "index_route",
|
||||
("GET", "/invite"): "index_route",
|
||||
("GET", "/devices"): "index_route",
|
||||
("GET", "/settings"): "index_route",
|
||||
@@ -77,6 +79,11 @@ class WebAppRouteContractTests(unittest.TestCase):
|
||||
("POST", "/api/auth/email/password"): "email_password_auth_route",
|
||||
("POST", "/api/auth/logout"): "logout_route",
|
||||
("GET", "/api/me"): "me_route",
|
||||
("GET", "/api/subscription-guides"): "subscription_guides_route",
|
||||
(
|
||||
"GET",
|
||||
"/api/subscription-guides/public/{short_uuid}",
|
||||
): "public_subscription_guides_route",
|
||||
("GET", "/api/account/avatar"): "account_avatar_route",
|
||||
("POST", "/api/account/language"): "account_language_route",
|
||||
("POST", "/api/account/email/request"): "account_email_request_route",
|
||||
|
||||
Reference in New Issue
Block a user