summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-cli/tests/test_listing.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-14 22:20:15 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-14 22:20:15 +0200
commit422647e7898b8df67cd11f8fb060aec23dec84aa (patch)
tree7ecca0e8df1bc8a7d26cf4b471bd44906dd9cfd1 /packages/meshbay-cli/tests/test_listing.py
parent28c88908b88c58cba4de08710f6bb268ede02d57 (diff)
downloadmeshbay-422647e7898b8df67cd11f8fb060aec23dec84aa.tar.gz
feat(cli): meshbay-cli, a command-line client for members
Signs in to the hub, lists the account's groups and their folders, and downloads and uploads files directly with each group's node, end-to-end encrypted. With no argument it opens a two-pane interactive mode (Textual). Session and device keys are kept encrypted under the system's key storage, or a 0600 key file where there is none; node keys are pinned at first contact; a new machine links as a device. Packaged on its own on Linux (deb and rpm, on meshbay-common's venv, with a man page generated from the parser) and frozen next to meshbay-node in the full Windows installer, not the light one, with a self-test the build runs. client/keys.py ports keyderive.js and is LGPL like it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-cli/tests/test_listing.py')
-rw-r--r--packages/meshbay-cli/tests/test_listing.py73
1 files changed, 73 insertions, 0 deletions
diff --git a/packages/meshbay-cli/tests/test_listing.py b/packages/meshbay-cli/tests/test_listing.py
new file mode 100644
index 0000000..407074e
--- /dev/null
+++ b/packages/meshbay-cli/tests/test_listing.py
@@ -0,0 +1,73 @@
+"""The index as the Files tab draws it, and names printed safely."""
+
+import pytest
+from meshbay_cli.client.tree import Tree
+from meshbay_cli.errors import NOT_FOUND, CliError
+from meshbay_cli.output import human_size, safe
+
+INDEX = {
+ "roots": [{"name": "share", "available": True, "writable": True},
+ {"name": "usb", "available": False, "writable": False}],
+ "dirs": ["share", "share/2024", "share/empty", "usb", "usb/old"],
+ "entries": [
+ {"id": "a" * 64, "name": "notes.txt", "path": "share", "size": 6, "type": "document"},
+ {"id": "b" * 64, "name": "x.flac", "path": "share/2024", "size": 9, "type": "audio"},
+ # A folder the node's walk did not list still exists through its file.
+ {"id": "c" * 64, "name": "y.jpg", "path": "share/2024/late", "size": 1,
+ "type": "image"},
+ {"id": "d" * 64, "name": "z.txt", "path": "usb/old", "size": 1, "type": "document"},
+ ],
+}
+
+
+def _names(items):
+ return sorted(i.name for i in items)
+
+
+def test_the_top_is_the_roots():
+ tree = Tree.from_index(INDEX)
+ top = tree.children("")
+ assert _names(top) == ["share", "usb"]
+ assert {i.name: i.available for i in top} == {"share": True, "usb": False}
+
+
+def test_a_folder_holds_its_folders_and_files():
+ tree = Tree.from_index(INDEX)
+ assert _names(tree.children("share")) == ["2024", "empty", "notes.txt"]
+ assert _names(tree.children("share/2024")) == ["late", "x.flac"]
+
+
+def test_an_unavailable_root_shows_nothing_unless_asked():
+ tree = Tree.from_index(INDEX)
+ assert tree.children("usb") == []
+ assert _names(tree.children("usb/old", include_unavailable=True)) == ["z.txt"]
+
+
+def test_walk_visits_every_folder():
+ tree = Tree.from_index(INDEX)
+ assert [p for p, _ in tree.walk("share")] == [
+ "share", "share/2024", "share/2024/late", "share/empty"]
+
+
+def test_a_missing_folder_is_not_found():
+ with pytest.raises(CliError) as e:
+ Tree.from_index(INDEX).children("share/nope")
+ assert e.value.code == NOT_FOUND
+
+
+def test_a_file_is_found_by_its_path():
+ tree = Tree.from_index(INDEX)
+ assert tree.file("share/2024/x.flac")["id"] == "b" * 64
+ assert tree.file("share/2024/nope") is None
+
+
+def test_control_characters_and_bidi_overrides_are_escaped():
+ assert safe("evil\x1b[31mred") == "evil\\x1b[31mred"
+ assert safe("a\nb\x7fc\x9b") == "a\\x0ab\\x7fc\\x9b"
+ assert safe("photo‮gpj.exe") == "photo\\u202egpj.exe"
+ assert safe("été 🎵 l'ouest") == "été 🎵 l'ouest"
+
+
+def test_sizes():
+ assert human_size(6) == "6 B"
+ assert human_size(2_500_000) == "2.4 MB"