Add the Remnashop legacy importer, compatibility tables, admin toggles, referral and promo lookup compatibility, and tests for the migration flow.
356 lines
12 KiB
Python
356 lines
12 KiB
Python
import json
|
||
import re
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
from bot.app.web.admin_settings_manifest import coerce_value, get_field_by_key, manifest_payload
|
||
from bot.middlewares.i18n import resolve_locale_key
|
||
|
||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||
|
||
SUPPORT_RELATED_SETTINGS = (
|
||
"LOG_SUPPORT_THREAD_ID",
|
||
"SUPPORT_TICKETS_ENABLED",
|
||
"SUPPORT_ADMIN_EMAIL_NOTIFICATIONS_ENABLED",
|
||
"SUPPORT_ADMIN_NOTIFICATION_COOLDOWN_SECONDS",
|
||
"SUPPORT_ADMIN_EMAIL_COOLDOWN_SECONDS",
|
||
"SUPPORT_TICKET_MAX_BODY_LENGTH",
|
||
"SUPPORT_TICKET_MAX_SUBJECT_LENGTH",
|
||
"SUPPORT_TICKET_RATE_LIMIT_PER_HOUR",
|
||
)
|
||
|
||
SUBSCRIPTION_PURCHASE_DESCRIPTION_SETTINGS = (
|
||
"SUBSCRIPTION_PURCHASE_DESCRIPTION_ENABLED",
|
||
"SUBSCRIPTION_PURCHASE_DESCRIPTION_RU",
|
||
"SUBSCRIPTION_PURCHASE_DESCRIPTION_EN",
|
||
)
|
||
|
||
SUBSCRIPTION_GUIDE_SETTINGS = (
|
||
"SUBSCRIPTION_GUIDES_ENABLED",
|
||
"SUBSCRIPTION_GUIDES_BOT_MENU_ENABLED",
|
||
"SUBSCRIPTION_PAGE_CONFIG_PANEL_ENABLED",
|
||
"SUBSCRIPTION_PAGE_CONFIG_JSON_OVERRIDE_ENABLED",
|
||
"SUBSCRIPTION_PAGE_CONFIG_PATH",
|
||
"SUBSCRIPTION_PAGE_CONFIG_JSON",
|
||
)
|
||
|
||
BACKUP_SETTINGS = (
|
||
"BACKUP_ENABLED",
|
||
"BACKUP_CHAT_ID",
|
||
"BACKUP_THREAD_ID",
|
||
"BACKUP_INTERVAL_SECONDS",
|
||
"BACKUP_LOCAL_RETENTION",
|
||
"BACKUP_COMPOSE_ENABLED",
|
||
)
|
||
|
||
REMNASHOP_MIGRATION_SETTINGS = (
|
||
"MIGRATION_REMNASHOP_REFERRAL_CODE_COMPAT_ENABLED",
|
||
"MIGRATION_REMNASHOP_PROMO_CODE_COMPAT_ENABLED",
|
||
"MIGRATION_REMNASHOP_IMPORTED_AT",
|
||
"MIGRATION_REMNASHOP_NOTES",
|
||
)
|
||
|
||
ADMIN_TARIFF_SETTINGS_PAGE_KEYS = {
|
||
"admin_tariffs_trial_title",
|
||
"admin_tariffs_trial_subtitle",
|
||
"admin_tariffs_trial_enabled",
|
||
"admin_tariffs_trial_days",
|
||
"admin_tariffs_trial_traffic",
|
||
"admin_tariffs_trial_strategy",
|
||
"admin_tariffs_trial_squads",
|
||
"admin_tariffs_trial_squads_hint",
|
||
"admin_tariffs_trial_add_squad",
|
||
"admin_tariffs_trial_group_switch",
|
||
"admin_tariffs_trial_group_switch_hint",
|
||
"admin_tariffs_trial_group_general",
|
||
"admin_tariffs_trial_group_general_hint",
|
||
"admin_tariffs_trial_group_reset",
|
||
"admin_tariffs_trial_group_reset_hint",
|
||
"admin_tariffs_trial_group_squads",
|
||
"admin_tariffs_trial_group_squads_hint",
|
||
"admin_tariffs_legacy_title",
|
||
"admin_tariffs_legacy_subtitle",
|
||
"admin_tariffs_legacy_period",
|
||
"admin_tariffs_legacy_enabled",
|
||
"admin_tariffs_legacy_ref_inviter",
|
||
"admin_tariffs_legacy_ref_referee",
|
||
"admin_tariffs_legacy_traffic_packages",
|
||
"admin_tariffs_legacy_stars_traffic_packages",
|
||
"admin_tariffs_legacy_traffic_hint",
|
||
"admin_payment_rub",
|
||
"admin_payment_stars",
|
||
}
|
||
|
||
ADMIN_VISIBLE_RU_KEYS = {
|
||
"admin_appearance_theme_accent",
|
||
"admin_clear",
|
||
"admin_save",
|
||
"admin_saving",
|
||
"admin_search",
|
||
"admin_settings_badge_override",
|
||
"admin_settings_badge_secret",
|
||
"admin_settings_badge_dirty",
|
||
"admin_settings_icon_current",
|
||
"admin_settings_icon_default_value",
|
||
"admin_settings_icon_empty",
|
||
"admin_settings_icon_picker_title",
|
||
"admin_settings_icon_use_default",
|
||
"admin_tariff_label_premium_squads",
|
||
"admin_tariff_premium",
|
||
"admin_tariff_squads",
|
||
"admin_tariff_tab_premium",
|
||
}
|
||
|
||
ADMIN_AT_SIMPLE_FALLBACK_RE = re.compile(
|
||
r"""\bat\(\s*["'`](?P<key>[^"'`$]+)["'`]\s*,\s*\{[^)]*?\}\s*,\s*["'`](?P<fallback>[^"'`$]*)["'`]""",
|
||
re.DOTALL,
|
||
)
|
||
|
||
|
||
def _manifest_by_key() -> dict[str, dict]:
|
||
return {item["key"]: item for item in manifest_payload()}
|
||
|
||
|
||
def _manifest_items() -> list[dict]:
|
||
return manifest_payload()
|
||
|
||
|
||
def _locale(language: str) -> dict[str, str]:
|
||
return json.loads((REPO_ROOT / "locales" / f"{language}.json").read_text(encoding="utf-8"))
|
||
|
||
|
||
def _has_locale_key(messages: dict[str, str], key: str) -> bool:
|
||
return resolve_locale_key(key) in messages
|
||
|
||
|
||
def test_webapp_title_is_first_general_admin_setting():
|
||
items = _manifest_items()
|
||
manifest = {item["key"]: item for item in items}
|
||
field = manifest["WEBAPP_TITLE"]
|
||
|
||
assert field["section"] == "general"
|
||
assert field["section_order"] == 1
|
||
assert [item["key"] for item in items if item["section"] == "general"][0] == "WEBAPP_TITLE"
|
||
|
||
|
||
def test_support_settings_manifest_uses_admin_i18n_keys():
|
||
manifest = _manifest_by_key()
|
||
|
||
assert manifest["SUPPORT_TICKETS_ENABLED"]["section"] == "support"
|
||
assert manifest["SUPPORT_TICKETS_ENABLED"]["section_order"] == 8
|
||
|
||
for setting_key in SUPPORT_RELATED_SETTINGS:
|
||
field = manifest[setting_key]
|
||
prefix = f"admin_settings_field_{setting_key.lower()}"
|
||
|
||
assert field["i18n_label_key"] == f"{prefix}_label"
|
||
assert field["i18n_description_key"] == f"{prefix}_description"
|
||
|
||
|
||
def test_support_settings_i18n_keys_exist_in_admin_locales():
|
||
manifest = _manifest_by_key()
|
||
|
||
for language in ("ru", "en"):
|
||
messages = _locale(language)
|
||
|
||
assert "admin_settings_section_support" in messages
|
||
for setting_key in SUPPORT_RELATED_SETTINGS:
|
||
field = manifest[setting_key]
|
||
assert field["i18n_label_key"] in messages
|
||
assert field["i18n_description_key"] in messages
|
||
|
||
|
||
def test_subscription_purchase_description_settings_i18n_keys_exist():
|
||
manifest = _manifest_by_key()
|
||
|
||
for language in ("ru", "en"):
|
||
messages = _locale(language)
|
||
for setting_key in SUBSCRIPTION_PURCHASE_DESCRIPTION_SETTINGS:
|
||
field = manifest[setting_key]
|
||
assert field["section"] == "payments"
|
||
assert field["subsection"] == "checkout"
|
||
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
|
||
|
||
|
||
def test_backup_settings_i18n_keys_exist():
|
||
manifest = _manifest_by_key()
|
||
|
||
assert manifest["BACKUP_ENABLED"]["section"] == "backups"
|
||
assert manifest["BACKUP_ENABLED"]["section_order"] == 9
|
||
assert manifest["BACKUP_INTERVAL_SECONDS"]["min"] == 60
|
||
assert manifest["BACKUP_INTERVAL_SECONDS"]["optional"] is False
|
||
assert manifest["BACKUP_LOCAL_RETENTION"]["min"] == 1
|
||
assert manifest["BACKUP_LOCAL_RETENTION"]["optional"] is False
|
||
|
||
for language in ("ru", "en"):
|
||
messages = _locale(language)
|
||
|
||
assert "admin_settings_section_backups" in messages
|
||
for setting_key in BACKUP_SETTINGS:
|
||
field = manifest[setting_key]
|
||
assert field["i18n_label_key"] in messages
|
||
assert field["i18n_description_key"] in messages
|
||
|
||
|
||
def test_remnashop_migration_settings_i18n_keys_exist():
|
||
manifest = _manifest_by_key()
|
||
|
||
for setting_key in REMNASHOP_MIGRATION_SETTINGS:
|
||
field = manifest[setting_key]
|
||
assert field["section"] == "migrations"
|
||
assert field["section_order"] == 13
|
||
assert field["subsection"] == "Remnashop"
|
||
assert field["i18n_subsection_key"] == "admin_settings_subsection_remnashop"
|
||
|
||
for language in ("ru", "en"):
|
||
messages = _locale(language)
|
||
|
||
assert "admin_settings_section_migrations" in messages
|
||
assert "admin_settings_subsection_remnashop" in messages
|
||
for setting_key in REMNASHOP_MIGRATION_SETTINGS:
|
||
field = manifest[setting_key]
|
||
assert field["i18n_label_key"] in messages
|
||
assert field["i18n_description_key"] in messages
|
||
|
||
|
||
def test_backup_required_numeric_settings_reject_empty_values():
|
||
with pytest.raises(ValueError):
|
||
coerce_value(get_field_by_key("BACKUP_INTERVAL_SECONDS"), "")
|
||
|
||
|
||
def test_trial_required_settings_reject_empty_values():
|
||
for key in (
|
||
"TRIAL_ENABLED",
|
||
"TRIAL_DURATION_DAYS",
|
||
"TRIAL_TRAFFIC_LIMIT_GB",
|
||
"TRIAL_TRAFFIC_STRATEGY",
|
||
):
|
||
with pytest.raises(ValueError):
|
||
coerce_value(get_field_by_key(key), "")
|
||
|
||
|
||
def test_payment_provider_settings_include_webhook_metadata():
|
||
manifest = _manifest_by_key()
|
||
|
||
assert manifest["FREEKASSA_ENABLED"]["webhook_path"] == "/webhook/freekassa"
|
||
assert manifest["FREEKASSA_ENABLED"]["provider_id"] == "freekassa"
|
||
assert manifest["PAYMENT_PLATEGA_CRYPTO_WEBAPP_LABEL_RU"]["webhook_path"] == "/webhook/platega"
|
||
assert manifest["YOOKASSA_SHOP_ID"]["webhook_requires_base_url"] is True
|
||
assert "webhook_path" not in manifest["PAYMENT_STARS_WEBAPP_LABEL_RU"]
|
||
|
||
|
||
def test_remnawave_settings_include_panel_webhook_metadata():
|
||
manifest = _manifest_by_key()
|
||
field = manifest["PANEL_WEBHOOK_SECRET"]
|
||
remnawave_keys = (
|
||
"PANEL_API_URL",
|
||
"PANEL_API_KEY",
|
||
"PANEL_WEBHOOK_SECRET",
|
||
"USER_SQUAD_UUIDS",
|
||
"USER_EXTERNAL_SQUAD_UUID",
|
||
)
|
||
|
||
assert field["webhook_path"] == "/webhook/panel"
|
||
assert field["webhook_requires_base_url"] is True
|
||
assert field["provider_id"] == "remnawave"
|
||
assert field["webhook_hint_i18n_key"] == "admin_settings_panel_webhook_url_hint"
|
||
for setting_key in remnawave_keys:
|
||
assert manifest[setting_key]["section"] == "remnawave"
|
||
assert manifest[setting_key]["section_order"] == 3
|
||
assert manifest[setting_key]["subsection"] is None
|
||
|
||
for language in ("ru", "en"):
|
||
messages = _locale(language)
|
||
assert "admin_settings_section_remnawave" in messages
|
||
assert field["webhook_hint_i18n_key"] in messages
|
||
|
||
|
||
def test_payment_provider_admin_only_toggles_are_mutually_exclusive():
|
||
manifest = _manifest_by_key()
|
||
|
||
assert manifest["WATA_ADMIN_ONLY_ENABLED"]["mutually_exclusive_key"] == "WATA_ENABLED"
|
||
assert manifest["WATA_ENABLED"]["mutually_exclusive_key"] == "WATA_ADMIN_ONLY_ENABLED"
|
||
assert (
|
||
manifest["PLATEGA_CRYPTO_ADMIN_ONLY_ENABLED"]["mutually_exclusive_key"]
|
||
== "PLATEGA_CRYPTO_ENABLED"
|
||
)
|
||
assert manifest["STARS_ADMIN_ONLY_ENABLED"]["mutually_exclusive_key"] == "STARS_ENABLED"
|
||
|
||
|
||
def test_legacy_tariff_settings_are_separated_from_payment_settings():
|
||
manifest = _manifest_by_key()
|
||
payment_method_fields = [
|
||
item for item in _manifest_items() if item["key"] == "PAYMENT_METHODS_ORDER"
|
||
]
|
||
|
||
assert len(payment_method_fields) == 1
|
||
assert payment_method_fields[0]["section"] == "payments"
|
||
assert manifest["MONTH_1_ENABLED"]["section"] == "pricing"
|
||
assert manifest["MONTH_1_ENABLED"]["section_order"] == 11
|
||
assert manifest["TRIAL_ENABLED"]["section"] == "pricing"
|
||
assert manifest["TRIAL_ENABLED"]["subsection"] == "trial"
|
||
assert manifest["TRIAL_SQUAD_UUIDS"]["section"] == "pricing"
|
||
assert manifest["TRIAL_SQUAD_UUIDS"]["subsection"] == "trial"
|
||
|
||
|
||
def test_platega_settings_share_one_admin_subsection():
|
||
manifest = _manifest_by_key()
|
||
platega_keys = [
|
||
key for key in manifest if key.startswith("PLATEGA_") or key.startswith("PAYMENT_PLATEGA_")
|
||
]
|
||
|
||
assert platega_keys
|
||
assert {manifest[key]["subsection"] for key in platega_keys} == {"Platega"}
|
||
|
||
|
||
def test_tariff_settings_page_i18n_keys_exist():
|
||
for language in ("ru", "en"):
|
||
messages = _locale(language)
|
||
missing = {
|
||
key for key in ADMIN_TARIFF_SETTINGS_PAGE_KEYS if not _has_locale_key(messages, key)
|
||
}
|
||
assert missing == set()
|
||
|
||
|
||
def test_visible_admin_russian_labels_do_not_fall_back_to_english():
|
||
messages = _locale("ru")
|
||
|
||
for key in ADMIN_VISIBLE_RU_KEYS:
|
||
assert key in messages
|
||
|
||
missing_latin_fallbacks = []
|
||
for file_path in (REPO_ROOT / "frontend" / "src" / "admin").rglob("*"):
|
||
if file_path.suffix not in {".svelte", ".js"}:
|
||
continue
|
||
text = file_path.read_text(encoding="utf-8")
|
||
for match in ADMIN_AT_SIMPLE_FALLBACK_RE.finditer(text):
|
||
locale_key = f"admin_{match.group('key')}"
|
||
fallback = match.group("fallback")
|
||
if _has_locale_key(messages, locale_key):
|
||
continue
|
||
has_latin = any("A" <= char <= "Z" or "a" <= char <= "z" for char in fallback)
|
||
has_cyrillic = any("А" <= char <= "я" or char in "Ёё" for char in fallback)
|
||
if has_latin and not has_cyrillic:
|
||
missing_latin_fallbacks.append((locale_key, fallback, str(file_path)))
|
||
|
||
assert missing_latin_fallbacks == []
|