132 lines
5.0 KiB
Python
132 lines
5.0 KiB
Python
import unittest
|
|
from unittest.mock import patch
|
|
|
|
from app.services.feishu_long_connection import FeishuLongConnectionManager
|
|
|
|
|
|
class _AliveThread:
|
|
def __init__(self) -> None:
|
|
self._alive = True
|
|
|
|
def is_alive(self) -> bool:
|
|
return self._alive
|
|
|
|
def join(self, timeout: float | None = None) -> None:
|
|
_ = timeout
|
|
self._alive = False
|
|
|
|
|
|
class _StubFeishuLongConnectionManager(FeishuLongConnectionManager):
|
|
def __init__(self, config: dict | None, preflight_exc: Exception | None = None) -> None:
|
|
super().__init__(session_factory=None) # type: ignore[arg-type]
|
|
self._sdk_available = True
|
|
self._stub_config = config
|
|
self._preflight_exc = preflight_exc
|
|
self.start_calls: list[tuple[str, str]] = []
|
|
|
|
async def _load_active_config(self) -> dict | None:
|
|
if self._stub_config is None:
|
|
return None
|
|
return dict(self._stub_config)
|
|
|
|
async def _preflight_auth(self, *, tenant_id: int) -> None:
|
|
_ = tenant_id
|
|
if self._preflight_exc is not None:
|
|
raise self._preflight_exc
|
|
|
|
def _start_ws_thread(self, app_id: str, app_secret: str) -> None:
|
|
self.start_calls.append((app_id, app_secret))
|
|
self._thread = _AliveThread()
|
|
|
|
|
|
class FeishuLongConnectionManagerTest(unittest.IsolatedAsyncioTestCase):
|
|
async def asyncTearDown(self) -> None:
|
|
manager = getattr(self, "manager", None)
|
|
if manager is not None and manager.status().get("running"):
|
|
await manager.stop()
|
|
|
|
async def test_auth_failure_is_classified_without_starting_thread(self) -> None:
|
|
self.manager = _StubFeishuLongConnectionManager(
|
|
config={
|
|
"tenant_id": 9,
|
|
"event_mode": "long_connection",
|
|
"app_id": "cli_xxx",
|
|
"app_secret": "secret_xxx",
|
|
},
|
|
preflight_exc=ValueError("获取 tenant_access_token 失败(http=401): {'code': 99991663}"),
|
|
)
|
|
|
|
await self.manager.start()
|
|
|
|
status = self.manager.status()
|
|
self.assertEqual(status["tenant_id"], 9)
|
|
self.assertEqual(status["last_error_code"], "auth_failed")
|
|
self.assertFalse(status["last_error_retryable"])
|
|
self.assertFalse(status["thread_alive"])
|
|
self.assertEqual(self.manager.start_calls, [])
|
|
|
|
async def test_network_failure_is_marked_retryable(self) -> None:
|
|
self.manager = _StubFeishuLongConnectionManager(
|
|
config={
|
|
"tenant_id": 3,
|
|
"event_mode": "long_connection",
|
|
"app_id": "cli_xxx",
|
|
"app_secret": "secret_xxx",
|
|
},
|
|
preflight_exc=ValueError("访问飞书 auth 接口失败: timed out"),
|
|
)
|
|
|
|
await self.manager.start()
|
|
|
|
status = self.manager.status()
|
|
self.assertEqual(status["last_error_code"], "network_error")
|
|
self.assertTrue(status["last_error_retryable"])
|
|
self.assertEqual(status["restart_attempts"], 1)
|
|
self.assertIsNotNone(status["next_retry_at"])
|
|
self.assertEqual(self.manager.start_calls, [])
|
|
|
|
async def test_successful_start_records_tenant_and_auth_timestamp(self) -> None:
|
|
self.manager = _StubFeishuLongConnectionManager(
|
|
config={
|
|
"tenant_id": 12,
|
|
"event_mode": "long_connection",
|
|
"app_id": "cli_xxx",
|
|
"app_secret": "secret_xxx",
|
|
}
|
|
)
|
|
|
|
await self.manager.start()
|
|
|
|
status = self.manager.status()
|
|
self.assertEqual(status["tenant_id"], 12)
|
|
self.assertTrue(status["thread_alive"])
|
|
self.assertIsNotNone(status["last_auth_at"])
|
|
self.assertEqual(self.manager.start_calls, [("cli_xxx", "secret_xxx")])
|
|
|
|
async def test_retry_strategy_is_1_2_4_and_stops_after_three_attempts(self) -> None:
|
|
self.manager = _StubFeishuLongConnectionManager(
|
|
config={
|
|
"tenant_id": 7,
|
|
"event_mode": "long_connection",
|
|
"app_id": "cli_xxx",
|
|
"app_secret": "secret_xxx",
|
|
}
|
|
)
|
|
self.manager._set_error("network", code="network_error", retryable=True)
|
|
|
|
with patch("app.services.feishu_long_connection.time.time", return_value=100.0):
|
|
self.manager._schedule_retry()
|
|
self.assertEqual(self.manager._restart_attempts, 1)
|
|
self.assertEqual(self.manager._next_retry_at, 101.0)
|
|
self.manager._schedule_retry()
|
|
self.assertEqual(self.manager._restart_attempts, 2)
|
|
self.assertEqual(self.manager._next_retry_at, 102.0)
|
|
self.manager._schedule_retry()
|
|
self.assertEqual(self.manager._restart_attempts, 3)
|
|
self.assertEqual(self.manager._next_retry_at, 104.0)
|
|
# 第 4 次触发应熔断,不再继续重试。
|
|
self.manager._schedule_retry()
|
|
self.assertEqual(self.manager._restart_attempts, 3)
|
|
self.assertEqual(self.manager._next_retry_at, 0.0)
|
|
self.assertFalse(self.manager._last_error_retryable)
|