diff options
Diffstat (limited to 'packages/meshbay-hub/tests')
| -rw-r--r-- | packages/meshbay-hub/tests/test_upload_controls_hidden.py | 122 |
1 files changed, 122 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_upload_controls_hidden.py b/packages/meshbay-hub/tests/test_upload_controls_hidden.py new file mode 100644 index 0000000..d859125 --- /dev/null +++ b/packages/meshbay-hub/tests/test_upload_controls_hidden.py @@ -0,0 +1,122 @@ +""" +When the operator closes uploading, the controls go — both of them. + +There are two ways to put a file into a group and they are in different +components: the Upload button in the Files toolbar, and the paperclip in the +chat composer. Hiding one and forgetting the other is the obvious mistake, and +the second one is the easier to forget because it does not look like an upload. + +Nothing here is a security property. **The node refuses the upload** — that is +`test_member_upload_policy.py` in the node package. This is about not offering +somebody a button whose only outcome is an error message. +""" + +import re +from pathlib import Path + +import pytest + +STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static" +APP = STATIC / "app.js" +TRANSPORT = STATIC / "transport.js" + +pytestmark = pytest.mark.skipif(not APP.exists(), reason="SPA sources unavailable") + + +@pytest.fixture(scope="module") +def app() -> str: + return APP.read_text(encoding="utf-8") + + +def _component(app: str, name: str) -> str: + start = app.index(f"\nfunction {name}(") + end = app.find("\nfunction ", start + 1) + return app[start:end if end != -1 else len(app)] + + +# ── Both controls ─────────────────────────────────────────────────────────── + +def test_the_files_toolbar_hides_its_upload_button(app): + page = _component(app, "GroupPage") + toolbar = page[page.index("file-toolbar"):] + toolbar = toolbar[:toolbar.index("group.mkdir")] + assert "mayUpload &&" in toolbar, "the Upload button is offered regardless" + + +def test_the_chat_composer_hides_its_paperclip(app): + chat = _component(app, "ChatPanel") + composer = chat[chat.index("chat-input-row"):] + assert "mayUpload &&" in composer, ( + "the chat attachment is the second way in and is still offered") + + +def test_both_read_the_same_answer(app): + """Two derivations would eventually disagree, and the disagreement would + be one of them offering an upload the node refuses.""" + page = _component(app, "GroupPage") + assert re.search(r"const mayUpload = memberUpload \|\| isNodeAdmin;", page), ( + "mayUpload is no longer derived in one place") + assert "mayUpload=${mayUpload}" in page, "the chat panel is told separately" + + +def test_the_operator_keeps_their_own_controls(app): + page = _component(app, "GroupPage") + assert "memberUpload || isNodeAdmin" in page, ( + "turning uploads off would hide the operator's own upload button") + + +# ── Learning the answer ───────────────────────────────────────────────────── + +def test_the_answer_comes_from_the_node(app): + """Not from the hub, which has no say in what may be written to someone + else's disk, and no way to be believed about it.""" + page = _component(app, "GroupPage") + assert "ack.member_upload !== false" in page, ( + "the handshake ack is what carries this") + assert "hubFetch" not in page[page.index("ack.member_upload") - 400: + page.index("ack.member_upload")] + + +def test_an_older_node_is_treated_as_permissive(app): + """A node that predates the setting sends no such field. Reading a missing + field as "off" would close every group on the older half of the network.""" + page = _component(app, "GroupPage") + assert "!== false" in page[page.index("ack.member_upload"): + page.index("ack.member_upload") + 60] + + +def test_a_change_reaches_people_already_connected(app): + """The operator may be someone else entirely, changing it while you have + the group open. A button that survives until the next reconnection is a + button somebody presses.""" + page = _component(app, "GroupPage") + assert "transport.onUploadPolicy" in page + transport = TRANSPORT.read_text(encoding="utf-8") + assert "member_upload_ack" in transport, "nothing routes the node's notice" + + +def test_the_notice_still_answers_the_operators_own_request(app): + """The same message is both a broadcast and the reply to the request that + caused it — returning early on it would leave that request hanging until it + timed out.""" + transport = TRANSPORT.read_text(encoding="utf-8") + block = transport[transport.index("member_upload_ack"):] + block = block[:block.index("index_sync")] + assert "return" not in block + + +# ── Changing it ───────────────────────────────────────────────────────────── + +def test_changing_it_is_signed(app): + transport = TRANSPORT.read_text(encoding="utf-8") + method = transport[transport.index("async setMemberUpload("):] + method = method[:method.index("\n async ", 1)] + assert "admin_challenge" in method and "_authorizeAdminOp" in method, ( + "an unsigned instruction would let any member turn uploads back on") + + +def test_only_the_operator_is_offered_the_setting(app): + panel = _component(app, "GroupSettingsPanel") + section = panel[panel.index("members.uploads_title") - 400: + panel.index("members.uploads_title")] + assert "isNodeAdmin && connected" in section |