summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_node_status.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-02 10:51:19 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-02 10:51:19 +0200
commit754387590fa1754436b4648f969915888c6f6c9e (patch)
tree53c833eeb4042e8ce5f96b882b23471daa9d9394 /packages/meshbay-node/tests/test_node_status.py
parentc928547ca6e402bfe5e06bb59d55ac91e6822cd0 (diff)
downloadmeshbay-754387590fa1754436b4648f969915888c6f6c9e.tar.gz
refactor(mnp): remove ten operator messages no client sent0.17
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 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/tests/test_node_status.py')
-rw-r--r--packages/meshbay-node/tests/test_node_status.py236
1 files changed, 29 insertions, 207 deletions
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