feat: install instruction inside web app

This commit is contained in:
3252a8
2026-05-22 13:58:34 +03:00
parent 8a38524774
commit 9e61a3d8a8
41 changed files with 8216 additions and 72 deletions
@@ -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
+67
View File
@@ -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)
+46
View File
@@ -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
+17
View File
@@ -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,
+310
View File
@@ -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"
+208
View File
@@ -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()
+22 -3
View File
@@ -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()
+7
View File
@@ -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",