Phase 9: Admin completion, reconcile, notifications, integrity, backups
Admin: Departments (name and stations; add shows the three groups IT must create), Profiles (welder stamp, station limits, active) with a CSV import that previews and applies only when clean, Checklists (weld and plain), Security (session, lockout and rate-limit settings with bounds, alerts for the last 24 hours, the integrity result), Access log with filters and a guarded CSV, Who is in, Stamp check, Notifications, and the employeeID flags. Every Admin change is audited. Worker: ad-reconcile (nightly and on "refresh from AD now"), session-refresh (15 minutes), integrity (seals, asset hashes, missing and orphan files, the audit chain), backup-check, notify-send and notify-digest. Notifications route per event type to AD groups, users, mailboxes or the department Supervisor group, immediately or in a daily digest, over STARTTLS verified against the PESCO CA, with dry run on the test stack. Backups: scripts/backup/ps-backup.sh and ps-restore.sh with a systemd timer at 02:00 America/Denver. Restore rehearsed on the test stack 2026-10-21. CI: pip-audit and npm audit. pypdf 6.21.1 and certifi 2026.10.19. Migration 0010. Closes the app part of I6, M3, O1, O2 and S3. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import { fireEvent, screen, waitFor, within } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { adminBase } from "../../test/admin";
|
||||
import { renderApp } from "../../test/auth";
|
||||
import { callsTo, mockFetch } from "../../test/fetchMock";
|
||||
|
||||
const PRINT = {
|
||||
id: 5,
|
||||
at: "2026-10-20T10:00:00Z",
|
||||
by_name: "Clara Clerk",
|
||||
kind: "databook",
|
||||
what: "Data book",
|
||||
pages: 12,
|
||||
packet_id: 42,
|
||||
packet_label: "L662001/1",
|
||||
};
|
||||
|
||||
async function openTab() {
|
||||
renderApp("/admin");
|
||||
fireEvent.click(await screen.findByRole("tab", { name: "Stamp check" }));
|
||||
return screen.findByLabelText("Stamp line or code");
|
||||
}
|
||||
|
||||
describe("Admin Stamp check tab", () => {
|
||||
it("checks a typed stamp line", async () => {
|
||||
const fetch = mockFetch({
|
||||
...adminBase(),
|
||||
"POST /api/admin/stamp-verify": { body: { found: true, valid: true, reason: null, print: PRINT } },
|
||||
});
|
||||
const box = await openTab();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Check stamp" }));
|
||||
expect(await screen.findByRole("alert")).toHaveTextContent("Type or paste the stamp line first.");
|
||||
fireEvent.change(box, { target: { value: " PS 0123456789abcdef " } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Check stamp" }));
|
||||
const result = await screen.findByRole("status", { name: "Stamp result" });
|
||||
expect(result).toHaveTextContent("Genuine.");
|
||||
expect(within(result).getByText("Clara Clerk")).toBeInTheDocument();
|
||||
expect(within(result).getByText("L662001/1")).toBeInTheDocument();
|
||||
expect(within(result).getByText("Data book, 12 pages")).toBeInTheDocument();
|
||||
expect(callsTo(fetch, "POST", "/api/admin/stamp-verify")).toEqual([{ text: "PS 0123456789abcdef" }]);
|
||||
});
|
||||
|
||||
it("says when a stamp is not found or does not verify", async () => {
|
||||
let n = 0;
|
||||
mockFetch({
|
||||
...adminBase(),
|
||||
"POST /api/admin/stamp-verify": () => {
|
||||
n += 1;
|
||||
return n === 1
|
||||
? { body: { found: false, valid: false, reason: null, print: null } }
|
||||
: { body: { found: true, valid: false, reason: "The signature does not match.", print: PRINT } };
|
||||
},
|
||||
});
|
||||
const box = await openTab();
|
||||
fireEvent.change(box, { target: { value: "ffffffffffffffff" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Check stamp" }));
|
||||
expect(await screen.findByText(/Not found\./)).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Check stamp" }));
|
||||
expect(await screen.findByText(/Does not verify\./)).toBeInTheDocument();
|
||||
expect(screen.getByText(/The signature does not match\./)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("uploads a PDF and lists each stamp found", async () => {
|
||||
const fetch = mockFetch({
|
||||
...adminBase(),
|
||||
"POST /api/admin/stamp-verify/pdf": {
|
||||
body: { stamps: [{ found: true, valid: true, reason: null, print: PRINT }, { found: false, valid: false, reason: null, print: null }] },
|
||||
},
|
||||
});
|
||||
await openTab();
|
||||
const input = screen.getByLabelText("PDF of a printed copy");
|
||||
fireEvent.change(input, { target: { files: [new File(["x"], "copy.txt", { type: "text/plain" })] } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Check PDF" }));
|
||||
expect(await screen.findByRole("alert")).toHaveTextContent("Only a PDF can be checked.");
|
||||
|
||||
fireEvent.change(input, { target: { files: [new File(["%PDF-1.7"], "copy.pdf", { type: "application/pdf" })] } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Check PDF" }));
|
||||
const results = await screen.findByRole("status", { name: "PDF stamp results" });
|
||||
expect(results).toHaveTextContent("Stamp 1: Genuine.");
|
||||
expect(results).toHaveTextContent("Stamp 2: Not found.");
|
||||
await waitFor(() => expect(callsTo(fetch, "POST", "/api/admin/stamp-verify/pdf")).toHaveLength(1));
|
||||
const init = fetch.mock.calls.find(([u]) => String(u) === "/api/admin/stamp-verify/pdf")?.[1];
|
||||
expect(init?.body).toBeInstanceOf(FormData);
|
||||
expect((init?.body as FormData).get("file")).toBeInstanceOf(File);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user