ForcePilot/backend/test/unit/channels/test_wechat_optimization.py

504 lines
19 KiB
Python
Raw Normal View History

from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from yuxi.channels.models import DeliveryResult
class TestMessageActionAdapterWithAdapter:
def test_get_handler_returns_none_without_adapter(self):
from yuxi.channels.adapters.wechat.message_actions import WeChatMessageActionAdapter
adapter = WeChatMessageActionAdapter()
assert adapter.get_handler("send") is None
assert adapter.get_handler("reply") is None
assert adapter.get_handler("react") is None
def test_get_handler_returns_callable_with_adapter(self):
from yuxi.channels.adapters.wechat.message_actions import WeChatMessageActionAdapter
mock_wechat = MagicMock()
mock_wechat.channel_id = "wechat"
mock_wechat.channel_type = "wechat"
action_adapter = WeChatMessageActionAdapter(mock_wechat)
handler = action_adapter.get_handler("send")
assert handler is not None
assert callable(handler)
handler = action_adapter.get_handler("reply")
assert handler is not None
assert callable(handler)
handler = action_adapter.get_handler("sendAttachment")
assert handler is not None
assert callable(handler)
def test_get_handler_returns_none_for_unsupported_actions(self):
from yuxi.channels.adapters.wechat.message_actions import WeChatMessageActionAdapter
mock_wechat = MagicMock()
mock_wechat.channel_id = "wechat"
mock_wechat.channel_type = "wechat"
action_adapter = WeChatMessageActionAdapter(mock_wechat)
assert action_adapter.get_handler("react") is None
assert action_adapter.get_handler("edit") is None
assert action_adapter.get_handler("unsend") is None
assert action_adapter.get_handler("delete") is None
def test_is_supported_works_without_adapter(self):
from yuxi.channels.adapters.wechat.message_actions import WeChatMessageActionAdapter
assert WeChatMessageActionAdapter.is_supported("send") is True
assert WeChatMessageActionAdapter.is_supported("reply") is True
assert WeChatMessageActionAdapter.is_supported("react") is False
assert WeChatMessageActionAdapter.is_supported("edit") is False
@pytest.mark.asyncio
async def test_send_handler_calls_adapter_send(self):
from yuxi.channels.adapters.wechat.message_actions import WeChatMessageActionAdapter
mock_wechat = MagicMock()
mock_wechat.channel_id = "wechat"
mock_wechat.channel_type = "wechat"
mock_wechat.send = AsyncMock(return_value=DeliveryResult(success=True))
action_adapter = WeChatMessageActionAdapter(mock_wechat)
handler = action_adapter.get_handler("send")
result = await handler(chat_id="test_chat", content="Hello World", channel_user_id="user_123")
assert result["success"] is True
mock_wechat.send.assert_called_once()
@pytest.mark.asyncio
async def test_send_handler_fails_without_chat_id(self):
from yuxi.channels.adapters.wechat.message_actions import WeChatMessageActionAdapter
mock_wechat = MagicMock()
mock_wechat.channel_id = "wechat"
mock_wechat.channel_type = "wechat"
action_adapter = WeChatMessageActionAdapter(mock_wechat)
handler = action_adapter.get_handler("send")
result = await handler(content="Hello World")
assert result["success"] is False
assert "missing chat_id" in result["error"]
@pytest.mark.asyncio
async def test_reply_handler_calls_adapter_send_with_reply_metadata(self):
from yuxi.channels.adapters.wechat.message_actions import WeChatMessageActionAdapter
mock_wechat = MagicMock()
mock_wechat.channel_id = "wechat"
mock_wechat.channel_type = "wechat"
mock_wechat.send = AsyncMock(return_value=DeliveryResult(success=True))
action_adapter = WeChatMessageActionAdapter(mock_wechat)
handler = action_adapter.get_handler("reply")
result = await handler(
chat_id="test_chat",
content="Reply message",
channel_user_id="user_123",
reply_to_msg_id="msg_456",
reply_to_channel_user_id="sender_789",
chat_type="direct",
)
assert result["success"] is True
mock_wechat.send.assert_called_once()
@pytest.mark.asyncio
async def test_download_file_handler_calls_download_media(self):
from yuxi.channels.adapters.wechat.message_actions import WeChatMessageActionAdapter
mock_data = b"fake_media_data"
mock_wechat = MagicMock()
mock_wechat.channel_id = "wechat"
mock_wechat.channel_type = "wechat"
mock_wechat.download_media = AsyncMock(return_value=mock_data)
action_adapter = WeChatMessageActionAdapter(mock_wechat)
handler = action_adapter.get_handler("download-file")
result = await handler(file_id="media_123")
assert result["success"] is True
assert result["data"] == mock_data
@pytest.mark.asyncio
async def test_download_file_handler_fails_without_file_id(self):
from yuxi.channels.adapters.wechat.message_actions import WeChatMessageActionAdapter
mock_wechat = MagicMock()
action_adapter = WeChatMessageActionAdapter(mock_wechat)
handler = action_adapter.get_handler("download-file")
result = await handler()
assert result["success"] is False
@pytest.mark.asyncio
async def test_read_handler_calls_read_message(self):
from yuxi.channels.adapters.wechat.message_actions import WeChatMessageActionAdapter
mock_wechat = MagicMock()
mock_wechat.channel_id = "wechat"
mock_wechat.channel_type = "wechat"
mock_wechat.read_message = AsyncMock(return_value={"msg_id": "123", "available": True})
action_adapter = WeChatMessageActionAdapter(mock_wechat)
handler = action_adapter.get_handler("read")
result = await handler(msg_id="123")
assert result["available"] is True
def test_list_supported_actions_static(self):
from yuxi.channels.adapters.wechat.message_actions import WeChatMessageActionAdapter
supported = WeChatMessageActionAdapter.list_supported_actions()
assert "send" in supported
assert "reply" in supported
assert "read" in supported
assert "react" not in supported
assert "edit" not in supported
def test_list_all_actions_static(self):
from yuxi.channels.adapters.wechat.message_actions import WeChatMessageActionAdapter
all_actions = WeChatMessageActionAdapter.list_all_actions()
assert len(all_actions) == 56
assert "send" in all_actions
assert "react" in all_actions
class TestWeChatHandleAction:
@pytest.mark.asyncio
async def test_handle_action_delegates_to_get_handler(self):
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
adapter = WeChatAdapter(config={})
adapter._mode = "wecom"
mock_client = MagicMock()
mock_client.get_access_token = AsyncMock(return_value="test_token")
adapter._wecom_client = mock_client
adapter._http_client = AsyncMock()
from yuxi.channels.adapters.wechat.wecom import WeComMonitor
class FakeMonitor:
async def start(self, cb):
pass
async def stop(self):
pass
adapter._monitor = FakeMonitor()
adapter._message_actions = MagicMock()
mock_handler = AsyncMock(return_value={"success": True})
adapter._message_actions.get_handler.return_value = mock_handler
adapter._message_actions.is_supported.return_value = True
from yuxi.channels.protocols.actions import ActionContext
ctx = ActionContext(
action="send",
channel_id="wechat",
chat_id="test_chat",
msg_id="msg_001",
args={"content": "Hello", "channel_user_id": "user_1"},
)
result = await adapter.handle_action(ctx)
assert result.success is True
adapter._message_actions.get_handler.assert_called_once_with("send")
mock_handler.assert_called_once()
@pytest.mark.asyncio
async def test_handle_action_returns_error_for_unsupported_action(self):
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
adapter = WeChatAdapter(config={})
adapter._message_actions = MagicMock()
adapter._message_actions.get_handler.return_value = None
adapter._message_actions.is_supported.return_value = False
from yuxi.channels.protocols.actions import ActionContext
ctx = ActionContext(
action="react",
channel_id="wechat",
chat_id="test_chat",
msg_id="msg_001",
args={"emoji": "👍"},
)
result = await adapter.handle_action(ctx)
assert result.success is False
assert "not supported" in result.error
def test_supports_action(self):
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
adapter = WeChatAdapter(config={})
adapter._message_actions = MagicMock()
adapter._message_actions.is_supported.side_effect = lambda action: action == "send"
assert adapter.supports_action("send") is True
assert adapter.supports_action("react") is False
def test_resolve_execution_mode(self):
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
adapter = WeChatAdapter(config={})
assert adapter.resolve_execution_mode("send") == "direct"
@pytest.mark.asyncio
async def test_handle_action_handler_exception(self):
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
adapter = WeChatAdapter(config={})
adapter._message_actions = MagicMock()
async def failing_handler(**kwargs):
raise RuntimeError("boom")
adapter._message_actions.get_handler.return_value = failing_handler
adapter._message_actions.is_supported.return_value = True
from yuxi.channels.protocols.actions import ActionContext
ctx = ActionContext(
action="send",
channel_id="wechat",
chat_id="test_chat",
msg_id="msg_001",
args={},
)
result = await adapter.handle_action(ctx)
assert result.success is False
assert "boom" in result.error
class TestWeChatAuthAdapter:
def test_get_dm_exposure(self):
from yuxi.channels.adapters.wechat.auth_adapter import WeChatAuthAdapter
auth = WeChatAuthAdapter()
assert auth.get_dm_exposure({}) == "pairing"
assert auth.get_dm_exposure({"dm_policy": "allowlist"}) == "allowlist"
assert auth.get_dm_exposure({"dm_policy": "open"}) == "open"
def test_get_authorization_url_with_config(self):
from yuxi.channels.adapters.wechat.auth_adapter import WeChatAuthAdapter
auth = WeChatAuthAdapter()
assert auth.get_authorization_url({}) is None
assert auth.get_authorization_url({"wecom_auth_url": "https://example.com/auth"}) == "https://example.com/auth"
def test_build_wecom_oauth_url(self):
from yuxi.channels.adapters.wechat.auth_adapter import WeChatAuthAdapter
auth = WeChatAuthAdapter()
url = auth.build_wecom_oauth_url("corp_123", "https://myapp.com/callback", state="state_456")
assert "open.weixin.qq.com" in url
assert "appid=corp_123" in url
assert "redirect_uri=https://myapp.com/callback" in url
assert "state=state_456" in url
assert url.endswith("#wechat_redirect")
def test_build_wecom_oauth_url_without_state(self):
from yuxi.channels.adapters.wechat.auth_adapter import WeChatAuthAdapter
auth = WeChatAuthAdapter()
url = auth.build_wecom_oauth_url("corp_123", "https://myapp.com/callback")
assert "state" not in url or url.endswith("#wechat_redirect")
def test_build_mp_oauth_url(self):
from yuxi.channels.adapters.wechat.auth_adapter import WeChatAuthAdapter
auth = WeChatAuthAdapter()
url = auth.build_mp_oauth_url("wx_app_123", "https://myapp.com/callback", state="state_789", scope="snsapi_userinfo")
assert "open.weixin.qq.com" in url
assert "appid=wx_app_123" in url
assert "scope=snsapi_userinfo" in url
assert "state=state_789" in url
def test_build_mp_oauth_url_default_scope(self):
from yuxi.channels.adapters.wechat.auth_adapter import WeChatAuthAdapter
auth = WeChatAuthAdapter()
url = auth.build_mp_oauth_url("wx_app_123", "https://myapp.com/callback")
assert "scope=snsapi_userinfo" in url
def test_get_oauth_enabled(self):
from yuxi.channels.adapters.wechat.auth_adapter import WeChatAuthAdapter
auth = WeChatAuthAdapter()
assert auth.get_oauth_enabled({}) is False
assert auth.get_oauth_enabled({"wecom_oauth_enabled": True}) is True
assert auth.get_oauth_enabled({"mp_oauth_enabled": True}) is True
@pytest.mark.asyncio
async def test_exchange_wecom_code_success(self):
from yuxi.channels.adapters.wechat.auth_adapter import WeChatAuthAdapter
auth = WeChatAuthAdapter()
with patch("httpx.AsyncClient") as mock_client_cls:
mock_client = AsyncMock()
mock_client_cls.return_value.__aenter__.return_value = mock_client
token_resp = MagicMock()
token_resp.json.return_value = {"access_token": "token_abc", "expires_in": 7200}
user_resp = MagicMock()
user_resp.json.return_value = {"errcode": 0, "UserId": "user_123"}
mock_client.get.side_effect = [token_resp, user_resp]
result = await auth.exchange_wecom_code("auth_code", "corp_123", "corp_secret_456")
assert result["success"] is True
assert result["access_token"] == "token_abc"
assert result["user_info"]["UserId"] == "user_123"
@pytest.mark.asyncio
async def test_exchange_wecom_code_token_failure(self):
from yuxi.channels.adapters.wechat.auth_adapter import WeChatAuthAdapter
auth = WeChatAuthAdapter()
with patch("httpx.AsyncClient") as mock_client_cls:
mock_client = AsyncMock()
mock_client_cls.return_value.__aenter__.return_value = mock_client
token_resp = MagicMock()
token_resp.json.return_value = {"errcode": 40001, "errmsg": "invalid credential"}
mock_client.get.return_value = token_resp
result = await auth.exchange_wecom_code("auth_code", "corp_123", "corp_secret_456")
assert result["success"] is False
@pytest.mark.asyncio
async def test_exchange_mp_code_success(self):
from yuxi.channels.adapters.wechat.auth_adapter import WeChatAuthAdapter
auth = WeChatAuthAdapter()
with patch("httpx.AsyncClient") as mock_client_cls:
mock_client = AsyncMock()
mock_client_cls.return_value.__aenter__.return_value = mock_client
token_resp = MagicMock()
token_resp.json.return_value = {
"access_token": "mp_token_abc",
"openid": "openid_123",
"refresh_token": "refresh_xyz",
}
userinfo_resp = MagicMock()
userinfo_resp.json.return_value = {"openid": "openid_123", "nickname": "TestUser"}
mock_client.get.side_effect = [token_resp, userinfo_resp]
result = await auth.exchange_mp_code("auth_code", "wx_app_123", "app_secret_456")
assert result["success"] is True
assert result["access_token"] == "mp_token_abc"
assert result["openid"] == "openid_123"
assert result["refresh_token"] == "refresh_xyz"
assert result["user_info"]["nickname"] == "TestUser"
@pytest.mark.asyncio
async def test_exchange_mp_code_failure(self):
from yuxi.channels.adapters.wechat.auth_adapter import WeChatAuthAdapter
auth = WeChatAuthAdapter()
with patch("httpx.AsyncClient") as mock_client_cls:
mock_client = AsyncMock()
mock_client_cls.return_value.__aenter__.return_value = mock_client
token_resp = MagicMock()
token_resp.json.return_value = {"errcode": 40029, "errmsg": "invalid code"}
mock_client.get.return_value = token_resp
result = await auth.exchange_mp_code("bad_code", "wx_app_123", "app_secret_456")
assert result["success"] is False
class TestWeChatSubAccount:
def test_sub_account_tracking_fields_in_init(self):
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
adapter = WeChatAdapter(config={})
assert isinstance(adapter._sub_monitors, dict)
assert len(adapter._sub_monitors) == 0
assert isinstance(adapter._sub_clients, dict)
assert len(adapter._sub_clients) == 0
assert isinstance(adapter._sub_polling_tasks, dict)
assert len(adapter._sub_polling_tasks) == 0
@pytest.mark.asyncio
async def test_connect_sub_accounts_skips_when_only_default(self):
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
adapter = WeChatAdapter(config={})
adapter._account_mgr = MagicMock()
adapter._account_mgr.list_account_ids.return_value = ["default"]
await adapter._connect_sub_accounts()
assert len(adapter._sub_monitors) == 0
@pytest.mark.asyncio
async def test_connect_sub_accounts_skips_disabled(self):
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
adapter = WeChatAdapter(config={})
adapter._account_mgr = MagicMock()
adapter._account_mgr.list_account_ids.return_value = ["default", "sub1"]
mock_account = MagicMock()
mock_account.enabled = False
mock_account.mode = "wecom"
adapter._account_mgr.resolve_account.return_value = mock_account
await adapter._connect_sub_accounts()
assert len(adapter._sub_monitors) == 0
@pytest.mark.asyncio
async def test_connect_sub_accounts_respects_limit(self):
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
adapter = WeChatAdapter(config={"max_sub_accounts": 2})
adapter._account_mgr = MagicMock()
adapter._account_mgr.list_account_ids.return_value = [
"default", "sub1", "sub2", "sub3", "sub4"
]
mock_account = MagicMock()
mock_account.enabled = True
mock_account.mode = "mp"
adapter._account_mgr.resolve_account.return_value = mock_account
await adapter._connect_sub_accounts()
adapter._account_mgr.resolve_account.assert_called()
call_count = adapter._account_mgr.resolve_account.call_count
assert call_count <= 2