aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/tests')
-rw-r--r--packages/meshbay-node/tests/test_apps_enabled_policy.py137
1 files changed, 137 insertions, 0 deletions
diff --git a/packages/meshbay-node/tests/test_apps_enabled_policy.py b/packages/meshbay-node/tests/test_apps_enabled_policy.py
new file mode 100644
index 0000000..671005a
--- /dev/null
+++ b/packages/meshbay-node/tests/test_apps_enabled_policy.py
@@ -0,0 +1,137 @@
+"""
+The operator decides which group "applications" (Chat, Files, ...) are shown.
+
+Same shape as `test_member_upload_policy.py`, because it is the same kind of
+setting: changed by a signed operator instruction, stored on the node rather
+than the hub, and safe for an existing group to have never heard of. The two
+things specific to this one: the whole set is signed in one message rather
+than one op per app, and an empty or unrecognised set is refused before a
+challenge is ever issued — there is no file write to refuse afterwards the
+way an unsigned upload is refused, so the check has to happen up front.
+"""
+
+from pathlib import Path
+
+import pytest
+
+from meshbay_common.adminop import OP_APPS_ENABLED
+from meshbay_node.indexer.group_index import GroupIndex
+from meshbay_node.roster import Roster
+from meshbay_node.transport.webrtc_server import WebRTCPeerSession
+from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
+
+from conftest import one_root
+
+pytestmark = pytest.mark.asyncio
+
+
+def _session(tmp_path: Path, user_id: str, *, operator: str | None = None) -> WebRTCPeerSession:
+ shared_root = tmp_path / "shared"
+ shared_root.mkdir(exist_ok=True)
+ index = GroupIndex(group_id="g" * 32, sk_node=Ed25519PrivateKey.generate())
+ ctx = {
+ "roots": one_root(shared_root),
+ "index": index,
+ "sk_node": index.sk_node,
+ "node_user_id": operator,
+ }
+ session = WebRTCPeerSession.__new__(WebRTCPeerSession)
+ session._ctx = ctx
+ session._group_id = None
+ session._user_id = user_id
+ session._pk_user = ""
+ session.sent = []
+ session._send = session.sent.append
+ session._audit = lambda *a, **k: None
+ return session
+
+
+# ── Refused before a challenge is even issued ───────────────────────────────
+
+async def test_an_empty_set_is_refused(tmp_path):
+ """Never let the operator lock a group down to nothing — no round trip
+ to the operator's browser needed to learn that."""
+ session = _session(tmp_path, "op", operator="op")
+ session._has_admin_authority = lambda: True
+ issued = []
+ session._issue_admin_challenge = lambda op, subject: issued.append((op, subject))
+
+ session._do_apps_enabled({"apps": []})
+
+ assert not issued
+ assert [m for m in session.sent if m.get("type") == "error"]
+
+
+async def test_an_unknown_app_is_refused(tmp_path):
+ session = _session(tmp_path, "op", operator="op")
+ session._has_admin_authority = lambda: True
+ issued = []
+ session._issue_admin_challenge = lambda op, subject: issued.append((op, subject))
+
+ session._do_apps_enabled({"apps": ["chat", "videos"]})
+
+ assert not issued, "videos is not registered yet — accepting it would " \
+ "silently store a setting no client can act on"
+ assert [m for m in session.sent if m.get("type") == "error"]
+
+
+async def test_a_request_with_nobody_to_authorize_it_is_refused(tmp_path):
+ session = _session(tmp_path, "member-1", operator="the-operator")
+ session._has_admin_authority = lambda: False
+
+ session._do_apps_enabled({"apps": ["chat"]})
+
+ assert [m for m in session.sent if m.get("type") == "error"]
+
+
+# ── Who may change it ───────────────────────────────────────────────────────
+
+async def test_changing_it_needs_a_signature(tmp_path):
+ """The request only ever produces a challenge. Nothing is applied until a
+ signature over the transcript verifies — the same path as member_upload."""
+ session = _session(tmp_path, "op", operator="op")
+ session._has_admin_authority = lambda: True
+ issued = []
+ session._issue_admin_challenge = lambda op, subject: issued.append((op, subject))
+
+ session._do_apps_enabled({"apps": ["files"]})
+
+ assert issued == [(OP_APPS_ENABLED, "files")]
+
+
+async def test_the_subject_is_the_sorted_set_so_both_sides_build_the_same_transcript(tmp_path):
+ """The operator's browser and the node must independently arrive at the
+ same subject string to sign/verify — order in the request must not
+ matter."""
+ session = _session(tmp_path, "op", operator="op")
+ session._has_admin_authority = lambda: True
+ issued = []
+ session._issue_admin_challenge = lambda op, subject: issued.append((op, subject))
+
+ session._do_apps_enabled({"apps": ["files", "chat"]})
+
+ assert issued == [(OP_APPS_ENABLED, "chat,files")]
+
+
+# ── Where it is stored ──────────────────────────────────────────────────────
+
+async def test_the_setting_lives_on_the_node_and_survives_a_restart(tmp_path):
+ roster = Roster(db_path=tmp_path / "roster.db")
+ await roster.open()
+ try:
+ assert sorted(await roster.enabled_apps("g1")) == ["chat", "files"], (
+ "absent must mean every registered app, or an upgrade hides one "
+ "for every existing group")
+ await roster.set_enabled_apps("g1", ["chat"], set_by="op")
+ assert await roster.enabled_apps("g1") == ["chat"]
+ finally:
+ await roster.close()
+
+ reopened = Roster(db_path=tmp_path / "roster.db")
+ await reopened.open()
+ try:
+ assert await reopened.enabled_apps("g1") == ["chat"]
+ assert sorted(await reopened.enabled_apps("g2")) == ["chat", "files"], (
+ "one group's setting must not answer for another")
+ finally:
+ await reopened.close()