169 lines
6.7 KiB
Python
169 lines
6.7 KiB
Python
|
|
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
|