ForcePilot/backend/test/unit/channels/test_wechat_bridge.py
Kris 3264900bc9 test: 新增多渠道单元测试用例并配置测试环境变量
新增了Twitch、Telegram、Discord、Slack、Mattermost、WeChat、Zalo等多渠道的单元测试用例,覆盖了令牌处理、速率限制、消息去重、会话解析、格式转换、安全策略等模块
同时在测试配置中添加了测试用的OpenAI API密钥环境变量
2026-05-12 00:56:47 +08:00

277 lines
9.6 KiB
Python

from __future__ import annotations
import asyncio
import time
from unittest.mock import AsyncMock, MagicMock
import pytest
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
from yuxi.channels.adapters.wechat.bridge import BridgeClient
from yuxi.channels.models import (
ChannelIdentity,
ChannelMessage,
ChannelResponse,
ChannelStatus,
ChannelType,
ChatType,
DeliveryResult,
MessageType,
)
class TestBridgeClient:
@pytest.fixture
def http_client(self):
return AsyncMock()
@pytest.fixture
def bridge_client(self, http_client):
return BridgeClient(http_client, "http://localhost:5555")
@pytest.mark.asyncio
async def test_health_check_healthy(self, bridge_client, http_client):
mock_resp = MagicMock()
mock_resp.status_code = 200
http_client.get = AsyncMock(return_value=mock_resp)
result = await bridge_client.health_check()
assert result is True
@pytest.mark.asyncio
async def test_health_check_unhealthy(self, bridge_client, http_client):
http_client.get = AsyncMock(side_effect=Exception("connection refused"))
result = await bridge_client.health_check()
assert result is False
@pytest.mark.asyncio
async def test_get_login_status_logged_in(self, bridge_client, http_client):
mock_resp = MagicMock()
mock_resp.json.return_value = {"logged_in": True, "nickname": "test_user"}
http_client.get = AsyncMock(return_value=mock_resp)
result = await bridge_client.get_login_status()
assert result["logged_in"] is True
@pytest.mark.asyncio
async def test_get_login_status_error(self, bridge_client, http_client):
http_client.get = AsyncMock(side_effect=Exception("timeout"))
result = await bridge_client.get_login_status()
assert result["logged_in"] is False
@pytest.mark.asyncio
async def test_send_message_success(self, bridge_client, http_client):
mock_resp = MagicMock()
mock_resp.json.return_value = {"success": True, "msg_id": "bridge_msg_001"}
http_client.post = AsyncMock(return_value=mock_resp)
result = await bridge_client.send_message({"content": "hello"})
assert result.success is True
assert result.message_id == "bridge_msg_001"
@pytest.mark.asyncio
async def test_send_message_failure(self, bridge_client, http_client):
mock_resp = MagicMock()
mock_resp.json.return_value = {"success": False, "error": "not logged in"}
http_client.post = AsyncMock(return_value=mock_resp)
result = await bridge_client.send_message({"content": "hello"})
assert result.success is False
assert "not logged in" in str(result.error)
@pytest.mark.asyncio
async def test_send_media_message(self, bridge_client, http_client):
mock_resp = MagicMock()
mock_resp.json.return_value = {"success": True, "msg_id": "media_001"}
http_client.post = AsyncMock(return_value=mock_resp)
result = await bridge_client.send_media_message({"image_data": "base64..."})
assert result.success is True
@pytest.mark.asyncio
async def test_fetch_events_with_messages(self, bridge_client, http_client):
mock_resp = MagicMock()
mock_resp.json.return_value = [
{"type": "message", "data": {"sender_id": "u1", "content": "hi", "msg_id": "1", "msg_type": 1, "is_group": False}},
]
http_client.get = AsyncMock(return_value=mock_resp)
events = await bridge_client.fetch_events()
assert len(events) == 1
assert events[0]["type"] == "message"
@pytest.mark.asyncio
async def test_fetch_events_empty(self, bridge_client, http_client):
mock_resp = MagicMock()
mock_resp.json.return_value = []
http_client.get = AsyncMock(return_value=mock_resp)
events = await bridge_client.fetch_events()
assert events == []
@pytest.mark.asyncio
async def test_fetch_events_error(self, bridge_client, http_client):
http_client.get = AsyncMock(side_effect=Exception("network error"))
events = await bridge_client.fetch_events()
assert events == []
@pytest.mark.asyncio
async def test_get_qr_code(self, bridge_client, http_client):
mock_resp = MagicMock()
mock_resp.json.return_value = {"qr_url": "http://qr.example.com/code"}
http_client.post = AsyncMock(return_value=mock_resp)
result = await bridge_client.get_qr_code()
assert result["qr_url"] == "http://qr.example.com/code"
class TestBridgeEventLoop:
@pytest.fixture
def adapter(self):
config = {"bridge_url": "http://localhost:5555", "dm_policy": "open", "poll_interval": 0.1}
adapter = WeChatAdapter(config)
adapter._mode = "personal"
adapter._bridge_client = MagicMock()
adapter._bridge_client.health_check = AsyncMock(return_value=True)
adapter._bridge_client.fetch_events = AsyncMock(return_value=[])
adapter._bridge_client.send_message = AsyncMock(
return_value=DeliveryResult(success=True, message_id="test_id")
)
adapter._http_client = AsyncMock()
adapter._polling_lease._active = True
adapter._polling_lease._last_renew = time.monotonic()
return adapter
@pytest.mark.asyncio
async def test_event_loop_stops_on_request(self, adapter):
adapter._status = ChannelStatus.CONNECTED
task = asyncio.create_task(adapter._bridge_event_loop(0.1))
await asyncio.sleep(0.05)
adapter._status = ChannelStatus.DISCONNECTED
try:
await asyncio.wait_for(task, timeout=1.0)
except asyncio.TimeoutError:
task.cancel()
with pytest.raises(asyncio.CancelledError):
await task
@pytest.mark.asyncio
async def test_event_loop_stops_when_lease_lost(self, adapter):
adapter._status = ChannelStatus.CONNECTED
adapter._polling_lease._active = False
task = asyncio.create_task(adapter._bridge_event_loop(0.05))
try:
await asyncio.wait_for(task, timeout=1.0)
except asyncio.TimeoutError:
task.cancel()
with pytest.raises(asyncio.CancelledError):
await task
@pytest.mark.asyncio
async def test_event_loop_health_check_failure(self, adapter):
adapter._status = ChannelStatus.CONNECTED
health_calls = []
async def failing_health():
health_calls.append(1)
return False
adapter._bridge_client.health_check = failing_health
adapter._bridge_client.fetch_events = AsyncMock(return_value=[])
task = asyncio.create_task(adapter._bridge_event_loop(0.1))
await asyncio.sleep(0.15)
adapter._status = ChannelStatus.DISCONNECTED
try:
await asyncio.wait_for(task, timeout=1.0)
except asyncio.TimeoutError:
task.cancel()
with pytest.raises(asyncio.CancelledError):
await task
assert len(health_calls) >= 1
@pytest.mark.asyncio
async def test_event_loop_message_delivery(self, adapter):
adapter._status = ChannelStatus.CONNECTED
message_count = 0
async def mock_handle(msg):
nonlocal message_count
message_count += 1
adapter._handle_message = mock_handle
adapter._bridge_client.fetch_events = AsyncMock(return_value=[
{"type": "message", "data": {"sender_id": "u1", "chat_id": "u1", "msg_id": "1", "content": "hello", "is_group": False, "msg_type": 1}},
])
task = asyncio.create_task(adapter._bridge_event_loop(0.1))
await asyncio.sleep(0.15)
adapter._status = ChannelStatus.DISCONNECTED
try:
await asyncio.wait_for(task, timeout=1.0)
except asyncio.TimeoutError:
task.cancel()
with pytest.raises(asyncio.CancelledError):
await task
assert message_count >= 1
@pytest.mark.asyncio
async def test_event_loop_network_error_resilience(self, adapter):
adapter._status = ChannelStatus.CONNECTED
call_count = 0
async def flaky_fetch():
nonlocal call_count
call_count += 1
if call_count == 1:
raise Exception("network error")
return []
adapter._bridge_client.fetch_events = flaky_fetch
task = asyncio.create_task(adapter._bridge_event_loop(0.05))
await asyncio.sleep(0.15)
adapter._status = ChannelStatus.DISCONNECTED
try:
await asyncio.wait_for(task, timeout=1.0)
except asyncio.TimeoutError:
task.cancel()
with pytest.raises(asyncio.CancelledError):
await task
assert call_count >= 2
@pytest.mark.asyncio
async def test_send_bridge_group_message_at_list(self, adapter):
response = ChannelResponse(
identity=ChannelIdentity(
channel_id="wechat",
channel_type=ChannelType.WECHAT,
channel_user_id="sender_wxid",
channel_chat_id="room_123",
),
content="群聊回复",
metadata={
"chat_type": "group",
"reply_to_message_id": "msg_orig",
"sender_wxid": "original_sender",
},
)
adapter.config["reply_to_mode"] = "first"
payload = adapter._format_bridge_text(response)
assert payload["is_group"] is True
assert payload["chat_id"] == "room_123"
assert "original_sender" in payload.get("at_list", [])
assert "@original_sender" in payload["content"]