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

426 lines
15 KiB
Python
Raw Normal View History

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