fix: harden Remnawave panel timeouts

This commit is contained in:
3252a8
2026-06-02 12:29:43 +03:00
parent 56796d9f22
commit 4d7577f4ec
10 changed files with 249 additions and 11 deletions
@@ -236,10 +236,15 @@ def test_remnawave_settings_include_panel_webhook_metadata():
remnawave_keys = (
"PANEL_API_URL",
"PANEL_API_KEY",
"PANEL_API_TOTAL_TIMEOUT_SECONDS",
"PANEL_API_CONNECT_TIMEOUT_SECONDS",
"PANEL_API_SOCK_CONNECT_TIMEOUT_SECONDS",
"PANEL_API_SOCK_READ_TIMEOUT_SECONDS",
"PANEL_WEBHOOK_SECRET",
"USER_SQUAD_UUIDS",
"USER_EXTERNAL_SQUAD_UUID",
)
timeout_keys = remnawave_keys[2:6]
assert field["webhook_path"] == "/webhook/panel"
assert field["webhook_requires_base_url"] is True
@@ -250,10 +255,18 @@ def test_remnawave_settings_include_panel_webhook_metadata():
assert manifest[setting_key]["section_order"] == 3
assert manifest[setting_key]["subsection"] is None
for setting_key in timeout_keys:
assert manifest[setting_key]["type"] == "float"
assert manifest[setting_key]["optional"] is False
assert manifest[setting_key]["min"] == 1
for language in ("ru", "en"):
messages = _locale(language)
assert "admin_settings_section_remnawave" in messages
assert field["webhook_hint_i18n_key"] in messages
for setting_key in timeout_keys:
assert manifest[setting_key]["i18n_label_key"] in messages
assert manifest[setting_key]["i18n_description_key"] in messages
def test_payment_provider_admin_only_toggles_are_mutually_exclusive():
+65
View File
@@ -1,8 +1,11 @@
import asyncio
import time
import unittest
from types import SimpleNamespace
from unittest.mock import AsyncMock, patch
import aiohttp
from bot.services.panel_api_service import PanelApiService
@@ -16,6 +19,68 @@ class PanelApiServiceLoggingTests(unittest.IsolatedAsyncioTestCase):
)
)
async def test_client_timeout_uses_panel_settings(self):
service = PanelApiService(
SimpleNamespace(
PANEL_API_URL="https://panel.example.test/api",
PANEL_API_KEY="panel-key",
PANEL_API_TOTAL_TIMEOUT_SECONDS="30",
PANEL_API_CONNECT_TIMEOUT_SECONDS="10",
PANEL_API_SOCK_CONNECT_TIMEOUT_SECONDS="9",
PANEL_API_SOCK_READ_TIMEOUT_SECONDS="20",
)
)
timeout = service._client_timeout()
self.assertEqual(timeout.total, 30)
self.assertEqual(timeout.connect, 10)
self.assertEqual(timeout.sock_connect, 9)
self.assertEqual(timeout.sock_read, 20)
async def test_get_request_retries_connection_timeout(self):
service = self._make_service()
request_calls = 0
class OkResponse:
status = 200
headers = {"Content-Type": "application/json"}
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
return None
async def text(self):
return '{"response": {"ok": true}}'
def fake_request(*_args, **_kwargs):
nonlocal request_calls
request_calls += 1
if request_calls == 1:
raise aiohttp.ConnectionTimeoutError("connect took too long")
return OkResponse()
service._get_session = AsyncMock(return_value=SimpleNamespace(request=fake_request))
with patch("bot.services.panel_api_service.asyncio.sleep", new=AsyncMock()):
result = await service._request("GET", "/internal-squads")
self.assertEqual(result, {"response": {"ok": True}})
self.assertEqual(request_calls, 2)
async def test_get_internal_squads_uses_stale_cache_after_refresh_failure(self):
service = self._make_service()
stale_squads = [{"uuid": "squad-1", "name": "Squad 1"}]
service._squads_cache._data["list"] = (time.monotonic() - 1, stale_squads)
service._get_internal_squads_uncached = AsyncMock(return_value=None)
squads = await service.get_internal_squads()
self.assertEqual(squads, stale_squads)
service._get_internal_squads_uncached.assert_awaited_once()
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"}})
+8
View File
@@ -42,6 +42,14 @@ class AsyncTTLCacheSingleflightTests(unittest.IsolatedAsyncioTestCase):
class AsyncTTLCacheInvalidationTests(unittest.IsolatedAsyncioTestCase):
def test_get_stale_returns_expired_cacheable_value(self):
cache = AsyncTTLCache(ttl_seconds=60)
value = {"ok": True}
cache._data["same"] = (time.monotonic() - 1, value)
self.assertIsNone(cache.get_fresh("same"))
self.assertEqual(cache.get_stale("same"), value)
async def test_invalidate_remote_deletes_single_redis_key(self):
settings = SimpleNamespace(REDIS_URL="redis://example", REDIS_KEY_PREFIX="test")
cache = AsyncTTLCache(ttl_seconds=60, settings=settings, namespace="bench")