149 lines
5.2 KiB
Python
149 lines
5.2 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
import unittest
|
|
from contextlib import asynccontextmanager
|
|
from pathlib import Path
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
|
|
BACKEND_DIR = Path(__file__).resolve().parents[1]
|
|
os.environ["KEFU_DB_TYPE"] = "sqlite"
|
|
os.environ["KEFU_DATABASE_URL"] = ""
|
|
os.environ["KEFU_DB_PATH"] = str(BACKEND_DIR / "kefu.db")
|
|
if str(BACKEND_DIR) not in sys.path:
|
|
sys.path.insert(0, str(BACKEND_DIR))
|
|
|
|
from rpa_engine.douyin_im.http_client import DouyinImHttpClient
|
|
from rpa_engine.douyin_im.session import DouyinImSession
|
|
from rpa_engine.douyin_im.service import DouyinImService, _conversation_poll_timing
|
|
from rpa_engine.douyin_im import service as service_module
|
|
|
|
|
|
class ConversationPollBandwidthTests(unittest.IsolatedAsyncioTestCase):
|
|
def _make_client(self) -> DouyinImHttpClient:
|
|
return DouyinImHttpClient(
|
|
DouyinImSession(cookies={"sessionid": "test"}, my_uid=10001),
|
|
account_id=9,
|
|
)
|
|
|
|
async def test_terminal_token_error_does_not_probe_other_payloads(self):
|
|
client = self._make_client()
|
|
client._request = AsyncMock(
|
|
return_value={
|
|
"status_code": 500,
|
|
"error_desc": "empty token",
|
|
"body": {},
|
|
}
|
|
)
|
|
|
|
self.assertEqual(await client.get_conversations(), [])
|
|
client._request.assert_awaited_once()
|
|
self.assertEqual(client._request.await_args.args[0], "POST")
|
|
|
|
async def test_successful_empty_response_stops_after_first_payload(self):
|
|
client = self._make_client()
|
|
client._request = AsyncMock(
|
|
return_value={"status_code": 0, "body": {"conversation_list": []}}
|
|
)
|
|
|
|
self.assertEqual(await client.get_conversations(), [])
|
|
client._request.assert_awaited_once()
|
|
|
|
async def test_parameter_error_can_fall_through_to_compatible_payload(self):
|
|
client = self._make_client()
|
|
client._request = AsyncMock(
|
|
side_effect=[
|
|
{"status_code": 400, "error_desc": "invalid parameter"},
|
|
{"status_code": 0, "body": {"conversation_list": []}},
|
|
]
|
|
)
|
|
|
|
self.assertEqual(await client.get_conversations(), [])
|
|
self.assertEqual(client._request.await_count, 2)
|
|
self.assertTrue(
|
|
all(call.args[0] == "POST" for call in client._request.await_args_list)
|
|
)
|
|
|
|
async def test_get_fallback_only_runs_after_transport_failure(self):
|
|
client = self._make_client()
|
|
client._request = AsyncMock(
|
|
side_effect=[None, {"status_code": 0, "body": {}}]
|
|
)
|
|
|
|
self.assertEqual(await client.get_conversations(), [])
|
|
self.assertEqual(
|
|
[call.args[0] for call in client._request.await_args_list],
|
|
["POST", "GET"],
|
|
)
|
|
|
|
async def test_transport_outage_stops_after_one_post_and_get_pair(self):
|
|
client = self._make_client()
|
|
client._request = AsyncMock(return_value=None)
|
|
|
|
with self.assertLogs("douyin_im.http", level="WARNING"):
|
|
self.assertEqual(await client.get_conversations(), [])
|
|
|
|
self.assertEqual(client._request.await_count, 2)
|
|
self.assertEqual(
|
|
[call.args[0] for call in client._request.await_args_list],
|
|
["POST", "GET"],
|
|
)
|
|
|
|
def test_websocket_reconciliation_is_slow_and_http_fallback_stays_fast(self):
|
|
with patch.dict(
|
|
os.environ,
|
|
{
|
|
"KEFU_WS_RECONCILE_INTERVAL_SECONDS": "120",
|
|
"KEFU_HTTP_POLL_INTERVAL_SECONDS": "15",
|
|
},
|
|
):
|
|
ws_interval, ws_stagger = _conversation_poll_timing(123, True)
|
|
http_interval, http_stagger = _conversation_poll_timing(123, False)
|
|
|
|
self.assertEqual(ws_interval, 120)
|
|
self.assertEqual(http_interval, 15)
|
|
self.assertGreaterEqual(ws_stagger, 0)
|
|
self.assertLess(ws_stagger, ws_interval)
|
|
self.assertGreaterEqual(http_stagger, 0)
|
|
self.assertLess(http_stagger, http_interval)
|
|
|
|
async def test_service_poll_uses_one_conversation_request_without_unread_probe(self):
|
|
class _Controller:
|
|
@asynccontextmanager
|
|
async def background_slot(self, *_args, **_kwargs):
|
|
yield
|
|
|
|
class _HttpClient:
|
|
def __init__(self):
|
|
self.get_conversations = AsyncMock(return_value=[])
|
|
|
|
async def __aenter__(self):
|
|
return self
|
|
|
|
async def __aexit__(self, *_args):
|
|
return False
|
|
|
|
http = _HttpClient()
|
|
service = DouyinImService(
|
|
session=DouyinImSession(cookies={"sessionid": "test"}, my_uid=10001),
|
|
match_reply=AsyncMock(return_value=None),
|
|
log_fn=AsyncMock(),
|
|
account_id=9,
|
|
)
|
|
service._index_conversations = AsyncMock()
|
|
|
|
with (
|
|
patch.object(service_module, "get_traffic_controller", return_value=_Controller()),
|
|
patch.object(service_module, "DouyinImHttpClient", return_value=http),
|
|
):
|
|
await service._poll_conversations()
|
|
|
|
http.get_conversations.assert_awaited_once_with(enrich_profiles=False)
|
|
service._index_conversations.assert_awaited_once_with([])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|