120 lines
5.0 KiB
Python
120 lines
5.0 KiB
Python
"""Phase 7 stamps: the controlled-copy and print stamp texts, the short stamp signature made with the seal key, and
|
||||
|
|
its check. Pure functions; no database."""
|
|||
|
|
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import re
|
|||
|
|
from datetime import UTC, datetime
|
|||
|
|
from typing import Any
|
|||
|
|
|
|||
|
|
from pshelpers import TEST_SEAL_KEY
|
|||
|
|
|
|||
|
|
from packetstation.pdf import stamps
|
|||
|
|
|
|||
|
|
KEY = TEST_SEAL_KEY.encode()
|
|||
|
|
NOW = datetime(2026, 10, 17, 21, 4, tzinfo=UTC)
|
|||
|
|
WHO = {"id": "00000000-0000-0000-0000-000000000c1a", "display_name": "Clara Clerk", "emp_no": "4001"}
|
|||
|
|
SEAL = "0123456789abcdef" * 4
|
|||
|
|
USER = WHO["id"]
|
|||
|
|
AT = NOW.isoformat()
|
|||
|
|
|
|||
|
|
|
|||
|
|
def sig(packet_id: int = 7, kind: str = "databook", seal: str = SEAL, user: str = USER, at: str = AT) -> str:
|
|||
|
|
return stamps.stamp_sig(KEY, packet_id, kind, seal, user, at)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_stamp_sig_is_16_hex_and_stable() -> None:
|
|||
|
|
s = sig()
|
|||
|
|
assert re.fullmatch(r"[0-9a-f]{16}", s), s
|
|||
|
|
assert sig() == s
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_stamp_sig_differs_per_kind_packet_user_time_seal_and_key() -> None:
|
|||
|
|
base = sig()
|
|||
|
|
assert sig(kind="traveler") != base
|
|||
|
|
assert sig(packet_id=8) != base
|
|||
|
|
assert sig(user="00000000-0000-0000-0000-000000000bbb") != base
|
|||
|
|
assert sig(at="2026-10-17T21:05:00+00:00") != base
|
|||
|
|
assert sig(seal="f" * 64) != base
|
|||
|
|
assert stamps.stamp_sig(b"another-key-0123456789abcdef", 7, "databook", SEAL, USER, AT) != base
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_verify_stamp_accepts_and_rejects() -> None:
|
|||
|
|
good = sig()
|
|||
|
|
assert stamps.verify_stamp(KEY, 7, "databook", SEAL, USER, AT, good) is True
|
|||
|
|
assert stamps.verify_stamp(KEY, 7, "databook", SEAL, USER, AT, "0" * 16) is False
|
|||
|
|
assert stamps.verify_stamp(KEY, 8, "databook", SEAL, USER, AT, good) is False
|
|||
|
|
assert stamps.verify_stamp(KEY, 7, "traveler", SEAL, USER, AT, good) is False
|
|||
|
|
# the time is signed to the minute, in any ISO form
|
|||
|
|
assert stamps.verify_stamp(KEY, 7, "databook", SEAL, USER, "2026-10-17T21:04:59+00:00", good) is True
|
|||
|
|
assert stamps.verify_stamp(KEY, 7, "databook", SEAL, USER, "2026-10-17T21:05:00+00:00", good) is False
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_controlled_stamp_texts() -> None:
|
|||
|
|
filed = stamps.controlled_stamp(WHO, NOW, mode="filed", seal_hash_hex=SEAL)
|
|||
|
|
assert filed.startswith("Controlled copy - exported by Clara Clerk (4001) 2026-10-17 21:04 UTC")
|
|||
|
|
assert "seal 0123456789ab" in filed and "seal 0123456789abc" not in filed
|
|||
|
|
preview = stamps.controlled_stamp(WHO, NOW, mode="preview", seal_hash_hex=None)
|
|||
|
|
assert preview.startswith("Controlled copy - exported by Clara Clerk (4001) 2026-10-17 21:04 UTC")
|
|||
|
|
assert "not yet filed" in preview
|
|||
|
|
printed = stamps.controlled_stamp(WHO, NOW, mode="print", seal_hash_hex=None)
|
|||
|
|
assert "printed by Clara Clerk (4001)" in printed
|
|||
|
|
trav = stamps.controlled_stamp(WHO, NOW, mode="traveler")
|
|||
|
|
assert trav == "Controlled copy - exported by Clara Clerk (4001) 2026-10-17 21:04 UTC"
|
|||
|
|
for text in (filed, preview, printed, trav):
|
|||
|
|
assert text.isascii(), text
|
|||
|
|
assert "—" not in text and "–" not in text
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_missing_emp_number_reads_question_mark() -> None:
|
|||
|
|
text = stamps.controlled_stamp({**WHO, "emp_no": None}, NOW, mode="preview", seal_hash_hex=None)
|
|||
|
|
assert "Clara Clerk (?)" in text
|
|||
|
|
|
|||
|
|
|
|||
|
|
def packet_like() -> Any:
|
|||
|
|
"""The fields print_stamp reads, as a dict (the read model) and as an object, so either shape works."""
|
|||
|
|
return {
|
|||
|
|
"id": 7,
|
|||
|
|
"drawing": "D-5001",
|
|||
|
|
"rev": "B",
|
|||
|
|
"unit": {
|
|||
|
|
"id": 3,
|
|||
|
|
"number": "L5A1B2C001",
|
|||
|
|
"lot": "001",
|
|||
|
|
"project": {"id": 2, "number": "L5A1B2C", "name": "Shop project"},
|
|||
|
|
},
|
|||
|
|
"project": {"number": "L5A1B2C"},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_print_stamp_text_is_ascii_and_carries_the_parts() -> None:
|
|||
|
|
text = stamps.print_stamp(WHO, NOW, "Drawing with weld map — sheet 2", packet_like())
|
|||
|
|
assert text.startswith("PRINTED COPY - Drawing with weld map")
|
|||
|
|
assert "printed by Clara Clerk (4001) 2026-10-17 21:04 UTC" in text
|
|||
|
|
assert "L5A1B2C Lot 001 dwg D-5001 RB" in text, text
|
|||
|
|
assert text.endswith(" - verify revision before use"), text
|
|||
|
|
assert text.isascii(), text
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_print_stamp_replaces_non_ascii() -> None:
|
|||
|
|
who = {**WHO, "display_name": "Zoë Łukasz"}
|
|||
|
|
text = stamps.print_stamp(who, NOW, "MTR → item 3", packet_like())
|
|||
|
|
assert text.isascii(), text
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_apply_stamp_and_metadata() -> None:
|
|||
|
|
from closehelpers import flat, page_texts, pdf_reader
|
|||
|
|
from pkthelpers import tiny_pdf
|
|||
|
|
|
|||
|
|
line = "Controlled copy - exported by Clara Clerk (4001) 2026-10-17 21:04 UTC - stamp 0123456789abcdef"
|
|||
|
|
for box in (False, True):
|
|||
|
|
out = stamps.apply_stamp(tiny_pdf(2), line, ours={0}, box=box)
|
|||
|
|
texts = page_texts(out)
|
|||
|
|
assert len(texts) == 2 and all(line in flat(t) for t in texts), (box, texts)
|
|||
|
|
out = stamps.set_metadata(tiny_pdf(1), "Title A", "Subject B", "k1, k2", "Packet Station - test")
|
|||
|
|
meta = pdf_reader(out).metadata
|
|||
|
|
assert meta.title == "Title A" and meta.subject == "Subject B" and meta.creator == "Packet Station - test"
|
|||
|
|
assert meta.producer == "Packet Station"
|
|||
|
|
assert meta.get("/Author") == "PESCO - Process Equipment & Service Company, Inc."
|
|||
|
|
assert meta.get("/CreationDate")
|