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": '', "Copy": '', "Download": '', "Phone": '', }, "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"] = '' 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"