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

333 lines
12 KiB
Python
Raw Normal View History

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