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

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