ForcePilot/backend/test/unit/channels/test_msteams_accounts_audit.py
Kris 69fe97a90d test: 批量修复并新增单元测试用例
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重连相关测试
2026-05-13 16:43:01 +08:00

426 lines
15 KiB
Python

"""MSTeams accounts / audit / connection_modes / group_mgmt / setup_wizard 单元测试。"""
from __future__ import annotations
import time
from unittest.mock import AsyncMock, MagicMock, Mock, patch
import pytest
from yuxi.channels.adapters.msteams.accounts import AccountManager
from yuxi.channels.adapters.msteams.audit import (
GraphPermissionAuditor,
REQUIRED_GRAPH_PERMISSIONS,
)
from yuxi.channels.adapters.msteams.connection_modes import (
WebSocketClient,
PollingClient,
ConnectionModeManager,
CONNECTION_MODE_WEBHOOK,
CONNECTION_MODE_WEBSOCKET,
CONNECTION_MODE_POLLING,
)
from yuxi.channels.adapters.msteams.setup_wizard import (
MSTeamsSetupWizard,
_validate_and_normalize_answers,
)
class TestAccountManager:
@pytest.fixture
def mgr(self):
return AccountManager()
def test_register_account(self, mgr):
mgr.register_account("acct-1", "app-id", "app-secret", tenant_id="t1", label="Account 1")
account = mgr.get_account("acct-1")
assert account is not None
assert account["app_id"] == "app-id"
assert account["app_password"] == "app-secret"
assert account["tenant_id"] == "t1"
assert account["label"] == "Account 1"
assert account["status"] == "registered"
def test_register_account_default_label(self, mgr):
mgr.register_account("acct-1", "app-id", "app-secret")
account = mgr.get_account("acct-1")
assert account["label"] == "acct-1"
def test_unregister_account(self, mgr):
mgr.register_account("acct-1", "app-id", "app-secret")
result = mgr.unregister_account("acct-1")
assert result is True
assert mgr.get_account("acct-1") is None
def test_unregister_nonexistent(self, mgr):
result = mgr.unregister_account("nonexistent")
assert result is False
def test_get_all_accounts(self, mgr):
mgr.register_account("a1", "id1", "pw1")
mgr.register_account("a2", "id2", "pw2")
accounts = mgr.get_all_accounts()
assert len(accounts) == 2
def test_list_account_ids(self, mgr):
mgr.register_account("a1", "id1", "pw1")
mgr.register_account("a2", "id2", "pw2")
ids = mgr.list_account_ids()
assert "a1" in ids
assert "a2" in ids
def test_account_count(self, mgr):
assert mgr.account_count == 0
mgr.register_account("a1", "id1", "pw1")
assert mgr.account_count == 1
@pytest.mark.asyncio
async def test_probe_account_nonexistent(self, mgr):
result = await mgr.probe_account("nonexistent")
assert result["status"] == "error"
assert "not found" in result["message"].lower()
@pytest.mark.asyncio
async def test_start_account(self, mgr):
mgr.register_account("acct-1", "app-id", "app-secret", tenant_id="t1")
mock_adapter = MagicMock()
mock_adapter.config = {}
result = await mgr.start_account("acct-1", adapter=mock_adapter)
assert result is True
assert mock_adapter.config["app_id"] == "app-id"
assert mock_adapter.config["app_password"] == "app-secret"
assert mock_adapter.config["tenant_id"] == "t1"
@pytest.mark.asyncio
async def test_start_account_nonexistent(self, mgr):
result = await mgr.start_account("nonexistent")
assert result is False
@pytest.mark.asyncio
async def test_start_account_without_adapter(self, mgr):
mgr.register_account("acct-1", "id", "pw")
result = await mgr.start_account("acct-1")
assert result is True
@pytest.mark.asyncio
async def test_start_account_no_tenant(self, mgr):
mgr.register_account("acct-1", "app-id", "app-secret")
mock_adapter = MagicMock()
mock_adapter.config = {}
result = await mgr.start_account("acct-1", adapter=mock_adapter)
assert result is True
assert "tenant_id" not in mock_adapter.config
class TestGraphPermissionAuditor:
def test_init(self):
auditor = GraphPermissionAuditor(token="fake-token", app_id="fake-app")
assert auditor._token == "fake-token"
assert auditor._app_id == "fake-app"
def test_parse_oauth2_permissions_empty(self):
auditor = GraphPermissionAuditor(token="t", app_id="a")
result = auditor._parse_oauth2_permissions({"value": []})
assert result == set()
def test_parse_oauth2_permissions_with_data(self):
auditor = GraphPermissionAuditor(token="t", app_id="a")
sp_data = {
"value": [{
"oauth2PermissionScopes": [
{"value": "scope1"},
{"value": "scope2"},
],
}],
}
result = auditor._parse_oauth2_permissions(sp_data)
assert result == {"scope1", "scope2"}
def test_get_missing_permissions(self):
auditor = GraphPermissionAuditor(token="t", app_id="a")
audit_result = {"perm1": True, "perm2": False, "perm3": False}
missing = auditor.get_missing_permissions(audit_result)
assert missing == ["perm2", "perm3"]
def test_get_missing_permissions_all_granted(self):
auditor = GraphPermissionAuditor(token="t", app_id="a")
audit_result = {"perm1": True, "perm2": True}
missing = auditor.get_missing_permissions(audit_result)
assert missing == []
def test_required_permissions_list(self):
assert "User.Read.All" in REQUIRED_GRAPH_PERMISSIONS
assert "ChannelMessage.Send" in REQUIRED_GRAPH_PERMISSIONS
def test_format_audit_display(self):
data = {
"scopes": [{"value": "scope1", "type": "Admin"}],
"roles": [{"value": "role1", "display_name": "Role 1"}],
}
result = GraphPermissionAuditor.format_audit_display(data)
assert "scope1" in result
assert "role1" in result
def test_format_audit_display_empty(self):
result = GraphPermissionAuditor.format_audit_display({})
assert result == "No scopes or roles found"
class TestWebSocketClient:
@pytest.fixture
def ws_client(self):
return WebSocketClient(app_id="test-id", app_password="test-secret")
def test_init_defaults(self, ws_client):
assert ws_client._app_id == "test-id"
assert ws_client._app_password == "test-secret"
assert ws_client._running is False
assert ws_client._ws is None
def test_set_message_handler(self, ws_client):
async def handler(msg):
pass
ws_client.set_message_handler(handler)
assert ws_client._on_message is handler
@pytest.mark.asyncio
async def test_connect_already_running(self, ws_client):
ws_client._running = True
await ws_client.connect()
assert ws_client._ws is None
@pytest.mark.asyncio
async def test_send_activity_closed(self, ws_client):
result = await ws_client.send_activity({"type": "message"})
assert result is False
@pytest.mark.asyncio
async def test_close_sets_running_false(self, ws_client):
ws_client._running = True
await ws_client.close()
assert ws_client._running is False
@pytest.mark.asyncio
async def test_close_with_session(self, ws_client):
mock_session = MagicMock()
mock_session.closed = False
mock_session.close = AsyncMock()
ws_client._session = mock_session
ws_client._ws = Mock()
ws_client._ws.closed = False
ws_client._ws.close = AsyncMock()
await ws_client.close()
assert ws_client._session is None
assert ws_client._ws is None
class TestPollingClient:
@pytest.fixture
def poll_client(self):
return PollingClient(app_id="test-id", app_password="test-secret")
def test_init_defaults(self, poll_client):
assert poll_client._app_id == "test-id"
assert poll_client._running is False
def test_set_message_handler(self, poll_client):
async def handler(msg):
pass
poll_client.set_message_handler(handler)
assert poll_client._on_message is handler
@pytest.mark.asyncio
async def test_connect_already_running(self, poll_client):
poll_client._running = True
await poll_client.connect()
assert poll_client._session is None
@pytest.mark.asyncio
async def test_close_sets_running_false(self, poll_client):
poll_client._running = True
await poll_client.close()
assert poll_client._running is False
@pytest.mark.asyncio
async def test_close_with_session(self, poll_client):
mock_session = MagicMock()
mock_session.closed = False
mock_session.close = AsyncMock()
poll_client._session = mock_session
await poll_client.close()
assert poll_client._session is None
class TestConnectionModeManager:
def test_default_mode(self):
mgr = ConnectionModeManager({})
assert mgr.mode == CONNECTION_MODE_WEBHOOK
assert mgr.is_webhook is True
assert mgr.is_websocket is False
assert mgr.is_polling is False
def test_custom_mode_websocket(self):
mgr = ConnectionModeManager({"connection_mode": CONNECTION_MODE_WEBSOCKET})
assert mgr.mode == CONNECTION_MODE_WEBSOCKET
assert mgr.is_websocket is True
def test_custom_mode_polling(self):
mgr = ConnectionModeManager({"connection_mode": CONNECTION_MODE_POLLING})
assert mgr.mode == CONNECTION_MODE_POLLING
assert mgr.is_polling is True
def test_invalid_mode_fallback(self):
mgr = ConnectionModeManager({"connection_mode": "invalid"})
assert mgr.mode == CONNECTION_MODE_WEBHOOK
def test_set_message_router(self):
mgr = ConnectionModeManager({})
async def router(msg):
pass
mgr.set_message_router(router)
assert mgr._message_router is router
@pytest.mark.asyncio
async def test_start_webhook_mode(self):
mgr = ConnectionModeManager({})
await mgr.start("app-id", "app-secret")
assert mgr._ws_client is None
assert mgr._poll_client is None
@pytest.mark.asyncio
async def test_start_websocket_mode(self):
mgr = ConnectionModeManager({"connection_mode": CONNECTION_MODE_WEBSOCKET})
with patch.object(WebSocketClient, "connect", new_callable=AsyncMock) as mock_connect:
await mgr.start("app-id", "app-secret")
assert mgr._ws_client is not None
mock_connect.assert_called_once()
@pytest.mark.asyncio
async def test_start_polling_mode(self):
mgr = ConnectionModeManager({"connection_mode": CONNECTION_MODE_POLLING})
with patch.object(PollingClient, "connect", new_callable=AsyncMock) as mock_connect:
await mgr.start("app-id", "app-secret")
assert mgr._poll_client is not None
mock_connect.assert_called_once()
@pytest.mark.asyncio
async def test_stop(self):
mgr = ConnectionModeManager({"connection_mode": CONNECTION_MODE_WEBSOCKET})
mgr._ws_client = MagicMock()
mgr._ws_client.closed = False
mgr._ws_client.close = AsyncMock()
mgr._poll_client = MagicMock()
mgr._poll_client.closed = False
mgr._poll_client.close = AsyncMock()
await mgr.stop()
assert mgr._ws_client is None
assert mgr._poll_client is None
@pytest.mark.asyncio
async def test_send_activity_webhook(self):
mgr = ConnectionModeManager({})
result = await mgr.send_activity({"type": "message"})
assert result is False
@pytest.mark.asyncio
async def test_send_activity_websocket_success(self):
mgr = ConnectionModeManager({"connection_mode": CONNECTION_MODE_WEBSOCKET})
mock_ws = MagicMock()
mock_ws.send_activity = AsyncMock(return_value=True)
mgr._ws_client = mock_ws
result = await mgr.send_activity({"type": "message"})
assert result is True
class TestSetupWizardNonInteractive:
def test_basic_answers(self):
wizard = MSTeamsSetupWizard()
config = wizard.run_non_interactive({
"app_id": "test-app",
"app_password": "test-pw",
"dm_policy": "open",
"group_policy": "open",
"streaming_mode": "block",
})
assert config["app_id"] == "test-app"
assert config["dm_policy"] == "open"
def test_allow_list_parsing(self):
wizard = MSTeamsSetupWizard()
config = wizard.run_non_interactive({
"allow_from": "user-1,user-2 , user-3",
"dm_policy": "allowlist",
})
assert config["allow_from"] == ["user-1", "user-2", "user-3"]
def test_allow_list_as_list(self):
wizard = MSTeamsSetupWizard()
config = wizard.run_non_interactive({
"allow_from": ["user-1", "user-2"],
"dm_policy": "allowlist",
})
assert config["allow_from"] == ["user-1", "user-2"]
def test_bool_keys(self):
wizard = MSTeamsSetupWizard()
config = wizard.run_non_interactive({
"feedback_enabled": True,
"welcome_card": False,
})
assert config["feedback_enabled"] is True
assert config["welcome_card"] is False
def test_oauth_enabled(self):
wizard = MSTeamsSetupWizard()
config = wizard.run_non_interactive({
"oauth_enabled": True,
"oauth_scopes": ["scope1"],
})
assert config["delegated_auth"]["enabled"] is True
assert "scope1" in config["delegated_auth"]["scopes"]
def test_sso_connection(self):
wizard = MSTeamsSetupWizard()
config = wizard.run_non_interactive({
"sso_enabled": True,
"sso_connection_name": "my-conn",
})
assert config["sso"]["enabled"] is True
assert config["sso"]["connection_name"] == "my-conn"
def test_save_config(self, tmp_path):
wizard = MSTeamsSetupWizard({"app_id": "test"})
filepath = wizard.save_config(str(tmp_path / "config.json"))
assert "config.json" in filepath
def test_existing_config_preserved(self):
wizard = MSTeamsSetupWizard({"app_id": "old-id", "extra_key": "extra"})
config = wizard.run_non_interactive({"app_id": "new-id"})
assert config["app_id"] == "new-id"
assert config["extra_key"] == "extra"
class TestValidateAnswers:
def test_str_keys(self):
result = _validate_and_normalize_answers({
"app_id": "test-id",
"dm_policy": "open",
})
assert result["app_id"] == "test-id"
assert result["dm_policy"] == "open"
def test_empty_str_skipped(self):
result = _validate_and_normalize_answers({"app_id": ""})
assert "app_id" not in result
def test_group_allow_from_comma(self):
result = _validate_and_normalize_answers({"group_allow_from": "g1,g2,g3"})
assert result["group_allow_from"] == ["g1", "g2", "g3"]
def test_invalid_bool_cast(self):
result = _validate_and_normalize_answers({"feedback_enabled": "yes"})
assert result["feedback_enabled"] is True