diff options
Diffstat (limited to 'packages/meshbay-node/tests/test_audio_root_policy.py')
| -rw-r--r-- | packages/meshbay-node/tests/test_audio_root_policy.py | 260 |
1 files changed, 0 insertions, 260 deletions
diff --git a/packages/meshbay-node/tests/test_audio_root_policy.py b/packages/meshbay-node/tests/test_audio_root_policy.py deleted file mode 100644 index e2e9254..0000000 --- a/packages/meshbay-node/tests/test_audio_root_policy.py +++ /dev/null @@ -1,260 +0,0 @@ -""" -Which folder (possibly a subfolder of a shared root) is the Music app's -entry point for a group. Same shape as test_video_root_policy.py — a -signed operator instruction, per-group, stored via roster.py's -group_settings table, added later once a real messy library showed -musicbay.md's original "no root, whole shared tree" call was wrong. -""" - -import base64 -from pathlib import Path - -import pytest -from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - -from meshbay_common.adminop import OP_AUDIO_ROOT, admin_transcript -from meshbay_common.crypto import pk_to_b64 -from meshbay_common.join import ROLE_OPERATOR -from meshbay_node.indexer.group_index import GroupIndex -from meshbay_node.roster import Roster, open_roster -from meshbay_node.transport.webrtc_server import WebRTCPeerSession - -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) - (shared_root / "Music").mkdir() - (shared_root / "Podcasts").mkdir() - 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_missing_path_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_audio_root({}) - - assert not issued - assert [m for m in session.sent if m.get("type") == "error"] - - -async def test_a_nonexistent_folder_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_audio_root({"path": "shared/Nonexistent"}) - - assert not issued, "a mistyped path must be refused before a signature round trip" - assert [m for m in session.sent if m.get("type") == "error"] - - -async def test_path_traversal_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_audio_root({"path": "../../etc"}) - - assert not issued - 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_audio_root({"path": "shared/Music"}) - - assert [m for m in session.sent if m.get("type") == "error"] - - -# ── Accepted cases ─────────────────────────────────────────────────────────── - -async def test_an_empty_path_is_always_accepted(tmp_path): - """Empty means 'unset' — Music shows nothing yet, always valid to clear.""" - 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_audio_root({"path": ""}) - - assert issued == [(OP_AUDIO_ROOT, "")] - - -async def test_a_real_subfolder_is_accepted_and_signed(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_audio_root({"path": "shared/Music"}) - - assert issued == [(OP_AUDIO_ROOT, "shared/Music")] - - -# ── 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 await roster.app_directories("g1", "music") == [], ( - "absent must mean unset") - await roster.set_app_directories("g1", "music", ["shared/Music"], - set_by="op") - assert await roster.app_directories("g1", "music") == ["shared/Music"] - finally: - await roster.close() - - reopened = Roster(db_path=tmp_path / "roster.db") - await reopened.open() - try: - assert await reopened.app_directories("g1", "music") == ["shared/Music"] - assert await reopened.app_directories("g2", "music") == [], ( - "one group's setting must not answer for another") - finally: - await reopened.close() - - -# ── End to end through the real signed-op path ────────────────────────────── -# -# Everything above either calls ops.set_audio_root directly or mocks out -# _issue_admin_challenge — neither one ever exercises real signature -# verification (_verify_admin_sig, _do_admin_response) or the shared -# groups_ctx/roster wiring _run_op depends on. Found live: a save that -# looked like it worked (the Music tab showed content right afterward) did -# not survive a reload — worth ruling out a break somewhere in that real -# path specifically, not just in the pure-Python setter. Session shape -# mirrors test_admin_ops_mnp.py's _session helper. - -GROUP = "g" * 32 - - -def _keypair(): - sk = Ed25519PrivateKey.generate() - return sk, pk_to_b64(sk.public_key()) - - -async def _full_session(tmp_path: Path, roster) -> tuple[WebRTCPeerSession, Ed25519PrivateKey]: - shared = tmp_path / "shared" - (shared / "Music").mkdir(parents=True) - index = GroupIndex(group_id=GROUP, sk_node=Ed25519PrivateKey.generate()) - roots = one_root(shared) - - sk_op, pk_op = _keypair() - await roster.pin_identity("grenet", "grenet", pk_op, pk_op, "code") - await roster.set_member("", "grenet", ROLE_OPERATOR, "active", "local-cli") - - group_ctx = {"gek": b"\x01" * 32, "roots": roots, "index": index, - "join_policy": "invite", "audio_root": ""} - state = { - "groups_ctx": {GROUP: group_ctx}, - "roster": roster, - "node_user_id": "node-user", - } - - session = WebRTCPeerSession.__new__(WebRTCPeerSession) - session._ctx = { - "roots": roots, "index": index, "sk_node": index.sk_node, - "roster": roster, "groups": {GROUP: group_ctx}, - "has_admin_authority": True, - "daemon_state": state, - } - session._group_id = GROUP - session._user_id = "grenet" - session._pk_user = "" - session._admin_ops = {} - session.sent = [] - session._send = session.sent.append - session._audit = lambda *a, **k: None - session.spawned = [] - session._spawn = session.spawned.append - # A real session registers itself here on handshake completion - # (`self._peer_registry()[self._user_id] = self`) — without it, the - # broadcast loop in _admin_exec_audio_root (and every other admin op) - # has nobody to send the final ack to, including the requester itself. - group_ctx["_peers"] = {"grenet": session} - session._peer_registry = lambda: group_ctx["_peers"] - return session, sk_op - - -async def _drain(session): - for coro in session.spawned: - await coro - session.spawned.clear() - - -async def test_a_real_signed_save_persists_and_survives_a_fresh_roster_read(tmp_path): - """ - The exact question a "worked, then reverted after reload" report raises: - does the value set through the real challenge/response path actually - land in the database, in a form any later connection — this one, or a - freshly-opened Roster after a restart — reads back correctly? - """ - roster = await open_roster(tmp_path) - try: - session, sk_op = await _full_session(tmp_path, roster) - - session._do_audio_root({"path": "shared/Music"}) - challenge = session.sent[-1] - assert challenge["type"] == "admin_challenge", challenge - - transcript = admin_transcript( - op=OP_AUDIO_ROOT, node_pk_b64=session._node_pk_b64(), group_id=GROUP, - subject="shared/Music", nonce=base64.b64decode(challenge["nonce"]), - ts=challenge["ts"]) - session._do_admin_response({ - "op_id": challenge["op_id"], - "signature": base64.b64encode(sk_op.sign(transcript)).decode(), - }) - await _drain(session) - - ack = session.sent[-1] - assert ack["type"] == "audio_root_ack", ack - assert ack["path"] == "shared/Music" - - assert session._ctx["groups"][GROUP]["audio_root"] == "shared/Music", ( - "the live in-memory context must reflect the new root immediately") - assert await roster.app_directories(GROUP, "music") == ["shared/Music"], ( - "the same Roster instance must read back what it just wrote") - finally: - await roster.close() - - # A fresh connection (or a restarted daemon) never touches the Roster - # instance above at all — it opens its own. This is the check that - # actually answers "does it survive a reload". - reopened = await open_roster(tmp_path) - try: - assert await reopened.app_directories(GROUP, "music") == ["shared/Music"], ( - "a freshly-opened Roster against the same db file must see the " - "committed value — anything else means the write was never " - "durable in the first place") - finally: - await reopened.close() |