""" 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"]