119 lines
4.5 KiB
TypeScript
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®ulatoryScope=soc2®ulatoryScope=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);
|
|
});
|
|
});
|