refactor: improve startup and sync performance
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import bot.app.web.subscription_webapp # noqa: F401
|
||||
from bot.app.web.admin_api_impl import stats as stats_module
|
||||
|
||||
|
||||
class AdminPanelStatsCacheTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def asyncSetUp(self):
|
||||
stats_module._ADMIN_PANEL_STATS_CACHES.clear()
|
||||
|
||||
async def asyncTearDown(self):
|
||||
stats_module._ADMIN_PANEL_STATS_CACHES.clear()
|
||||
|
||||
def _settings(self):
|
||||
return SimpleNamespace(
|
||||
ADMIN_PANEL_STATS_CACHE_TTL_SECONDS=15,
|
||||
REDIS_URL="redis://redis:6379/0",
|
||||
REDIS_KEY_PREFIX="shop",
|
||||
)
|
||||
|
||||
def _panel_service(self):
|
||||
return SimpleNamespace(
|
||||
get_system_stats=AsyncMock(return_value={"users": {"totalUsers": 10}}),
|
||||
get_bandwidth_stats=AsyncMock(return_value={"current": 123}),
|
||||
get_nodes_statistics=AsyncMock(return_value={"nodes": []}),
|
||||
get_nodes_bandwidth_usage=AsyncMock(return_value={"topNodes": []}),
|
||||
get_nodes_online_lookups=AsyncMock(return_value={"byUuid": {}, "byName": {}}),
|
||||
)
|
||||
|
||||
async def test_admin_panel_stats_are_cached_between_requests(self):
|
||||
settings = self._settings()
|
||||
panel_service = self._panel_service()
|
||||
cache_store = {}
|
||||
|
||||
async def fake_get(_settings, key):
|
||||
return cache_store.get(key)
|
||||
|
||||
async def fake_set(_settings, key, value, ttl):
|
||||
cache_store[key] = value
|
||||
|
||||
with (
|
||||
patch("bot.infra.redis.cache_get_json", fake_get),
|
||||
patch("bot.infra.redis.cache_set_json", fake_set),
|
||||
):
|
||||
first = await stats_module._load_admin_panel_stats(None, settings, panel_service)
|
||||
second = await stats_module._load_admin_panel_stats(None, settings, panel_service)
|
||||
|
||||
self.assertEqual(first, second)
|
||||
panel_service.get_system_stats.assert_awaited_once()
|
||||
panel_service.get_bandwidth_stats.assert_awaited_once()
|
||||
panel_service.get_nodes_statistics.assert_awaited_once()
|
||||
panel_service.get_nodes_bandwidth_usage.assert_awaited_once()
|
||||
panel_service.get_nodes_online_lookups.assert_awaited_once()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,71 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from bot.handlers.admin.sync_admin import (
|
||||
_coerce_panel_telegram_id,
|
||||
_description_matches,
|
||||
_subscription_update_delta,
|
||||
)
|
||||
from db.models import Subscription
|
||||
|
||||
|
||||
def test_description_match_ignores_whitespace_shape():
|
||||
assert _description_matches("email@example.com username", "email@example.com\nusername")
|
||||
|
||||
|
||||
def test_panel_telegram_id_is_coerced_to_int():
|
||||
assert _coerce_panel_telegram_id("12345") == 12345
|
||||
assert _coerce_panel_telegram_id("") is None
|
||||
|
||||
|
||||
def test_subscription_update_delta_skips_unchanged_fields():
|
||||
end_date = datetime(2026, 5, 20, 12, 0, tzinfo=timezone.utc)
|
||||
subscription = Subscription(
|
||||
user_id=1,
|
||||
panel_user_uuid="panel-1",
|
||||
panel_subscription_uuid="sub-1",
|
||||
end_date=end_date,
|
||||
is_active=True,
|
||||
status_from_panel="ACTIVE",
|
||||
)
|
||||
|
||||
assert (
|
||||
_subscription_update_delta(
|
||||
subscription,
|
||||
{
|
||||
"user_id": 1,
|
||||
"panel_user_uuid": "panel-1",
|
||||
"end_date": end_date + timedelta(milliseconds=500),
|
||||
"is_active": True,
|
||||
"status_from_panel": "ACTIVE",
|
||||
},
|
||||
)
|
||||
== {}
|
||||
)
|
||||
|
||||
|
||||
def test_subscription_update_delta_returns_only_changed_fields():
|
||||
end_date = datetime(2026, 5, 20, 12, 0, tzinfo=timezone.utc)
|
||||
subscription = Subscription(
|
||||
user_id=1,
|
||||
panel_user_uuid="panel-1",
|
||||
panel_subscription_uuid="sub-1",
|
||||
end_date=end_date,
|
||||
is_active=True,
|
||||
status_from_panel="ACTIVE",
|
||||
)
|
||||
|
||||
assert _subscription_update_delta(
|
||||
subscription,
|
||||
{
|
||||
"user_id": 2,
|
||||
"panel_user_uuid": "panel-1",
|
||||
"end_date": end_date + timedelta(seconds=2),
|
||||
"is_active": False,
|
||||
"status_from_panel": "EXPIRED",
|
||||
},
|
||||
) == {
|
||||
"user_id": 2,
|
||||
"end_date": end_date + timedelta(seconds=2),
|
||||
"is_active": False,
|
||||
"status_from_panel": "EXPIRED",
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, patch
|
||||
@@ -46,6 +47,65 @@ class PanelApiServiceLoggingTests(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
self.assertTrue(service._request.await_args.kwargs["log_full_response"])
|
||||
|
||||
async def test_get_user_by_uuid_uses_short_ttl_cache_and_update_invalidates(self):
|
||||
service = self._make_service()
|
||||
service._request = AsyncMock(return_value={"response": {"uuid": "user-uuid"}})
|
||||
|
||||
first = await service.get_user_by_uuid("user-uuid")
|
||||
second = await service.get_user_by_uuid("user-uuid")
|
||||
|
||||
self.assertEqual(first, {"uuid": "user-uuid"})
|
||||
self.assertEqual(second, {"uuid": "user-uuid"})
|
||||
self.assertEqual(service._request.await_count, 1)
|
||||
|
||||
await service.update_user_details_on_panel("user-uuid", {"description": "updated"})
|
||||
await service.get_user_by_uuid("user-uuid")
|
||||
|
||||
self.assertEqual(service._request.await_count, 3)
|
||||
|
||||
async def test_get_user_devices_uses_short_ttl_cache_and_disconnect_invalidates(self):
|
||||
service = self._make_service()
|
||||
service._request = AsyncMock(return_value={"response": [{"hwid": "device-1"}]})
|
||||
|
||||
first = await service.get_user_devices("user-uuid")
|
||||
second = await service.get_user_devices("user-uuid")
|
||||
|
||||
self.assertEqual(first, [{"hwid": "device-1"}])
|
||||
self.assertEqual(second, [{"hwid": "device-1"}])
|
||||
self.assertEqual(service._request.await_count, 1)
|
||||
|
||||
await service.disconnect_device("user-uuid", "device-1")
|
||||
await service.get_user_devices("user-uuid")
|
||||
|
||||
self.assertEqual(service._request.await_count, 3)
|
||||
|
||||
async def test_get_all_panel_users_uses_singleflight_cache_and_update_invalidates(self):
|
||||
service = self._make_service()
|
||||
get_calls = 0
|
||||
|
||||
async def fake_request(method, endpoint, **kwargs):
|
||||
nonlocal get_calls
|
||||
if method == "GET":
|
||||
get_calls += 1
|
||||
return {"response": {"users": [{"uuid": "user-uuid"}]}}
|
||||
return {"response": {"uuid": "user-uuid"}}
|
||||
|
||||
service._request = AsyncMock(side_effect=fake_request)
|
||||
|
||||
first, second = await asyncio.gather(
|
||||
service.get_all_panel_users(),
|
||||
service.get_all_panel_users(),
|
||||
)
|
||||
|
||||
self.assertEqual(first, [{"uuid": "user-uuid"}])
|
||||
self.assertEqual(second, [{"uuid": "user-uuid"}])
|
||||
self.assertEqual(get_calls, 1)
|
||||
|
||||
await service.update_user_details_on_panel("user-uuid", {"description": "updated"})
|
||||
await service.get_all_panel_users()
|
||||
|
||||
self.assertEqual(get_calls, 2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from bot.middlewares import profile_sync as profile_sync_module
|
||||
from bot.middlewares.profile_sync import ProfileSyncMiddleware
|
||||
|
||||
|
||||
class ProfileSyncMiddlewareCacheTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def asyncSetUp(self):
|
||||
profile_sync_module._LOCAL_PROFILE_SYNC_CHECKS.clear()
|
||||
|
||||
async def asyncTearDown(self):
|
||||
profile_sync_module._LOCAL_PROFILE_SYNC_CHECKS.clear()
|
||||
|
||||
def _settings(self):
|
||||
return SimpleNamespace(
|
||||
PROFILE_SYNC_CACHE_TTL_SECONDS=900,
|
||||
REDIS_URL="redis://redis:6379/0",
|
||||
REDIS_KEY_PREFIX="shop",
|
||||
)
|
||||
|
||||
async def test_profile_sync_skips_repeated_user_checks_inside_ttl(self):
|
||||
middleware = ProfileSyncMiddleware()
|
||||
handler = AsyncMock(return_value="ok")
|
||||
event = SimpleNamespace()
|
||||
tg_user = SimpleNamespace(
|
||||
id=42,
|
||||
username="alice",
|
||||
first_name="Alice",
|
||||
last_name="Smith",
|
||||
)
|
||||
db_user = SimpleNamespace(
|
||||
user_id=42,
|
||||
telegram_id=42,
|
||||
username="alice",
|
||||
first_name="Alice",
|
||||
last_name="Smith",
|
||||
email=None,
|
||||
panel_user_uuid=None,
|
||||
)
|
||||
cache_store = {}
|
||||
|
||||
async def fake_get(_settings, key):
|
||||
return cache_store.get(key)
|
||||
|
||||
async def fake_set(_settings, key, value, ttl):
|
||||
cache_store[key] = value
|
||||
|
||||
data = {
|
||||
"session": AsyncMock(),
|
||||
"event_from_user": tg_user,
|
||||
"settings": self._settings(),
|
||||
}
|
||||
with (
|
||||
patch.object(profile_sync_module, "cache_get_json", fake_get),
|
||||
patch.object(profile_sync_module, "cache_set_json", fake_set),
|
||||
patch.object(
|
||||
profile_sync_module.user_dal,
|
||||
"get_user_by_telegram_id",
|
||||
AsyncMock(return_value=db_user),
|
||||
) as get_user,
|
||||
):
|
||||
first = await middleware(handler, event, data)
|
||||
second = await middleware(handler, event, data)
|
||||
|
||||
self.assertEqual(first, "ok")
|
||||
self.assertEqual(second, "ok")
|
||||
get_user.assert_awaited_once()
|
||||
self.assertEqual(handler.await_count, 2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user