summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-15 12:32:24 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-15 12:32:24 +0200
commite0a1036e0fee15b3c6c83f3e935d50f472a2e776 (patch)
tree75d28932b3010ebe3781b4f36dffed19dae74297 /packages/meshbay-node/tests
parenta0be6bfcac07a256949082e935e1be06753ab872 (diff)
downloadmeshbay-e0a1036e0fee15b3c6c83f3e935d50f472a2e776.tar.gz
fix(node): an operator who was a member first holds the authority at once
A member pinned on the node who then redeems the operator pairing code was written down as the operator but the node kept its "nobody can authorize" flag until it restarted: every admin operation — removing a folder, inviting, the apps' settings — was refused meanwhile. Only a key paired fresh set it. The join result now names `operator` for an account holding the node-wide operator row, whatever its row in the group says. The page and the CLI read that role to know what to offer, and refused before asking deletions the node would have accepted. Nothing is granted by it: every admin operation is still checked against the operators' keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/tests')
-rw-r--r--packages/meshbay-node/tests/test_roster_pairing.py44
1 files changed, 44 insertions, 0 deletions
diff --git a/packages/meshbay-node/tests/test_roster_pairing.py b/packages/meshbay-node/tests/test_roster_pairing.py
index 5687215..b2799e9 100644
--- a/packages/meshbay-node/tests/test_roster_pairing.py
+++ b/packages/meshbay-node/tests/test_roster_pairing.py
@@ -586,6 +586,50 @@ async def test_known_device_already_a_member_still_needs_no_code(tmp_path, roste
assert unwrap_gek_aes(reply, *_x_raw(sk_x, pk_x_b64)) == gek_b
+@pytest.mark.parametrize("operator_row, announced", [
+ (None, ROLE_MEMBER),
+ ("active", ROLE_OPERATOR),
+ ("revoked", ROLE_MEMBER),
+])
+async def test_an_operator_who_was_a_member_first_is_told_so(tmp_path, roster,
+ operator_row, announced):
+ """A member of B who then paired as the node's operator holds both rows; the
+ join result names the authority, so a client offers what the node accepts."""
+ GROUP_B = "b" * 32
+ sk_ed, pk_ed_b64, pk_x_b64 = _keypair()
+ await roster.pin_identity("cbesson", "cbesson", pk_ed_b64, pk_x_b64, "code")
+ await roster.set_member(GROUP_B, "cbesson", ROLE_MEMBER, "active", "cbesson")
+ if operator_row:
+ await roster.set_member("", "cbesson", ROLE_OPERATOR, operator_row, "cli")
+
+ session = _session(tmp_path, roster, user_id="cbesson", group_id=GROUP_B,
+ gek=generate_gek(), join_policy="invite")
+ await session._do_join_request(
+ _join_msg(session, sk_ed, pk_ed_b64, pk_x_b64, user_id="cbesson", group_id=""))
+ reply = _last(session)
+ assert reply["ok"] is True and reply["role"] == announced
+
+
+async def test_a_member_who_pairs_as_operator_has_the_authority_at_once(tmp_path, roster):
+ """The pairing code redeemed by a key already pinned here as a member: the
+ node must count an operator from then on, not from its next restart."""
+ GROUP_B = "b" * 32
+ sk_ed, pk_ed_b64, pk_x_b64 = _keypair()
+ await roster.pin_identity("cbesson", "cbesson", pk_ed_b64, pk_x_b64, "code")
+ await roster.set_member(GROUP_B, "cbesson", ROLE_MEMBER, "active", "cbesson")
+ code = await roster.create_invite("", "cbesson", ROLE_OPERATOR, "cli")
+
+ session = _session(tmp_path, roster, user_id="cbesson", group_id=GROUP_B,
+ gek=generate_gek(), join_policy="invite")
+ session._ctx["has_admin_authority"] = False # as the node started
+ await session._do_join_request(
+ _join_msg(session, sk_ed, pk_ed_b64, pk_x_b64, code=code, user_id="cbesson",
+ group_id=GROUP_B))
+ reply = _last(session)
+ assert reply["ok"] is True and reply["role"] == ROLE_OPERATOR
+ assert session._has_admin_authority()
+
+
async def test_unknown_group_is_invite_only(tmp_path, roster):
"""
Fail closed: a group whose policy the node cannot read is treated as