新增了Twitch、Telegram、Discord、Slack、Mattermost、WeChat、Zalo等多渠道的单元测试用例,覆盖了令牌处理、速率限制、消息去重、会话解析、格式转换、安全策略等模块 同时在测试配置中添加了测试用的OpenAI API密钥环境变量
330 lines
11 KiB
Python
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() |