"""Tests for RNSTransportAdapter peer discovery logic (RNS networking mocked).""" import time from unittest.mock import MagicMock, patch, call import pytest import RNS from radicle_reticulum.adapter import ( PeerInfo, RNSTransportAdapter, NODE_APP_DATA_MAGIC, REPO_APP_DATA_MAGIC, ) from radicle_reticulum.identity import RadicleIdentity # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _make_dest_mock(hash_bytes: bytes = b"\xaa" * 16) -> MagicMock: dest = MagicMock() dest.hash = hash_bytes dest.hexhash = hash_bytes.hex() return dest def _make_adapter() -> RNSTransportAdapter: """Instantiate adapter with all RNS I/O patched out.""" identity = RadicleIdentity.generate() dest_mock = _make_dest_mock() with patch("radicle_reticulum.adapter.RNS.Reticulum"), \ patch("radicle_reticulum.adapter.RNS.Destination", return_value=dest_mock), \ patch("radicle_reticulum.adapter.RNS.log"): adapter = RNSTransportAdapter(identity=identity) return adapter # --------------------------------------------------------------------------- # PeerInfo # --------------------------------------------------------------------------- class TestPeerInfo: def test_age_increases_over_time(self): identity = RadicleIdentity.generate() peer = PeerInfo( identity=identity, destination_hash=b"\x01" * 16, last_seen=time.time() - 5.0, ) assert peer.age >= 5.0 def test_age_near_zero_for_fresh_peer(self): identity = RadicleIdentity.generate() peer = PeerInfo( identity=identity, destination_hash=b"\x02" * 16, last_seen=time.time(), ) assert peer.age < 1.0 def test_announced_repos_defaults_empty(self): identity = RadicleIdentity.generate() peer = PeerInfo(identity=identity, destination_hash=b"\x03" * 16, last_seen=0) assert peer.announced_repos == set() # --------------------------------------------------------------------------- # Adapter construction & peer list # --------------------------------------------------------------------------- class TestAdapterConstruction: def test_identity_is_stored(self): adapter = _make_adapter() assert adapter.identity is not None assert adapter.identity.did.startswith("did:key:") def test_initial_peer_list_is_empty(self): adapter = _make_adapter() assert adapter.get_peers() == [] def test_get_peer_by_did_returns_none_when_absent(self): adapter = _make_adapter() assert adapter.get_peer_by_did("did:key:z6Mkunknown") is None # --------------------------------------------------------------------------- # _handle_announce # --------------------------------------------------------------------------- class TestHandleAnnounce: def _make_rns_identity(self) -> RNS.Identity: """Create a real RNS.Identity (keypair only, no networking).""" return RNS.Identity() def test_ignores_own_announce(self): adapter = _make_adapter() own_hash = adapter.node_destination.hash # the mock's hash discovered = [] adapter.set_on_peer_discovered(discovered.append) rns_id = self._make_rns_identity() with patch("radicle_reticulum.adapter.RNS.log"): adapter._handle_announce(own_hash, rns_id, NODE_APP_DATA_MAGIC) assert discovered == [] assert adapter.get_peers() == [] def test_ignores_non_radicle_announce(self): adapter = _make_adapter() rns_id = self._make_rns_identity() foreign_hash = b"\xbb" * 16 discovered = [] adapter.set_on_peer_discovered(discovered.append) with patch("radicle_reticulum.adapter.RNS.log"): adapter._handle_announce(foreign_hash, rns_id, b"SOME_OTHER_APP") assert discovered == [] def test_ignores_announce_with_no_app_data(self): adapter = _make_adapter() rns_id = self._make_rns_identity() with patch("radicle_reticulum.adapter.RNS.log"): adapter._handle_announce(b"\xcc" * 16, rns_id, None) assert adapter.get_peers() == [] def test_discovers_valid_peer(self): adapter = _make_adapter() rns_id = self._make_rns_identity() peer_hash = b"\xdd" * 16 discovered = [] adapter.set_on_peer_discovered(discovered.append) with patch("radicle_reticulum.adapter.RNS.log"): adapter._handle_announce(peer_hash, rns_id, NODE_APP_DATA_MAGIC) assert len(discovered) == 1 peers = adapter.get_peers() assert len(peers) == 1 assert peers[0].destination_hash == peer_hash def test_second_announce_updates_last_seen_not_duplicates(self): adapter = _make_adapter() rns_id = self._make_rns_identity() peer_hash = b"\xee" * 16 discovered = [] adapter.set_on_peer_discovered(discovered.append) with patch("radicle_reticulum.adapter.RNS.log"): adapter._handle_announce(peer_hash, rns_id, NODE_APP_DATA_MAGIC) old_seen = adapter.get_peers()[0].last_seen time.sleep(0.01) adapter._handle_announce(peer_hash, rns_id, NODE_APP_DATA_MAGIC) # Callback only fires once (first discovery) assert len(discovered) == 1 # But last_seen was refreshed assert adapter.get_peers()[0].last_seen >= old_seen def test_multiple_distinct_peers_are_all_tracked(self): adapter = _make_adapter() with patch("radicle_reticulum.adapter.RNS.log"): for i in range(3): rns_id = self._make_rns_identity() peer_hash = bytes([i]) * 16 adapter._handle_announce(peer_hash, rns_id, NODE_APP_DATA_MAGIC) assert len(adapter.get_peers()) == 3 def test_get_peer_by_did_returns_correct_peer(self): adapter = _make_adapter() rns_id = self._make_rns_identity() peer_hash = b"\xff" * 16 with patch("radicle_reticulum.adapter.RNS.log"): adapter._handle_announce(peer_hash, rns_id, NODE_APP_DATA_MAGIC) peers = adapter.get_peers() did = peers[0].identity.did found = adapter.get_peer_by_did(did) assert found is not None assert found.destination_hash == peer_hash def test_extra_app_data_after_magic_still_accepted(self): """Announces with extra bytes after the magic prefix are valid.""" adapter = _make_adapter() rns_id = self._make_rns_identity() peer_hash = b"\x11" * 16 with patch("radicle_reticulum.adapter.RNS.log"): adapter._handle_announce( peer_hash, rns_id, NODE_APP_DATA_MAGIC + b"\x00\x01extra" ) assert len(adapter.get_peers()) == 1