summaryrefslogtreecommitdiffstats
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_index_delta_carries_roots.py131
-rw-r--r--packages/meshbay-node/tests/test_root_paths_are_operator_only.py1
2 files changed, 131 insertions, 1 deletions
diff --git a/packages/meshbay-node/tests/test_index_delta_carries_roots.py b/packages/meshbay-node/tests/test_index_delta_carries_roots.py
new file mode 100644
index 0000000..227f2d0
--- /dev/null
+++ b/packages/meshbay-node/tests/test_index_delta_carries_roots.py
@@ -0,0 +1,131 @@
+"""
+The message that says something changed has to be able to say what.
+
+A group's directory table travelled on `index_sync` alone — a *full* index,
+which the node only ever sends on request. Every ongoing change went out as an
+`index_delta`, which carried files and nothing else. So a root added, removed,
+ejected or plugged by the operator reached every other client's screen only
+when somebody happened to reload the page.
+
+It was hidden by the acks: `root_add_ack` and friends broadcast the new table
+to whoever was connected, so the common cases looked fine. What that could not
+cover is a client connecting mid-change, one whose ack was lost, or — the one
+that surfaced it — the operator's own client, where the ack landed and was then
+overwritten by an index fetched before the node had rebuilt anything.
+
+Additive on the wire (MNP 1.1): a 1.0 client sees a field it does not read.
+"""
+
+from pathlib import Path
+
+from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
+
+from meshbay_common.crypto import generate_gek
+from meshbay_common.groupbox import PURPOSE_INDEX, unseal
+from meshbay_node.indexer.group_index import GroupIndex
+from meshbay_node.roots import RootSet
+from meshbay_node.transport.wire import index_delta_message, index_sync_message
+
+
+GROUP = "g" * 32
+
+
+def _roots(tmp_path: Path) -> RootSet:
+ for name in ("Films", "Albums"):
+ (tmp_path / name).mkdir()
+ return RootSet.build([
+ {"path": str(tmp_path / "Films"), "writable": True},
+ {"path": str(tmp_path / "Albums"), "removable": True},
+ ])
+
+
+def _index() -> GroupIndex:
+ return GroupIndex(group_id=GROUP, sk_node=Ed25519PrivateKey.generate(),
+ gek=generate_gek())
+
+
+def _payload(msg: dict, index: GroupIndex) -> dict:
+ """What a member actually reads, through the seal rather than around it."""
+ return unseal(index.gek, PURPOSE_INDEX, msg["type"], GROUP, msg)
+
+
+class _Delta:
+ base_version = 1
+ version = 2
+ additions: list = []
+ deletions: list = []
+ updates: list = []
+
+
+def test_a_delta_carries_the_directory_table(tmp_path):
+ index = _index()
+ msg = index_delta_message(index, _Delta(), _roots(tmp_path))
+ payload = _payload(msg, index)
+
+ assert [r["name"] for r in payload["roots"]] == ["Films", "Albums"]
+ assert payload["roots"][0]["writable"] is True
+ assert payload["roots"][1]["removable"] is True
+
+
+def test_the_table_is_sealed_with_the_rest(tmp_path):
+ """
+ It is group content, not routing. Only `type`, `v` and `group_id` stay in
+ clear, because a receiver has to route and authenticate before it would
+ trust a decryption.
+ """
+ index = _index()
+ msg = index_delta_message(index, _Delta(), _roots(tmp_path))
+ assert set(msg) - {"type", "v", "group_id"}, "nothing was sealed"
+ assert "roots" not in msg, "the directory table is outside the envelope"
+
+
+def test_a_delta_still_works_without_a_table(tmp_path):
+ """
+ The argument is optional, so an older caller — or a path that has no root
+ set to hand — produces a message a client reads exactly as before.
+ """
+ index = _index()
+ payload = _payload(index_delta_message(index, _Delta()), index)
+ assert "roots" not in payload
+ assert payload["version"] == 2
+
+
+def test_the_table_says_the_same_thing_on_both_messages(tmp_path):
+ """
+ Two encodings of one idea is the drift `wire.py` exists to prevent — it
+ already happened twice, for `index_sync` and for `file_chunk`.
+ """
+ index = _index()
+ roots = _roots(tmp_path)
+ delta = _payload(index_delta_message(index, _Delta(), roots), index)
+ sync = _payload(index_sync_message(index, roots), index)
+ assert delta["roots"] == sync["roots"]
+
+
+def test_the_table_never_carries_a_path(tmp_path):
+ """
+ This message goes to every member. Where a directory lives on the
+ operator's disk is theirs — see test_root_paths_are_operator_only.py.
+ """
+ index = _index()
+ payload = _payload(index_delta_message(index, _Delta(), _roots(tmp_path)),
+ index)
+ assert not any("path" in r for r in payload["roots"])
+
+
+def test_an_ejected_root_is_visible_in_the_delta(tmp_path):
+ """
+ The case this was written for. An eject changes no file — the entries
+ freeze — so the delta it produces is empty of additions, deletions and
+ updates. Without the table it says literally nothing, which is how a
+ library disappearing from under the group's feet went unannounced.
+ """
+ index = _index()
+ roots = _roots(tmp_path)
+ roots.roots[1].ejected = True
+ roots.roots[1].available = False
+
+ payload = _payload(index_delta_message(index, _Delta(), roots), index)
+ assert payload["additions"] == [] and payload["deletions"] == []
+ assert payload["roots"][1]["ejected"] is True
+ assert payload["roots"][1]["available"] is False
diff --git a/packages/meshbay-node/tests/test_root_paths_are_operator_only.py b/packages/meshbay-node/tests/test_root_paths_are_operator_only.py
index 6ba2e08..080d4be 100644
--- a/packages/meshbay-node/tests/test_root_paths_are_operator_only.py
+++ b/packages/meshbay-node/tests/test_root_paths_are_operator_only.py
@@ -21,7 +21,6 @@ import inspect
import re
from pathlib import Path
-import pytest
from meshbay_node import daemon as daemon_mod
from meshbay_node import ops