新增了Twitch、Telegram、Discord、Slack、Mattermost、WeChat、Zalo等多渠道的单元测试用例,覆盖了令牌处理、速率限制、消息去重、会话解析、格式转换、安全策略等模块 同时在测试配置中添加了测试用的OpenAI API密钥环境变量
273 lines
9.9 KiB
Python
273 lines
9.9 KiB
Python
from __future__ import annotations
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from yuxi.channels.models import DeliveryResult
|
|
|
|
|
|
class TestSendMediaAdapter:
|
|
@pytest.mark.asyncio
|
|
async def test_send_media_not_initialized_mode(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()
|
|
|
|
result = await adapter.send_media("chat1", "image", b"\x89PNG")
|
|
assert result.success is False
|
|
assert "not supported" in result.error
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_media_wecom_image_bytes_required(self):
|
|
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
|
|
|
|
adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"})
|
|
adapter._mode = "wecom"
|
|
adapter._wecom_client = MagicMock()
|
|
|
|
result = await adapter._send_wecom_media("image", "not_bytes", "user1")
|
|
assert result.success is False
|
|
assert "raw bytes" in result.error
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_media_wecom_image_success(self):
|
|
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
|
|
|
|
adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"})
|
|
adapter._mode = "wecom"
|
|
adapter._http_client = AsyncMock()
|
|
adapter.config = {"agent_id": "1000001", "dm_policy": "open"}
|
|
|
|
mock_client = MagicMock()
|
|
mock_client.upload_media = AsyncMock(return_value="media_123")
|
|
adapter._wecom_client = mock_client
|
|
|
|
with patch(
|
|
"yuxi.channels.adapters.wechat.wecom.send.send_wecom_message",
|
|
new=AsyncMock(return_value=DeliveryResult(success=True)),
|
|
) as mock_send:
|
|
result = await adapter._send_wecom_media("image", b"\x89PNG\x00", "user1")
|
|
|
|
assert result.success is True
|
|
mock_client.upload_media.assert_called_once()
|
|
mock_send.assert_called_once()
|
|
payload = mock_send.call_args[0][2]
|
|
assert payload["msgtype"] == "image"
|
|
assert payload["image"]["media_id"] == "media_123"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_media_wecom_file_success(self):
|
|
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
|
|
|
|
adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"})
|
|
adapter._mode = "wecom"
|
|
adapter._http_client = AsyncMock()
|
|
adapter.config = {"agent_id": "1000001", "dm_policy": "open"}
|
|
|
|
mock_client = MagicMock()
|
|
mock_client.upload_media = AsyncMock(return_value="file_456")
|
|
adapter._wecom_client = mock_client
|
|
|
|
with patch(
|
|
"yuxi.channels.adapters.wechat.wecom.send.send_wecom_message",
|
|
new=AsyncMock(return_value=DeliveryResult(success=True)),
|
|
) as mock_send:
|
|
result = await adapter._send_wecom_media("file", b"PDF_CONTENT", "user1")
|
|
|
|
assert result.success is True
|
|
payload = mock_send.call_args[0][2]
|
|
assert payload["msgtype"] == "file"
|
|
assert payload["file"]["media_id"] == "file_456"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_media_wecom_unsupported_type(self):
|
|
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
|
|
|
|
adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"})
|
|
adapter._mode = "wecom"
|
|
adapter.config = {"agent_id": "1000001", "dm_policy": "open"}
|
|
|
|
mock_client = MagicMock()
|
|
mock_client.upload_media = AsyncMock(return_value="voice_789")
|
|
adapter._wecom_client = mock_client
|
|
adapter._http_client = AsyncMock()
|
|
|
|
result = await adapter._send_wecom_media("voice", b"audio_data", "user1")
|
|
assert result.success is False
|
|
assert "Unsupported media_type" in result.error
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_media_wecom_upload_failure(self):
|
|
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
|
|
|
|
adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"})
|
|
adapter._mode = "wecom"
|
|
adapter.config = {"agent_id": "1000001", "dm_policy": "open"}
|
|
|
|
mock_client = MagicMock()
|
|
mock_client.upload_media = AsyncMock(side_effect=RuntimeError("Network error"))
|
|
adapter._wecom_client = mock_client
|
|
|
|
result = await adapter._send_wecom_media("image", b"img", "user1")
|
|
assert result.success is False
|
|
assert "upload failed" in result.error
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_media_mp_not_bytes(self):
|
|
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
|
|
|
|
adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"})
|
|
adapter._mode = "mp"
|
|
adapter._mp_client = MagicMock()
|
|
|
|
result = await adapter._send_mp_media("image", "not_bytes", "user1")
|
|
assert result.success is False
|
|
assert "raw bytes" in result.error
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_media_mp_unsupported_type(self):
|
|
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
|
|
|
|
adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"})
|
|
adapter._mode = "mp"
|
|
adapter._mp_client = MagicMock()
|
|
|
|
result = await adapter._send_mp_media("file", b"data", "user1")
|
|
assert result.success is False
|
|
assert "only supports image" in result.error
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_media_mp_image_success(self):
|
|
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
|
|
|
|
adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"})
|
|
adapter._mode = "mp"
|
|
adapter._http_client = AsyncMock()
|
|
|
|
with patch(
|
|
"yuxi.channels.adapters.wechat.mp.send.send_mp_image",
|
|
new=AsyncMock(return_value=DeliveryResult(success=True)),
|
|
) as mock_send:
|
|
result = await adapter._send_mp_media("image", b"\x89PNG", "openid123")
|
|
|
|
assert result.success is True
|
|
mock_send.assert_called_once()
|
|
|
|
|
|
class TestWeComMediaPayload:
|
|
def test_build_image_payload(self):
|
|
from yuxi.channels.adapters.wechat.wecom.send import build_wecom_image_payload
|
|
|
|
payload = build_wecom_image_payload("agent1", "user123", "media_abc")
|
|
assert payload["touser"] == "user123"
|
|
assert payload["msgtype"] == "image"
|
|
assert payload["agentid"] == "agent1"
|
|
assert payload["image"]["media_id"] == "media_abc"
|
|
|
|
def test_build_file_payload(self):
|
|
from yuxi.channels.adapters.wechat.wecom.send import build_wecom_file_payload
|
|
|
|
payload = build_wecom_file_payload("agent1", "user123", "media_xyz")
|
|
assert payload["msgtype"] == "file"
|
|
assert payload["file"]["media_id"] == "media_xyz"
|
|
|
|
|
|
class TestRetryHelper:
|
|
@pytest.mark.asyncio
|
|
async def test_retry_success_first_attempt(self):
|
|
from yuxi.channels.adapters.wechat.retry import retry_with_backoff
|
|
|
|
call_count = 0
|
|
|
|
async def succeed():
|
|
nonlocal call_count
|
|
call_count += 1
|
|
return "ok"
|
|
|
|
result = await retry_with_backoff(succeed, max_retries=3, base_delay=0.01)
|
|
assert result == "ok"
|
|
assert call_count == 1
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_retry_timeout_retries(self):
|
|
from yuxi.channels.adapters.wechat.retry import retry_with_backoff
|
|
|
|
fail_count = 0
|
|
|
|
async def flaky():
|
|
nonlocal fail_count
|
|
fail_count += 1
|
|
if fail_count < 2:
|
|
raise httpx.TimeoutException("timeout")
|
|
return "recovered"
|
|
|
|
result = await retry_with_backoff(flaky, max_retries=3, base_delay=0.01)
|
|
assert result == "recovered"
|
|
assert fail_count == 2
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_retry_timeout_exhausted(self):
|
|
from yuxi.channels.adapters.wechat.retry import retry_with_backoff
|
|
|
|
async def always_fail():
|
|
raise httpx.TimeoutException("always timeout")
|
|
|
|
with pytest.raises(httpx.TimeoutException):
|
|
await retry_with_backoff(always_fail, max_retries=2, base_delay=0.01)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_retry_no_retry_on_non_retryable(self):
|
|
from yuxi.channels.adapters.wechat.retry import retry_with_backoff
|
|
|
|
async def auth_fail():
|
|
raise ValueError("auth error")
|
|
|
|
with pytest.raises(ValueError):
|
|
await retry_with_backoff(auth_fail, max_retries=3, base_delay=0.01)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_retry_backoff_increasing_delay(self):
|
|
from yuxi.channels.adapters.wechat.retry import retry_with_backoff
|
|
|
|
delays = []
|
|
|
|
async def fail_then_ok():
|
|
delays.append(len(delays))
|
|
if len(delays) >= 3:
|
|
return "ok"
|
|
raise httpx.ConnectError("refused")
|
|
|
|
result = await retry_with_backoff(fail_then_ok, max_retries=4, base_delay=0.01)
|
|
assert result == "ok"
|
|
assert delays == [0, 1, 2]
|
|
|
|
|
|
class TestMediaExtensionHelper:
|
|
def test_image_extension(self):
|
|
from yuxi.channels.adapters.wechat.adapter import _media_extension
|
|
|
|
assert _media_extension("image") == "png"
|
|
|
|
def test_voice_extension(self):
|
|
from yuxi.channels.adapters.wechat.adapter import _media_extension
|
|
|
|
assert _media_extension("voice") == "mp3"
|
|
|
|
def test_video_extension(self):
|
|
from yuxi.channels.adapters.wechat.adapter import _media_extension
|
|
|
|
assert _media_extension("video") == "mp4"
|
|
|
|
def test_unknown_extension(self):
|
|
from yuxi.channels.adapters.wechat.adapter import _media_extension
|
|
|
|
assert _media_extension("unknown") == "bin"
|
|
|
|
def test_file_extension(self):
|
|
from yuxi.channels.adapters.wechat.adapter import _media_extension
|
|
|
|
assert _media_extension("file") == "bin" |