2026-05-12 00:56:47 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
from yuxi.channels.adapters.imessage.security import (
|
|
|
|
|
DmPolicy,
|
|
|
|
|
GroupPolicy,
|
|
|
|
|
IMessageSecurityPolicy,
|
|
|
|
|
SecurityCheckResult,
|
2026-05-13 16:43:01 +08:00
|
|
|
resolve_dm_policy,
|
|
|
|
|
resolve_group_policy,
|
2026-05-12 00:56:47 +08:00
|
|
|
_match_entry,
|
|
|
|
|
_normalize_handle,
|
|
|
|
|
_strip_prefix,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
class TestResolveDmPolicy:
|
|
|
|
|
def test_explicit_pairing(self):
|
|
|
|
|
assert resolve_dm_policy({"dmPolicy": "pairing"}) == DmPolicy.PAIRING
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_explicit_open(self):
|
|
|
|
|
assert resolve_dm_policy({"dm_policy": "open"}) == DmPolicy.OPEN
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_explicit_disabled(self):
|
|
|
|
|
assert resolve_dm_policy({"dmPolicy": "disabled"}) == DmPolicy.DISABLED
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_explicit_allowlist(self):
|
|
|
|
|
assert resolve_dm_policy({"dm_policy": "allowlist"}) == DmPolicy.ALLOWLIST
|
|
|
|
|
|
|
|
|
|
def test_invalid_explicit_falls_back(self):
|
|
|
|
|
assert resolve_dm_policy({"dm_policy": "invalid"}) == DmPolicy.OPEN
|
|
|
|
|
|
|
|
|
|
def test_allow_from_with_wildcard(self):
|
|
|
|
|
assert resolve_dm_policy({"allowFrom": ["*"]}) == DmPolicy.OPEN
|
|
|
|
|
|
|
|
|
|
def test_allow_from_with_entries(self):
|
|
|
|
|
assert resolve_dm_policy({"allowFrom": ["+8613800138000"]}) == DmPolicy.ALLOWLIST
|
|
|
|
|
|
|
|
|
|
def test_empty_allow_from(self):
|
|
|
|
|
assert resolve_dm_policy({"allowFrom": []}) == DmPolicy.OPEN
|
|
|
|
|
|
|
|
|
|
def test_no_policy_specified(self):
|
|
|
|
|
assert resolve_dm_policy({}) == DmPolicy.OPEN
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestResolveGroupPolicy:
|
|
|
|
|
def test_explicit_open(self):
|
|
|
|
|
assert resolve_group_policy({"groupPolicy": "open"}) == GroupPolicy.OPEN
|
|
|
|
|
|
|
|
|
|
def test_explicit_disabled(self):
|
|
|
|
|
assert resolve_group_policy({"group_policy": "disabled"}) == GroupPolicy.DISABLED
|
|
|
|
|
|
|
|
|
|
def test_explicit_allowlist(self):
|
|
|
|
|
assert resolve_group_policy({"groupPolicy": "allowlist"}) == GroupPolicy.ALLOWLIST
|
|
|
|
|
|
|
|
|
|
def test_invalid_explicit_falls_back(self):
|
|
|
|
|
assert resolve_group_policy({"groupPolicy": "invalid"}) == GroupPolicy.ALLOWLIST
|
|
|
|
|
|
|
|
|
|
def test_no_policy_specified(self):
|
|
|
|
|
assert resolve_group_policy({}) == GroupPolicy.ALLOWLIST
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestNormalizeHandle:
|
|
|
|
|
def test_removes_plus(self):
|
|
|
|
|
assert _normalize_handle("+8613800138000") == "8613800138000"
|
|
|
|
|
|
|
|
|
|
def test_removes_spaces(self):
|
|
|
|
|
assert _normalize_handle(" +86 138 0013 8000 ") == "8613800138000"
|
|
|
|
|
|
|
|
|
|
def test_email_preserved(self):
|
|
|
|
|
assert _normalize_handle("test@example.com") == "test@example.com"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestStripPrefix:
|
|
|
|
|
def test_no_prefix(self):
|
|
|
|
|
assert _strip_prefix("+8613800138000") == ("+8613800138000", None)
|
|
|
|
|
|
|
|
|
|
def test_imessage_prefix(self):
|
|
|
|
|
assert _strip_prefix("imessage:+8613800138000") == ("+8613800138000", "imessage")
|
|
|
|
|
|
|
|
|
|
def test_sms_prefix(self):
|
|
|
|
|
assert _strip_prefix("sms:+8613800138000") == ("+8613800138000", "sms")
|
|
|
|
|
|
|
|
|
|
def test_chat_id_prefix(self):
|
|
|
|
|
assert _strip_prefix("chat_id:test123") == ("test123", "chat_id")
|
|
|
|
|
|
|
|
|
|
def test_chat_guid_prefix(self):
|
|
|
|
|
assert _strip_prefix("chat_guid:test456") == ("test456", "chat_guid")
|
|
|
|
|
|
|
|
|
|
def test_prefix_case_insensitive(self):
|
|
|
|
|
assert _strip_prefix("IMESSAGE:+8613800138000") == ("+8613800138000", "imessage")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestMatchEntry:
|
|
|
|
|
def test_wildcard(self):
|
|
|
|
|
assert _match_entry("anyone", ["*"]) is True
|
|
|
|
|
|
|
|
|
|
def test_exact_simple_match(self):
|
|
|
|
|
assert _match_entry("+8613800138000", ["+8613800138000"]) is True
|
|
|
|
|
|
|
|
|
|
def test_exact_match_with_imessage_prefix(self):
|
|
|
|
|
assert _match_entry("+8613800138000", ["imessage:+8613800138000"]) is True
|
|
|
|
|
|
|
|
|
|
def test_exact_match_with_sms_prefix(self):
|
|
|
|
|
assert _match_entry("+8613800138000", ["sms:+8613800138000"]) is True
|
|
|
|
|
|
|
|
|
|
def test_no_match(self):
|
|
|
|
|
assert _match_entry("+8613800138000", ["+8613900139000"]) is False
|
|
|
|
|
|
|
|
|
|
def test_email_match(self):
|
|
|
|
|
assert _match_entry("test@example.com", ["test@example.com"]) is True
|
|
|
|
|
|
|
|
|
|
def test_chat_guid_match(self):
|
|
|
|
|
assert _match_entry("iMessage;-;group001", ["chat_guid:iMessage;-;group001"]) is True
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_empty_allow_list(self):
|
|
|
|
|
assert _match_entry("+8613800138000", []) is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestSecurityCheckResult:
|
|
|
|
|
def test_allowed(self):
|
|
|
|
|
r = SecurityCheckResult(allowed=True)
|
|
|
|
|
assert r.allowed is True
|
|
|
|
|
assert r.reject_reason is None
|
|
|
|
|
assert r.reply is None
|
|
|
|
|
|
|
|
|
|
def test_blocked_with_reason(self):
|
|
|
|
|
r = SecurityCheckResult(allowed=False, reject_reason="not_in_allowlist", reply="Unauthorized")
|
|
|
|
|
assert r.allowed is False
|
|
|
|
|
assert r.reject_reason == "not_in_allowlist"
|
|
|
|
|
assert r.reply == "Unauthorized"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestIMessageSecurityPolicy:
|
|
|
|
|
def make_policy(self, **overrides):
|
|
|
|
|
config = {
|
|
|
|
|
"dmPolicy": "pairing",
|
|
|
|
|
"groupPolicy": "allowlist",
|
|
|
|
|
"allowFrom": ["+8613800138000"],
|
|
|
|
|
"groupAllowFrom": ["iMessage;-;group001"],
|
|
|
|
|
**overrides,
|
|
|
|
|
}
|
|
|
|
|
return IMessageSecurityPolicy(config)
|
|
|
|
|
|
|
|
|
|
def test_initialization(self):
|
|
|
|
|
policy = self.make_policy()
|
|
|
|
|
assert policy.dm_policy == DmPolicy.PAIRING
|
|
|
|
|
assert policy.group_policy == GroupPolicy.ALLOWLIST
|
|
|
|
|
assert len(policy.allow_list) > 0
|
|
|
|
|
assert len(policy.group_allow_list) > 0
|
|
|
|
|
|
|
|
|
|
def test_check_dm_open(self):
|
|
|
|
|
policy = self.make_policy(dmPolicy="open")
|
|
|
|
|
result = policy.check_dm_access("+8613900139000")
|
2026-05-12 00:56:47 +08:00
|
|
|
assert result.allowed is True
|
|
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_check_dm_disabled(self):
|
|
|
|
|
policy = self.make_policy(dmPolicy="disabled")
|
|
|
|
|
result = policy.check_dm_access("+8613800138000")
|
2026-05-12 00:56:47 +08:00
|
|
|
assert result.allowed is False
|
2026-05-13 16:43:01 +08:00
|
|
|
assert "disabled" in str(result.reject_reason)
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_check_dm_allowlist_allowed(self):
|
|
|
|
|
policy = self.make_policy(dmPolicy="allowlist", allowFrom=["+8613800138000"])
|
|
|
|
|
result = policy.check_dm_access("+8613800138000")
|
2026-05-12 00:56:47 +08:00
|
|
|
assert result.allowed is True
|
|
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_check_dm_allowlist_blocked(self):
|
|
|
|
|
policy = self.make_policy(dmPolicy="allowlist", allowFrom=["+8613800138000"])
|
|
|
|
|
result = policy.check_dm_access("+8613900139000")
|
2026-05-12 00:56:47 +08:00
|
|
|
assert result.allowed is False
|
2026-05-13 16:43:01 +08:00
|
|
|
assert "not_in_dm_allowlist" in str(result.reject_reason)
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_check_dm_pairing(self):
|
|
|
|
|
policy = self.make_policy(dmPolicy="pairing")
|
|
|
|
|
result = policy.check_dm_access("+8613900139000")
|
2026-05-12 00:56:47 +08:00
|
|
|
assert result.allowed is True
|
|
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_check_group_open(self):
|
|
|
|
|
policy = self.make_policy(groupPolicy="open")
|
|
|
|
|
result = policy.check_group_access("iMessage;-;group002")
|
2026-05-12 00:56:47 +08:00
|
|
|
assert result.allowed is True
|
|
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_check_group_disabled(self):
|
|
|
|
|
policy = self.make_policy(groupPolicy="disabled")
|
|
|
|
|
result = policy.check_group_access("iMessage;-;group001")
|
2026-05-12 00:56:47 +08:00
|
|
|
assert result.allowed is False
|
|
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_check_group_allowlist_allowed(self):
|
|
|
|
|
policy = self.make_policy(groupPolicy="allowlist", groupAllowFrom=["iMessage;-;group001"])
|
|
|
|
|
result = policy.check_group_access("iMessage;-;group001")
|
2026-05-12 00:56:47 +08:00
|
|
|
assert result.allowed is True
|
|
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_check_group_allowlist_blocked(self):
|
|
|
|
|
policy = self.make_policy(groupPolicy="allowlist", groupAllowFrom=["iMessage;-;group001"])
|
|
|
|
|
result = policy.check_group_access("iMessage;-;group002")
|
2026-05-12 00:56:47 +08:00
|
|
|
assert result.allowed is False
|
|
|
|
|
|
|
|
|
|
def test_check_group_override_disabled(self):
|
2026-05-13 16:43:01 +08:00
|
|
|
policy = self.make_policy(
|
|
|
|
|
groupPolicy="open",
|
|
|
|
|
groups={"iMessage;-;group001": {"enabled": False}},
|
2026-05-12 00:56:47 +08:00
|
|
|
)
|
|
|
|
|
result = policy.check_group_access("iMessage;-;group001")
|
|
|
|
|
assert result.allowed is False
|
|
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_check_mention_required_not_required(self):
|
|
|
|
|
policy = self.make_policy(requireMention=False)
|
|
|
|
|
result = policy.check_mention_required("iMessage;-;group001", None, "")
|
2026-05-12 00:56:47 +08:00
|
|
|
assert result.allowed is True
|
|
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_check_mention_required_bot_mentioned(self):
|
|
|
|
|
policy = self.make_policy(requireMention=True)
|
|
|
|
|
from yuxi.channels.models import MentionsInfo
|
|
|
|
|
|
|
|
|
|
mentions = MentionsInfo(mentioned_user_ids=["+8613800138000"])
|
2026-05-12 00:56:47 +08:00
|
|
|
result = policy.check_mention_required(
|
2026-05-13 16:43:01 +08:00
|
|
|
"iMessage;-;group001", mentions, "+8613800138000"
|
2026-05-12 00:56:47 +08:00
|
|
|
)
|
|
|
|
|
assert result.allowed is True
|
|
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_check_mention_required_bot_not_mentioned(self):
|
|
|
|
|
policy = self.make_policy(requireMention=True)
|
|
|
|
|
from yuxi.channels.models import MentionsInfo
|
|
|
|
|
|
|
|
|
|
mentions = MentionsInfo(mentioned_user_ids=["+8613900139000"])
|
2026-05-12 00:56:47 +08:00
|
|
|
result = policy.check_mention_required(
|
2026-05-13 16:43:01 +08:00
|
|
|
"iMessage;-;group001", mentions, "+8613800138000"
|
2026-05-12 00:56:47 +08:00
|
|
|
)
|
|
|
|
|
assert result.allowed is False
|
|
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_add_to_allow_list(self):
|
|
|
|
|
policy = self.make_policy(allowFrom=["+8613800138000"])
|
|
|
|
|
policy.add_to_allow_list("+8613900139000")
|
|
|
|
|
assert "8613900139000" in str(policy.allow_list)
|
|
|
|
|
|
|
|
|
|
def test_add_duplicate_noop(self):
|
|
|
|
|
policy = self.make_policy(allowFrom=["+8613800138000"])
|
|
|
|
|
length_before = len(policy.allow_list)
|
|
|
|
|
policy.add_to_allow_list("+8613800138000")
|
|
|
|
|
assert len(policy.allow_list) == length_before
|
|
|
|
|
|
|
|
|
|
def test_remove_from_allow_list(self):
|
|
|
|
|
policy = self.make_policy(allowFrom=["+8613800138000", "+8613900139000"])
|
|
|
|
|
assert policy.remove_from_allow_list("+8613800138000") is True
|
|
|
|
|
assert policy.remove_from_allow_list("nonexistent") is False
|
|
|
|
|
|
|
|
|
|
def test_add_to_group_allow_list(self):
|
|
|
|
|
policy = self.make_policy(groupAllowFrom=[])
|
|
|
|
|
policy.add_to_group_allow_list("iMessage;-;group_new")
|
|
|
|
|
assert "iMessage;-;group_new" in policy.group_allow_list
|
|
|
|
|
|
|
|
|
|
def test_remove_from_group_allow_list(self):
|
|
|
|
|
policy = self.make_policy(
|
|
|
|
|
groupAllowFrom=["iMessage;-;group001", "iMessage;-;group002"]
|
2026-05-12 00:56:47 +08:00
|
|
|
)
|
2026-05-13 16:43:01 +08:00
|
|
|
assert policy.remove_from_group_allow_list("iMessage;-;group001") is True
|
|
|
|
|
assert policy.remove_from_group_allow_list("nonexistent") is False
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_collect_warnings_dm_open(self):
|
|
|
|
|
policy = self.make_policy(dmPolicy="open")
|
|
|
|
|
warnings = policy.collect_warnings()
|
|
|
|
|
assert any("open" in w.lower() for w in warnings)
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_collect_warnings_allowlist_empty(self):
|
|
|
|
|
policy = self.make_policy(dmPolicy="allowlist", allowFrom=[])
|
|
|
|
|
warnings = policy.collect_warnings()
|
|
|
|
|
assert any("empty" in w.lower() for w in warnings)
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_collect_warnings_group_allowlist_empty(self):
|
|
|
|
|
policy = self.make_policy(groupPolicy="allowlist", groupAllowFrom=[])
|
|
|
|
|
warnings = policy.collect_warnings()
|
|
|
|
|
assert any("empty" in w.lower() for w in warnings)
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_collect_warnings_group_open(self):
|
|
|
|
|
policy = self.make_policy(groupPolicy="open", groupAllowFrom=[])
|
|
|
|
|
warnings = policy.collect_warnings()
|
|
|
|
|
assert len(warnings) >= 1
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_collect_warnings_fallback_enabled(self):
|
|
|
|
|
policy = self.make_policy(groupAllowFromFallback=True)
|
|
|
|
|
warnings = policy.collect_warnings()
|
|
|
|
|
assert any("fallback" in w.lower() for w in warnings)
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_resolve_runtime_group_policy_explicit(self):
|
|
|
|
|
policy = self.make_policy(
|
|
|
|
|
groups={"iMessage;-;group001": {"group_policy": "open"}}
|
2026-05-12 00:56:47 +08:00
|
|
|
)
|
|
|
|
|
result = policy.resolve_runtime_group_policy("iMessage;-;group001")
|
2026-05-13 16:43:01 +08:00
|
|
|
assert result == GroupPolicy.OPEN
|
|
|
|
|
|
|
|
|
|
def test_resolve_runtime_group_policy_default(self):
|
|
|
|
|
policy = self.make_policy(groupPolicy="allowlist")
|
|
|
|
|
result = policy.resolve_runtime_group_policy("iMessage;-;nonexistent")
|
2026-05-12 00:56:47 +08:00
|
|
|
assert result == GroupPolicy.ALLOWLIST
|
|
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_resolve_pinned_dm_owner(self):
|
|
|
|
|
policy = self.make_policy(allowFrom=["+8613800138000"])
|
|
|
|
|
owner = policy.resolve_pinned_dm_owner()
|
|
|
|
|
assert owner == "8613800138000"
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_resolve_pinned_dm_owner_with_prefix(self):
|
|
|
|
|
policy = self.make_policy(allowFrom=["imsg:+8613800138000"])
|
|
|
|
|
owner = policy.resolve_pinned_dm_owner()
|
|
|
|
|
assert owner is not None
|
|
|
|
|
|
|
|
|
|
def test_context_visibility_open(self):
|
|
|
|
|
policy = self.make_policy(contextVisibilityMode="open")
|
|
|
|
|
assert policy.evaluate_context_visibility(
|
|
|
|
|
"iMessage;+;chat001", "+8613900139000"
|
|
|
|
|
) is True
|
|
|
|
|
|
|
|
|
|
def test_context_visibility_disabled(self):
|
|
|
|
|
policy = self.make_policy(contextVisibilityMode="disabled")
|
|
|
|
|
assert policy.evaluate_context_visibility(
|
|
|
|
|
"iMessage;+;chat001", "+8613800138000"
|
|
|
|
|
) is False
|
|
|
|
|
|
|
|
|
|
def test_context_visibility_allowlist_matched(self):
|
|
|
|
|
policy = self.make_policy(
|
|
|
|
|
contextVisibilityMode="allowlist", allowFrom=["+8613800138000"]
|
2026-05-12 00:56:47 +08:00
|
|
|
)
|
2026-05-13 16:43:01 +08:00
|
|
|
assert policy.evaluate_context_visibility(
|
|
|
|
|
"iMessage;+;chat001", "+8613800138000"
|
|
|
|
|
) is True
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_context_visibility_allowlist_not_matched(self):
|
|
|
|
|
policy = self.make_policy(
|
|
|
|
|
contextVisibilityMode="allowlist", allowFrom=["+8613800138000"]
|
2026-05-12 00:56:47 +08:00
|
|
|
)
|
2026-05-13 16:43:01 +08:00
|
|
|
assert policy.evaluate_context_visibility(
|
|
|
|
|
"iMessage;+;chat001", "+8613900139000"
|
|
|
|
|
) is False
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_require_mention_property(self):
|
|
|
|
|
policy = self.make_policy(requireMention=True)
|
|
|
|
|
assert policy.require_mention is True
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_should_update_last_route(self):
|
|
|
|
|
policy = self.make_policy(allowFrom=["+8613800138000"])
|
|
|
|
|
assert policy.should_update_last_route("+8613800138000") is True
|
2026-05-12 00:56:47 +08:00
|
|
|
|
2026-05-13 16:43:01 +08:00
|
|
|
def test_should_update_last_route_no_owner(self):
|
|
|
|
|
policy = self.make_policy(allowFrom=[])
|
|
|
|
|
assert policy.should_update_last_route("+8613800138000") is True
|