aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_app_directories.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/tests/test_app_directories.py')
-rw-r--r--packages/meshbay-node/tests/test_app_directories.py104
1 files changed, 92 insertions, 12 deletions
diff --git a/packages/meshbay-node/tests/test_app_directories.py b/packages/meshbay-node/tests/test_app_directories.py
index 3ede1b6..69b3be5 100644
--- a/packages/meshbay-node/tests/test_app_directories.py
+++ b/packages/meshbay-node/tests/test_app_directories.py
@@ -26,10 +26,14 @@ from types import SimpleNamespace
import pytest
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
+from meshbay_common.adminop import OP_APP_DIRECTORIES
from meshbay_node import ops
from meshbay_node.indexer.group_index import GroupIndex
from meshbay_node.roots import RootSet
from meshbay_node.roster import Roster
+from meshbay_node.transport.webrtc_server import WebRTCPeerSession
+
+from conftest import one_root
pytestmark = pytest.mark.asyncio
@@ -157,11 +161,11 @@ async def test_a_directory_on_an_unplugged_drive_can_still_be_configured(tmp_pat
# ── The derived scalar ───────────────────────────────────────────────────────
-async def test_the_legacy_scalar_follows_the_list_in_the_live_context(tmp_path):
+async def test_the_list_is_what_the_live_context_carries(tmp_path):
"""
- `video_root` rides on the handshake ack for MNP 1.0 clients and is read
- from the group context. Left behind by a save, it would disagree with the
- list until the next restart.
+ One name per app in the group context, and it is the list. A second name
+ for the same idea is a second thing a save has to update, and the one it
+ forgets disagrees until the next restart.
"""
state, roster = await _state(tmp_path)
ctx = state["groups_ctx"][GROUP]
@@ -169,26 +173,33 @@ async def test_the_legacy_scalar_follows_the_list_in_the_live_context(tmp_path):
await ops.set_app_directories(state, GROUP, "video",
["Media/Films", "Media/Albums"])
assert ctx["video_directories"] == ["Media/Albums", "Media/Films"]
- assert ctx["video_root"] == "Media/Albums", (
- "the scalar must be the first of the list, not a stale value")
+ assert "video_root" not in ctx, (
+ "a scalar nothing reads is a scalar that can go stale unnoticed")
await ops.set_app_directories(state, GROUP, "video", [])
- assert ctx["video_root"] == ""
+ assert ctx["video_directories"] == []
finally:
await roster.close()
-async def test_the_photo_alias_stays_a_list_and_chat_stays_a_string(tmp_path):
- """The alias table has to carry the shape, not just the name."""
+async def test_chat_is_also_published_under_the_name_the_ack_uses(tmp_path):
+ """
+ Chat has one destination and the handshake ack publishes it as
+ `chat_directory`, which the paperclip reads. That second name is derived
+ from the list on every save, never stored beside it.
+ """
state, roster = await _state(tmp_path)
ctx = state["groups_ctx"][GROUP]
try:
- await ops.set_app_directories(state, GROUP, "photo",
- ["Media/Films", "Media/Albums"])
- assert ctx["photo_roots"] == ["Media/Albums", "Media/Films"]
await ops.set_app_directory(state, GROUP, "chat", "Media",
require_writable=True)
assert ctx["chat_directory"] == "Media"
+ assert ctx["chat_directories"] == ["Media"]
+
+ await ops.set_app_directories(state, GROUP, "photo",
+ ["Media/Films", "Media/Albums"])
+ assert ctx["photo_directories"] == ["Media/Albums", "Media/Films"]
+ assert "photo_roots" not in ctx
finally:
await roster.close()
@@ -290,3 +301,72 @@ async def test_setting_link_previews_updates_the_live_context(tmp_path):
assert state["groups_ctx"][GROUP]["chat_link_preview"] is False
finally:
await roster.close()
+
+
+# ── The handler, before any signature ────────────────────────────────────────
+#
+# `_do_app_directories` refuses three things up front and asks for a signature
+# for everything else. The paths are not among the three: `_validate_app_dirs`
+# checks those after the signature, deliberately (a settings change is not a
+# capability), and `test_app_directories_signed.py` drives that whole path.
+
+def _handler_session(tmp_path, *, authorized: bool) -> WebRTCPeerSession:
+ shared = tmp_path / "shared"
+ (shared / "Films").mkdir(parents=True, exist_ok=True)
+ index = GroupIndex(group_id=GROUP, sk_node=Ed25519PrivateKey.generate())
+ session = WebRTCPeerSession.__new__(WebRTCPeerSession)
+ session._ctx = {"roots": one_root(shared), "index": index,
+ "sk_node": index.sk_node}
+ session._group_id = GROUP
+ session._user_id = "op"
+ session.sent = []
+ session._send = session.sent.append
+ session._audit = lambda *a, **k: None
+ session._has_admin_authority = lambda: authorized
+ session.issued = []
+ session._issue_admin_challenge = lambda op, subject: session.issued.append(
+ (op, subject))
+ return session
+
+
+async def test_an_app_this_node_does_not_know_is_refused(tmp_path):
+ """A client-supplied key is otherwise a way to write arbitrary rows into
+ `group_settings`."""
+ session = _handler_session(tmp_path, authorized=True)
+ session._do_app_directories({"app": "../etc", "directories": ["shared"]})
+ assert session.issued == []
+ assert [m for m in session.sent if m.get("type") == "error"]
+
+
+@pytest.mark.parametrize("dirs", [None, "shared/Films", [1], {"a": 1}])
+async def test_directories_that_are_not_a_list_of_strings_are_refused(tmp_path, dirs):
+ session = _handler_session(tmp_path, authorized=True)
+ session._do_app_directories({"app": "video", "directories": dirs})
+ assert session.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):
+ """No operator key on this node's roster means no signature can ever
+ arrive, so asking for one would be a dialog that cannot be completed."""
+ session = _handler_session(tmp_path, authorized=False)
+ session._do_app_directories({"app": "video", "directories": ["shared/Films"]})
+ assert session.issued == []
+ assert [m for m in session.sent if m.get("type") == "error"]
+
+
+async def test_the_subject_names_the_app_and_the_cleaned_paths(tmp_path):
+ """What the operator is shown before signing has to say which application
+ is about to be pointed where — two apps' challenges are otherwise
+ indistinguishable — and it must match what the node will store."""
+ session = _handler_session(tmp_path, authorized=True)
+ session._do_app_directories(
+ {"app": "video", "directories": ["/b/", "a", "a", ""]})
+ assert session.issued == [(OP_APP_DIRECTORIES, "video:a,b")]
+
+
+async def test_an_empty_set_is_signable(tmp_path):
+ """Clearing an app's folders is an instruction like any other."""
+ session = _handler_session(tmp_path, authorized=True)
+ session._do_app_directories({"app": "video", "directories": []})
+ assert session.issued == [(OP_APP_DIRECTORIES, "video:")]