2026-05-12 00:56:47 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
import httpx
|
2026-05-12 00:56:47 +08:00
|
|
|
import pytest
|
|
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
from yuxi.channels.adapters.wechat.mp.client import MPClient
|
|
|
|
|
from yuxi.channels.adapters.wechat.mp.send import (
|
|
|
|
|
send_mp_custom_message,
|
|
|
|
|
send_mp_template_message,
|
|
|
|
|
send_mp_image,
|
|
|
|
|
send_mp_voice,
|
|
|
|
|
send_mp_video,
|
|
|
|
|
send_mp_news,
|
|
|
|
|
build_mp_news_payload,
|
2026-05-12 00:56:47 +08:00
|
|
|
)
|
2026-05-13 16:43:01 +08:00
|
|
|
from yuxi.channels.adapters.wechat.wecom.client import WeComClient
|
|
|
|
|
from yuxi.channels.adapters.wechat.wecom.send import (
|
|
|
|
|
send_wecom_message,
|
|
|
|
|
send_wecom_voice,
|
|
|
|
|
send_wecom_video,
|
|
|
|
|
build_wecom_text_payload,
|
|
|
|
|
)
|
|
|
|
|
from yuxi.channels.models import DeliveryResult
|
2026-05-12 00:56:47 +08:00
|
|
|
|
|
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
class TestSendWeComMessage:
|
|
|
|
|
def setup_method(self):
|
|
|
|
|
self.http_client = AsyncMock(spec=httpx.AsyncClient)
|
|
|
|
|
self.config = {"corp_id": "test_corp", "agent_id": "100001", "corp_secret": "test_secret"}
|
|
|
|
|
self.client = WeComClient(self.http_client, self.config)
|
|
|
|
|
self.client.set_token("test_token", 9999999999.0)
|
2026-05-12 00:56:47 +08:00
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
2026-05-13 16:43:01 +08:00
|
|
|
async def test_send_success(self):
|
2026-05-12 00:56:47 +08:00
|
|
|
mock_resp = MagicMock()
|
2026-05-13 16:43:01 +08:00
|
|
|
mock_resp.json.return_value = {"errcode": 0, "msgid": "msg_001"}
|
|
|
|
|
self.http_client.post.return_value = mock_resp
|
|
|
|
|
|
|
|
|
|
payload = build_wecom_text_payload("100001", "user1", "hello")
|
|
|
|
|
result = await send_wecom_message(self.client, self.http_client, payload)
|
|
|
|
|
|
|
|
|
|
assert result.success is True
|
|
|
|
|
assert result.message_id == "msg_001"
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_send_token_expired_retries(self):
|
|
|
|
|
mock_resp_expired = MagicMock()
|
|
|
|
|
mock_resp_expired.json.return_value = {"errcode": 42001, "errmsg": "token expired"}
|
|
|
|
|
mock_resp_success = MagicMock()
|
|
|
|
|
mock_resp_success.json.return_value = {"errcode": 0, "msgid": "retry_msg"}
|
|
|
|
|
mock_resp_token = MagicMock()
|
|
|
|
|
mock_resp_token.json.return_value = {"access_token": "new_token", "expires_in": 7200}
|
|
|
|
|
|
|
|
|
|
self.http_client.post.side_effect = [mock_resp_expired, mock_resp_success]
|
|
|
|
|
self.http_client.get.return_value = mock_resp_token
|
|
|
|
|
|
|
|
|
|
payload = build_wecom_text_payload("100001", "user1", "hello")
|
|
|
|
|
result = await send_wecom_message(self.client, self.http_client, payload)
|
2026-05-12 00:56:47 +08:00
|
|
|
|
|
|
|
|
assert result.success is True
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
2026-05-13 16:43:01 +08:00
|
|
|
async def test_send_http_error(self):
|
|
|
|
|
self.http_client.post.side_effect = httpx.NetworkError("connection refused")
|
|
|
|
|
|
|
|
|
|
payload = build_wecom_text_payload("100001", "user1", "hello")
|
|
|
|
|
result = await send_wecom_message(self.client, self.http_client, payload)
|
|
|
|
|
|
2026-05-12 00:56:47 +08:00
|
|
|
assert result.success is False
|
2026-05-13 16:43:01 +08:00
|
|
|
assert result.error is not None
|
2026-05-12 00:56:47 +08:00
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
2026-05-13 16:43:01 +08:00
|
|
|
async def test_send_api_error(self):
|
2026-05-12 00:56:47 +08:00
|
|
|
mock_resp = MagicMock()
|
2026-05-13 16:43:01 +08:00
|
|
|
mock_resp.json.return_value = {"errcode": 40003, "errmsg": "invalid userid"}
|
|
|
|
|
self.http_client.post.return_value = mock_resp
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
payload = build_wecom_text_payload("100001", "invalid_user", "hello")
|
|
|
|
|
result = await send_wecom_message(self.client, self.http_client, payload)
|
|
|
|
|
|
|
|
|
|
assert result.success is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestSendMpMessage:
|
|
|
|
|
def setup_method(self):
|
|
|
|
|
self.http_client = AsyncMock(spec=httpx.AsyncClient)
|
|
|
|
|
self.config = {"app_id": "wx_test", "app_secret": "test_secret"}
|
|
|
|
|
self.client = MPClient(self.http_client, self.config)
|
|
|
|
|
self.client.set_token("mp_token", 9999999999.0)
|
2026-05-12 00:56:47 +08:00
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
2026-05-13 16:43:01 +08:00
|
|
|
async def test_send_custom_message_success(self):
|
|
|
|
|
mock_resp = MagicMock()
|
|
|
|
|
mock_resp.json.return_value = {"errcode": 0, "msgid": "mp_msg_001"}
|
|
|
|
|
self.http_client.post.return_value = mock_resp
|
|
|
|
|
|
|
|
|
|
result = await send_mp_custom_message(self.client, self.http_client, "user1", "hello")
|
2026-05-12 00:56:47 +08:00
|
|
|
|
|
|
|
|
assert result.success is True
|
2026-05-13 16:43:01 +08:00
|
|
|
assert result.message_id == "mp_msg_001"
|
2026-05-12 00:56:47 +08:00
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
2026-05-13 16:43:01 +08:00
|
|
|
async def test_send_custom_message_with_reply(self):
|
|
|
|
|
mock_resp = MagicMock()
|
|
|
|
|
mock_resp.json.return_value = {"errcode": 0, "msgid": "mp_reply_001"}
|
|
|
|
|
self.http_client.post.return_value = mock_resp
|
|
|
|
|
|
|
|
|
|
result = await send_mp_custom_message(
|
|
|
|
|
self.client, self.http_client, "user1", "reply content",
|
|
|
|
|
reply_to_msg_id="orig_123", reply_to_user="sender",
|
2026-05-12 00:56:47 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert result.success is True
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
2026-05-13 16:43:01 +08:00
|
|
|
async def test_send_custom_message_http_error(self):
|
|
|
|
|
self.http_client.post.side_effect = httpx.NetworkError("connection error")
|
|
|
|
|
|
|
|
|
|
result = await send_mp_custom_message(self.client, self.http_client, "user1", "hello")
|
2026-05-12 00:56:47 +08:00
|
|
|
|
|
|
|
|
assert result.success is False
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
2026-05-13 16:43:01 +08:00
|
|
|
async def test_send_template_message_success(self):
|
|
|
|
|
mock_resp = MagicMock()
|
|
|
|
|
mock_resp.json.return_value = {"errcode": 0, "msgid": "tpl_msg_001"}
|
|
|
|
|
self.http_client.post.return_value = mock_resp
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
result = await send_mp_template_message(
|
|
|
|
|
self.client, self.http_client, "user1",
|
|
|
|
|
"template_001", {"first": {"value": "hello"}},
|
|
|
|
|
url="http://example.com",
|
|
|
|
|
)
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
assert result.success is True
|
|
|
|
|
assert result.message_id == "tpl_msg_001"
|
2026-05-12 00:56:47 +08:00
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
2026-05-13 16:43:01 +08:00
|
|
|
async def test_send_mp_image_upload_failure(self):
|
|
|
|
|
self.client.upload_temp_media = AsyncMock(side_effect=RuntimeError("upload failed"))
|
|
|
|
|
|
|
|
|
|
result = await send_mp_image(self.client, self.http_client, "user1", b"fake_image")
|
|
|
|
|
|
|
|
|
|
assert result.success is False
|
|
|
|
|
assert "upload failed" in (result.error or "")
|
2026-05-12 00:56:47 +08:00
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
2026-05-13 16:43:01 +08:00
|
|
|
async def test_send_mp_voice_upload_failure(self):
|
|
|
|
|
self.client.upload_temp_media = AsyncMock(side_effect=RuntimeError("voice upload failed"))
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
result = await send_mp_voice(self.client, self.http_client, "user1", b"fake_voice")
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
assert result.success is False
|
2026-05-12 00:56:47 +08:00
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
2026-05-13 16:43:01 +08:00
|
|
|
async def test_send_mp_video_upload_failure(self):
|
|
|
|
|
self.client.upload_temp_media = AsyncMock(side_effect=RuntimeError("video upload failed"))
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
result = await send_mp_video(self.client, self.http_client, "user1", b"fake_video")
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
assert result.success is False
|
2026-05-12 00:56:47 +08:00
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
2026-05-13 16:43:01 +08:00
|
|
|
async def test_send_mp_news_success(self):
|
|
|
|
|
mock_resp = MagicMock()
|
|
|
|
|
mock_resp.json.return_value = {"errcode": 0, "msgid": "news_001"}
|
|
|
|
|
self.http_client.post.return_value = mock_resp
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
payload = build_mp_news_payload("user1", [{"title": "News1", "url": "http://example.com"}])
|
|
|
|
|
result = await send_mp_news(self.client, self.http_client, payload)
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
assert result.success is True
|
2026-05-12 00:56:47 +08:00
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
2026-05-13 16:43:01 +08:00
|
|
|
async def test_send_wecom_voice_upload_failure(self):
|
|
|
|
|
wecom_client = WeComClient(AsyncMock(spec=httpx.AsyncClient), {
|
|
|
|
|
"corp_id": "test", "agent_id": "100001", "corp_secret": "secret",
|
|
|
|
|
})
|
|
|
|
|
wecom_client.set_token("token", 9999999999.0)
|
|
|
|
|
wecom_client.upload_media = AsyncMock(side_effect=RuntimeError("upload failed"))
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
result = await send_wecom_voice(wecom_client, AsyncMock(), "100001", "user1", b"data")
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
assert result.success is False
|
2026-05-12 00:56:47 +08:00
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
2026-05-13 16:43:01 +08:00
|
|
|
async def test_send_wecom_video_upload_failure(self):
|
|
|
|
|
wecom_client = WeComClient(AsyncMock(spec=httpx.AsyncClient), {
|
|
|
|
|
"corp_id": "test", "agent_id": "100001", "corp_secret": "secret",
|
|
|
|
|
})
|
|
|
|
|
wecom_client.set_token("token", 9999999999.0)
|
|
|
|
|
wecom_client.upload_media = AsyncMock(side_effect=RuntimeError("upload failed"))
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
result = await send_wecom_video(wecom_client, AsyncMock(), "100001", "user1", b"data")
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
assert result.success is False
|