1"""POST /api/config/upload writes only a basename, only under the caller's folder, only if it parses.
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.
10from __future__
import annotations
14from pathlib
import Path
18sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
20pytest.importorskip(
"fastapi", reason=
"API tests need fastapi")
21pytest.importorskip(
"httpx", reason=
"fastapi TestClient needs httpx")
23from fastapi.testclient
import TestClient
26from backend
import session
as session_mod
28H = {
"X-Auth-Email":
"upload-confined@berkeley.edu"}
29VALID = session_mod._DEFAULT_CONFIG_PATH.read_bytes()
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)
44 return TestClient(backend_main.app, raise_server_exceptions=
False)
48 return {p
for p
in base.rglob(
"*.y*ml")}
54 files={
"file": (
"../../../escaped.yaml", VALID,
"application/x-yaml")},
57 assert r.status_code == 200, r.text
58 uploads = (root /
"output" /
"user_configs").resolve()
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))}"
64 assert "../" not in r.json()[
"message"]
68 target = tmp_path /
"abs_escape.yaml"
71 files={
"file": (str(target), VALID,
"application/x-yaml")},
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()
82 files={
"file": (
"broken.yaml", b
"not: [valid",
"application/x-yaml")},
85 assert r.status_code == 400, r.text
86 assert not _yaml_files(tmp_path),
"an upload refused as invalid YAML was written anyway"
92 files={
"file": (
"notaconfig.yaml", b
"chamber: 3\n",
"application/x-yaml")},
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"
99@pytest.mark.parametrize("name", ["", ".yaml", "../", "config.txt"])
102 "/api/config/upload",
103 files={
"file": (name, VALID,
"application/x-yaml")},
106 assert r.status_code
in (400, 422), r.text[:200]
set[Path] _yaml_files(Path base)
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)
root(tmp_path, monkeypatch)
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)