STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_config_checkout.py
Go to the documentation of this file.
1"""The live-config routes refuse a write from someone who does not hold the design.
2
3The checkout already gated /api/engine/documents/{id}/autosave, which protects
4the *stored* design. It did not gate /api/config, which is what the UI actually
5edits -- so a read-only viewer's changes landed in their own session, the screen
6updated, and the edit then evaporated at the next reload with nothing said.
7These tests pin the refusal at the point of the edit.
8
9See backend/checkout.py for why /api/config/load is deliberately NOT gated: it
10is how the design bar opens a design you may only view.
11"""
12
13import os
14import sys
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 import Depends, FastAPI # noqa: E402
24from fastapi.testclient import TestClient # noqa: E402
25
26from backend.checkout import DesignCheckout # noqa: E402
27from backend.routers import documents # noqa: E402
28
29A = {"X-Auth-Email": "alice@berkeley.edu"}
30B = {"X-Auth-Email": "bob@berkeley.edu"}
31BASE = "/api/engine/documents"
32
33
34# A stand-in for the real config routes. Mounting backend.main here would drag
35# in the engine kernel and the optimizer; what is under test is the dependency,
36# and this exercises exactly the same one the real routes declare.
37app = FastAPI()
38app.include_router(documents.router)
39
40
41@app.post("/probe/header")
42async def probe_header(_: None = DesignCheckout):
43 return {"ok": True}
44
45
46@app.get("/probe/query")
47async def probe_query(_: None = DesignCheckout):
48 return {"ok": True}
49
50
51@pytest.fixture(autouse=True)
52def _isolate(tmp_path, monkeypatch):
53 monkeypatch.setenv("USERDATA_DIR", str(tmp_path))
54 documents.store.last_micro.clear()
55
56
57@pytest.fixture
58def client():
59 return TestClient(app)
60
61
62def _create(client, headers, name="Baseline"):
63 r = client.post(BASE, headers=headers, json={"name": name, "config": {"combustion": {}}})
64 assert r.status_code == 200, r.text
65 return r.json()["id"]
66
67
68def _take(client, doc_id, headers, params=None):
69 r = client.post(f"{BASE}/{doc_id}/checkout", headers=headers, params=params or {})
70 assert r.status_code == 200, r.text
71
72
73def _release(client, doc_id, headers, params=None):
74 r = client.delete(f"{BASE}/{doc_id}/checkout", headers=headers, params=params or {})
75 assert r.status_code == 200, r.text
76
77
79 """The config routes predate designs; scripted and file-upload callers still
80 use them with nothing open, and must keep working."""
81 assert client.post("/probe/header", headers=A).status_code == 200
82
83
85 doc_id = _create(client, A)
86 _take(client, doc_id, A)
87 r = client.post("/probe/header", headers={**A, "X-Design-Id": doc_id})
88 assert r.status_code == 200, r.text
89
90
92 """Creating a design checks it out to you, so release it first: this is the
93 'my checkout lapsed and I kept typing' case."""
94 doc_id = _create(client, A)
95 _release(client, doc_id, A)
96 r = client.post("/probe/header", headers={**A, "X-Design-Id": doc_id})
97 assert r.status_code == 423, r.text
98 assert "Take" in r.json()["detail"]
99
100
102 doc_id = _create(client, A)
103 _take(client, doc_id, A)
104 client.put(f"{BASE}/{doc_id}/share", headers=A, json={"sharedWith": [B["X-Auth-Email"]]})
105
106 owner = {"owner": A["X-Auth-Email"]}
107 r = client.post(
108 "/probe/header",
109 headers={**B, "X-Design-Id": doc_id, "X-Design-Owner": A["X-Auth-Email"]},
110 params=owner,
111 )
112 assert r.status_code == 423, r.text
113 assert "checked out by" in r.json()["detail"]
114
115
117 """The optimizer layers stream over EventSource, which cannot set headers."""
118 doc_id = _create(client, A)
119 _release(client, doc_id, A)
120 r = client.get("/probe/query", headers=A, params={"design_id": doc_id})
121 assert r.status_code == 423, r.text
122
123 _take(client, doc_id, A)
124 assert client.get("/probe/query", headers=A, params={"design_id": doc_id}).status_code == 200
125
126
128 """A stale id in localStorage must not brick every config write."""
129 r = client.post("/probe/header", headers={**A, "X-Design-Id": "no-such-design"})
130 assert r.status_code == 200, r.text
131
132
134 """The config edit that arrives after a lapse is kept, not refused, when
135 nobody has taken the design since -- the same rule as the design's own
136 autosave (`DesignStore.claim_for_write`)."""
137 doc_id = _create(client, A)
138 _take(client, doc_id, A)
139 monkeypatch.setattr(documents.store, "lock_ttl", 0) # every checkout reads as expired
140 r = client.post("/probe/header", headers={**A, "X-Design-Id": doc_id})
141 assert r.status_code == 200, r.text
142
143
145 doc_id = _create(client, A)
146 _take(client, doc_id, A)
147 client.put(f"{BASE}/{doc_id}/share", headers=A, json={"sharedWith": [B["X-Auth-Email"]]})
148 monkeypatch.setattr(documents.store, "lock_ttl", 0)
149 _take(client, doc_id, B, params={"owner": A["X-Auth-Email"]})
150 monkeypatch.setattr(documents.store, "lock_ttl", 900)
151 r = client.post("/probe/header", headers={**A, "X-Design-Id": doc_id})
152 assert r.status_code == 423, r.text
test_a_lapsed_checkout_someone_else_took_is_refused(client, monkeypatch)
_create(client, headers, name="Baseline")
probe_query(None _=DesignCheckout)
_take(client, doc_id, headers, params=None)
test_write_without_the_checkout_is_refused(client)
_release(client, doc_id, headers, params=None)
test_write_while_someone_else_holds_it_is_refused(client)
_isolate(tmp_path, monkeypatch)
test_a_lapsed_checkout_nobody_took_is_reclaimed_by_a_config_write(client, monkeypatch)
probe_header(None _=DesignCheckout)