"""The policy matrix: every role against every status for every transition, then the rules one by one. The expected table below is written from the rule table in the phase 2 design contract, not from the code. """ from __future__ import annotations import dataclasses from types import MappingProxyType from typing import Any import pytest from packetstation.policy import ( RULES, STATUSES, Action, PacketState, Principal, can, check, holder_name, holds, principal_from_roles, works_on, ) from packetstation.policy.preconditions import INPUT_ONLY, MESSAGES A = Action WELDER = "00000000-0000-0000-0000-00000000a11d" OTHER = "00000000-0000-0000-0000-0000000000b0" IT = "Ticket 4411: fix a typo" # Every input a step can need. The default state satisfies every state precondition. FULL_EXTRA: dict[str, Any] = { "reason": "Needs another look at the fit-up.", "station_id": 1, "welder_id": WELDER, "dept": "06", "routed_to": "databook", "book": True, "spool": True, "location": "Rack 4", } def P(roles: list[str], user_id: str = OTHER, limits: tuple[int, ...] = ()) -> Principal: return principal_from_roles(user_id, roles, station_limits=limits) ROLES: dict[str, Principal] = { "admin": P(["admin"]), "clerical": P(["clerical"]), "clerical_sup": P(["clerical_supervisor"]), "warehouse": P(["warehouse"]), "warehouse_sup": P(["warehouse_supervisor"]), "qc": P(["qc"]), "qc_sup": P(["qc_supervisor"]), "pm": P(["pm"]), "worker03": P(["shop:03:worker"]), "lead03": P(["shop:03:lead"]), "sup03": P(["shop:03:supervisor"]), "worker06": P(["shop:06:worker"], user_id=WELDER), # the assigned welder, but limited to a station that is not the packet's "worker03_limited": P(["shop:03:worker"], user_id=WELDER, limits=(99,)), "welder": P(["shop:03:worker"], user_id=WELDER), } def state(status: str, **kw: Any) -> PacketState: base = PacketState( id=1, status=status, dept="03", station_id=1, welder_id=WELDER, wh_skipped=True, routed_to="databook", vclosed=True, removed=False, source_kind="hand", weld_dept=True, co_hold=False, drawing_attached=True, heat_map=True, bom_total=3, bom_ready=3, welds=2, checklist_complete=True, filler_set=True, open_shorts=1, ) return dataclasses.replace(base, **kw) CL = {"clerical", "clerical_sup"} WH = {"warehouse", "warehouse_sup"} QC = {"qc", "qc_sup"} LEADS = {"lead03", "sup03"} SUP = {"sup03"} # action -> (from statuses, roles allowed). From the contract rule table. EXPECTED: dict[Action, tuple[set[str], set[str]]] = { A.SEND_TO_WAREHOUSE: ({"draft"}, CL), A.RELEASE_TO_SHOP: ({"draft"}, CL), A.SEND_DRAFT_TO_QC: ({"draft"}, CL), A.WH_SEND_BACK: ({"warehouse"}, WH), A.RETURN_TO_CLERICAL: ({"ready"}, LEADS), A.WH_RELEASE: ({"warehouse"}, WH), A.SEND_BACK_TO_WAREHOUSE: ({"ready"}, LEADS), A.ASSIGN: ({"ready"}, LEADS), A.UNASSIGN: ({"assigned"}, LEADS), A.PULL_BACK: ({"in_progress"}, LEADS), A.START: ({"assigned"}, {*LEADS, "welder"}), A.SUBMIT: ({"assigned", "in_progress"}, {*LEADS, "welder"}), A.RETURN_TO_STATION: ({"review"}, SUP), A.SEND_TO_QC: ({"review"}, SUP), A.ROUTE_FROM_REVIEW: ({"review"}, SUP), A.QC_REJECT: ({"qc"}, QC), A.QC_ACCEPT: ({"qc"}, QC), A.QC_ACCEPT_AND_ROUTE: ({"qc"}, QC), A.FILE: ({"complete"}, CL), A.ADMIN_REOPEN: ({"filed"}, {"admin"}), } def expected(role: str, status: str, action: Action) -> str: """'' when the step passes, else the expected code.""" froms, allowed = EXPECTED[action] if status not in froms: return "wrong_status" if role in allowed: return "" return "it_reason_required" if role == "admin" else "forbidden" MATRIX = [(r, s, a) for r in ROLES for s in STATUSES for a in EXPECTED] def test_expected_table_covers_every_user_transition() -> None: transitions = {a for a, r in RULES.items() if r.to_status is not None and not r.system_only} assert transitions == set(EXPECTED) @pytest.mark.parametrize(("role", "status", "action"), MATRIX, ids=[f"{r}-{s}-{a}" for r, s, a in MATRIX]) def test_matrix(role: str, status: str, action: Action) -> None: d = check(ROLES[role], state(status), action, extra=FULL_EXTRA) want = expected(role, status, action) assert d.ok is (want == ""), (d.code, d.why) assert d.code == want if d.ok: assert d.signature == RULES[action].signature assert d.admin_bypass is False # ---------- custody ---------- HOLDERS: dict[str, set[str]] = { "draft": {"clerical", "clerical_sup"}, "warehouse": {"warehouse", "warehouse_sup"}, "ready": {"lead03", "sup03"}, "assigned": {"lead03", "sup03", "worker03", "welder"}, "in_progress": {"lead03", "sup03", "worker03", "welder"}, "review": {"lead03", "sup03"}, "qc": {"qc", "qc_sup"}, "complete": {"clerical", "clerical_sup"}, "filed": set(), } @pytest.mark.parametrize("status", STATUSES) def test_custody_per_status(status: str) -> None: got = {name for name, p in ROLES.items() if holds(p, state(status))} assert got == HOLDERS[status] def test_holder_name() -> None: assert holder_name(state("draft")) == "Clerical" assert holder_name(state("warehouse")) == "Warehouse" assert holder_name(state("qc")) == "Q.C." assert holder_name(state("complete")) == "Clerical (data book)" assert holder_name(state("filed")) == "the data book (filed)" assert holder_name(state("ready"), "Weld Parts") == "Weld Parts" assert holder_name(state("assigned"), "Weld Parts", "Stall 1") == "Weld Parts ยท Stall 1" assert holder_name(state("review"), "Weld Parts", "Stall 1") == "Weld Parts" assert holder_name(state("ready")) == "03" def test_works_on_station_limits() -> None: s = state("assigned", station_id=4) assert works_on(P(["shop:03:worker"]), s) assert works_on(P(["shop:03:worker"], limits=(4, 5)), s) assert not works_on(P(["shop:03:worker"], limits=(5,)), s) assert not works_on(P(["shop:06:worker"]), s) # leads and supervisors ignore station limits assert works_on(P(["shop:03:lead"], limits=(5,)), s) assert works_on(P(["shop:03:supervisor"], limits=(5,)), s) # a limited worker with no station on the packet does not work on it assert not works_on(P(["shop:03:worker"], limits=(5,)), state("assigned", station_id=None)) @pytest.mark.parametrize("action", [A.START, A.SUBMIT]) def test_start_and_submit_need_the_assigned_welder_for_a_worker(action: Action) -> None: s = state("assigned") assert check(P(["shop:03:worker"], user_id=WELDER), s, action).ok other = check(P(["shop:03:worker"], user_id=OTHER), s, action) assert (other.ok, other.code) == (False, "forbidden") assert other.why == "Your access doesn't allow this step." unassigned = check(P(["shop:03:worker"], user_id=WELDER), state("assigned", welder_id=None), action) assert unassigned.code == "forbidden" limited = check(P(["shop:03:worker"], user_id=WELDER, limits=(2,)), s, action) assert limited.code == "forbidden" # a lead does not need to be the welder assert check(P(["shop:03:lead"], user_id=OTHER), s, action).ok assert check(P(["shop:03:lead"], user_id=OTHER), state("assigned", welder_id=None), action).ok def test_worker_who_is_not_the_welder_still_holds_for_edits() -> None: s = state("in_progress") worker = P(["shop:03:worker"], user_id=OTHER) assert check(worker, s, A.EDIT_WELDS).ok assert check(worker, s, A.CLOCK).ok assert check(P(["shop:03:worker"], limits=(9,)), s, A.EDIT_WELDS).code == "forbidden" def test_leads_write_shop_fields_only_while_they_hold() -> None: lead = ROLES["lead03"] for status in ("ready", "assigned", "in_progress", "review"): assert check(lead, state(status), A.EDIT_WELDS).ok d = check(ROLES["lead03"], state("qc"), A.EDIT_WELDS) assert d.code == "wrong_status" other_dept = check(P(["shop:06:lead"]), state("review"), A.EDIT_BOM) assert other_dept.code == "forbidden" assert other_dept.why == "Your access doesn't allow changing the materials on this packet. It's with 03 right now." named = check(P(["shop:06:lead"]), state("review"), A.EDIT_BOM, extra={"holder_name": "Weld Parts"}) assert named.why.endswith("It's with Weld Parts right now.") # ---------- change-order hold ---------- CO_HOLD_ACTIONS = {a for a, r in RULES.items() if r.co_hold} def test_co_hold_set_is_the_contract_set() -> None: assert { A.WH_RELEASE, A.SUBMIT, A.SEND_TO_QC, A.ROUTE_FROM_REVIEW, A.QC_ACCEPT, A.QC_ACCEPT_AND_ROUTE, A.FILE, } == CO_HOLD_ACTIONS @pytest.mark.parametrize("action", list(EXPECTED)) def test_co_hold_blocks_exactly_the_co_hold_actions(action: Action) -> None: froms, allowed = EXPECTED[action] status = sorted(froms)[0] role = sorted(allowed)[0] why = "Change order 2 must be verified first by: WH." d = check(ROLES[role], state(status, co_hold=True, co_hold_why=why), action, extra=FULL_EXTRA) if action in CO_HOLD_ACTIONS: assert (d.ok, d.code, d.why) == (False, "co_hold", why) else: assert d.ok, (d.code, d.why) def test_co_hold_applies_to_admin() -> None: d = check(ROLES["admin"], state("warehouse", co_hold=True), A.WH_RELEASE, it_reason=IT) assert (d.code, d.why) == ("co_hold", "A change order holds this step.") # ---------- admin bypass ---------- def test_admin_bypass_needs_an_it_reason() -> None: admin = ROLES["admin"] d = check(admin, state("draft"), A.SEND_TO_WAREHOUSE) assert (d.ok, d.code) == (False, "it_reason_required") short = check(admin, state("draft"), A.SEND_TO_WAREHOUSE, it_reason="too sh") assert short.code == "it_reason_required" ok = check(admin, state("draft"), A.SEND_TO_WAREHOUSE, it_reason=IT) assert ok.ok and ok.admin_bypass def test_admin_bypass_passes_custody_and_is_marked() -> None: admin = ROLES["admin"] for status in ("ready", "assigned", "in_progress", "review"): d = check(admin, state(status), A.EDIT_WELDS, it_reason=IT) assert d.ok and d.admin_bypass d = check(admin, state("in_progress"), A.SUBMIT, it_reason=IT) assert d.ok and d.admin_bypass and d.signature == "Submit work to the supervisor" def test_admin_who_also_holds_is_not_a_bypass() -> None: both = P(["admin", "clerical"]) d = check(both, state("draft"), A.EDIT_HEADER) assert d.ok and not d.admin_bypass def test_admin_reopen_is_admin_only_and_not_a_bypass() -> None: d = check(ROLES["admin"], state("filed"), A.ADMIN_REOPEN, extra=FULL_EXTRA) assert d.ok and not d.admin_bypass and d.signature == "Reopen a filed packet" for name, p in ROLES.items(): if name != "admin": assert check(p, state("filed"), A.ADMIN_REOPEN, extra=FULL_EXTRA).code == "forbidden" def test_admin_set_databook_at_needs_no_reason() -> None: d = check(ROLES["admin"], state("filed"), A.SET_DATABOOK_AT) assert d.ok and d.admin_bypass def test_itp_reopen_qc_supervisor_or_admin_with_it_reason() -> None: extra = {"reason": "Wrong line signed by mistake."} assert check(ROLES["qc_sup"], state("complete"), A.ITP_REOPEN, extra=extra).ok assert check(ROLES["qc"], state("complete"), A.ITP_REOPEN, extra=extra).code == "forbidden" assert check(ROLES["admin"], state("complete"), A.ITP_REOPEN, extra=extra).code == "it_reason_required" assert check(ROLES["admin"], state("complete"), A.ITP_REOPEN, extra=extra, it_reason=IT).ok # ---------- filed lock and removed ---------- @pytest.mark.parametrize("action", list(RULES)) def test_filed_lock(action: Action) -> None: s = state("filed") for name, p in ROLES.items(): d = check(p, s, action, extra={**FULL_EXTRA, "dept": "03"}, it_reason=IT) if action is A.SET_DATABOOK_AT and name in {"admin", "clerical", "clerical_sup", "qc", "qc_sup"}: assert d.ok, (name, d.code) elif action is A.ADMIN_REOPEN and name == "admin": assert d.ok else: assert not d.ok, (name, action) if not RULES[action].from_status and not RULES[action].system_only and name == "admin": assert d.code == "filed" assert d.why == "This packet is filed and locked. IT can reopen it if a correction is needed." @pytest.mark.parametrize("action", list(RULES)) def test_removed_refuses_everything(action: Action) -> None: for status in STATUSES: s = state(status, removed=True) for p in [*ROLES.values(), Principal(user_id=None, system="visual-feed")]: d = check(p, s, action, extra=FULL_EXTRA, it_reason=IT) assert (d.ok, d.code) == (False, "removed") def test_unknown_status_is_wrong_status() -> None: for action in RULES: d = check(ROLES["admin"], state("shipped"), action, extra=FULL_EXTRA, it_reason=IT) assert d.code == "wrong_status" allowed, blocked = can(ROLES["clerical"], state("shipped")) assert allowed == [] and blocked == {} # ---------- preconditions ---------- # (action, status, role, state change or extra change, precondition name) PRECONDITION_CASES: list[tuple[Action, str, str, dict[str, Any], dict[str, Any], str]] = [ (A.SEND_TO_WAREHOUSE, "draft", "clerical", {"heat_map": None}, {}, "heat_map_answered"), (A.SEND_TO_WAREHOUSE, "draft", "clerical", {"drawing_attached": False}, {}, "drawing_attached"), (A.SEND_DRAFT_TO_QC, "draft", "clerical", {"drawing_attached": False}, {}, "drawing_attached"), (A.RELEASE_TO_SHOP, "draft", "clerical", {}, {"dept": ""}, "target_dept_given"), (A.WH_RELEASE, "warehouse", "warehouse", {"bom_ready": 2}, {}, "bom_all_pulled_or_short"), (A.WH_RELEASE, "warehouse", "warehouse", {"bom_missing_heat": 1}, {}, "heats_recorded"), (A.ASSIGN, "ready", "lead03", {}, {"welder_id": None}, "welder_set"), (A.SUBMIT, "in_progress", "welder", {"checklist_complete": False}, {}, "checklist_complete"), (A.SUBMIT, "in_progress", "welder", {"welds": 0}, {}, "welds_recorded"), (A.SUBMIT, "in_progress", "welder", {"filler_set": False}, {}, "filler_set"), (A.SEND_TO_QC, "review", "sup03", {}, {"spool": False}, "qc_book_spool_location"), (A.SEND_TO_QC, "review", "sup03", {}, {"location": " "}, "qc_book_spool_location"), (A.ROUTE_FROM_REVIEW, "review", "sup03", {}, {"dept": "03"}, "target_dept_given"), (A.QC_ACCEPT, "qc", "qc", {"qc_missing": ("weld exam",)}, {}, "qc_missing_empty"), (A.QC_ACCEPT, "qc", "qc", {}, {"routed_to": "scrap"}, "route_given"), (A.QC_ACCEPT_AND_ROUTE, "qc", "qc", {}, {"dept": "WH"}, "target_dept_given"), (A.FILE, "complete", "clerical", {"vclosed": False}, {}, "vclosed_if_databook"), (A.WH_SEND_BACK, "warehouse", "warehouse", {}, {"reason": "short"}, "reason_given"), (A.QC_REJECT, "qc", "qc", {}, {"reason": None}, "reason_given"), (A.REMOVE, "draft", "clerical", {}, {"reason": " x "}, "reason_given"), (A.WH_RECEIVE_SHORT, "ready", "warehouse", {"open_shorts": 0}, {}, "short_open"), ] @pytest.mark.parametrize( ("action", "status", "role", "change", "extra", "name"), PRECONDITION_CASES, ids=[f"{c[0]}-{c[5]}" for c in PRECONDITION_CASES], ) def test_each_precondition_fails_alone( action: Action, status: str, role: str, change: dict[str, Any], extra: dict[str, Any], name: str ) -> None: s = state(status, **change) x = {**FULL_EXTRA, **extra} assert check(ROLES[role], state(status), action, extra=FULL_EXTRA).ok d = check(ROLES[role], s, action, extra=x) want = "reason_required" if name == "reason_given" else "precondition" assert (d.ok, d.code) == (False, want) if name == "qc_missing_empty": assert d.why == "Q.C. still needs: weld exam." elif name == "target_dept_given" and x["dept"] in ("", "WH"): assert d.why == "Choose a department." else: assert d.why == MESSAGES[name] # never bypassed by admin a = check(ROLES["admin"], s, action, extra=x, it_reason=IT) assert (a.ok, a.code, a.why) == (False, want, d.why) def test_every_named_precondition_is_tested() -> None: used = {n for r in RULES.values() for n in r.preconditions} assert used <= {c[5] for c in PRECONDITION_CASES} def test_filler_and_welds_only_in_weld_departments() -> None: s = state("in_progress", weld_dept=False, welds=0, filler_set=False) assert check(ROLES["welder"], s, A.SUBMIT).ok def test_vclosed_only_needed_for_the_data_book_route() -> None: assert check(ROLES["clerical"], state("complete", routed_to="shipping", vclosed=False), A.FILE).ok def test_co_staged_if_dwg_change() -> None: from packetstation.policy.preconditions import co_staged_if_dwg_change s = state("draft") assert co_staged_if_dwg_change(s, {}) is None assert co_staged_if_dwg_change(s, {"dwg_change": True, "staged": True}) is None assert co_staged_if_dwg_change(s, {"dwg_change": True}) == MESSAGES["co_staged_if_dwg_change"] # ---------- specific rules ---------- @pytest.mark.parametrize("status", [s for s in STATUSES if s != "filed"]) def test_wh_receive_short_passes_in_every_non_filed_status(status: str) -> None: assert check(ROLES["warehouse"], state(status), A.WH_RECEIVE_SHORT).ok assert check(ROLES["warehouse_sup"], state(status), A.WH_RECEIVE_SHORT).ok assert check(ROLES["lead03"], state(status), A.WH_RECEIVE_SHORT).code == "forbidden" def test_wh_receive_short_refused_when_filed() -> None: assert check(ROLES["warehouse"], state("filed"), A.WH_RECEIVE_SHORT).code == "filed" def test_wh_receive_short_needs_an_open_short() -> None: d = check(ROLES["warehouse"], state("ready", open_shorts=0), A.WH_RECEIVE_SHORT) assert d.code == "precondition" and d.why == MESSAGES["short_open"] allowed, blocked = can(ROLES["warehouse"], state("draft", open_shorts=0)) assert allowed == [] and blocked == {A.WH_RECEIVE_SHORT: [MESSAGES["short_open"]]} def test_return_to_clerical_needs_wh_skipped() -> None: assert check(ROLES["lead03"], state("ready"), A.RETURN_TO_CLERICAL, extra=FULL_EXTRA).ok d = check(ROLES["lead03"], state("ready", wh_skipped=False), A.RETURN_TO_CLERICAL, extra=FULL_EXTRA) assert d.code == "wrong_status" assert "warehouse" in d.why def test_set_vclosed_needs_the_data_book_route() -> None: assert check(ROLES["clerical"], state("complete"), A.SET_VCLOSED).ok for route in ("shipping", None): d = check(ROLES["clerical"], state("complete", routed_to=route), A.SET_VCLOSED) assert d.code == "wrong_status" assert check(ROLES["qc"], state("complete"), A.SET_VCLOSED).code == "forbidden" @pytest.mark.parametrize("status", STATUSES) def test_set_databook_at_is_clerical_or_qc(status: str) -> None: s = state(status) for role in ("clerical", "clerical_sup", "qc", "qc_sup"): assert check(ROLES[role], s, A.SET_DATABOOK_AT).ok for role in ("warehouse", "warehouse_sup", "sup03", "lead03", "pm", "welder"): assert check(ROLES[role], s, A.SET_DATABOOK_AT).code == "forbidden" def test_rev_approve_needs_clerical_supervisor() -> None: extra = {"reason": "Drafting confirmed rev C."} assert check(ROLES["clerical_sup"], state("draft"), A.REV_APPROVE, extra=extra).ok assert check(ROLES["clerical"], state("draft"), A.REV_APPROVE, extra=extra).code == "forbidden" def test_co_verify_needs_a_supervisor_of_the_named_department() -> None: s = state("in_progress") assert check(ROLES["sup03"], s, A.CO_VERIFY, extra={"dept": "03"}).ok assert check(ROLES["sup03"], s, A.CO_VERIFY, extra={"dept": "06"}).code == "forbidden" assert check(ROLES["lead03"], s, A.CO_VERIFY, extra={"dept": "03"}).code == "forbidden" assert check(ROLES["warehouse_sup"], s, A.CO_VERIFY, extra={"dept": "WH"}).ok assert check(ROLES["warehouse"], s, A.CO_VERIFY, extra={"dept": "WH"}).code == "forbidden" assert check(ROLES["qc_sup"], s, A.CO_VERIFY, extra={"dept": "QC"}).ok assert check(ROLES["clerical_sup"], s, A.CO_VERIFY, extra={"dept": "CL"}).code == "forbidden" assert check(ROLES["sup03"], s, A.CO_VERIFY).code == "forbidden" @pytest.mark.parametrize("action", list(RULES)) def test_pm_can_do_nothing(action: Action) -> None: pm = ROLES["pm"] for status in STATUSES: assert not check(pm, state(status), action, extra=FULL_EXTRA).ok for status in STATUSES: assert can(pm, state(status)) == ([], {}) SYSTEM = Principal(user_id=None, system="visual-feed") VIS = {A.VIS_UPDATE_DRAFT, A.VIS_OPEN_CO, A.VIS_AUTO_RELEASE, A.VIS_RETIRE_DRAFT} VIS_FROM = { A.VIS_UPDATE_DRAFT: {"draft"}, A.VIS_RETIRE_DRAFT: {"draft"}, A.VIS_AUTO_RELEASE: {"draft"}, A.VIS_OPEN_CO: set(STATUSES) - {"draft", "filed"}, } @pytest.mark.parametrize("action", list(RULES)) def test_system_principal(action: Action) -> None: assert {a for a, r in RULES.items() if r.system_only} == VIS for status in STATUSES: d = check(SYSTEM, state(status), action) if action in VIS: assert d.ok is (status in VIS_FROM[action]), (status, d.code) if not d.ok: assert d.code == "wrong_status" elif not d.ok and d.code != "wrong_status": assert d.code == "forbidden" else: assert not d.ok assert can(SYSTEM, state("draft")) == ([], {}) @pytest.mark.parametrize("action", sorted(VIS)) def test_people_cannot_do_vis_actions(action: Action) -> None: s = state(sorted(VIS_FROM[action])[0]) for p in ROLES.values(): d = check(p, s, action, it_reason=IT) assert (d.ok, d.code) == (False, "system_only") def test_vis_auto_release_preconditions() -> None: d = check(SYSTEM, state("draft", heat_map=None), A.VIS_AUTO_RELEASE) assert (d.code, d.why) == ("precondition", MESSAGES["heat_map_answered"]) # ---------- can and blocked ---------- def test_can_and_blocked_on_a_bare_draft() -> None: s = state("draft", heat_map=None, drawing_attached=False) allowed, blocked = can(ROLES["clerical"], s) reasons = [MESSAGES["heat_map_answered"], MESSAGES["drawing_attached"]] assert blocked[A.SEND_TO_WAREHOUSE] == reasons assert blocked[A.SEND_DRAFT_TO_QC] == reasons assert blocked[A.RELEASE_TO_SHOP] == reasons assert A.SEND_TO_WAREHOUSE not in allowed assert {A.EDIT_HEADER, A.EDIT_BOM, A.EDIT_WOS, A.REMOVE, A.CO_ISSUE, A.DOC_ADD} <= set(allowed) assert A.REV_APPROVE not in allowed # clerical supervisor only assert all(not RULES[a].system_only for a in allowed) wh_allowed, wh_blocked = can(ROLES["warehouse"], s) assert A.SEND_TO_WAREHOUSE not in wh_allowed assert A.SEND_TO_WAREHOUSE not in wh_blocked assert wh_allowed == [A.WH_RECEIVE_SHORT] def test_can_skips_input_only_preconditions() -> None: assert {"welder_set", "target_dept_given", "route_given", "reason_given"} <= INPUT_ONLY allowed, _ = can(ROLES["lead03"], state("ready")) assert {A.ASSIGN, A.SEND_BACK_TO_WAREHOUSE, A.RETURN_TO_CLERICAL} <= set(allowed) allowed, _ = can(ROLES["sup03"], state("review")) assert {A.SEND_TO_QC, A.ROUTE_FROM_REVIEW, A.RETURN_TO_STATION} <= set(allowed) allowed, _ = can(ROLES["qc"], state("qc")) assert {A.QC_ACCEPT, A.QC_ACCEPT_AND_ROUTE, A.QC_REJECT} <= set(allowed) def test_blocked_by_co_hold_only_after_who_passes() -> None: s = state("warehouse", co_hold=True, co_hold_why="Held for CO 1.", bom_ready=1) _, blocked = can(ROLES["warehouse"], s) assert blocked[A.WH_RELEASE] == ["Held for CO 1.", MESSAGES["bom_all_pulled_or_short"]] _, other = can(ROLES["clerical"], s) assert A.WH_RELEASE not in other def test_can_for_admin_assumes_an_it_reason() -> None: from packetstation.policy import evaluate allowed, _ = can(ROLES["admin"], state("draft")) assert A.SEND_TO_WAREHOUSE in allowed and A.ADMIN_REOPEN not in allowed assert evaluate(ROLES["admin"], state("draft"), A.SEND_TO_WAREHOUSE).admin_bypass no_assume, _ = can(ROLES["admin"], state("draft"), assume_it_reason=False) assert A.SEND_TO_WAREHOUSE not in no_assume filed, _ = can(ROLES["admin"], state("filed")) assert set(filed) == {A.ADMIN_REOPEN, A.SET_DATABOOK_AT} assert not evaluate(ROLES["admin"], state("filed"), A.ADMIN_REOPEN).admin_bypass def test_can_for_the_welder() -> None: allowed, blocked = can(ROLES["welder"], state("in_progress", checklist_complete=False)) assert A.SUBMIT in blocked and blocked[A.SUBMIT] == [MESSAGES["checklist_complete"]] assert {A.EDIT_WELDS, A.CLOCK, A.EDIT_CHECKLIST} <= set(allowed) assert A.PULL_BACK not in allowed # ---------- principal_from_roles ---------- def test_principal_from_roles() -> None: cs = principal_from_roles("u1", ["clerical_supervisor"]) assert cs.support == {"CL"} and cs.support_sup == {"CL"} c = principal_from_roles("u1", ["clerical"]) assert c.support == {"CL"} and c.support_sup == frozenset() w = principal_from_roles("u1", ["warehouse_supervisor", "qc"]) assert w.support == {"WH", "QC"} and w.support_sup == {"WH"} shop = principal_from_roles("u1", ["shop:03:worker", "shop:06:supervisor", "shop:03:lead", "shop:10:worker"]) assert dict(shop.shop) == {"03": 2, "06": 3, "10": 1} assert isinstance(shop.shop, MappingProxyType) admin = principal_from_roles("u1", ["admin"]) assert admin.admin and not admin.support and not admin.shop and not admin.pm pm = principal_from_roles("u1", ["pm"], session_kind="pin") assert pm.pm and pm.session_kind == "pin" and not pm.support limited = principal_from_roles("u1", ["shop:03:worker"], station_limits=[3, 4]) assert limited.station_limits == {3, 4} assert principal_from_roles("u1", ["nonsense", "shop:03:boss"]) == Principal(user_id="u1") assert principal_from_roles("u1", []).system is None def test_multi_role_user() -> None: mia = principal_from_roles("u9", ["clerical", "shop:06:supervisor"]) assert check(mia, state("draft"), A.SEND_TO_WAREHOUSE).ok assert check(mia, state("review", dept="06"), A.SEND_TO_QC, extra=FULL_EXTRA).ok assert check(mia, state("review"), A.SEND_TO_QC, extra=FULL_EXTRA).code == "forbidden"