107 lines
3.1 KiB
TypeScript
107 lines
3.1 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { PERMS } from "@/lib/permission-slugs";
|
|
import type { HousekeepingCapabilityContext } from "../../../foundation/contracts";
|
|
import {
|
|
CONTENT_MUTATION_OPERATIONS,
|
|
createContentMutationInvocation,
|
|
createContentMutationService,
|
|
} from "./mutations";
|
|
|
|
function context(
|
|
granted: readonly string[],
|
|
actorId = 42,
|
|
): HousekeepingCapabilityContext {
|
|
const permissions = new Set(granted);
|
|
return {
|
|
actor: { id: actorId, username: "operator", rank: 7 },
|
|
isSuperAdmin: false,
|
|
has: (slug) => permissions.has(slug),
|
|
hasAny: (...slugs) => slugs.some((slug) => permissions.has(slug)),
|
|
hasAll: (...slugs) => slugs.every((slug) => permissions.has(slug)),
|
|
};
|
|
}
|
|
|
|
describe("Content mutation service authority", () => {
|
|
it("rehydrates server authority and rejects forged actor identity", async () => {
|
|
const adapter = { execute: vi.fn() };
|
|
const service = createContentMutationService(adapter, async () =>
|
|
context([PERMS.NEWS_EDIT], 42),
|
|
);
|
|
|
|
const result = await service.execute(
|
|
{ correlationId: "forged", expectedActorId: 7 },
|
|
"article.change",
|
|
{ action: "create", title: "Forged" },
|
|
);
|
|
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
error: { code: "FORBIDDEN" },
|
|
});
|
|
expect(adapter.execute).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("requires the exact operation ACL even after dispatcher authorization", async () => {
|
|
const adapter = { execute: vi.fn() };
|
|
const service = createContentMutationService(adapter, async () =>
|
|
context([PERMS.NEWS_EDIT]),
|
|
);
|
|
|
|
const result = await service.execute(
|
|
{ correlationId: "brand", expectedActorId: 42 },
|
|
"theme.update",
|
|
{},
|
|
);
|
|
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
error: { code: "FORBIDDEN" },
|
|
});
|
|
expect(adapter.execute).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("executes every declared operation through the real service boundary", async () => {
|
|
const execute = vi.fn(async (_operation, _input, mutationContext) => ({
|
|
before: null,
|
|
after: { actorId: mutationContext.capability.actor.id },
|
|
}));
|
|
const service = createContentMutationService({ execute }, async () =>
|
|
context(Object.values(PERMS)),
|
|
);
|
|
const invocation = createContentMutationInvocation(
|
|
{ id: 42 },
|
|
"operation-matrix",
|
|
);
|
|
|
|
for (const operation of CONTENT_MUTATION_OPERATIONS) {
|
|
const result = await service.execute(invocation, operation, {});
|
|
expect(result.ok, operation).toBe(true);
|
|
}
|
|
expect(execute).toHaveBeenCalledTimes(CONTENT_MUTATION_OPERATIONS.length);
|
|
});
|
|
|
|
it("maps adapter failures without exposing storage or template payloads", async () => {
|
|
const service = createContentMutationService(
|
|
{
|
|
execute: async () => {
|
|
throw new Error("C:\\private\\template.json: secret body");
|
|
},
|
|
},
|
|
async () => context([PERMS.SETTINGS_EDIT]),
|
|
);
|
|
const result = await service.execute(
|
|
{ correlationId: "redacted", expectedActorId: 42 },
|
|
"translation.cms.save",
|
|
{ data: { secret: "body" } },
|
|
);
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
code: "DEPENDENCY_UNAVAILABLE",
|
|
messageKey: "errors.housekeeping.dependencyUnavailable",
|
|
},
|
|
});
|
|
expect(JSON.stringify(result)).not.toContain("secret body");
|
|
});
|
|
});
|