新增了Twitch、Telegram、Discord、Slack、Mattermost、WeChat、Zalo等多渠道的单元测试用例,覆盖了令牌处理、速率限制、消息去重、会话解析、格式转换、安全策略等模块 同时在测试配置中添加了测试用的OpenAI API密钥环境变量
322 lines
12 KiB
Python
322 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from yuxi.channels.models import (
|
|
ChannelIdentity,
|
|
ChannelResponse,
|
|
ChannelType,
|
|
DeliveryResult,
|
|
)
|
|
|
|
|
|
class TestTokenBucketRateLimiter:
|
|
def test_acquires_tokens_and_depletes(self):
|
|
from yuxi.channels.adapters.wechat.rate_limiter import TokenBucketRateLimiter
|
|
|
|
limiter = TokenBucketRateLimiter(rate=5, per=60.0)
|
|
for _ in range(5):
|
|
assert limiter.acquire_sync() is True
|
|
assert limiter.acquire_sync() is False
|
|
|
|
def test_available_tokens_reflects_remaining(self):
|
|
from yuxi.channels.adapters.wechat.rate_limiter import TokenBucketRateLimiter
|
|
|
|
limiter = TokenBucketRateLimiter(rate=5, per=60.0)
|
|
assert limiter.available_tokens == 5.0
|
|
limiter.acquire_sync()
|
|
assert limiter.available_tokens == 4.0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_acquire_replenishes_over_time(self):
|
|
from yuxi.channels.adapters.wechat.rate_limiter import TokenBucketRateLimiter
|
|
|
|
limiter = TokenBucketRateLimiter(rate=5, per=0.2)
|
|
|
|
for _ in range(5):
|
|
assert await limiter.acquire() is True
|
|
assert await limiter.acquire() is False
|
|
|
|
await asyncio.sleep(0.25)
|
|
|
|
assert await limiter.acquire() is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_acquire_partial_refill(self):
|
|
from yuxi.channels.adapters.wechat.rate_limiter import TokenBucketRateLimiter
|
|
|
|
limiter = TokenBucketRateLimiter(rate=5, per=0.5)
|
|
|
|
for _ in range(5):
|
|
assert await limiter.acquire() is True
|
|
assert await limiter.acquire() is False
|
|
|
|
await asyncio.sleep(0.3)
|
|
assert await limiter.acquire() is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_never_exceeds_max_tokens(self):
|
|
from yuxi.channels.adapters.wechat.rate_limiter import TokenBucketRateLimiter
|
|
|
|
limiter = TokenBucketRateLimiter(rate=5, per=60.0)
|
|
await asyncio.sleep(0.01)
|
|
assert limiter.available_tokens <= 5.0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_rate_property(self):
|
|
from yuxi.channels.adapters.wechat.rate_limiter import TokenBucketRateLimiter
|
|
|
|
limiter = TokenBucketRateLimiter(rate=20, per=60.0)
|
|
assert limiter.rate == 20
|
|
assert limiter.period == 60.0
|
|
|
|
|
|
class TestMessageDedup:
|
|
def test_first_message_not_duplicate(self):
|
|
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
|
|
|
|
adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"})
|
|
adapter._mode = "wecom"
|
|
|
|
response = ChannelResponse(
|
|
identity=ChannelIdentity(
|
|
channel_id="wechat",
|
|
channel_type=ChannelType.WECHAT,
|
|
channel_user_id="user1",
|
|
channel_chat_id="user1",
|
|
),
|
|
content="Hello",
|
|
)
|
|
assert adapter._is_wecom_duplicate(response) is False
|
|
|
|
def test_duplicate_within_window(self):
|
|
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
|
|
|
|
adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"})
|
|
adapter._mode = "wecom"
|
|
|
|
response = ChannelResponse(
|
|
identity=ChannelIdentity(
|
|
channel_id="wechat",
|
|
channel_type=ChannelType.WECHAT,
|
|
channel_user_id="user1",
|
|
channel_chat_id="user1",
|
|
),
|
|
content="Duplicate content",
|
|
)
|
|
assert adapter._is_wecom_duplicate(response) is False
|
|
assert adapter._is_wecom_duplicate(response) is True
|
|
|
|
def test_different_users_not_duplicate(self):
|
|
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
|
|
|
|
adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"})
|
|
adapter._mode = "wecom"
|
|
|
|
r1 = ChannelResponse(
|
|
identity=ChannelIdentity(
|
|
channel_id="wechat", channel_type=ChannelType.WECHAT, channel_user_id="user1", channel_chat_id="user1",
|
|
),
|
|
content="Same content",
|
|
)
|
|
r2 = ChannelResponse(
|
|
identity=ChannelIdentity(
|
|
channel_id="wechat", channel_type=ChannelType.WECHAT, channel_user_id="user2", channel_chat_id="user2",
|
|
),
|
|
content="Same content",
|
|
)
|
|
|
|
assert adapter._is_wecom_duplicate(r1) is False
|
|
assert adapter._is_wecom_duplicate(r2) is False
|
|
|
|
def test_different_content_not_duplicate(self):
|
|
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
|
|
|
|
adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"})
|
|
adapter._mode = "wecom"
|
|
|
|
r1 = ChannelResponse(
|
|
identity=ChannelIdentity(
|
|
channel_id="wechat", channel_type=ChannelType.WECHAT, channel_user_id="user1", channel_chat_id="user1",
|
|
),
|
|
content="Content A",
|
|
)
|
|
r2 = ChannelResponse(
|
|
identity=ChannelIdentity(
|
|
channel_id="wechat", channel_type=ChannelType.WECHAT, channel_user_id="user1", channel_chat_id="user1",
|
|
),
|
|
content="Content B",
|
|
)
|
|
|
|
assert adapter._is_wecom_duplicate(r1) is False
|
|
assert adapter._is_wecom_duplicate(r2) is False
|
|
|
|
def test_ttl_cleanup(self):
|
|
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
|
|
|
|
adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"})
|
|
adapter._mode = "wecom"
|
|
|
|
import time
|
|
|
|
adapter._dedup[("old_user", "old_content")] = time.monotonic() - 10
|
|
|
|
response = ChannelResponse(
|
|
identity=ChannelIdentity(
|
|
channel_id="wechat", channel_type=ChannelType.WECHAT, channel_user_id="user1", channel_chat_id="user1",
|
|
),
|
|
content="New",
|
|
)
|
|
assert adapter._is_wecom_duplicate(response) is False
|
|
assert ("old_user", "old_content") not in adapter._dedup
|
|
|
|
|
|
class TestSendRateLimiting:
|
|
@pytest.mark.asyncio
|
|
async def test_send_blocked_by_rate_limit(self):
|
|
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
|
|
|
|
adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"})
|
|
adapter._mode = "wecom"
|
|
|
|
for _ in range(20):
|
|
adapter._rate_limiter.acquire_sync()
|
|
|
|
response = ChannelResponse(
|
|
identity=ChannelIdentity(
|
|
channel_id="wechat", channel_type=ChannelType.WECHAT, channel_user_id="u1", channel_chat_id="u1",
|
|
),
|
|
content="test",
|
|
)
|
|
result = await adapter.send(response)
|
|
assert result.success is False
|
|
assert "Rate limit exceeded" in result.error
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_blocked_by_dedup(self):
|
|
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
|
|
|
|
adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"})
|
|
adapter._mode = "wecom"
|
|
|
|
response = ChannelResponse(
|
|
identity=ChannelIdentity(
|
|
channel_id="wechat", channel_type=ChannelType.WECHAT, channel_user_id="u1", channel_chat_id="u1",
|
|
),
|
|
content="dup test",
|
|
)
|
|
|
|
adapter._is_wecom_duplicate(response)
|
|
result = await adapter.send(response)
|
|
assert result.success is False
|
|
assert "Duplicate message" in result.error
|
|
|
|
|
|
class TestBridgeMediaPayload:
|
|
def test_build_image_payload(self):
|
|
from yuxi.channels.adapters.wechat.bridge import build_bridge_image_payload
|
|
|
|
payload = build_bridge_image_payload("chat1", b"\x89PNG_IMAGE_DATA", is_group=False)
|
|
assert payload["chat_id"] == "chat1"
|
|
assert payload["msg_type"] == 3
|
|
assert "image_data" in payload
|
|
assert isinstance(payload["image_data"], str)
|
|
|
|
def test_build_file_payload(self):
|
|
from yuxi.channels.adapters.wechat.bridge import build_bridge_file_payload
|
|
|
|
payload = build_bridge_file_payload("chat1", b"PDF_CONTENT", "doc.pdf", is_group=True)
|
|
assert payload["chat_id"] == "chat1"
|
|
assert payload["msg_type"] == 6
|
|
assert payload["is_group"] is True
|
|
assert payload["filename"] == "doc.pdf"
|
|
assert payload["size"] == 11
|
|
assert "file_data" in payload
|
|
|
|
def test_image_payload_base64_encodes(self):
|
|
from yuxi.channels.adapters.wechat.bridge import build_bridge_image_payload
|
|
|
|
payload = build_bridge_image_payload("chat1", b"\x00\x01\x02")
|
|
import base64
|
|
|
|
expected = base64.b64encode(b"\x00\x01\x02").decode("ascii")
|
|
assert payload["image_data"] == expected
|
|
|
|
|
|
class TestBridgeSendMediaAdapter:
|
|
@pytest.mark.asyncio
|
|
async def test_bridge_image_send(self):
|
|
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
|
|
|
|
adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"})
|
|
adapter._mode = "personal"
|
|
adapter._bridge_url = "http://localhost:5555"
|
|
adapter._http_client = AsyncMock()
|
|
|
|
mock_bridge = MagicMock()
|
|
mock_bridge.send_media_message = AsyncMock(return_value=DeliveryResult(success=True))
|
|
adapter._bridge_client = mock_bridge
|
|
|
|
result = await adapter._send_bridge_media("image", b"\x89PNG", "chat1")
|
|
assert result.success is True
|
|
mock_bridge.send_media_message.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bridge_file_send(self):
|
|
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
|
|
|
|
adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"})
|
|
adapter._mode = "personal"
|
|
adapter._bridge_url = "http://localhost:5555"
|
|
adapter._http_client = AsyncMock()
|
|
|
|
mock_bridge = MagicMock()
|
|
mock_bridge.send_media_message = AsyncMock(return_value=DeliveryResult(success=True))
|
|
adapter._bridge_client = mock_bridge
|
|
|
|
result = await adapter._send_bridge_media("file", b"CONTENT", "chat1")
|
|
assert result.success is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bridge_media_not_bytes(self):
|
|
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
|
|
|
|
adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"})
|
|
adapter._mode = "personal"
|
|
adapter._bridge_client = MagicMock()
|
|
|
|
result = await adapter._send_bridge_media("image", "string_data", "chat1")
|
|
assert result.success is False
|
|
assert "raw bytes" in result.error
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_bridge_unsupported_media_type(self):
|
|
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
|
|
|
|
adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"})
|
|
adapter._mode = "personal"
|
|
adapter._bridge_client = MagicMock()
|
|
|
|
result = await adapter._send_bridge_media("voice", b"data", "chat1")
|
|
assert result.success is False
|
|
assert "Unsupported media_type" in result.error
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_send_media_routes_to_bridge(self):
|
|
from yuxi.channels.adapters.wechat.adapter import WeChatAdapter
|
|
|
|
adapter = WeChatAdapter({"bridge_url": "http://localhost:5555", "dm_policy": "open"})
|
|
adapter._mode = "personal"
|
|
adapter._bridge_url = "http://localhost:5555"
|
|
adapter._http_client = AsyncMock()
|
|
|
|
mock_bridge = MagicMock()
|
|
mock_bridge.send_media_message = AsyncMock(return_value=DeliveryResult(success=True))
|
|
adapter._bridge_client = mock_bridge
|
|
|
|
result = await adapter.send_media("chat1", "image", b"\x89PNG")
|
|
assert result.success is True |