"""The packet API: hand drafts, header CAS, drawing pages, transitions, filing and the seal, the boards, assets.""" from __future__ import annotations from collections.abc import Iterator from typing import Any import pytest from fakedir import FakeDirectory from fastapi.testclient import TestClient from pkthelpers import ( SIGN, as_user, attach_drawing, get, happy_path, make_draft, make_unit, ok_step, phase2_client, ready_draft, seed_users, set_header, step, tiny_pdf, upload, ) from pshelpers import app_conn, needs_pg from psycopg.types.json import Jsonb from packetstation import events from packetstation.settings import Settings pytestmark = needs_pg @pytest.fixture def pg_client2(pg_settings: Settings, fake_dir: FakeDirectory, dev_visual: None) -> Iterator[TestClient]: with phase2_client(pg_settings, fake_dir) as client: seed_users(client) yield client @pytest.fixture def unit(pg_settings: Settings) -> tuple[int, int]: with app_conn(pg_settings) as conn: return make_unit(conn) def _one(settings: Settings, sql: str, params: Any = ()) -> Any: with app_conn(settings) as conn: return conn.execute(sql, params).fetchone() def _all(settings: Settings, sql: str, params: Any = ()) -> list[Any]: with app_conn(settings) as conn: return conn.execute(sql, params).fetchall() # ---------------------------------------------------------------- hand drafts def test_hand_draft_by_clerical(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None: as_user(pg_client2, "clerk.clara") pkt = make_draft(pg_client2, unit[1]) assert pkt["status"] == "draft" and pkt["source_kind"] == "hand" and pkt["source"] == {"kind": "hand"} assert pkt["flags"] == ["hand"] and pkt["drawing_state"] == "none" and pkt["version"] == 1 assert pkt["flag"] == "Made by hand - check it and send it on" assert pkt["holder"] == "Clerical" and pkt["dept"] == {"code": "03", "name": "Weld Parts", "weld": True} wo = pkt["work_orders"][0] assert (wo["wo_base"], wo["lot"], wo["split"], wo["sub"]) == ("L670001", "001", "0", "0") assert pkt["unit"]["number"].endswith("001") and pkt["unit"]["project"]["customer"] == "Acme Refining" assert [i["key"] for i in pkt["checklist_items"]][:2] == ["dwg", "heat"] assert "D-1001 Rev B" in pkt["checklist_items"][0]["text"] and len(pkt["qc_items"]) == 10 assert "edit_header" in pkt["can"] and "send_to_warehouse" not in pkt["can"] assert pkt["blocked"]["send_to_warehouse"] == [ "Say whether this packet needs a heat map.", "Attach the drawing first.", ] log = pg_client2.get(f"/api/packets/{pkt['id']}/log").json() assert [r["action"] for r in log] == ["Packet created by hand"] and log[0]["by"]["display_name"] == "Clara Clerk" row = _one(pg_settings, "SELECT action, detail FROM audit_events WHERE action = 'packet.created'") assert row[1]["source"] == "hand" and row[1]["work_orders"] == 1 def test_hand_draft_rules(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None: from pkthelpers import draft_body as_user(pg_client2, "wh.wade") r = pg_client2.post("/api/packets", json=draft_body(unit[1])) assert r.status_code == 403 and r.json()["code"] == "forbidden" as_user(pg_client2, "clerk.clara") r = pg_client2.post("/api/packets", json=draft_body(unit[1], dept_code="QC")) assert r.status_code == 422 and r.json()["code"] == "invalid_dept" r = pg_client2.post("/api/packets", json=draft_body(unit[1], work_orders=[{"wo_base": 670001}])) assert r.status_code == 422 # work order keys are text, never numbers with app_conn(pg_settings) as conn: conn.execute("UPDATE units SET status = 'closed' WHERE id = %s", (unit[1],)) r = pg_client2.post("/api/packets", json=draft_body(unit[1])) assert r.status_code == 409 and r.json()["code"] == "unit_closed" r = pg_client2.post("/api/packets", json=draft_body(unit[1]) | {"pad": "x" * 210_000}) assert r.status_code == 413 and r.json()["code"] == "too_large" assert pg_client2.get("/api/packets/999999").status_code == 404 # ---------------------------------------------------------------- header CAS def test_patch_header_cas(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None: as_user(pg_client2, "clerk.clara") pkt = make_draft(pg_client2, unit[1]) pid = pkt["id"] r = pg_client2.patch(f"/api/packets/{pid}", json={"set": {"title": "New title"}, "base": {"title": pkt["title"]}}) assert r.status_code == 200 and r.json()["title"] == "New title" and r.json()["version"] == 2 r = pg_client2.patch(f"/api/packets/{pid}", json={"set": {"title": "Third"}, "base": {"title": "Spool 4 inlet"}}) assert r.status_code == 409 and r.json()["code"] == "field_conflict" assert r.json()["current"] == {"title": "New title"} # heat map chips r = pg_client2.patch(f"/api/packets/{pid}", json={"set": {"heat_map": True}, "base": {"heat_map": None}}) assert r.status_code == 200 and r.json()["heat_map"] is True assert pg_client2.get(f"/api/packets/{pid}/log").json()[0]["action"] == "Heat map required" r = pg_client2.patch(f"/api/packets/{pid}", json={"set": {"heat_map": False}, "base": {"heat_map": True}}) assert r.status_code == 200 and r.json()["heat_map"] is False r = pg_client2.patch(f"/api/packets/{pid}", json={"set": {"status": "filed"}}) assert r.status_code == 422 as_user(pg_client2, "wh.wade") r = pg_client2.patch(f"/api/packets/{pid}", json={"set": {"title": "Mine"}}) assert r.status_code == 403 and r.json()["code"] == "forbidden" assert r.json()["detail"] == ( "Your access doesn't allow changing the packet details on this packet. It's with Clerical right now." ) # ---------------------------------------------------------------- drawing pages and assets def test_attach_drawing_needs_signature(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None: as_user(pg_client2, "clerk.clara") pkt = make_draft(pg_client2, unit[1]) a = upload(pg_client2, tiny_pdf(2), name="D-1001 B.pdf") assert a.status_code == 201, a.text asset = a.json() assert asset["mime"] == "application/pdf" and asset["pages"] == 2 and asset["url"] == f"/_blob/{asset['id']}" url = f"/api/packets/{pkt['id']}/pages" r = pg_client2.post(url, json={"asset_id": asset["id"], "type": "drawing"}) assert r.status_code == 400 and r.json()["code"] == "signature_required" assert r.json()["meaning"] == "Attach a drawing by hand" r = pg_client2.post(url, json={"asset_id": asset["id"], "sign": {"factor": "password", "secret": "nope"}}) assert r.status_code == 403 and r.json()["code"] == "wrong_secret" and r.json()["attempts_left"] == 2 fails = _one(pg_settings, "SELECT stepup_failures FROM sessions WHERE revoked_at IS NULL ORDER BY created_at DESC") assert fails[0] == 1 # the wrong attempt was kept although the write was refused r = pg_client2.post(url, json={"asset_id": asset["id"], "sign": SIGN}) assert r.status_code == 200, r.text out = r.json() assert [(p["n"], p["pn"], p["dwg"]) for p in out["pages"]] == [(1, 1, "D-1001"), (2, 2, "D-1001")] assert out["drawing_state"] == "hand" and out["pages"][0]["url"] == f"/_blob/{asset['id']}" log = pg_client2.get(f"/api/packets/{pkt['id']}/log").json()[0] assert log["action"] == "Drawing attached by hand" and log["signed"] is True r = pg_client2.post(url, json={"asset_id": asset["id"], "sign": SIGN}) assert r.status_code == 409 and r.json()["code"] == "already_attached" r = pg_client2.request("DELETE", f"/api/packets/{pkt['id']}/pages/2", json={"reason": "wrong sheet"}) assert r.status_code == 200 and r.json()["pages"] == [] and r.json()["drawing_state"] == "none" def test_assets_blob_and_limits( pg_client2: TestClient, pg_settings: Settings, fake_dir: FakeDirectory, dev_visual: None ) -> None: as_user(pg_client2, "clerk.clara") data = tiny_pdf(1) a = upload(pg_client2, data, name="..\\evil/name.pdf").json() assert a["original_name"] == "name.pdf" and a["size"] == len(data) again = upload(pg_client2, data, name="other.pdf").json() assert again["id"] == a["id"] assert pg_client2.get(f"/api/assets/{a['id']}").json()["sha256"] == a["sha256"] b = pg_client2.get(a["url"]) assert b.status_code == 200 and b.content == data assert b.headers["x-content-type-options"] == "nosniff" assert b.headers["content-disposition"] == 'inline; filename="name.pdf"' assert b.headers["cache-control"] == "private, max-age=3600" and a["sha256"] in b.headers["etag"] assert pg_client2.get(a["url"] + "?download=1").headers["content-disposition"].startswith("attachment;") assert pg_client2.get("/_blob/00000000-0000-0000-0000-000000000000").status_code == 404 r = upload(pg_client2, b"just some text, not a document", name="x.txt", mime="text/plain") assert r.status_code == 415 and r.json()["code"] == "unsupported_type" r = upload(pg_client2, b"%PDF-1.7\nthis is not really a pdf at all\n%%EOF\n", name="bad.pdf") assert r.status_code == 422 and r.json()["code"] == "bad_pdf" small = pg_settings.model_copy(update={"upload_max_bytes": 2048}) with phase2_client(small, fake_dir) as c2: as_user(c2, "clerk.clara") r = upload(c2, b"%PDF-1.4\n" + b"0" * 5000) assert r.status_code == 413 and r.json()["code"] == "too_large" with TestClient(c2.app) as anon: assert anon.get(a["url"]).status_code == 401 # ---------------------------------------------------------------- transitions def test_send_to_warehouse( pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int], monkeypatch: pytest.MonkeyPatch ) -> None: as_user(pg_client2, "clerk.clara") pkt = make_draft(pg_client2, unit[1]) r = step(pg_client2, pkt, "send_to_warehouse") assert r.status_code == 403 and r.json() == { "detail": "Say whether this packet needs a heat map.", "code": "precondition", } pkt = set_header(pg_client2, pkt["id"], heat_map=False).json() r = step(pg_client2, pkt, "send_to_warehouse") assert r.status_code == 403 and r.json()["detail"] == "Attach the drawing first." pkt = attach_drawing(pg_client2, pkt["id"]) assert "send_to_warehouse" in pkt["can"] and "send_to_warehouse" not in pkt["blocked"] r = pg_client2.post( f"/api/packets/{pkt['id']}/transitions", json={"action": "send_to_warehouse", "expect": {"status": "warehouse", "dept": "03"}}, ) assert r.status_code == 409 and r.json()["code"] == "stale_step" assert r.json()["current"] == {"status": "draft", "dept": "03", "version": pkt["version"]} published: list[dict[str, Any]] = [] real = events.publish monkeypatch.setattr(events, "publish", lambda conn, payload: (published.append(payload), real(conn, payload))) out = ok_step(pg_client2, pkt, "send_to_warehouse") assert out["status"] == "warehouse" and out["version"] == pkt["version"] + 1 and out["holder"] == "Warehouse" assert out["wh"]["stage"] == "incoming" and out["wh"]["by"]["display_name"] == "Clara Clerk" assert out["wh_sent_at"] and out["wh_skipped"] is False assert published == [ {"event": "changed", "packet_id": pkt["id"], "version": out["version"], "status": "warehouse", "dept": "03"} ] tr = _one( pg_settings, "SELECT action, from_status, to_status, by FROM packet_transitions WHERE packet_id = %s", (pkt["id"],), ) assert tr[:3] == ("send_to_warehouse", "draft", "warehouse") lg = _one( pg_settings, "SELECT action, transition_id FROM packet_log WHERE packet_id = %s ORDER BY id DESC", (pkt["id"],) ) assert lg[0] == "Sent to warehouse" and lg[1] is not None au = _one(pg_settings, "SELECT detail FROM audit_events WHERE action = 'packet.send_to_warehouse'") assert au[0]["from"] == "draft" and au[0]["to"] == "warehouse" as_user(pg_client2, "clerk.clara") r = pg_client2.patch(f"/api/packets/{pkt['id']}", json={"set": {"title": "x"}}) assert r.status_code == 403 and r.json()["code"] == "wrong_status" def test_happy_path_file_and_reopen(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None: pkt = happy_path(pg_client2, pg_settings, unit[1]) pid = pkt["id"] seal = _one(pg_settings, "SELECT key_id, signature_id, broken_at FROM packet_seals WHERE packet_id = %s", (pid,)) assert seal[1] is not None and seal[2] is None view = pg_client2.get(f"/api/packets/{pid}/seal").json() assert view["valid"] is True and view["seal"]["key_id"] == seal[0] actions = [ r[0] for r in _all(pg_settings, "SELECT action FROM packet_transitions WHERE packet_id = %s ORDER BY id", (pid,)) ] assert actions == [ "send_to_warehouse", "wh_release", "assign", "start", "submit", "send_to_qc", "qc_accept", "file", ] assert pg_client2.get(f"/api/packets/{pid}/log").json()[0]["action"] == "Filed in the data book" # filed: every write is refused, Admin included r = pg_client2.patch(f"/api/packets/{pid}", json={"set": {"drawing": "D-2"}}) assert r.status_code == 403 and r.json()["code"] == "filed" r = pg_client2.patch(f"/api/packets/{pid}", json={"set": {"title": "x"}}) assert r.status_code == 403 and r.json()["code"] == "filed" as_user(pg_client2, "admin.ann") r = pg_client2.patch( f"/api/packets/{pid}", json={"set": {"drawing": "D-2"}, "it_reason": "Fix a typo in the drawing"} ) assert r.status_code == 403 and r.json()["code"] == "filed" # the data book date: Clerical and Q.C., not the warehouse for sam, code in (("clerk.clara", 200), ("qc.quinn", 200), ("wh.wade", 403)): as_user(pg_client2, sam) r = pg_client2.post(f"/api/packets/{pid}/databook", json={}) assert r.status_code == code, (sam, r.text) assert pg_client2.get(f"/api/packets/{pid}/seal").json()["valid"] is True # reopen by Admin with a reason and a signature breaks the seal as_user(pg_client2, "clerk.clara") r = pg_client2.post(f"/api/packets/{pid}/reopen", json={"reason": "Wrong heat number filed", "sign": SIGN}) assert r.status_code == 403 and r.json()["code"] == "forbidden" as_user(pg_client2, "admin.ann") r = pg_client2.post(f"/api/packets/{pid}/reopen", json={"reason": "short", "sign": SIGN}) assert r.status_code == 400 and r.json()["code"] == "reason_required" r = pg_client2.post(f"/api/packets/{pid}/reopen", json={"reason": "Wrong heat number filed"}) assert r.status_code == 400 and r.json()["code"] == "signature_required" r = pg_client2.post(f"/api/packets/{pid}/reopen", json={"reason": "Wrong heat number filed", "sign": SIGN}) assert r.status_code == 200, r.text assert r.json()["status"] == "complete" and r.json()["seal"]["broken_at"] is not None view = pg_client2.get(f"/api/packets/{pid}/seal").json() assert view["seal"] is None and view["seals"][0]["broken_reason"] == "Wrong heat number filed" def test_co_hold_blocks_filing(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None: as_user(pg_client2, "clerk.clara") pkt = make_draft(pg_client2, unit[1]) pid = pkt["id"] with app_conn(pg_settings) as conn: conn.execute("UPDATE packets SET status = 'complete', routed_to = 'shipping' WHERE id = %s", (pid,)) co = conn.execute( "INSERT INTO change_orders (packet_id, co_no, origin, review) VALUES (%s, 1, 'manual', 'approved')" " RETURNING id", (pid,), ).fetchone()[0] conn.execute("INSERT INTO co_required_depts (co_id, dept_code) VALUES (%s, '03')", (co,)) pkt = get(pg_client2, pid) assert pkt["open_cos"] == [1] and "file" in pkt["blocked"] r = step(pg_client2, pkt, "file", sign=SIGN) assert r.status_code == 403 and r.json()["code"] == "co_hold" assert r.json()["detail"] == "Change order 1 is waiting for verification by Weld Parts." as_user(pg_client2, "admin.ann") r = step(pg_client2, pkt, "file", sign=SIGN, it_reason="Close out the old job now") assert r.status_code == 403 and r.json()["code"] == "co_hold" card = pg_client2.get("/api/packets", params={"status": "complete"}).json()["packets"][0] assert card["open_cos"] == [1] and "file" in card["blocked"] def test_admin_bypass_needs_it_reason(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None: as_user(pg_client2, "clerk.clara") pkt = make_draft(pg_client2, unit[1]) as_user(pg_client2, "admin.ann") r = pg_client2.patch(f"/api/packets/{pkt['id']}", json={"set": {"title": "IT title"}}) assert r.status_code == 400 and r.json()["code"] == "it_reason_required" r = pg_client2.patch( f"/api/packets/{pkt['id']}", json={"set": {"title": "IT title"}, "it_reason": "Ticket 4411 fix"} ) assert r.status_code == 200 and r.json()["title"] == "IT title" log = pg_client2.get(f"/api/packets/{pkt['id']}/log").json()[0] assert log["it_reason"] == "Ticket 4411 fix" and log["by"]["display_name"] == "Ann Admin" au = _one(pg_settings, "SELECT it_reason, detail FROM audit_events WHERE action = 'packet.edit_header'") assert au[0] == "Ticket 4411 fix" and au[1]["fields"] == ["title"] def test_return_to_clerical_only_when_wh_skipped( pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int] ) -> None: as_user(pg_client2, "clerk.clara") pkt = ready_draft(pg_client2, unit[1]) r = step(pg_client2, pkt, "release_to_shop", dept="QC") assert r.status_code == 422 and r.json()["code"] == "invalid_dept" pkt = ok_step(pg_client2, pkt, "release_to_shop", dept="06", note="Rush job") assert pkt["status"] == "ready" and pkt["dept"]["code"] == "06" and pkt["wh_skipped"] is True assert pkt["stage"]["dept_code"] == "06" and pkt["return_note"] == "Rush job" as_user(pg_client2, "lead.lou") r = step(pg_client2, pkt, "return_to_clerical", reason="Not ours") assert r.status_code == 403 and r.json()["code"] == "forbidden" # lead.lou leads 03, the packet is in 06 with app_conn(pg_settings) as conn: conn.execute("UPDATE packets SET dept_code = '03' WHERE id = %s", (pkt["id"],)) pkt = get(pg_client2, pkt["id"]) r = step(pg_client2, pkt, "return_to_clerical", reason="short") assert r.status_code == 400 and r.json()["code"] == "reason_required" out = ok_step(pg_client2, pkt, "return_to_clerical", reason="Missing the second sheet") assert out["status"] == "draft" and out["return_note"] == "Missing the second sheet" and out["stage"] is None def test_assign_validation_and_draft_to_qc( pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int] ) -> None: as_user(pg_client2, "clerk.clara") pkt = ok_step(pg_client2, ready_draft(pg_client2, unit[1]), "release_to_shop", dept="03") with app_conn(pg_settings) as conn: other = conn.execute("SELECT id FROM stations WHERE dept_code = '06' LIMIT 1").fetchone()[0] andy = conn.execute("SELECT id FROM users WHERE sam_account = 'weld.andy'").fetchone()[0] as_user(pg_client2, "sup.wes") r = step(pg_client2, pkt, "assign", station_id=other, welder_id=str(andy)) assert r.status_code == 422 and r.json()["code"] == "invalid_station" r = step(pg_client2, pkt, "assign") assert r.status_code == 403 and r.json()["detail"] == "Pick a station and a welder." as_user(pg_client2, "clerk.clara") q = ok_step(pg_client2, ready_draft(pg_client2, unit[1]), "send_draft_to_qc", note="Inspect as is") assert q["status"] == "qc" and q["wh_skipped"] is True and q["qc"]["sent_note"] == "Inspect as is" assert q["holder"] == "Q.C." def test_move_and_remove(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None: from pkthelpers import add_unit with app_conn(pg_settings) as conn: u2 = add_unit(conn, unit[0], "002") u3 = add_unit(conn, unit[0], "7A", status="closed") as_user(pg_client2, "clerk.clara") pkt = make_draft(pg_client2, unit[1]) r = pg_client2.post(f"/api/packets/{pkt['id']}/move", json={"unit_id": u3}) assert r.status_code == 409 and r.json()["code"] == "unit_closed" r = pg_client2.post(f"/api/packets/{pkt['id']}/move", json={"unit_id": u2, "reason": "Belongs to unit two"}) assert r.status_code == 200 and r.json()["unit"]["id"] == u2 and r.json()["unit"]["lot"] == "002" assert pg_client2.get(f"/api/packets/{pkt['id']}/log").json()[0]["action"] == "Moved to another unit" assert _one(pg_settings, "SELECT 1 FROM audit_events WHERE action = 'packet.moved'") is not None r = pg_client2.request("DELETE", f"/api/packets/{pkt['id']}", json={"reason": "dup"}) assert r.status_code == 400 and r.json()["code"] == "reason_required" r = pg_client2.request("DELETE", f"/api/packets/{pkt['id']}", json={"reason": "Made twice by mistake"}) assert r.status_code == 200 and r.json()["removed"]["reason"] == "Made twice by mistake" assert get(pg_client2, pkt["id"])["removed"] is not None and get(pg_client2, pkt["id"])["can"] == [] assert pg_client2.get("/api/packets").json()["packets"] == [] r = pg_client2.patch(f"/api/packets/{pkt['id']}", json={"set": {"title": "x"}}) assert r.status_code == 403 and r.json()["code"] == "removed" assert pg_client2.get("/api/packets", params={"removed": 1}).json()["packets"] == [] as_user(pg_client2, "admin.ann") cards = pg_client2.get("/api/packets", params={"removed": 1}).json()["packets"] assert [c["id"] for c in cards] == [pkt["id"]] r = pg_client2.patch(f"/api/packets/{pkt['id']}", json={"set": {"title": "x"}, "it_reason": "Restore the title"}) assert r.status_code == 403 and r.json()["code"] == "removed" def test_list_filters_and_cards(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None: with app_conn(pg_settings) as conn: _, other_unit = make_unit(conn, number="L777") as_user(pg_client2, "clerk.clara") a = make_draft(pg_client2, unit[1], drawing="D-55", work_orders=[{"wo_base": "007123", "lot": "7A"}]) b = ready_draft(pg_client2, other_unit, title="Other one") ok_step(pg_client2, b, "send_to_warehouse") cards = pg_client2.get("/api/packets").json()["packets"] assert {c["id"] for c in cards} == {a["id"], b["id"]} card = next(c for c in cards if c["id"] == a["id"]) assert card["holder"] == "Clerical" and card["flag"] == "Made by hand - check it and send it on" assert card["can"] == [] and set(card["blocked"]) == {"send_to_warehouse", "release_to_shop", "send_draft_to_qc"} assert card["unit"]["id"] == unit[1] and card["dept"] == {"code": "03", "name": "Weld Parts"} assert card["wo_count"] == 1 and card["open_shorts"] == 0 and card["open_cos"] == [] bcard = next(c for c in cards if c["id"] == b["id"]) assert bcard["wh_stage"] == "incoming" and bcard["flag"] is None and bcard["holder"] == "Warehouse" def ids(**params: Any) -> list[int]: return [c["id"] for c in pg_client2.get("/api/packets", params=params).json()["packets"]] assert ids(status="warehouse") == [b["id"]] assert ids(status="draft,warehouse", unit_id=unit[1]) == [a["id"]] assert ids(q="d-55") == [a["id"]] and ids(q="007123") == [a["id"]] assert ids(q="L777 other") == [b["id"]] and ids(q="L777 D-55") == [] assert ids(flag="1") == [a["id"]] and ids(flag="0") == [b["id"]] as_user(pg_client2, "wh.wade") wcard = next(c for c in pg_client2.get("/api/packets", params={"status": "warehouse"}).json()["packets"]) assert set(wcard["can"]) == {"wh_send_back", "wh_release"} # transitions only, no edits def test_pm_reads_but_cannot_write(pg_client2: TestClient, unit: tuple[int, int]) -> None: as_user(pg_client2, "clerk.clara") pkt = make_draft(pg_client2, unit[1]) as_user(pg_client2, "pm.paula") view = get(pg_client2, pkt["id"]) assert view["can"] == [] and view["holder"] == "Clerical" assert pg_client2.get("/api/packets").json()["packets"][0]["id"] == pkt["id"] r = pg_client2.patch(f"/api/packets/{pkt['id']}", json={"set": {"title": "x"}}) assert r.status_code == 403 and r.json()["code"] == "forbidden" r = step(pg_client2, view, "send_to_warehouse") assert r.status_code == 403 and r.json()["code"] == "forbidden" def test_work_orders_put_and_docs(pg_client2: TestClient, unit: tuple[int, int]) -> None: as_user(pg_client2, "clerk.clara") pkt = make_draft(pg_client2, unit[1]) url = f"/api/packets/{pkt['id']}/work-orders" wos = {"work_orders": [{"wo_base": "0070001", "lot": "7A", "split": "", "sub": "01"}]} assert pg_client2.put(url, json=wos).status_code == 428 r = pg_client2.put(url, json=wos, headers={"If-Match": str(pkt["version"] + 5)}) assert r.status_code == 409 and r.json()["code"] == "version_conflict" r = pg_client2.put(url, json=wos, headers={"If-Match": str(pkt["version"])}) assert r.status_code == 200 wo = r.json()["work_orders"] assert [(w["wo_base"], w["lot"], w["sub"]) for w in wo] == [("0070001", "7A", "01")] a = upload(pg_client2, tiny_pdf(1)).json() r = pg_client2.post(f"/api/packets/{pkt['id']}/docs", json={"asset_id": a["id"], "kind": "mtr", "title": "MTR 1"}) assert r.status_code == 200 and r.json()["docs"][0]["title"] == "MTR 1" doc_id = r.json()["docs"][0]["id"] as_user(pg_client2, "wh.wade") r = pg_client2.request("DELETE", f"/api/packets/{pkt['id']}/docs/{doc_id}", json={}) assert r.status_code == 403 as_user(pg_client2, "qc.quinn") r = pg_client2.request("DELETE", f"/api/packets/{pkt['id']}/docs/{doc_id}", json={"reason": "wrong file"}) assert r.status_code == 200 and r.json()["docs"] == [] def test_seal_detects_tampering(pg_client2: TestClient, pg_settings: Settings, unit: tuple[int, int]) -> None: pkt = happy_path(pg_client2, pg_settings, unit[1]) with app_conn(pg_settings) as conn: conn.execute("UPDATE packets SET title = 'changed behind the app' WHERE id = %s", (pkt["id"],)) conn.execute("UPDATE bom_lines SET heat = 'X' WHERE packet_id = %s", (pkt["id"],)) conn.execute( "UPDATE packet_stages SET checklist = %s WHERE packet_id = %s", (Jsonb({"dwg": {"done": False}}), pkt["id"]) ) assert pg_client2.get(f"/api/packets/{pkt['id']}/seal").json()["valid"] is False