192 lines
6.9 KiB
Python
192 lines
6.9 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
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.session import DouyinImSession
|
|
from rpa_engine.playwright_worker import DouyinWorker
|
|
|
|
|
|
class WorkerScaleControlTests(unittest.IsolatedAsyncioTestCase):
|
|
async def test_storage_load_selects_only_cookie_column(self):
|
|
class _ScalarResult:
|
|
def scalar_one_or_none(self):
|
|
return '{"cookies": [{"name": "sessionid", "value": "ok"}]}'
|
|
|
|
db = SimpleNamespace(
|
|
execute=AsyncMock(return_value=_ScalarResult()),
|
|
close=AsyncMock(),
|
|
)
|
|
worker = DouyinWorker(account_id=499)
|
|
worker.get_db = AsyncMock(return_value=db)
|
|
|
|
storage = await worker._load_storage_state()
|
|
|
|
self.assertEqual(storage["cookies"][0]["value"], "ok")
|
|
statement = db.execute.await_args.args[0]
|
|
selected_names = [
|
|
item.get("name") for item in statement.column_descriptions
|
|
]
|
|
self.assertEqual(selected_names, ["cookie_data"])
|
|
self.assertNotIn("im_session_data", str(statement).lower())
|
|
|
|
async def test_direct_service_marks_worker_ready_after_initialization(self):
|
|
worker = DouyinWorker(account_id=500, login_mode="im_direct")
|
|
worker._refresh_follow_welcome_config = AsyncMock(
|
|
return_value=(False, "", "sec-user")
|
|
)
|
|
worker.get_reply_delay = AsyncMock(return_value=None)
|
|
fake_service = SimpleNamespace(run=AsyncMock(), stop=AsyncMock())
|
|
|
|
async def complete_initialization():
|
|
service_factory.call_args.kwargs["on_ready"]()
|
|
|
|
fake_service.run.side_effect = complete_initialization
|
|
session = DouyinImSession(
|
|
cookies={"sessionid": "session"},
|
|
my_uid=0,
|
|
)
|
|
|
|
with patch(
|
|
"rpa_engine.playwright_worker.DouyinImService",
|
|
return_value=fake_service,
|
|
) as service_factory:
|
|
await worker._run_im_direct_service(session)
|
|
|
|
await worker.wait_until_ready()
|
|
worker._refresh_follow_welcome_config.assert_awaited_once_with(force=True)
|
|
fake_service.run.assert_awaited_once_with()
|
|
fake_service.stop.assert_awaited_once_with()
|
|
|
|
async def test_direct_start_failure_releases_readiness_waiter(self):
|
|
worker = DouyinWorker(account_id=501, login_mode="im_direct")
|
|
worker._load_storage_state = AsyncMock(return_value=None)
|
|
worker.update_account_status = AsyncMock()
|
|
worker.cleanup = AsyncMock()
|
|
|
|
await worker.start()
|
|
|
|
with self.assertRaisesRegex(RuntimeError, "未保存 Cookie"):
|
|
await worker.wait_until_ready()
|
|
|
|
if worker._task:
|
|
await worker._task
|
|
worker.update_account_status.assert_awaited_once_with(
|
|
"error",
|
|
error_msg="未保存 Cookie,无法直连 IM",
|
|
)
|
|
|
|
async def test_prevalidated_start_skips_duplicate_remote_validation(self):
|
|
worker = DouyinWorker(
|
|
account_id=502,
|
|
login_mode="im_direct",
|
|
credential_prevalidated=True,
|
|
)
|
|
session = DouyinImSession(
|
|
cookies={"sessionid": "session"},
|
|
my_uid=10001,
|
|
keys_str=json.dumps({"ec_privateKey": "private"}),
|
|
web_protect_str=json.dumps(
|
|
{
|
|
"ticket": "ticket",
|
|
"ts_sign": "sign",
|
|
"client_cert": "certificate",
|
|
}
|
|
),
|
|
)
|
|
worker._load_user_agent = AsyncMock(return_value="test-agent")
|
|
worker._build_im_session_from_storage = AsyncMock(return_value=session)
|
|
worker._require_sec_user_id = AsyncMock(return_value="sec-user")
|
|
worker._persist_im_session = AsyncMock()
|
|
worker._run_im_direct_service = AsyncMock()
|
|
|
|
with patch(
|
|
"rpa_engine.playwright_worker.validate_im_session",
|
|
new_callable=AsyncMock,
|
|
) as validate:
|
|
started, reason = await worker._try_cookie_only_im_start(
|
|
{"cookies": []}
|
|
)
|
|
|
|
self.assertTrue(started)
|
|
self.assertEqual(reason, "")
|
|
validate.assert_not_awaited()
|
|
worker._require_sec_user_id.assert_awaited_once()
|
|
worker._run_im_direct_service.assert_awaited_once_with(session)
|
|
|
|
async def test_disabled_follow_welcome_uses_cached_lightweight_config(self):
|
|
class _Result:
|
|
def first(self):
|
|
return False, "", "sec-user"
|
|
|
|
db = SimpleNamespace(
|
|
execute=AsyncMock(return_value=_Result()),
|
|
close=AsyncMock(),
|
|
)
|
|
worker = DouyinWorker(account_id=503)
|
|
worker.get_db = AsyncMock(return_value=db)
|
|
|
|
first = await worker._refresh_follow_welcome_config()
|
|
second = await worker._refresh_follow_welcome_config()
|
|
|
|
self.assertEqual(first, (False, "", "sec-user"))
|
|
self.assertEqual(second, first)
|
|
worker.get_db.assert_awaited_once_with()
|
|
db.execute.assert_awaited_once()
|
|
db.close.assert_awaited_once_with()
|
|
|
|
# The account PUT endpoint calls this synchronous hook so enabling the
|
|
# feature does not wait for the disabled-account ten-minute TTL.
|
|
worker.invalidate_follow_welcome_config()
|
|
await worker._refresh_follow_welcome_config()
|
|
self.assertEqual(worker.get_db.await_count, 2)
|
|
self.assertEqual(db.execute.await_count, 2)
|
|
|
|
async def test_disabled_follow_tick_does_not_read_follower_log(self):
|
|
worker = DouyinWorker(account_id=504)
|
|
worker._im_service = SimpleNamespace(session=object())
|
|
worker._follow_config_loaded = True
|
|
worker._refresh_follow_welcome_config = AsyncMock(
|
|
return_value=(False, "", "sec-user")
|
|
)
|
|
worker.get_db = AsyncMock()
|
|
worker._require_sec_user_id = AsyncMock()
|
|
|
|
await worker.follow_welcome_tick()
|
|
|
|
worker._refresh_follow_welcome_config.assert_awaited_once_with()
|
|
worker.get_db.assert_not_awaited()
|
|
worker._require_sec_user_id.assert_not_awaited()
|
|
|
|
async def test_missing_cached_sec_user_id_stops_hosting(self):
|
|
worker = DouyinWorker(account_id=505)
|
|
worker._im_service = SimpleNamespace(session=object())
|
|
worker._follow_config_loaded = True
|
|
worker._refresh_follow_welcome_config = AsyncMock(
|
|
return_value=(False, "", "")
|
|
)
|
|
worker._require_sec_user_id = AsyncMock(return_value="")
|
|
|
|
await worker.follow_welcome_tick()
|
|
|
|
worker._require_sec_user_id.assert_awaited_once_with(
|
|
"托管运行中"
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|