187 lines
6.7 KiB
Python
187 lines
6.7 KiB
Python
from __future__ import annotations
|
|
|
|
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))
|
|
|
|
import main
|
|
|
|
|
|
def _queue_item(account_id: int, job_id: str = "job-1") -> dict:
|
|
return {
|
|
"job_id": job_id,
|
|
"account_id": account_id,
|
|
"position": 1,
|
|
"status": "waiting",
|
|
"expedited": False,
|
|
"description": "回复 测试用户",
|
|
"sender_name": "测试用户",
|
|
"sender_id": "peer-1",
|
|
"sender_avatar": None,
|
|
"conversation_id": "conv-1",
|
|
"incoming_content": "你好",
|
|
"incoming_contents": ["你好", "第二条"],
|
|
"message_count": 2,
|
|
"replies": ["您好"],
|
|
"interval_seconds": 60,
|
|
"enqueued_at": "2026-07-20T10:00:00+00:00",
|
|
"scheduled_at": "2026-07-20T10:01:00+00:00",
|
|
"remaining_seconds": 60,
|
|
}
|
|
|
|
|
|
class _FakeService:
|
|
def __init__(self, account_id: int, items=None, action=None):
|
|
self.account_id = account_id
|
|
self._running = True
|
|
self.items = list(items or [])
|
|
self.action = action or {
|
|
"status": "accepted",
|
|
"job_id": "job-1",
|
|
"shifted_count": 2,
|
|
}
|
|
|
|
async def get_reply_queue_snapshot(self):
|
|
return list(self.items)
|
|
|
|
async def send_queued_reply_now(self, job_id: str):
|
|
return {**self.action, "job_id": job_id}
|
|
|
|
|
|
class ReplyQueueApiTests(unittest.IsolatedAsyncioTestCase):
|
|
async def asyncSetUp(self):
|
|
self.original_workers = main.manager.workers
|
|
main.manager.workers = {}
|
|
|
|
async def asyncTearDown(self):
|
|
main.manager.workers = self.original_workers
|
|
|
|
async def test_summary_filters_accounts_by_owner_scope(self):
|
|
service_one = _FakeService(1, [_queue_item(1)])
|
|
service_two = _FakeService(2, [_queue_item(2, "job-2")])
|
|
main.manager.workers = {
|
|
1: SimpleNamespace(is_running=True, _im_service=service_one),
|
|
2: SimpleNamespace(is_running=True, _im_service=service_two),
|
|
}
|
|
|
|
with patch.object(main, "owned_account_ids", AsyncMock(return_value={1})):
|
|
response = await main.get_reply_queue_summaries(
|
|
db=object(),
|
|
user=SimpleNamespace(id=10, role="operator"),
|
|
)
|
|
|
|
self.assertEqual(response.total_pending, 1)
|
|
self.assertEqual([item.account_id for item in response.items], [1])
|
|
|
|
async def test_summary_with_account_ids_only_scans_requested_page(self):
|
|
service_one = _FakeService(1, [_queue_item(1)])
|
|
service_two = _FakeService(2, [_queue_item(2, "job-2")])
|
|
service_one.get_reply_queue_snapshot = AsyncMock(return_value=service_one.items)
|
|
service_two.get_reply_queue_snapshot = AsyncMock(return_value=service_two.items)
|
|
main.manager.workers = {
|
|
1: SimpleNamespace(is_running=True, _im_service=service_one),
|
|
2: SimpleNamespace(is_running=True, _im_service=service_two),
|
|
}
|
|
|
|
response = await main.get_reply_queue_summaries(
|
|
account_ids="2",
|
|
db=object(),
|
|
user=SimpleNamespace(id=1, role="admin"),
|
|
)
|
|
|
|
service_one.get_reply_queue_snapshot.assert_not_awaited()
|
|
service_two.get_reply_queue_snapshot.assert_awaited_once()
|
|
self.assertEqual(response.total_pending, 1)
|
|
self.assertEqual([item.account_id for item in response.items], [2])
|
|
|
|
async def test_offline_account_detail_returns_empty_snapshot(self):
|
|
account = SimpleNamespace(id=1, reply_delay_seconds=0)
|
|
with (
|
|
patch.object(main, "get_owned_account", AsyncMock(return_value=account)),
|
|
patch.object(main, "_global_reply_delay_seconds", return_value=0),
|
|
):
|
|
response = await main.get_account_reply_queue(
|
|
account_id=1,
|
|
db=object(),
|
|
user=SimpleNamespace(id=10, role="operator"),
|
|
)
|
|
|
|
self.assertFalse(response.running)
|
|
self.assertEqual(response.pending_count, 0)
|
|
self.assertEqual(response.items, [])
|
|
|
|
async def test_detail_preserves_merged_incoming_messages(self):
|
|
account = SimpleNamespace(id=1, reply_delay_seconds=60)
|
|
service = _FakeService(1, [_queue_item(1)])
|
|
main.manager.workers = {
|
|
1: SimpleNamespace(is_running=True, _im_service=service),
|
|
}
|
|
with (
|
|
patch.object(main, "get_owned_account", AsyncMock(return_value=account)),
|
|
patch.object(main, "_resolve_effective_reply_delay", return_value=60),
|
|
):
|
|
response = await main.get_account_reply_queue(
|
|
account_id=1,
|
|
db=object(),
|
|
user=SimpleNamespace(id=10, role="operator"),
|
|
)
|
|
|
|
self.assertEqual(response.pending_count, 1)
|
|
self.assertEqual(response.items[0].incoming_contents, ["你好", "第二条"])
|
|
self.assertEqual(response.items[0].message_count, 2)
|
|
self.assertEqual(response.items[0].replies, ["您好"])
|
|
|
|
async def test_send_now_returns_shifted_count(self):
|
|
service = _FakeService(1)
|
|
main.manager.workers = {
|
|
1: SimpleNamespace(is_running=True, _im_service=service),
|
|
}
|
|
with (
|
|
patch.object(main, "get_owned_account", AsyncMock(return_value=SimpleNamespace(id=1))),
|
|
patch.object(main.system_logger, "record"),
|
|
):
|
|
response = await main.send_account_queued_reply_now(
|
|
account_id=1,
|
|
job_id="job-1",
|
|
db=object(),
|
|
user=SimpleNamespace(id=10, role="operator"),
|
|
)
|
|
|
|
self.assertEqual(response.status, "accepted")
|
|
self.assertEqual(response.shifted_count, 2)
|
|
|
|
async def test_cross_account_or_finished_job_returns_not_found(self):
|
|
service = _FakeService(1, action={"status": "not_found"})
|
|
main.manager.workers = {
|
|
1: SimpleNamespace(is_running=True, _im_service=service),
|
|
}
|
|
with patch.object(
|
|
main,
|
|
"get_owned_account",
|
|
AsyncMock(return_value=SimpleNamespace(id=1)),
|
|
):
|
|
with self.assertRaises(main.HTTPException) as caught:
|
|
await main.send_account_queued_reply_now(
|
|
account_id=1,
|
|
job_id="other-account-job",
|
|
db=object(),
|
|
user=SimpleNamespace(id=10, role="operator"),
|
|
)
|
|
|
|
self.assertEqual(caught.exception.status_code, 404)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|