ForcePilot/backend/test/unit/channels/test_dingding_adapter_quick.py

299 lines
9.9 KiB
Python
Raw Normal View History

"""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}")