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

364 lines
14 KiB
Python
Raw Normal View History

from __future__ import annotations
import asyncio
import time
from unittest.mock import MagicMock, patch
import pytest
from yuxi.channels.adapters.whatsapp.error_policy import ErrorPolicy, ErrorPolicyConfig
from yuxi.channels.adapters.whatsapp.group_gating import GroupGating
from yuxi.channels.adapters.whatsapp.pairing import PairRequest, PairingManager
from yuxi.channels.adapters.whatsapp.security import (
DmPolicy,
GroupPolicy,
WhatsAppSecurityConfig,
WhatsAppSecurityPolicy,
)
class TestErrorPolicy:
def test_enum_values(self):
assert ErrorPolicy.OFF == "off"
assert ErrorPolicy.FULL == "full"
assert ErrorPolicy.MINIMAL == "minimal"
class TestErrorPolicyConfig:
def test_from_config_default(self):
cfg = ErrorPolicyConfig.from_config({})
assert cfg.policy == ErrorPolicy.OFF
assert cfg.cooldown_ms == 5000
assert cfg.max_errors_per_window == 5
assert cfg.window_seconds == 60.0
def test_from_config_explicit(self):
cfg = ErrorPolicyConfig.from_config({
"errorPolicy": "full",
"errorCooldownMs": 3000,
"errorMaxPerWindow": 10,
"errorWindowSeconds": 120.0,
})
assert cfg.policy == ErrorPolicy.FULL
assert cfg.cooldown_ms == 3000
assert cfg.max_errors_per_window == 10
assert cfg.window_seconds == 120.0
def test_from_config_invalid_fallback(self):
cfg = ErrorPolicyConfig.from_config({"errorPolicy": "invalid"})
assert cfg.policy == ErrorPolicy.OFF
def test_should_notify_off_policy(self):
cfg = ErrorPolicyConfig(policy=ErrorPolicy.OFF)
assert cfg.should_notify("chat1", "test error") is False
def test_should_notify_full_policy(self):
cfg = ErrorPolicyConfig(policy=ErrorPolicy.FULL, window_seconds=60.0)
assert cfg.should_notify("chat1", "test error") is True
def test_should_notify_rate_limited(self):
cfg = ErrorPolicyConfig(policy=ErrorPolicy.FULL, max_errors_per_window=2, window_seconds=60.0)
cfg.should_notify("chat1", "error1")
cfg.should_notify("chat1", "error2")
assert cfg.should_notify("chat1", "error3") is False
def test_format_error_message_full(self):
cfg = ErrorPolicyConfig(policy=ErrorPolicy.FULL)
assert "Error:" in cfg.format_error_message("something broke")
def test_format_error_message_minimal(self):
cfg = ErrorPolicyConfig(policy=ErrorPolicy.MINIMAL)
msg = cfg.format_error_message("something broke")
assert "Error:" not in msg
assert "try again later" in msg
class TestDmPolicy:
def test_enum_values(self):
assert DmPolicy.PAIRING == "pairing"
assert DmPolicy.ALLOWLIST == "allowlist"
assert DmPolicy.OPEN == "open"
assert DmPolicy.DISABLED == "disabled"
class TestGroupPolicy:
def test_enum_values(self):
assert GroupPolicy.OPEN == "open"
assert GroupPolicy.ALLOWLIST == "allowlist"
assert GroupPolicy.DISABLED == "disabled"
class TestWhatsAppSecurityConfig:
def test_default_values(self):
cfg = WhatsAppSecurityConfig()
assert cfg.dm_policy == DmPolicy.PAIRING
assert cfg.group_policy == GroupPolicy.ALLOWLIST
assert cfg.allow_from == []
assert cfg.group_allow_from == []
class TestWhatsAppSecurityPolicy:
def test_default_resolution(self):
policy = WhatsAppSecurityPolicy({})
assert policy.dm_policy == DmPolicy.OPEN
def test_dm_policy_pairing(self):
policy = WhatsAppSecurityPolicy({"dmPolicy": "pairing"})
assert policy.dm_policy == DmPolicy.PAIRING
def test_dm_policy_allowlist(self):
policy = WhatsAppSecurityPolicy({"dmPolicy": "allowlist", "allowFrom": ["+86138"]})
assert policy.dm_policy == DmPolicy.ALLOWLIST
def test_dm_policy_disabled(self):
policy = WhatsAppSecurityPolicy({"dmPolicy": "disabled"})
assert policy.dm_policy == DmPolicy.DISABLED
def test_group_policy_open(self):
policy = WhatsAppSecurityPolicy({"groupPolicy": "open"})
assert policy.group_policy == GroupPolicy.OPEN
def test_group_policy_allowlist_default(self):
policy = WhatsAppSecurityPolicy({})
assert policy.group_policy == GroupPolicy.ALLOWLIST
def test_group_policy_disabled(self):
policy = WhatsAppSecurityPolicy({"groupPolicy": "disabled"})
assert policy.group_policy == GroupPolicy.DISABLED
def test_check_dm_access_open(self):
policy = WhatsAppSecurityPolicy({"dmPolicy": "open"})
ok, reason = policy.check_dm_access("8613800138000")
assert ok is True
assert reason is None
def test_check_dm_access_disabled(self):
policy = WhatsAppSecurityPolicy({"dmPolicy": "disabled"})
ok, reason = policy.check_dm_access("8613800138000")
assert ok is False
assert reason == "dm_disabled"
def test_check_dm_access_allowlist_match(self):
policy = WhatsAppSecurityPolicy({"dmPolicy": "allowlist", "allowFrom": ["8613800138000"]})
ok, reason = policy.check_dm_access("8613800138000")
assert ok is True
def test_check_dm_access_allowlist_no_match(self):
policy = WhatsAppSecurityPolicy({"dmPolicy": "allowlist", "allowFrom": ["8613800138000"]})
ok, reason = policy.check_dm_access("8613800000000")
assert ok is False
assert reason == "not_in_allowlist"
def test_check_dm_access_pairing(self):
policy = WhatsAppSecurityPolicy({"dmPolicy": "pairing"})
ok, reason = policy.check_dm_access("8613800138000")
assert ok is True
def test_check_group_access_open(self):
policy = WhatsAppSecurityPolicy({"groupPolicy": "open"})
ok, reason = policy.check_group_access("123456789@g.us")
assert ok is True
def test_check_group_access_disabled(self):
policy = WhatsAppSecurityPolicy({"groupPolicy": "disabled"})
ok, reason = policy.check_group_access("123456789@g.us")
assert ok is False
assert reason == "group_disabled"
def test_check_group_access_allowlist_match(self):
policy = WhatsAppSecurityPolicy(
{"groupPolicy": "allowlist", "groupAllowFrom": ["123456789@g.us"]}
)
ok, reason = policy.check_group_access("123456789@g.us")
assert ok is True
def test_check_group_access_allowlist_no_match(self):
policy = WhatsAppSecurityPolicy(
{"groupPolicy": "allowlist", "groupAllowFrom": ["123456789@g.us"]}
)
ok, reason = policy.check_group_access("999999999@g.us")
assert ok is False
def test_add_to_allow_list(self):
policy = WhatsAppSecurityPolicy({"dmPolicy": "allowlist", "allowFrom": []})
policy.add_to_allow_list("8613800138000")
assert "8613800138000" in policy.allow_list
def test_add_to_allow_list_duplicate(self):
policy = WhatsAppSecurityPolicy({"dmPolicy": "allowlist", "allowFrom": ["8613800138000"]})
policy.add_to_allow_list("8613800138000")
assert len(policy.allow_list) == 1
def test_remove_from_allow_list(self):
policy = WhatsAppSecurityPolicy({"dmPolicy": "allowlist", "allowFrom": ["8613800138000"]})
assert policy.remove_from_allow_list("8613800138000") is True
assert "8613800138000" not in policy.allow_list
def test_remove_from_allow_list_nonexistent(self):
policy = WhatsAppSecurityPolicy({"dmPolicy": "allowlist", "allowFrom": []})
assert policy.remove_from_allow_list("nonexistent") is False
def test_wildcard_allow_list(self):
policy = WhatsAppSecurityPolicy({"dmPolicy": "allowlist", "allowFrom": ["*"]})
ok, _ = policy.check_dm_access("any_number")
assert ok is True
def test_normalize_allow_list_strips_plus(self):
result = WhatsAppSecurityPolicy._normalize_allow_list(["+8613800138000"])
assert "8613800138000" in result
def test_match_allow_list_wildcard(self):
assert WhatsAppSecurityPolicy._match_allow_list("anything", ["*"]) is True
def test_match_allow_list_exact(self):
assert WhatsAppSecurityPolicy._match_allow_list("8613800138000", ["8613800138000"]) is True
def test_collect_warnings_group_open_no_list(self):
policy = WhatsAppSecurityPolicy({"groupPolicy": "open"})
warnings = policy.collect_warnings()
assert len(warnings) > 0
def test_apply_config_fixes(self):
policy = WhatsAppSecurityPolicy({"groupPolicy": "open"})
fixes = policy.apply_config_fixes()
assert len(fixes) > 0
def test_allow_list_property(self):
policy = WhatsAppSecurityPolicy({"allowFrom": ["8613800138000", "8613800138001"]})
assert len(policy.allow_list) == 2
def test_group_allow_list_property(self):
policy = WhatsAppSecurityPolicy({"groupAllowFrom": ["123456789@g.us"]})
assert len(policy.group_allow_list) == 1
class TestPairRequest:
def test_create_pair_request(self):
req = PairRequest(phone_number="8613800138000", pairing_code="ABC123", expires_at=9999999999.0)
assert req.phone_number == "8613800138000"
assert req.pairing_code == "ABC123"
assert req.expires_at == 9999999999.0
def test_not_expired_future(self):
req = PairRequest(phone_number="86138", pairing_code="ABC", expires_at=9999999999.0)
assert req.is_expired is False
def test_not_expired_no_expiry(self):
req = PairRequest(phone_number="86138")
assert req.is_expired is False
def test_expired(self):
req = PairRequest(phone_number="86138", pairing_code="ABC", expires_at=0)
assert req.is_expired is True
class TestPairingManager:
def test_create_pair_request(self):
mgr = PairingManager()
req = mgr.create_pair_request("8613800138000", "ABC123", timeout=300)
assert req.phone_number == "8613800138000"
assert req.pairing_code == "ABC123"
def test_confirm_pair_success(self):
mgr = PairingManager()
mgr.create_pair_request("8613800138000", "ABC123")
assert mgr.confirm_pair("8613800138000") is True
def test_confirm_pair_nonexistent(self):
mgr = PairingManager()
assert mgr.confirm_pair("nonexistent") is False
def test_confirm_pair_expired(self):
mgr = PairingManager()
mgr.create_pair_request("8613800138000", "ABC123", timeout=0)
time.sleep(0.01)
assert mgr.confirm_pair("8613800138000") is False
def test_is_paired_after_confirm(self):
mgr = PairingManager()
mgr.create_pair_request("8613800138000", "ABC123")
mgr.confirm_pair("8613800138000")
assert mgr.is_paired("8613800138000") is True
def test_is_paired_false(self):
mgr = PairingManager()
assert mgr.is_paired("unknown") is False
def test_revoke_pair_pending(self):
mgr = PairingManager()
mgr.create_pair_request("8613800138000", "ABC123")
assert mgr.revoke_pair("8613800138000") is True
assert mgr.confirm_pair("8613800138000") is False
def test_revoke_pair_paired(self):
mgr = PairingManager()
mgr.create_pair_request("8613800138000", "ABC123")
mgr.confirm_pair("8613800138000")
assert mgr.is_paired("8613800138000") is True
assert mgr.revoke_pair("8613800138000") is True
assert mgr.is_paired("8613800138000") is False
def test_get_pending_phones(self):
mgr = PairingManager()
mgr.create_pair_request("8613800138000", "ABC")
mgr.create_pair_request("8613800138001", "DEF")
phones = mgr.get_pending_phones()
assert len(phones) == 2
assert "8613800138000" in phones
def test_cleanup_expired(self):
mgr = PairingManager()
mgr.create_pair_request("8613800138000", "ABC", timeout=0)
mgr.create_pair_request("8613800138001", "DEF", timeout=300)
time.sleep(0.01)
cleaned = mgr.cleanup_expired()
assert cleaned == 1
phones = mgr.get_pending_phones()
assert "8613800138001" in phones
assert "8613800138000" not in phones
class TestGroupGating:
def test_default_activated(self):
gg = GroupGating()
assert gg.is_activated("123456789@g.us") is True
def test_require_mention_default_false(self):
gg = GroupGating()
assert gg.require_mention("123456789@g.us") is False
def test_require_mention_configured(self):
gg = GroupGating({"groups": {"123456789": {"requireMention": True}}})
assert gg.require_mention("123456789@g.us") is True
def test_require_mention_by_full_jid(self):
gg = GroupGating({"groups": {"123456789@g.us": {"requireMention": True}}})
assert gg.require_mention("123456789@g.us") is True
def test_is_activated_configured_false(self):
gg = GroupGating({"groups": {"123456789": {"activated": False}}})
assert gg.is_activated("123456789@g.us") is False
def test_activate(self):
gg = GroupGating()
gg.deactivate("123456789@g.us")
assert gg.is_activated("123456789@g.us") is False
gg.activate("123456789@g.us")
assert gg.is_activated("123456789@g.us") is True
def test_deactivate(self):
gg = GroupGating()
gg.deactivate("123456789@g.us")
assert gg.is_activated("123456789@g.us") is False
def test_empty_config(self):
gg = GroupGating({})
assert gg.is_activated("any@g.us") is True
def test_invalid_groups_config(self):
gg = GroupGating({"groups": "not_a_dict"})
assert gg.is_activated("test@g.us") is True
def test_non_dict_group_entry(self):
gg = GroupGating({"groups": {"g1": "not_a_dict"}})
assert gg.is_activated("g1@g.us") is True