summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_roster_pairing.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/tests/test_roster_pairing.py')
-rw-r--r--packages/meshbay-node/tests/test_roster_pairing.py48
1 files changed, 31 insertions, 17 deletions
diff --git a/packages/meshbay-node/tests/test_roster_pairing.py b/packages/meshbay-node/tests/test_roster_pairing.py
index 11704ce..d13225d 100644
--- a/packages/meshbay-node/tests/test_roster_pairing.py
+++ b/packages/meshbay-node/tests/test_roster_pairing.py
@@ -445,6 +445,22 @@ async def test_revoked_member_stops_receiving_the_key(tmp_path, roster):
assert _last(session).get("gek") is False
+# ── What a first-time joiner can know ─────────────────────────────────────────
+
+def test_challenge_carries_node_pk_in_source():
+ """
+ Belt and braces for the above: the field must be in the message the node
+ builds, whatever the surrounding handshake does.
+ """
+ source = (Path(__file__).parent.parent
+ / "src" / "meshbay_node" / "transport" / "webrtc_server.py").read_text()
+ challenge = source[source.find("MNP.HANDSHAKE_CHALLENGE,"):]
+ challenge = challenge[:challenge.find("})")]
+ assert "node_pk" in challenge, (
+ "the challenge must announce the node key — a first-time joiner cannot "
+ "learn it any other way, and join_request signs it")
+
+
# ── Code lifetimes ────────────────────────────────────────────────────────────
async def test_invitations_outlive_pairing_codes(roster):
@@ -635,35 +651,33 @@ def _run_cli(monkeypatch, tmp_path, argv, responses):
def test_cli_member_commands_reach_the_right_endpoints(monkeypatch, tmp_path, capsys):
- roster_reply = {"identities": [{"user_id": "u-bob", "username": "bob",
- "pk_ed25519": "K", "pinned_at": "now",
- "pinned_via": "code"}],
- "members": [{"group_id": GROUP, "user_id": "u-bob",
- "role": "member", "status": "active"}],
- "invites": []}
+ resolved = {"user_id": "u-bob", "source": "roster"}
calls = _run_cli(monkeypatch, tmp_path, ["member", "revoke", "bob"],
- {"/api/roster": roster_reply,
+ {"/api/resolve": resolved,
"revoke": {"status": "revoked", "reminder": "gek-init"}})
assert ("POST", f"/api/members/u-bob/revoke?group_id={GROUP}") in calls
# The operator is told the revocation does not take back the key they hold.
assert "rotate" in capsys.readouterr().out.lower()
calls = _run_cli(monkeypatch, tmp_path, ["member", "unpin", "bob"],
- {"/api/roster": roster_reply, "unpin": {"status": "unpinned"}})
+ {"/api/resolve": resolved, "unpin": {"status": "unpinned"}})
assert ("POST", "/api/members/u-bob/unpin") in calls
-def test_cli_refuses_to_act_on_someone_it_does_not_know(monkeypatch, tmp_path, capsys):
- """A typo must not silently do nothing — or worse, act on the wrong person."""
- calls = _run_cli(monkeypatch, tmp_path, ["member", "revoke", "nobody"],
- {"/api/roster": {"identities": [], "members": [],
- "invites": []}})
- assert ("exit", 1) in calls
- assert not any(method == "POST" for method, _ in calls), (
- "the CLI acted on the server despite not knowing who was meant")
- assert "not pinned" in capsys.readouterr().out
+def test_cli_resolves_a_name_before_acting(monkeypatch, tmp_path):
+ """
+ The name has to be turned into an account first, and the node's own roster is
+ asked before the hub. A JWT carries no username, so an identity pinned without
+ an invitation has none — the hub fallback is what keeps it manageable.
+ """
+ calls = _run_cli(monkeypatch, tmp_path, ["member", "revoke", "bob"],
+ {"/api/resolve": {"user_id": "u-bob", "source": "hub"},
+ "revoke": {"status": "revoked", "reminder": "gek-init"}})
+ assert ("GET", "/api/resolve?username=bob") == calls[0], (
+ "the CLI must resolve the name before acting on anyone")
+ assert ("POST", f"/api/members/u-bob/revoke?group_id={GROUP}") in calls
def test_daemon_does_not_auto_pin_keystore_key():
"""