From fd1b910236afb3199cbefd74593ba48b6f7234d0 Mon Sep 17 00:00:00 2001 From: 3252a8 <3252a8@proton.me> Date: Sun, 17 May 2026 17:26:49 +0300 Subject: [PATCH] chore: cover can_topup_devices flag derivation --- tests/test_can_topup_devices_flag.py | 183 +++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 tests/test_can_topup_devices_flag.py diff --git a/tests/test_can_topup_devices_flag.py b/tests/test_can_topup_devices_flag.py new file mode 100644 index 0000000..e03e029 --- /dev/null +++ b/tests/test_can_topup_devices_flag.py @@ -0,0 +1,183 @@ +"""Tests for the ``can_topup_devices`` flag emitted by ``_serialize_subscription``. + +The flag drives whether the Web App shows the "buy HWID devices" button on the +Devices screen. Before this flag existed, the button was rendered for any +active subscription with a finite ``max_devices``, even if the user's tariff +had no HWID device packages configured. Clicking led the user into an empty +modal with "no options available" — confusing. + +These tests pin the visibility contract: + +* unlimited subscribers (max_devices == 0) → flag is False (no point in top-up); +* tariff without HWID packages → flag is False; +* tariff with rub-only or stars-only packages → flag is True; +* legacy mode without a tariffs catalog → flag is False; +* malformed tariff lookup → flag is False (and does not raise). +""" + +import json +import tempfile +import unittest +from datetime import datetime, timedelta, timezone +from pathlib import Path +from typing import Any, Dict, Optional + +# Importing the facade populates ``webapp.serializers`` with helpers like +# ``_format_remaining`` that come from ``_runtime`` via the facade init dance. +import bot.app.web.subscription_webapp # noqa: F401 +from bot.app.web.webapp.serializers import _serialize_subscription +from config.settings import Settings + + +def _tariffs_payload(*, hwid_rub=None, hwid_stars=None, has_premium=False) -> dict: + tariff: Dict[str, Any] = { + "key": "standard", + "names": {"en": "Standard"}, + "descriptions": {"en": "Base"}, + "squad_uuids": ["main"], + "billing_model": "period", + "monthly_gb": 100, + "prices_rub": {"1": 100}, + "prices_stars": {"1": 0}, + "enabled_periods": [1], + "hwid_device_limit": 3, + "enabled": True, + } + if has_premium: + tariff.update( + premium_squad_uuids=["premium-squad"], + premium_monthly_gb=20, + ) + if hwid_rub or hwid_stars: + tariff["hwid_device_packages"] = { + "rub": hwid_rub or [], + "stars": hwid_stars or [], + } + return {"default_tariff": "standard", "tariffs": [tariff]} + + +def _make_settings(tmpdir: str, payload: Optional[dict] = None, **overrides: Any) -> Settings: + values: Dict[str, Any] = { + "_env_file": None, + "BOT_TOKEN": "token", + "POSTGRES_USER": "u", + "POSTGRES_PASSWORD": "p", + } + if payload is not None: + config_path = Path(tmpdir) / "tariffs.json" + config_path.write_text(json.dumps(payload), encoding="utf-8") + values["TARIFFS_CONFIG_PATH"] = str(config_path) + values.update(overrides) + return Settings(**values) + + +def _active(**overrides) -> Dict[str, Any]: + """Build a minimal ``active`` dict shaped like ``get_active_subscription_details``.""" + base: Dict[str, Any] = { + "tariff_key": "standard", + "status_from_panel": "ACTIVE", + "end_date": datetime.now(timezone.utc) + timedelta(days=30), + "traffic_limit_bytes": 0, + "traffic_used_bytes": 0, + "premium_baseline_bytes": 0, + "premium_topup_balance_bytes": 0, + "premium_topup_used_bytes": 0, + "premium_used_bytes": 0, + "premium_limit_bytes": 0, + "max_devices": 3, + "base_hwid_device_limit": 3, + "extra_hwid_devices": 0, + "billing_model": "period", + "tariff_name": "Standard", + "tariff_description": "Base", + "premium_title": None, + "config_link": None, + "connect_button_url": None, + "tier_baseline_bytes": 0, + "topup_balance_bytes": 0, + "premium_bonus_bytes": 0, + "regular_bonus_bytes": 0, + "regular_unlimited_override": False, + "premium_unlimited_override": False, + "premium_is_limited": False, + "premium_squad_labels": [], + "premium_node_labels": [], + "period_start_at": None, + "is_throttled": False, + "traffic_limit_strategy": "", + } + base.update(overrides) + return base + + +class CanTopupDevicesFlagTests(unittest.TestCase): + def test_flag_is_true_when_tariff_has_rub_packages_and_limit_is_finite(self): + with tempfile.TemporaryDirectory() as tmpdir: + settings = _make_settings( + tmpdir, + _tariffs_payload(hwid_rub=[{"count": 1, "price": 50}]), + ) + payload = _serialize_subscription(settings, _active(max_devices=3), None, "en") + self.assertTrue(payload["can_topup_devices"]) + + def test_flag_is_true_when_only_stars_packages_are_configured(self): + # The bot only renders rub packages today, but the Web App grid renders + # both. The flag drives the Web App, so stars-only must still be True. + with tempfile.TemporaryDirectory() as tmpdir: + settings = _make_settings( + tmpdir, + _tariffs_payload(hwid_stars=[{"count": 1, "price": 100}]), + ) + payload = _serialize_subscription(settings, _active(max_devices=3), None, "en") + self.assertTrue(payload["can_topup_devices"]) + + def test_flag_is_false_when_max_devices_is_zero(self): + # Unlimited subscribers: hwid_devices topup is a dead end. + with tempfile.TemporaryDirectory() as tmpdir: + settings = _make_settings( + tmpdir, + _tariffs_payload(hwid_rub=[{"count": 1, "price": 50}]), + ) + payload = _serialize_subscription(settings, _active(max_devices=0), None, "en") + self.assertFalse(payload["can_topup_devices"]) + + def test_flag_is_false_when_tariff_has_no_hwid_packages(self): + with tempfile.TemporaryDirectory() as tmpdir: + settings = _make_settings(tmpdir, _tariffs_payload()) + payload = _serialize_subscription(settings, _active(), None, "en") + self.assertFalse(payload["can_topup_devices"]) + + def test_flag_is_false_when_packages_dict_is_empty(self): + with tempfile.TemporaryDirectory() as tmpdir: + settings = _make_settings( + tmpdir, + _tariffs_payload(hwid_rub=[], hwid_stars=[]), + ) + payload = _serialize_subscription(settings, _active(), None, "en") + self.assertFalse(payload["can_topup_devices"]) + + def test_flag_is_false_in_legacy_mode_without_tariff_catalog(self): + with tempfile.TemporaryDirectory() as tmpdir: + settings = _make_settings(tmpdir, payload=None) # no tariffs catalog + payload = _serialize_subscription( + settings, _active(tariff_key=None, max_devices=3), None, "en" + ) + self.assertFalse(payload["can_topup_devices"]) + + def test_flag_is_false_when_tariff_key_does_not_resolve(self): + # If the user's stored ``tariff_key`` was renamed/removed, the helper + # raises inside ``settings.tariffs_config.require``; we should swallow + # that and emit False, not 500. + with tempfile.TemporaryDirectory() as tmpdir: + settings = _make_settings( + tmpdir, + _tariffs_payload(hwid_rub=[{"count": 1, "price": 50}]), + ) + payload = _serialize_subscription( + settings, _active(tariff_key="missing-tariff"), None, "en" + ) + self.assertFalse(payload["can_topup_devices"]) + + +if __name__ == "__main__": # pragma: no cover + unittest.main()