1. 移除Telegram格式化测试中未使用的导入项 2. 修复Teams测试用例,添加monkeypatch参数并配置通配符开关 3. 更新钉钉适配器测试,替换弃用的流属性检查 4. 修正Twitch规范化测试,更新ROOMSTATE测试逻辑 5. 重构会话映射测试,完善数据库执行结果模拟 6. 格式化Slack块构建测试的长参数调用 7. 修复LINE适配器测试,更新能力断言和异步锁使用 8. 修正Slack会话解析测试,修复聊天类型判断错误 9. 更新能力测试,补充缺失的字段检查 10. 修复Matrix适配器测试,修正位置参数和配置校验逻辑 11. 为飞书分析模块测试添加跳过标记 12. 新增微信能力、限流、链接格式、会话路由等模块的单元测试 13. 修复Twitch适配器导入路径和测试断言 14. 新增Discord Webhook、Nextcloud Talk、Signal多账户等模块的单元测试 15. 修复Manager阶段测试的导入路径 16. 新增iMessage异常和命令处理的单元测试 17. 新增Nostr健康检查和相关模块的单元测试 18. 新增Signal守护进程和SSE重连相关测试
349 lines
12 KiB
Python
349 lines
12 KiB
Python
"""MSTeams 辅助工具类单元测试 (media/mentions/secret_input/user_agent/debounce/doctor)。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
|
|
import pytest
|
|
|
|
from yuxi.channels.adapters.msteams.media_helpers import (
|
|
get_mime_type,
|
|
extract_filename,
|
|
extract_message_id,
|
|
)
|
|
from yuxi.channels.adapters.msteams.media_tools import (
|
|
get_mime_type as mt_get_mime_type,
|
|
extract_filename as mt_extract_filename,
|
|
extract_message_id as mt_extract_message_id,
|
|
extract_attachment_urls,
|
|
get_edited_timestamp,
|
|
)
|
|
from yuxi.channels.adapters.msteams.mentions import (
|
|
parse_outbound_mentions,
|
|
build_mention_entity,
|
|
apply_mentions_to_activity,
|
|
)
|
|
from yuxi.channels.adapters.msteams.secret_input import (
|
|
normalize_secret_input,
|
|
has_configured_secret,
|
|
)
|
|
from yuxi.channels.adapters.msteams.user_agent import (
|
|
build_user_agent,
|
|
ensure_user_agent,
|
|
get_default_user_agent,
|
|
)
|
|
from yuxi.channels.adapters.msteams.debounce import DebounceManager
|
|
from yuxi.channels.adapters.msteams.doctor import (
|
|
is_mutable_allowlist_entry,
|
|
collect_mutable_allowlist_warnings,
|
|
)
|
|
|
|
|
|
class TestMediaHelpersMimeType:
|
|
def test_known_extension(self):
|
|
assert get_mime_type("file.png") == "image/png"
|
|
|
|
def test_jpeg(self):
|
|
assert get_mime_type("photo.jpg") == "image/jpeg"
|
|
|
|
def test_pdf(self):
|
|
assert get_mime_type("doc.pdf") == "application/pdf"
|
|
|
|
def test_docx(self):
|
|
assert get_mime_type("doc.docx") == "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
|
|
def test_xlsx(self):
|
|
assert get_mime_type("sheet.xlsx") == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
|
|
def test_unknown_extension(self):
|
|
assert get_mime_type("file.xyz") == "application/octet-stream"
|
|
|
|
def test_no_extension(self):
|
|
result = get_mime_type("noextension")
|
|
assert result in ("application/octet-stream", "text/plain")
|
|
|
|
def test_uppercase_extension(self):
|
|
assert get_mime_type("FILE.PNG") == "image/png"
|
|
|
|
|
|
class TestMediaHelpersExtractFilename:
|
|
def test_basic(self):
|
|
result = extract_filename("https://example.com/path/file.txt")
|
|
assert result == "file.txt"
|
|
|
|
def test_with_query_params(self):
|
|
result = extract_filename("https://example.com/file.txt?token=abc")
|
|
assert result == "file.txt"
|
|
|
|
def test_with_fragment(self):
|
|
result = extract_filename("https://example.com/file.txt#section")
|
|
assert result == "file.txt"
|
|
|
|
def test_empty_string(self):
|
|
assert extract_filename("") == "unknown"
|
|
|
|
def test_no_filename(self):
|
|
result = extract_filename("https://example.com/")
|
|
assert result == "example.com"
|
|
|
|
|
|
class TestMediaHelpersExtractMessageId:
|
|
def test_standard_format(self):
|
|
result = extract_message_id("123456789@thread.skype")
|
|
assert result == "123456789"
|
|
|
|
def test_tacv2_format(self):
|
|
result = extract_message_id("987654321@thread.tacv2")
|
|
assert result == "987654321"
|
|
|
|
def test_plain_id(self):
|
|
result = extract_message_id("simple-id")
|
|
assert result == "simple-id"
|
|
|
|
def test_empty_string(self):
|
|
assert extract_message_id("") == ""
|
|
|
|
|
|
class TestMediaToolsExtract:
|
|
def test_extract_filename(self):
|
|
activity = {"attachments": [{"name": "report.pdf"}]}
|
|
result = mt_extract_filename(activity)
|
|
assert result == "report.pdf"
|
|
|
|
def test_extract_filename_from_content(self):
|
|
activity = {"attachments": [{"content": {"name": "doc.txt"}}]}
|
|
result = mt_extract_filename(activity)
|
|
assert result == "doc.txt"
|
|
|
|
def test_extract_filename_empty(self):
|
|
activity = {"attachments": []}
|
|
result = mt_extract_filename(activity)
|
|
assert result == ""
|
|
|
|
def test_extract_message_id(self):
|
|
activity = {"id": "msg-123"}
|
|
result = mt_extract_message_id(activity)
|
|
assert result == "msg-123"
|
|
|
|
def test_extract_message_id_from_channel_data(self):
|
|
activity = {"channelData": {"id": "ch-msg-001"}}
|
|
result = mt_extract_message_id(activity)
|
|
assert result == "ch-msg-001"
|
|
|
|
def test_extract_attachment_urls(self):
|
|
activity = {
|
|
"attachments": [
|
|
{
|
|
"contentType": "image/png",
|
|
"contentUrl": "https://example.com/img.png",
|
|
"name": "photo.png",
|
|
}
|
|
]
|
|
}
|
|
urls = extract_attachment_urls(activity)
|
|
assert len(urls) == 1
|
|
assert urls[0]["url"] == "https://example.com/img.png"
|
|
|
|
def test_get_edited_timestamp(self):
|
|
activity = {"channelData": {"editedTimestamp": "2024-01-01T00:00:00Z"}}
|
|
assert get_edited_timestamp(activity) == "2024-01-01T00:00:00Z"
|
|
|
|
def test_get_edited_timestamp_fallback(self):
|
|
activity = {"editedTimestamp": "2024-01-01T00:00:00Z"}
|
|
assert get_edited_timestamp(activity) == "2024-01-01T00:00:00Z"
|
|
|
|
|
|
class TestMentions:
|
|
def test_parse_outbound_mentions_bot_prefix(self):
|
|
text = "Hello @[Bot](28:bot-id-123)"
|
|
result_text, entities = parse_outbound_mentions(text)
|
|
assert len(entities) == 1
|
|
assert entities[0]["mentioned"]["id"] == "28:bot-id-123"
|
|
|
|
def test_parse_outbound_mentions_uuid(self):
|
|
text = "Hello @[User](12345678-1234-1234-1234-123456789abc)"
|
|
result_text, entities = parse_outbound_mentions(text)
|
|
assert len(entities) == 1
|
|
assert entities[0]["mentioned"]["name"] == "User"
|
|
|
|
def test_parse_outbound_mentions_invalid(self):
|
|
text = "Hello @[Test](ab)"
|
|
result_text, entities = parse_outbound_mentions(text)
|
|
assert len(entities) == 0
|
|
|
|
def test_parse_outbound_mentions_email(self):
|
|
text = "Hey @[User](user@example.com)"
|
|
result_text, entities = parse_outbound_mentions(text)
|
|
assert len(entities) == 1
|
|
|
|
def test_build_mention_entity(self):
|
|
entity = build_mention_entity("Test", "user-1")
|
|
assert entity["type"] == "mention"
|
|
assert entity["mentioned"]["name"] == "Test"
|
|
assert entity["mentioned"]["id"] == "user-1"
|
|
|
|
def test_apply_mentions_to_activity(self):
|
|
activity = {"type": "message", "text": "hello"}
|
|
mentions = [build_mention_entity("User", "user-1")]
|
|
result = apply_mentions_to_activity(activity, mentions)
|
|
assert len(result["entities"]) == 1
|
|
|
|
|
|
class TestSecretInput:
|
|
def test_normalize_basic(self):
|
|
result = normalize_secret_input(" my-secret ")
|
|
assert result == "my-secret"
|
|
|
|
def test_normalize_multiline(self):
|
|
result = normalize_secret_input("line1\n line2 \n\nline3")
|
|
assert result == "line1 line2 line3"
|
|
|
|
def test_normalize_empty(self):
|
|
assert normalize_secret_input("") == ""
|
|
|
|
def test_normalize_strips_non_printable(self):
|
|
result = normalize_secret_input("hello\x00world")
|
|
assert result == "helloworld"
|
|
|
|
def test_has_configured_secret_valid(self):
|
|
assert has_configured_secret("my-secret-key") is True
|
|
|
|
def test_has_configured_secret_too_short(self):
|
|
assert has_configured_secret("abc") is False
|
|
|
|
def test_has_configured_secret_placeholder(self):
|
|
assert has_configured_secret("password") is False
|
|
assert has_configured_secret("changeme") is False
|
|
assert has_configured_secret("secret") is False
|
|
|
|
def test_has_configured_secret_empty(self):
|
|
assert has_configured_secret("") is False
|
|
|
|
|
|
class TestUserAgent:
|
|
def test_build_user_agent(self):
|
|
ua = build_user_agent()
|
|
assert "ForcePilot-MSTeams" in ua
|
|
|
|
def test_build_custom_product(self):
|
|
ua = build_user_agent(product="CustomApp", version="2.0")
|
|
assert "CustomApp/2.0" in ua
|
|
|
|
def test_build_with_extra(self):
|
|
ua = build_user_agent(extra="test-suite")
|
|
assert "test-suite" in ua
|
|
|
|
def test_ensure_user_agent_adds_header(self):
|
|
headers = ensure_user_agent()
|
|
assert "User-Agent" in headers
|
|
|
|
def test_ensure_user_agent_preserves_existing(self):
|
|
headers = ensure_user_agent({"Content-Type": "application/json"})
|
|
assert "Content-Type" in headers
|
|
assert "User-Agent" in headers
|
|
|
|
def test_get_default_user_agent(self):
|
|
assert "ForcePilot-MSTeams" in get_default_user_agent()
|
|
|
|
|
|
class TestDebounceManager:
|
|
@pytest.fixture
|
|
def mgr(self):
|
|
return DebounceManager(ttl=5.0)
|
|
|
|
def test_make_key(self):
|
|
key = DebounceManager.make_key("app-1", "conv-1", "user-1")
|
|
assert key == "msteams:app-1:conv-1:user-1"
|
|
|
|
def test_merge_first_time(self, mgr):
|
|
result = mgr.merge("key-1", "hello")
|
|
assert result is not None
|
|
assert result["text"] == "hello"
|
|
assert result["merge_count"] == 1
|
|
|
|
def test_merge_within_ttl(self, mgr):
|
|
mgr.merge("key-1", "hello")
|
|
result = mgr.merge("key-1", "world")
|
|
assert result is None
|
|
entry = mgr.get("key-1")
|
|
assert "hello\nworld" in entry["text"]
|
|
assert entry["merge_count"] == 2
|
|
|
|
def test_merge_after_ttl_expires(self, mgr):
|
|
quick_mgr = DebounceManager(ttl=0.01)
|
|
quick_mgr.merge("key-1", "first")
|
|
time.sleep(0.02)
|
|
result = quick_mgr.merge("key-1", "second")
|
|
assert result is not None
|
|
assert result["text"] == "second"
|
|
assert result["merge_count"] == 1
|
|
|
|
def test_merge_max_text_length(self, mgr):
|
|
long_text = "A" * 4500
|
|
mgr.merge("key-1", long_text)
|
|
result = mgr.merge("key-1", "appended")
|
|
entry = mgr.get("key-1")
|
|
assert len(entry["text"]) <= 4000
|
|
|
|
def test_consume(self, mgr):
|
|
mgr.merge("key-1", "hello")
|
|
consumed = mgr.consume("key-1")
|
|
assert consumed is not None
|
|
assert consumed["text"] == "hello"
|
|
assert mgr.get("key-1") is None
|
|
|
|
def test_consume_nonexistent(self, mgr):
|
|
assert mgr.consume("nonexistent") is None
|
|
|
|
def test_pending_count(self, mgr):
|
|
mgr.merge("key-1", "a")
|
|
mgr.merge("key-2", "b")
|
|
assert mgr.pending_count == 2
|
|
|
|
def test_clear(self, mgr):
|
|
mgr.merge("key-1", "hello")
|
|
mgr.clear()
|
|
assert mgr.pending_count == 0
|
|
|
|
|
|
class TestDoctor:
|
|
def test_is_mutable_email(self):
|
|
assert is_mutable_allowlist_entry("user@example.com") is True
|
|
|
|
def test_is_mutable_uuid(self):
|
|
assert is_mutable_allowlist_entry("12345678-1234-1234-1234-123456789abc") is False
|
|
|
|
def test_is_mutable_wildcard(self):
|
|
assert is_mutable_allowlist_entry("*") is True
|
|
|
|
def test_is_mutable_display_name(self):
|
|
assert is_mutable_allowlist_entry("张三") is True
|
|
|
|
def test_is_mutable_name_with_spaces(self):
|
|
assert is_mutable_allowlist_entry("John Doe") is True
|
|
|
|
def test_is_mutable_empty(self):
|
|
assert is_mutable_allowlist_entry("") is False
|
|
|
|
def test_collect_warnings_dm(self):
|
|
warnings = collect_mutable_allowlist_warnings(
|
|
allow_from=["user@example.com", "12345678-1234-1234-1234-123456789abc"],
|
|
)
|
|
assert len(warnings) == 1
|
|
assert warnings[0]["source"] == "allow_from"
|
|
|
|
def test_collect_warnings_group(self):
|
|
warnings = collect_mutable_allowlist_warnings(
|
|
group_allow_from=["Team Name"],
|
|
)
|
|
assert len(warnings) == 1
|
|
assert warnings[0]["source"] == "group_allow_from"
|
|
|
|
def test_collect_warnings_name_matching(self):
|
|
warnings = collect_mutable_allowlist_warnings(allow_name_matching=True)
|
|
assert len(warnings) == 1
|
|
assert warnings[0]["source"] == "allow_name_matching"
|
|
|
|
def test_collect_warnings_empty(self):
|
|
warnings = collect_mutable_allowlist_warnings()
|
|
assert len(warnings) == 0 |