新增了Twitch、Telegram、Discord、Slack、Mattermost、WeChat、Zalo等多渠道的单元测试用例,覆盖了令牌处理、速率限制、消息去重、会话解析、格式转换、安全策略等模块 同时在测试配置中添加了测试用的OpenAI API密钥环境变量
254 lines
8.8 KiB
Python
254 lines
8.8 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import tempfile
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from yuxi.channels.adapters.feishu.dedup import FeishuDedupStore
|
|
from yuxi.channels.adapters.feishu.reply_dispatcher import extract_urls
|
|
from yuxi.channels.adapters.feishu.secret_resolver import resolve_secret
|
|
from yuxi.channels.adapters.feishu.comment_handler import handle_comment_event
|
|
from yuxi.channels.adapters.feishu.directory_static import FeishuStaticDirectory
|
|
from yuxi.channels.adapters.feishu.cards import (
|
|
make_date_picker,
|
|
make_datetime_picker,
|
|
make_select_person,
|
|
make_time_picker,
|
|
make_overflow_menu,
|
|
)
|
|
from yuxi.channels.models import ChannelType
|
|
|
|
|
|
class TestDedupPersistence:
|
|
def test_persist_and_load(self):
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
|
|
persist_path = f.name
|
|
|
|
try:
|
|
store = FeishuDedupStore(persist_path=persist_path, ttl_s=3600)
|
|
event = {"event": {"event_id": "evt_001", "message": {"message_id": "msg_001"}}}
|
|
assert not store.has_processed(event)
|
|
store.record_processed(event)
|
|
store.finalize_processing(event)
|
|
store._persist_to_disk()
|
|
|
|
store2 = FeishuDedupStore(persist_path=persist_path, ttl_s=3600)
|
|
assert store2.has_processed(event)
|
|
finally:
|
|
try:
|
|
os.unlink(persist_path)
|
|
except OSError:
|
|
pass
|
|
|
|
def test_persist_skips_expired(self):
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
|
|
persist_path = f.name
|
|
|
|
try:
|
|
store = FeishuDedupStore(persist_path=persist_path, ttl_s=0)
|
|
event = {"event": {"event_id": "evt_002", "message": {"message_id": "msg_002"}}}
|
|
store.record_processed(event)
|
|
store.finalize_processing(event)
|
|
store._persist_to_disk()
|
|
|
|
store2 = FeishuDedupStore(persist_path=persist_path, ttl_s=3600)
|
|
assert not store2.has_processed(event)
|
|
finally:
|
|
try:
|
|
os.unlink(persist_path)
|
|
except OSError:
|
|
pass
|
|
|
|
def test_no_persist_path_no_error(self):
|
|
store = FeishuDedupStore()
|
|
event = {"event": {"event_id": "evt_003"}}
|
|
store.record_processed(event)
|
|
store.finalize_processing(event)
|
|
assert store.has_processed(event)
|
|
|
|
|
|
class TestURLUnfurl:
|
|
def test_extract_urls_basic(self):
|
|
content = "Check https://example.com and http://test.org/path"
|
|
urls = extract_urls(content)
|
|
assert "https://example.com" in urls
|
|
assert "http://test.org/path" in urls
|
|
|
|
def test_extract_urls_max(self):
|
|
content = (
|
|
"https://a.com https://b.com https://c.com "
|
|
"https://d.com https://e.com https://f.com"
|
|
)
|
|
urls = extract_urls(content)
|
|
assert len(urls) <= 5
|
|
|
|
def test_extract_urls_no_urls(self):
|
|
assert extract_urls("Hello world") == []
|
|
|
|
def test_extract_urls_dedup(self):
|
|
content = "https://a.com https://a.com"
|
|
assert len(extract_urls(content)) == 1
|
|
|
|
def test_extract_urls_strips_trailing_punctuation(self):
|
|
content = "See https://example.com. And more."
|
|
urls = extract_urls(content)
|
|
assert "https://example.com" in urls
|
|
|
|
|
|
class TestSecretResolver:
|
|
def test_env_resolution(self, monkeypatch):
|
|
monkeypatch.setenv("TEST_SECRET", "env_value")
|
|
result = resolve_secret({}, "test_secret", "TEST_SECRET")
|
|
assert result == "env_value"
|
|
|
|
def test_file_source(self):
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
|
|
f.write("file_secret_value")
|
|
path = f.name
|
|
|
|
try:
|
|
result = resolve_secret(
|
|
{"test_key": {"source": "file", "path": path}}, "test_key"
|
|
)
|
|
assert result == "file_secret_value"
|
|
finally:
|
|
os.unlink(path)
|
|
|
|
def test_raw_source(self):
|
|
result = resolve_secret(
|
|
{"test_key": {"source": "raw", "value": "raw_value"}}, "test_key"
|
|
)
|
|
assert result == "raw_value"
|
|
|
|
def test_exec_source(self):
|
|
result = resolve_secret(
|
|
{"test_key": {"source": "exec", "command": "echo exec_value"}}, "test_key"
|
|
)
|
|
assert result == "exec_value"
|
|
|
|
def test_plain_string(self):
|
|
result = resolve_secret({"test_key": "plain_value"}, "test_key")
|
|
assert result == "plain_value"
|
|
|
|
def test_missing_file(self):
|
|
result = resolve_secret(
|
|
{"test_key": {"source": "file", "path": "/nonexistent/file"}}, "test_key"
|
|
)
|
|
assert result == ""
|
|
|
|
|
|
class TestCommentHandler:
|
|
def test_comment_event(self):
|
|
raw = {
|
|
"event": {
|
|
"comment": {
|
|
"comment_id": "cmt_001",
|
|
"content": {
|
|
"elements": [
|
|
{"textRun": {"text": "这是一个测试评论"}}
|
|
]
|
|
},
|
|
"commenter": {"open_id": "ou_user1"},
|
|
},
|
|
"doc": {"name": "测试文档", "url": "https://doc.example.com/abc", "type": "doc"},
|
|
}
|
|
}
|
|
msg = handle_comment_event("feishu", ChannelType.FEISHU, raw, "bot_001")
|
|
assert msg is not None
|
|
assert "测试文档" in msg.content
|
|
assert "测试评论" in msg.content
|
|
assert msg.metadata["doc_type"] == "doc"
|
|
assert msg.metadata["comment_id"] == "cmt_001"
|
|
|
|
def test_comment_event_no_comment(self):
|
|
raw = {"event": {}}
|
|
msg = handle_comment_event("feishu", ChannelType.FEISHU, raw)
|
|
assert msg is None
|
|
|
|
def test_comment_plain_text_content(self):
|
|
raw = {
|
|
"event": {
|
|
"comment": {
|
|
"comment_id": "cmt_002",
|
|
"content": {"text": "纯文本评论"},
|
|
"commenter": {"open_id": "ou_user2"},
|
|
}
|
|
}
|
|
}
|
|
msg = handle_comment_event("feishu", ChannelType.FEISHU, raw)
|
|
assert msg is not None
|
|
assert "纯文本评论" in msg.content
|
|
|
|
|
|
class TestStaticDirectory:
|
|
def test_peers_loaded_from_config(self):
|
|
config = {
|
|
"static_directory": {
|
|
"peers": [{"open_id": "ou_static1", "name": "静态用户1"}],
|
|
"groups": [{"chat_id": "oc_static1", "name": "静态群组1"}],
|
|
}
|
|
}
|
|
sd = FeishuStaticDirectory(config)
|
|
assert len(sd.peers) == 1
|
|
assert sd.peers[0]["open_id"] == "ou_static1"
|
|
assert sd.peers[0]["static"] is True
|
|
assert len(sd.groups) == 1
|
|
assert sd.groups[0]["chat_id"] == "oc_static1"
|
|
|
|
def test_merge_peers_no_duplicate(self):
|
|
config = {
|
|
"static_directory": {
|
|
"peers": [
|
|
{"open_id": "ou_dynamic", "name": "Static Override"},
|
|
{"open_id": "ou_static_only", "name": "Static Only"},
|
|
]
|
|
}
|
|
}
|
|
sd = FeishuStaticDirectory(config)
|
|
dynamic = [{"open_id": "ou_dynamic", "name": "Dynamic User"}]
|
|
merged = sd.merge_peers(dynamic)
|
|
assert len(merged) == 2
|
|
names = {p["name"] for p in merged}
|
|
assert "Dynamic User" in names
|
|
assert "Static Only" in names
|
|
|
|
def test_merge_groups_no_duplicate(self):
|
|
config = {"static_directory": {"groups": [{"chat_id": "oc_only", "name": "Static Group"}]}}
|
|
sd = FeishuStaticDirectory(config)
|
|
dynamic = [{"chat_id": "oc_existing", "name": "Dynamic Group"}]
|
|
merged = sd.merge_groups(dynamic)
|
|
assert len(merged) == 2
|
|
|
|
def test_empty_config(self):
|
|
sd = FeishuStaticDirectory({})
|
|
assert sd.peers == []
|
|
assert sd.groups == []
|
|
|
|
|
|
class TestSelectors:
|
|
def test_make_date_picker(self):
|
|
sel = make_date_picker(placeholder="选择日期", value="2026-05-10")
|
|
assert sel["tag"] == "date_picker"
|
|
assert sel["placeholder"]["content"] == "选择日期"
|
|
assert sel["value"] == "2026-05-10"
|
|
|
|
def test_make_time_picker(self):
|
|
sel = make_time_picker(placeholder="选择时间")
|
|
assert sel["tag"] == "time_picker"
|
|
|
|
def test_make_datetime_picker(self):
|
|
sel = make_datetime_picker(initial_datetime="2026-05-10 12:00")
|
|
assert sel["tag"] == "datetime_picker"
|
|
assert sel["initial_datetime"] == "2026-05-10 12:00"
|
|
|
|
def test_make_select_person(self):
|
|
sel = make_select_person(placeholder="选择成员")
|
|
assert sel["tag"] == "select_person"
|
|
|
|
def test_make_overflow_menu(self):
|
|
sel = make_overflow_menu(options=[{"text": "选项1", "value": "opt1"}])
|
|
assert sel["tag"] == "overflow"
|
|
assert len(sel["options"]) == 1 |