2026-05-12 00:56:47 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
from yuxi.channels.adapters.wechat.security import (
|
|
|
|
|
check_dm_policy,
|
|
|
|
|
check_group_policy,
|
|
|
|
|
is_bot_mentioned,
|
|
|
|
|
should_require_mention,
|
2026-05-12 00:56:47 +08:00
|
|
|
)
|
2026-05-13 16:43:01 +08:00
|
|
|
from yuxi.channels.adapters.wechat.security import WeChatSecurityAdapter
|
2026-05-12 00:56:47 +08:00
|
|
|
|
|
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
class TestCheckDmPolicy:
|
|
|
|
|
def test_open_policy_allows(self):
|
|
|
|
|
assert check_dm_policy({"dm_policy": "open"}, "user123") is True
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_disabled_policy_denies(self):
|
|
|
|
|
assert check_dm_policy({"dm_policy": "disabled"}, "user123") is False
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_allowlist_user_in_list(self):
|
|
|
|
|
config = {"dm_policy": "allowlist", "allow_from": ["wx:user123"]}
|
|
|
|
|
assert check_dm_policy(config, "user123") is True
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_allowlist_user_not_in_list(self):
|
|
|
|
|
config = {"dm_policy": "allowlist", "allow_from": ["wx:other"]}
|
|
|
|
|
assert check_dm_policy(config, "other_user") is False
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_allowlist_empty(self):
|
|
|
|
|
config = {"dm_policy": "allowlist", "allow_from": []}
|
|
|
|
|
assert check_dm_policy(config, "anyone") is False
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_unknown_policy_denies(self):
|
|
|
|
|
assert check_dm_policy({"dm_policy": "unknown_policy"}, "user") is False
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_no_policy_defaults_to_open(self):
|
|
|
|
|
assert check_dm_policy({}, "user") is True
|
2026-05-12 00:56:47 +08:00
|
|
|
|
|
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
class TestCheckGroupPolicy:
|
|
|
|
|
def test_open_policy(self):
|
|
|
|
|
config = {"group_policy": "open"}
|
|
|
|
|
assert check_group_policy(config, "chat1", "user1") is True
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_disabled_policy(self):
|
|
|
|
|
config = {"group_policy": "disabled"}
|
|
|
|
|
assert check_group_policy(config, "chat1", "user1") is False
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_global_allowlist_match(self):
|
|
|
|
|
config = {"group_policy": "allowlist", "group_allow_from": ["wx:user1"]}
|
|
|
|
|
assert check_group_policy(config, "chat1", "user1") is True
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_global_allowlist_no_match(self):
|
|
|
|
|
config = {"group_policy": "allowlist", "group_allow_from": ["wx:other"]}
|
|
|
|
|
assert check_group_policy(config, "chat1", "user1") is False
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_per_group_allowlist_match(self):
|
|
|
|
|
config = {
|
2026-05-12 00:56:47 +08:00
|
|
|
"group_policy": "allowlist",
|
2026-05-13 16:43:01 +08:00
|
|
|
"groups": {"chat1": {"allow_from": ["wx:user1"]}},
|
|
|
|
|
}
|
|
|
|
|
assert check_group_policy(config, "chat1", "user1") is True
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_per_group_allowlist_no_match(self):
|
2026-05-12 00:56:47 +08:00
|
|
|
config = {
|
2026-05-13 16:43:01 +08:00
|
|
|
"group_policy": "allowlist",
|
|
|
|
|
"groups": {"chat1": {"allow_from": ["wx:other"]}},
|
2026-05-12 00:56:47 +08:00
|
|
|
}
|
2026-05-13 16:43:01 +08:00
|
|
|
assert check_group_policy(config, "chat1", "user1") is False
|
|
|
|
|
|
|
|
|
|
def test_no_policy(self):
|
|
|
|
|
assert check_group_policy({}, "chat1", "user1") is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestIsBotMentioned:
|
|
|
|
|
def test_wecom_with_at_and_name(self):
|
|
|
|
|
config = {"agent_name": "小助手"}
|
|
|
|
|
payload = {"Content": "@小助手 你好"}
|
|
|
|
|
assert is_bot_mentioned(config, payload, "wecom") is True
|
|
|
|
|
|
|
|
|
|
def test_wecom_at_only_no_name(self):
|
|
|
|
|
config = {"agent_name": "小助手"}
|
|
|
|
|
payload = {"Content": "@someone 你好"}
|
|
|
|
|
assert is_bot_mentioned(config, payload, "wecom") is True
|
|
|
|
|
|
|
|
|
|
def test_wecom_no_at(self):
|
|
|
|
|
config = {"agent_name": "小助手"}
|
|
|
|
|
payload = {"Content": "你好"}
|
|
|
|
|
assert is_bot_mentioned(config, payload, "wecom") is False
|
|
|
|
|
|
|
|
|
|
def test_wecom_empty_content(self):
|
|
|
|
|
config = {"agent_name": "小助手"}
|
|
|
|
|
payload = {"Content": ""}
|
|
|
|
|
assert is_bot_mentioned(config, payload, "wecom") is False
|
|
|
|
|
|
|
|
|
|
def test_mp_with_at(self):
|
|
|
|
|
config = {}
|
|
|
|
|
payload = {"Content": "@bot help"}
|
|
|
|
|
assert is_bot_mentioned(config, payload, "mp") is True
|
|
|
|
|
|
|
|
|
|
def test_mp_without_at(self):
|
|
|
|
|
config = {}
|
|
|
|
|
payload = {"Content": "help"}
|
|
|
|
|
assert is_bot_mentioned(config, payload, "mp") is False
|
|
|
|
|
|
|
|
|
|
def test_personal_with_at_list(self):
|
|
|
|
|
config = {}
|
|
|
|
|
payload = {"at_list": ["user1"]}
|
|
|
|
|
assert is_bot_mentioned(config, payload, "personal") is True
|
|
|
|
|
|
|
|
|
|
def test_personal_empty_at_list(self):
|
|
|
|
|
config = {}
|
|
|
|
|
payload = {"at_list": []}
|
|
|
|
|
assert is_bot_mentioned(config, payload, "personal") is False
|
|
|
|
|
|
|
|
|
|
def test_unknown_mode(self):
|
|
|
|
|
assert is_bot_mentioned({}, {}, "unknown") is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestShouldRequireMention:
|
|
|
|
|
def test_group_requires_mention_default(self):
|
|
|
|
|
config = {"groups": {"chat1": {}}}
|
|
|
|
|
assert should_require_mention(config, "chat1") is True
|
|
|
|
|
|
|
|
|
|
def test_group_requires_mention_explicit_true(self):
|
|
|
|
|
config = {"groups": {"chat1": {"require_mention": True}}}
|
|
|
|
|
assert should_require_mention(config, "chat1") is True
|
|
|
|
|
|
|
|
|
|
def test_group_requires_mention_explicit_false(self):
|
|
|
|
|
config = {"groups": {"chat1": {"require_mention": False}}}
|
|
|
|
|
assert should_require_mention(config, "chat1") is False
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_group_not_configured(self):
|
|
|
|
|
assert should_require_mention({}, "nonexistent") is True
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
|
|
|
|
|
class TestWeChatSecurityAdapter:
|
|
|
|
|
def test_resolve_dm_policy_default(self):
|
|
|
|
|
result = WeChatSecurityAdapter.resolve_dm_policy({})
|
|
|
|
|
assert result == "pairing"
|
|
|
|
|
|
|
|
|
|
def test_resolve_dm_policy_from_root(self):
|
|
|
|
|
result = WeChatSecurityAdapter.resolve_dm_policy({"dm_policy": "allowlist"})
|
|
|
|
|
assert result == "allowlist"
|
|
|
|
|
|
|
|
|
|
def test_resolve_dm_policy_from_account(self):
|
|
|
|
|
config = {"accounts": {"acc1": {"dm_policy": "open"}}}
|
|
|
|
|
result = WeChatSecurityAdapter.resolve_dm_policy(config, "acc1")
|
|
|
|
|
assert result == "open"
|
|
|
|
|
|
|
|
|
|
def test_resolve_dm_policy_account_not_found_falls_back(self):
|
|
|
|
|
config = {"dm_policy": "disabled", "accounts": {}}
|
|
|
|
|
result = WeChatSecurityAdapter.resolve_dm_policy(config, "missing")
|
|
|
|
|
assert result == "disabled"
|
|
|
|
|
|
|
|
|
|
def test_resolve_dm_allow_from_root(self):
|
|
|
|
|
result = WeChatSecurityAdapter.resolve_dm_allow_from({"allow_from": ["wx:a"]})
|
|
|
|
|
assert result == ["wx:a"]
|
|
|
|
|
|
|
|
|
|
def test_resolve_dm_allow_from_account(self):
|
|
|
|
|
config = {"accounts": {"acc1": {"allow_from": ["wx:b"]}}}
|
|
|
|
|
result = WeChatSecurityAdapter.resolve_dm_allow_from(config, "acc1")
|
|
|
|
|
assert result == ["wx:b"]
|
|
|
|
|
|
|
|
|
|
def test_resolve_dm_allow_from_empty(self):
|
|
|
|
|
assert WeChatSecurityAdapter.resolve_dm_allow_from({}) == []
|
|
|
|
|
|
|
|
|
|
def test_resolve_group_policy_default(self):
|
|
|
|
|
assert WeChatSecurityAdapter.resolve_group_policy({}) == "allowlist"
|
|
|
|
|
|
|
|
|
|
def test_resolve_group_policy_from_account(self):
|
|
|
|
|
config = {"accounts": {"acc1": {"group_policy": "open"}}}
|
|
|
|
|
result = WeChatSecurityAdapter.resolve_group_policy(config, "acc1")
|
|
|
|
|
assert result == "open"
|
|
|
|
|
|
|
|
|
|
def test_resolve_group_allow_from_combined(self):
|
2026-05-12 00:56:47 +08:00
|
|
|
config = {
|
2026-05-13 16:43:01 +08:00
|
|
|
"group_allow_from": ["wx:global"],
|
|
|
|
|
"groups": {"chat1": {"allow_from": ["wx:local"]}},
|
2026-05-12 00:56:47 +08:00
|
|
|
}
|
2026-05-13 16:43:01 +08:00
|
|
|
result = WeChatSecurityAdapter.resolve_group_allow_from(config, "chat1")
|
|
|
|
|
assert "wx:global" in result
|
|
|
|
|
assert "wx:local" in result
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_resolve_group_allow_from_no_duplicates(self):
|
2026-05-12 00:56:47 +08:00
|
|
|
config = {
|
2026-05-13 16:43:01 +08:00
|
|
|
"group_allow_from": ["wx:a"],
|
|
|
|
|
"groups": {"chat1": {"allow_from": ["wx:a", "wx:b"]}},
|
2026-05-12 00:56:47 +08:00
|
|
|
}
|
2026-05-13 16:43:01 +08:00
|
|
|
result = WeChatSecurityAdapter.resolve_group_allow_from(config, "chat1")
|
|
|
|
|
assert result.count("wx:a") == 1
|
|
|
|
|
assert "wx:b" in result
|
|
|
|
|
|
|
|
|
|
def test_collect_warnings_open_dm_policy(self):
|
|
|
|
|
warnings = WeChatSecurityAdapter.collect_warnings({"dm_policy": "open"})
|
|
|
|
|
assert any("dm_policy='open'" in w for w in warnings)
|
|
|
|
|
|
|
|
|
|
def test_collect_warnings_empty_allowlist(self):
|
|
|
|
|
warnings = WeChatSecurityAdapter.collect_warnings({"dm_policy": "allowlist", "allow_from": []})
|
|
|
|
|
assert any("allowlist 为空" in w for w in warnings)
|
|
|
|
|
|
|
|
|
|
def test_collect_warnings_empty_group_allowlist(self):
|
|
|
|
|
warnings = WeChatSecurityAdapter.collect_warnings({"group_policy": "allowlist"})
|
|
|
|
|
assert any("群组 allowlist 为空" in w for w in warnings)
|
|
|
|
|
|
|
|
|
|
def test_collect_warnings_no_pairing(self):
|
|
|
|
|
warnings = WeChatSecurityAdapter.collect_warnings({"dm_policy": "open"})
|
|
|
|
|
assert any("配对机制" in w for w in warnings)
|
|
|
|
|
|
|
|
|
|
def test_collect_warnings_with_pairing_ok(self):
|
|
|
|
|
warnings = WeChatSecurityAdapter.collect_warnings({
|
|
|
|
|
"dm_policy": "pairing",
|
|
|
|
|
"accounts": {"acc1": {"dm_policy": "pairing"}},
|
|
|
|
|
})
|
|
|
|
|
assert not any("配对机制" in w for w in warnings)
|
|
|
|
|
|
|
|
|
|
def test_collect_audit_findings_normal(self):
|
|
|
|
|
findings = WeChatSecurityAdapter.collect_audit_findings({"allow_from": ["wx:a"]})
|
|
|
|
|
assert len(findings) == 0
|
|
|
|
|
|
|
|
|
|
def test_collect_audit_findings_large_allowlist(self):
|
|
|
|
|
large_list = [f"wx:{i}" for i in range(2000)]
|
|
|
|
|
findings = WeChatSecurityAdapter.collect_audit_findings({"allow_from": large_list})
|
|
|
|
|
assert len(findings) > 0
|
|
|
|
|
assert findings[0]["category"] == "allowlist_size"
|