feat: add admin HWID device limit overrides
This commit is contained in:
@@ -50,6 +50,149 @@ def _make_settings(payload: dict, tmpdir: str) -> Settings:
|
||||
|
||||
|
||||
class AdminGrantTopupTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_hwid_limit_sync_pushes_effective_device_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",
|
||||
hwid_device_limit=4,
|
||||
extra_hwid_devices=0,
|
||||
)
|
||||
|
||||
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.tariff_dal.sum_active_hwid_devices",
|
||||
new=AsyncMock(return_value=2),
|
||||
),
|
||||
):
|
||||
effective_limit = await service.sync_hwid_device_limit_to_panel(AsyncMock(), 42)
|
||||
|
||||
self.assertEqual(effective_limit, 6)
|
||||
self.assertEqual(sub.extra_hwid_devices, 2)
|
||||
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["hwidDeviceLimit"], 6)
|
||||
|
||||
async def test_hwid_limit_sync_keeps_zero_unlimited(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",
|
||||
hwid_device_limit=0,
|
||||
extra_hwid_devices=0,
|
||||
)
|
||||
|
||||
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.tariff_dal.sum_active_hwid_devices",
|
||||
new=AsyncMock(return_value=3),
|
||||
),
|
||||
):
|
||||
effective_limit = await service.sync_hwid_device_limit_to_panel(AsyncMock(), 42)
|
||||
|
||||
self.assertEqual(effective_limit, 0)
|
||||
panel_payload = panel_service.update_user_details_on_panel.await_args.args[1]
|
||||
self.assertEqual(panel_payload["hwidDeviceLimit"], 0)
|
||||
|
||||
async def test_hwid_limit_sync_treats_missing_default_as_unlimited(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",
|
||||
hwid_device_limit=None,
|
||||
extra_hwid_devices=0,
|
||||
)
|
||||
|
||||
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.tariff_dal.sum_active_hwid_devices",
|
||||
new=AsyncMock(return_value=2),
|
||||
),
|
||||
):
|
||||
effective_limit = await service.sync_hwid_device_limit_to_panel(AsyncMock(), 42)
|
||||
|
||||
self.assertEqual(effective_limit, 0)
|
||||
panel_payload = panel_service.update_user_details_on_panel.await_args.args[1]
|
||||
self.assertEqual(panel_payload["hwidDeviceLimit"], 0)
|
||||
|
||||
async def test_regular_grant_increases_balance_and_panel_limit(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
settings = _make_settings(_tariffs_config_payload(), tmpdir)
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import json
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from bot.app.web.admin_api_impl import users as admin_users
|
||||
|
||||
|
||||
class FakeSession:
|
||||
def __init__(self):
|
||||
self.committed = False
|
||||
self.rolled_back = False
|
||||
self.refreshed = None
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
return False
|
||||
|
||||
async def commit(self):
|
||||
self.committed = True
|
||||
|
||||
async def rollback(self):
|
||||
self.rolled_back = True
|
||||
|
||||
async def refresh(self, obj):
|
||||
self.refreshed = obj
|
||||
|
||||
|
||||
class FakeRequest:
|
||||
def __init__(self, body, session, subscription_service):
|
||||
self.app = {
|
||||
"settings": SimpleNamespace(),
|
||||
"async_session_factory": lambda: session,
|
||||
"subscription_service": subscription_service,
|
||||
}
|
||||
self.match_info = {"user_id": "42"}
|
||||
self._body = body
|
||||
|
||||
async def json(self):
|
||||
return self._body
|
||||
|
||||
|
||||
class AdminUserHwidLimitRouteTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_unlimited_payload_stores_zero_and_syncs_panel(self):
|
||||
session = FakeSession()
|
||||
active = SimpleNamespace(hwid_device_limit=3)
|
||||
subscription_service = SimpleNamespace(
|
||||
sync_hwid_device_limit_to_panel=AsyncMock(return_value=0)
|
||||
)
|
||||
request = FakeRequest(
|
||||
{"unlimited": True, "hwid_device_limit": 999}, session, subscription_service
|
||||
)
|
||||
|
||||
with (
|
||||
patch.object(admin_users, "_require_admin_user_id", return_value=100),
|
||||
patch.object(
|
||||
admin_users.subscription_dal,
|
||||
"get_active_subscription_by_user_id",
|
||||
AsyncMock(return_value=active),
|
||||
),
|
||||
patch.object(admin_users.message_log_dal, "create_message_log", AsyncMock()),
|
||||
patch.object(admin_users, "_invalidate_after_admin_user_mutation", AsyncMock()),
|
||||
patch.object(
|
||||
admin_users,
|
||||
"_serialize_subscription",
|
||||
return_value={"hwid_device_limit": 0},
|
||||
),
|
||||
):
|
||||
response = await admin_users.admin_user_hwid_device_limit_route(request)
|
||||
|
||||
self.assertEqual(response.status, 200)
|
||||
self.assertEqual(json.loads(response.text)["subscription"]["hwid_device_limit"], 0)
|
||||
self.assertEqual(active.hwid_device_limit, 0)
|
||||
subscription_service.sync_hwid_device_limit_to_panel.assert_awaited_once_with(session, 42)
|
||||
self.assertTrue(session.committed)
|
||||
self.assertEqual(session.refreshed, active)
|
||||
|
||||
async def test_use_default_payload_stores_null_override(self):
|
||||
session = FakeSession()
|
||||
active = SimpleNamespace(hwid_device_limit=5)
|
||||
subscription_service = SimpleNamespace(
|
||||
sync_hwid_device_limit_to_panel=AsyncMock(return_value=3)
|
||||
)
|
||||
request = FakeRequest({"use_default": True}, session, subscription_service)
|
||||
|
||||
with (
|
||||
patch.object(admin_users, "_require_admin_user_id", return_value=100),
|
||||
patch.object(
|
||||
admin_users.subscription_dal,
|
||||
"get_active_subscription_by_user_id",
|
||||
AsyncMock(return_value=active),
|
||||
),
|
||||
patch.object(admin_users.message_log_dal, "create_message_log", AsyncMock()),
|
||||
patch.object(admin_users, "_invalidate_after_admin_user_mutation", AsyncMock()),
|
||||
patch.object(
|
||||
admin_users,
|
||||
"_serialize_subscription",
|
||||
return_value={"hwid_device_limit": None},
|
||||
),
|
||||
):
|
||||
response = await admin_users.admin_user_hwid_device_limit_route(request)
|
||||
|
||||
self.assertEqual(response.status, 200)
|
||||
self.assertIsNone(json.loads(response.text)["subscription"]["hwid_device_limit"])
|
||||
self.assertIsNone(active.hwid_device_limit)
|
||||
subscription_service.sync_hwid_device_limit_to_panel.assert_awaited_once_with(session, 42)
|
||||
|
||||
async def test_negative_limit_is_rejected(self):
|
||||
session = FakeSession()
|
||||
subscription_service = SimpleNamespace(
|
||||
sync_hwid_device_limit_to_panel=AsyncMock(return_value=None)
|
||||
)
|
||||
request = FakeRequest({"hwid_device_limit": -1}, session, subscription_service)
|
||||
|
||||
with patch.object(admin_users, "_require_admin_user_id", return_value=100):
|
||||
response = await admin_users.admin_user_hwid_device_limit_route(request)
|
||||
|
||||
self.assertEqual(response.status, 400)
|
||||
self.assertEqual(json.loads(response.text)["error"], "invalid_hwid_device_limit")
|
||||
subscription_service.sync_hwid_device_limit_to_panel.assert_not_awaited()
|
||||
|
||||
async def test_over_max_limit_is_rejected(self):
|
||||
session = FakeSession()
|
||||
subscription_service = SimpleNamespace(
|
||||
sync_hwid_device_limit_to_panel=AsyncMock(return_value=None)
|
||||
)
|
||||
request = FakeRequest({"hwid_device_limit": 1_000_001}, session, subscription_service)
|
||||
|
||||
with patch.object(admin_users, "_require_admin_user_id", return_value=100):
|
||||
response = await admin_users.admin_user_hwid_device_limit_route(request)
|
||||
|
||||
self.assertEqual(response.status, 400)
|
||||
self.assertEqual(json.loads(response.text)["error"], "invalid_hwid_device_limit")
|
||||
subscription_service.sync_hwid_device_limit_to_panel.assert_not_awaited()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -157,6 +157,16 @@ class CanTopupDevicesFlagTests(unittest.TestCase):
|
||||
payload = _serialize_subscription(settings, _active(max_devices=0), None, "en")
|
||||
self.assertFalse(payload["can_topup_devices"])
|
||||
|
||||
def test_flag_is_false_when_max_devices_is_missing(self):
|
||||
# Missing device limit is unlimited for Remnawave HWID limits.
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
settings = _make_settings(
|
||||
tmpdir,
|
||||
_tariffs_payload(hwid_rub=[{"count": 1, "price": 50}]),
|
||||
)
|
||||
payload = _serialize_subscription(settings, _active(max_devices=None), 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())
|
||||
|
||||
@@ -306,7 +306,9 @@ def test_admin_locale_keys_are_split_into_smaller_internal_groups():
|
||||
"inline_financial_description": "admin_dashboard",
|
||||
"inline_system_stats_message": "admin_dashboard",
|
||||
"admin_user_card_title": "admin_users",
|
||||
"admin_hwid_limit_title": "admin_users",
|
||||
"user_card_open_profile_button": "admin_users",
|
||||
"user_hwid_limit_card_title": "admin_users",
|
||||
"user_premium_override_card_title": "admin_users",
|
||||
"traffic_grant_regular_done": "admin_users",
|
||||
"admin_payment_detail_title": "admin_payments",
|
||||
|
||||
@@ -173,6 +173,10 @@ class WebAppRouteContractTests(unittest.TestCase):
|
||||
"POST",
|
||||
"/api/admin/users/{user_id}/regular-traffic-override",
|
||||
): "admin_user_regular_traffic_override_route",
|
||||
(
|
||||
"POST",
|
||||
"/api/admin/users/{user_id}/hwid-device-limit",
|
||||
): "admin_user_hwid_device_limit_route",
|
||||
("POST", "/api/admin/users/{user_id}/traffic-grant"): "admin_user_traffic_grant_route",
|
||||
("DELETE", "/api/admin/users/{user_id}"): "admin_user_delete_route",
|
||||
("GET", "/api/admin/payments"): "admin_payments_list_route",
|
||||
|
||||
Reference in New Issue
Block a user