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

349 lines
12 KiB
Python
Raw Normal View History

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