487 lines
26 KiB
Python
487 lines
26 KiB
Python
"""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
|