aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_zip_size_limit.py
blob: 203c10c71ed6095bb53d26baf5a2332eb843108e (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
107
"""
The arbitrary ceiling on a directory zip.

`downloadDirectory` is the one implementation behind every "download this
folder as a zip" button — Files' single folder, Files' multi-folder selection,
and the Photos album button (docs/photos.md §3) — so the limit is checked
once, there, and holds for all of them.

Two things are worth pinning. That an oversized folder is refused *before*
`_openDownloadTarget`, because a save dialog for an archive that will never be
written is worse than no dialog at all. And that a folder at exactly the limit
still goes through, since an off-by-one here silently costs a whole megabyte
of allowance and nobody would ever notice.
"""

import json
import shutil
import subprocess
from pathlib import Path

import pytest

STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static"
FILE_UTILS = STATIC / "file-utils.js"

pytestmark = pytest.mark.skipif(
    shutil.which("node") is None or not FILE_UTILS.exists(),
    reason="node or the SPA sources are not available")

MIB = 1024 * 1024


def _run(total_bytes, tmp_path):
    """
    Call downloadDirectory over one folder holding `total_bytes`, and report
    what it did: the errors it set, how many times it put a question to the
    person, and how many transfers it started.
    """
    sandbox = tmp_path / "static"
    sandbox.mkdir()
    for src in STATIC.glob("*.js"):
        (sandbox / src.name).write_text(src.read_text(encoding="utf-8"),
                                        encoding="utf-8")
    (tmp_path / "package.json").write_text('{"type":"module"}')

    script = tmp_path / "case.mjs"
    script.write_text(f"""
const store = new Map();
globalThis.localStorage = {{
  getItem: k => (store.has(k) ? store.get(k) : null),
  setItem: (k, v) => store.set(k, String(v)),
  removeItem: k => store.delete(k),
}};
// Node 22 defines `navigator` itself, so it is left alone; `window` is what
// platform.js reaches for to decide it is not running in the desktop app.
globalThis.window = globalThis;
const out = {{ errors: [], started: 0, asked: 0 }};
// Reached only once the size check has passed: with no File System Access API
// under Node, downloadDirectory falls through to its build-in-memory path and
// asks first. Answering yes is what lets the at-the-limit case get as far as
// starting a transfer, and `asked` is how the refusal proves it never did.
globalThis.confirm = () => {{ out.asked += 1; return true; }};

const M = await import('{(sandbox / "file-utils.js").as_posix()}');

const transfers = {{ start: () => {{ out.started += 1; }} }};
const transport = {{ connected: true }};
// One file, in the folder itself — entriesUnder keys on `path`.
const entries = [{{ id: 'f1', name: 'big.bin', path: 'album',
                   size: {total_bytes}, added_at: 0 }}];

await M.downloadDirectory(transfers, transport, null, entries, 'album', {{
  setError: (m) => out.errors.push(m),
}});

out.limit = M.ZIP_MAX_BYTES;
console.log(JSON.stringify(out));
""", encoding="utf-8")

    proc = subprocess.run(["node", str(script)], capture_output=True, text=True)
    assert proc.returncode == 0, proc.stderr
    return json.loads(proc.stdout)


def test_the_limit_is_512_mib(tmp_path):
    """The number the refusal quotes is the number the module enforces."""
    assert _run(0, tmp_path)["limit"] == 512 * MIB


def test_an_oversized_folder_is_refused_before_anything_opens(tmp_path):
    """
    One byte over. Nothing is started, and the person is told why — a silent
    return would read as a dead button.
    """
    result = _run(512 * MIB + 1, tmp_path)
    assert result["started"] == 0, "no transfer may begin"
    assert result["asked"] == 0, "nor may a save dialog have been put up first"
    assert result["errors"] == ["group.zip_too_large"], (
        "the refusal must name its own key; t() falls back to the key with no "
        "catalogue loaded, and test_locales.py holds the ten translations of it")


def test_a_folder_exactly_at_the_limit_still_downloads(tmp_path):
    """The bound is inclusive: `> ZIP_MAX_BYTES`, not `>=`."""
    result = _run(512 * MIB, tmp_path)
    assert result["errors"] == []
    assert result["started"] == 1