STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_config_upload_confined.py
Go to the documentation of this file.
1"""POST /api/config/upload writes only a basename, only under the caller's folder, only if it parses.
2
3The multipart filename is the client's string. Joined onto the uploads folder as given,
4"../../../x.yaml" climbed out of output/user_configs/<user>/ and an absolute path replaced the
5folder outright (pathlib's ``/`` drops the left side for an absolute right side) -- so a request
6naming /app/configs/default.yaml overwrote the default every new session loads. The bytes were
7written before they were parsed, so even a file answered with 400 "Invalid YAML" had landed.
8"""
9
10from __future__ import annotations
11
12import os
13import sys
14from pathlib import Path
15
16import pytest
17
18sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
19
20pytest.importorskip("fastapi", reason="API tests need fastapi")
21pytest.importorskip("httpx", reason="fastapi TestClient needs httpx")
22
23from fastapi.testclient import TestClient # noqa: E402
24
25import backend.main as backend_main # noqa: E402
26from backend import session as session_mod # noqa: E402
27
28H = {"X-Auth-Email": "upload-confined@berkeley.edu"}
29VALID = session_mod._DEFAULT_CONFIG_PATH.read_bytes()
30
31
32@pytest.fixture
33def root(tmp_path, monkeypatch):
34 """A project root three folders deep, so a climb of three lands inside tmp_path."""
35 monkeypatch.setenv("USERDATA_DIR", str(tmp_path / "userdata"))
36 proj = tmp_path / "a" / "b" / "c"
37 proj.mkdir(parents=True)
38 monkeypatch.setattr(backend_main, "project_root", proj)
39 return proj
40
41
42@pytest.fixture
43def client():
44 return TestClient(backend_main.app, raise_server_exceptions=False)
45
46
47def _yaml_files(base: Path) -> set[Path]:
48 return {p for p in base.rglob("*.y*ml")}
49
50
52 r = client.post(
53 "/api/config/upload",
54 files={"file": ("../../../escaped.yaml", VALID, "application/x-yaml")},
55 headers=H,
56 )
57 assert r.status_code == 200, r.text
58 uploads = (root / "output" / "user_configs").resolve()
59 written = _yaml_files(tmp_path)
60 assert written, "nothing was written at all; the upload path is not exercised"
61 assert all(uploads in p.resolve().parents for p in written), (
62 f"the upload escaped output/user_configs/<user>/: {sorted(map(str, written))}"
63 )
64 assert "../" not in r.json()["message"]
65
66
68 target = tmp_path / "abs_escape.yaml"
69 r = client.post(
70 "/api/config/upload",
71 files={"file": (str(target), VALID, "application/x-yaml")},
72 headers=H,
73 )
74 assert r.status_code == 200, r.text
75 assert not target.exists(), "an absolute filename wrote outside the project"
76 assert (root / "output" / "user_configs").exists()
77
78
80 r = client.post(
81 "/api/config/upload",
82 files={"file": ("broken.yaml", b"not: [valid", "application/x-yaml")},
83 headers=H,
84 )
85 assert r.status_code == 400, r.text
86 assert not _yaml_files(tmp_path), "an upload refused as invalid YAML was written anyway"
87
88
90 r = client.post(
91 "/api/config/upload",
92 files={"file": ("notaconfig.yaml", b"chamber: 3\n", "application/x-yaml")},
93 headers=H,
94 )
95 assert r.status_code in (400, 422), r.text[:200]
96 assert not _yaml_files(tmp_path), "an upload refused by validation was written anyway"
97
98
99@pytest.mark.parametrize("name", ["", ".yaml", "../", "config.txt"])
101 r = client.post(
102 "/api/config/upload",
103 files={"file": (name, VALID, "application/x-yaml")},
104 headers=H,
105 )
106 assert r.status_code in (400, 422), r.text[:200]
test_names_that_are_not_yaml_files_are_refused(root, client, name)
test_a_file_that_is_not_a_config_is_not_written(root, tmp_path, client)
test_a_file_that_does_not_parse_is_not_written(root, tmp_path, client)
test_an_absolute_filename_does_not_replace_the_folder(root, tmp_path, client)
test_a_climbing_filename_lands_in_the_users_folder(root, tmp_path, client)