diff options
Diffstat (limited to 'packages/meshbay-hub/tests/test_availability_between_members.py')
| -rw-r--r-- | packages/meshbay-hub/tests/test_availability_between_members.py | 123 |
1 files changed, 123 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) |