165 lines
4.5 KiB
TypeScript
165 lines
4.5 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import type { AuditEntry, HousekeepingAuditWriter } from "@/lib/services/audit";
|
|
import {
|
|
runWithAuditIntent,
|
|
writeIntent,
|
|
writeOutcome,
|
|
} from "./audit-envelope";
|
|
|
|
const auditEntry: AuditEntry = {
|
|
userId: 7,
|
|
action: "maintenance.run",
|
|
target: "system",
|
|
correlationId: "corr-command-7",
|
|
domain: "system",
|
|
reason: "Scheduled maintenance",
|
|
};
|
|
|
|
function createWriter(): HousekeepingAuditWriter {
|
|
return { write: vi.fn().mockResolvedValue(undefined) };
|
|
}
|
|
|
|
describe("audit command envelope", () => {
|
|
it("writes an intent with the original correlation evidence", async () => {
|
|
const writer = createWriter();
|
|
|
|
await writeIntent(writer, auditEntry);
|
|
|
|
expect(writer.write).toHaveBeenCalledWith(
|
|
{ ...auditEntry, outcome: "intent" },
|
|
undefined,
|
|
);
|
|
});
|
|
|
|
it("writes denied and failure outcomes as durable evidence", async () => {
|
|
const writer = createWriter();
|
|
|
|
await writeOutcome(writer, auditEntry, "denied");
|
|
await writeOutcome(writer, auditEntry, "failure");
|
|
|
|
expect(writer.write).toHaveBeenNthCalledWith(
|
|
1,
|
|
{ ...auditEntry, outcome: "denied" },
|
|
undefined,
|
|
);
|
|
expect(writer.write).toHaveBeenNthCalledWith(
|
|
2,
|
|
{ ...auditEntry, outcome: "failure" },
|
|
undefined,
|
|
);
|
|
});
|
|
|
|
it("blocks the operation when intent persistence fails", async () => {
|
|
const writer: HousekeepingAuditWriter = {
|
|
write: vi.fn().mockRejectedValue(new Error("audit unavailable")),
|
|
};
|
|
const operation = vi.fn().mockResolvedValue("external operation result");
|
|
|
|
await expect(
|
|
runWithAuditIntent(writer, auditEntry, operation),
|
|
).rejects.toThrow("audit unavailable");
|
|
|
|
expect(operation).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("writes a success outcome after the operation completes", async () => {
|
|
const writer = createWriter();
|
|
|
|
await expect(
|
|
runWithAuditIntent(writer, auditEntry, () => Promise.resolve("done")),
|
|
).resolves.toBe("done");
|
|
|
|
expect(writer.write).toHaveBeenNthCalledWith(
|
|
1,
|
|
{ ...auditEntry, outcome: "intent" },
|
|
undefined,
|
|
);
|
|
expect(writer.write).toHaveBeenNthCalledWith(
|
|
2,
|
|
{ ...auditEntry, outcome: "success" },
|
|
undefined,
|
|
);
|
|
});
|
|
|
|
it("writes a failure outcome before rethrowing an operation failure", async () => {
|
|
const writer = createWriter();
|
|
|
|
await expect(
|
|
runWithAuditIntent(writer, auditEntry, () =>
|
|
Promise.reject(new Error("operation failed")),
|
|
),
|
|
).rejects.toThrow("operation failed");
|
|
|
|
expect(writer.write).toHaveBeenNthCalledWith(
|
|
2,
|
|
{ ...auditEntry, outcome: "failure" },
|
|
undefined,
|
|
);
|
|
});
|
|
it("preserves the original operation error when failure evidence rejects", async () => {
|
|
const original = new Error("operation failed");
|
|
const auditFailure = new Error("audit failure");
|
|
const writer: HousekeepingAuditWriter = {
|
|
write: vi
|
|
.fn()
|
|
.mockResolvedValueOnce(undefined)
|
|
.mockRejectedValueOnce(auditFailure),
|
|
};
|
|
await expect(
|
|
runWithAuditIntent(writer, auditEntry, () => Promise.reject(original)),
|
|
).rejects.toBe(original);
|
|
expect(
|
|
(original as Error & { auditOutcomeError?: unknown }).auditOutcomeError,
|
|
).toBe(auditFailure);
|
|
});
|
|
|
|
it("throws a completed-operation error when success evidence rejects", async () => {
|
|
const auditFailure = new Error("audit failure");
|
|
const writer: HousekeepingAuditWriter = {
|
|
write: vi
|
|
.fn()
|
|
.mockResolvedValueOnce(undefined)
|
|
.mockRejectedValueOnce(auditFailure),
|
|
};
|
|
await expect(
|
|
runWithAuditIntent(writer, auditEntry, () => Promise.resolve("done")),
|
|
).rejects.toMatchObject({
|
|
operationCompleted: true,
|
|
operationResult: "done",
|
|
auditOutcomeError: auditFailure,
|
|
});
|
|
});
|
|
it("preserves a frozen operation error when failure evidence rejects", async () => {
|
|
const original = Object.freeze(new Error("operation failed"));
|
|
const writer: HousekeepingAuditWriter = {
|
|
write: vi
|
|
.fn()
|
|
.mockResolvedValueOnce(undefined)
|
|
.mockRejectedValueOnce(new Error("audit failure")),
|
|
};
|
|
await expect(
|
|
runWithAuditIntent(writer, auditEntry, () => Promise.reject(original)),
|
|
).rejects.toBe(original);
|
|
});
|
|
it("preserves a pathological rejection when failure evidence rejects", async () => {
|
|
const { proxy, revoke } = Proxy.revocable(
|
|
new Error("operation failed"),
|
|
{},
|
|
);
|
|
revoke();
|
|
const writer: HousekeepingAuditWriter = {
|
|
write: vi
|
|
.fn()
|
|
.mockResolvedValueOnce(undefined)
|
|
.mockRejectedValueOnce(new Error("audit failure")),
|
|
};
|
|
let rejected: unknown;
|
|
try {
|
|
await runWithAuditIntent(writer, auditEntry, () => Promise.reject(proxy));
|
|
} catch (error) {
|
|
rejected = error;
|
|
}
|
|
expect(rejected === proxy).toBe(true);
|
|
});
|
|
});
|