"""MSTeams runtime / presentation / welcome / file_upload 单元测试。""" from __future__ import annotations import time from unittest import mock from unittest.mock import patch, AsyncMock, MagicMock import pytest from yuxi.channels.adapters.msteams.runtime import ( MSTeamsRuntime, get_msteams_runtime, set_msteams_runtime, reset_msteams_runtime, ) from yuxi.channels.adapters.msteams.presentation import ( build_presentation_card, add_actions_to_card, ) from yuxi.channels.adapters.msteams.welcome import ( build_personal_welcome_card, build_group_welcome_message, build_welcome_response, ) from yuxi.channels.adapters.msteams.file_upload import ( needs_file_consent, choose_upload_strategy, build_file_consent_card, build_teams_file_info_card, create_shareable_link, PendingUploadStore, FILE_CONSENT_MAX_SIZE, ) class TestMSTeamsRuntime: def test_default_values(self): rt = MSTeamsRuntime() assert rt.app_id == "" assert rt.connected is False assert rt.streaming_mode == "block" def test_update(self): rt = MSTeamsRuntime() rt.update(app_id="test-123", streaming_mode="off") assert rt.app_id == "test-123" assert rt.streaming_mode == "off" def test_update_ignores_unknown(self): rt = MSTeamsRuntime() rt.update(nonexistent_field="value") assert not hasattr(rt, "nonexistent_field") def test_set_connected(self): rt = MSTeamsRuntime() rt.set_connected(app_id="app-1", tenant_id="tenant-1") assert rt.connected is True assert rt.app_id == "app-1" assert rt.tenant_id == "tenant-1" assert rt.connected_at > 0 def test_set_disconnected(self): rt = MSTeamsRuntime() rt.set_connected(app_id="app-1") rt.set_disconnected() assert rt.connected is False assert rt.connected_at == 0 def test_increment_decrement_uploads(self): rt = MSTeamsRuntime() rt.increment_pending_uploads(3) assert rt.pending_uploads_count == 3 rt.decrement_pending_uploads(2) assert rt.pending_uploads_count == 1 rt.decrement_pending_uploads(5) assert rt.pending_uploads_count == 0 def test_increment_decrement_streams(self): rt = MSTeamsRuntime() rt.increment_active_streams(2) assert rt.active_streams_count == 2 rt.decrement_active_streams(1) assert rt.active_streams_count == 1 rt.decrement_active_streams(5) assert rt.active_streams_count == 0 def test_get_snapshot(self): rt = MSTeamsRuntime() rt.update(app_id="app-id-1234567890") snapshot = rt.get_snapshot() assert "app_id" in snapshot assert "connected" in snapshot assert "streaming_mode" in snapshot assert snapshot["app_id"] == "app-id-12345..." def test_thread_safety(self): rt = MSTeamsRuntime() import threading def _update(): for i in range(100): rt.update(app_id=f"app-{i}") threads = [threading.Thread(target=_update) for _ in range(5)] for t in threads: t.start() for t in threads: t.join() assert rt.app_id.startswith("app-") class TestRuntimeSingleton: def teardown_method(self): reset_msteams_runtime() def test_get_default_returns_singleton(self): reset_msteams_runtime() r1 = get_msteams_runtime() r2 = get_msteams_runtime() assert r1 is r2 def test_set_replaces_singleton(self): reset_msteams_runtime() custom = MSTeamsRuntime(app_id="custom") set_msteams_runtime(custom) rt = get_msteams_runtime() assert rt.app_id == "custom" assert rt is custom def test_reset_creates_new_instance(self): reset_msteams_runtime() rt = get_msteams_runtime() rt.update(app_id="old") reset_msteams_runtime() new_rt = get_msteams_runtime() assert new_rt.app_id == "" assert new_rt is not rt class TestPresentationCard: def test_empty_card(self): card = build_presentation_card() assert card["type"] == "AdaptiveCard" assert card["version"] == "1.5" def test_with_title(self): card = build_presentation_card(title="My Card") assert card["body"][0]["text"] == "My Card" assert card["body"][0]["size"] == "Large" def test_text_block(self): card = build_presentation_card(blocks=[{"type": "text", "text": "hello"}]) body_item = card["body"][0] assert body_item["type"] == "TextBlock" assert body_item["text"] == "hello" def test_context_block(self): card = build_presentation_card(blocks=[{"type": "context", "text": "info"}]) body_item = card["body"][0] assert body_item["isSubtle"] is True assert body_item["size"] == "small" def test_section_block(self): card = build_presentation_card(blocks=[{"type": "section", "title": "Sec", "text": "body text"}]) assert len(card["body"]) >= 2 def test_facts_block(self): card = build_presentation_card(blocks=[{"type": "facts", "facts": {"key1": "val1"}}]) body_item = card["body"][0] assert body_item["type"] == "FactSet" assert body_item["facts"][0]["title"] == "key1" def test_image_block(self): card = build_presentation_card(blocks=[{"type": "image", "url": "https://example.com/img.png", "alt": "desc"}]) body_item = card["body"][0] assert body_item["type"] == "Image" assert body_item["url"] == "https://example.com/img.png" def test_buttons_block(self): card = build_presentation_card(blocks=[{ "type": "buttons", "buttons": [ {"title": "Open", "url": "https://example.com"}, {"title": "Submit", "data": {"action": "ok"}}, ], }]) body_item = card["body"][0] assert body_item["type"] == "ActionSet" assert len(body_item["actions"]) == 2 assert body_item["actions"][0]["type"] == "Action.OpenUrl" assert body_item["actions"][1]["type"] == "Action.Submit" def test_unknown_block_type(self): card = build_presentation_card(blocks=[{"type": "unknown", "text": "hi"}]) assert card["body"] == [] def test_add_actions_to_card(self): card = {"type": "AdaptiveCard", "body": []} actions = [{"title": "Click", "url": "https://example.com", "style": "positive"}] result = add_actions_to_card(card, actions) assert len(result["actions"]) == 1 assert result["actions"][0]["type"] == "Action.OpenUrl" class TestWelcome: def test_personal_welcome_card(self): card = build_personal_welcome_card() assert card["type"] == "AdaptiveCard" assert len(card["body"]) >= 4 assert card["body"][0]["text"].startswith("欢迎") def test_personal_welcome_custom_name(self): card = build_personal_welcome_card(bot_name="MyBot") assert "MyBot" in card["body"][0]["text"] def test_personal_welcome_custom_starters(self): starters = ["What can you do?", "Help me"] card = build_personal_welcome_card(prompt_starters=starters) assert "What can you do?" in str(card["body"]) def test_group_welcome_message(self): msg = build_group_welcome_message() assert "/help" in msg def test_group_welcome_custom_name(self): msg = build_group_welcome_message(bot_name="MyBot") assert "MyBot" in msg def test_build_welcome_response_personal(self): resp = build_welcome_response("personal", "Bot") assert resp["type"] == "card" assert "adaptive_card" in resp def test_build_welcome_response_group(self): resp = build_welcome_response("groupChat", "Bot") assert resp["type"] == "text" assert "content" in resp class TestFileUpload: def test_needs_file_consent_direct_under_limit(self): assert needs_file_consent(FILE_CONSENT_MAX_SIZE - 1, "direct") is False def test_needs_file_consent_direct_over_limit(self): assert needs_file_consent(FILE_CONSENT_MAX_SIZE + 1, "direct") is True def test_needs_file_consent_group_under_limit(self): assert needs_file_consent(FILE_CONSENT_MAX_SIZE * 4, "group") is False def test_needs_file_consent_group_over_limit(self): assert needs_file_consent(FILE_CONSENT_MAX_SIZE * 6, "group") is True def test_choose_upload_strategy_direct_small(self): result = choose_upload_strategy(1024, "direct") assert result == "direct" def test_choose_upload_strategy_sharepoint(self): result = choose_upload_strategy( FILE_CONSENT_MAX_SIZE + 1, "group", sharepoint_site_id="site-1", ) assert result == "sharepoint" def test_choose_upload_strategy_onedrive(self): result = choose_upload_strategy( FILE_CONSENT_MAX_SIZE + 1, "group", sharepoint_site_id="", ) assert result == "onedrive" def test_build_file_consent_card(self): card = build_file_consent_card("report.pdf", 2097152, "desc") assert card["type"] == "AdaptiveCard" assert len(card["actions"]) == 2 assert card["actions"][0]["title"] == "接受" assert card["actions"][1]["title"] == "拒绝" assert "report.pdf" in card["body"][1]["text"] assert "2.0 MB" in card["body"][1]["text"] def test_build_file_consent_card_with_context(self): card = build_file_consent_card( "file.txt", 1024, accept_context={"file_id": "123"}, decline_context={"file_id": "123"}, ) assert card["actions"][0]["data"]["file_id"] == "123" def test_create_shareable_link(self): result = create_shareable_link("token", "item-1", "edit") assert result["item_id"] == "item-1" assert result["link_type"] == "edit" def test_build_teams_file_info_card(self): card = build_teams_file_info_card("doc.pdf", 1048576, content_url="https://example.com") assert card["contentType"] == "application/vnd.microsoft.teams.file.info" assert card["content"]["name"] == "doc.pdf" assert "1.0 MB" in card["metadata"]["size_str"] def test_build_teams_file_info_card_kb(self): card = build_teams_file_info_card("doc.txt", 512) assert "KB" in card["metadata"]["size_str"] class TestPendingUploadStore: @pytest.fixture def store(self, tmp_path): return PendingUploadStore(storage_dir=str(tmp_path)) def test_save_and_load(self, store): store.save_pending("upload-1", "file.txt", "base64data", "conv-1") entry = store.load_pending("upload-1") assert entry is not None assert entry["filename"] == "file.txt" assert entry["conversation_id"] == "conv-1" def test_load_nonexistent(self, store): assert store.load_pending("nonexistent") is None def test_remove(self, store): store.save_pending("upload-1", "file.txt", "data", "conv-1") store.remove_pending("upload-1") assert store.load_pending("upload-1") is None def test_list_pending_ids(self, store): store.save_pending("u1", "f1", "d1", "c1") store.save_pending("u2", "f2", "d2", "c2") ids = store.list_pending_ids() assert "u1" in ids assert "u2" in ids def test_cleanup_expired(self, store): store.save_pending("u1", "f1", "d1", "c1") cleaned = store.cleanup_expired(max_age_seconds=0) assert cleaned >= 0