Files

247 lines
10 KiB
Python
Raw Permalink Normal View History

"""Helpers for the packet API tests: projects and units by SQL, hand drafts, PDFs, transitions, the happy path."""
from __future__ import annotations
import io
import secrets
from collections.abc import Iterator
from contextlib import contextmanager
from typing import Any
from fakedir import FAKE_PASSWORD, FakeDirectory
from fastapi.testclient import TestClient
from pshelpers import ORIGIN_HEADERS, app_conn, sign_in
from psycopg.types.json import Jsonb
from packetstation.domain import checklists
from packetstation.settings import Settings
SIGN = {"factor": "password", "secret": FAKE_PASSWORD}
USERS = ("admin.ann", "clerk.clara", "wh.wade", "qc.quinn", "pm.paula", "weld.andy", "lead.lou", "sup.wes", "dup.dave")
@contextmanager
def phase2_client(settings: Settings, fake_dir: FakeDirectory) -> Iterator[TestClient]:
from packetstation.main import create_app
app = create_app(settings, directory=fake_dir) # main.py registers every phase 2 router
with TestClient(app, headers=ORIGIN_HEADERS) as client:
yield client
def seed_users(client: TestClient, users: tuple[str, ...] = USERS) -> None:
"""Sign each test user in once so their users rows exist."""
for sam in users:
assert sign_in(client, sam).status_code == 200, sam
def as_user(client: TestClient, sam: str) -> None:
r = sign_in(client, sam)
assert r.status_code == 200, r.text
def user_id(conn: Any, sam: str) -> str:
row = conn.execute("SELECT id FROM users WHERE sam_account = %s", (sam,)).fetchone()
assert row is not None, sam
return str(row[0])
def station_id(conn: Any, dept: str = "03", name: str = "Stall 1") -> int:
row = conn.execute("SELECT id FROM stations WHERE dept_code = %s AND name = %s", (dept, name)).fetchone()
assert row is not None
return int(row[0])
def make_unit(conn: Any, *, number: str | None = None, lot: str = "001", status: str = "open") -> tuple[int, int]:
"""A project and one unit by SQL. Returns (project_id, unit_id)."""
number = number or "L9" + secrets.token_hex(3).upper()
pid = conn.execute(
"INSERT INTO projects (number, name, customer, po) VALUES (%s, %s, %s, %s) RETURNING id",
(number, "Test project", "Acme Refining", "PO-77"),
).fetchone()[0]
uid = conn.execute(
"INSERT INTO units (project_id, lot, name, status) VALUES (%s, %s, %s, %s::project_status) RETURNING id",
(pid, lot, "Unit one", status),
).fetchone()[0]
return int(pid), int(uid)
def add_unit(conn: Any, project_id: int, lot: str, status: str = "open") -> int:
return int(
conn.execute(
"INSERT INTO units (project_id, lot, status) VALUES (%s, %s, %s::project_status) RETURNING id",
(project_id, lot, status),
).fetchone()[0]
)
def draft_body(unit_id: int, **over: Any) -> dict[str, Any]:
body: dict[str, Any] = {
"unit_id": unit_id,
"dept_code": "03",
"op": "WELD-PART",
"title": "Spool 4 inlet",
"customer": "Acme Refining",
"drawing": "D-1001",
"rev": "B",
"wps": "WPS-11",
"work_orders": [
{"wo_base": "L670001", "lot": "001", "split": "0", "sub": "0", "qty": 2, "target_hrs": 1.5},
],
"bom": [],
}
body.update(over)
return body
def make_draft(client: TestClient, unit_id: int, **over: Any) -> dict[str, Any]:
"""A hand draft through the API as whoever is signed in (Clerical). Returns the read model."""
r = client.post("/api/packets", json=draft_body(unit_id, **over))
assert r.status_code == 201, r.text
out: dict[str, Any] = r.json()
return out
def tiny_pdf(pages: int = 1) -> bytes:
from pypdf import PdfWriter
w = PdfWriter()
for _ in range(pages):
w.add_blank_page(width=612, height=792)
buf = io.BytesIO()
w.write(buf)
return buf.getvalue()
def upload(client: TestClient, data: bytes, name: str = "drawing.pdf", mime: str = "application/pdf") -> Any:
return client.post("/api/assets", files={"file": (name, data, mime)})
def attach_drawing(client: TestClient, packet_id: int, pages: int = 1) -> dict[str, Any]:
a = upload(client, tiny_pdf(pages))
assert a.status_code == 201, a.text
r = client.post(
f"/api/packets/{packet_id}/pages", json={"asset_id": a.json()["id"], "type": "drawing", "sign": SIGN}
)
assert r.status_code == 200, r.text
out: dict[str, Any] = r.json()
return out
def set_header(client: TestClient, packet_id: int, **fields: Any) -> Any:
return client.patch(f"/api/packets/{packet_id}", json={"set": fields, "base": {}})
def ready_draft(client: TestClient, unit_id: int, **over: Any) -> dict[str, Any]:
"""A hand draft with the heat map answered and a drawing attached: it can leave Clerical."""
pkt = make_draft(client, unit_id, **over)
assert set_header(client, pkt["id"], heat_map=True).status_code == 200
return attach_drawing(client, pkt["id"])
def step(client: TestClient, pkt: dict[str, Any], action: str, **fields: Any) -> Any:
"""POST a transition with the expect taken from the packet answer."""
body = {"action": action, "expect": {"status": pkt["status"], "dept": pkt["dept"]["code"]}, **fields}
return client.post(f"/api/packets/{pkt['id']}/transitions", json=body)
def ok_step(client: TestClient, pkt: dict[str, Any], action: str, **fields: Any) -> dict[str, Any]:
r = step(client, pkt, action, **fields)
assert r.status_code == 200, r.text
out: dict[str, Any] = r.json()
return out
def get(client: TestClient, packet_id: int) -> dict[str, Any]:
r = client.get(f"/api/packets/{packet_id}")
assert r.status_code == 200, r.text
out: dict[str, Any] = r.json()
return out
def finish_stage_by_sql(settings: Settings, packet_id: int) -> None:
"""The checklist, filler and one weld, which the child endpoints (built later) would write."""
keys = checklists.checklist_keys(True)
with app_conn(settings) as conn:
conn.execute(
"UPDATE packet_stages SET checklist = %s, filler = 'ER70S-6' WHERE packet_id = %s AND closed_at IS NULL",
(Jsonb({k: {"done": True, "stamp": "A7"} for k in keys}), packet_id),
)
conn.execute(
"INSERT INTO welds (packet_id, weld_no, items, type, size) VALUES (%s, 1, '{1}', 'Fillet', '1/4')",
(packet_id,),
)
def finish_qc_by_sql(settings: Settings, packet_id: int) -> None:
with app_conn(settings) as conn:
conn.execute(
"UPDATE qc_inspections SET checks = %s, weld_exam = %s, inspector = 'Quinn QC'"
" WHERE packet_id = %s AND closed_at IS NULL",
(Jsonb({k: {"done": True} for k in checklists.qc_keys()}), Jsonb({"1": {"result": "accept"}}), packet_id),
)
def happy_path(client: TestClient, settings: Settings, unit_id: int) -> dict[str, Any]:
"""Draft -> warehouse -> ready -> assigned -> in progress -> review -> Q.C. -> complete (data book) -> filed."""
with app_conn(settings) as conn:
stall = station_id(conn)
andy = user_id(conn, "weld.andy")
as_user(client, "clerk.clara")
pkt = ready_draft(client, unit_id)
pkt = ok_step(client, pkt, "send_to_warehouse")
assert pkt["status"] == "warehouse" and pkt["wh"]["stage"] == "incoming"
as_user(client, "wh.wade")
pkt = ok_step(client, pkt, "wh_release")
assert pkt["status"] == "ready" and pkt["stage"]["dept_code"] == "03" and pkt["wh"] is None
as_user(client, "lead.lou")
r = step(client, pkt, "return_to_clerical", reason="Wrong department for this one")
assert r.status_code == 403 and r.json()["code"] == "wrong_status", r.text
as_user(client, "sup.wes")
pkt = ok_step(client, pkt, "assign", station_id=stall, welder_id=andy)
assert pkt["status"] == "assigned" and pkt["station"]["name"] == "Stall 1"
assert pkt["welder"]["id"] == andy and pkt["stage"]["welder"]["id"] == andy
as_user(client, "dup.dave")
r = step(client, pkt, "start")
assert r.status_code == 403 and r.json()["code"] == "forbidden", r.text
as_user(client, "weld.andy")
pkt = ok_step(client, pkt, "start")
assert pkt["status"] == "in_progress" and pkt["stage"]["clock"]["running_since"] is not None
r = step(client, pkt, "submit", sign=SIGN)
assert r.status_code == 403 and r.json()["code"] == "precondition", r.text
finish_stage_by_sql(settings, pkt["id"])
r = step(client, pkt, "submit")
assert r.status_code == 400 and r.json()["code"] == "signature_required", r.text
pkt = ok_step(client, pkt, "submit", sign=SIGN)
assert pkt["status"] == "review" and pkt["stage"]["clock"]["running_since"] is None
as_user(client, "sup.wes")
r = step(client, pkt, "send_to_qc", book=True, spool=True, sign=SIGN)
assert r.status_code == 403 and r.json()["code"] == "precondition", r.text
pkt = ok_step(client, pkt, "send_to_qc", book=True, spool=True, location="Bay 3 rack B", sign=SIGN)
assert pkt["status"] == "qc" and pkt["qc"]["location"] == "Bay 3 rack B" and pkt["stage"] is not None
as_user(client, "qc.quinn")
r = step(client, pkt, "qc_accept", routed_to="databook", sign=SIGN)
assert r.status_code == 403 and r.json()["code"] == "precondition", r.text
assert "Q.C. still needs" in r.json()["detail"]
finish_qc_by_sql(settings, pkt["id"])
pkt = ok_step(client, pkt, "qc_accept", routed_to="databook", sign=SIGN)
assert pkt["status"] == "complete" and pkt["routed_to"] == "databook" and pkt["qc"] is None
assert pkt["stage"] is None and len(pkt["stages"]) == 1 and pkt["qc_history"][0]["result"] == "accepted"
as_user(client, "clerk.clara")
r = step(client, pkt, "file", sign=SIGN)
assert r.status_code == 403 and r.json()["code"] == "precondition"
assert r.json()["detail"] == "Close the job in VISUAL first."
r = client.post(f"/api/packets/{pkt['id']}/vclosed", json={"closed": True})
assert r.status_code == 200 and r.json()["vclosed"]["by"]["display_name"] == "Clara Clerk", r.text
pkt = ok_step(client, r.json(), "file", sign=SIGN)
assert pkt["status"] == "filed" and pkt["seal"]["broken_at"] is None and len(pkt["seal"]["key_id"]) == 8
return pkt