aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/tests')
-rw-r--r--packages/meshbay-hub/tests/test_availability_between_members.py123
-rw-r--r--packages/meshbay-hub/tests/test_hub_api.py12
2 files changed, 135 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_availability_between_members.py b/packages/meshbay-hub/tests/test_availability_between_members.py
index 282169d..bfbfcc0 100644
--- a/packages/meshbay-hub/tests/test_availability_between_members.py
+++ b/packages/meshbay-hub/tests/test_availability_between_members.py
@@ -518,3 +518,126 @@ async def test_a_list_cannot_be_asked_for_the_whole_table(client):
r = await client.get("/v1/notifications?limit=20", headers=headers)
assert r.status_code == 200, r.text
+
+
+# ── A node that hosts nothing is not a free target ──────────────────────────
+
+@pytest.mark.asyncio
+async def test_a_node_hosting_nothing_is_not_brokered_to_a_stranger(client):
+ """
+ Signaling read its membership check as `if node_group_ids:` — so when the
+ set was empty, the membership check, the group-status check and the
+ public-group gate were all skipped and the offer was relayed.
+
+ Since AV1, an empty claim is the *normal* registration of a node that hosts
+ nothing: the unconfigured node left running, which is the machine in this
+ register's founding incident. Each offer makes it allocate an
+ `RTCPeerConnection` and gather ICE, which is finding H6 restored in exactly
+ the case AV1 made common — and the stranger paying nothing for it.
+ """
+ from meshbay_hub.api import revocation as rev
+
+ owner = await _make_user(client, "av_sig_owner")
+ stranger = await _make_user(client, "av_sig_stranger")
+ node_id = await _announce_node(client, owner)
+
+ class _FakeWS:
+ def __init__(self):
+ self.sent = []
+
+ async def send_text(self, text):
+ self.sent.append(text)
+
+ ws = _FakeWS()
+ rev._connected_nodes[node_id] = ws
+ rev._node_groups[node_id] = [] # hosts nothing, as in the incident
+ try:
+ r = await client.post(f"/v1/nodes/{node_id}/webrtc/offer",
+ json={"sdp": "v=0\r\noffer", "ice_candidates": []},
+ headers={"Authorization": f"Bearer {stranger['token']}"})
+ assert r.status_code == 403, r.text
+ assert not ws.sent, (
+ "the node was made to negotiate for someone with no group on it")
+ finally:
+ rev._connected_nodes.pop(node_id, None)
+ rev._node_groups.pop(node_id, None)
+
+
+@pytest.mark.asyncio
+async def test_a_member_still_reaches_the_node_they_share_a_group_with(client):
+ """The other half, so the test above is about the claim and not about
+ refusing everyone."""
+ from meshbay_hub.api import revocation as rev
+
+ owner = await _make_user(client, "av_sig_owner2")
+ member = await _make_user(client, "av_sig_member2")
+ group_id = await _make_group(client, owner, "shared-one")
+ await _add_member(client, owner, group_id, member)
+ node_id = await _announce_node(client, owner)
+
+ answered = []
+
+ class _AnsweringWS:
+ async def send_text(self, text):
+ import json as _json
+ from meshbay_hub.api.signaling import handle_webrtc_answer
+ msg = _json.loads(text)
+ answered.append(msg)
+ handle_webrtc_answer({"peer_id": msg["peer_id"], "sdp": "v=0\r\nanswer",
+ "ice_candidates": []}, node_id)
+
+ rev._connected_nodes[node_id] = _AnsweringWS()
+ rev._node_groups[node_id] = [group_id]
+ try:
+ r = await client.post(f"/v1/nodes/{node_id}/webrtc/offer",
+ json={"sdp": "v=0\r\noffer", "ice_candidates": []},
+ headers={"Authorization": f"Bearer {member['token']}"})
+ assert r.status_code == 200, r.text
+ assert answered
+ finally:
+ rev._connected_nodes.pop(node_id, None)
+ rev._node_groups.pop(node_id, None)
+
+
+# ── A private group's hosts are not public knowledge ────────────────────────
+
+@pytest.mark.asyncio
+async def test_a_private_groups_node_list_is_for_its_members(client):
+ """
+ `GET /v1/groups/{id}/nodes` checked membership only for a public group with
+ public groups switched off. A private group answered any authenticated
+ account that knew the id — which an ex-member knows for ever — with the ids
+ and public keys of the machines hosting it.
+
+ §7.4 already states the property for the public case: a non-member is handed
+ no node to connect to. This is that sentence, for the groups the whole
+ design optimises for.
+ """
+ from meshbay_hub.api import revocation as rev
+
+ owner = await _make_user(client, "av_nodes_owner")
+ member = await _make_user(client, "av_nodes_member")
+ stranger = await _make_user(client, "av_nodes_stranger")
+ group_id = await _make_group(client, owner, "private-hosts")
+ await _add_member(client, owner, group_id, member)
+ node_id = await _announce_node(client, owner)
+
+ rev._node_groups[node_id] = [group_id]
+ rev._connected_nodes[node_id] = object()
+ try:
+ for who in (owner, member):
+ r = await client.get(
+ f"/v1/groups/{group_id}/nodes",
+ headers={"Authorization": f"Bearer {who['token']}"})
+ assert r.status_code == 200, r.text
+ assert [n["node_id"] for n in r.json()["nodes"]] == [node_id]
+
+ r = await client.get(
+ f"/v1/groups/{group_id}/nodes",
+ headers={"Authorization": f"Bearer {stranger['token']}"})
+ assert r.status_code == 403, (
+ "a stranger who knows the group id learned which machines host it: "
+ + r.text)
+ finally:
+ rev._connected_nodes.pop(node_id, None)
+ rev._node_groups.pop(node_id, None)
diff --git a/packages/meshbay-hub/tests/test_hub_api.py b/packages/meshbay-hub/tests/test_hub_api.py
index 5bb6af9..73f7fea 100644
--- a/packages/meshbay-hub/tests/test_hub_api.py
+++ b/packages/meshbay-hub/tests/test_hub_api.py
@@ -682,6 +682,17 @@ async def test_webrtc_signaling_roundtrip(client, app):
fake_ws = FakeWS(node_id)
_connected_nodes[node_id] = fake_ws
+ # The caller must share an active group with the node, which is what this
+ # test used to get away without: a node registered for no group skipped the
+ # membership check entirely, so this exercised the relay through the hole
+ # rather than through the door. Registering the group is what a real node
+ # does on its socket.
+ from meshbay_hub.api.revocation import _node_groups
+ r = await client.post("/v1/groups", json={"name": "sig-group"},
+ headers={"Authorization": f"Bearer {token}"})
+ assert r.status_code == 201, r.text
+ _node_groups[node_id] = [r.json()["group_id"]]
+
try:
r = await client.post(f"/v1/nodes/{node_id}/webrtc/offer",
json={"sdp": "v=0\r\noffer-sdp", "ice_candidates": []},
@@ -693,6 +704,7 @@ async def test_webrtc_signaling_roundtrip(client, app):
assert "peer_id" in data
finally:
_connected_nodes.pop(node_id, None)
+ _node_groups.pop(node_id, None)
# ── IP log cleanup (8.9) ────────────────────────────────────────────────────