feat: add remnawave panel dry-run mode
This commit is contained in:
@@ -22,6 +22,7 @@ from unittest.mock import MagicMock
|
||||
|
||||
from bot.app.factories.build_services import build_core_services
|
||||
from bot.payment_providers.yookassa import YooKassaService
|
||||
from bot.services.panel_dry_run_api_service import PanelDryRunApiService
|
||||
from bot.services.panel_webhook_service import PanelWebhookService
|
||||
from bot.services.subscription_service import SubscriptionService
|
||||
from config.settings import Settings
|
||||
@@ -125,6 +126,23 @@ class BuildServicesWiringTests(unittest.TestCase):
|
||||
self.assertIsInstance(panel_webhook, PanelWebhookService)
|
||||
self.assertIs(getattr(panel_webhook, "subscription_service", None), subscription)
|
||||
|
||||
def test_development_runtime_wires_panel_dry_run_service(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
settings = _make_settings(tmpdir, APP_RUNTIME_MODE="development")
|
||||
services = build_core_services(
|
||||
settings=settings,
|
||||
bot=MagicMock(),
|
||||
async_session_factory=MagicMock(),
|
||||
i18n=MagicMock(),
|
||||
bot_username_for_default_return="testbot",
|
||||
)
|
||||
|
||||
self.assertIsInstance(services["panel_service"], PanelDryRunApiService)
|
||||
self.assertIs(
|
||||
services["subscription_service"].panel_service,
|
||||
services["panel_service"],
|
||||
)
|
||||
|
||||
def test_factory_returns_every_documented_service(self):
|
||||
"""Guards against silently dropping a service from the bundle. The
|
||||
web layer reads these keys off ``request.app`` directly — a missing
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from bot.services.panel_dry_run_api_service import PanelDryRunApiService
|
||||
|
||||
|
||||
def _settings(**overrides):
|
||||
values = {
|
||||
"PANEL_API_URL": "https://panel.example.test/api",
|
||||
"PANEL_API_KEY": "panel-key",
|
||||
"PANEL_DRY_RUN_VALIDATE_REMOTE": False,
|
||||
"PANEL_DRY_RUN_SYNTHETIC_CREATE": True,
|
||||
"PANEL_USER_CACHE_TTL_SECONDS": 0,
|
||||
"PANEL_DEVICES_CACHE_TTL_SECONDS": 0,
|
||||
"PANEL_ALL_USERS_CACHE_TTL_SECONDS": 0,
|
||||
"PANEL_ALL_USERS_PAGE_SIZE": 1000,
|
||||
"REDIS_KEY_PREFIX": "tests",
|
||||
"USER_HWID_DEVICE_LIMIT": None,
|
||||
}
|
||||
values.update(overrides)
|
||||
return SimpleNamespace(**values)
|
||||
|
||||
|
||||
class PanelDryRunApiServiceTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_update_user_details_returns_synthetic_success_without_http_write(self):
|
||||
service = PanelDryRunApiService(_settings())
|
||||
service._request_once = AsyncMock()
|
||||
|
||||
result = await service.update_user_details_on_panel(
|
||||
"user-uuid",
|
||||
{"trafficLimitBytes": 1024, "trafficLimitStrategy": "NO_RESET"},
|
||||
)
|
||||
|
||||
self.assertEqual(result["uuid"], "user-uuid")
|
||||
self.assertTrue(result["dryRun"])
|
||||
self.assertEqual(result["trafficLimitBytes"], 1024)
|
||||
service._request_once.assert_not_awaited()
|
||||
|
||||
async def test_update_user_details_rejects_invalid_payload(self):
|
||||
service = PanelDryRunApiService(_settings())
|
||||
service._request_once = AsyncMock()
|
||||
|
||||
result = await service.update_user_details_on_panel(
|
||||
"user-uuid",
|
||||
{"trafficLimitBytes": -1, "trafficLimitStrategy": "NO_RESET"},
|
||||
)
|
||||
|
||||
self.assertIsNone(result)
|
||||
service._request_once.assert_not_awaited()
|
||||
|
||||
async def test_remote_validation_blocks_missing_panel_user(self):
|
||||
service = PanelDryRunApiService(_settings(PANEL_DRY_RUN_VALIDATE_REMOTE=True))
|
||||
service._request_once = AsyncMock(
|
||||
return_value={"error": True, "status_code": 404, "details": {"errorCode": "A062"}}
|
||||
)
|
||||
|
||||
result = await service.update_user_details_on_panel(
|
||||
"missing-user",
|
||||
{"trafficLimitBytes": 1024, "trafficLimitStrategy": "NO_RESET"},
|
||||
)
|
||||
|
||||
self.assertIsNone(result)
|
||||
service._request_once.assert_awaited_once()
|
||||
|
||||
async def test_create_panel_user_returns_synthetic_user(self):
|
||||
service = PanelDryRunApiService(_settings())
|
||||
service._request_once = AsyncMock()
|
||||
|
||||
result = await service.create_panel_user(
|
||||
username_on_panel="tg_42",
|
||||
telegram_id=42,
|
||||
default_traffic_limit_bytes=2048,
|
||||
default_traffic_limit_strategy="NO_RESET",
|
||||
)
|
||||
|
||||
panel_user = result["response"]
|
||||
self.assertTrue(panel_user["dryRun"])
|
||||
self.assertEqual(panel_user["username"], "tg_42")
|
||||
self.assertIn("uuid", panel_user)
|
||||
self.assertTrue(
|
||||
panel_user["subscriptionUrl"].startswith("https://panel.example.test/api/sub/")
|
||||
)
|
||||
self.assertTrue(panel_user["subscriptionUrl"].endswith(panel_user["shortUuid"]))
|
||||
service._request_once.assert_not_awaited()
|
||||
|
||||
async def test_happ_encrypt_post_stays_live(self):
|
||||
service = PanelDryRunApiService(_settings())
|
||||
service._request_once = AsyncMock(
|
||||
return_value={"response": {"encryptedLink": "happ://crypt4/test"}}
|
||||
)
|
||||
|
||||
result = await service.encrypt_happ_link("https://panel.example.test/sub/abc")
|
||||
|
||||
self.assertEqual(result, "happ://crypt4/test")
|
||||
service._request_once.assert_awaited_once()
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
unittest.main()
|
||||
@@ -42,6 +42,51 @@ class SettingsTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(settings.WEBAPP_TITLE, "/minishop")
|
||||
|
||||
def test_panel_write_mode_defaults_to_live_in_production(self):
|
||||
settings = Settings(
|
||||
_env_file=None,
|
||||
BOT_TOKEN="token",
|
||||
POSTGRES_USER="app_user",
|
||||
POSTGRES_PASSWORD="app_password",
|
||||
)
|
||||
|
||||
self.assertEqual(settings.APP_RUNTIME_MODE, "production")
|
||||
self.assertEqual(settings.PANEL_WRITE_MODE, "auto")
|
||||
self.assertFalse(settings.panel_dry_run_enabled)
|
||||
|
||||
def test_development_runtime_enables_panel_dry_run_in_auto_mode(self):
|
||||
settings = Settings(
|
||||
_env_file=None,
|
||||
BOT_TOKEN="token",
|
||||
POSTGRES_USER="app_user",
|
||||
POSTGRES_PASSWORD="app_password",
|
||||
APP_RUNTIME_MODE="development",
|
||||
)
|
||||
|
||||
self.assertTrue(settings.panel_dry_run_enabled)
|
||||
|
||||
def test_panel_write_mode_live_overrides_development_runtime(self):
|
||||
settings = Settings(
|
||||
_env_file=None,
|
||||
BOT_TOKEN="token",
|
||||
POSTGRES_USER="app_user",
|
||||
POSTGRES_PASSWORD="app_password",
|
||||
APP_RUNTIME_MODE="development",
|
||||
PANEL_WRITE_MODE="live",
|
||||
)
|
||||
|
||||
self.assertFalse(settings.panel_dry_run_enabled)
|
||||
|
||||
def test_panel_write_mode_rejects_unknown_value(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
Settings(
|
||||
_env_file=None,
|
||||
BOT_TOKEN="token",
|
||||
POSTGRES_USER="app_user",
|
||||
POSTGRES_PASSWORD="app_password",
|
||||
PANEL_WRITE_MODE="danger",
|
||||
)
|
||||
|
||||
def test_legacy_subscription_prices_have_defaults(self):
|
||||
settings = Settings(
|
||||
_env_file=None,
|
||||
|
||||
Reference in New Issue
Block a user