Spaces:
Running
Running
| """The construct as the object every tool operates on. | |
| THE INVERSION UNDER TEST | |
| ------------------------ | |
| A tool used to be a destination: eight routes, eight empty inputs, and a | |
| "hand-off" that copied a string from one <textarea> into another. Nothing | |
| carried across them and the backend never learned which project a run belonged | |
| to, so /api/mission had to reconstruct "constructs" by grouping artifacts on a | |
| normalised NAME STRING — which merges everything left at a default name and | |
| splits a project the moment you rename something. | |
| These tests pin the three properties that decide whether the replacement is | |
| better than what it replaces, because each is a way it could be worse: | |
| 1. SIGNED OUT, NOTHING CHANGES. Public tool access was deliberate. If any of | |
| this becomes a precondition for running a tool, the change is a | |
| regression no matter how tidy the workspace looks. | |
| 2. THE SAME SEQUENCE IS THE SAME PROJECT. Pasting twice must not spawn a | |
| duplicate — that is how a workspace becomes a list the user has to tidy. | |
| 3. ATTRIBUTION IS BEST-EFFORT AND NEVER FATAL. A failed cross-reference must | |
| not fail the save the user actually asked for. | |
| """ | |
| import json | |
| import pytest | |
| from dee import auth as dee_auth | |
| from dee import server | |
| def _client(): | |
| app = server.create_app() | |
| app.config.update(TESTING=True) | |
| return app.test_client() | |
| class _Auth: | |
| def __init__(self, user_id=None): | |
| self.user_id = user_id | |
| self.anonymous = user_id is None | |
| # --------------------------------------------------------------------------- # | |
| # 1. Signed out, nothing changes | |
| # --------------------------------------------------------------------------- # | |
| def test_listing_constructs_signed_out_is_an_empty_gate_not_an_error(monkeypatch): | |
| """A 200 with gated:true, not a 401. The switcher has to render for a | |
| signed-out visitor without treating them as a failure.""" | |
| monkeypatch.setattr(server._auth, "get_auth", lambda: _Auth(None)) | |
| body = _client().get("/api/constructs").get_json() | |
| assert body == {"ok": True, "gated": True, "constructs": []} | |
| def test_writing_constructs_signed_out_is_refused_with_a_signin_kind( | |
| monkeypatch, method, path): | |
| monkeypatch.setattr(server._auth, "get_auth", lambda: _Auth(None)) | |
| cl = _client() | |
| res = getattr(cl, method)(path, json={"sequence_dna": "ACGT" * 20}) | |
| assert res.status_code == 401 | |
| assert res.get_json().get("kind") == "signin_required" | |
| def test_the_client_keeps_its_own_selection_when_signed_out(): | |
| """context.js must not depend on the server for the signed-out path — the | |
| selection lives in localStorage and still threads every view.""" | |
| with open("dee/static/context.js", encoding="utf-8") as fh: | |
| src = fh.read() | |
| assert "localStorage" in src | |
| # adopt() posts, but a non-ok response must be swallowed, not thrown. | |
| assert "if (!r.ok) return null;" in src | |
| def test_context_never_blocks_a_tool_run(): | |
| """Rule 2. There must be no code path where a missing construct prevents | |
| a request — the interceptor only ever ADDS a field.""" | |
| with open("dee/static/context.js", encoding="utf-8") as fh: | |
| src = fh.read() | |
| assert "return nativeFetch(input, init);" in src | |
| # Exactly ONE call site, so no early bail-out can creep in later: every | |
| # path through the wrapper ends at the same return. | |
| assert src.count("nativeFetch(") == 1 | |
| def test_prefill_never_clobbers_what_the_user_typed(): | |
| """Rule 3. Overwriting a filled input is the fastest way to make the whole | |
| idea untrustworthy.""" | |
| with open("dee/static/context.js", encoding="utf-8") as fh: | |
| src = fh.read() | |
| assert "if (!el || (el.value || '').trim()) return false; // never clobber" in src | |
| # --------------------------------------------------------------------------- # | |
| # 2. The same sequence is the same project | |
| # --------------------------------------------------------------------------- # | |
| def test_posting_the_same_sequence_twice_reuses_the_construct(monkeypatch): | |
| monkeypatch.setattr(server._auth, "get_auth", lambda: _Auth("u1")) | |
| existing = {"id": "abc", "name": "MC1R", "phase": "Build", | |
| "sequence_hash": "deadbeef"} | |
| monkeypatch.setattr(server._auth, "find_construct_by_hash", | |
| lambda uid, h: existing) | |
| called = {"n": 0} | |
| def _never(*a, **k): | |
| called["n"] += 1 | |
| return {"ok": True, "id": "new"} | |
| monkeypatch.setattr(server._auth, "save_construct", _never) | |
| body = _client().post("/api/constructs", | |
| json={"sequence_dna": "ACGT" * 20}).get_json() | |
| assert body["ok"] is True and body["reused"] is True | |
| assert body["construct"]["id"] == "abc" | |
| assert called["n"] == 0, "a duplicate project must not be created" | |
| def test_a_construct_needs_a_sequence(monkeypatch): | |
| monkeypatch.setattr(server._auth, "get_auth", lambda: _Auth("u1")) | |
| res = _client().post("/api/constructs", json={"name": "no sequence"}) | |
| assert res.status_code == 400 | |
| def test_the_list_payload_does_not_ship_the_sequence(monkeypatch): | |
| """The switcher renders every project. Shipping the DNA with each would | |
| make a routine dropdown megabytes.""" | |
| monkeypatch.setattr(server._auth, "get_auth", lambda: _Auth("u1")) | |
| monkeypatch.setattr(server._auth, "list_constructs", lambda uid: [{ | |
| "id": "a", "name": "N", "phase": "Design", "sequence_hash": "h", | |
| "sequence_dna": "ACGT" * 5000, "wt_protein": "MKV" * 100, | |
| "plasmid_ids": ["p1"], "crispr_ids": [], "primer_ids": [], | |
| }]) | |
| row = _client().get("/api/constructs").get_json()["constructs"][0] | |
| assert "sequence_dna" not in row and "wt_protein" not in row | |
| assert row["n_plasmids"] == 1 and row["n_crispr"] == 0 | |
| def test_the_single_get_does_ship_the_sequence(monkeypatch): | |
| """...because that one is the construct the user actually selected, and | |
| the tools need it to pre-fill.""" | |
| monkeypatch.setattr(server._auth, "get_auth", lambda: _Auth("u1")) | |
| monkeypatch.setattr(server._auth, "get_construct", lambda uid, cid: { | |
| "id": cid, "name": "N", "phase": "Design", | |
| "sequence_dna": "ACGTACGT", "wt_protein": "MKV", | |
| }) | |
| c = _client().get( | |
| "/api/constructs/11111111-1111-1111-1111-111111111111" | |
| ).get_json()["construct"] | |
| assert c["sequence_dna"] == "ACGTACGT" and c["wt_protein"] == "MKV" | |
| # --------------------------------------------------------------------------- # | |
| # 3. Attribution is best-effort and never fatal | |
| # --------------------------------------------------------------------------- # | |
| def test_attach_rejects_a_bad_kind_or_id(): | |
| assert dee_auth.attach_artifact("u1", "not-a-uuid", "plasmid", "x")["ok"] is False | |
| good = "11111111-1111-1111-1111-111111111111" | |
| assert dee_auth.attach_artifact("u1", good, "nonsense", good)["ok"] is False | |
| def test_phase_only_ever_moves_forward(monkeypatch): | |
| """Opening the CRISPR tool after logging results must not drag a project | |
| back from Learn to Edit.""" | |
| good = "11111111-1111-1111-1111-111111111111" | |
| monkeypatch.setattr(dee_auth, "get_construct", lambda uid, cid: { | |
| "id": cid, "phase": "Learn", "crispr_ids": []}) | |
| seen = {} | |
| def _upd(uid, cid, **fields): | |
| seen.update(fields) | |
| return {"ok": True, "construct": {}} | |
| monkeypatch.setattr(dee_auth, "update_construct", _upd) | |
| dee_auth.attach_artifact("u1", good, "crispr", good) | |
| assert seen["crispr_ids"] == [good] | |
| assert "phase" not in seen, "Learn must not regress to Edit" | |
| def test_phase_advances_when_it_should(monkeypatch): | |
| good = "11111111-1111-1111-1111-111111111111" | |
| monkeypatch.setattr(dee_auth, "get_construct", lambda uid, cid: { | |
| "id": cid, "phase": "Design", "crispr_ids": []}) | |
| seen = {} | |
| monkeypatch.setattr(dee_auth, "update_construct", | |
| lambda uid, cid, **f: (seen.update(f), | |
| {"ok": True, "construct": {}})[1]) | |
| dee_auth.attach_artifact("u1", good, "crispr", good) | |
| assert seen["phase"] == "Edit" | |
| def test_attaching_the_same_artifact_twice_is_a_no_op(monkeypatch): | |
| good = "11111111-1111-1111-1111-111111111111" | |
| monkeypatch.setattr(dee_auth, "get_construct", lambda uid, cid: { | |
| "id": cid, "phase": "Build", "plasmid_ids": [good]}) | |
| monkeypatch.setattr(dee_auth, "update_construct", | |
| lambda *a, **k: pytest.fail("should not write")) | |
| assert dee_auth.attach_artifact("u1", good, "plasmid", good)["already"] is True | |
| def test_a_failing_attribution_does_not_fail_the_save(monkeypatch): | |
| """The artifact still exists in its own table; only the cross-reference is | |
| missing. Losing the save instead would be strictly worse.""" | |
| monkeypatch.setattr(server._auth, "get_auth", lambda: _Auth("u1")) | |
| monkeypatch.setattr(server._auth, "save_plasmid", | |
| lambda *a, **k: {"ok": True, "id": "p-1"}) | |
| monkeypatch.setattr(server._auth, "cleanup_expired_plasmids_async", | |
| lambda uid: None) | |
| def _boom(*a, **k): | |
| raise RuntimeError("supabase down") | |
| monkeypatch.setattr(server._auth, "attach_artifact", _boom) | |
| res = _client().post("/api/plasmid/save", json={ | |
| "name": "p", "topology": "circular", "sequence": "ACGT" * 30, | |
| "features": [], "construct_id": "11111111-1111-1111-1111-111111111111", | |
| }) | |
| assert res.status_code == 200 and res.get_json()["ok"] is True | |
| def test_user_id_cannot_be_passed_as_a_field_at_all(): | |
| """The strongest version of the check: `user_id` is a positional parameter | |
| of update_construct, so a caller splatting a request body that contains it | |
| gets a TypeError rather than a silent ownership change. Asserted because | |
| it is a property of the signature that a future refactor to **kwargs would | |
| quietly remove.""" | |
| good = "11111111-1111-1111-1111-111111111111" | |
| with pytest.raises(TypeError): | |
| dee_auth.update_construct("u1", good, **{"user_id": "attacker"}) | |
| def test_update_ignores_fields_the_client_should_not_write(monkeypatch): | |
| """Everything outside the allow-list is dropped before the row is touched | |
| — a TTL or an id from a request body must never reach it.""" | |
| good = "11111111-1111-1111-1111-111111111111" | |
| captured = {} | |
| import urllib.request | |
| class _Resp: | |
| def __enter__(self): return self | |
| def __exit__(self, *a): return False | |
| def read(self): return b"[]" | |
| def _fake(req, timeout=0): | |
| captured["body"] = json.loads(req.data.decode()) | |
| return _Resp() | |
| monkeypatch.setattr(dee_auth, "SUPABASE_URL", "https://x") | |
| monkeypatch.setattr(dee_auth, "SUPABASE_SERVICE_KEY", "k") | |
| monkeypatch.setattr(urllib.request, "urlopen", _fake) | |
| dee_auth.update_construct("u1", good, name="ok", expires_at="never", | |
| id="other", sequence_hash="forged", phase="Build") | |
| assert "expires_at" not in captured["body"] | |
| assert "id" not in captured["body"] | |
| assert "sequence_hash" not in captured["body"] | |
| assert captured["body"]["name"] == "ok" | |
| assert captured["body"]["phase"] == "Build" | |