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