207 lines
6.9 KiB
Python
207 lines
6.9 KiB
Python
"""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
|