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

330 lines
11 KiB
Python

"""Unit tests for Yuanbao channel adapter module fixes.
Tests the security module, token manager, adapter normalization, and probe module.
"""
import time
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from yuxi.channels.adapters.yuanbao.adapter import YuanbaoAdapter
from yuxi.channels.adapters.yuanbao.probe import health_check_yuanbao
from yuxi.channels.adapters.yuanbao.security import (
check_dm_policy,
check_group_policy,
check_mention_required,
)
from yuxi.channels.adapters.yuanbao.token import YuanbaoTokenManager
from yuxi.channels.models import (
ChannelIdentity,
ChannelMessage,
ChannelType,
ChatType,
HealthStatus,
MentionsInfo,
)
class TestSecurityPrefixFix:
@pytest.mark.asyncio
async def test_dm_policy_allowlist_with_yb_prefix(self):
"""allowlist检查现在直接使用user_id(yb:前缀已在normalize_inbound中添加)"""
config = {"dm_policy": "allowlist", "allow_from": ["yb:user123"]}
result = await check_dm_policy("yb:user123", config)
assert result is True
@pytest.mark.asyncio
async def test_dm_policy_allowlist_rejects_non_matching(self):
config = {"dm_policy": "allowlist", "allow_from": ["yb:user123"]}
result = await check_dm_policy("yb:user456", config)
assert result is False
@pytest.mark.asyncio
async def test_dm_policy_pairing_matches_prefixed_user(self):
"""配对策略现在应正确匹配带yb:前缀的用户"""
config = {"dm_policy": "pairing", "paired_users": ["yb:user123"]}
result = await check_dm_policy("yb:user123", config)
assert result is True
@pytest.mark.asyncio
async def test_dm_policy_pairing_rejects_non_paired(self):
config = {"dm_policy": "pairing", "paired_users": ["yb:user123"]}
result = await check_dm_policy("yb:user456", config)
assert result is False
@pytest.mark.asyncio
async def test_dm_policy_open_always_true(self):
config = {"dm_policy": "open"}
assert await check_dm_policy("yb:anyone", config) is True
@pytest.mark.asyncio
async def test_dm_policy_disabled_always_false(self):
config = {"dm_policy": "disabled"}
assert await check_dm_policy("yb:anyone", config) is False
@pytest.mark.asyncio
async def test_group_policy_allowlist_with_prefixed_user(self):
config = {"group_policy": "allowlist", "group_allow_from": ["yb:user123"]}
result = await check_group_policy("group_001", "yb:user123", config)
assert result is True
@pytest.mark.asyncio
async def test_group_policy_per_group_allowlist(self):
config = {
"group_policy": "allowlist",
"groups": {"group_001": {"allow_from": ["yb:user123"]}},
}
result = await check_group_policy("group_001", "yb:user123", config)
assert result is True
class TestCheckMentionRequired:
@pytest.mark.asyncio
async def test_mention_required_bot_mentioned(self):
msg = ChannelMessage(
identity=ChannelIdentity(
channel_id="yuanbao",
channel_type=ChannelType.YUANBAO,
channel_user_id="yb:user123",
channel_chat_id="group_001",
),
content="@bot hello",
mentions=MentionsInfo(is_bot_mentioned=True, raw_text="@bot hello"),
)
config = {"group_require_mention": True}
assert await check_mention_required("group_001", msg, config, ["bot"]) is True
@pytest.mark.asyncio
async def test_mention_required_not_mentioned(self):
msg = ChannelMessage(
identity=ChannelIdentity(
channel_id="yuanbao",
channel_type=ChannelType.YUANBAO,
channel_user_id="yb:user123",
channel_chat_id="group_001",
),
content="hello",
)
config = {"group_require_mention": True}
assert await check_mention_required("group_001", msg, config, ["bot"]) is False
@pytest.mark.asyncio
async def test_mention_not_required(self):
msg = ChannelMessage(
identity=ChannelIdentity(
channel_id="yuanbao",
channel_type=ChannelType.YUANBAO,
channel_user_id="yb:user123",
channel_chat_id="group_001",
),
content="hello",
)
config = {"group_require_mention": False}
assert await check_mention_required("group_001", msg, config, ["bot"]) is True
class TestTokenManager:
def test_is_expired_public_method_exists(self):
manager = YuanbaoTokenManager(
app_key="test_key",
app_secret="test_secret",
bot_app_id="test_bot",
)
assert hasattr(manager, "is_expired")
assert callable(manager.is_expired)
def test_is_expired_true_when_no_token(self):
manager = YuanbaoTokenManager(
app_key="test_key",
app_secret="test_secret",
bot_app_id="test_bot",
)
assert manager.is_expired() is True
def test_is_expired_true_when_expired(self):
manager = YuanbaoTokenManager(
app_key="test_key",
app_secret="test_secret",
bot_app_id="test_bot",
)
manager._access_token = "fake_token"
manager._expires_at = time.time() - 3600
assert manager.is_expired() is True
def test_is_expired_false_when_valid(self):
manager = YuanbaoTokenManager(
app_key="test_key",
app_secret="test_secret",
bot_app_id="test_bot",
)
manager._access_token = "fake_token"
manager._expires_at = time.time() + 7200
assert manager.is_expired() is False
def test_backward_compatible_is_expired(self):
manager = YuanbaoTokenManager(
app_key="test_key",
app_secret="test_secret",
bot_app_id="test_bot",
)
assert manager._is_expired == manager.is_expired
class TestAdapterNormalizeInbound:
@pytest.fixture
def adapter(self):
return YuanbaoAdapter(config={"app_key": "k", "app_secret": "s"})
def test_normalize_inbound_adds_yb_prefix(self, adapter):
raw = {
"open_id": "user123",
"msg_id": "msg001",
"msg_type": "text",
"content": "Hello",
"chat_type": "direct",
}
msg = adapter.normalize_inbound(raw)
assert msg.identity.channel_user_id == "yb:user123"
def test_normalize_inbound_empty_open_id_no_prefix(self, adapter):
raw = {
"open_id": "",
"msg_id": "msg001",
"msg_type": "text",
"content": "Hello",
}
msg = adapter.normalize_inbound(raw)
assert msg.identity.channel_user_id == ""
def test_normalize_inbound_direct_message(self, adapter):
raw = {
"open_id": "user456",
"msg_id": "msg002",
"msg_type": "text",
"content": "Hi there",
"chat_type": "direct",
}
msg = adapter.normalize_inbound(raw)
assert msg.chat_type == ChatType.DIRECT
assert msg.message_type.value == "text"
assert msg.content == "Hi there"
def test_normalize_inbound_group_message(self, adapter):
raw = {
"open_id": "user789",
"group_open_id": "group_001",
"msg_id": "msg003",
"msg_type": "text",
"content": "Hello group",
"chat_type": "group",
}
msg = adapter.normalize_inbound(raw)
assert msg.chat_type == ChatType.GROUP
assert msg.identity.channel_user_id == "yb:user789"
assert msg.identity.channel_chat_id == "group_001"
def test_normalize_inbound_command_detection(self, adapter):
raw = {
"open_id": "user123",
"msg_id": "msg004",
"msg_type": "text",
"content": "/start",
"chat_type": "direct",
}
msg = adapter.normalize_inbound(raw)
assert msg.message_type.value == "command"
def test_normalize_inbound_with_attachments(self, adapter):
raw = {
"open_id": "user123",
"msg_id": "msg005",
"msg_type": "image",
"content": "",
"chat_type": "direct",
"attachments": [
{"type": "image", "url": "http://example.com/img.png",
"filename": "img.png", "size": 1024, "mime_type": "image/png"}
],
}
msg = adapter.normalize_inbound(raw)
assert len(msg.attachments) == 1
assert msg.attachments[0].type == "image"
def test_normalize_inbound_missing_open_id_key(self, adapter):
raw = {
"msg_id": "msg006",
"msg_type": "text",
"content": "Hello",
}
msg = adapter.normalize_inbound(raw)
assert msg.identity.channel_user_id == ""
def test_no_receive_method(self, adapter):
assert not hasattr(adapter.__class__, "receive") or \
adapter.__class__.receive == adapter.__class__.__bases__[0].__dict__.get("receive")
class TestAdapterGetUserInfo:
@pytest.fixture
def adapter(self):
return YuanbaoAdapter(config={"app_key": "k", "app_secret": "s"})
@pytest.mark.asyncio
async def test_get_user_info_no_client_returns_empty(self, adapter):
result = await adapter.get_user_info("yb:user123")
assert result == {}
@pytest.mark.asyncio
async def test_get_user_info_strips_yb_prefix(self, adapter):
result = await adapter.get_user_info("yb:user123")
assert result == {}
@pytest.mark.asyncio
async def test_get_user_info_empty_user_id(self, adapter):
result = await adapter.get_user_info("")
assert result == {}
class TestProbeHealthCheck:
@pytest.mark.asyncio
async def test_health_check_accepts_session_param(self):
with patch("aiohttp.ClientSession") as mock_session_cls:
mock_session = MagicMock()
mock_session.get.return_value.__aenter__.return_value.status = 200
mock_session.get.return_value.__aenter__.return_value.json = AsyncMock(
return_value={"bot_app_id": "test_bot"}
)
result = await health_check_yuanbao(
"https://api.example.com",
"fake_token",
session=mock_session,
)
assert isinstance(result, HealthStatus)
assert result.status == "healthy"
@pytest.mark.asyncio
async def test_health_check_creates_session_when_none(self):
with patch("aiohttp.ClientSession") as mock_session_cls:
mock_session = MagicMock()
mock_session_cls.return_value = mock_session
mock_session.__aenter__.return_value = mock_session
mock_session.get.return_value.__aenter__.return_value.status = 200
mock_session.get.return_value.__aenter__.return_value.json = AsyncMock(
return_value={"bot_app_id": "test_bot"}
)
result = await health_check_yuanbao(
"https://api.example.com",
"fake_token",
)
assert isinstance(result, HealthStatus)
mock_session_cls.assert_called_once()