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

891 lines
31 KiB
Python

from __future__ import annotations
import asyncio
from unittest.mock import AsyncMock, MagicMock
import pytest
from yuxi.channels.models import (
ChannelIdentity,
ChannelMessage,
ChannelResponse,
ChannelStatus,
ChannelType,
ChatType,
DeliveryResult,
MessageType,
)
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
class TestWeChatAdapter:
@pytest.fixture
def wecom_config(self):
return {
"mode": "wecom",
"corp_id": "test_corp_id",
"corp_secret": "test_corp_secret",
"agent_id": "1000001",
"dm_policy": "open",
}
@pytest.fixture
def mp_config(self):
return {
"mode": "mp",
"app_id": "test_app_id",
"app_secret": "test_app_secret",
"dm_policy": "open",
}
@pytest.fixture
def personal_config(self):
return {
"mode": "personal",
"bridge_url": "http://localhost:5555",
"dm_policy": "open",
}
def test_channel_id(self):
adapter = WeChatAdapter({"dm_policy": "open"})
assert adapter.channel_id == "wechat"
def test_channel_type(self):
adapter = WeChatAdapter({"dm_policy": "open"})
assert adapter.channel_type == ChannelType.WECHAT
def test_capabilities(self):
adapter = WeChatAdapter({"dm_policy": "open"})
assert adapter.supports_streaming is False
assert adapter.supports_markdown is False
assert adapter.text_chunk_limit == 2048
assert adapter.max_media_size_mb == 20
def test_mode_selection_wecom(self, wecom_config):
adapter = WeChatAdapter(wecom_config)
assert adapter._mode == "personal"
def test_mode_selection_mp(self, mp_config):
adapter = WeChatAdapter(mp_config)
assert adapter._mode == "personal"
def test_mode_selection_personal(self, personal_config):
adapter = WeChatAdapter(personal_config)
assert adapter._mode == "personal"
class TestNormalizeInbound:
@pytest.fixture
def wecom_adapter(self):
config = {"corp_id": "test", "corp_secret": "test", "agent_id": "1000001", "dm_policy": "open"}
adapter = WeChatAdapter(config)
adapter._mode = "wecom"
return adapter
@pytest.fixture
def mp_adapter(self):
config = {"app_id": "test", "app_secret": "test", "dm_policy": "open"}
adapter = WeChatAdapter(config)
adapter._mode = "mp"
return adapter
@pytest.fixture
def personal_adapter(self):
config = {"bridge_url": "http://localhost:5555", "dm_policy": "open"}
adapter = WeChatAdapter(config)
adapter._mode = "personal"
return adapter
def test_normalize_wecom_text_message(self, wecom_adapter):
payload = {
"FromUserName": "user123",
"ToUserName": "agent456",
"MsgId": "789012",
"MsgType": "text",
"Content": "你好",
}
result = wecom_adapter.normalize_inbound(payload)
assert isinstance(result, ChannelMessage)
assert result.content == "你好"
assert result.message_type == MessageType.TEXT
assert result.identity.channel_user_id == "user123"
assert result.identity.channel_message_id == "789012"
assert result.metadata["wechat_mode"] == "wecom"
def test_normalize_wecom_image_message(self, wecom_adapter):
payload = {
"FromUserName": "user123",
"ToUserName": "agent456",
"MsgId": "789013",
"MsgType": "image",
"MediaId": "media_abc",
"PicUrl": "http://example.com/pic.jpg",
}
result = wecom_adapter.normalize_inbound(payload)
assert result.message_type == MessageType.IMAGE
assert len(result.attachments) == 1
assert result.attachments[0].type == "image"
assert result.attachments[0].file_id == "media_abc"
def test_normalize_mp_text_message(self, mp_adapter):
payload = {
"FromUserName": "openid_abc",
"ToUserName": "mp_appid_xyz",
"MsgId": "123456",
"MsgType": "text",
"Content": "你好公众号",
}
result = mp_adapter.normalize_inbound(payload)
assert result.content == "你好公众号"
assert result.message_type == MessageType.TEXT
assert result.identity.channel_user_id == "openid_abc"
assert result.identity.channel_chat_id == "openid_abc"
assert result.metadata["wechat_mode"] == "mp"
def test_normalize_bridge_direct_message(self, personal_adapter):
payload = {
"sender_id": "wxid_abc",
"chat_id": "wxid_abc",
"msg_id": "111",
"content": "你好",
"is_group": False,
"msg_type": 1,
}
result = personal_adapter.normalize_inbound(payload)
assert result.content == "你好"
assert result.message_type == MessageType.TEXT
assert result.chat_type == ChatType.DIRECT
assert result.identity.channel_user_id == "wxid_abc"
assert result.metadata["wechat_mode"] == "personal"
def test_normalize_bridge_group_message(self, personal_adapter):
payload = {
"sender_id": "wxid_abc",
"chat_id": "room_123",
"msg_id": "222",
"content": "群聊消息",
"is_group": True,
"msg_type": 1,
}
result = personal_adapter.normalize_inbound(payload)
assert result.content == "群聊消息"
assert result.chat_type == ChatType.GROUP
assert result.identity.channel_chat_id == "room_123"
assert result.metadata["room_id"] == "room_123"
assert result.metadata["sender_wxid"] == "wxid_abc"
def test_normalize_webhook_xml_body(self, wecom_adapter):
xml_body = (
"<xml><ToUserName>agent</ToUserName>"
"<FromUserName>user</FromUserName>"
"<MsgType>text</MsgType>"
"<Content>test</Content>"
"<MsgId>123</MsgId></xml>"
)
result = wecom_adapter.normalize_inbound(xml_body)
assert result.content == "test"
assert result.identity.channel_user_id == "user"
assert result.metadata["wechat_mode"] == "wecom"
class TestFormatOutbound:
@pytest.fixture
def wecom_adapter(self):
config = {"corp_id": "test", "corp_secret": "test", "agent_id": "1000001", "dm_policy": "open"}
adapter = WeChatAdapter(config)
adapter._mode = "wecom"
return adapter
@pytest.fixture
def mp_adapter(self):
config = {"app_id": "test", "app_secret": "test", "dm_policy": "open"}
adapter = WeChatAdapter(config)
adapter._mode = "mp"
return adapter
@pytest.fixture
def personal_adapter(self):
config = {"bridge_url": "http://localhost:5555", "dm_policy": "open"}
adapter = WeChatAdapter(config)
adapter._mode = "personal"
return adapter
def test_format_wecom_direct_message(self, wecom_adapter):
response = ChannelResponse(
identity=ChannelIdentity(
channel_id="wechat",
channel_type=ChannelType.WECHAT,
channel_user_id="user123",
channel_chat_id="user123",
),
content="回复消息",
metadata={"chat_type": "direct"},
)
payload = wecom_adapter.format_outbound(response)
assert payload["msgtype"] == "text"
assert payload["touser"] == "user123"
assert payload["agentid"] == "1000001"
assert payload["text"]["content"] == "回复消息"
def test_format_mp_message(self, mp_adapter):
response = ChannelResponse(
identity=ChannelIdentity(
channel_id="wechat",
channel_type=ChannelType.WECHAT,
channel_user_id="openid_abc",
channel_chat_id="openid_abc",
),
content="公众号回复",
)
payload = mp_adapter.format_outbound(response)
assert payload["touser"] == "openid_abc"
assert payload["msgtype"] == "text"
assert payload["text"]["content"] == "公众号回复"
def test_format_bridge_direct_message(self, personal_adapter):
response = ChannelResponse(
identity=ChannelIdentity(
channel_id="wechat",
channel_type=ChannelType.WECHAT,
channel_user_id="wxid_abc",
channel_chat_id="wxid_abc",
),
content="桥接回复",
metadata={"chat_type": "direct"},
)
payload = personal_adapter.format_outbound(response)
assert payload["chat_id"] == "wxid_abc"
assert payload["is_group"] is False
assert payload["content"] == "桥接回复"
def test_format_bridge_group_message(self, personal_adapter):
response = ChannelResponse(
identity=ChannelIdentity(
channel_id="wechat",
channel_type=ChannelType.WECHAT,
channel_user_id="wxid_abc",
channel_chat_id="room_123",
),
content="群聊回复",
metadata={"chat_type": "group"},
)
payload = personal_adapter.format_outbound(response)
assert payload["chat_id"] == "room_123"
assert payload["is_group"] is True
def test_text_truncation(self, wecom_adapter):
long_content = "A" * 3000
response = ChannelResponse(
identity=ChannelIdentity(
channel_id="wechat",
channel_type=ChannelType.WECHAT,
channel_user_id="user123",
channel_chat_id="user123",
),
content=long_content,
metadata={"chat_type": "direct"},
)
payload = wecom_adapter.format_outbound(response)
assert len(payload["text"]["content"]) <= 2048
class TestSecurity:
@pytest.fixture
def adapter(self):
config = {"dm_policy": "open", "allow_from": ["wx:allowed_user"]}
return WeChatAdapter(config)
def test_dm_policy_open(self):
config = {"dm_policy": "open"}
adapter = WeChatAdapter(config)
assert adapter._check_dm_policy("any_user") is True
def test_dm_policy_disabled(self):
config = {"dm_policy": "disabled"}
adapter = WeChatAdapter(config)
assert adapter._check_dm_policy("any_user") is False
def test_dm_policy_allowlist_allowed(self):
config = {"dm_policy": "allowlist", "allow_from": ["wx:allowed_user"]}
adapter = WeChatAdapter(config)
assert adapter._check_dm_policy("allowed_user") is True
def test_dm_policy_allowlist_denied(self):
config = {"dm_policy": "allowlist", "allow_from": ["wx:other_user"]}
adapter = WeChatAdapter(config)
assert adapter._check_dm_policy("denied_user") is False
def test_group_policy_open(self):
config = {"group_policy": "open"}
adapter = WeChatAdapter(config)
assert adapter._check_group_policy("any_chat", "any_user") is True
def test_group_policy_disabled(self):
config = {"group_policy": "disabled"}
adapter = WeChatAdapter(config)
assert adapter._check_group_policy("any_chat", "any_user") is False
def test_group_policy_allowlist_global(self):
config = {
"group_policy": "allowlist",
"group_allow_from": ["wx:allowed_user"],
}
adapter = WeChatAdapter(config)
assert adapter._check_group_policy("some_chat", "allowed_user") is True
def test_at_bot_wecom(self):
adapter = WeChatAdapter({"dm_policy": "open"})
adapter._mode = "wecom"
adapter.config["agent_name"] = "MyBot"
payload = {"Content": "@MyBot 你好"}
assert adapter._is_at_bot(payload) is True
def test_at_bot_personal(self):
adapter = WeChatAdapter({"dm_policy": "open"})
adapter._mode = "personal"
payload = {"at_list": ["wxid_bot"]}
assert adapter._is_at_bot(payload) is True
class TestHealthCheck:
@pytest.fixture
async def adapter(self):
config = {"corp_id": "test", "corp_secret": "test", "agent_id": "1000001", "dm_policy": "open"}
adapter = WeChatAdapter(config)
adapter._mode = "wecom"
adapter._http_client = AsyncMock()
return adapter
@pytest.mark.asyncio
async def test_health_check_no_client(self):
adapter = WeChatAdapter({"dm_policy": "open"})
result = await adapter.health_check()
assert result.status == "unhealthy"
assert "HTTP client not initialized" in (result.last_error or "")
@pytest.mark.asyncio
async def test_health_check_wecom_success(self):
config = {"corp_id": "test", "corp_secret": "test", "agent_id": "1000001", "dm_policy": "open"}
adapter = WeChatAdapter(config)
adapter._mode = "wecom"
mock_resp = MagicMock()
mock_resp.json.return_value = {"access_token": "test_token", "expires_in": 7200}
mock_resp.headers = {"content-type": "application/json"}
mock_client = AsyncMock()
mock_client.get.return_value = mock_resp
adapter._http_client = mock_client
result = await adapter.health_check()
assert result.status == "healthy"
class TestPreConnect:
@pytest.fixture
def adapter(self):
config = {"dm_policy": "open"}
return WeChatAdapter(config)
@pytest.mark.asyncio
async def test_pre_connect_wecom(self, adapter):
adapter._mode = "wecom"
result = await adapter.pre_connect()
assert result["status"] == "ready"
@pytest.mark.asyncio
async def test_pre_connect_mp(self, adapter):
adapter._mode = "mp"
result = await adapter.pre_connect()
assert result["status"] == "pending_scan"
class TestCapabilitiesUpdated:
def test_tts_voice_enabled(self):
from yuxi.channels.adapters.wechat.capabilities import WECHAT_CAPABILITIES
assert WECHAT_CAPABILITIES["tts"]["voice"]["enabled"] is True
assert WECHAT_CAPABILITIES["tts"]["voice"]["synthesis_target"] == "voice-note"
def test_reply_enabled(self):
from yuxi.channels.adapters.wechat.capabilities import WECHAT_CAPABILITIES
assert WECHAT_CAPABILITIES["reply"] is True
def test_group_management_enabled(self):
from yuxi.channels.adapters.wechat.capabilities import WECHAT_CAPABILITIES
assert WECHAT_CAPABILITIES["groupManagement"] is True
class TestReplySupport:
@pytest.fixture
def wecom_adapter(self):
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
config = {"corp_id": "test", "corp_secret": "test", "agent_id": "1000001", "dm_policy": "open"}
adapter = WeChatAdapter(config)
adapter._mode = "wecom"
adapter._wecom_client = MagicMock()
adapter._http_client = AsyncMock()
return adapter
@pytest.fixture
def mp_adapter(self):
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
config = {"app_id": "test", "app_secret": "test", "dm_policy": "open"}
adapter = WeChatAdapter(config)
adapter._mode = "mp"
adapter._mp_client = MagicMock()
adapter._http_client = AsyncMock()
return adapter
@pytest.mark.asyncio
async def test_wecom_format_reply_with_context(self, wecom_adapter):
response = ChannelResponse(
identity=ChannelIdentity(
channel_id="wechat",
channel_type=ChannelType.WECHAT,
channel_user_id="user123",
channel_chat_id="user123",
),
content="回复消息",
reply_to_message_id="msg_456",
metadata={"reply_to_channel_user_id": "original_sender"},
)
payload = wecom_adapter.format_outbound(response)
assert payload is not None
@pytest.mark.asyncio
async def test_mp_format_reply_with_context(self, mp_adapter):
response = ChannelResponse(
identity=ChannelIdentity(
channel_id="wechat",
channel_type=ChannelType.WECHAT,
channel_user_id="openid_abc",
channel_chat_id="openid_abc",
),
content="回复消息",
reply_to_message_id="msg_789",
metadata={"reply_to_channel_user_id": "original_sender"},
)
payload = mp_adapter.format_outbound(response)
assert payload["touser"] == "openid_abc"
def test_normalize_bridge_reply_id(self):
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"})
adapter._mode = "personal"
payload = {
"sender_id": "wxid_abc",
"chat_id": "wxid_abc",
"msg_id": "333",
"content": "回复",
"is_group": False,
"msg_type": 1,
"reply_to_msg_id": "orig_222",
}
result = adapter.normalize_inbound(payload)
assert result.reply_to_message_id == "orig_222"
class TestBannedDetection:
@pytest.fixture
def adapter(self):
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
config = {"corp_id": "test", "corp_secret": "test", "agent_id": "1000001", "dm_policy": "open"}
return WeChatAdapter(config)
def test_banned_initial_state(self, adapter):
assert adapter.is_banned is False
assert adapter.banned_reason is None
def test_check_banned_response_48001(self, adapter):
result = DeliveryResult(success=False, error="errcode=48001 API unauthorized")
adapter._check_banned_response(result)
assert adapter.is_banned is True
assert "48001" in adapter.banned_reason
assert adapter._ban_permanent is False
assert adapter._ban_attempts == 1
assert adapter._ban_cooldown_until is not None
assert adapter._status.value == "error"
def test_check_banned_response_progressive_permanent(self, adapter):
adapter._ban_backoff_intervals = [1, 5]
result = DeliveryResult(success=False, error="errcode=48001 API unauthorized")
adapter._check_banned_response(result)
assert adapter._ban_permanent is False
assert adapter._ban_attempts == 1
adapter._check_banned_response(result)
assert adapter._ban_permanent is False
assert adapter._ban_attempts == 2
adapter._check_banned_response(result)
assert adapter._ban_permanent is True
assert adapter._ban_attempts == 3
assert adapter._status.value == "disabled"
def test_unban_channel_temporary(self, adapter):
adapter._banned = True
adapter._banned_reason = "48001"
adapter._ban_permanent = False
adapter._ban_cooldown_until = 1000.0
result = adapter.unban_channel()
assert result is True
assert adapter._banned is False
assert adapter._banned_reason is None
assert adapter._ban_cooldown_until is None
def test_unban_channel_permanent(self, adapter):
adapter._banned = True
adapter._ban_permanent = True
result = adapter.unban_channel()
assert result is False
assert adapter._banned is True
def test_ban_status_property(self, adapter):
adapter._banned = True
adapter._banned_reason = "48001"
adapter._ban_permanent = False
adapter._ban_attempts = 1
adapter._ban_cooldown_until = 9999999999.0
status = adapter.ban_status
assert status["banned"] is True
assert status["permanent"] is False
assert status["attempts"] == 1
assert status["cooldown_remaining_seconds"] is not None
def test_check_ban_cooldown_expired(self, adapter):
import time
adapter._banned = True
adapter._ban_permanent = False
adapter._ban_attempts = 1
adapter._ban_cooldown_until = time.time() - 10
adapter._check_ban_cooldown()
assert adapter._banned is False
assert adapter._ban_cooldown_until is None
class TestAutoJoin:
@pytest.fixture
def wecom_adapter(self):
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
config = {
"corp_id": "test",
"corp_secret": "test",
"agent_id": "1000001",
"dm_policy": "open",
"auto_join_groups": ["chat_1", "chat_2"],
}
adapter = WeChatAdapter(config)
adapter._mode = "wecom"
adapter._wecom_client = MagicMock()
adapter._wecom_client.get_access_token = AsyncMock(return_value="test_token")
adapter._http_client = AsyncMock()
return adapter
@pytest.mark.asyncio
async def test_auto_join_groups_wecom_existing(self, wecom_adapter):
get_resp = MagicMock()
get_resp.json.return_value = {"errcode": 0, "chat_info": {"chatid": "chat_1"}}
wecom_adapter._http_client.get = AsyncMock(return_value=get_resp)
await wecom_adapter._auto_join_groups()
@pytest.mark.asyncio
async def test_auto_join_groups_disabled_when_empty(self):
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"})
adapter._mode = "personal"
adapter._http_client = AsyncMock()
await adapter._auto_join_groups()
class TestAutoDerive:
def test_auto_derive_disabled_by_default(self):
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"})
assert adapter._should_auto_derive() is False
def test_auto_derive_enabled_with_config(self):
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
config = {
"bridge_url": "http://localhost:5555",
"dm_policy": "open",
"auto_derive_sessions": True,
}
adapter = WeChatAdapter(config)
assert adapter._should_auto_derive() is True
def test_auto_derive_key_format(self):
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"})
adapter._mode = "personal"
key = adapter._get_auto_derive_key("room_123", "sender_abc")
assert "personal" in key
assert "room_123" in key
assert "sender_abc" in key
def test_auto_derive_metadata_in_bridge_group(self):
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
config = {
"bridge_url": "http://localhost:5555",
"dm_policy": "open",
"auto_derive_sessions": True,
}
adapter = WeChatAdapter(config)
adapter._mode = "personal"
payload = {
"sender_id": "wxid_abc",
"chat_id": "room_123",
"msg_id": "555",
"content": "群聊消息",
"is_group": True,
"msg_type": 1,
}
result = adapter.normalize_inbound(payload)
assert result.metadata.get("auto_derive_session") is True
assert "auto_derive_key" in result.metadata
class TestLogoutAccount:
@pytest.fixture
def adapter(self):
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
config = {"corp_id": "test", "corp_secret": "test", "agent_id": "1000001", "dm_policy": "open"}
adapter = WeChatAdapter(config)
adapter._mode = "wecom"
adapter._wecom_client = MagicMock()
adapter._wecom_client.invalidate_token = MagicMock()
return adapter
@pytest.mark.asyncio
async def test_logout_clears_wecom_client(self, adapter):
await adapter.logout_account()
assert adapter._wecom_client is None
@pytest.mark.asyncio
async def test_logout_returns_result(self, adapter):
result = await adapter.logout_account()
assert "cleared" in result
assert "loggedOut" in result
class TestReadMessage:
@pytest.mark.asyncio
async def test_read_message_wecom(self):
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
adapter = WeChatAdapter({"corp_id": "test", "corp_secret": "test", "agent_id": "1000001", "dm_policy": "open"})
adapter._mode = "wecom"
adapter._wecom_client = MagicMock()
adapter._wecom_client.get_access_token = AsyncMock(return_value="token")
adapter._http_client = AsyncMock()
mock_head_resp = MagicMock()
mock_head_resp.status_code = 200
adapter._http_client.head = AsyncMock(return_value=mock_head_resp)
result = await adapter.read_message("media_123")
assert result["msg_id"] == "media_123"
assert result["available"] is True
assert "read_at" in result
@pytest.mark.asyncio
async def test_read_message_unavailable(self):
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
adapter = WeChatAdapter({"app_id": "test", "app_secret": "test", "dm_policy": "open"})
adapter._mode = "mp"
adapter._mp_client = MagicMock()
adapter._mp_client.get_access_token = AsyncMock(return_value="token")
adapter._http_client = AsyncMock()
mock_head_resp = MagicMock()
mock_head_resp.status_code = 404
adapter._http_client.head = AsyncMock(return_value=mock_head_resp)
result = await adapter.read_message("expired_media")
assert result["msg_id"] == "expired_media"
assert result["available"] is False
def test_read_action_is_implemented(self):
from yuxi.channels.adapters.wechat.message_actions import WECHAT_MESSAGE_ACTIONS
assert WECHAT_MESSAGE_ACTIONS["read"]["status"] == "implemented"
class TestConnectionLifecycle:
@pytest.fixture
def wecom_config(self):
return {
"corp_id": "test_corp",
"corp_secret": "test_secret",
"agent_id": "1000001",
"dm_policy": "open",
}
@pytest.fixture
def personal_config(self):
return {
"bridge_url": "http://localhost:5555",
"dm_policy": "open",
}
def test_initial_status_disconnected(self):
adapter = WeChatAdapter({"dm_policy": "open"})
assert adapter._status == ChannelStatus.DISCONNECTED
def test_connect_twice_idempotent(self, wecom_config):
adapter = WeChatAdapter(wecom_config)
adapter._mode = "wecom"
adapter._status = ChannelStatus.CONNECTED
@pytest.mark.asyncio
async def test_disconnect_cleans_up(self, wecom_config):
adapter = WeChatAdapter(wecom_config)
adapter._mode = "wecom"
adapter._http_client = AsyncMock()
adapter._status = ChannelStatus.CONNECTED
await adapter.disconnect()
assert adapter._status == ChannelStatus.DISCONNECTED
assert adapter._http_client is None
@pytest.mark.asyncio
async def test_disconnect_already_disconnected(self):
adapter = WeChatAdapter({"dm_policy": "open"})
adapter._status = ChannelStatus.DISCONNECTED
await adapter.disconnect()
assert adapter._status == ChannelStatus.DISCONNECTED
@pytest.mark.asyncio
async def test_disconnect_cancels_polling_task(self, personal_config):
adapter = WeChatAdapter(personal_config)
adapter._mode = "personal"
adapter._http_client = AsyncMock()
adapter._status = ChannelStatus.CONNECTED
async def mock_loop(_interval):
try:
while True:
await asyncio.sleep(10)
except asyncio.CancelledError:
raise
adapter._polling_task = asyncio.create_task(mock_loop(1.0))
await asyncio.sleep(0.01)
await adapter.disconnect()
assert adapter._polling_task is None
assert adapter._status == ChannelStatus.DISCONNECTED
@pytest.mark.asyncio
async def test_pre_connect_wecom(self):
adapter = WeChatAdapter({"corp_id": "test", "corp_secret": "test", "agent_id": "1", "dm_policy": "open"})
adapter._mode = "wecom"
result = await adapter.pre_connect()
assert result["status"] == "ready"
assert "token" in result["message"].lower()
@pytest.mark.asyncio
async def test_pre_connect_mp(self):
adapter = WeChatAdapter({"app_id": "test", "app_secret": "test", "dm_policy": "open"})
adapter._mode = "mp"
result = await adapter.pre_connect()
assert result["status"] == "pending_scan"
@pytest.mark.asyncio
async def test_pre_connect_unknown_mode(self):
adapter = WeChatAdapter({"dm_policy": "open"})
adapter._mode = "personal"
result = await adapter.pre_connect()
assert result["status"] in ("error", "pending_scan")
@pytest.mark.asyncio
async def test_login_with_qr_wait_wecom(self):
adapter = WeChatAdapter({"corp_id": "test", "corp_secret": "test", "agent_id": "1", "dm_policy": "open"})
adapter._mode = "wecom"
result = await adapter.login_with_qr_wait()
assert result["status"] == "ready"
@pytest.mark.asyncio
async def test_login_with_qr_wait_mp(self):
adapter = WeChatAdapter({"app_id": "test", "app_secret": "test", "dm_policy": "open"})
adapter._mode = "mp"
result = await adapter.login_with_qr_wait()
assert result["status"] == "ready"
def test_state_snapshot_disconnected(self):
adapter = WeChatAdapter({"dm_policy": "open"})
snapshot = adapter.state_snapshot
assert snapshot.configured is True
assert snapshot.connected is False
def test_resolve_config_from_env(self, monkeypatch):
monkeypatch.setenv("WECHAT_BRIDGE_URL", "http://bridge.example.com")
adapter = WeChatAdapter({"dm_policy": "open"})
value = adapter._resolve_config("bridge_url", "WECHAT_BRIDGE_URL")
assert value == "http://bridge.example.com"
def test_resolve_config_from_config_first(self):
adapter = WeChatAdapter({"bridge_url": "http://cfg.example.com", "dm_policy": "open"})
value = adapter._resolve_config("bridge_url", "WECHAT_BRIDGE_URL")
assert value == "http://cfg.example.com"
def test_logout_clears_all_clients(self):
adapter = WeChatAdapter({"corp_id": "test", "corp_secret": "test", "agent_id": "1", "dm_policy": "open"})
adapter._mode = "wecom"
adapter._wecom_client = MagicMock()
adapter._mp_client = MagicMock()
adapter._bridge_client = MagicMock()
import asyncio
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
result = loop.run_until_complete(adapter.logout_account())
assert adapter._wecom_client is None
assert adapter._mp_client is None
assert adapter._bridge_client is None
finally:
loop.close()