from __future__ import annotations from unittest.mock import AsyncMock, MagicMock, patch import pytest from yuxi.channels.adapters.nostr.pair import NostrPairingManager class TestNostrPairingManager: @pytest.fixture def crypto(self): crypto = MagicMock() crypto.build_and_sign_event.return_value = { "id": "pairing_event_id", "kind": 4, "content": "pairing challenge message", "tags": [], } crypto.npub = "npub1test" crypto.nsec.return_value = "nsec1test" return crypto @pytest.fixture def relay_manager(self): rm = MagicMock() rm.broadcast = AsyncMock(return_value=2) return rm @pytest.fixture def manager(self, crypto, relay_manager): return NostrPairingManager(crypto, relay_manager) def test_issue_challenge(self, manager): token = manager.issue_challenge("target_pubkey_hex") assert token.startswith(manager.PAIRING_CHALLENGE_PREFIX) assert "::" in token def test_issue_challenge_adds_to_pending(self, manager): token = manager.issue_challenge("target_pubkey_hex") body = token[len(manager.PAIRING_CHALLENGE_PREFIX):] challenge_id = body.split("::")[0] assert manager.get_pending(challenge_id) is not None assert manager.get_pending(challenge_id)["status"] == "pending" def test_verify_response_valid(self, manager): token = manager.issue_challenge("target_pubkey_hex") success, challenge_id = manager.verify_response(token) assert success is True def test_verify_response_invalid_prefix(self, manager): success, reason = manager.verify_response("not_a_pairing_response") assert success is False assert "not a pairing" in reason def test_verify_response_wrong_format(self, manager): success, reason = manager.verify_response(manager.PAIRING_CHALLENGE_PREFIX + "badformat") assert success is False assert "unknown challenge" in reason or "invalid pairing" in reason def test_verify_response_unknown_challenge(self, manager): success, reason = manager.verify_response( manager.PAIRING_CHALLENGE_PREFIX + "unknown_id::some_token" ) assert success is False assert "unknown challenge" in reason def test_verify_response_token_mismatch(self, manager): manager.issue_challenge("target_pubkey_hex") challenge_id = list(manager._pending.keys())[0] bad_token = manager.PAIRING_CHALLENGE_PREFIX + f"{challenge_id}::wrong_token" success, reason = manager.verify_response(bad_token) assert success is False assert "token mismatch" in reason def test_verify_response_already_approved(self, manager): token = manager.issue_challenge("target_pubkey_hex") manager.verify_response(token) success, reason = manager.verify_response(token) assert success is False assert "already approved" in reason async def test_approve_challenge(self, manager): token = manager.issue_challenge("target_pubkey_hex") body = token[len(manager.PAIRING_CHALLENGE_PREFIX):] challenge_id = body.split("::")[0] success, result = await manager.approve_challenge(challenge_id) assert success is True assert result == challenge_id async def test_approve_unknown_challenge(self, manager): success, reason = await manager.approve_challenge("unknown_id") assert success is False def test_deny_challenge(self, manager): token = manager.issue_challenge("target_pubkey_hex") body = token[len(manager.PAIRING_CHALLENGE_PREFIX):] challenge_id = body.split("::")[0] assert manager.deny_challenge(challenge_id) is True assert manager.get_pending(challenge_id)["status"] == "denied" def test_deny_unknown_challenge(self, manager): assert manager.deny_challenge("unknown_id") is False async def test_deny_challenge_async(self, manager): token = manager.issue_challenge("target_pubkey_hex") body = token[len(manager.PAIRING_CHALLENGE_PREFIX):] challenge_id = body.split("::")[0] assert await manager.deny_challenge_async(challenge_id) is True assert manager.get_pending(challenge_id)["status"] == "denied" def test_list_pending_only_shows_pending(self, manager): manager.issue_challenge("pubkey_a") challenge_id_b = manager.issue_challenge("pubkey_b") body = challenge_id_b[len(manager.PAIRING_CHALLENGE_PREFIX):] cid_b = body.split("::")[0] manager.deny_challenge(cid_b) pending = manager.list_pending() assert len(pending) == 1 def test_list_all_shows_all(self, manager): manager.issue_challenge("pubkey_a") manager.issue_challenge("pubkey_b") all_entries = manager.list_all() assert len(all_entries) == 2 def test_get_pending_unknown(self, manager): assert manager.get_pending("unknown") is None def test_cleanup_expired_removes_non_pending(self, manager): token = manager.issue_challenge("pubkey_a") body = token[len(manager.PAIRING_CHALLENGE_PREFIX):] challenge_id = body.split("::")[0] manager.deny_challenge(challenge_id) removed = manager.cleanup_expired(ttl_sec=0) assert removed >= 1 assert manager.get_pending(challenge_id) is None def test_cleanup_expired_removes_expired_pending(self, manager): manager.issue_challenge("pubkey_a") challenge_ids = list(manager._pending.keys()) manager._pending[challenge_ids[0]]["created_at"] = 0 removed = manager.cleanup_expired(ttl_sec=0) assert removed >= 1 async def test_send_pairing_challenge(self, manager): token = await manager.send_pairing_challenge("target_pubkey_hex") assert token is not None assert manager.PAIRING_CHALLENGE_PREFIX in token async def test_send_pairing_challenge_no_relays(self, manager): manager._relay_manager.broadcast = AsyncMock(return_value=0) token = await manager.send_pairing_challenge("target_pubkey_hex") assert token is None def test_on_pairing_request_callback(self, manager): callback_mock = AsyncMock() manager.on_pairing_request(callback_mock) assert len(manager._on_pairing_request) == 1 def test_on_pairing_approved_callback(self, manager): callback_mock = AsyncMock() manager.on_pairing_approved(callback_mock) assert len(manager._on_pairing_approved) == 1 def test_on_pairing_denied_callback(self, manager): callback_mock = AsyncMock() manager.on_pairing_denied(callback_mock) assert len(manager._on_pairing_denied) == 1