1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
|
"""
Dropping files and folders onto Files (`files-app.js`).
What a drop does is decided before anything is sent, and both decisions are
tested here by running the real functions out of the source:
* a name already in the folder refuses the whole drop — left to the node, a
colliding file is quietly stored as "x (1).jpg" and a colliding folder is
refused after the ones before it were made;
* a name the node would refuse refuses it too, which only works while the
client's copy of the node's filename rule gives the node's answers. That is
a second copy of a rule, so it is checked against the first one here.
"""
import json
import re
import shutil
import subprocess
from pathlib import Path
import pytest
STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static"
FILES_APP = STATIC / "files-app.js"
pytestmark = pytest.mark.skipif(
shutil.which("node") is None or not FILES_APP.exists(),
reason="node or the SPA sources are not available")
@pytest.fixture(scope="module")
def source():
text = FILES_APP.read_text(encoding="utf-8")
const = re.search(r"^const UPLOAD_NAME = .*;$", text, re.M)
funcs = [re.search(rf"^function {name}\(.*?^\}}", text, re.M | re.S)
for name in ("namesIn", "planDrop")]
assert const and all(funcs), "files-app.js no longer has what this test reads"
return "\n".join([const.group(0)] + [m.group(0) for m in funcs])
def _run(tmp_path, source, expr):
script = tmp_path / "case.js"
script.write_text(f"{source}\nconsole.log(JSON.stringify({expr}));", encoding="utf-8")
out = subprocess.run(["node", str(script)], capture_output=True, text=True, check=True)
return json.loads(out.stdout)
NAMES = [
"photo.jpg", "Été 2024", "東京", "track (1).flac", "[live] set", "a_b-c",
"rock & roll", "l’été", "x+y#z@w", "9lives",
".DS_Store", "_private", "-dash", " space", "trailing ", "trailing.",
"a/b", "a\\b", "semi;colon", "x" * 128, "x" * 129, "", "tab\tname",
]
def test_the_client_refuses_exactly_the_names_the_node_refuses(tmp_path, source):
roots = pytest.importorskip("meshbay_node.roots")
node = [bool(roots.SAFE_UPLOAD_NAME.match(n)) for n in NAMES]
client = _run(tmp_path, source, f"{json.dumps(NAMES)}.map((n) => UPLOAD_NAME.test(n))")
differ = [n for n, a, b in zip(NAMES, node, client) if a != b]
assert not differ, f"client and node disagree on: {differ}"
def test_names_in_a_folder_count_files_folders_and_empty_folders(tmp_path, source):
entries = [{"path": "music", "name": "a.mp3"},
{"path": "music/Album", "name": "t.flac"},
{"path": "musicals", "name": "not-here.txt"}]
got = _run(tmp_path, source,
f"namesIn({json.dumps(entries)}, "
f"['music/Empty', 'music/Album/cd1'], 'music').sort()")
assert got == ["Album", "Empty", "a.mp3"]
def _plan(tmp_path, source, items, existing):
return _run(tmp_path, source, f"planDrop({json.dumps(items)}, {json.dumps(existing)})")
def test_a_name_already_there_is_a_conflict_whatever_its_case(tmp_path, source):
items = [{"kind": "file", "path": "Photo.JPG"},
{"kind": "dir", "path": "Album"}, {"kind": "file", "path": "Album/x.jpg"},
{"kind": "file", "path": "new.txt"}]
plan = _plan(tmp_path, source, items, ["photo.jpg", "album", "other"])
assert plan["conflicts"] == ["Album", "Photo.JPG"]
def test_only_the_top_level_can_conflict(tmp_path, source):
items = [{"kind": "file", "path": "Album/photo.jpg"}]
plan = _plan(tmp_path, source, items, ["photo.jpg"])
assert plan["conflicts"] == []
def test_a_name_the_node_would_refuse_is_reported_wherever_it_is(tmp_path, source):
items = [{"kind": "dir", "path": "Album"},
{"kind": "file", "path": "Album/.DS_Store"},
{"kind": "file", "path": "Album/ok.jpg"}]
plan = _plan(tmp_path, source, items, [])
assert plan["invalid"] == [".DS_Store"]
def test_folders_are_created_parents_first_including_implied_ones(tmp_path, source):
items = [{"kind": "file", "path": "A/B/C/deep.txt"},
{"kind": "dir", "path": "A/Empty"},
{"kind": "file", "path": "top.txt"}]
plan = _plan(tmp_path, source, items, [])
assert plan["dirs"] == ["A", "A/B", "A/Empty", "A/B/C"]
assert [f["path"] for f in plan["files"]] == ["A/B/C/deep.txt", "top.txt"]
|