ForcePilot/backend/test/unit/channels/test_channels_feishu_optimizations.py
Kris 3264900bc9 test: 新增多渠道单元测试用例并配置测试环境变量
新增了Twitch、Telegram、Discord、Slack、Mattermost、WeChat、Zalo等多渠道的单元测试用例,覆盖了令牌处理、速率限制、消息去重、会话解析、格式转换、安全策略等模块
同时在测试配置中添加了测试用的OpenAI API密钥环境变量
2026-05-12 00:56:47 +08:00

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