Complete Non-Conformance Report system replacing the PowerApps/SharePoint prototype: FastAPI + SQLAlchemy 2 (async) + Alembic + MySQL 8 backend, React 18 + Vite + TypeScript + MUI frontend, Entra ID auth (MSAL / JWKS, group-gated), Microsoft Graph delegated Mail.Send notifications (OBO), six-stage workflow state machine with server-side enforcement, atomic NCR-YYYY-NNNN numbering, attachments with camera capture, immutable field-level audit trail, admin reopen, reports + CSV export, WeasyPrint PDF traveler, Power BI reporting views + read-only DB user, documented VISUAL ERP job-lookup stub, pytest suite (26 tests), docker-compose deployment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
63 lines
2.3 KiB
Python
63 lines
2.3 KiB
Python
"""Attachment upload validation, metadata, download, and closure locking."""
|
|
from .util import create_ncr, hdr, to_closed
|
|
|
|
TINY_PNG = (
|
|
b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01"
|
|
b"\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx\x9cc\x00\x01"
|
|
b"\x00\x00\x05\x00\x01\r\n-\xb4\x00\x00\x00\x00IEND\xaeB`\x82"
|
|
)
|
|
|
|
|
|
async def test_upload_download_and_metadata(client, team):
|
|
ncr = await create_ncr(client, team)
|
|
r = await client.post(
|
|
f"/api/ncrs/{ncr['id']}/attachments",
|
|
files=[
|
|
("files", ("photo one.png", TINY_PNG, "image/png")),
|
|
("files", ("notes.txt", b"observed at station 4", "text/plain")),
|
|
],
|
|
headers=hdr(team["requester"]),
|
|
)
|
|
assert r.status_code == 201, r.text
|
|
items = r.json()
|
|
assert len(items) == 2
|
|
png = next(i for i in items if i["is_image"])
|
|
assert png["original_filename"] == "photo one.png"
|
|
assert png["uploaded_by"]["email"] == team["requester"]
|
|
assert png["size_bytes"] == len(TINY_PNG)
|
|
|
|
r = await client.get(
|
|
f"/api/attachments/{png['id']}/download", headers=hdr(team["ops"])
|
|
)
|
|
assert r.status_code == 200
|
|
assert r.content == TINY_PNG
|
|
|
|
# attachment add shows in detail + audit
|
|
detail = (await client.get(f"/api/ncrs/{ncr['id']}", headers=hdr(team["qc"]))).json()
|
|
assert len(detail["attachments"]) == 2
|
|
audit = (
|
|
await client.get(f"/api/ncrs/{ncr['id']}/audit", headers=hdr(team["qc"]))
|
|
).json()
|
|
assert sum(1 for a in audit["items"] if a["action"] == "attachment_add") == 2
|
|
|
|
|
|
async def test_disallowed_type_rejected(client, team):
|
|
ncr = await create_ncr(client, team)
|
|
r = await client.post(
|
|
f"/api/ncrs/{ncr['id']}/attachments",
|
|
files=[("files", ("malware.exe", b"MZ...", "application/octet-stream"))],
|
|
headers=hdr(team["requester"]),
|
|
)
|
|
assert r.status_code == 422
|
|
assert "not allowed" in r.json()["detail"]
|
|
|
|
|
|
async def test_attachments_locked_when_closed(client, team):
|
|
ncr = await to_closed(client, team, (await create_ncr(client, team))["id"])
|
|
r = await client.post(
|
|
f"/api/ncrs/{ncr['id']}/attachments",
|
|
files=[("files", ("late.png", TINY_PNG, "image/png"))],
|
|
headers=hdr(team["requester"]),
|
|
)
|
|
assert r.status_code == 409
|