ForcePilot/backend/test/unit/channels/test_msteams_phase1.py
Kris 3264900bc9 test: 新增多渠道单元测试用例并配置测试环境变量
新增了Twitch、Telegram、Discord、Slack、Mattermost、WeChat、Zalo等多渠道的单元测试用例,覆盖了令牌处理、速率限制、消息去重、会话解析、格式转换、安全策略等模块
同时在测试配置中添加了测试用的OpenAI API密钥环境变量
2026-05-12 00:56:47 +08:00

359 lines
13 KiB
Python

"""Phase 1 P0 安全与补齐 — 单元测试。
覆盖:
- P0-01: SecurityPolicy DM/Group Policy
- P0-02: messageReaction 事件标准化
- P0-03: adaptiveCard/action invoke 路由
- P0-08: 入站上下文字段
- P0-10: HTTP 错误分类
"""
from __future__ import annotations
import pytest
from yuxi.channels.adapters.msteams.security import (
SecurityPolicy,
clean_allow_entry,
DM_POLICY_OPEN,
DM_POLICY_PAIRING,
DM_POLICY_ALLOWLIST,
DM_POLICY_DISABLED,
GROUP_POLICY_OPEN,
GROUP_POLICY_ALLOWLIST,
GROUP_POLICY_DISABLED,
)
from yuxi.channels.adapters.msteams.invoke_handler import (
normalize_reaction,
normalize_card_action,
)
from yuxi.channels.adapters.msteams.normalizer import normalize_inbound
from yuxi.channels.adapters.msteams.send import classify_http_error
from yuxi.channels.models import EventType
# ── P0-01 + P0-05: SecurityPolicy ──────────────────────────────────────────
class TestSecurityPolicy:
def test_dm_policy_open_allows_all(self):
sp = SecurityPolicy({"dm_policy": DM_POLICY_OPEN})
assert sp.check_dm("any-user") is True
assert sp.check_dm("another-user") is True
def test_dm_policy_disabled_rejects_all(self):
sp = SecurityPolicy({"dm_policy": DM_POLICY_DISABLED})
assert sp.check_dm("any-user") is False
def test_dm_policy_allowlist_with_exact_match(self):
sp = SecurityPolicy({
"dm_policy": DM_POLICY_ALLOWLIST,
"allow_from": ["user-001", "user-002"],
})
assert sp.check_dm("user-001") is True
assert sp.check_dm("user-002") is True
assert sp.check_dm("user-003") is False
def test_dm_policy_allowlist_with_wildcard(self):
sp = SecurityPolicy({
"dm_policy": DM_POLICY_ALLOWLIST,
"allow_from": ["*"],
})
assert sp.check_dm("any-user") is True
def test_dm_policy_pairing_allows_paired_users(self):
sp = SecurityPolicy({"dm_policy": DM_POLICY_PAIRING})
assert sp.check_dm("user-001") is False
sp.add_paired_user("user-001")
assert sp.check_dm("user-001") is True
sp.remove_paired_user("user-001")
assert sp.check_dm("user-001") is False
def test_dm_policy_allowlist_name_matching(self):
sp = SecurityPolicy({
"dm_policy": DM_POLICY_ALLOWLIST,
"allow_from": ["*@example.com"],
"allow_name_matching": True,
})
assert sp.check_dm("user@example.com") is True
assert sp.check_dm("user@other.com") is False
def test_group_policy_open_allows_all(self):
sp = SecurityPolicy({"group_policy": GROUP_POLICY_OPEN})
assert sp.check_group("any-user", conversation_id="conv-001") is True
def test_group_policy_disabled_rejects_all(self):
sp = SecurityPolicy({"group_policy": GROUP_POLICY_DISABLED})
assert sp.check_group("any-user", conversation_id="conv-001") is False
def test_group_policy_allowlist_user_match(self):
sp = SecurityPolicy({
"group_policy": GROUP_POLICY_ALLOWLIST,
"group_allow_from": ["user-001"],
})
assert sp.check_group("user-001") is True
assert sp.check_group("user-002") is False
def test_group_policy_allowlist_conv_match(self):
sp = SecurityPolicy({
"group_policy": GROUP_POLICY_ALLOWLIST,
"group_allow_from": ["conv-abc"],
})
assert sp.check_group("user-001", conversation_id="conv-abc") is True
assert sp.check_group("user-001", conversation_id="conv-xyz") is False
def test_require_mention_direct_chat_bypass(self):
sp = SecurityPolicy()
assert sp.check_require_mention(False, "direct") is True
assert sp.check_require_mention(True, "direct") is True
def test_require_mention_group_requires_mention(self):
sp = SecurityPolicy()
assert sp.check_require_mention(False, "group") is False
assert sp.check_require_mention(True, "group") is True
def test_require_mention_config_override(self):
sp = SecurityPolicy()
assert sp.check_require_mention(False, "group", {"require_mention": False}) is True
assert sp.check_require_mention(True, "group", {"require_mention": False}) is True
def test_invalid_dm_policy_fallback(self):
sp = SecurityPolicy({"dm_policy": "invalid"})
assert sp.dm_policy == DM_POLICY_OPEN
def test_invalid_group_policy_fallback(self):
sp = SecurityPolicy({"group_policy": "invalid"})
assert sp.group_policy == GROUP_POLICY_OPEN
def test_clean_allow_entry_strips_prefix(self):
assert clean_allow_entry("28:abc123") == "abc123"
def test_clean_allow_entry_strips_did_prefix(self):
assert clean_allow_entry("did:example:123") == "example:123"
def test_allow_from_count(self):
sp = SecurityPolicy({"allow_from": ["user-001", "user-002"]})
assert sp.allow_from_count == 2
assert sp.group_allow_from_count == 0
# ── P0-02: Reaction Events ─────────────────────────────────────────────────
class TestReactionNormalization:
def test_reactions_added(self):
activity = {
"type": "messageReaction",
"id": "reaction-001",
"from": {"id": "user-001", "name": "Test User"},
"conversation": {"id": "conv-001"},
"reactionsAdded": [{"type": "like"}, {"type": "heart"}],
"reactionsRemoved": [],
"replyToId": "msg-001",
}
result = normalize_reaction(activity)
assert result.event_type == EventType.SYSTEM_EVENT
assert result.metadata["reaction_direction"] == "added"
assert len(result.metadata["reactions"]["added"]) == 2
assert result.reply_to_message_id == "msg-001"
def test_reactions_removed(self):
activity = {
"type": "messageReaction",
"id": "reaction-002",
"from": {"id": "user-001", "name": "Test User"},
"conversation": {"id": "conv-001"},
"reactionsAdded": [],
"reactionsRemoved": [{"type": "like"}],
"replyToId": "msg-001",
}
result = normalize_reaction(activity)
assert result.event_type == EventType.SYSTEM_EVENT
assert result.metadata["reaction_direction"] == "removed"
def test_reaction_no_direction(self):
activity = {
"type": "messageReaction",
"id": "reaction-003",
"from": {"id": "user-001"},
"conversation": {"id": "conv-001"},
"reactionsAdded": [],
"reactionsRemoved": [],
}
result = normalize_reaction(activity)
assert result.metadata["reaction_direction"] == "unknown"
# ── P0-03: Invoke Card Action Routing ──────────────────────────────────────
class TestCardActionNormalization:
def test_vote_action(self):
activity = {
"type": "invoke",
"name": "adaptiveCard/action",
"id": "invoke-001",
"from": {"id": "user-001", "name": "Voter"},
"conversation": {"id": "conv-001"},
"value": {"voteOption": "Option A"},
}
result = normalize_card_action(activity)
assert result.event_type == EventType.CARD_ACTION
assert result.metadata["action_type"] == "vote"
assert result.content == "Option A"
def test_confirm_action(self):
activity = {
"type": "invoke",
"name": "adaptiveCard/action",
"id": "invoke-002",
"from": {"id": "user-001"},
"conversation": {"id": "conv-001"},
"value": {"data": {"action": "confirm", "id": "123"}},
}
result = normalize_card_action(activity)
assert result.metadata["action_type"] == "confirm"
def test_cancel_action(self):
activity = {
"type": "invoke",
"name": "adaptiveCard/action",
"id": "invoke-003",
"from": {"id": "user-001"},
"conversation": {"id": "conv-001"},
"value": {"data": {"action": "cancel"}},
}
result = normalize_card_action(activity)
assert result.metadata["action_type"] == "cancel"
def test_input_action(self):
activity = {
"type": "invoke",
"name": "adaptiveCard/action",
"id": "invoke-004",
"from": {"id": "user-001"},
"conversation": {"id": "conv-001"},
"value": {"data": {"action": "submit"}, "field1": "value1"},
}
result = normalize_card_action(activity)
assert result.metadata["action_type"] == "input"
def test_feedback_invoke(self):
activity = {
"type": "invoke",
"name": "message/submitAction",
"id": "invoke-005",
"from": {"id": "user-001"},
"conversation": {"id": "conv-001"},
"value": {"feedbackValue": "like"},
}
result = normalize_card_action(activity)
assert result.metadata["action_type"] == "feedback"
assert result.content == "like"
def test_file_consent_invoke(self):
activity = {
"type": "invoke",
"name": "fileConsent/invoke",
"id": "invoke-006",
"from": {"id": "user-001"},
"conversation": {"id": "conv-001"},
"value": {"accept": True},
}
result = normalize_card_action(activity)
assert result.metadata["action_type"] == "file_consent"
def test_generic_card_action(self):
activity = {
"type": "invoke",
"name": "adaptiveCard/action",
"id": "invoke-007",
"from": {"id": "user-001"},
"conversation": {"id": "conv-001"},
"value": {},
}
result = normalize_card_action(activity)
assert result.metadata["action_type"] == "card_action"
# ── P0-08: Context Fields ──────────────────────────────────────────────────
class TestContextFields:
def test_message_includes_context_fields(self):
activity = {
"type": "message",
"id": "ctx-001",
"from": {"id": "user-001", "name": "Context User", "aadObjectId": "aad-001"},
"conversation": {"id": "conv-001", "conversationType": "personal"},
"recipient": {"id": "bot-001"},
"text": "Hello bot",
"textFormat": "plain",
"channelId": "msteams",
"serviceUrl": "https://example.com",
}
result = normalize_inbound(activity)
assert result.metadata.get("Body") == "Hello bot"
assert result.metadata.get("BodyForAgent") == "Hello bot"
assert result.metadata.get("RawBody") == "Hello bot"
assert result.metadata.get("SessionKey") is not None
assert result.metadata.get("Provider") == "msteams"
assert result.metadata.get("Surface") == "personal"
assert result.metadata.get("WasMentioned") is False
def test_command_body_extraction(self):
activity = {
"type": "message",
"id": "ctx-002",
"from": {"id": "user-001", "name": "Cmd User", "aadObjectId": "aad-001"},
"conversation": {"id": "conv-001", "conversationType": "personal"},
"recipient": {"id": "bot-001"},
"text": "/help",
"textFormat": "plain",
}
result = normalize_inbound(activity)
assert result.metadata.get("CommandBody") == "/help"
def test_mentioned_context(self):
activity = {
"type": "message",
"id": "ctx-003",
"from": {"id": "user-001", "name": "Mention User", "aadObjectId": "aad-001"},
"conversation": {"id": "conv-001", "conversationType": "groupChat"},
"recipient": {"id": "bot-001"},
"text": "<at>Test Bot</at> hello",
"textFormat": "markdown",
"entities": [
{"type": "mention", "mentioned": {"id": "bot-001", "name": "Test Bot"}},
],
}
result = normalize_inbound(activity)
assert result.metadata.get("WasMentioned") is True
assert "Bot mentioned" in result.metadata.get("BodyForAgent", "")
# ── P0-10: HTTP Error Classification ───────────────────────────────────────
class TestErrorClassification:
def test_401_auth_failed(self):
error = classify_http_error(401, "Unauthorized")
assert "auth_failed:401" in error
assert "token_expired_or_invalid" in error
def test_403_insufficient_permissions(self):
error = classify_http_error(403, "Forbidden")
assert "auth_failed:403" in error
assert "insufficient_permissions" in error
def test_429_rate_limited(self):
error = classify_http_error(429, "Too Many Requests")
assert error.startswith("rate_limited:")
def test_5xx_server_error(self):
error = classify_http_error(500)
assert error.startswith("server_error:500")
def test_other_status(self):
error = classify_http_error(404, "Not Found")
assert error.startswith("http_404:")