fix: harden Remnawave panel timeouts
This commit is contained in:
@@ -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():
|
||||
|
||||
@@ -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"}})
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user