import { describe, it } from "@suaveplan/testing/runner"; import * as fc from "fast-check"; import { MemoryAuditAdapter } from "../src/adapters/memory/memory.js"; import { AuditEventType, AuditSeverity } from "../src/constants/index.js"; import { AuditLogger } from "../src/core/audit-logger/audit-logger.js"; import { computeEventHash, verifyChain, } from "../src/core/chain-integrity/chain-integrity.js"; import type { AuditEvent } from "../src/types/audit-logging.types.js"; const eventBaseArb = fc.record({ id: fc.uuid(), type: fc.constantFrom(...Object.values(AuditEventType)), severity: fc.constantFrom(...Object.values(AuditSeverity)), actor: fc.record({ userId: fc.option(fc.uuid(), { nil: undefined }), sessionId: fc.option(fc.uuid(), { nil: undefined }), }), timestamp: fc.date({ min: new Date("2020-01-01"), max: new Date("2030-01-01"), }), }); describe("property: HMAC stability", () => { it("same inputs produce same hash", { timeout: 30000 }, () => { fc.assert( fc.property( eventBaseArb, fc.string({ minLength: 1 }), (event, secret) => { const h1 = computeEventHash(event, secret); const h2 = computeEventHash(event, secret); return h1 === h2; } ), { numRuns: 50 } ); }); it("different secrets produce different hashes", { timeout: 30000 }, () => { fc.assert( fc.property( eventBaseArb, fc.string({ minLength: 1 }), fc.string({ minLength: 1 }), (event, s1, s2) => { fc.pre(s1 !== s2); return ( computeEventHash(event, s1) !== computeEventHash(event, s2) ); } ), { numRuns: 50 } ); }); it("different events produce different hashes (same secret)", { timeout: 30000, }, () => { fc.assert( fc.property( eventBaseArb, eventBaseArb, fc.string({ minLength: 1 }), (e1, e2, secret) => { // Only guarantee different hashes when key fields differ fc.pre(e1.id !== e2.id); return ( computeEventHash(e1, secret) !== computeEventHash(e2, secret) ); } ), { numRuns: 50 } ); }); }); describe("property: chain integrity mutation detection", () => { it("single mutation breaks chain at mutation index", { timeout: 30000, }, async () => { await fc.assert( fc.asyncProperty( fc.array(eventBaseArb, { minLength: 2, maxLength: 5 }), fc.string({ minLength: 1 }), async (bases, secret) => { // Build a valid chain const events: AuditEvent[] = []; let previousHash: string | undefined; for (const base of bases) { const hash = computeEventHash( base, secret, previousHash ); const event: AuditEvent = { ...base, hash, ...(previousHash !== undefined ? { previousHash } : {}), }; events.push(event); previousHash = hash; } const validResult = verifyChain(events, secret); if (!validResult.valid) return false; // Tamper with a random event (change userId) const tamperIdx = 0; const tampered = [...events]; tampered[tamperIdx] = { ...events[tamperIdx]!, actor: { userId: "tampered-user" }, }; const brokenResult = verifyChain(tampered, secret); return ( !brokenResult.valid && brokenResult.brokenAt === tamperIdx ); } ), { numRuns: 30 } ); }); }); describe("property: query filter correctness", () => { it("userId filter returns only matching events", { timeout: 30000, }, async () => { await fc.assert( fc.asyncProperty( fc.array(fc.uuid(), { minLength: 1, maxLength: 3 }), async (userIds) => { const adapter = new MemoryAuditAdapter(); const logger = new AuditLogger({ storage: adapter, secretKey: "s", }); for (const userId of userIds) { await logger.log({ type: AuditEventType.AUTH_LOGIN, severity: AuditSeverity.INFO, actor: { userId }, }); } for (const userId of userIds) { const results = await logger.query({ userId }); return results.every((e) => e.actor.userId === userId); } return true; } ), { numRuns: 20 } ); }); it("limit never exceeds requested count", { timeout: 30000 }, async () => { await fc.assert( fc.asyncProperty( fc.integer({ min: 1, max: 10 }), fc.integer({ min: 0, max: 5 }), async (total, limit) => { const adapter = new MemoryAuditAdapter(); const logger = new AuditLogger({ storage: adapter, secretKey: "s", }); for (let i = 0; i < total; i++) { await logger.log({ type: AuditEventType.AUTH_LOGIN, severity: AuditSeverity.INFO, actor: { userId: `u-${i}` }, }); } const results = await logger.query({ limit }); return results.length <= limit; } ), { numRuns: 20 } ); }); });