1"""The live-config routes refuse a write from someone who does not hold the design.
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.
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.
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
import Depends, FastAPI
24from fastapi.testclient
import TestClient
29A = {
"X-Auth-Email":
"alice@berkeley.edu"}
30B = {
"X-Auth-Email":
"bob@berkeley.edu"}
31BASE =
"/api/engine/documents"
38app.include_router(documents.router)
41@app.post("/probe/header")
46@app.get("/probe/query")
51@pytest.fixture(autouse=True)
53 monkeypatch.setenv(
"USERDATA_DIR", str(tmp_path))
54 documents.store.last_micro.clear()
59 return TestClient(app)
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
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
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
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
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
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."""
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"]
103 _take(client, doc_id, A)
104 client.put(f
"{BASE}/{doc_id}/share", headers=A, json={
"sharedWith": [B[
"X-Auth-Email"]]})
106 owner = {
"owner": A[
"X-Auth-Email"]}
109 headers={**B,
"X-Design-Id": doc_id,
"X-Design-Owner": A[
"X-Auth-Email"]},
112 assert r.status_code == 423, r.text
113 assert "checked out by" in r.json()[
"detail"]
117 """The optimizer layers stream over EventSource, which cannot set headers."""
120 r = client.get(
"/probe/query", headers=A, params={
"design_id": doc_id})
121 assert r.status_code == 423, r.text
123 _take(client, doc_id, A)
124 assert client.get(
"/probe/query", headers=A, params={
"design_id": doc_id}).status_code == 200
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
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`)."""
138 _take(client, doc_id, A)
139 monkeypatch.setattr(documents.store,
"lock_ttl", 0)
140 r = client.post(
"/probe/header", headers={**A,
"X-Design-Id": doc_id})
141 assert r.status_code == 200, r.text
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")
test_no_design_named_is_allowed(client)
test_holder_may_write(client)
probe_query(None _=DesignCheckout)
_take(client, doc_id, headers, params=None)
test_write_without_the_checkout_is_refused(client)
test_unknown_design_is_allowed(client)
test_query_params_work_too(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)