Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/routes/admin/audit-export.test.ts
T

119 lines
4.5 KiB
TypeScript

// @vitest-environment node
import { describe, expect, it } from "vitest";
import { computeAuditHash } from "@toolctrl/db/audit-hash";
import {
AUDIT_EXPORT_MAX_ROWS,
buildAuditExportWhere,
computeAuditIntegritySummary,
csvCell,
parseAuditExportFilters,
renderAuditExportCsv,
type AuditExportRow,
} from "./audit-export.helpers.server";
describe("admin audit-export filters", () => {
it("parses ISO dates and rejects malformed values", () => {
const f = parseAuditExportFilters(new URL("https://x/?from=2026-05-01&to=2026-05-17&actorType=admin&outcome=success&regulatoryScope=soc2&regulatoryScope=gdpr"));
expect(f.from?.toISOString()).toBe("2026-05-01T00:00:00.000Z");
expect(f.to?.toISOString()).toBe("2026-05-17T00:00:00.000Z");
expect(f.actorType).toBe("admin");
expect(f.outcome).toBe("success");
expect(f.regulatoryScope).toEqual(["soc2", "gdpr"]);
});
it("rejects bad dates as null", () => {
expect(parseAuditExportFilters(new URL("https://x/?from=bad")).from).toBeNull();
});
it("builds where with hasSome for regulatoryScope and inclusive date range", () => {
const where = buildAuditExportWhere({ from: new Date("2026-05-01T00:00:00Z"), to: new Date("2026-05-02T00:00:00Z"), regulatoryScope: ["soc2"], actorType: null, outcome: null, tenantId: null }) as Record<string, unknown>;
expect(where.regulatoryScope).toEqual({ hasSome: ["soc2"] });
expect((where.createdAt as { lt: Date }).lt.toISOString()).toBe("2026-05-03T00:00:00.000Z");
});
it("AUDIT_EXPORT_MAX_ROWS is set", () => {
expect(AUDIT_EXPORT_MAX_ROWS).toBeGreaterThan(0);
});
});
describe("admin audit-export csv rendering", () => {
it("escapes commas, quotes and newlines", () => {
expect(csvCell("hello")).toBe("hello");
expect(csvCell("a,b")).toBe('"a,b"');
expect(csvCell('a"b')).toBe('"a""b"');
expect(csvCell("line\nbreak")).toBe('"line\nbreak"');
expect(csvCell(null)).toBe("");
expect(csvCell(["a", "b"])).toBe("a|b");
expect(csvCell({ a: 1 })).toBe('"{""a"":1}"');
});
it("renders integrity header line and column row", () => {
const csv = renderAuditExportCsv([], { ok: true, inspected: 0, firstBreakId: null, reason: null });
const lines = csv.trim().split("\n");
expect(lines[0]).toMatch(/^# verifyAuditChain: ok=true/);
expect(lines[1]).toContain("id,createdAt,tenantId,actorType");
});
});
describe("admin audit-export integrity verifier", () => {
function payload(over: Record<string, unknown> = {}) {
return {
tenantId: "t1",
actorType: "user",
actorId: "u1",
actorEmail: null,
actorIp: null,
actorUserAgent: null,
action: "x.do",
targetType: null,
targetId: null,
requestId: null,
sourceModule: null,
outcome: "success",
dataSensitivity: "internal",
regulatoryScope: [] as string[],
metadata: null,
...over,
};
}
function hashOf(prev: string | null, p: ReturnType<typeof payload>): string {
return computeAuditHash({ previousHash: prev, payload: { id: "", createdAt: "", ...p } });
}
it("returns ok for a valid 2-event chain", () => {
const p1 = payload();
const h1 = hashOf(null, p1);
const p2 = payload({ action: "x.do2" });
const h2 = hashOf(h1, p2);
const rows: AuditExportRow[] = [
{ id: "1", createdAt: new Date(1), previousHash: null, eventHash: h1, ...p1 },
{ id: "2", createdAt: new Date(2), previousHash: h1, eventHash: h2, ...p2 },
];
expect(computeAuditIntegritySummary(rows)).toEqual({ ok: true, inspected: 2, firstBreakId: null, reason: null });
});
it("detects tampered metadata", () => {
const p1 = payload();
const h1 = hashOf(null, p1);
const rows: AuditExportRow[] = [
{ id: "1", createdAt: new Date(1), previousHash: null, eventHash: h1, ...payload({ metadata: { tampered: true } }) },
];
const out = computeAuditIntegritySummary(rows);
expect(out.ok).toBe(false);
expect(out.reason).toBe("hash-mismatch");
});
it("verifies chains independently per tenant", () => {
const pA = payload({ tenantId: "tA" });
const pB = payload({ tenantId: "tB", action: "y.do" });
const hA = hashOf(null, pA);
const hB = hashOf(null, pB);
const rows: AuditExportRow[] = [
{ id: "a", createdAt: new Date(1), previousHash: null, eventHash: hA, ...pA },
{ id: "b", createdAt: new Date(2), previousHash: null, eventHash: hB, ...pB },
];
expect(computeAuditIntegritySummary(rows).ok).toBe(true);
});
});