aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_files_drop_upload.py
blob: fc15c47771b577b34f32b64e0ebd5efb3220a60e (plain) (blame)
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"]