refactor: split backend domains and add API behavior coverage
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from bot.services.panel_api_service import PanelApiService
|
||||
|
||||
|
||||
class PanelApiServiceLoggingTests(unittest.IsolatedAsyncioTestCase):
|
||||
def _make_service(self) -> PanelApiService:
|
||||
return PanelApiService(
|
||||
SimpleNamespace(
|
||||
PANEL_API_URL="https://panel.example.test/api",
|
||||
PANEL_API_KEY="panel-key",
|
||||
USER_HWID_DEVICE_LIMIT=None,
|
||||
)
|
||||
)
|
||||
|
||||
async def test_update_user_details_does_not_log_full_response_by_default(self):
|
||||
service = self._make_service()
|
||||
service._request = AsyncMock(return_value={"response": {"uuid": "user-uuid"}})
|
||||
|
||||
with patch("bot.services.panel_api_service.logging.info") as info_log:
|
||||
result = await service.update_user_details_on_panel(
|
||||
"user-uuid",
|
||||
{"description": "profile"},
|
||||
)
|
||||
|
||||
self.assertEqual(result, {"uuid": "user-uuid"})
|
||||
service._request.assert_awaited_once_with(
|
||||
"PATCH",
|
||||
"/users",
|
||||
json={"description": "profile", "uuid": "user-uuid"},
|
||||
log_full_response=False,
|
||||
)
|
||||
info_log.assert_not_called()
|
||||
|
||||
async def test_update_user_details_can_still_request_full_response_logging(self):
|
||||
service = self._make_service()
|
||||
service._request = AsyncMock(return_value={"response": {"uuid": "user-uuid"}})
|
||||
|
||||
await service.update_user_details_on_panel(
|
||||
"user-uuid",
|
||||
{"description": "profile"},
|
||||
log_response=True,
|
||||
)
|
||||
|
||||
self.assertTrue(service._request.await_args.kwargs["log_full_response"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -9,6 +9,7 @@ from urllib.parse import parse_qs, urlsplit
|
||||
from aiohttp import web
|
||||
|
||||
from bot.app.web import admin_api, subscription_webapp
|
||||
from bot.app.web.admin_api_impl import settings as admin_settings_routes
|
||||
from bot.app.web.webapp_auth import (
|
||||
create_telegram_oauth_nonce,
|
||||
create_webapp_session_token,
|
||||
@@ -405,7 +406,7 @@ class AdminSettingsSecurityTests(unittest.IsolatedAsyncioTestCase):
|
||||
request.get = lambda key, default=None: getattr(request, key, default)
|
||||
|
||||
with (
|
||||
patch.object(admin_api, "_require_admin_user_id", return_value=1),
|
||||
patch.object(admin_settings_routes, "_require_admin_user_id", return_value=1),
|
||||
patch.object(
|
||||
admin_api.app_settings_dal,
|
||||
"get_overrides_with_meta",
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from bot.services.panel_api_service import PanelApiService
|
||||
from bot.services.subscription_service import SubscriptionService
|
||||
from config.settings import Settings
|
||||
|
||||
GIB = 1024**3
|
||||
|
||||
|
||||
def _tariffs_config_payload() -> dict:
|
||||
return {
|
||||
"default_tariff": "standard",
|
||||
"tariffs": [
|
||||
{
|
||||
"key": "standard",
|
||||
"names": {"en": "Standard"},
|
||||
"descriptions": {"en": "Base period plan"},
|
||||
"squad_uuids": ["main-squad", "shared-squad"],
|
||||
"premium_squad_uuids": ["premium-squad", "shared-squad"],
|
||||
"premium_monthly_gb": 25,
|
||||
"billing_model": "period",
|
||||
"monthly_gb": 100,
|
||||
"prices_rub": {"1": 150},
|
||||
"prices_stars": {"1": 0},
|
||||
"enabled_periods": [1],
|
||||
"hwid_device_limit": 3,
|
||||
"enabled": True,
|
||||
},
|
||||
{
|
||||
"key": "traffic",
|
||||
"names": {"en": "Traffic"},
|
||||
"descriptions": {"en": "Traffic package"},
|
||||
"squad_uuids": ["traffic-squad"],
|
||||
"billing_model": "traffic",
|
||||
"monthly_gb": 0,
|
||||
"traffic_packages": {"rub": [{"gb": 50, "price": 400}], "stars": []},
|
||||
"enabled": True,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _make_settings(payload: dict, tmpdir: str, **overrides) -> Settings:
|
||||
config_path = Path(tmpdir) / "tariffs.json"
|
||||
config_path.write_text(json.dumps(payload), encoding="utf-8")
|
||||
values = {
|
||||
"_env_file": None,
|
||||
"BOT_TOKEN": "token",
|
||||
"POSTGRES_USER": "app_user",
|
||||
"POSTGRES_PASSWORD": "app_password",
|
||||
"TARIFFS_CONFIG_PATH": str(config_path),
|
||||
}
|
||||
values.update(overrides)
|
||||
return Settings(**values)
|
||||
|
||||
|
||||
def _make_service(settings: Settings) -> SubscriptionService:
|
||||
panel_service = AsyncMock(spec=PanelApiService)
|
||||
return SubscriptionService(settings, panel_service)
|
||||
|
||||
|
||||
class SubscriptionServiceCalculationTests(unittest.TestCase):
|
||||
def test_panel_squads_for_tariff_deduplicates_and_can_hide_premium(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
settings = _make_settings(_tariffs_config_payload(), tmpdir)
|
||||
service = _make_service(settings)
|
||||
tariff = settings.tariffs_config.require("standard")
|
||||
|
||||
self.assertEqual(
|
||||
service._panel_squads_for_tariff(tariff),
|
||||
["main-squad", "shared-squad", "premium-squad"],
|
||||
)
|
||||
self.assertEqual(
|
||||
service._panel_squads_for_tariff(tariff, include_premium=False),
|
||||
["main-squad", "shared-squad"],
|
||||
)
|
||||
|
||||
def test_panel_squads_falls_back_to_default_settings_without_tariff(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
settings = _make_settings(
|
||||
_tariffs_config_payload(),
|
||||
tmpdir,
|
||||
USER_SQUAD_UUIDS="fallback-a, fallback-b",
|
||||
)
|
||||
service = _make_service(settings)
|
||||
|
||||
self.assertEqual(
|
||||
service._panel_squads_for_tariff(None),
|
||||
["fallback-a", "fallback-b"],
|
||||
)
|
||||
|
||||
def test_main_traffic_limit_includes_topup_bonus_and_unlimited_floor(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
settings = _make_settings(_tariffs_config_payload(), tmpdir)
|
||||
service = _make_service(settings)
|
||||
|
||||
regular_limit = service._compute_main_traffic_limit_bytes(
|
||||
tier_baseline_bytes=100 * GIB,
|
||||
topup_balance_bytes=10 * GIB,
|
||||
regular_bonus_bytes=5 * GIB,
|
||||
regular_unlimited_override=False,
|
||||
traffic_used_bytes=500 * GIB,
|
||||
)
|
||||
self.assertEqual(regular_limit, 115 * GIB)
|
||||
|
||||
unlimited_limit = service._compute_main_traffic_limit_bytes(
|
||||
tier_baseline_bytes=100 * GIB,
|
||||
topup_balance_bytes=0,
|
||||
regular_bonus_bytes=0,
|
||||
regular_unlimited_override=True,
|
||||
traffic_used_bytes=2 * (1024**5),
|
||||
)
|
||||
self.assertEqual(unlimited_limit, 2 * (1024**5) + 512 * GIB)
|
||||
|
||||
def test_premium_effective_limit_ignores_negative_balances(self):
|
||||
self.assertEqual(
|
||||
SubscriptionService._premium_effective_limit_bytes(
|
||||
premium_baseline_bytes=25 * GIB,
|
||||
premium_topup_balance_bytes=-5 * GIB,
|
||||
premium_topup_used_bytes=3 * GIB,
|
||||
premium_bonus_bytes=-1 * GIB,
|
||||
),
|
||||
28 * GIB,
|
||||
)
|
||||
|
||||
def test_build_panel_update_payload_preserves_panel_contract_fields(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
settings = _make_settings(
|
||||
_tariffs_config_payload(),
|
||||
tmpdir,
|
||||
USER_SQUAD_UUIDS="squad-a,squad-b",
|
||||
USER_EXTERNAL_SQUAD_UUID="external-squad",
|
||||
USER_TRAFFIC_STRATEGY="MONTH",
|
||||
)
|
||||
service = _make_service(settings)
|
||||
expire_at = datetime(2026, 5, 13, 12, 34, 56, 789000, tzinfo=timezone.utc)
|
||||
|
||||
payload = service._build_panel_update_payload(
|
||||
panel_user_uuid="panel-uuid",
|
||||
expire_at=expire_at,
|
||||
status="ACTIVE",
|
||||
traffic_limit_bytes=12345,
|
||||
hwid_device_limit="4",
|
||||
)
|
||||
|
||||
self.assertEqual(payload["uuid"], "panel-uuid")
|
||||
self.assertEqual(payload["expireAt"], "2026-05-13T12:34:56.789Z")
|
||||
self.assertEqual(payload["status"], "ACTIVE")
|
||||
self.assertEqual(payload["trafficLimitBytes"], 12345)
|
||||
self.assertEqual(payload["trafficLimitStrategy"], "MONTH")
|
||||
self.assertEqual(payload["hwidDeviceLimit"], 4)
|
||||
self.assertEqual(payload["activeInternalSquads"], ["squad-a", "squad-b"])
|
||||
self.assertEqual(payload["externalSquadUuid"], "external-squad")
|
||||
|
||||
def test_extract_panel_traffic_details_accepts_nested_and_top_level_shapes(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
settings = _make_settings(_tariffs_config_payload(), tmpdir)
|
||||
service = _make_service(settings)
|
||||
|
||||
self.assertEqual(
|
||||
service._extract_panel_traffic_details(
|
||||
{
|
||||
"userTraffic": {
|
||||
"usedTrafficBytes": 15,
|
||||
"trafficLimitStrategy": "MONTH",
|
||||
},
|
||||
"trafficLimitBytes": 100,
|
||||
}
|
||||
),
|
||||
(15, 100, "MONTH"),
|
||||
)
|
||||
self.assertEqual(
|
||||
service._extract_panel_traffic_details(
|
||||
{
|
||||
"usedTrafficBytes": 20,
|
||||
"trafficLimitBytes": 200,
|
||||
"trafficLimitStrategy": "NO_RESET",
|
||||
}
|
||||
),
|
||||
(20, 200, "NO_RESET"),
|
||||
)
|
||||
|
||||
|
||||
class SubscriptionServiceActivationDispatchTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_activate_subscription_dispatches_traffic_sale_mode(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
settings = _make_settings(_tariffs_config_payload(), tmpdir)
|
||||
service = _make_service(settings)
|
||||
service._activate_traffic_package = AsyncMock(return_value={"kind": "traffic"})
|
||||
|
||||
result = await service.activate_subscription(
|
||||
session=AsyncMock(),
|
||||
user_id=42,
|
||||
months=3,
|
||||
payment_amount=500,
|
||||
payment_db_id=9,
|
||||
provider="stars",
|
||||
sale_mode="traffic@traffic",
|
||||
)
|
||||
|
||||
self.assertEqual(result, {"kind": "traffic"})
|
||||
service._activate_traffic_package.assert_awaited_once()
|
||||
kwargs = service._activate_traffic_package.await_args.kwargs
|
||||
self.assertEqual(kwargs["user_id"], 42)
|
||||
self.assertEqual(kwargs["traffic_gb"], 3.0)
|
||||
self.assertEqual(kwargs["payment_db_id"], 9)
|
||||
self.assertEqual(kwargs["provider"], "stars")
|
||||
self.assertEqual(kwargs["tariff_key"], "traffic")
|
||||
self.assertEqual(kwargs["sale_mode"], "traffic_package")
|
||||
|
||||
async def test_activate_subscription_dispatches_regular_topup_sale_mode(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
settings = _make_settings(_tariffs_config_payload(), tmpdir)
|
||||
service = _make_service(settings)
|
||||
service.activate_topup = AsyncMock(return_value={"kind": "topup"})
|
||||
|
||||
result = await service.activate_subscription(
|
||||
session=AsyncMock(),
|
||||
user_id=42,
|
||||
months=1,
|
||||
payment_amount=250,
|
||||
payment_db_id=10,
|
||||
provider="yookassa",
|
||||
sale_mode="topup@standard",
|
||||
traffic_gb=12.5,
|
||||
)
|
||||
|
||||
self.assertEqual(result, {"kind": "topup"})
|
||||
service.activate_topup.assert_awaited_once()
|
||||
kwargs = service.activate_topup.await_args.kwargs
|
||||
self.assertEqual(kwargs["user_id"], 42)
|
||||
self.assertEqual(kwargs["tariff_key"], "standard")
|
||||
self.assertEqual(kwargs["traffic_gb"], 12.5)
|
||||
self.assertEqual(kwargs["payment_amount"], 250)
|
||||
self.assertEqual(kwargs["payment_db_id"], 10)
|
||||
|
||||
async def test_activate_subscription_dispatches_premium_topup_sale_mode(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
settings = _make_settings(_tariffs_config_payload(), tmpdir)
|
||||
service = _make_service(settings)
|
||||
service.activate_premium_topup = AsyncMock(return_value={"kind": "premium"})
|
||||
|
||||
result = await service.activate_subscription(
|
||||
session=AsyncMock(),
|
||||
user_id=77,
|
||||
months=1,
|
||||
payment_amount=350,
|
||||
payment_db_id=11,
|
||||
provider="cryptopay",
|
||||
sale_mode="premium_topup|standard",
|
||||
traffic_gb=20,
|
||||
)
|
||||
|
||||
self.assertEqual(result, {"kind": "premium"})
|
||||
service.activate_premium_topup.assert_awaited_once()
|
||||
kwargs = service.activate_premium_topup.await_args.kwargs
|
||||
self.assertEqual(kwargs["user_id"], 77)
|
||||
self.assertEqual(kwargs["tariff_key"], "standard")
|
||||
self.assertEqual(kwargs["traffic_gb"], 20)
|
||||
self.assertEqual(kwargs["provider"], "cryptopay")
|
||||
|
||||
async def test_activate_subscription_dispatches_hwid_device_sale_mode(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
settings = _make_settings(_tariffs_config_payload(), tmpdir)
|
||||
service = _make_service(settings)
|
||||
service.activate_hwid_device_topup = AsyncMock(return_value={"kind": "hwid"})
|
||||
|
||||
result = await service.activate_subscription(
|
||||
session=AsyncMock(),
|
||||
user_id=88,
|
||||
months=2,
|
||||
payment_amount=150,
|
||||
payment_db_id=12,
|
||||
sale_mode="hwid_devices@standard",
|
||||
)
|
||||
|
||||
self.assertEqual(result, {"kind": "hwid"})
|
||||
service.activate_hwid_device_topup.assert_awaited_once()
|
||||
kwargs = service.activate_hwid_device_topup.await_args.kwargs
|
||||
self.assertEqual(kwargs["user_id"], 88)
|
||||
self.assertEqual(kwargs["device_count"], 2)
|
||||
self.assertEqual(kwargs["tariff_key"], "standard")
|
||||
self.assertEqual(kwargs["payment_db_id"], 12)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -9,6 +9,7 @@ from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from bot.app.web import subscription_webapp
|
||||
from bot.app.web.webapp import assets as webapp_assets
|
||||
from config.settings import Settings
|
||||
|
||||
|
||||
@@ -196,7 +197,7 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
||||
os.utime(old_asset, (1, 1))
|
||||
os.utime(new_asset, (2, 2))
|
||||
|
||||
with patch.object(subscription_webapp, "ASSET_DIR", asset_dir):
|
||||
with patch.object(webapp_assets, "ASSET_DIR", asset_dir):
|
||||
self.assertEqual(
|
||||
subscription_webapp._resolve_webapp_js_asset_name(),
|
||||
"subscription_webapp.min.22222222.js",
|
||||
@@ -213,7 +214,7 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
||||
match_info={"asset_hash": "abcdef12"},
|
||||
)
|
||||
|
||||
with patch.object(subscription_webapp, "ASSET_DIR", asset_dir):
|
||||
with patch.object(webapp_assets, "ASSET_DIR", asset_dir):
|
||||
response = await subscription_webapp.js_asset_route(request)
|
||||
|
||||
self.assertEqual(
|
||||
@@ -226,7 +227,7 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
||||
logo_url = "https://cdn.example.com/logo.png"
|
||||
logo = (b"png-bytes", "image/png")
|
||||
|
||||
with patch.object(subscription_webapp, "WEBAPP_LOGO_CACHE_DIR", Path(tmpdir)):
|
||||
with patch.object(webapp_assets, "WEBAPP_LOGO_CACHE_DIR", Path(tmpdir)):
|
||||
subscription_webapp._write_webapp_logo_to_disk(logo_url, logo)
|
||||
|
||||
self.assertEqual(subscription_webapp._read_webapp_logo_from_disk(logo_url), logo)
|
||||
@@ -242,13 +243,13 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
||||
}
|
||||
|
||||
with (
|
||||
patch.object(subscription_webapp, "WEBAPP_LOGO_CACHE_DIR", Path(tmpdir)),
|
||||
patch.object(webapp_assets, "WEBAPP_LOGO_CACHE_DIR", Path(tmpdir)),
|
||||
patch.object(
|
||||
subscription_webapp,
|
||||
webapp_assets,
|
||||
"_hostname_resolves_to_public_address",
|
||||
return_value=True,
|
||||
),
|
||||
patch.object(subscription_webapp, "_fetch_webapp_logo") as fetch_logo,
|
||||
patch.object(webapp_assets, "_fetch_webapp_logo") as fetch_logo,
|
||||
):
|
||||
subscription_webapp._write_webapp_logo_to_disk(logo_url, logo)
|
||||
|
||||
@@ -259,7 +260,7 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
def test_webapp_animated_emoji_disk_cache_roundtrip(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
with patch.object(subscription_webapp, "WEBAPP_EMOJI_CACHE_DIR", Path(tmpdir)):
|
||||
with patch.object(webapp_assets, "WEBAPP_EMOJI_CACHE_DIR", Path(tmpdir)):
|
||||
subscription_webapp._write_webapp_animated_emoji_to_disk(
|
||||
"1f929",
|
||||
"gif",
|
||||
@@ -283,8 +284,8 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase):
|
||||
}
|
||||
|
||||
with (
|
||||
patch.object(subscription_webapp, "WEBAPP_EMOJI_CACHE_DIR", Path(tmpdir)),
|
||||
patch.object(subscription_webapp, "_fetch_webapp_animated_emoji") as fetch_emoji,
|
||||
patch.object(webapp_assets, "WEBAPP_EMOJI_CACHE_DIR", Path(tmpdir)),
|
||||
patch.object(webapp_assets, "_fetch_webapp_animated_emoji") as fetch_emoji,
|
||||
):
|
||||
subscription_webapp._write_webapp_animated_emoji_to_disk(
|
||||
"1f929",
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from bot.app.web import admin_api, subscription_webapp
|
||||
from bot.app.web.admin_api_impl import auth as admin_auth_routes
|
||||
from bot.app.web.webapp_auth import create_webapp_session_token
|
||||
|
||||
|
||||
class _Request(dict):
|
||||
def __init__(self, *, path="/", app=None, headers=None, cookies=None):
|
||||
super().__init__()
|
||||
self.path = path
|
||||
self.app = app or {}
|
||||
self.headers = headers or {}
|
||||
self.cookies = cookies or {}
|
||||
|
||||
|
||||
class _AsyncSessionFactory:
|
||||
def __call__(self):
|
||||
return self
|
||||
|
||||
async def __aenter__(self):
|
||||
return object()
|
||||
|
||||
async def __aexit__(self, exc_type, exc, tb):
|
||||
return False
|
||||
|
||||
|
||||
def _route_map(app: web.Application) -> dict[tuple[str, str], str]:
|
||||
return {
|
||||
(route.method, route.resource.canonical): route.handler.__name__
|
||||
for route in app.router.routes()
|
||||
if route.method != "HEAD"
|
||||
}
|
||||
|
||||
|
||||
class WebAppRouteContractTests(unittest.TestCase):
|
||||
def test_subscription_webapp_registers_public_and_api_routes(self):
|
||||
app = web.Application()
|
||||
|
||||
subscription_webapp.setup_subscription_webapp_routes(app)
|
||||
|
||||
routes = _route_map(app)
|
||||
expected = {
|
||||
("GET", "/"): "index_route",
|
||||
("GET", "/home"): "index_route",
|
||||
("GET", "/invite"): "index_route",
|
||||
("GET", "/devices"): "index_route",
|
||||
("GET", "/settings"): "index_route",
|
||||
("GET", "/admin"): "index_route",
|
||||
("GET", "/admin/{section}"): "index_route",
|
||||
("GET", "/admin/users/{user_id}"): "index_route",
|
||||
("GET", "/auth/telegram/start"): "telegram_oauth_start_route",
|
||||
("GET", "/auth/telegram/callback"): "telegram_oauth_callback_route",
|
||||
("GET", "/health"): "health_route",
|
||||
("GET", "/webapp-logo"): "webapp_logo_route",
|
||||
("GET", "/webapp-emoji/{codepoints}/512.{ext}"): "webapp_animated_emoji_route",
|
||||
("GET", "/subscription_webapp.css"): "css_asset_route",
|
||||
("GET", "/subscription_webapp.min.{asset_hash}.js"): "js_asset_route",
|
||||
("GET", "/subscription_webapp.js"): "js_asset_route",
|
||||
("POST", "/api/auth/telegram/nonce"): "telegram_oauth_nonce_route",
|
||||
("POST", "/api/auth/token"): "auth_token_route",
|
||||
("POST", "/api/auth/email/request"): "email_auth_request_route",
|
||||
("POST", "/api/auth/email/verify"): "email_auth_verify_route",
|
||||
("POST", "/api/auth/email/magic"): "email_auth_magic_route",
|
||||
("POST", "/api/auth/logout"): "logout_route",
|
||||
("GET", "/api/me"): "me_route",
|
||||
("GET", "/api/account/avatar"): "account_avatar_route",
|
||||
("POST", "/api/account/language"): "account_language_route",
|
||||
("POST", "/api/account/email/request"): "account_email_request_route",
|
||||
("POST", "/api/account/email/verify"): "account_email_verify_route",
|
||||
("POST", "/api/account/telegram/link"): "account_telegram_link_route",
|
||||
("POST", "/api/promo/apply"): "apply_promo_route",
|
||||
("POST", "/api/trial/activate"): "activate_trial_route",
|
||||
("GET", "/api/devices"): "devices_route",
|
||||
("POST", "/api/devices/disconnect"): "disconnect_device_route",
|
||||
("GET", "/api/devices/topup-options"): "device_topup_options_route",
|
||||
("GET", "/api/tariffs/topup-options"): "tariff_topup_options_route",
|
||||
("GET", "/api/tariffs/change-options"): "tariff_change_options_route",
|
||||
("POST", "/api/tariffs/change"): "tariff_change_route",
|
||||
("POST", "/api/tariffs/change-payment"): "tariff_change_payment_route",
|
||||
("POST", "/api/payments"): "create_payment_route",
|
||||
("GET", "/api/payments/{payment_id}"): "payment_status_route",
|
||||
}
|
||||
|
||||
for key, handler_name in expected.items():
|
||||
self.assertEqual(routes.get(key), handler_name, key)
|
||||
|
||||
def test_admin_api_registers_expected_routes(self):
|
||||
app = web.Application()
|
||||
|
||||
admin_api.setup_admin_routes(app)
|
||||
|
||||
routes = _route_map(app)
|
||||
expected = {
|
||||
("GET", "/api/admin/me"): "admin_me_route",
|
||||
("GET", "/api/admin/stats"): "admin_stats_route",
|
||||
("GET", "/api/admin/users"): "admin_users_list_route",
|
||||
("GET", "/api/admin/users/{user_id}"): "admin_user_detail_route",
|
||||
("GET", "/api/admin/users/{user_id}/avatar"): "admin_user_avatar_route",
|
||||
("POST", "/api/admin/users/{user_id}/ban"): "admin_user_ban_route",
|
||||
("POST", "/api/admin/users/{user_id}/message"): "admin_user_message_route",
|
||||
(
|
||||
"POST",
|
||||
"/api/admin/users/{user_id}/message/preview",
|
||||
): "admin_user_message_preview_route",
|
||||
("POST", "/api/admin/users/{user_id}/reset-trial"): "admin_user_reset_trial_route",
|
||||
("POST", "/api/admin/users/{user_id}/extend"): "admin_user_extend_route",
|
||||
(
|
||||
"POST",
|
||||
"/api/admin/users/{user_id}/premium-override",
|
||||
): "admin_user_premium_override_route",
|
||||
(
|
||||
"POST",
|
||||
"/api/admin/users/{user_id}/regular-traffic-override",
|
||||
): "admin_user_regular_traffic_override_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",
|
||||
("GET", "/api/admin/payments/export.csv"): "admin_payments_export_route",
|
||||
("GET", "/api/admin/promos"): "admin_promos_list_route",
|
||||
("POST", "/api/admin/promos"): "admin_promo_create_route",
|
||||
("PATCH", "/api/admin/promos/{promo_id}"): "admin_promo_update_route",
|
||||
("DELETE", "/api/admin/promos/{promo_id}"): "admin_promo_delete_route",
|
||||
("GET", "/api/admin/logs"): "admin_logs_route",
|
||||
("POST", "/api/admin/broadcast"): "admin_broadcast_route",
|
||||
("POST", "/api/admin/sync"): "admin_sync_route",
|
||||
("GET", "/api/admin/ads"): "admin_ads_list_route",
|
||||
("POST", "/api/admin/ads"): "admin_ad_create_route",
|
||||
("POST", "/api/admin/ads/{campaign_id}/toggle"): "admin_ad_toggle_route",
|
||||
("DELETE", "/api/admin/ads/{campaign_id}"): "admin_ad_delete_route",
|
||||
("GET", "/api/admin/settings"): "admin_settings_get_route",
|
||||
("PATCH", "/api/admin/settings"): "admin_settings_patch_route",
|
||||
("GET", "/api/admin/tariffs"): "admin_tariffs_get_route",
|
||||
("PUT", "/api/admin/tariffs"): "admin_tariffs_save_route",
|
||||
("GET", "/api/admin/panel/internal-squads"): "admin_panel_internal_squads_route",
|
||||
}
|
||||
|
||||
for key, handler_name in expected.items():
|
||||
self.assertEqual(routes.get(key), handler_name, key)
|
||||
|
||||
|
||||
class AdminApiAuthContractTests(unittest.IsolatedAsyncioTestCase):
|
||||
def _settings(self):
|
||||
return SimpleNamespace(
|
||||
ADMIN_IDS=[999],
|
||||
WEBAPP_SESSION_SECRET="session-secret",
|
||||
WEBAPP_SESSION_TTL_SECONDS=3600,
|
||||
)
|
||||
|
||||
async def test_admin_auth_middleware_resolves_telegram_id_from_webapp_session(self):
|
||||
settings = self._settings()
|
||||
token = create_webapp_session_token(settings, 42)
|
||||
request = _Request(
|
||||
path="/api/admin/me",
|
||||
app={
|
||||
"settings": settings,
|
||||
"async_session_factory": _AsyncSessionFactory(),
|
||||
},
|
||||
headers={},
|
||||
cookies={"rw_webapp_session": token},
|
||||
)
|
||||
handler = AsyncMock(return_value=web.json_response({"ok": True}))
|
||||
db_user = SimpleNamespace(user_id=42, telegram_id=999)
|
||||
|
||||
with patch.object(
|
||||
admin_auth_routes.user_dal,
|
||||
"get_user_by_id",
|
||||
AsyncMock(return_value=db_user),
|
||||
):
|
||||
response = await admin_api.admin_auth_middleware(request, handler)
|
||||
|
||||
self.assertEqual(response.status, 200)
|
||||
self.assertEqual(request["admin_telegram_id"], 999)
|
||||
handler.assert_awaited_once_with(request)
|
||||
|
||||
async def test_admin_route_rejects_authenticated_non_admin(self):
|
||||
settings = self._settings()
|
||||
token = create_webapp_session_token(settings, 42)
|
||||
request = _Request(
|
||||
path="/api/admin/me",
|
||||
app={"settings": settings},
|
||||
headers={},
|
||||
cookies={"rw_webapp_session": token},
|
||||
)
|
||||
request["admin_telegram_id"] = 123
|
||||
|
||||
with self.assertRaises(web.HTTPForbidden) as raised:
|
||||
await admin_api.admin_me_route(request)
|
||||
|
||||
self.assertEqual(raised.exception.status, 403)
|
||||
Reference in New Issue
Block a user