482 lines
20 KiB
Python
482 lines
20 KiB
Python
"""Behavioural tests for ``HwidDeviceMixin.activate_hwid_device_topup``.
|
|
|
|
The HWID device top-up flow already checks the panel update result correctly
|
|
(unlike the regular/premium traffic top-ups, which were the subject of a
|
|
recent fix). These tests pin that:
|
|
|
|
* an unknown package count for the tariff is rejected (anti-tamper);
|
|
* unlimited subscribers (``hwid_device_limit == 0``) are a no-op that still
|
|
returns a meaningful payload;
|
|
* the panel call uses ``hwidDeviceLimit = base + extra``, not just ``base``;
|
|
* a panel failure returns ``None`` and DOES NOT write the audit row;
|
|
* the happy path records a validity window ending at the subscription end;
|
|
* renewal top-ups start after the current HWID entitlement and do not double
|
|
the active device limit before that date.
|
|
"""
|
|
|
|
import json
|
|
import tempfile
|
|
import unittest
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from typing import Any
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
from bot.services.panel_api_service import PanelApiService
|
|
from bot.services.subscription_service import SubscriptionService
|
|
from config.settings import Settings
|
|
|
|
|
|
def _tariffs_config_payload(**overrides: Any) -> dict:
|
|
tariff = {
|
|
"key": "standard",
|
|
"names": {"en": "Standard"},
|
|
"descriptions": {"en": "Base"},
|
|
"squad_uuids": ["main-squad"],
|
|
"billing_model": "period",
|
|
"monthly_gb": 100,
|
|
"prices_rub": {"1": 100},
|
|
"prices_stars": {"1": 0},
|
|
"enabled_periods": [1],
|
|
"hwid_device_limit": 3,
|
|
"hwid_device_packages": {
|
|
"rub": [{"count": 1, "price": 50}, {"count": 3, "price": 120}],
|
|
"stars": [],
|
|
},
|
|
"enabled": True,
|
|
}
|
|
tariff.update(overrides)
|
|
return {"default_tariff": "standard", "tariffs": [tariff]}
|
|
|
|
|
|
def _make_settings(tmpdir: str, payload: dict, **overrides: Any) -> Settings:
|
|
config_path = Path(tmpdir) / "tariffs.json"
|
|
config_path.write_text(json.dumps(payload), encoding="utf-8")
|
|
values: dict[str, Any] = {
|
|
"_env_file": None,
|
|
"BOT_TOKEN": "token",
|
|
"POSTGRES_USER": "u",
|
|
"POSTGRES_PASSWORD": "p",
|
|
"TARIFFS_CONFIG_PATH": str(config_path),
|
|
}
|
|
values.update(overrides)
|
|
return Settings(**values)
|
|
|
|
|
|
def _make_service(settings: Settings) -> SubscriptionService:
|
|
return SubscriptionService(settings, AsyncMock(spec=PanelApiService))
|
|
|
|
|
|
def _make_sub(*, hwid_device_limit=3, extra_hwid_devices=0):
|
|
return SimpleNamespace(
|
|
subscription_id=11,
|
|
user_id=42,
|
|
panel_user_uuid="panel-uuid",
|
|
panel_subscription_uuid="panel-sub",
|
|
tariff_key="standard",
|
|
end_date=datetime(2099, 1, 1, tzinfo=timezone.utc),
|
|
start_date=datetime(2098, 12, 1, tzinfo=timezone.utc),
|
|
duration_months=1,
|
|
hwid_device_limit=hwid_device_limit,
|
|
extra_hwid_devices=extra_hwid_devices,
|
|
)
|
|
|
|
|
|
def _make_user():
|
|
return SimpleNamespace(
|
|
user_id=42,
|
|
telegram_id=42,
|
|
email=None,
|
|
username="u",
|
|
first_name="U",
|
|
last_name="L",
|
|
language_code="en",
|
|
panel_user_uuid="panel-uuid",
|
|
)
|
|
|
|
|
|
class HwidDeviceTopupInputTests(unittest.IsolatedAsyncioTestCase):
|
|
async def test_rejects_non_positive_device_count(self):
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
settings = _make_settings(tmpdir, _tariffs_config_payload())
|
|
service = _make_service(settings)
|
|
for bad in (0, -1, "abc"):
|
|
with self.subTest(bad=bad):
|
|
result = await service.activate_hwid_device_topup(
|
|
session=AsyncMock(),
|
|
user_id=42,
|
|
device_count=bad,
|
|
payment_amount=50,
|
|
payment_db_id=1,
|
|
)
|
|
self.assertIsNone(result)
|
|
|
|
async def test_rejects_count_not_in_tariff_packages(self):
|
|
# Tariff only offers 1-device and 3-device packages; 2 should be rejected.
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
settings = _make_settings(tmpdir, _tariffs_config_payload())
|
|
service = _make_service(settings)
|
|
sub = _make_sub()
|
|
user = _make_user()
|
|
with (
|
|
patch(
|
|
"bot.services.subscription_service_impl.devices.user_dal.get_user_by_id",
|
|
AsyncMock(return_value=user),
|
|
),
|
|
patch(
|
|
"bot.services.subscription_service_impl.devices.subscription_dal.get_active_subscription_by_user_id",
|
|
AsyncMock(return_value=sub),
|
|
),
|
|
):
|
|
result = await service.activate_hwid_device_topup(
|
|
session=AsyncMock(),
|
|
user_id=42,
|
|
device_count=2,
|
|
payment_amount=70,
|
|
payment_db_id=1,
|
|
)
|
|
self.assertIsNone(result)
|
|
|
|
async def test_rejects_traffic_tariff(self):
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
settings = _make_settings(
|
|
tmpdir,
|
|
_tariffs_config_payload(
|
|
billing_model="traffic",
|
|
traffic_packages={"rub": [{"gb": 100, "price": 100}], "stars": []},
|
|
),
|
|
)
|
|
service = _make_service(settings)
|
|
sub = _make_sub()
|
|
user = _make_user()
|
|
with (
|
|
patch(
|
|
"bot.services.subscription_service_impl.devices.user_dal.get_user_by_id",
|
|
AsyncMock(return_value=user),
|
|
),
|
|
patch(
|
|
"bot.services.subscription_service_impl.devices.subscription_dal.get_active_subscription_by_user_id",
|
|
AsyncMock(return_value=sub),
|
|
),
|
|
):
|
|
result = await service.activate_hwid_device_topup(
|
|
session=AsyncMock(),
|
|
user_id=42,
|
|
device_count=1,
|
|
payment_amount=50,
|
|
payment_db_id=1,
|
|
)
|
|
self.assertIsNone(result)
|
|
|
|
|
|
class HwidDeviceTopupBehaviourTests(unittest.IsolatedAsyncioTestCase):
|
|
async def test_quote_prorates_period_price_for_remaining_subscription_window(self):
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
settings = _make_settings(
|
|
tmpdir,
|
|
_tariffs_config_payload(
|
|
hwid_device_packages={
|
|
"rub": [
|
|
{
|
|
"count": 1,
|
|
"price": 100,
|
|
"prices": {"1": 100},
|
|
"min_price": 10,
|
|
}
|
|
],
|
|
"stars": [],
|
|
}
|
|
),
|
|
)
|
|
service = _make_service(settings)
|
|
sub = _make_sub()
|
|
sub.start_date = datetime(2099, 1, 1, tzinfo=timezone.utc)
|
|
sub.end_date = datetime(2099, 1, 31, tzinfo=timezone.utc)
|
|
user = _make_user()
|
|
|
|
with (
|
|
patch(
|
|
"bot.services.subscription_service_impl.devices.user_dal.get_user_by_id",
|
|
AsyncMock(return_value=user),
|
|
),
|
|
patch(
|
|
"bot.services.subscription_service_impl.devices.subscription_dal.get_active_subscription_by_user_id",
|
|
AsyncMock(return_value=sub),
|
|
),
|
|
patch(
|
|
"bot.services.subscription_service_impl.devices.tariff_dal.get_hwid_device_entitlement_summary",
|
|
AsyncMock(return_value={"active_devices": 0, "active_until": None}),
|
|
),
|
|
):
|
|
quote = await service.quote_hwid_device_topup(
|
|
session=AsyncMock(),
|
|
user_id=42,
|
|
device_count=1,
|
|
tariff_key="standard",
|
|
currency="rub",
|
|
now=datetime(2099, 1, 16, tzinfo=timezone.utc),
|
|
)
|
|
|
|
self.assertIsNotNone(quote)
|
|
self.assertEqual(quote["price"], 50)
|
|
self.assertAlmostEqual(quote["proration_ratio"], 0.5)
|
|
|
|
async def test_unlimited_subscriber_returns_noop_payload(self):
|
|
# hwid_device_limit == 0 means unlimited — top-up makes no sense and must skip.
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
settings = _make_settings(tmpdir, _tariffs_config_payload())
|
|
service = _make_service(settings)
|
|
sub = _make_sub(hwid_device_limit=0, extra_hwid_devices=0)
|
|
user = _make_user()
|
|
with (
|
|
patch(
|
|
"bot.services.subscription_service_impl.devices.user_dal.get_user_by_id",
|
|
AsyncMock(return_value=user),
|
|
),
|
|
patch(
|
|
"bot.services.subscription_service_impl.devices.subscription_dal.get_active_subscription_by_user_id",
|
|
AsyncMock(return_value=sub),
|
|
),
|
|
patch(
|
|
"bot.services.subscription_service_impl.devices.tariff_dal.create_hwid_device_purchase",
|
|
AsyncMock(),
|
|
) as create_purchase,
|
|
):
|
|
result = await service.activate_hwid_device_topup(
|
|
session=AsyncMock(),
|
|
user_id=42,
|
|
device_count=1,
|
|
payment_amount=50,
|
|
payment_db_id=1,
|
|
)
|
|
self.assertEqual(result["hwid_device_limit"], 0)
|
|
self.assertEqual(result["purchased_hwid_devices"], 0)
|
|
# Unlimited subscriber: no audit row, no panel touch.
|
|
create_purchase.assert_not_awaited()
|
|
|
|
async def test_panel_payload_uses_effective_limit_with_active_entitlements(self):
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
settings = _make_settings(tmpdir, _tariffs_config_payload())
|
|
service = _make_service(settings)
|
|
sub = _make_sub(hwid_device_limit=3, extra_hwid_devices=2)
|
|
user = _make_user()
|
|
updated_sub = SimpleNamespace(
|
|
subscription_id=11,
|
|
end_date=sub.end_date,
|
|
)
|
|
service.panel_service.update_user_details_on_panel = AsyncMock(
|
|
return_value={"ok": True}
|
|
)
|
|
with (
|
|
patch(
|
|
"bot.services.subscription_service_impl.devices.user_dal.get_user_by_id",
|
|
AsyncMock(return_value=user),
|
|
),
|
|
patch(
|
|
"bot.services.subscription_service_impl.devices.subscription_dal.get_active_subscription_by_user_id",
|
|
AsyncMock(return_value=sub),
|
|
),
|
|
patch(
|
|
"bot.services.subscription_service_impl.devices.subscription_dal.update_subscription",
|
|
AsyncMock(return_value=updated_sub),
|
|
),
|
|
patch(
|
|
"bot.services.subscription_service_impl.devices.tariff_dal.get_hwid_device_entitlement_summary",
|
|
AsyncMock(return_value={"active_devices": 2, "active_until": sub.end_date}),
|
|
),
|
|
patch(
|
|
"bot.services.subscription_service_impl.payments.payment_dal.get_payment_by_db_id",
|
|
AsyncMock(return_value=SimpleNamespace()),
|
|
),
|
|
patch(
|
|
"bot.services.subscription_service_impl.devices.tariff_dal.create_hwid_device_purchase",
|
|
AsyncMock(),
|
|
) as create_purchase,
|
|
):
|
|
result = await service.activate_hwid_device_topup(
|
|
session=AsyncMock(),
|
|
user_id=42,
|
|
device_count=1,
|
|
payment_amount=50,
|
|
payment_db_id=1,
|
|
)
|
|
|
|
# Caller-facing payload: base=3, new extras = 2 + 1 purchased = 3 → effective = 6.
|
|
self.assertEqual(result["hwid_device_limit"], 6)
|
|
self.assertEqual(result["extra_hwid_devices"], 3)
|
|
self.assertEqual(result["purchased_hwid_devices"], 1)
|
|
create_purchase.assert_awaited_once()
|
|
purchase_kwargs = create_purchase.await_args.kwargs
|
|
self.assertEqual(purchase_kwargs["valid_until"], sub.end_date)
|
|
self.assertLess(purchase_kwargs["valid_from"], sub.end_date)
|
|
# Panel must see the full effective limit, not just the base.
|
|
panel_payload = service.panel_service.update_user_details_on_panel.await_args.args[1]
|
|
self.assertEqual(panel_payload["hwidDeviceLimit"], 6)
|
|
|
|
async def test_activation_uses_frozen_payment_validity_window(self):
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
settings = _make_settings(tmpdir, _tariffs_config_payload())
|
|
service = _make_service(settings)
|
|
sub = _make_sub(hwid_device_limit=3, extra_hwid_devices=0)
|
|
sub.end_date = datetime(2099, 3, 1, tzinfo=timezone.utc)
|
|
user = _make_user()
|
|
frozen_until = datetime(2099, 2, 1, tzinfo=timezone.utc)
|
|
payment = SimpleNamespace(
|
|
hwid_valid_from=datetime(2099, 1, 1, tzinfo=timezone.utc),
|
|
hwid_valid_until=frozen_until,
|
|
hwid_pricing_period_months=1,
|
|
hwid_proration_ratio=1.0,
|
|
hwid_full_price=50,
|
|
)
|
|
updated_sub = SimpleNamespace(subscription_id=11, end_date=sub.end_date)
|
|
service.panel_service.update_user_details_on_panel = AsyncMock(
|
|
return_value={"ok": True}
|
|
)
|
|
with (
|
|
patch(
|
|
"bot.services.subscription_service_impl.devices.user_dal.get_user_by_id",
|
|
AsyncMock(return_value=user),
|
|
),
|
|
patch(
|
|
"bot.services.subscription_service_impl.devices.subscription_dal.get_active_subscription_by_user_id",
|
|
AsyncMock(return_value=sub),
|
|
),
|
|
patch(
|
|
"bot.services.subscription_service_impl.devices.subscription_dal.update_subscription",
|
|
AsyncMock(return_value=updated_sub),
|
|
),
|
|
patch(
|
|
"bot.services.subscription_service_impl.devices.tariff_dal.get_hwid_device_entitlement_summary",
|
|
AsyncMock(return_value={"active_devices": 0, "active_until": None}),
|
|
),
|
|
patch(
|
|
"bot.services.subscription_service_impl.payments.payment_dal.get_payment_by_db_id",
|
|
AsyncMock(return_value=payment),
|
|
),
|
|
patch(
|
|
"bot.services.subscription_service_impl.devices.tariff_dal.create_hwid_device_purchase",
|
|
AsyncMock(),
|
|
) as create_purchase,
|
|
):
|
|
result = await service.activate_hwid_device_topup(
|
|
session=AsyncMock(),
|
|
user_id=42,
|
|
device_count=1,
|
|
payment_amount=50,
|
|
payment_db_id=1,
|
|
)
|
|
|
|
self.assertEqual(result["hwid_devices_valid_until"], frozen_until)
|
|
purchase_kwargs = create_purchase.await_args.kwargs
|
|
self.assertEqual(purchase_kwargs["valid_until"], frozen_until)
|
|
|
|
async def test_renewal_topup_starts_after_existing_entitlement(self):
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
settings = _make_settings(tmpdir, _tariffs_config_payload())
|
|
service = _make_service(settings)
|
|
current_entitlement_end = datetime(2099, 1, 1, tzinfo=timezone.utc)
|
|
renewed_end = datetime(2099, 2, 1, tzinfo=timezone.utc)
|
|
sub = _make_sub(hwid_device_limit=3, extra_hwid_devices=2)
|
|
sub.end_date = renewed_end
|
|
user = _make_user()
|
|
updated_sub = SimpleNamespace(subscription_id=11, end_date=renewed_end)
|
|
service.panel_service.update_user_details_on_panel = AsyncMock(
|
|
return_value={"ok": True}
|
|
)
|
|
with (
|
|
patch(
|
|
"bot.services.subscription_service_impl.devices.user_dal.get_user_by_id",
|
|
AsyncMock(return_value=user),
|
|
),
|
|
patch(
|
|
"bot.services.subscription_service_impl.devices.subscription_dal.get_active_subscription_by_user_id",
|
|
AsyncMock(return_value=sub),
|
|
),
|
|
patch(
|
|
"bot.services.subscription_service_impl.devices.subscription_dal.update_subscription",
|
|
AsyncMock(return_value=updated_sub),
|
|
),
|
|
patch(
|
|
"bot.services.subscription_service_impl.devices.tariff_dal.get_hwid_device_entitlement_summary",
|
|
AsyncMock(
|
|
return_value={
|
|
"active_devices": 2,
|
|
"active_until": current_entitlement_end,
|
|
}
|
|
),
|
|
),
|
|
patch(
|
|
"bot.services.subscription_service_impl.payments.payment_dal.get_payment_by_db_id",
|
|
AsyncMock(return_value=SimpleNamespace()),
|
|
),
|
|
patch(
|
|
"bot.services.subscription_service_impl.devices.tariff_dal.create_hwid_device_purchase",
|
|
AsyncMock(),
|
|
) as create_purchase,
|
|
):
|
|
result = await service.activate_hwid_device_topup(
|
|
session=AsyncMock(),
|
|
user_id=42,
|
|
device_count=1,
|
|
payment_amount=50,
|
|
payment_db_id=1,
|
|
renewal=True,
|
|
)
|
|
|
|
self.assertEqual(result["extra_hwid_devices"], 2)
|
|
self.assertEqual(result["hwid_device_limit"], 5)
|
|
purchase_kwargs = create_purchase.await_args.kwargs
|
|
self.assertEqual(purchase_kwargs["valid_from"], current_entitlement_end)
|
|
self.assertEqual(purchase_kwargs["valid_until"], renewed_end)
|
|
panel_payload = service.panel_service.update_user_details_on_panel.await_args.args[1]
|
|
self.assertEqual(panel_payload["hwidDeviceLimit"], 5)
|
|
|
|
async def test_panel_failure_returns_none_and_skips_audit(self):
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
settings = _make_settings(tmpdir, _tariffs_config_payload())
|
|
service = _make_service(settings)
|
|
sub = _make_sub()
|
|
user = _make_user()
|
|
updated_sub = SimpleNamespace(subscription_id=11, end_date=sub.end_date)
|
|
service.panel_service.update_user_details_on_panel = AsyncMock(return_value=None)
|
|
with (
|
|
patch(
|
|
"bot.services.subscription_service_impl.devices.user_dal.get_user_by_id",
|
|
AsyncMock(return_value=user),
|
|
),
|
|
patch(
|
|
"bot.services.subscription_service_impl.devices.subscription_dal.get_active_subscription_by_user_id",
|
|
AsyncMock(return_value=sub),
|
|
),
|
|
patch(
|
|
"bot.services.subscription_service_impl.devices.subscription_dal.update_subscription",
|
|
AsyncMock(return_value=updated_sub),
|
|
),
|
|
patch(
|
|
"bot.services.subscription_service_impl.devices.tariff_dal.get_hwid_device_entitlement_summary",
|
|
AsyncMock(return_value={"active_devices": 0, "active_until": None}),
|
|
),
|
|
patch(
|
|
"bot.services.subscription_service_impl.payments.payment_dal.get_payment_by_db_id",
|
|
AsyncMock(return_value=SimpleNamespace()),
|
|
),
|
|
patch(
|
|
"bot.services.subscription_service_impl.devices.tariff_dal.create_hwid_device_purchase",
|
|
AsyncMock(),
|
|
) as create_purchase,
|
|
):
|
|
result = await service.activate_hwid_device_topup(
|
|
session=AsyncMock(),
|
|
user_id=42,
|
|
device_count=1,
|
|
payment_amount=50,
|
|
payment_db_id=1,
|
|
)
|
|
self.assertIsNone(result)
|
|
create_purchase.assert_not_awaited()
|
|
|
|
|
|
if __name__ == "__main__": # pragma: no cover
|
|
unittest.main()
|