aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-20 22:21:24 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-20 22:21:24 +0200
commitae52b69b14a6997d01aad66f49fbea7b5ca2dfe6 (patch)
tree26f850a88565846a139868a4b85c715734751a41 /packages/meshbay-node/tests
parentc8af746c846b5dbc792f7e4f0d806647d513cc5c (diff)
parent2e9490ca27047ae03e495d397abbe1aec1b2273a (diff)
downloadmeshbay-ae52b69b14a6997d01aad66f49fbea7b5ca2dfe6.tar.gz
Merge feat/unified-group-management: wizard, public groups, activity sidebar
Diffstat (limited to 'packages/meshbay-node/tests')
-rw-r--r--packages/meshbay-node/tests/test_ops.py46
-rw-r--r--packages/meshbay-node/tests/test_roots.py67
-rw-r--r--packages/meshbay-node/tests/test_roster_pairing.py4
-rw-r--r--packages/meshbay-node/tests/test_webrtc_transport.py4
4 files changed, 119 insertions, 2 deletions
diff --git a/packages/meshbay-node/tests/test_ops.py b/packages/meshbay-node/tests/test_ops.py
index f7fd259..d2ccc0d 100644
--- a/packages/meshbay-node/tests/test_ops.py
+++ b/packages/meshbay-node/tests/test_ops.py
@@ -63,7 +63,9 @@ def test_the_http_adapter_adds_no_logic():
source = inspect.getsource(ui)
# Every endpoint that performs an operation routes through _op(...).
for endpoint in ("operator_pair", "create_invite", "revoke_member",
- "unpin_member", "init_gek", "attach_group", "delete_file"):
+ "unpin_member", "init_gek", "attach_group", "delete_file",
+ "add_root", "remove_root", "set_member_upload",
+ "reload_config"):
start = source.index(f"async def {endpoint}(")
body = source[start:start + 700]
assert "_op(" in body.split("\n\n")[0] + body, (
@@ -177,3 +179,45 @@ async def test_an_unhosted_group_offers_what_it_does_host(tmp_path):
await ops.delete_file(state, "z" * 32, "a" * 64)
assert exc.value.status == 404
assert exc.value.extra.get("available")
+
+
+# ── Upload policy (set_member_upload) ───────────────────────────────────────
+
+async def test_set_member_upload_toggles_and_persists(tmp_path):
+ from meshbay_node.roster import Roster
+ state = _state(tmp_path)
+ roster = Roster(db_path=tmp_path / "roster.db")
+ await roster.open()
+ state["roster"] = roster
+ state["node_user_id"] = "operator"
+
+ out = await ops.set_member_upload(state, "g" * 32, True)
+
+ assert out["allowed"] is True
+ assert state["groups_ctx"]["g" * 32]["member_upload"] is True
+
+ out2 = await ops.set_member_upload(state, "g" * 32, False)
+
+ assert out2["allowed"] is False
+ assert state["groups_ctx"]["g" * 32]["member_upload"] is False
+
+
+# ── Reload ──────────────────────────────────────────────────────────────────
+
+async def test_reload_config_calls_reload_fn(tmp_path):
+ state = _state(tmp_path)
+ called = []
+ async def fake_reload():
+ called.append(True)
+ state["reload_fn"] = fake_reload
+
+ out = await ops.reload_config(state)
+
+ assert out["status"] == "reloaded"
+ assert called
+
+
+async def test_reload_config_without_fn_is_refused(tmp_path):
+ state = _state(tmp_path)
+ with pytest.raises(ops.OpError, match="Reload not available"):
+ await ops.reload_config(state)
diff --git a/packages/meshbay-node/tests/test_roots.py b/packages/meshbay-node/tests/test_roots.py
index ea4ba6a..fc5bd64 100644
--- a/packages/meshbay-node/tests/test_roots.py
+++ b/packages/meshbay-node/tests/test_roots.py
@@ -11,7 +11,10 @@ from pathlib import Path
import pytest
-from meshbay_node.roots import Root, RootError, RootSet, entry_abs_path
+from meshbay_node.roots import (
+ Root, RootError, RootSet, entry_abs_path,
+ SAFE_UPLOAD_NAME, safe_subdir, _free_name,
+)
from meshbay_common.protocol import IndexEntry
@@ -240,3 +243,65 @@ def test_describe_reports_what_a_member_needs(tmp_path):
# Deliberately no paths: a member is told what exists and whether it is
# readable, not where on the operator's disk it lives.
assert not any("path" in d for d in described)
+
+
+# ── SAFE_UPLOAD_NAME ────────────────────────────────────────────────────────
+
+def test_safe_name_accepts_unicode_letters():
+ assert SAFE_UPLOAD_NAME.match("rapport (1).pdf")
+ assert SAFE_UPLOAD_NAME.match("hello.txt")
+
+
+def test_safe_name_rejects_dotfiles():
+ assert not SAFE_UPLOAD_NAME.match(".hidden")
+ assert not SAFE_UPLOAD_NAME.match("..secret")
+
+
+def test_safe_name_rejects_trailing_dot_or_space():
+ assert not SAFE_UPLOAD_NAME.match("file.")
+ assert not SAFE_UPLOAD_NAME.match("file ")
+
+
+# ── _free_name ──────────────────────────────────────────────────────────────
+
+def test_free_name_returns_original_when_not_taken(tmp_path):
+ assert _free_name(tmp_path, "photo.jpg") == "photo.jpg"
+
+
+def test_free_name_appends_counter_on_collision(tmp_path):
+ (tmp_path / "photo.jpg").write_text("x")
+ assert _free_name(tmp_path, "photo.jpg") == "photo (2).jpg"
+
+
+def test_free_name_increments_past_multiple_collisions(tmp_path):
+ (tmp_path / "photo.jpg").write_text("x")
+ (tmp_path / "photo (2).jpg").write_text("x")
+ assert _free_name(tmp_path, "photo.jpg") == "photo (3).jpg"
+
+
+# ── safe_subdir ─────────────────────────────────────────────────────────────
+
+def test_safe_subdir_resolves_valid_path(tmp_path):
+ (tmp_path / "Films").mkdir()
+ (tmp_path / "Films" / "2024").mkdir()
+ roots = RootSet.build([_spec(tmp_path / "Films")])
+ assert safe_subdir(roots, "Films/2024") == (tmp_path / "Films" / "2024").resolve()
+
+
+def test_safe_subdir_refuses_traversal(tmp_path):
+ (tmp_path / "Films").mkdir()
+ roots = RootSet.build([_spec(tmp_path / "Films")])
+ assert safe_subdir(roots, "Films/../../etc") is None
+
+
+def test_safe_subdir_refuses_empty_virtual_root(tmp_path):
+ (tmp_path / "Films").mkdir()
+ roots = RootSet.build([_spec(tmp_path / "Films")])
+ assert safe_subdir(roots, "") is None
+
+
+def test_safe_subdir_refuses_unavailable_root(tmp_path):
+ (tmp_path / "Films").mkdir()
+ roots = RootSet.build([_spec(tmp_path / "Films")])
+ roots.roots[0].available = False
+ assert safe_subdir(roots, "Films/2024") is None
diff --git a/packages/meshbay-node/tests/test_roster_pairing.py b/packages/meshbay-node/tests/test_roster_pairing.py
index 61d53ac..7eb436c 100644
--- a/packages/meshbay-node/tests/test_roster_pairing.py
+++ b/packages/meshbay-node/tests/test_roster_pairing.py
@@ -91,6 +91,10 @@ def _session(tmp_path: Path, roster, user_id: str = "grenet",
session.sent = []
session._send = session.sent.append
session._audit = lambda *a, **k: None
+ session._ctx["daemon_state"] = {
+ "roster": roster,
+ "groups_ctx": session._ctx.get("groups", {}),
+ }
return session
diff --git a/packages/meshbay-node/tests/test_webrtc_transport.py b/packages/meshbay-node/tests/test_webrtc_transport.py
index 5727ef9..ec6e987 100644
--- a/packages/meshbay-node/tests/test_webrtc_transport.py
+++ b/packages/meshbay-node/tests/test_webrtc_transport.py
@@ -1134,6 +1134,10 @@ async def test_invite_then_join_delivers_the_gek(sk_node, sk_hub, gek, shared_di
transport._ctx["groups"] = {
TEST_GROUP: {"gek": gek, "roots": shared_dir, "index": indexer.index},
}
+ transport._ctx["daemon_state"] = {
+ "roster": roster,
+ "groups_ctx": transport._ctx["groups"],
+ }
# A paired operator, as `meshbay-node operator pair` would have left it.
sk_admin = Ed25519PrivateKey.generate()