feat(housekeeping): correlate command audit evidence
This commit is contained in:
1 parent
7c93d3e766
commit
89dec9da05
4 files changed
+226
-4
No files matched your search
@@ -0,0 +1,99 @@
|
||||
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,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import type {
|
||||
AuditEntry,
|
||||
HousekeepingAuditTransaction,
|
||||
HousekeepingAuditWriter,
|
||||
} from "@/lib/services/audit";
|
||||
|
||||
type AuditOutcome = Exclude<NonNullable<AuditEntry["outcome"]>, "intent">;
|
||||
|
||||
export async function writeIntent(
|
||||
writer: HousekeepingAuditWriter,
|
||||
entry: AuditEntry,
|
||||
transaction?: HousekeepingAuditTransaction,
|
||||
): Promise<void> {
|
||||
await writer.write({ ...entry, outcome: "intent" }, transaction);
|
||||
}
|
||||
|
||||
export async function writeOutcome(
|
||||
writer: HousekeepingAuditWriter,
|
||||
entry: AuditEntry,
|
||||
outcome: AuditOutcome,
|
||||
transaction?: HousekeepingAuditTransaction,
|
||||
): Promise<void> {
|
||||
await writer.write({ ...entry, outcome }, transaction);
|
||||
}
|
||||
|
||||
export async function runWithAuditIntent<T>(
|
||||
writer: HousekeepingAuditWriter,
|
||||
entry: AuditEntry,
|
||||
operation: () => Promise<T>,
|
||||
transaction?: HousekeepingAuditTransaction,
|
||||
): Promise<T> {
|
||||
await writeIntent(writer, entry, transaction);
|
||||
|
||||
let result: T;
|
||||
try {
|
||||
result = await operation();
|
||||
} catch (error) {
|
||||
await writeOutcome(writer, entry, "failure", transaction);
|
||||
throw error;
|
||||
}
|
||||
|
||||
await writeOutcome(writer, entry, "success", transaction);
|
||||
return result;
|
||||
}
|
||||
Reference in new issue
Block a user