155 lines
5.4 KiB
TypeScript
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-");
|
|
});
|
|
});
|