"""Tests for US-017: P2P gossip, relay node, and TLS infrastructure.""" from __future__ import annotations import json import threading import time from pathlib import Path from unittest.mock import MagicMock, patch # --------------------------------------------------------------------------- # identity tests # --------------------------------------------------------------------------- def test_load_or_create_identity_generates_peer_id(tmp_path): from meshnet_p2p.identity import load_or_create_identity identity = load_or_create_identity(tmp_path / "identity.json") assert len(identity["peer_id"]) == 16 assert "public_key_pem" in identity assert "private_key_pem" in identity def test_identity_is_stable_across_loads(tmp_path): from meshnet_p2p.identity import load_or_create_identity path = tmp_path / "identity.json" first = load_or_create_identity(path) second = load_or_create_identity(path) assert first["peer_id"] == second["peer_id"] assert first["public_key_pem"] == second["public_key_pem"] def test_identity_different_for_different_paths(tmp_path): from meshnet_p2p.identity import load_or_create_identity a = load_or_create_identity(tmp_path / "a.json") b = load_or_create_identity(tmp_path / "b.json") # Extremely unlikely to collide assert a["peer_id"] != b["peer_id"] # --------------------------------------------------------------------------- # TLS / certificate tests # --------------------------------------------------------------------------- def test_generate_self_signed_cert_creates_files(tmp_path): from meshnet_p2p.tls import generate_self_signed_cert cert_p, key_p = generate_self_signed_cert( cert_path=tmp_path / "cert.pem", key_path=tmp_path / "key.pem", common_name="localhost", ) assert cert_p.exists() assert key_p.exists() assert cert_p.stat().st_size > 100 assert key_p.stat().st_size > 100 def test_generate_self_signed_cert_is_idempotent(tmp_path): from meshnet_p2p.tls import generate_self_signed_cert args = dict(cert_path=tmp_path / "cert.pem", key_path=tmp_path / "key.pem", common_name="test") generate_self_signed_cert(**args) mtime1 = (tmp_path / "cert.pem").stat().st_mtime generate_self_signed_cert(**args) mtime2 = (tmp_path / "cert.pem").stat().st_mtime assert mtime1 == mtime2 # file not regenerated def test_cert_fingerprint_returns_sha256_prefix(tmp_path): from meshnet_p2p.tls import generate_self_signed_cert, cert_fingerprint cert_p, key_p = generate_self_signed_cert( cert_path=tmp_path / "cert.pem", key_path=tmp_path / "key.pem", common_name="test", ) fp = cert_fingerprint(cert_p) assert fp.startswith("sha256:") assert len(fp) == len("sha256:") + 64 # 32 bytes hex = 64 chars def test_make_server_ssl_context_loads_cert(tmp_path): import ssl from meshnet_p2p.tls import generate_self_signed_cert, make_server_ssl_context cert_p, key_p = generate_self_signed_cert( cert_path=tmp_path / "cert.pem", key_path=tmp_path / "key.pem", common_name="test", ) ctx = make_server_ssl_context(cert_p, key_p) assert isinstance(ctx, ssl.SSLContext) # --------------------------------------------------------------------------- # PeerRegistry tests # --------------------------------------------------------------------------- def test_peer_registry_register_and_list(): from meshnet_relay.peer_registry import PeerRegistry reg = PeerRegistry() ws_mock = MagicMock() reg.register("peer1", "http://1.2.3.4:8001", ws_mock) assert len(reg) == 1 peers = reg.list_peers() assert len(peers) == 1 assert peers[0]["peer_id"] == "peer1" def test_peer_registry_all_except_excludes_sender(): from meshnet_relay.peer_registry import PeerRegistry reg = PeerRegistry() reg.register("a", "http://a:8001", MagicMock()) reg.register("b", "http://b:8001", MagicMock()) reg.register("c", "http://c:8001", MagicMock()) others = reg.all_except("a") assert len(others) == 2 assert all(e.peer_id != "a" for e in others) def test_peer_registry_unregister_removes_peer(): from meshnet_relay.peer_registry import PeerRegistry reg = PeerRegistry() reg.register("x", "http://x:8001", MagicMock()) reg.unregister("x") assert len(reg) == 0 assert reg.get("x") is None # --------------------------------------------------------------------------- # GossipClient + RelayServer integration test # --------------------------------------------------------------------------- def _start_relay(host="127.0.0.1", port=0): from meshnet_relay.server import RelayServer server = RelayServer(host=host, port=port) actual_port = server.start() return server, actual_port def test_gossip_fanout_through_relay(): """Node B publishes node-join; node A receives it within 2 seconds.""" from meshnet_p2p.gossip import GossipClient, TOPIC_NODE_JOIN relay, port = _start_relay() relay_url = f"ws://127.0.0.1:{port}/ws" client_a = GossipClient(relay_url=relay_url, peer_id="peer_a") client_b = GossipClient(relay_url=relay_url, peer_id="peer_b") received = [] client_a.subscribe(TOPIC_NODE_JOIN, lambda env: received.append(env)) client_a.start() client_b.start() assert client_a.wait_connected(timeout=5), "client_a failed to connect to relay" assert client_b.wait_connected(timeout=5), "client_b failed to connect to relay" # Give both peers time to register with relay time.sleep(0.2) client_b.publish(TOPIC_NODE_JOIN, {"addr": "http://192.168.1.10:8001", "peer_id": "peer_b"}) deadline = time.monotonic() + 2.0 while time.monotonic() < deadline and not received: time.sleep(0.05) client_a.stop() client_b.stop() relay.stop() assert received, "client_a did not receive node-join message from client_b" assert received[0]["topic"] == TOPIC_NODE_JOIN assert received[0]["from_peer"] == "peer_b" def test_gossip_dedup_prevents_processing_duplicate_message_ids(): """A message with a duplicate msg_id is only processed once.""" from meshnet_p2p.gossip import GossipClient, TOPIC_NODE_JOIN relay, port = _start_relay() relay_url = f"ws://127.0.0.1:{port}/ws" client_a = GossipClient(relay_url=relay_url, peer_id="peer_a2") client_b = GossipClient(relay_url=relay_url, peer_id="peer_b2") received = [] client_a.subscribe(TOPIC_NODE_JOIN, lambda env: received.append(env)) client_a.start() client_b.start() client_a.wait_connected(5) client_b.wait_connected(5) time.sleep(0.2) # Publish once client_b.publish(TOPIC_NODE_JOIN, {"test": "dedup"}) time.sleep(0.5) count = len(received) client_a.stop() client_b.stop() relay.stop() # Should have received exactly one message (not duplicated) assert count <= 1 def test_relay_server_peer_list_grows_on_connect(): """Relay registry grows when clients connect.""" from meshnet_p2p.gossip import GossipClient relay, port = _start_relay() relay_url = f"ws://127.0.0.1:{port}/ws" client = GossipClient(relay_url=relay_url, peer_id="solo_peer") client.start() client.wait_connected(5) time.sleep(0.3) # let peer-register message process peer_count = len(relay.registry) client.stop() relay.stop() assert peer_count >= 1 def test_relay_circuit_relay_proxies_message(): """A node behind NAT (client_a) receives a message via circuit relay from client_b.""" import websockets.sync.client # type: ignore[import] from meshnet_relay.server import RelayServer relay = RelayServer(host="127.0.0.1", port=0) port = relay.start() # client_a connects to gossip hub and registers received_via_relay = [] ready = threading.Event() def run_nat_client(): import websockets.sync.client as wsc with wsc.connect(f"ws://127.0.0.1:{port}/ws") as ws: ws.send(json.dumps({ "topic": "peer-register", "version": 1, "from_peer": "nat_peer", "msg_id": "reg-001", "timestamp": "2026-06-29T00:00:00Z", "ttl": 3, "payload": {"peer_id": "nat_peer", "addr": ""}, })) # consume peer-list response ws.recv() ready.set() # wait for a relayed message try: msg = ws.recv(timeout=3) received_via_relay.append(json.loads(msg)) except Exception: pass nat_thread = threading.Thread(target=run_nat_client, daemon=True) nat_thread.start() ready.wait(timeout=5) time.sleep(0.1) # ensure peer is in registry # client_b connects via circuit relay path def send_via_relay(): try: import websockets.sync.client as wsc with wsc.connect(f"ws://127.0.0.1:{port}/relay/nat_peer") as ws: ws.send(json.dumps({"topic": "direct-relay-test", "payload": {"hi": "there"}})) time.sleep(0.3) except Exception: pass relay_thread = threading.Thread(target=send_via_relay, daemon=True) relay_thread.start() relay_thread.join(timeout=3) nat_thread.join(timeout=3) relay.stop() assert received_via_relay, "NAT'd peer did not receive message via circuit relay" def test_relay_rpc_round_trips_http_request_to_peer(): """Relay /rpc/ sends one HTTP-shaped request to a connected peer.""" import websockets.sync.client as wsc # type: ignore[import] from meshnet_relay.server import RelayServer relay = RelayServer(host="127.0.0.1", port=0) port = relay.start() ready = threading.Event() def run_peer(): with wsc.connect(f"ws://127.0.0.1:{port}/ws") as ws: ws.send(json.dumps({ "topic": "peer-register", "version": 1, "from_peer": "rpc_peer", "msg_id": "rpc-reg-001", "payload": {"peer_id": "rpc_peer", "addr": ""}, })) ws.recv() ready.set() envelope = json.loads(ws.recv(timeout=3)) payload = envelope["payload"] ws.send(json.dumps({ "topic": "relay-http-response", "version": 1, "from_peer": "rpc_peer", "payload": { "request_id": payload["request_id"], "status": 200, "headers": {"Content-Type": "application/json"}, "body": json.dumps({"ok": True, "path": payload["path"]}), }, })) peer_thread = threading.Thread(target=run_peer, daemon=True) peer_thread.start() assert ready.wait(timeout=5) with wsc.connect(f"ws://127.0.0.1:{port}/rpc/rpc_peer") as ws: ws.send(json.dumps({ "request_id": "req-1", "method": "POST", "path": "/v1/chat/completions", "headers": {"Content-Type": "application/json"}, "body": "{}", })) response = json.loads(ws.recv(timeout=5)) relay.stop() peer_thread.join(timeout=3) assert response["status"] == 200 assert json.loads(response["body"]) == {"ok": True, "path": "/v1/chat/completions"} def test_node_relay_bridge_reconnects_after_failed_connection(monkeypatch): """Node-side relay bridge keeps retrying its outbound WebSocket connection.""" import websockets.sync.client as wsc # type: ignore[import] from meshnet_node.relay_bridge import RelayHttpBridge attempts = [] class FakeWebSocket: def __enter__(self): return self def __exit__(self, exc_type, exc, tb): return False def send(self, _message): pass def recv(self, timeout=1): raise TimeoutError def fake_connect(url, open_timeout=5): attempts.append((url, open_timeout)) if len(attempts) == 1: raise OSError("temporary relay outage") return FakeWebSocket() monkeypatch.setattr(wsc, "connect", fake_connect) bridge = RelayHttpBridge( relay_url="ws://relay.example/ws", peer_id="peer-reconnect", local_base_url="http://127.0.0.1:8001", advertised_addr="http://127.0.0.1:8001", reconnect_interval=0.01, ) try: info = bridge.start() assert info.relay_addr == "ws://relay.example/rpc/peer-reconnect" assert bridge.wait_connected(timeout=1.0) finally: bridge.stop() assert len(attempts) >= 2 # --------------------------------------------------------------------------- # Tracker gossip fields tests # --------------------------------------------------------------------------- def _start_tracker_and_register(extra_fields: dict) -> dict: """Helper: start tracker, register node with extra gossip fields, return response.""" import http.server import json as _json import urllib.request from meshnet_tracker.server import TrackerServer tracker = TrackerServer(host="127.0.0.1", port=0) port = tracker.start() url = f"http://127.0.0.1:{port}" payload = { "endpoint": f"http://127.0.0.1:8001", "shard_start": 0, "shard_end": 7, "model": "stub-model", "hardware_profile": {}, "score": 1.0, **extra_fields, } data = _json.dumps(payload).encode() req = urllib.request.Request( f"{url}/v1/nodes/register", data=data, headers={"Content-Type": "application/json"}, method="POST", ) with urllib.request.urlopen(req, timeout=5) as r: resp = _json.loads(r.read()) tracker.stop() return resp def test_tracker_derives_relay_url_from_public_self_url(): from meshnet_tracker.server import TrackerServer, derive_relay_url_from_public_tracker_url assert derive_relay_url_from_public_tracker_url("https://ai.neuron.d-popov.com") == ( "wss://ai.neuron.d-popov.com/ws" ) assert derive_relay_url_from_public_tracker_url("http://127.0.0.1:8081") is None tracker = TrackerServer(cluster_self_url="https://ai.neuron.d-popov.com") port = tracker.start() try: import json as _json import urllib.request with urllib.request.urlopen(f"http://127.0.0.1:{port}/v1/network/map", timeout=5) as resp: body = _json.loads(resp.read()) assert body["relay_url"] == "wss://ai.neuron.d-popov.com/ws" finally: tracker.stop() def test_tracker_accepts_relay_addr_in_registration(): resp = _start_tracker_and_register({ "relay_addr": "ws://relay.meshnet.ai:8765/relay/abc123", "cert_fingerprint": "sha256:deadbeef", "peer_id": "abc123def456ef01", }) assert "node_id" in resp def test_tracker_accepts_registration_without_gossip_fields(): """Existing registrations without P2P fields still work.""" resp = _start_tracker_and_register({}) assert "node_id" in resp def test_tracker_network_map_exposes_relay_and_registered_peer(): import json as _json import urllib.request from meshnet_tracker.server import TrackerServer tracker = TrackerServer(host="127.0.0.1", port=0, relay_url="wss://ai.neuron.d-popov.com/ws") port = tracker.start() url = f"http://127.0.0.1:{port}" try: req = urllib.request.Request( f"{url}/v1/nodes/register", data=_json.dumps({ "endpoint": "http://192.0.2.10:7000", "model": "stub-model", "shard_start": 0, "shard_end": 31, "relay_addr": "wss://ai.neuron.d-popov.com/rpc/peer123", "peer_id": "peer123", }).encode(), headers={"Content-Type": "application/json"}, method="POST", ) with urllib.request.urlopen(req, timeout=5): pass with urllib.request.urlopen(f"{url}/v1/network/map", timeout=5) as resp: body = _json.loads(resp.read()) finally: tracker.stop() assert body["relay_url"] == "wss://ai.neuron.d-popov.com/ws" assert body["nodes"][0]["relay_addr"] == "wss://ai.neuron.d-popov.com/rpc/peer123" assert body["nodes"][0]["peer_id"] == "peer123" # --------------------------------------------------------------------------- # mDNS (no-op without zeroconf installed) # --------------------------------------------------------------------------- def test_mdns_discovery_is_available_flag(): from meshnet_p2p.mdns import MdnsDiscovery disc = MdnsDiscovery(peer_id="test", port=8001) # is_available() should be bool regardless of zeroconf install status assert isinstance(disc.is_available(), bool) def test_mdns_start_and_stop_without_zeroconf(monkeypatch): from meshnet_p2p import mdns as mdns_mod monkeypatch.setattr(mdns_mod, "_HAS_ZEROCONF", False) from meshnet_p2p.mdns import MdnsDiscovery disc = MdnsDiscovery(peer_id="x", port=8001) disc.start() # should not raise disc.stop() # should not raise