新增了Twitch、Telegram、Discord、Slack、Mattermost、WeChat、Zalo等多渠道的单元测试用例,覆盖了令牌处理、速率限制、消息去重、会话解析、格式转换、安全策略等模块 同时在测试配置中添加了测试用的OpenAI API密钥环境变量
849 lines
32 KiB
Python
849 lines
32 KiB
Python
"""Unit tests for Feishu/Lark channel adapter.
|
|
|
|
Tests the normalizer, formatter, cards, session helpers, and verify modules.
|
|
"""
|
|
from yuxi.channels.adapters.feishu.adapter import FeishuAdapter
|
|
from yuxi.channels.adapters.feishu.cards import build_feishu_card, build_feishu_text_content, TEXT_CHUNK_LIMIT
|
|
from yuxi.channels.adapters.feishu.formatter import format_outbound
|
|
from yuxi.channels.adapters.feishu.normalizer import (
|
|
extract_attachments,
|
|
normalize_inbound,
|
|
)
|
|
from yuxi.channels.adapters.feishu.session import generate_feishu_chat_id, resolve_feishu_chat_type
|
|
from yuxi.channels.adapters.feishu.verify import verify_feishu_signature
|
|
from yuxi.channels.models import ChannelStatus, ChannelType, ChatType, EventType, MessageType
|
|
|
|
CHANNEL_ID = "feishu"
|
|
CHANNEL_TYPE = ChannelType.FEISHU
|
|
|
|
|
|
class TestFeishuNormalizer:
|
|
def test_dm_text_message(self):
|
|
raw = {
|
|
"event": {
|
|
"sender": {"sender_id": {"open_id": "ou_abc123"}},
|
|
"message": {
|
|
"message_id": "om_msg001",
|
|
"chat_id": "oc_dm001",
|
|
"chat_type": "p2p",
|
|
"msg_type": "text",
|
|
"content": '{"text": "Hello"}',
|
|
},
|
|
},
|
|
"event_type": "im.message.receive_v1",
|
|
}
|
|
msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw)
|
|
assert msg.chat_type == ChatType.DIRECT
|
|
assert msg.event_type == EventType.MESSAGE_RECEIVED
|
|
assert msg.message_type == MessageType.TEXT
|
|
assert msg.identity.channel_chat_id == "feishu:dm:ou_abc123"
|
|
assert msg.identity.channel_user_id == "ou_abc123"
|
|
assert msg.identity.channel_message_id == "om_msg001"
|
|
|
|
def test_group_message_with_mentions(self):
|
|
raw = {
|
|
"event": {
|
|
"sender": {"sender_id": {"open_id": "ou_user2"}},
|
|
"message": {
|
|
"message_id": "om_msg002",
|
|
"chat_id": "oc_group001",
|
|
"chat_type": "group",
|
|
"msg_type": "text",
|
|
"content": '{"text": "@bot hello"}',
|
|
"mentions": [{"key": "ou_bot", "name": "AI助手"}],
|
|
},
|
|
},
|
|
"event_type": "im.message.receive_v1",
|
|
}
|
|
msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw)
|
|
assert msg.chat_type == ChatType.GROUP
|
|
assert msg.identity.channel_chat_id == "feishu:group:oc_group001"
|
|
assert msg.mentions is not None
|
|
assert "ou_bot" in msg.mentions.mentioned_user_ids
|
|
|
|
def test_card_action_event(self):
|
|
raw = {
|
|
"event": {
|
|
"sender": {"sender_id": {"open_id": "ou_user3"}},
|
|
"message": {
|
|
"message_id": "om_msg003",
|
|
"chat_id": "oc_dm001",
|
|
"chat_type": "p2p",
|
|
"msg_type": "interactive",
|
|
},
|
|
},
|
|
"event_type": "card.action.trigger",
|
|
}
|
|
msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw)
|
|
assert msg.event_type == EventType.CARD_ACTION
|
|
assert msg.message_type == MessageType.CARD
|
|
|
|
def test_bot_added_event(self):
|
|
raw = {
|
|
"event": {
|
|
"sender": {"sender_id": {"open_id": "ou_admin"}},
|
|
"message": {"chat_id": "oc_group002", "chat_type": "group"},
|
|
},
|
|
"event_type": "im.chat.member.bot.added_v1",
|
|
}
|
|
msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw)
|
|
assert msg.event_type == EventType.BOT_ADDED
|
|
|
|
def test_bot_removed_event(self):
|
|
raw = {
|
|
"event": {
|
|
"sender": {"sender_id": {"open_id": "ou_admin"}},
|
|
"message": {"chat_id": "oc_group002", "chat_type": "group"},
|
|
},
|
|
"event_type": "im.chat.member.bot.deleted_v1",
|
|
}
|
|
msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw)
|
|
assert msg.event_type == EventType.BOT_REMOVED
|
|
|
|
def test_thread_message(self):
|
|
raw = {
|
|
"event": {
|
|
"sender": {"sender_id": {"open_id": "ou_user4"}},
|
|
"message": {
|
|
"message_id": "om_msg006",
|
|
"chat_id": "oc_group001",
|
|
"chat_type": "group",
|
|
"msg_type": "text",
|
|
"content": '{"text": "reply"}',
|
|
"root_id": "om_root001",
|
|
"parent_id": "om_parent001",
|
|
},
|
|
},
|
|
"event_type": "im.message.receive_v1",
|
|
}
|
|
msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw)
|
|
assert msg.chat_type == ChatType.THREAD
|
|
assert msg.metadata.get("root_id") == "om_root001"
|
|
assert msg.metadata.get("parent_id") == "om_parent001"
|
|
|
|
def test_post_message_type(self):
|
|
raw = {
|
|
"event": {
|
|
"sender": {"sender_id": {"open_id": "ou_user5"}},
|
|
"message": {
|
|
"message_id": "om_msg007",
|
|
"chat_id": "oc_dm002",
|
|
"chat_type": "p2p",
|
|
"msg_type": "post",
|
|
"content": {
|
|
"zh_cn": {
|
|
"title": "post title",
|
|
"content": [[{"tag": "text", "text": "rich text"}]],
|
|
}
|
|
},
|
|
},
|
|
},
|
|
"event_type": "im.message.receive_v1",
|
|
}
|
|
msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw)
|
|
assert msg.message_type == MessageType.TEXT
|
|
assert "rich text" in msg.content
|
|
|
|
def test_image_message_type(self):
|
|
raw = {
|
|
"event": {
|
|
"sender": {"sender_id": {"open_id": "ou_user6"}},
|
|
"message": {
|
|
"message_id": "om_msg008",
|
|
"chat_id": "oc_dm001",
|
|
"chat_type": "p2p",
|
|
"msg_type": "image",
|
|
},
|
|
},
|
|
"event_type": "im.message.receive_v1",
|
|
}
|
|
msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw)
|
|
assert msg.message_type == MessageType.IMAGE
|
|
|
|
def test_audio_message_type(self):
|
|
raw = {
|
|
"event": {
|
|
"sender": {"sender_id": {"open_id": "ou_user7"}},
|
|
"message": {
|
|
"message_id": "om_msg009",
|
|
"chat_id": "oc_dm001",
|
|
"chat_type": "p2p",
|
|
"msg_type": "audio",
|
|
},
|
|
},
|
|
"event_type": "im.message.receive_v1",
|
|
}
|
|
msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw)
|
|
assert msg.message_type == MessageType.AUDIO
|
|
|
|
def test_file_message_type(self):
|
|
raw = {
|
|
"event": {
|
|
"sender": {"sender_id": {"open_id": "ou_user8"}},
|
|
"message": {
|
|
"message_id": "om_msg010",
|
|
"chat_id": "oc_dm001",
|
|
"chat_type": "p2p",
|
|
"msg_type": "file",
|
|
},
|
|
},
|
|
"event_type": "im.message.receive_v1",
|
|
}
|
|
msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw)
|
|
assert msg.message_type == MessageType.FILE
|
|
|
|
def test_image_attachment_extraction(self):
|
|
raw = {
|
|
"event": {
|
|
"sender": {"sender_id": {"open_id": "ou_user1"}},
|
|
"message": {
|
|
"message_id": "om_img001",
|
|
"chat_id": "oc_dm001",
|
|
"chat_type": "p2p",
|
|
"msg_type": "image",
|
|
"content": '{"image_key": "img_abc123"}',
|
|
},
|
|
},
|
|
"event_type": "im.message.receive_v1",
|
|
}
|
|
msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw)
|
|
assert msg.content == "[图片]"
|
|
assert len(msg.attachments) == 1
|
|
assert msg.attachments[0].type == "image"
|
|
|
|
import json
|
|
parsed = json.loads(msg.attachments[0].file_id)
|
|
assert parsed["file_key"] == "img_abc123"
|
|
assert parsed["file_type"] == "image"
|
|
|
|
def test_file_attachment_extraction(self):
|
|
raw = {
|
|
"event": {
|
|
"sender": {"sender_id": {"open_id": "ou_user2"}},
|
|
"message": {
|
|
"message_id": "om_file001",
|
|
"chat_id": "oc_dm001",
|
|
"chat_type": "p2p",
|
|
"msg_type": "file",
|
|
"content": '{"file_key": "file_xyz789", "file_name": "report.pdf", "file_size": 1024}',
|
|
},
|
|
},
|
|
"event_type": "im.message.receive_v1",
|
|
}
|
|
msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw)
|
|
assert msg.content == "[文件: report.pdf]"
|
|
assert len(msg.attachments) == 1
|
|
assert msg.attachments[0].type == "file"
|
|
assert msg.attachments[0].filename == "report.pdf"
|
|
assert msg.attachments[0].size_bytes == 1024
|
|
|
|
def test_audio_attachment_extraction(self):
|
|
raw = {
|
|
"event": {
|
|
"sender": {"sender_id": {"open_id": "ou_user3"}},
|
|
"message": {
|
|
"message_id": "om_audio001",
|
|
"chat_id": "oc_dm001",
|
|
"chat_type": "p2p",
|
|
"msg_type": "audio",
|
|
"content": '{"file_key": "audio_key_001"}',
|
|
},
|
|
},
|
|
"event_type": "im.message.receive_v1",
|
|
}
|
|
msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw)
|
|
assert msg.content == "[语音]"
|
|
assert len(msg.attachments) == 1
|
|
assert msg.attachments[0].type == "audio"
|
|
|
|
def test_video_attachment_extraction(self):
|
|
raw = {
|
|
"event": {
|
|
"sender": {"sender_id": {"open_id": "ou_user4"}},
|
|
"message": {
|
|
"message_id": "om_video001",
|
|
"chat_id": "oc_dm001",
|
|
"chat_type": "p2p",
|
|
"msg_type": "video",
|
|
"content": '{"image_key": "vid_cover_001", "file_key": "vid_file_001"}',
|
|
},
|
|
},
|
|
"event_type": "im.message.receive_v1",
|
|
}
|
|
msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw)
|
|
assert msg.content == "[视频]"
|
|
assert len(msg.attachments) == 2
|
|
types = {a.type for a in msg.attachments}
|
|
assert types == {"image", "video"}
|
|
cover = [a for a in msg.attachments if a.type == "image"][0]
|
|
assert cover.metadata.get("role") == "cover"
|
|
|
|
def test_sticker_attachment_extraction(self):
|
|
raw = {
|
|
"event": {
|
|
"sender": {"sender_id": {"open_id": "ou_user5"}},
|
|
"message": {
|
|
"message_id": "om_sticker001",
|
|
"chat_id": "oc_dm001",
|
|
"chat_type": "p2p",
|
|
"msg_type": "sticker",
|
|
"content": '{"file_key": "sticker_key_001"}',
|
|
},
|
|
},
|
|
"event_type": "im.message.receive_v1",
|
|
}
|
|
msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw)
|
|
assert msg.content == "[贴纸]"
|
|
assert len(msg.attachments) == 1
|
|
assert msg.attachments[0].type == "sticker"
|
|
|
|
def test_text_message_no_attachments(self):
|
|
raw = {
|
|
"event": {
|
|
"sender": {"sender_id": {"open_id": "ou_user6"}},
|
|
"message": {
|
|
"message_id": "om_text001",
|
|
"chat_id": "oc_dm001",
|
|
"chat_type": "p2p",
|
|
"msg_type": "text",
|
|
"content": '{"text": "hello"}',
|
|
},
|
|
},
|
|
"event_type": "im.message.receive_v1",
|
|
}
|
|
msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw)
|
|
assert msg.attachments == []
|
|
|
|
def test_extract_attachments_image(self):
|
|
event = {
|
|
"message": {
|
|
"message_id": "om_123",
|
|
"msg_type": "image",
|
|
"content": '{"image_key": "img_abc"}',
|
|
}
|
|
}
|
|
attachments = extract_attachments(event)
|
|
assert len(attachments) == 1
|
|
assert attachments[0].type == "image"
|
|
assert attachments[0].file_id
|
|
|
|
def test_extract_attachments_file(self):
|
|
event = {
|
|
"message": {
|
|
"message_id": "om_456",
|
|
"msg_type": "file",
|
|
"content": '{"file_key": "file_xyz", "file_name": "doc.pdf", "file_size": 2048}',
|
|
}
|
|
}
|
|
attachments = extract_attachments(event)
|
|
assert len(attachments) == 1
|
|
assert attachments[0].type == "file"
|
|
assert attachments[0].filename == "doc.pdf"
|
|
assert attachments[0].size_bytes == 2048
|
|
|
|
def test_extract_attachments_empty_for_text(self):
|
|
event = {
|
|
"message": {
|
|
"message_id": "om_789",
|
|
"msg_type": "text",
|
|
"content": '{"text": "hello"}',
|
|
}
|
|
}
|
|
assert extract_attachments(event) == []
|
|
|
|
def test_extract_attachments_media_combined(self):
|
|
event = {
|
|
"message": {
|
|
"message_id": "om_media001",
|
|
"msg_type": "media",
|
|
"content": '{"items": ['
|
|
'{"msg_type": "image", "content": "{\\"image_key\\": \\"img_1\\"}"},'
|
|
'{"msg_type": "image", "content": "{\\"image_key\\": \\"img_2\\"}"}'
|
|
"]}",
|
|
}
|
|
}
|
|
attachments = extract_attachments(event)
|
|
assert len(attachments) == 2
|
|
assert all(a.type == "image" for a in attachments)
|
|
|
|
def test_unknown_event_type_defaults_to_message_received(self):
|
|
raw = {
|
|
"event": {
|
|
"sender": {"sender_id": {"open_id": "ou_user9"}},
|
|
"message": {
|
|
"message_id": "om_msg011",
|
|
"chat_id": "oc_dm001",
|
|
"chat_type": "p2p",
|
|
"msg_type": "text",
|
|
"content": '{"text": "test"}',
|
|
},
|
|
},
|
|
"event_type": "unknown.event.type",
|
|
}
|
|
msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw)
|
|
assert msg.event_type == EventType.MESSAGE_RECEIVED
|
|
|
|
def test_no_mentions(self):
|
|
raw = {
|
|
"event": {
|
|
"sender": {"sender_id": {"open_id": "ou_user10"}},
|
|
"message": {
|
|
"message_id": "om_msg012",
|
|
"chat_id": "oc_dm001",
|
|
"chat_type": "p2p",
|
|
"msg_type": "text",
|
|
"content": '{"text": "plain text"}',
|
|
},
|
|
},
|
|
"event_type": "im.message.receive_v1",
|
|
}
|
|
msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw)
|
|
assert msg.mentions is not None
|
|
assert msg.mentions.mentioned_user_ids == []
|
|
|
|
def test_metadata(self):
|
|
raw = {
|
|
"event": {
|
|
"sender": {"sender_id": {"open_id": "ou_user11"}},
|
|
"message": {
|
|
"message_id": "om_msg013",
|
|
"chat_id": "oc_dm001",
|
|
"chat_type": "p2p",
|
|
"msg_type": "text",
|
|
"content": '{"text": "test"}',
|
|
},
|
|
},
|
|
"event_type": "im.message.receive_v1",
|
|
}
|
|
msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw)
|
|
assert msg.metadata.get("msg_type") == "text"
|
|
assert msg.metadata.get("chat_type_raw") == "p2p"
|
|
|
|
def test_bot_mentioned(self):
|
|
raw = {
|
|
"event": {
|
|
"sender": {"sender_id": {"open_id": "ou_user12"}},
|
|
"message": {
|
|
"message_id": "om_msg014",
|
|
"chat_id": "oc_group001",
|
|
"chat_type": "group",
|
|
"msg_type": "text",
|
|
"content": '{"text": "@bot help"}',
|
|
"mentions": [{"key": "ou_bot123", "name": "AI助手"}],
|
|
},
|
|
},
|
|
"event_type": "im.message.receive_v1",
|
|
}
|
|
msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw, bot_open_id="ou_bot123")
|
|
assert msg.mentions is not None
|
|
assert msg.mentions.is_bot_mentioned is True
|
|
|
|
def test_all_users_mentioned(self):
|
|
raw = {
|
|
"event": {
|
|
"sender": {"sender_id": {"open_id": "ou_user13"}},
|
|
"message": {
|
|
"message_id": "om_msg015",
|
|
"chat_id": "oc_group001",
|
|
"chat_type": "group",
|
|
"msg_type": "text",
|
|
"content": '{"text": "@所有人 hello"}',
|
|
"mentions": [{"key": "@all_users", "name": "所有人"}],
|
|
},
|
|
},
|
|
"event_type": "im.message.receive_v1",
|
|
}
|
|
msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw)
|
|
assert msg.mentions is not None
|
|
assert msg.mentions.is_bot_mentioned is True
|
|
|
|
def test_bot_not_mentioned(self):
|
|
raw = {
|
|
"event": {
|
|
"sender": {"sender_id": {"open_id": "ou_user14"}},
|
|
"message": {
|
|
"message_id": "om_msg016",
|
|
"chat_id": "oc_group001",
|
|
"chat_type": "group",
|
|
"msg_type": "text",
|
|
"content": '{"text": "@someone hello"}',
|
|
"mentions": [{"key": "ou_other", "name": "Someone"}],
|
|
},
|
|
},
|
|
"event_type": "im.message.receive_v1",
|
|
}
|
|
msg = normalize_inbound(CHANNEL_ID, CHANNEL_TYPE, raw, bot_open_id="ou_bot123")
|
|
assert msg.mentions is not None
|
|
assert msg.mentions.is_bot_mentioned is False
|
|
|
|
|
|
class TestFeishuFormatter:
|
|
def test_basic_format(self):
|
|
fmt = format_outbound("Hello", chat_type="private")
|
|
assert fmt["content"] == "Hello"
|
|
assert fmt["chat_type"] == "private"
|
|
|
|
def test_format_with_buttons(self):
|
|
fmt = format_outbound(
|
|
"Click me",
|
|
chat_type="private",
|
|
metadata={"buttons": [{"text": "OK", "action": "confirm"}]},
|
|
)
|
|
assert fmt["buttons"] == [{"text": "OK", "action": "confirm"}]
|
|
|
|
def test_format_with_thread_id(self):
|
|
fmt = format_outbound("Reply", chat_type="group", metadata={"thread_id": "om_thread_123"})
|
|
assert fmt["thread_id"] == "om_thread_123"
|
|
|
|
|
|
class TestFeishuCards:
|
|
def test_text_content(self):
|
|
result = build_feishu_text_content("Hello")
|
|
assert result == {"text": "Hello"}
|
|
|
|
def test_basic_card(self):
|
|
card = build_feishu_card("Hello World", title="Test")
|
|
assert card["header"]["template"] == "blue"
|
|
assert card["header"]["title"]["content"] == "Test"
|
|
assert len(card["elements"]) >= 1
|
|
assert "Hello World" in card["elements"][0]["content"]
|
|
|
|
def test_card_with_buttons(self):
|
|
card = build_feishu_card("Click me", buttons=[{"text": "OK", "action": "ok"}])
|
|
has_action = any(elem.get("tag") == "action" for elem in card["elements"])
|
|
assert has_action
|
|
|
|
def test_card_with_streaming(self):
|
|
card = build_feishu_card("Loading", streaming=True)
|
|
assert "回复中" in card["header"]["title"]["content"]
|
|
|
|
def test_text_chunk_limit(self):
|
|
assert TEXT_CHUNK_LIMIT == 30000
|
|
|
|
def test_card_with_images(self):
|
|
card = build_feishu_card("Look at this", images=["img_key_001", "img_key_002"])
|
|
img_elements = [e for e in card["elements"] if e.get("tag") == "img"]
|
|
assert len(img_elements) == 2
|
|
assert img_elements[0]["img_key"] == "img_key_001"
|
|
assert img_elements[1]["img_key"] == "img_key_002"
|
|
|
|
def test_card_with_files(self):
|
|
card = build_feishu_card(
|
|
"Files attached",
|
|
files=[{"name": "report.pdf", "url": "https://example.com/report.pdf"}],
|
|
)
|
|
markdown_elements = [e for e in card["elements"] if e.get("tag") == "markdown"]
|
|
file_md = [e for e in markdown_elements if "report.pdf" in e.get("content", "")]
|
|
assert len(file_md) == 1
|
|
|
|
def test_card_with_files_no_url(self):
|
|
card = build_feishu_card(
|
|
"Files attached",
|
|
files=[{"name": "data.csv"}],
|
|
)
|
|
markdown_elements = [e for e in card["elements"] if e.get("tag") == "markdown"]
|
|
file_md = [e for e in markdown_elements if "data.csv" in e.get("content", "")]
|
|
assert len(file_md) == 1
|
|
|
|
def test_card_with_note(self):
|
|
card = build_feishu_card("Hello", note="仅供参考")
|
|
note_elements = [e for e in card["elements"] if e.get("tag") == "note"]
|
|
assert len(note_elements) == 1
|
|
assert note_elements[0]["elements"][0]["content"] == "仅供参考"
|
|
|
|
def test_card_with_all_features(self):
|
|
card = build_feishu_card(
|
|
"Main content",
|
|
title="综合卡片",
|
|
images=["img_1"],
|
|
files=[{"name": "data.csv", "url": "https://example.com/data.csv"}],
|
|
buttons=[{"text": "确认", "action": "ok"}],
|
|
url_unfurl=["https://example.com"],
|
|
note="此消息由AI生成",
|
|
streaming=False,
|
|
)
|
|
tags = {e.get("tag") for e in card["elements"]}
|
|
assert "img" in tags
|
|
assert "markdown" in tags
|
|
assert "action" in tags
|
|
assert "note" in tags
|
|
assert card["header"]["title"]["content"] == "综合卡片"
|
|
|
|
|
|
class TestFeishuSession:
|
|
def test_dm_chat_id(self):
|
|
assert generate_feishu_chat_id(raw_chat_id="ou_123", chat_type="direct") == "feishu:dm:ou_123"
|
|
|
|
def test_group_chat_id(self):
|
|
assert generate_feishu_chat_id(raw_chat_id="oc_456", chat_type="group") == "feishu:group:oc_456"
|
|
|
|
def test_dm_chat_type(self):
|
|
assert resolve_feishu_chat_type("ou_123") == "direct"
|
|
|
|
def test_group_chat_type(self):
|
|
assert resolve_feishu_chat_type("oc_456") == "group"
|
|
|
|
def test_thread_chat_type(self):
|
|
assert resolve_feishu_chat_type("oc_thread_123") == "group"
|
|
|
|
def test_thread_chat_type_by_parent_id(self):
|
|
assert resolve_feishu_chat_type("oc_456") == "group"
|
|
|
|
def test_default_chat_type(self):
|
|
assert resolve_feishu_chat_type("bt_unknown") == "group"
|
|
|
|
|
|
class TestFeishuVerify:
|
|
def test_empty_headers_fail_closed(self):
|
|
assert verify_feishu_signature({}, b"") is False
|
|
|
|
def test_missing_signature_headers(self):
|
|
headers = {"X-Lark-Request-Timestamp": "123"}
|
|
assert verify_feishu_signature(headers, b"") is False
|
|
|
|
def test_valid_signature_with_encrypt_key_and_body(self):
|
|
encrypt_key = "test_encrypt_key_32_bytes_long_"
|
|
timestamp = "1234567890"
|
|
nonce = "abcdefgh"
|
|
body = b'{"challenge":"test_challenge_value","token":"v_token","type":"url_verification"}'
|
|
|
|
import hashlib
|
|
raw = f"{timestamp}{nonce}{encrypt_key}".encode("utf-8") + body
|
|
expected_signature = hashlib.sha256(raw).hexdigest()
|
|
|
|
headers = {
|
|
"X-Lark-Request-Timestamp": timestamp,
|
|
"X-Lark-Request-Nonce": nonce,
|
|
"X-Lark-Signature": expected_signature,
|
|
}
|
|
|
|
assert verify_feishu_signature(headers, body, encrypt_key) is True
|
|
|
|
def test_invalid_signature_with_wrong_encrypt_key(self):
|
|
encrypt_key = "test_encrypt_key_32_bytes_long_"
|
|
wrong_key = "wrong_encrypt_key_32_bytes_xxxx_"
|
|
timestamp = "1234567890"
|
|
nonce = "abcdefgh"
|
|
body = b'{"challenge":"test_challenge_value"}'
|
|
|
|
import hashlib
|
|
raw = f"{timestamp}{nonce}{wrong_key}".encode("utf-8") + body
|
|
fake_signature = hashlib.sha256(raw).hexdigest()
|
|
|
|
headers = {
|
|
"X-Lark-Request-Timestamp": timestamp,
|
|
"X-Lark-Request-Nonce": nonce,
|
|
"X-Lark-Signature": fake_signature,
|
|
}
|
|
|
|
assert verify_feishu_signature(headers, body, encrypt_key) is False
|
|
|
|
def test_signature_verification_fails_with_wrong_body(self):
|
|
encrypt_key = "test_encrypt_key_32_bytes_long_"
|
|
timestamp = "1234567890"
|
|
nonce = "abcdefgh"
|
|
original_body = b'{"challenge":"original"}'
|
|
tampered_body = b'{"challenge":"tampered"}'
|
|
|
|
import hashlib
|
|
raw = f"{timestamp}{nonce}{encrypt_key}".encode("utf-8") + original_body
|
|
signature = hashlib.sha256(raw).hexdigest()
|
|
|
|
headers = {
|
|
"X-Lark-Request-Timestamp": timestamp,
|
|
"X-Lark-Request-Nonce": nonce,
|
|
"X-Lark-Signature": signature,
|
|
}
|
|
|
|
assert verify_feishu_signature(headers, tampered_body, encrypt_key) is False
|
|
|
|
def test_signature_case_insensitive_headers(self):
|
|
encrypt_key = "test_encrypt_key_32_bytes_long_"
|
|
timestamp = "1234567890"
|
|
nonce = "abcdefgh"
|
|
body = b'{"challenge":"test"}'
|
|
|
|
import hashlib
|
|
raw = f"{timestamp}{nonce}{encrypt_key}".encode("utf-8") + body
|
|
signature = hashlib.sha256(raw).hexdigest()
|
|
|
|
headers = {
|
|
"x-lark-request-timestamp": timestamp,
|
|
"x-lark-request-nonce": nonce,
|
|
"x-lark-signature": signature,
|
|
}
|
|
|
|
# Headers are accessed with case-sensitive keys by the implementation
|
|
# This tests the actual behavior - headers dict keys should match exactly
|
|
assert verify_feishu_signature(headers, body, encrypt_key) is False
|
|
|
|
|
|
class TestFeishuSessionDefensive:
|
|
def test_generate_chat_id_with_missing_sender(self):
|
|
chat_id = generate_feishu_chat_id(raw_chat_id="oc_group123", chat_type="group")
|
|
assert chat_id == "feishu:group:oc_group123"
|
|
|
|
def test_generate_chat_id_with_none_sender(self):
|
|
chat_id = generate_feishu_chat_id(raw_chat_id="oc_group123", chat_type="group", sender_id=None)
|
|
assert chat_id == "feishu:group:oc_group123"
|
|
|
|
def test_generate_chat_id_with_empty_sender_dict(self):
|
|
chat_id = generate_feishu_chat_id(raw_chat_id="oc_group123", chat_type="group")
|
|
assert chat_id == "feishu:group:oc_group123"
|
|
|
|
def test_generate_chat_id_full_event(self):
|
|
chat_id = generate_feishu_chat_id(
|
|
raw_chat_id="oc_chat456",
|
|
chat_type="group",
|
|
root_id="om_root123",
|
|
sender_id="ou_sender456",
|
|
)
|
|
assert chat_id == "feishu:group:om_root123:oc_chat456"
|
|
|
|
|
|
class TestFeishuNormalizerValidation:
|
|
CHANNEL_ID = "feishu"
|
|
CHANNEL_TYPE = ChannelType.FEISHU
|
|
|
|
def test_invalid_raw_payload_not_dict(self):
|
|
import pytest
|
|
with pytest.raises(ValueError, match="Invalid raw_payload"):
|
|
normalize_inbound(self.CHANNEL_ID, self.CHANNEL_TYPE, "not_a_dict")
|
|
|
|
def test_invalid_raw_payload_missing_event(self):
|
|
import pytest
|
|
with pytest.raises(ValueError, match="Invalid raw_payload"):
|
|
normalize_inbound(self.CHANNEL_ID, self.CHANNEL_TYPE, {"not_event": "value"})
|
|
|
|
def test_invalid_raw_payload_none(self):
|
|
import pytest
|
|
with pytest.raises(ValueError, match="Invalid raw_payload"):
|
|
normalize_inbound(self.CHANNEL_ID, self.CHANNEL_TYPE, None)
|
|
|
|
|
|
class TestFeishuAnalysisModule:
|
|
def test_orchestrator_import(self):
|
|
from yuxi.channels.adapters.feishu.analysis import FeishuAnalyzer
|
|
analyzer = FeishuAnalyzer()
|
|
assert analyzer is not None
|
|
assert len(analyzer._agents) == 3
|
|
|
|
def test_web_research_agent(self):
|
|
from yuxi.channels.adapters.feishu.analysis.sub_agents import SubAgentWebResearch
|
|
agent = SubAgentWebResearch()
|
|
assert agent is not None
|
|
|
|
def test_functional_research_agent(self):
|
|
from yuxi.channels.adapters.feishu.analysis.sub_agents import SubAgentFunctionalResearch
|
|
agent = SubAgentFunctionalResearch()
|
|
assert agent is not None
|
|
|
|
def test_issue_research_agent(self):
|
|
from yuxi.channels.adapters.feishu.analysis.sub_agents import SubAgentIssueResearch
|
|
agent = SubAgentIssueResearch()
|
|
assert agent is not None
|
|
|
|
def test_report_generator_import(self):
|
|
from yuxi.channels.adapters.feishu.analysis.report_generator import (
|
|
GapReport,
|
|
generate_gap_report,
|
|
)
|
|
assert GapReport is not None
|
|
assert generate_gap_report is not None
|
|
|
|
|
|
class TestReplyMessage:
|
|
async def test_reply_message_no_sdk(self, monkeypatch):
|
|
import yuxi.channels.adapters.feishu.send as send_mod
|
|
monkeypatch.setattr(send_mod, "HAS_LARK_SDK", False)
|
|
result = await send_mod.reply_message(None, "msg_123", "hello")
|
|
assert result.success is False
|
|
assert "not available" in result.error
|
|
|
|
async def test_reply_message_text(self):
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
mock_client = MagicMock()
|
|
mock_resp = MagicMock()
|
|
mock_resp.success.return_value = True
|
|
mock_resp.data = {"message_id": "reply_msg_456"}
|
|
mock_client.im.v1.message.reply = AsyncMock(return_value=mock_resp)
|
|
|
|
from yuxi.channels.adapters.feishu.send import reply_message
|
|
result = await reply_message(mock_client, "msg_123", "hello")
|
|
assert result.success is True
|
|
assert result.message_id == "reply_msg_456"
|
|
|
|
async def test_reply_message_with_buttons(self):
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
mock_client = MagicMock()
|
|
mock_resp = MagicMock()
|
|
mock_resp.success.return_value = True
|
|
mock_resp.data = {"message_id": "reply_card_msg"}
|
|
mock_client.im.v1.message.reply = AsyncMock(return_value=mock_resp)
|
|
|
|
from yuxi.channels.adapters.feishu.send import reply_message
|
|
buttons = [{"text": "OK", "value": "confirm"}]
|
|
result = await reply_message(mock_client, "msg_123", "Choose an option", buttons=buttons)
|
|
assert result.success is True
|
|
assert result.message_id == "reply_card_msg"
|
|
|
|
async def test_reply_message_api_error(self):
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
mock_client = MagicMock()
|
|
mock_resp = MagicMock()
|
|
mock_resp.success.return_value = False
|
|
mock_resp.msg = "permission denied"
|
|
mock_client.im.v1.message.reply = AsyncMock(return_value=mock_resp)
|
|
|
|
from yuxi.channels.adapters.feishu.send import reply_message
|
|
result = await reply_message(mock_client, "msg_123", "hello")
|
|
assert result.success is False
|
|
assert "permission denied" in result.error
|
|
|
|
|
|
class TestTokenRefresh:
|
|
def test_token_fields_initialized(self):
|
|
adapter = FeishuAdapter(config={"app_id": "test", "app_secret": "test"})
|
|
assert adapter._token_expire_at == 0
|
|
assert adapter._token_refresh_task is None
|
|
|
|
def test_disconnect_cancels_token_refresh(self):
|
|
import asyncio
|
|
adapter = FeishuAdapter(config={"app_id": "test", "app_secret": "test"})
|
|
adapter._status = ChannelStatus.CONNECTED
|
|
adapter._ws_task = asyncio.Future()
|
|
adapter._token_refresh_task = asyncio.Future()
|
|
|
|
loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(loop)
|
|
try:
|
|
loop.run_until_complete(adapter.disconnect())
|
|
finally:
|
|
loop.close()
|
|
|
|
assert adapter._token_refresh_task is None
|
|
assert adapter._status == ChannelStatus.DISCONNECTED
|
|
|
|
def test_receive_method_removed(self):
|
|
adapter = FeishuAdapter(config={"app_id": "test", "app_secret": "test"})
|
|
assert "receive" not in type(adapter).__dict__
|
|
|
|
|
|
class TestFeishuAdapterLongPoll:
|
|
def test_start_long_poll_no_sdk(self, monkeypatch):
|
|
import pytest
|
|
|
|
import yuxi.channels.adapters.feishu.adapter as adapter_mod
|
|
|
|
monkeypatch.setattr(adapter_mod, "HAS_LARK_SDK", False)
|
|
adapter = FeishuAdapter(config={"app_id": "test", "app_secret": "test"})
|
|
with pytest.raises(Exception) as exc_info:
|
|
import asyncio
|
|
loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(loop)
|
|
try:
|
|
loop.run_until_complete(adapter._start_long_poll())
|
|
finally:
|
|
loop.close()
|
|
assert "not installed" in str(exc_info.value)
|