From 754387590fa1754436b4648f969915888c6f6c9e Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Fri, 2 Oct 2026 10:51:19 +0200 Subject: refactor(mnp): remove ten operator messages no client sent node_status, node_settings_set, roster_read, denylist_read, denylist_clear, node_reload and the signed gek_rotate, member_unpin, transfer_limits, group_detach leave MNP 6.0; the Node page and the CLI do this work over loopback. Their ops keep their tests, moved to the ops level. Co-Authored-By: Claude Opus 5.5 --- packages/meshbay-node/tests/test_node_status.py | 236 +++--------------------- 1 file changed, 29 insertions(+), 207 deletions(-) (limited to 'packages/meshbay-node/tests/test_node_status.py') diff --git a/packages/meshbay-node/tests/test_node_status.py b/packages/meshbay-node/tests/test_node_status.py index 257c60c..075d11f 100644 --- a/packages/meshbay-node/tests/test_node_status.py +++ b/packages/meshbay-node/tests/test_node_status.py @@ -1,25 +1,19 @@ """ -node_status and root_remove over MNP. +The node management panel's server-side behaviour. -These test the D5 node management panel's server-side behaviour: the admin -identity check on node_status, the list_groups operation, and the root -removal flow through the MNP handlers. Adding a root is not an MNP message -(MNP 6.0); `ops.add_root` is still tested here, as the loopback and CLI use it. +The `_is_node_admin` hint, root removal over MNP, and the operations the Node +page and the CLI reach over loopback: listing groups, adding and updating +roots, the roster, the denylist and unpinning. Their own MNP messages +(`node_status`, `root_add`, `roster_read`, …) are gone since MNP 6.0. """ -import base64 from dataclasses import asdict from pathlib import Path import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey -from meshbay_common.adminop import ( - OP_MEMBER_UNPIN, - admin_transcript, -) from meshbay_common.crypto import pk_to_b64 from meshbay_common.join import ROLE_OPERATOR -from meshbay_common.protocol import MNP from meshbay_node import ops from meshbay_node.indexer.group_index import GroupIndex from meshbay_node.roots import RootSet @@ -134,19 +128,6 @@ async def _session( return session -async def _sign_and_exec(session, op: str, subject: str, sk, exec_fn, - group_id: str = GROUP): - challenge = _last(session) - assert challenge["type"] == "admin_challenge", challenge - transcript = admin_transcript( - op=op, node_pk_b64=session._node_pk_b64(), group_id=group_id, - subject=subject, nonce=base64.b64decode(challenge["nonce"]), - ts=challenge["ts"]) - pending = session._admin_ops.get(challenge["op_id"]) or { - "op": op, "subject": subject} - await exec_fn(pending, transcript, sk.sign(transcript)) - - # ── _is_node_admin ────────────────────────────────────────────────────────── async def test_is_node_admin_matches_user_id(tmp_path, roster): @@ -168,62 +149,6 @@ async def test_is_node_admin_rejects_missing_node_user_id(tmp_path, roster): assert not session._is_node_admin() -# ── node_status ───────────────────────────────────────────────────────────── - -async def test_node_status_returns_groups_for_admin(tmp_path, roster): - session = await _session(tmp_path, roster, operator=True, - node_user_id="grenet") - session._spawn(session._do_node_status({})) - await _drain(session) - msg = _last(session) - assert msg["type"] == MNP.NODE_STATUS_ACK - assert len(msg["groups"]) == 1 - assert msg["groups"][0]["id"] == GROUP - - -async def test_node_status_refused_for_non_admin(tmp_path, roster): - session = await _session(tmp_path, roster, operator=False, - node_user_id="grenet") - session._spawn(session._do_node_status({})) - await _drain(session) - msg = _last(session) - assert msg["type"] == "error" - assert "operator" in msg["detail"].lower() - - -async def test_node_status_refused_when_user_is_operator_but_ids_mismatch( - tmp_path, roster, -): - """A paired operator who is not the node owner cannot see node_status.""" - session = await _session(tmp_path, roster, operator=True, - node_user_id="someone-else") - session._spawn(session._do_node_status({})) - await _drain(session) - msg = _last(session) - assert msg["type"] == "error" - - -async def test_node_status_catches_send_failure(tmp_path, roster): - """If _send itself throws (e.g. msgpack encoding fails), the error must - not silently vanish — it used to, because _send was outside the try block.""" - session = await _session(tmp_path, roster, operator=True, - node_user_id="grenet") - sent = [] - call_count = [0] - - def _exploding_send(msg): - call_count[0] += 1 - if msg.get("type") == "node_status_ack": - raise TypeError("msgpack cannot encode this") - sent.append(msg) - - session._send = _exploding_send - session._spawn(session._do_node_status({})) - await _drain(session) - # The try/except around _send should catch the error and send an error reply - assert any(m.get("type") == "error" for m in sent) - - # ── ops.list_groups ───────────────────────────────────────────────────────── async def test_list_groups_returns_group_metadata(tmp_path): @@ -504,52 +429,32 @@ async def test_root_remove_refuses_missing_fields(tmp_path, roster): assert "Missing" in _last(session)["detail"] -# ── roster_read MNP handler ────────────────────────────────────────────── - -async def test_roster_read_returns_members_for_admin(tmp_path, roster): - session = await _session(tmp_path, roster, operator=True, - node_user_id="grenet") - session._spawn(session._do_roster_read({"group_id": GROUP})) - await _drain(session) - msg = _last(session) - assert msg["type"] == "roster_read_ack" - assert "members" in msg - assert "identities" in msg - - -async def test_roster_read_refused_for_non_admin(tmp_path, roster): - session = await _session(tmp_path, roster, operator=False, - node_user_id="grenet") - session._spawn(session._do_roster_read({"group_id": GROUP})) - await _drain(session) - msg = _last(session) - assert msg["type"] == "error" - +# ── The roster, the denylist and unpinning (loopback, CLI) ────────────────── +# +# Their MNP messages are gone (MNP 6.0, no client sent them); the operations are +# what the Node page and the CLI call. -async def test_roster_read_filters_ghost_members(tmp_path, roster): +async def test_read_roster_filters_ghost_members(tmp_path, roster): """Members whose identity was deleted (revoked then unpinned) are filtered out by read_roster — the LEFT JOIN returns them with pk_ed25519 = NULL but they should never reach the UI.""" session = await _session(tmp_path, roster, operator=True, node_user_id="grenet") - sk2, pk2 = _keypair() + _, pk2 = _keypair() await roster.pin_identity("ghost", "ghost", pk2, pk2, "code") await roster.set_member(GROUP, "ghost", "member", "revoked", "local-cli") await roster._db.execute("DELETE FROM identities WHERE user_id = 'ghost'") await roster._db.commit() # Also add a real member so the roster isn't empty - sk3, pk3 = _keypair() + _, pk3 = _keypair() await roster.pin_identity("real", "real", pk3, pk3, "code") await roster.set_member(GROUP, "real", "member", "active", "local-cli") - session._spawn(session._do_roster_read({"group_id": GROUP})) - await _drain(session) - msg = _last(session) - assert msg["type"] == "roster_read_ack" - ghost = [m for m in msg["members"] if m["user_id"] == "ghost"] - assert len(ghost) == 0, "ghost members must be filtered out" - real = [m for m in msg["members"] if m["user_id"] == "real"] - assert len(real) == 1 + result = await ops.read_roster(session.state, GROUP) + assert "identities" in result + assert not [m for m in result["members"] if m["user_id"] == "ghost"], ( + "ghost members must be filtered out") + assert len([m for m in result["members"] if m["user_id"] == "real"]) == 1 async def test_unpin_fails_for_ghost_member(tmp_path, roster): @@ -557,118 +462,35 @@ async def test_unpin_fails_for_ghost_member(tmp_path, roster): session = await _session(tmp_path, roster, operator=True, node_user_id="grenet") await roster.set_member(GROUP, "ghost", "member", "revoked", "local-cli") - # ghost has no identity row - session._do_member_unpin({"user_id": "ghost"}) - challenge = _last(session) - assert challenge["type"] == "admin_challenge" + with pytest.raises(ops.OpError, match="No such pinned identity"): + await ops.unpin_member(session.state, "ghost") - await _sign_and_exec(session, OP_MEMBER_UNPIN, "ghost", - session.sk_op, session._admin_exec_member_unpin) - msg = _last(session) - assert msg["type"] == "error" - assert "No such pinned identity" in msg["detail"] - - -async def test_unpin_succeeds_for_real_identity(tmp_path, roster): - """Full unpin flow: challenge → sign → exec → identity deleted.""" - session = await _session(tmp_path, roster, operator=True, - node_user_id="grenet") - sk2, pk2 = _keypair() - await roster.pin_identity("target", "target", pk2, pk2, "code") - await roster.set_member(GROUP, "target", "member", "active", "local-cli") - - session._do_member_unpin({"user_id": "target"}) - challenge = _last(session) - assert challenge["type"] == "admin_challenge" - - await _sign_and_exec(session, OP_MEMBER_UNPIN, "target", - session.sk_op, session._admin_exec_member_unpin) - msg = _last(session) - assert msg["type"] == "member_unpin_ack" - assert msg["user_id"] == "target" - - idents = await roster.list_identities() - assert not any(i["user_id"] == "target" for i in idents) - -# ── denylist_read MNP handler ──────────────────────────────────────────── - -async def test_denylist_read_returns_entries_for_admin(tmp_path, roster): +async def test_read_denylist_lists_entries(tmp_path, roster): session = await _session(tmp_path, roster, operator=True, node_user_id="grenet") session.denylist.deny_user("bad-user") - session._spawn(session._do_denylist_read({})) - await _drain(session) - msg = _last(session) - assert msg["type"] == "denylist_read_ack" - assert msg["count"] == 1 - assert "bad-user" in msg["users"] - - -async def test_denylist_read_refused_for_non_admin(tmp_path, roster): - session = await _session(tmp_path, roster, operator=False, - node_user_id="grenet") - session._spawn(session._do_denylist_read({})) - await _drain(session) - msg = _last(session) - assert msg["type"] == "error" - + result = await ops.read_denylist(session.state) + assert result["count"] == 1 + assert "bad-user" in result["users"] -# ── denylist_clear MNP handler ─────────────────────────────────────────── -async def test_denylist_clear_removes_entry_for_admin(tmp_path, roster): +async def test_clear_denylist_removes_one_entry(tmp_path, roster): session = await _session(tmp_path, roster, operator=True, node_user_id="grenet") session.denylist.deny_user("bad-user") session.denylist.deny_user("other-user") - session._spawn(session._do_denylist_clear({"subject": "bad-user"})) - await _drain(session) - msg = _last(session) - assert msg["type"] == "denylist_clear_ack" - assert msg["removed"] == 1 + result = await ops.clear_denylist(session.state, subject="bad-user") + assert result["removed"] == 1 assert "bad-user" not in session.denylist.entries()["users"] assert "other-user" in session.denylist.entries()["users"] -async def test_denylist_clear_all_for_admin(tmp_path, roster): +async def test_clear_denylist_all(tmp_path, roster): session = await _session(tmp_path, roster, operator=True, node_user_id="grenet") session.denylist.deny_user("a") session.denylist.deny_user("b") session.denylist.deny_jti("j") - session._spawn(session._do_denylist_clear({"subject": ""})) - await _drain(session) - msg = _last(session) - assert msg["type"] == "denylist_clear_ack" - assert msg["removed"] == 3 - - -async def test_denylist_clear_refused_for_non_admin(tmp_path, roster): - session = await _session(tmp_path, roster, operator=False, - node_user_id="grenet") - session._spawn(session._do_denylist_clear({"subject": "bad-user"})) - await _drain(session) - msg = _last(session) - assert msg["type"] == "error" - - -# ── node_reload MNP handler ───────────────────────────────────────────── - -async def test_node_reload_runs_for_admin(tmp_path, roster): - session = await _session(tmp_path, roster, operator=True, - node_user_id="grenet") - session._spawn(session._do_node_reload({})) - await _drain(session) - msg = _last(session) - assert msg["type"] == "node_reload_ack" - assert msg["status"] == "reloaded" - assert len(session.reload_called) == 1 - - -async def test_node_reload_refused_for_non_admin(tmp_path, roster): - session = await _session(tmp_path, roster, operator=False, - node_user_id="grenet") - session._spawn(session._do_node_reload({})) - await _drain(session) - msg = _last(session) - assert msg["type"] == "error" + result = await ops.clear_denylist(session.state, subject="") + assert result["removed"] == 3 -- cgit v1.2.3