aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_upload_size_cap.py
blob: dca6e15bfc3bc2fdd6c24cbed92c82b75bb5cb5f (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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
"""
How large a single upload may be, and who decides.

The cap used to be a constant: 4 GB, in `webrtc_server.py`, the same on a Pi
with a 32 GB card and on a machine holding a film library. It is the operator's
disk that fills, so the number is theirs — `max_upload_gb` under [node] in
node.toml, the Node page, and `meshbay-node transfers max-size`, with the
constant as the default when they have said nothing.

These follow the value along the whole path rather than checking that the field
parses, for the reason `test_stream_capacity_config.py` gives: every join in
such a path has been wrong at least once, and a ceiling read from the wrong
place fails only when somebody sends a large file.
"""

import textwrap
from pathlib import Path

import pytest

from meshbay_node.config import load_config
from meshbay_node.roots import RootSet
from meshbay_node.transport.webrtc_server import (
    GB_BYTES,
    MAX_UPLOAD_BYTES,
    WebRTCPeerSession,
    WebRTCTransport,
)


def _cfg(tmp_path: Path, body: str):
    p = tmp_path / "node.toml"
    p.write_text(textwrap.dedent(body))
    return load_config(p)


class _FakePC:
    def on(self, *a, **k):
        return lambda f: f


def _session(gb):
    t = WebRTCTransport(
        sk_node=None, hub_pk_pem=b"", gek=b"\0" * 32,
        roots=RootSet(), index=None, max_upload_gb=gb)
    return t, WebRTCPeerSession(_FakePC(), t._ctx, peer_id="p")


# ── What the operator writes ─────────────────────────────────────────────────

def test_the_default_is_eight_gb():
    assert MAX_UPLOAD_BYTES == 8 * GB_BYTES


def test_the_operator_sets_it(tmp_path):
    cfg = _cfg(tmp_path, """
        [node]
        max_upload_gb = 20
    """)
    assert cfg.node.max_upload_gb == 20


def test_a_fraction_of_a_gigabyte_is_legitimate(tmp_path):
    """Not a count, so it does not get `_positive`'s floor of one.

    A node on a small disk may well want to stop at half a gigabyte, and
    rounding that to zero would refuse every upload.
    """
    cfg = _cfg(tmp_path, """
        [node]
        max_upload_gb = 0.5
    """)
    assert cfg.node.max_upload_gb == 0.5
    _t, s = _session(0.5)
    assert s._max_upload_bytes() == GB_BYTES // 2


def test_saying_nothing_gets_the_default(tmp_path):
    cfg = _cfg(tmp_path, """
        [node]
        quic_port = 19010
    """)
    assert cfg.node.max_upload_gb * GB_BYTES == MAX_UPLOAD_BYTES, (
        "the config default and the source default disagree, so the ceiling "
        "depends on whether a node.toml happens to mention it")


@pytest.mark.parametrize("value", ["0", "-2", '"lots"', "true"])
def test_a_value_that_would_refuse_every_upload_is_refused(tmp_path, value, caplog):
    cfg = _cfg(tmp_path, f"""
        [node]
        max_upload_gb = {value}
    """)
    assert cfg.node.max_upload_gb * GB_BYTES == MAX_UPLOAD_BYTES
    assert "max_upload_gb" in caplog.text, (
        "the value was silently discarded — the operator has no way to learn "
        "their setting is not in effect")


# ── That the number reaches the check it governs ─────────────────────────────

@pytest.mark.parametrize("gb,expect", [
    (None, MAX_UPLOAD_BYTES),
    (2, 2 * GB_BYTES),
    (16, 16 * GB_BYTES),
])
def test_the_configured_size_is_what_the_handler_enforces(gb, expect):
    _t, s = _session(gb)
    assert s._max_upload_bytes() == expect


def test_the_handler_reads_it_rather_than_the_constant():
    """A ceiling captured once is one the operator cannot change.

    The check runs per chunk, so raising the cap has to reach an upload that
    is already in flight — which it does only if the handler asks the context
    each time instead of closing over the constant.
    """
    src = (Path(__file__).resolve().parents[1] / "src" / "meshbay_node"
           / "transport" / "webrtc_server.py").read_text(encoding="utf-8")
    i = src.index("Upload exceeds size limit")
    check = src[src.rindex("if state.bytes", 0, i):i]
    assert "_max_upload_bytes()" in check, (
        "the upload check reads the module constant, so node.toml, the Node "
        "page and the CLI are all read and then ignored")


def test_raising_it_reaches_an_upload_already_running():
    t, s = _session(2)
    assert s._max_upload_bytes() == 2 * GB_BYTES
    t.set_capacity(max_upload_gb=10)
    assert s._max_upload_bytes() == 10 * GB_BYTES, (
        "the session kept the ceiling it started with, so the setting only "
        "takes effect on a restart"
    )


def test_zero_is_refused_at_the_transport_too():
    t, _s = _session(4)
    with pytest.raises(ValueError):
        t.set_capacity(max_upload_gb=0)


def test_the_daemon_passes_it():
    """The join that syntax checking cannot see."""
    daemon = (Path(__file__).resolve().parents[1] / "src" / "meshbay_node"
              / "daemon.py").read_text(encoding="utf-8")
    i = daemon.index("WebRTCTransport(")
    call = daemon[i:daemon.index(")", daemon.index("denylist=denylist", i))]
    assert "max_upload_gb=self._config.node.max_upload_gb" in call, (
        "the daemon builds the transport without the operator's ceiling, so "
        "node.toml is read and then ignored")