diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-10 22:12:59 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-10 22:12:59 +0200 |
| commit | 60c4570e72e36c2a9720593c8baec74ee2ab52d6 (patch) | |
| tree | e833e222a6a95e64e5b24a0813882636044198c2 /packages/meshbay-hub/tests/test_hub_api.py | |
| parent | 1a53eb4cc404ec94658fde0ae04cfe2ccf1810dc (diff) | |
| download | meshbay-60c4570e72e36c2a9720593c8baec74ee2ab52d6.tar.gz | |
feat: Phase 9.1–9.5 — WebRTC DataChannel transport for browser P2P
Browser clients can now connect P2P to nodes behind residential NAT via
WebRTC DataChannel with ICE/STUN. Validated on SFR Port-Restricted Cone
NAT + 4G CGNAT across three scenarios (WiFi LAN, 4G IPv6, 4G IPv4 STUN).
No TURN relay needed. Hub serves only as signaling relay (<1 KB).
New files:
- webrtc_server.py: aiortc-based WebRTC transport (node side)
- signaling.py: SDP/ICE relay endpoint (hub side)
- transport.js: browser WebRTC client with msgpack framing
- webrtc-test.html: spike test page for browser→NAT→node validation
- test_webrtc_transport.py: 4 tests (handshake, file transfer, auth, guard)
- meshbay-draft-v4.md: architecture spec updated for web client
Modified:
- hub_client.py: WebRTC offer handling via hub WebSocket
- revocation.py: node_id from WS auth + webrtc_answer routing
- pyproject.toml: aiortc>=1.9 dependency
123 tests passing (117 existing + 6 new).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/tests/test_hub_api.py')
| -rw-r--r-- | packages/meshbay-hub/tests/test_hub_api.py | 70 |
1 files changed, 70 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_hub_api.py b/packages/meshbay-hub/tests/test_hub_api.py index 327158f..a9c97a3 100644 --- a/packages/meshbay-hub/tests/test_hub_api.py +++ b/packages/meshbay-hub/tests/test_hub_api.py @@ -483,6 +483,76 @@ async def test_password_rehash_on_login(client, app): assert r.status_code == 200 +# ── WebRTC signaling (9.2) ────────────────────────────────────────────────── + +@pytest.mark.asyncio +async def test_webrtc_offer_no_node(client): + """WebRTC offer to a non-connected node returns 404.""" + pk_ed, pk_x, _ = _gen_user_keys() + await client.post("/v1/users/register", json={ + "username": "sig_user", "email": "sig@x.com", "password": "sigpass99", + "pk_user_ed25519": pk_ed, "pk_user_x25519": pk_x}) + r = await client.post("/v1/users/login", json={ + "username": "sig_user", "password": "sigpass99"}) + token = r.json()["access_token"] + + r = await client.post("/v1/nodes/fake-node-id/webrtc/offer", + json={"sdp": "v=0\r\n...", "ice_candidates": []}, + headers={"Authorization": f"Bearer {token}"}) + assert r.status_code == 404 + assert "not connected" in r.json()["detail"].lower() + + +@pytest.mark.asyncio +async def test_webrtc_signaling_roundtrip(client, app): + """WebRTC signaling: offer relayed to node via WS, answer returned to browser.""" + import asyncio + import json + + pk_ed, pk_x, _ = _gen_user_keys() + await client.post("/v1/users/register", json={ + "username": "sig_user2", "email": "sig2@x.com", "password": "sigpass99", + "pk_user_ed25519": pk_ed, "pk_user_x25519": pk_x}) + r = await client.post("/v1/users/login", json={ + "username": "sig_user2", "password": "sigpass99"}) + token = r.json()["access_token"] + + from meshbay_hub.api.revocation import _connected_nodes + from meshbay_hub.api.signaling import handle_webrtc_answer + + class FakeWS: + def __init__(self): + self.sent = [] + + async def send_text(self, text): + self.sent.append(json.loads(text)) + msg = self.sent[-1] + if msg.get("type") == "webrtc_offer": + await asyncio.sleep(0.01) + handle_webrtc_answer({ + "type": "webrtc_answer", + "peer_id": msg["peer_id"], + "sdp": "v=0\r\nanswer-sdp", + "ice_candidates": [{"candidate": "test"}], + }) + + fake_ws = FakeWS() + node_id = "test-node-sig" + _connected_nodes[node_id] = fake_ws + + try: + r = await client.post(f"/v1/nodes/{node_id}/webrtc/offer", + json={"sdp": "v=0\r\noffer-sdp", "ice_candidates": []}, + headers={"Authorization": f"Bearer {token}"}) + assert r.status_code == 200 + data = r.json() + assert "answer-sdp" in data["sdp"] + assert len(data["ice_candidates"]) == 1 + assert "peer_id" in data + finally: + _connected_nodes.pop(node_id, None) + + # ── IP log cleanup (8.9) ──────────────────────────────────────────────────── @pytest.mark.asyncio |