feat: tune visual of web app admin panel, tune limit messages and etc
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
"""Unit tests for admin-driven traffic grants on SubscriptionService.
|
||||
|
||||
These tests cover the new ``admin_grant_topup`` and ``admin_grant_premium_topup``
|
||||
methods that let admins credit GB to a user as if they had purchased a top-up.
|
||||
The DAL and panel-service interactions are mocked so the tests stay hermetic.
|
||||
"""
|
||||
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
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(premium: bool = False) -> dict:
|
||||
tariff = {
|
||||
"key": "standard",
|
||||
"names": {"ru": "Стандарт"},
|
||||
"descriptions": {"ru": "Base"},
|
||||
"squad_uuids": ["squad-1"],
|
||||
"billing_model": "period",
|
||||
"monthly_gb": 100,
|
||||
"prices_rub": {"1": 150},
|
||||
"prices_stars": {"1": 0},
|
||||
"enabled_periods": [1],
|
||||
"enabled": True,
|
||||
}
|
||||
if premium:
|
||||
tariff["premium_squad_uuids"] = ["premium-squad"]
|
||||
tariff["premium_monthly_gb"] = 25
|
||||
return {"default_tariff": "standard", "tariffs": [tariff]}
|
||||
|
||||
|
||||
def _make_settings(payload: dict, tmpdir: str) -> Settings:
|
||||
config_path = Path(tmpdir) / "tariffs.json"
|
||||
config_path.write_text(json.dumps(payload), encoding="utf-8")
|
||||
return Settings(
|
||||
_env_file=None,
|
||||
BOT_TOKEN="token",
|
||||
POSTGRES_USER="app_user",
|
||||
POSTGRES_PASSWORD="app_password",
|
||||
TARIFFS_CONFIG_PATH=str(config_path),
|
||||
)
|
||||
|
||||
|
||||
class AdminGrantTopupTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_regular_grant_increases_balance_and_panel_limit(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
settings = _make_settings(_tariffs_config_payload(), tmpdir)
|
||||
panel_service = AsyncMock(spec=PanelApiService)
|
||||
panel_service.update_user_details_on_panel = AsyncMock(return_value={"response": {}})
|
||||
service = SubscriptionService(settings, panel_service)
|
||||
|
||||
db_user = SimpleNamespace(
|
||||
user_id=42,
|
||||
first_name="Tester",
|
||||
last_name=None,
|
||||
username="tester",
|
||||
language_code="ru",
|
||||
panel_user_uuid="panel-uuid",
|
||||
email=None,
|
||||
telegram_id=42,
|
||||
)
|
||||
sub = SimpleNamespace(
|
||||
subscription_id=7,
|
||||
user_id=42,
|
||||
panel_user_uuid="panel-uuid",
|
||||
end_date=datetime.now(timezone.utc) + timedelta(days=10),
|
||||
tariff_key="standard",
|
||||
tier_baseline_bytes=100 * (1024**3),
|
||||
topup_balance_bytes=5 * (1024**3),
|
||||
traffic_limit_bytes=105 * (1024**3),
|
||||
is_throttled=True,
|
||||
hwid_device_limit=3,
|
||||
extra_hwid_devices=0,
|
||||
premium_is_limited=False,
|
||||
)
|
||||
updated_sub = SimpleNamespace(**vars(sub))
|
||||
updated_sub.topup_balance_bytes = 55 * (1024**3)
|
||||
updated_sub.traffic_limit_bytes = 155 * (1024**3)
|
||||
updated_sub.is_throttled = False
|
||||
|
||||
with patch("bot.services.subscription_service.user_dal.get_user_by_id", new=AsyncMock(return_value=db_user)), \
|
||||
patch("bot.services.subscription_service.subscription_dal.get_active_subscription_by_user_id", new=AsyncMock(return_value=sub)), \
|
||||
patch("bot.services.subscription_service.subscription_dal.update_subscription", new=AsyncMock(return_value=updated_sub)) as upd, \
|
||||
patch("bot.services.subscription_service.tariff_dal.create_traffic_topup", new=AsyncMock()) as topup_log:
|
||||
result = await service.admin_grant_topup(AsyncMock(), 42, 50.0)
|
||||
|
||||
self.assertIsNotNone(result)
|
||||
self.assertEqual(result["topup_balance_bytes"], 55 * (1024**3))
|
||||
self.assertEqual(result["traffic_limit_bytes"], 155 * (1024**3))
|
||||
self.assertEqual(result["granted_bytes"], 50 * (1024**3))
|
||||
|
||||
upd.assert_awaited_once()
|
||||
sub_update_payload = upd.await_args.args[2]
|
||||
self.assertEqual(sub_update_payload["topup_balance_bytes"], 55 * (1024**3))
|
||||
self.assertEqual(sub_update_payload["traffic_limit_bytes"], 155 * (1024**3))
|
||||
self.assertFalse(sub_update_payload["is_throttled"])
|
||||
|
||||
topup_log.assert_awaited_once()
|
||||
self.assertEqual(topup_log.await_args.kwargs["kind"], "admin_topup")
|
||||
self.assertIsNone(topup_log.await_args.kwargs["payment_id"])
|
||||
self.assertEqual(topup_log.await_args.kwargs["purchased_bytes"], 50 * (1024**3))
|
||||
|
||||
panel_service.update_user_details_on_panel.assert_awaited_once()
|
||||
panel_payload = panel_service.update_user_details_on_panel.await_args.args[1]
|
||||
self.assertEqual(panel_payload["trafficLimitBytes"], 155 * (1024**3))
|
||||
|
||||
async def test_regular_grant_rejects_zero(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
settings = _make_settings(_tariffs_config_payload(), tmpdir)
|
||||
service = SubscriptionService(settings, AsyncMock(spec=PanelApiService))
|
||||
self.assertIsNone(await service.admin_grant_topup(AsyncMock(), 1, 0))
|
||||
self.assertIsNone(await service.admin_grant_topup(AsyncMock(), 1, -10))
|
||||
|
||||
async def test_premium_grant_clears_limited_state_when_balance_covers_overuse(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
settings = _make_settings(_tariffs_config_payload(premium=True), tmpdir)
|
||||
panel_service = AsyncMock(spec=PanelApiService)
|
||||
panel_service.update_user_details_on_panel = AsyncMock(return_value={"response": {}})
|
||||
service = SubscriptionService(settings, panel_service)
|
||||
|
||||
db_user = SimpleNamespace(
|
||||
user_id=77,
|
||||
first_name="Premium",
|
||||
last_name=None,
|
||||
username="premium",
|
||||
language_code="ru",
|
||||
panel_user_uuid="panel-uuid",
|
||||
email=None,
|
||||
telegram_id=77,
|
||||
)
|
||||
# User has used 30 GB on a 25 GB premium baseline → currently
|
||||
# premium_is_limited=True. Admin grants 20 GB of premium top-up.
|
||||
sub = SimpleNamespace(
|
||||
subscription_id=9,
|
||||
user_id=77,
|
||||
panel_user_uuid="panel-uuid",
|
||||
tariff_key="standard",
|
||||
premium_baseline_bytes=25 * (1024**3),
|
||||
premium_topup_balance_bytes=0,
|
||||
premium_topup_used_bytes=0,
|
||||
premium_used_bytes=30 * (1024**3),
|
||||
premium_is_limited=True,
|
||||
premium_period_start_at=datetime.now(timezone.utc).replace(
|
||||
day=1, hour=0, minute=0, second=0, microsecond=0
|
||||
),
|
||||
premium_unlimited_override=False,
|
||||
premium_bonus_bytes=0,
|
||||
)
|
||||
updated_sub = SimpleNamespace(**vars(sub))
|
||||
|
||||
with patch("bot.services.subscription_service.user_dal.get_user_by_id", new=AsyncMock(return_value=db_user)), \
|
||||
patch("bot.services.subscription_service.subscription_dal.get_active_subscription_by_user_id", new=AsyncMock(return_value=sub)), \
|
||||
patch("bot.services.subscription_service.subscription_dal.update_subscription", new=AsyncMock(return_value=updated_sub)) as upd, \
|
||||
patch("bot.services.subscription_service.tariff_dal.create_traffic_topup", new=AsyncMock()) as topup_log:
|
||||
result = await service.admin_grant_premium_topup(AsyncMock(), 77, 20.0)
|
||||
|
||||
self.assertIsNotNone(result)
|
||||
self.assertFalse(result["premium_is_limited"])
|
||||
# 5 GB of overuse should be backfilled into premium_topup_used.
|
||||
self.assertEqual(result["premium_topup_used_bytes"], 5 * (1024**3))
|
||||
# Remaining balance after backfill: 15 GB.
|
||||
self.assertEqual(result["premium_topup_balance_bytes"], 15 * (1024**3))
|
||||
self.assertEqual(result["granted_bytes"], 20 * (1024**3))
|
||||
|
||||
topup_log.assert_awaited_once()
|
||||
self.assertEqual(topup_log.await_args.kwargs["kind"], "admin_premium_topup")
|
||||
self.assertIsNone(topup_log.await_args.kwargs["payment_id"])
|
||||
|
||||
panel_service.update_user_details_on_panel.assert_awaited_once()
|
||||
panel_payload = panel_service.update_user_details_on_panel.await_args.args[1]
|
||||
self.assertIn("premium-squad", panel_payload["activeInternalSquads"])
|
||||
|
||||
sub_update_payload = upd.await_args.args[2]
|
||||
self.assertFalse(sub_update_payload["premium_is_limited"])
|
||||
|
||||
async def test_premium_grant_fails_when_tariff_has_no_premium_squads(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
settings = _make_settings(_tariffs_config_payload(premium=False), tmpdir)
|
||||
service = SubscriptionService(settings, AsyncMock(spec=PanelApiService))
|
||||
|
||||
db_user = SimpleNamespace(
|
||||
user_id=11, panel_user_uuid="panel-uuid",
|
||||
)
|
||||
sub = SimpleNamespace(subscription_id=1, user_id=11, panel_user_uuid="panel-uuid", tariff_key="standard")
|
||||
|
||||
with patch("bot.services.subscription_service.user_dal.get_user_by_id", new=AsyncMock(return_value=db_user)), \
|
||||
patch("bot.services.subscription_service.subscription_dal.get_active_subscription_by_user_id", new=AsyncMock(return_value=sub)):
|
||||
self.assertIsNone(await service.admin_grant_premium_topup(AsyncMock(), 11, 10.0))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,35 @@
|
||||
import unittest
|
||||
|
||||
from bot.utils.mini_app_url import append_query_params, subscription_mini_app_topup_url
|
||||
from config.settings import Settings
|
||||
|
||||
|
||||
class MiniAppUrlTests(unittest.TestCase):
|
||||
def test_append_query_params_adds_topup(self):
|
||||
self.assertEqual(
|
||||
append_query_params("https://app.example.com/home", {"topup": "regular"}),
|
||||
"https://app.example.com/home?topup=regular",
|
||||
)
|
||||
|
||||
def test_append_query_params_merges_existing(self):
|
||||
self.assertEqual(
|
||||
append_query_params("https://app.example.com/?lang=ru", {"topup": "premium"}),
|
||||
"https://app.example.com/?lang=ru&topup=premium",
|
||||
)
|
||||
|
||||
def test_subscription_mini_app_topup_url_none_when_unset(self):
|
||||
s = Settings(_env_file=None, BOT_TOKEN="x", POSTGRES_USER="u", POSTGRES_PASSWORD="p", SUBSCRIPTION_MINI_APP_URL=None)
|
||||
self.assertIsNone(subscription_mini_app_topup_url(s, "regular"))
|
||||
|
||||
def test_subscription_mini_app_topup_url(self):
|
||||
s = Settings(
|
||||
_env_file=None,
|
||||
BOT_TOKEN="x",
|
||||
POSTGRES_USER="u",
|
||||
POSTGRES_PASSWORD="p",
|
||||
SUBSCRIPTION_MINI_APP_URL="https://app.example.com/webapp",
|
||||
)
|
||||
self.assertEqual(
|
||||
subscription_mini_app_topup_url(s, "regular"),
|
||||
"https://app.example.com/webapp?topup=regular",
|
||||
)
|
||||
@@ -265,3 +265,121 @@ class TariffWorkerTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertEqual(sub.premium_topup_balance_bytes, int(1.5 * (1024**3)))
|
||||
self.assertEqual(sub.premium_topup_used_bytes, 0)
|
||||
self.assertEqual(sub.premium_period_start_at, datetime(2026, 6, 1, tzinfo=timezone.utc))
|
||||
|
||||
async def test_premium_unlimited_override_never_throttles(self):
|
||||
payload = _tariffs_config_payload()
|
||||
payload["tariffs"][0]["premium_squad_uuids"] = ["premium-squad"]
|
||||
payload["tariffs"][0]["premium_monthly_gb"] = 1
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
config_path = Path(tmpdir) / "tariffs.json"
|
||||
config_path.write_text(json.dumps(payload), encoding="utf-8")
|
||||
|
||||
settings = Settings(
|
||||
_env_file=None,
|
||||
BOT_TOKEN="token",
|
||||
POSTGRES_USER="app_user",
|
||||
POSTGRES_PASSWORD="app_password",
|
||||
TARIFFS_CONFIG_PATH=str(config_path),
|
||||
TARIFF_TRAFFIC_WARNING_LEVELS="101",
|
||||
)
|
||||
panel_service = AsyncMock(spec=PanelApiService)
|
||||
panel_service.get_internal_squad_accessible_nodes = AsyncMock(
|
||||
return_value=[{"uuid": "node-1"}]
|
||||
)
|
||||
panel_service.get_node_users_bandwidth_stats = AsyncMock(
|
||||
return_value={
|
||||
"topUsers": [
|
||||
{"username": "tg_42", "total": 50 * (1024**3)},
|
||||
]
|
||||
}
|
||||
)
|
||||
panel_service.update_user_details_on_panel = AsyncMock(return_value={"response": {}})
|
||||
subscription_service = SubscriptionService(settings, panel_service)
|
||||
worker = TariffTrafficWorker(
|
||||
settings=settings,
|
||||
session_factory=SimpleNamespace(),
|
||||
panel_service=panel_service,
|
||||
subscription_service=subscription_service,
|
||||
)
|
||||
sub = SimpleNamespace(
|
||||
subscription_id=1,
|
||||
user_id=42,
|
||||
panel_user_uuid="panel-uuid",
|
||||
premium_baseline_bytes=1 * (1024**3),
|
||||
premium_topup_balance_bytes=0,
|
||||
premium_topup_used_bytes=0,
|
||||
premium_used_bytes=0,
|
||||
premium_is_limited=False,
|
||||
premium_period_start_at=None,
|
||||
premium_unlimited_override=True,
|
||||
premium_bonus_bytes=0,
|
||||
)
|
||||
tariff = settings.tariffs_config.require("standard")
|
||||
|
||||
await worker._sync_premium_squad_limit(
|
||||
AsyncMock(), sub, tariff, datetime.now(timezone.utc), panel_username="tg_42"
|
||||
)
|
||||
|
||||
self.assertFalse(sub.premium_is_limited)
|
||||
self.assertEqual(int(sub.premium_used_bytes), 50 * (1024**3))
|
||||
payload_sent = panel_service.update_user_details_on_panel.await_args.args[1]
|
||||
self.assertIn("premium-squad", payload_sent["activeInternalSquads"])
|
||||
|
||||
async def test_premium_bonus_extends_limit(self):
|
||||
payload = _tariffs_config_payload()
|
||||
payload["tariffs"][0]["premium_squad_uuids"] = ["premium-squad"]
|
||||
payload["tariffs"][0]["premium_monthly_gb"] = 1
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
config_path = Path(tmpdir) / "tariffs.json"
|
||||
config_path.write_text(json.dumps(payload), encoding="utf-8")
|
||||
|
||||
settings = Settings(
|
||||
_env_file=None,
|
||||
BOT_TOKEN="token",
|
||||
POSTGRES_USER="app_user",
|
||||
POSTGRES_PASSWORD="app_password",
|
||||
TARIFFS_CONFIG_PATH=str(config_path),
|
||||
TARIFF_TRAFFIC_WARNING_LEVELS="101",
|
||||
)
|
||||
panel_service = AsyncMock(spec=PanelApiService)
|
||||
panel_service.get_internal_squad_accessible_nodes = AsyncMock(
|
||||
return_value=[{"uuid": "node-1"}]
|
||||
)
|
||||
# Used 4 GB > tariff baseline 1 GB, but admin granted +10 GB bonus.
|
||||
panel_service.get_node_users_bandwidth_stats = AsyncMock(
|
||||
return_value={
|
||||
"topUsers": [
|
||||
{"username": "tg_77", "total": 4 * (1024**3)},
|
||||
]
|
||||
}
|
||||
)
|
||||
panel_service.update_user_details_on_panel = AsyncMock(return_value={"response": {}})
|
||||
subscription_service = SubscriptionService(settings, panel_service)
|
||||
worker = TariffTrafficWorker(
|
||||
settings=settings,
|
||||
session_factory=SimpleNamespace(),
|
||||
panel_service=panel_service,
|
||||
subscription_service=subscription_service,
|
||||
)
|
||||
sub = SimpleNamespace(
|
||||
subscription_id=1,
|
||||
user_id=77,
|
||||
panel_user_uuid="panel-uuid",
|
||||
premium_baseline_bytes=1 * (1024**3),
|
||||
premium_topup_balance_bytes=0,
|
||||
premium_topup_used_bytes=0,
|
||||
premium_used_bytes=0,
|
||||
premium_is_limited=False,
|
||||
premium_period_start_at=None,
|
||||
premium_unlimited_override=False,
|
||||
premium_bonus_bytes=10 * (1024**3),
|
||||
)
|
||||
tariff = settings.tariffs_config.require("standard")
|
||||
|
||||
await worker._sync_premium_squad_limit(
|
||||
AsyncMock(), sub, tariff, datetime.now(timezone.utc), panel_username="tg_77"
|
||||
)
|
||||
|
||||
# 4 GB used vs 1 GB baseline + 10 GB bonus = 11 GB limit → not limited.
|
||||
self.assertFalse(sub.premium_is_limited)
|
||||
self.assertEqual(int(sub.premium_used_bytes), 4 * (1024**3))
|
||||
|
||||
Reference in New Issue
Block a user