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_incoming_membership.py76
-rw-r--r--packages/meshbay-hub/tests/test_invite_email_choice.py5
-rw-r--r--packages/meshbay-hub/tests/test_mnp_token.py21
3 files changed, 100 insertions, 2 deletions
diff --git a/packages/meshbay-hub/tests/test_incoming_membership.py b/packages/meshbay-hub/tests/test_incoming_membership.py
new file mode 100644
index 0000000..708e327
--- /dev/null
+++ b/packages/meshbay-hub/tests/test_incoming_membership.py
@@ -0,0 +1,76 @@
+"""The NAT-punch signal is not a liveness oracle, and only a member reaches it.
+
+`POST /v1/nodes/{id}/incoming` used to check nothing but the caller's own
+address, then reveal whether the node was connected (404 vs 504) and, with QUIC
+on, make it punch. Any authenticated account could poll it for a node's liveness
+and make a stranger's node emit a UDP probe. It now requires a shared active
+group with the node first — the same gate the offer relay uses — checked before
+anything depends on the node's connection state, so a non-member gets one uniform
+403 whether the node is connected or not.
+"""
+
+import pytest
+from test_availability_between_members import (
+ _add_member,
+ _announce_node,
+ _make_group,
+ _make_user,
+)
+
+
+def _incoming(client, node_id, user, peer_ip="1.2.3.4", peer_port=5000):
+ return client.post(f"/v1/nodes/{node_id}/incoming",
+ json={"peer_ip": peer_ip, "peer_port": peer_port},
+ headers={"Authorization": f"Bearer {user['token']}"})
+
+
+@pytest.mark.asyncio
+async def test_a_non_member_is_refused_whether_the_node_is_connected_or_not(client):
+ from meshbay_hub.api import revocation as rev
+
+ owner = await _make_user(client, "inc_owner")
+ stranger = await _make_user(client, "inc_stranger")
+ group_id = await _make_group(client, owner, "inc-group")
+ node_id = await _announce_node(client, owner)
+
+ # Node NOT in the connected registry — the stranger gets the membership 403
+ # (not the connection 404), so the answer says nothing about whether the node
+ # is up. The detail is what distinguishes it from the peer_ip refusal that a
+ # request without the gate would give.
+ r_off = await _incoming(client, node_id, stranger)
+ assert r_off.status_code == 403
+ assert "member" in r_off.json()["detail"]
+
+ # Node connected and serving the group — the stranger, not a member, still gets
+ # the membership 403, and never reaches the punch or the connection-state answer.
+ rev._connected_nodes[node_id] = object()
+ rev._node_groups[node_id] = [group_id]
+ try:
+ r_on = await _incoming(client, node_id, stranger)
+ assert r_on.status_code == 403
+ assert "member" in r_on.json()["detail"]
+ finally:
+ rev._connected_nodes.pop(node_id, None)
+ rev._node_groups.pop(node_id, None)
+
+
+@pytest.mark.asyncio
+async def test_a_member_passes_the_membership_gate(client):
+ """A member is not turned away by the gate. (It then reaches the connection
+ check — 404 here, since no real node socket is registered — never 403.)"""
+ from meshbay_hub.api import revocation as rev
+
+ owner = await _make_user(client, "inc2_owner")
+ member = await _make_user(client, "inc2_member")
+ group_id = await _make_group(client, owner, "inc2-group")
+ await _add_member(client, owner, group_id, member)
+ node_id = await _announce_node(client, owner)
+
+ rev._node_groups[node_id] = [group_id] # registered/hosted, but no live socket
+ try:
+ r = await _incoming(client, node_id, member)
+ # Past the membership gate: the refusal, if any, is about the connection
+ # or the peer address, never "not a member of any group on this node".
+ assert r.status_code != 403 or "member" not in r.json().get("detail", "")
+ finally:
+ rev._node_groups.pop(node_id, None)
diff --git a/packages/meshbay-hub/tests/test_invite_email_choice.py b/packages/meshbay-hub/tests/test_invite_email_choice.py
index e04c31f..de9410d 100644
--- a/packages/meshbay-hub/tests/test_invite_email_choice.py
+++ b/packages/meshbay-hub/tests/test_invite_email_choice.py
@@ -3,8 +3,9 @@ Whether the hub mails an invitation is the inviter's choice, and it is remembere
Mailing it hands the hub the code — `invite-notify` writes it into the message —
which is exactly what §3.4 says the code is for not doing. So the Members tab
-offers it as a box, checked by default, and an unchecked box must mean the hub
-is never asked. The choice lives in an account preference; a key the hub does
+offers it as a box, unchecked by default (mailing the code is opt-in), and an
+unchecked box must mean the hub is never asked. The choice lives in an account
+preference, remembered once set; a key the hub does
not list is refused, and the box would snap back on every click with nothing on
screen to say why.
"""
diff --git a/packages/meshbay-hub/tests/test_mnp_token.py b/packages/meshbay-hub/tests/test_mnp_token.py
index 7b483ff..3aa3115 100644
--- a/packages/meshbay-hub/tests/test_mnp_token.py
+++ b/packages/meshbay-hub/tests/test_mnp_token.py
@@ -69,3 +69,24 @@ async def test_a_session_token_is_refused_by_a_node_but_the_mnp_token_is_not(cli
# The MNP token authorises the member to the node.
peer = authorize_token(mnp, pk, group_id=gid)
assert peer.group_id == gid
+
+
+@pytest.mark.asyncio
+async def test_the_mnp_token_is_bound_to_the_node_it_names(client):
+ """E10: a token minted for node A is refused by node B, so an operator who
+ captures a member's token cannot replay it to another of the member's nodes."""
+ from meshbay_hub.auth import hub_public_key_pem
+
+ tok = await _session_token(client, "mnp_bind_test")
+ H = {"Authorization": f"Bearer {tok}"}
+ gid = (await client.post("/v1/groups", headers=H, json={
+ "name": "g", "visibility": "private", "join_policy": "invite"})).json()["group_id"]
+ # A token bound to node A's key.
+ mnp = (await client.post("/v1/nodes/mnp-token", headers=H,
+ json={"node_pk": "node-A-pk"})).json()["mnp_token"]
+ pk = hub_public_key_pem()
+ # Node B refuses it; node A accepts it.
+ with pytest.raises(HandshakeError, match="this node"):
+ authorize_token(mnp, pk, group_id=gid, node_pk_b64="node-B-pk")
+ peer = authorize_token(mnp, pk, group_id=gid, node_pk_b64="node-A-pk")
+ assert peer.group_id == gid