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