diff options
Diffstat (limited to 'packages/meshbay-node/tests')
| -rw-r--r-- | packages/meshbay-node/tests/test_index_delta_carries_roots.py | 131 | ||||
| -rw-r--r-- | packages/meshbay-node/tests/test_root_paths_are_operator_only.py | 1 |
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 |