STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_control_paths_confined.py
Go to the documentation of this file.
1"""The controller routes read only configs/ (and the caller's own uploads), and fail in a sentence.
2
3``config_path`` and ``controller_config_path`` were opened as given: POST
4/api/control/simulate-from-config {config_path: "<anywhere>/design.json"} opened a file outside the
5app, any user could run another user's upload (output/user_configs/<them>/x.yaml) through the
6controller past design sharing, and the 500 carried a full traceback with server paths.
7"""
8
9from __future__ import annotations
10
11import os
12import sys
13
14import pytest
15
16sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
17
18pytest.importorskip("fastapi", reason="API tests need fastapi")
19pytest.importorskip("httpx", reason="fastapi TestClient needs httpx")
20
21from fastapi.testclient import TestClient # noqa: E402
22
23import backend.main as backend_main # noqa: E402
24from backend.routers import control as control_router # noqa: E402
25
26H = {"X-Auth-Email": "control-paths@berkeley.edu"}
27
28
29class _Opened(Exception):
30 pass
31
32
33@pytest.fixture
34def opened(monkeypatch):
35 """Record every path the route asks the loader to open; refuse to actually solve."""
36 seen: list = []
37
38 def load(path, *a, **k):
39 seen.append(str(path))
40 raise _Opened(f"opened {path}")
41
42 monkeypatch.setattr(control_router, "load_engine_config", load)
43 monkeypatch.setattr(control_router, "load_controller_config", load)
44 return seen
45
46
47@pytest.fixture
48def client():
49 return TestClient(backend_main.app, raise_server_exceptions=False)
50
51
52@pytest.mark.parametrize("route", ["/api/control/simulate-from-config"])
53def test_a_path_outside_configs_is_refused_unopened(client, opened, tmp_path, route):
54 other = tmp_path / "otheruser" / "design.yaml"
55 other.parent.mkdir()
56 other.write_text("chamber: 1\n")
57 for path in (str(other), "../output/user_configs/someone-else/design.yaml", "configs/../../../etc/passwd"):
58 r = client.post(route, json={"config_path": path}, headers=H)
59 assert r.status_code == 400, (path, r.status_code, r.text[:300])
60 assert opened == [], f"the route opened {opened}"
61
62
64 r = client.post("/api/control/simulate-from-config", json={"config_path": "configs/default.yaml"}, headers=H)
65 assert opened and opened[0].endswith(os.path.join("configs", "default.yaml")), opened
66 assert r.status_code == 500 and "Traceback" not in r.text and "File \\\"" not in r.text, r.text[:400]
67
68
70 r = client.post("/api/control/init", json={"controller_config_path": "../../etc/hosts.yaml"}, headers=H)
71 assert r.status_code == 400, r.text[:300]
72 assert opened == []
73
74
76 r = client.post("/api/control/simulate-from-config-stream", json={"config_path": str(tmp_path / "x.yaml")}, headers=H)
77 assert r.status_code == 200
78 assert "must name a .yaml file under configs/" in r.text and opened == []
79
80
82 import engine.core.runner as runner_mod
83
84 def boom(*_a, **_k):
85 raise RuntimeError("the solve fell over")
86
87 monkeypatch.setattr(runner_mod, "PintleEngineRunner", boom)
88 r = client.post("/api/control/simulate-from-config", json={}, headers=H) # the session's own config
89 assert r.status_code == 500
90 assert "the solve fell over" in r.text and "Traceback" not in r.text, r.text[:400]
test_the_stream_refuses_the_path_in_its_first_event(client, opened, tmp_path)
test_a_path_outside_configs_is_refused_unopened(client, opened, tmp_path, route)
test_a_failure_is_a_sentence_not_a_traceback(client, monkeypatch)
test_a_controller_config_outside_configs_is_refused(client, opened)