新增了Twitch、Telegram、Discord、Slack、Mattermost、WeChat、Zalo等多渠道的单元测试用例,覆盖了令牌处理、速率限制、消息去重、会话解析、格式转换、安全策略等模块 同时在测试配置中添加了测试用的OpenAI API密钥环境变量
299 lines
9.9 KiB
Python
299 lines
9.9 KiB
Python
"""Quick validation test for DingDing adapter modules.
|
||
|
||
Uses direct imports to avoid triggering full yuxi.channels init chain.
|
||
""" # noqa: E402
|
||
from yuxi.channels.adapters.dingding.normalizer import normalize_inbound
|
||
from yuxi.channels.models import ChannelType, EventType, MessageType, ChatType
|
||
|
||
channel_id = "dingding"
|
||
channel_type = ChannelType.DINGDING
|
||
|
||
# Test 1: DM text message
|
||
raw_dm = {
|
||
"senderId": "manager1234",
|
||
"senderNick": "zhangsan",
|
||
"isGroupChat": False,
|
||
"conversationId": "cid123456",
|
||
"msgId": "msg001",
|
||
"msgtype": "text",
|
||
"text": {"content": "你好,AI助手"},
|
||
}
|
||
msg = normalize_inbound(channel_id, channel_type, raw_dm)
|
||
assert msg.chat_type == ChatType.DIRECT, f"Expected DIRECT, got {msg.chat_type}"
|
||
assert msg.event_type == EventType.MESSAGE_RECEIVED
|
||
assert msg.message_type == MessageType.TEXT
|
||
assert msg.content == "你好,AI助手"
|
||
assert msg.identity.channel_chat_id == "dm_manager1234"
|
||
assert msg.identity.channel_user_id == "manager1234"
|
||
assert msg.identity.channel_message_id == "msg001"
|
||
print("Test 1 (DM text) PASSED")
|
||
|
||
# Test 2: Group message with @mention
|
||
raw_group = {
|
||
"senderId": "user456",
|
||
"senderNick": "lisi",
|
||
"isGroupChat": True,
|
||
"conversationId": "cid6KeBBLovxxxx",
|
||
"msgId": "msg002",
|
||
"msgtype": "text",
|
||
"text": {"content": "@AI助手 今天天气怎么样"},
|
||
"atUsers": [{"dingtalkId": "dingbot001"}, {"dingtalkId": "user789"}],
|
||
}
|
||
msg2 = normalize_inbound(channel_id, channel_type, raw_group)
|
||
assert msg2.chat_type == ChatType.GROUP
|
||
assert msg2.identity.channel_chat_id == "group_cid6KeBBLovxxxx"
|
||
assert msg2.mentions is not None
|
||
assert "dingbot001" in msg2.mentions.mentioned_user_ids
|
||
assert "user789" in msg2.mentions.mentioned_user_ids
|
||
assert msg2.metadata["isGroupChat"] is True
|
||
print("Test 2 (Group message with @mention) PASSED")
|
||
|
||
# Test 3: Group message @all
|
||
raw_at_all = {
|
||
"senderId": "admin001",
|
||
"senderNick": "admin",
|
||
"isGroupChat": True,
|
||
"conversationId": "cid_grp_admin",
|
||
"msgId": "msg003",
|
||
"msgtype": "text",
|
||
"text": {"content": "@all 重要通知"},
|
||
"isAtAll": True,
|
||
}
|
||
msg3 = normalize_inbound(channel_id, channel_type, raw_at_all)
|
||
assert msg3.chat_type == ChatType.GROUP
|
||
assert msg3.mentions is not None
|
||
assert "@all" in msg3.mentions.mentioned_user_ids
|
||
print("Test 3 (Group message @all) PASSED")
|
||
|
||
# Test 4: Markdown message type
|
||
raw_md = {
|
||
"senderId": "user111",
|
||
"isGroupChat": False,
|
||
"conversationId": "cid_dm_111",
|
||
"msgId": "msg004",
|
||
"msgtype": "markdown",
|
||
"text": {"content": "**bold**"},
|
||
}
|
||
msg4 = normalize_inbound(channel_id, channel_type, raw_md)
|
||
assert msg4.message_type == MessageType.TEXT, f"Expected TEXT for markdown, got {msg4.message_type}"
|
||
assert msg4.content == "**bold**"
|
||
print("Test 4 (Markdown message type) PASSED")
|
||
|
||
# Test 4b: ActionCard message type
|
||
raw_ac = {
|
||
"senderId": "user555",
|
||
"isGroupChat": True,
|
||
"conversationId": "cid_ac_grp",
|
||
"msgId": "msg004b",
|
||
"msgtype": "actionCard",
|
||
}
|
||
msg4b = normalize_inbound(channel_id, channel_type, raw_ac)
|
||
assert msg4b.message_type == MessageType.CARD, f"Expected CARD for actionCard, got {msg4b.message_type}"
|
||
print("Test 4b (ActionCard message type) PASSED")
|
||
|
||
# Test 5: Image message (now returns media description)
|
||
raw_img = {
|
||
"senderId": "user222",
|
||
"isGroupChat": True,
|
||
"conversationId": "cid_img_grp",
|
||
"msgId": "msg005",
|
||
"msgtype": "image",
|
||
"text": {},
|
||
}
|
||
msg5 = normalize_inbound(channel_id, channel_type, raw_img)
|
||
assert msg5.message_type == MessageType.IMAGE
|
||
assert msg5.content == "[图片]"
|
||
print("Test 5 (Image message) PASSED")
|
||
|
||
# Test 5b: Image message with downloadCode attachment
|
||
raw_img_dl = {
|
||
"senderId": "user555",
|
||
"isGroupChat": True,
|
||
"conversationId": "cid_img_dl",
|
||
"msgId": "msg005b",
|
||
"msgtype": "image",
|
||
"downloadCode": "dc12345",
|
||
}
|
||
# ruff: noqa: E402
|
||
from yuxi.channels.adapters.dingding.normalizer import extract_attachments # noqa: E402
|
||
|
||
att_img = extract_attachments(raw_img_dl)
|
||
assert len(att_img) == 1
|
||
assert att_img[0].type == "image"
|
||
assert att_img[0].file_id == "dc12345"
|
||
print("Test 5b (Image attachment extraction) PASSED")
|
||
|
||
# Test 6: Voice (audio) message
|
||
raw_voice = {
|
||
"senderId": "user333",
|
||
"isGroupChat": False,
|
||
"conversationId": "cid_dm_333",
|
||
"msgId": "msg006",
|
||
"msgtype": "voice",
|
||
}
|
||
msg6 = normalize_inbound(channel_id, channel_type, raw_voice)
|
||
assert msg6.message_type == MessageType.AUDIO
|
||
assert msg6.content == "[语音]"
|
||
print("Test 6 (Voice message) PASSED")
|
||
|
||
# Test 6b: Video message
|
||
raw_video = {
|
||
"senderId": "user444",
|
||
"isGroupChat": False,
|
||
"conversationId": "cid_dm_444",
|
||
"msgId": "msg006b",
|
||
"msgtype": "video",
|
||
}
|
||
msg6b = normalize_inbound(channel_id, channel_type, raw_video)
|
||
assert msg6b.message_type == MessageType.VIDEO
|
||
assert msg6b.content == "[视频]"
|
||
print("Test 6b (Video message) PASSED")
|
||
|
||
# Test 6c: File message with fileName
|
||
raw_file = {
|
||
"senderId": "user999",
|
||
"isGroupChat": False,
|
||
"conversationId": "cid_dm_999",
|
||
"msgId": "msg006c",
|
||
"msgtype": "file",
|
||
"fileName": "report.pdf",
|
||
}
|
||
msg6c = normalize_inbound(channel_id, channel_type, raw_file)
|
||
assert msg6c.message_type == MessageType.FILE
|
||
assert msg6c.content == "[文件: report.pdf]"
|
||
print("Test 6c (File message) PASSED")
|
||
|
||
# Test 7: Session helpers
|
||
# ruff: noqa: E402
|
||
from yuxi.channels.adapters.dingding.session import ( # noqa: E402
|
||
generate_dingding_chat_id,
|
||
resolve_dingding_chat_type,
|
||
generate_thread_key,
|
||
)
|
||
|
||
dm_raw = {"senderId": "user_dm", "isGroupChat": False, "conversationId": "cid_dm"}
|
||
assert generate_dingding_chat_id(dm_raw, "direct") == "dm_user_dm"
|
||
|
||
grp_raw = {"senderId": "user_grp", "isGroupChat": True, "conversationId": "cid_grp"}
|
||
assert generate_dingding_chat_id(grp_raw, "group") == "group_cid_grp"
|
||
|
||
assert resolve_dingding_chat_type(dm_raw) == "direct"
|
||
assert resolve_dingding_chat_type(grp_raw) == "group"
|
||
|
||
thread_key_dm = generate_thread_key("dingding", "dm_user_dm")
|
||
assert thread_key_dm == "dingding:direct:dm_user_dm"
|
||
|
||
thread_key_grp = generate_thread_key("dingding", "group_cid_grp")
|
||
assert thread_key_grp == "dingding:group:group_cid_grp"
|
||
print("Test 7 (Session helpers) PASSED")
|
||
|
||
# Test 8: Formatter
|
||
# ruff: noqa: E402
|
||
from yuxi.channels.adapters.dingding.formatter import format_outbound # noqa: E402
|
||
|
||
fmt = format_outbound("Hello", chat_type="direct")
|
||
assert fmt["msgKey"] == "sampleText"
|
||
assert "content" in fmt["msgParam"]
|
||
|
||
fmt_md = format_outbound("**bold**", chat_type="direct", metadata={"use_markdown": True})
|
||
assert fmt_md["msgKey"] == "sampleMarkdown"
|
||
print("Test 8 (Formatter) PASSED")
|
||
|
||
# Test 9: Cards
|
||
# ruff: noqa: E402
|
||
from yuxi.channels.adapters.dingding.cards import ( # noqa: E402
|
||
build_dingding_text_payload,
|
||
build_dingding_markdown_payload,
|
||
build_dingding_action_card,
|
||
build_dingding_feed_card,
|
||
TEXT_CHUNK_LIMIT,
|
||
)
|
||
|
||
text_payload = build_dingding_text_payload("Hello")
|
||
assert text_payload["content"] == "Hello"
|
||
|
||
md_pl = build_dingding_markdown_payload("Title", "**Bold**")
|
||
assert md_pl["title"] == "Title"
|
||
assert md_pl["text"] == "**Bold**"
|
||
|
||
ac = build_dingding_action_card("Test", "Content", single_title="查看", single_url="https://example.com")
|
||
assert ac["msg_key"] == "sampleActionCard1"
|
||
assert ac["msg_param"]["singleTitle"] == "查看"
|
||
|
||
ac2 = build_dingding_action_card("Multi", "Buttons", buttons=[{"title": "OK", "actionURL": "https://ok.com"}])
|
||
assert ac2["msg_key"] == "sampleActionCard2"
|
||
|
||
fc = build_dingding_feed_card([{"title": "News", "messageURL": "https://news.com", "picURL": "https://img.com"}])
|
||
assert fc["msg_key"] == "sampleFeedCard"
|
||
print("Test 9 (Cards) PASSED")
|
||
|
||
# Test 10: Sign computation and verification
|
||
# ruff: noqa: E402
|
||
from yuxi.channels.adapters.dingding.sign import compute_dingtalk_sign, verify_webhook_signature # noqa: E402
|
||
import time # noqa: E402
|
||
|
||
sign = compute_dingtalk_sign("1700000000000", "test_secret")
|
||
assert sign, "Sign should not be empty"
|
||
|
||
assert verify_webhook_signature({}, "") is True
|
||
|
||
now_ms = str(int(time.time() * 1000))
|
||
valid_headers = {
|
||
"timestamp": now_ms,
|
||
"sign": compute_dingtalk_sign(now_ms, "test_secret"),
|
||
}
|
||
assert verify_webhook_signature(valid_headers, "test_secret") is True
|
||
|
||
tampered = dict(valid_headers)
|
||
tampered["sign"] = "wrong_sign"
|
||
assert verify_webhook_signature(tampered, "test_secret") is False
|
||
print("Test 10 (Sign) PASSED")
|
||
|
||
# Test 11: Token manager
|
||
# ruff: noqa: E402
|
||
from yuxi.channels.adapters.dingding.token import DingDingTokenManager # noqa: E402
|
||
|
||
tm = DingDingTokenManager("fake_appkey", "fake_secret")
|
||
assert tm._access_token is None
|
||
assert tm._is_valid() is False
|
||
tm.invalidate()
|
||
assert tm._access_token is None
|
||
print("Test 11 (Token manager) PASSED")
|
||
|
||
# Test 12: Streaming attributes and adapter methods
|
||
# ruff: noqa: E402
|
||
from yuxi.channels.adapters.dingding.adapter import DingDingChannelAdapter # noqa: E402
|
||
|
||
assert DingDingChannelAdapter.supports_streaming is True
|
||
assert "off" in DingDingChannelAdapter.streaming_modes
|
||
assert "block" in DingDingChannelAdapter.streaming_modes
|
||
|
||
adapter = DingDingChannelAdapter(config={"name": "test", "accounts": {"default": {}}})
|
||
assert adapter._pending_streams == {}
|
||
assert adapter._stream_lock is not None
|
||
print("Test 12 (Streaming attributes) PASSED")
|
||
|
||
# Test 13: Card action event type mapping
|
||
raw_card = {
|
||
"senderId": "user777",
|
||
"isGroupChat": False,
|
||
"conversationId": "cid_card",
|
||
"msgId": "msg_card",
|
||
"event_type": "card_action",
|
||
"msgtype": "actionCard",
|
||
}
|
||
msg_card = normalize_inbound(channel_id, channel_type, raw_card)
|
||
assert msg_card.event_type == EventType.CARD_ACTION
|
||
print("Test 13 (Card action event) PASSED")
|
||
|
||
# Test 14: Normalizer helpers (extract_text, extract_attachments)
|
||
# ruff: noqa: E402
|
||
from yuxi.channels.adapters.dingding.normalizer import extract_text # noqa: E402
|
||
|
||
assert extract_text({"msgtype": "image", "text": {}}) == "[图片]"
|
||
assert extract_text({"msgtype": "voice"}) == "[语音]"
|
||
assert extract_text({"msgtype": "text", "text": {"content": "hi"}}) == "hi"
|
||
print("Test 14 (extract_text helper) PASSED")
|
||
|
||
print(f"\nAll 14 tests PASSED! TEXT_CHUNK_LIMIT={TEXT_CHUNK_LIMIT}")
|