170 lines
4.7 KiB
TypeScript
170 lines
4.7 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import type { AuditEntry } from "@/lib/services/audit";
|
|
import type { HousekeepingCapabilityContext } from "../../../foundation/contracts";
|
|
import { CONTENT_MUTATION_OPERATIONS } from "./mutations";
|
|
import {
|
|
CONTENT_DATABASE_OPERATIONS,
|
|
CONTENT_EXTERNAL_OPERATIONS,
|
|
CONTENT_MIXED_OPERATIONS,
|
|
ContentCommittedExternalFailure,
|
|
createContentProductionMutationAdapter,
|
|
} from "./mutations-production";
|
|
|
|
const capability: HousekeepingCapabilityContext = {
|
|
actor: { id: 42, username: "operator", rank: 7 },
|
|
isSuperAdmin: false,
|
|
has: () => true,
|
|
hasAny: () => true,
|
|
hasAll: () => true,
|
|
};
|
|
const mutationContext = {
|
|
capability,
|
|
correlationId: "production-matrix",
|
|
legacy: false,
|
|
};
|
|
|
|
function dependencies() {
|
|
const transactionToken = { transaction: true };
|
|
const writeAudit = vi.fn(
|
|
async (_entry: AuditEntry, _transaction?: unknown) => undefined,
|
|
);
|
|
const executeOperation = vi.fn(async (operation: string) => ({
|
|
before: { operation, state: "before" },
|
|
after: { operation, state: "after" },
|
|
output: { id: "18446744073709551615" },
|
|
}));
|
|
const transaction = vi.fn(async (run) => run(transactionToken));
|
|
return {
|
|
transactionToken,
|
|
writeAudit,
|
|
executeOperation,
|
|
transaction,
|
|
adapter: createContentProductionMutationAdapter({
|
|
transaction,
|
|
writeAudit,
|
|
executeOperation,
|
|
}),
|
|
};
|
|
}
|
|
|
|
describe("Content production mutation adapter", () => {
|
|
it("classifies every operation exactly once", () => {
|
|
const classified = [
|
|
...CONTENT_DATABASE_OPERATIONS,
|
|
...CONTENT_EXTERNAL_OPERATIONS,
|
|
...CONTENT_MIXED_OPERATIONS,
|
|
];
|
|
expect([...classified].sort()).toEqual(
|
|
[...CONTENT_MUTATION_OPERATIONS].sort(),
|
|
);
|
|
expect(new Set(classified).size).toBe(classified.length);
|
|
});
|
|
|
|
it("executes every production operation and serializes identifiers", async () => {
|
|
const deps = dependencies();
|
|
for (const operation of CONTENT_MUTATION_OPERATIONS) {
|
|
const snapshot = await deps.adapter.execute(
|
|
operation,
|
|
{ action: "update" },
|
|
mutationContext,
|
|
);
|
|
expect(snapshot.output?.id, operation).toBe("18446744073709551615");
|
|
}
|
|
expect(deps.executeOperation).toHaveBeenCalledTimes(
|
|
CONTENT_MUTATION_OPERATIONS.length,
|
|
);
|
|
});
|
|
|
|
it("commits database mutation and canonical success audit in one transaction", async () => {
|
|
const deps = dependencies();
|
|
await deps.adapter.execute(
|
|
"article.change",
|
|
{ action: "update", id: "18446744073709551615" },
|
|
mutationContext,
|
|
);
|
|
|
|
expect(deps.transaction).toHaveBeenCalledTimes(1);
|
|
expect(deps.executeOperation).toHaveBeenCalledWith(
|
|
"article.change",
|
|
expect.anything(),
|
|
mutationContext,
|
|
deps.transactionToken,
|
|
);
|
|
expect(deps.writeAudit).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
action: "content.article.change",
|
|
outcome: "success",
|
|
domain: "content",
|
|
correlationId: "production-matrix",
|
|
}),
|
|
deps.transactionToken,
|
|
);
|
|
});
|
|
|
|
it("persists correlated intent and truthful outcome around external storage writes", async () => {
|
|
const deps = dependencies();
|
|
await deps.adapter.execute(
|
|
"translation.cms.save",
|
|
{ locale: "en", data: { welcome: "Hello" } },
|
|
mutationContext,
|
|
);
|
|
|
|
expect(deps.transaction).not.toHaveBeenCalled();
|
|
expect(deps.writeAudit.mock.calls.map(([entry]) => entry.outcome)).toEqual([
|
|
"intent",
|
|
"success",
|
|
]);
|
|
expect(
|
|
deps.writeAudit.mock.calls.every(
|
|
([entry]) => entry.correlationId === "production-matrix",
|
|
),
|
|
).toBe(true);
|
|
expect(JSON.stringify(deps.writeAudit.mock.calls)).not.toContain("Hello");
|
|
});
|
|
|
|
it("returns typed partial when database committed but the external effect failed", async () => {
|
|
const deps = dependencies();
|
|
deps.executeOperation.mockRejectedValueOnce(
|
|
new ContentCommittedExternalFailure({
|
|
before: { value: "/old.ico" },
|
|
after: { value: "/new.ico" },
|
|
}),
|
|
);
|
|
const snapshot = await deps.adapter.execute(
|
|
"favicon.save",
|
|
{ file: { name: "favicon.ico" } },
|
|
mutationContext,
|
|
);
|
|
|
|
expect(snapshot).toMatchObject({
|
|
before: { value: "/old.ico" },
|
|
after: { value: "/new.ico" },
|
|
completion: {
|
|
status: "partial",
|
|
external: "failed",
|
|
audit: "persisted",
|
|
},
|
|
});
|
|
expect(deps.writeAudit.mock.calls.at(-1)?.[0]).toMatchObject({
|
|
outcome: "partial",
|
|
});
|
|
});
|
|
|
|
it("records failure without optimistic after-state when external storage fails before commit", async () => {
|
|
const deps = dependencies();
|
|
deps.executeOperation.mockRejectedValueOnce(new Error("disk offline"));
|
|
await expect(
|
|
deps.adapter.execute(
|
|
"media.upload",
|
|
{ file: { name: "image.png" } },
|
|
mutationContext,
|
|
),
|
|
).rejects.toThrow("disk offline");
|
|
expect(deps.writeAudit.mock.calls.at(-1)?.[0]).toMatchObject({
|
|
outcome: "failure",
|
|
before: undefined,
|
|
after: undefined,
|
|
});
|
|
});
|
|
});
|