84 lines
2.5 KiB
TypeScript
84 lines
2.5 KiB
TypeScript
|
|
// Unit tests for the audit helper. Outside a request context (vitest), the
|
||
|
|
// `headers()` call throws and we should silently fall back to nulls — which
|
||
|
|
// is exactly what the helper does.
|
||
|
|
|
||
|
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
||
|
|
import { PrismaPg } from "@prisma/adapter-pg";
|
||
|
|
import { PrismaClient } from "@/generated/prisma/client";
|
||
|
|
import { audit } from "@/lib/audit";
|
||
|
|
import { seed, type SeedResult } from "@/lib/seed";
|
||
|
|
|
||
|
|
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
|
||
|
|
const db = new PrismaClient({ adapter });
|
||
|
|
|
||
|
|
let fx: SeedResult;
|
||
|
|
|
||
|
|
beforeAll(async () => {
|
||
|
|
fx = await seed(db);
|
||
|
|
});
|
||
|
|
|
||
|
|
afterAll(async () => {
|
||
|
|
await db.$disconnect();
|
||
|
|
});
|
||
|
|
|
||
|
|
beforeEach(async () => {
|
||
|
|
await db.auditLog.deleteMany();
|
||
|
|
});
|
||
|
|
|
||
|
|
describe("audit", () => {
|
||
|
|
test("creates an AuditLog row with the supplied fields", async () => {
|
||
|
|
await audit(db, {
|
||
|
|
actorId: fx.admin.id,
|
||
|
|
action: "test.event",
|
||
|
|
entityType: "Test",
|
||
|
|
entityId: "abc",
|
||
|
|
meta: { foo: "bar" },
|
||
|
|
});
|
||
|
|
const rows = await db.auditLog.findMany();
|
||
|
|
expect(rows).toHaveLength(1);
|
||
|
|
expect(rows[0].actorId).toBe(fx.admin.id);
|
||
|
|
expect(rows[0].action).toBe("test.event");
|
||
|
|
expect(rows[0].entityType).toBe("Test");
|
||
|
|
expect(rows[0].entityId).toBe("abc");
|
||
|
|
expect(rows[0].meta).toEqual({ foo: "bar" });
|
||
|
|
});
|
||
|
|
|
||
|
|
test("nulls IP/UA when called outside a request context", async () => {
|
||
|
|
await audit(db, {
|
||
|
|
actorId: fx.admin.id,
|
||
|
|
action: "test.event",
|
||
|
|
entityType: "Test",
|
||
|
|
entityId: "abc",
|
||
|
|
});
|
||
|
|
const row = await db.auditLog.findFirst();
|
||
|
|
expect(row?.ip).toBeNull();
|
||
|
|
expect(row?.ua).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
test("accepts a null actorId for system events", async () => {
|
||
|
|
await audit(db, {
|
||
|
|
actorId: null,
|
||
|
|
action: "system.heartbeat",
|
||
|
|
entityType: "System",
|
||
|
|
entityId: "1",
|
||
|
|
});
|
||
|
|
const row = await db.auditLog.findFirst();
|
||
|
|
expect(row?.actorId).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
test("does not throw if write fails (best-effort semantics)", async () => {
|
||
|
|
// Simulate by sending an entityId that violates a column type or similar.
|
||
|
|
// Easiest: inject a JSON value that's structured to roundtrip cleanly,
|
||
|
|
// and verify no throw. (Real failure modes are network/disk, not validation.)
|
||
|
|
await expect(
|
||
|
|
audit(db, {
|
||
|
|
actorId: fx.admin.id,
|
||
|
|
action: "test.event",
|
||
|
|
entityType: "Test",
|
||
|
|
entityId: "ok",
|
||
|
|
meta: { nested: { a: 1, b: [1, 2, 3] } },
|
||
|
|
}),
|
||
|
|
).resolves.toBeUndefined();
|
||
|
|
});
|
||
|
|
});
|