summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_ops.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/tests/test_ops.py')
-rw-r--r--packages/meshbay-node/tests/test_ops.py100
1 files changed, 90 insertions, 10 deletions
diff --git a/packages/meshbay-node/tests/test_ops.py b/packages/meshbay-node/tests/test_ops.py
index 92e32bf..b3f0378 100644
--- a/packages/meshbay-node/tests/test_ops.py
+++ b/packages/meshbay-node/tests/test_ops.py
@@ -12,11 +12,13 @@ call them.
import asyncio
import inspect
from pathlib import Path
+from types import SimpleNamespace
import pytest
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from meshbay_node import ops
from meshbay_node.indexer.group_index import GroupIndex
+from meshbay_node.roots import RootSet
from meshbay_node.transport.quic_server import Denylist
from conftest import one_root
@@ -64,8 +66,8 @@ def test_the_http_adapter_adds_no_logic():
# 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",
- "add_root", "remove_root", "set_member_upload",
- "reload_config"):
+ "add_root", "remove_root", "update_root",
+ "eject_root", "plug_root", "reload_config"):
start = source.index(f"async def {endpoint}(")
body = source[start:start + 700]
assert "_op(" in body.split("\n\n")[0] + body, (
@@ -181,25 +183,103 @@ async def test_an_unhosted_group_offers_what_it_does_host(tmp_path):
assert exc.value.extra.get("available")
-# ── Upload policy (set_member_upload) ───────────────────────────────────────
+# ── Upload policy (per-root writable) ───────────────────────────────────────
-async def test_set_member_upload_toggles_and_persists(tmp_path):
+async def test_the_group_wide_upload_switch_is_gone(tmp_path):
+ """
+ `set_member_upload` was the whole of the old policy, and it is deliberately
+ not here any more — RO/RW on the root replaced it. A wrapper kept "for
+ compatibility" would be a second way to decide who writes to the operator's
+ disk, and two answers to that question is how C1 and C6 both happened.
+ """
+ assert not hasattr(ops, "set_member_upload")
+ from meshbay_node.roster import Roster
+ assert not hasattr(Roster, "set_member_upload")
+ assert not hasattr(Roster, "member_upload_allowed")
+
+
+async def test_eject_and_plug_persist_through_the_roster(tmp_path):
+ """
+ The state has to outlive the process: an operator ejects a drive, unplugs
+ it, and restarts the node — and the rescan that follows must not read the
+ empty mount point as an erased library.
+ """
from meshbay_node.roster import Roster
state = _state(tmp_path)
+ usb = tmp_path / "USB"
+ usb.mkdir()
+ state["groups_ctx"]["g" * 32]["roots"] = RootSet.build(
+ [{"path": str(usb), "removable": True, "writable": True}])
+ state["config"] = SimpleNamespace(
+ groups=[SimpleNamespace(id="g" * 32, roots=[])])
roster = Roster(db_path=tmp_path / "roster.db")
await roster.open()
state["roster"] = roster
state["node_user_id"] = "operator"
+ try:
+ out = await ops.eject_root(state, "g" * 32, "USB")
+ assert out["status"] == "ejected"
+ assert await roster.ejected_roots("g" * 32) == {"usb"}
+ assert out["roots"][0]["ejected"] is True
+ assert out["roots"][0]["available"] is False
+
+ out = await ops.plug_root(state, "g" * 32, "USB")
+ assert out["status"] == "plugged"
+ assert await roster.ejected_roots("g" * 32) == set()
+ finally:
+ await roster.close()
- 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
+async def test_a_root_that_is_not_removable_cannot_be_ejected(tmp_path):
+ """
+ Eject means "I am about to unplug this". On a directory that is not on a
+ removable device it would hide a library with no way for the safety net to
+ notice anything happened, and nothing to plug back in.
+ """
+ from meshbay_node.roster import Roster
+ state = _state(tmp_path)
+ fixed = tmp_path / "Fixed"
+ fixed.mkdir()
+ state["groups_ctx"]["g" * 32]["roots"] = RootSet.build(
+ [{"path": str(fixed), "writable": True}])
+ state["config"] = SimpleNamespace(
+ groups=[SimpleNamespace(id="g" * 32, roots=[])])
+ roster = Roster(db_path=tmp_path / "roster.db")
+ await roster.open()
+ state["roster"] = roster
+ try:
+ with pytest.raises(ops.OpError, match="removable"):
+ await ops.eject_root(state, "g" * 32, "Fixed")
+ finally:
+ await roster.close()
- 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
+async def test_plugging_a_drive_that_is_not_there_is_refused(tmp_path):
+ """
+ Clearing the flag while the device is still absent would restart the
+ watchdog on a missing path and hand the next reconcile an empty directory —
+ the deletion storm the eject was there to prevent, produced by the recovery.
+ """
+ from meshbay_node.roster import Roster
+ state = _state(tmp_path)
+ usb = tmp_path / "USB"
+ usb.mkdir()
+ roots = RootSet.build([{"path": str(usb), "removable": True}])
+ roots.roots[0].ejected = True
+ roots.roots[0].available = False
+ state["groups_ctx"]["g" * 32]["roots"] = roots
+ state["config"] = SimpleNamespace(
+ groups=[SimpleNamespace(id="g" * 32, roots=[])])
+ roster = Roster(db_path=tmp_path / "roster.db")
+ await roster.open()
+ state["roster"] = roster
+ usb.rmdir()
+ try:
+ with pytest.raises(ops.OpError, match="device connected"):
+ await ops.plug_root(state, "g" * 32, "USB")
+ assert roots.roots[0].ejected is True
+ finally:
+ await roster.close()
# ── Reload ──────────────────────────────────────────────────────────────────