Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/utils/audit/pdfRender.test.ts
T

155 lines
5.4 KiB
TypeScript

// @vitest-environment node
import { describe, expect, it } from "vitest";
import type { TFunction } from "i18next";
import type { AuditExportFilters, AuditExportRow, AuditIntegritySummary } from "~/routes/admin/audit-export.helpers.server";
import {
buildAuditPdfLabels,
buildAuditPdfReportInput,
renderAuditExportPdfResponse,
toAuditPdfEvent,
toAuditPdfIntegrity,
} from "./pdfRender";
function makeRow(over: Partial<AuditExportRow> = {}): AuditExportRow {
return {
id: "audit_1",
createdAt: new Date("2026-05-17T00:00:00.000Z"),
tenantId: "tenant_1",
actorType: "user",
actorId: "user_1",
actorEmail: null,
actorIp: null,
actorUserAgent: null,
action: "widget.created",
targetType: "widget",
targetId: "widget_1",
requestId: null,
sourceModule: "crm",
outcome: "success",
dataSensitivity: "internal",
regulatoryScope: ["soc2"],
metadata: null,
previousHash: null,
eventHash: "h1",
...over,
};
}
const filters: AuditExportFilters = {
from: new Date("2026-05-01T00:00:00.000Z"),
to: new Date("2026-05-17T00:00:00.000Z"),
regulatoryScope: ["soc2"],
actorType: null,
outcome: null,
tenantId: "tenant_1",
};
const integrity: AuditIntegritySummary = { ok: true, inspected: 1, firstBreakId: null, reason: null };
describe("toAuditPdfEvent / toAuditPdfIntegrity", () => {
it("maps an AuditExportRow into AuditPdfEvent unchanged", () => {
const row = makeRow();
expect(toAuditPdfEvent(row)).toMatchObject({
id: row.id,
createdAt: row.createdAt,
tenantId: row.tenantId,
action: row.action,
eventHash: row.eventHash,
});
});
it("maps integrity 1:1", () => {
expect(toAuditPdfIntegrity(integrity)).toEqual(integrity);
});
});
describe("buildAuditPdfReportInput", () => {
it("combines filters + rows + integrity into the renderer input shape", () => {
const rows = [makeRow()];
const input = buildAuditPdfReportInput({ tenantId: "tenant_1", filters, integrity, rows, generatedBy: "ops-bot" });
expect(input.tenantId).toBe("tenant_1");
expect(input.from).toBe("2026-05-01T00:00:00.000Z");
expect(input.to).toBe("2026-05-17T00:00:00.000Z");
expect(input.filterSummary).toContain("regulatoryScope=soc2");
expect(input.events).toHaveLength(1);
expect(input.integrity.ok).toBe(true);
expect(input.generatedBy).toBe("ops-bot");
});
it("passes localized label overrides to the renderer input", () => {
const t = ((key: string, args?: Record<string, unknown>) => {
if (key === "settings:auditExport.pdf.events") {
return `translated events ${args?.count}`;
}
if (key === "settings:auditExport.pdf.integrity.ok") {
return `translated ok ${args?.inspected}`;
}
if (key === "settings:auditExport.pdf.integrity.broken") {
return `translated broken ${args?.firstBreakId}`;
}
return `translated:${key}`;
}) as TFunction;
const input = buildAuditPdfReportInput({
tenantId: "tenant_1",
filters,
integrity,
rows: [makeRow()],
labels: buildAuditPdfLabels(t),
});
expect(input.labels?.tenant).toBe("translated:settings:auditExport.pdf.tenant");
expect(input.labels?.events?.(3)).toBe("translated events 3");
expect(input.labels?.integrityOk?.(1)).toBe("translated ok 1");
expect(input.labels?.integrityBroken?.({ inspected: 1, firstBreakId: "audit_1", reason: "hash" })).toBe("translated broken audit_1");
expect(input.labels?.columns?.createdAt).toBe("translated:settings:auditExport.pdf.columns.createdAt");
});
});
describe("renderAuditExportPdfResponse", () => {
it("returns a Response with PDF content-type and chain-status header", async () => {
const rows = [makeRow()];
const response = await renderAuditExportPdfResponse(
{ tenantId: "tenant_1", filters, integrity, rows },
{ filenamePrefix: "audit-export-admin" }
);
expect(response.headers.get("Content-Type")).toBe("application/pdf");
expect(response.headers.get("X-Audit-Chain-Status")).toBe("ok");
const disposition = response.headers.get("Content-Disposition") ?? "";
expect(disposition).toContain("audit-export-admin");
expect(disposition).toContain("tenant_1");
const bytes = new Uint8Array(await response.arrayBuffer());
expect(bytes.length).toBeGreaterThan(0);
expect(Buffer.from(bytes.slice(0, 4)).toString("utf8")).toBe("%PDF");
});
it("marks the chain status as broken when integrity.ok=false", async () => {
const rows = [makeRow()];
const response = await renderAuditExportPdfResponse(
{
tenantId: "tenant_1",
filters,
integrity: { ok: false, inspected: 1, firstBreakId: "audit_1", reason: "hash-mismatch" },
rows,
},
{ filenamePrefix: "audit-export-admin" }
);
expect(response.headers.get("X-Audit-Chain-Status")).toBe("broken");
});
it("falls back to 'platform' in the filename when tenantId is null", async () => {
const rows: AuditExportRow[] = [];
const response = await renderAuditExportPdfResponse(
{
tenantId: null,
filters: { ...filters, tenantId: null },
integrity: { ok: true, inspected: 0, firstBreakId: null, reason: null },
rows,
},
{ filenamePrefix: "audit-export-admin" }
);
expect(response.headers.get("Content-Disposition") ?? "").toContain("audit-export-admin-platform-");
});
});