fix(housekeeping): address task 14 review round 2

This commit is contained in:
Simo committed 2026-08-30 11:30:17 +02:00
1 parent d09eaa33d6
commit c524b305d7
17 files changed
+614 -130

No files matched your search

+78 -6
View File
@@ -2,6 +2,7 @@
import { readFileSync } from "node:fs";
import { redirect } from "next/navigation";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { getHousekeepingCapabilityContext } from "@/features/housekeeping/foundation/server-capability-context";
import { requirePermission, requireStaff } from "@/lib/admin/guard";
import { createAd } from "./admin-ads";
import { createArticle } from "./admin-articles";
@@ -9,12 +10,17 @@ import { uploadMedia } from "./admin-media";
import { deleteFavicon, saveFavicon } from "./save-favicon";
import { saveLogo } from "./save-logo";
const { execute } = vi.hoisted(() => ({
const { execute, auditedBrandExecute } = vi.hoisted(() => ({
execute: vi.fn(async () => ({
ok: true,
data: { before: null, after: { id: "1" }, output: { url: "/api/media/x" } },
correlationId: "legacy",
})),
auditedBrandExecute: vi.fn(async () => ({
before: null,
after: { value: "/api/media/x" },
output: { url: "/api/media/x" },
})),
}));
const { executeLegacyBrandAssetMutation } = vi.hoisted(() => ({
executeLegacyBrandAssetMutation: vi.fn(async () => ({
@@ -32,6 +38,21 @@ vi.mock("@/features/housekeeping/domains/content/services/mutations", () => ({
legacy: true,
}),
}));
vi.mock(
"@/features/housekeeping/domains/content/services/mutations-production",
() => ({
contentProductionMutationAdapter: { execute: auditedBrandExecute },
}),
);
vi.mock("@/features/housekeeping/foundation/server-capability-context", () => ({
getHousekeepingCapabilityContext: vi.fn(async () => ({
actor: { id: 42, username: "operator", rank: 7 },
isSuperAdmin: false,
has: () => false,
hasAny: () => false,
hasAll: () => false,
})),
}));
vi.mock(
"@/features/housekeeping/domains/content/services/mutation-runtime-external",
() => ({
@@ -108,6 +129,11 @@ beforeEach(() => {
data: { before: null, after: { id: "1" }, output: { url: "/api/media/x" } },
correlationId: "legacy",
});
auditedBrandExecute.mockResolvedValue({
before: null,
after: { value: "/api/media/x" },
output: { url: "/api/media/x" },
});
});
describe("Content legacy wrappers", () => {
@@ -153,10 +179,17 @@ describe("Content legacy wrappers", () => {
"media.upload",
expect.objectContaining({ file }),
);
expect(executeLegacyBrandAssetMutation).toHaveBeenCalledWith(
expect(auditedBrandExecute).toHaveBeenCalledWith(
"favicon.save",
{ file },
expect.objectContaining({
capability: expect.objectContaining({
actor: expect.objectContaining({ id: 42 }),
}),
legacy: true,
}),
);
expect(executeLegacyBrandAssetMutation).not.toHaveBeenCalled();
});
it("preserves the legacy favicon page gate and establishes a staff logo floor", async () => {
@@ -170,10 +203,9 @@ describe("Content legacy wrappers", () => {
expect(requirePermission).not.toHaveBeenCalledWith("settings.edit");
expect(requireStaff).toHaveBeenCalledOnce();
expect(
executeLegacyBrandAssetMutation.mock.calls.map(
([operation]) => operation,
),
auditedBrandExecute.mock.calls.map(([operation]) => operation),
).toEqual(["favicon.save", "favicon.delete", "logo.save"]);
expect(executeLegacyBrandAssetMutation).not.toHaveBeenCalled();
});
it("does not mutate brand assets when either legacy guard denies access", async () => {
@@ -185,12 +217,52 @@ describe("Content legacy wrappers", () => {
"favicon denied",
);
expect(executeLegacyBrandAssetMutation).not.toHaveBeenCalled();
expect(auditedBrandExecute).not.toHaveBeenCalled();
vi.mocked(requireStaff).mockRejectedValueOnce(new Error("logo denied"));
await expect(saveLogo(form({ file }) as FormData)).rejects.toThrow(
"logo denied",
);
expect(executeLegacyBrandAssetMutation).not.toHaveBeenCalled();
expect(auditedBrandExecute).not.toHaveBeenCalled();
});
it("lets a requireStaff-approved actor without an additional ACL use the audited logo boundary", async () => {
vi.clearAllMocks();
vi.mocked(requireStaff).mockResolvedValue(staff as never);
const file = new File(["bytes"], "logo.png", { type: "image/png" });
await expect(saveLogo(form({ file }) as FormData)).resolves.toEqual({
success: true,
url: "/api/media/x",
});
expect(requirePermission).not.toHaveBeenCalled();
expect(requireStaff).toHaveBeenCalledOnce();
expect(auditedBrandExecute).toHaveBeenCalledWith(
"logo.save",
{ file },
expect.objectContaining({
capability: expect.objectContaining({
actor: expect.objectContaining({ id: 42 }),
}),
legacy: true,
}),
);
});
it("refuses a brand mutation when the rehydrated actor changes after the legacy guard", async () => {
vi.mocked(getHousekeepingCapabilityContext).mockResolvedValueOnce({
actor: { id: 99, username: "other", rank: 7 },
isSuperAdmin: false,
has: () => false,
hasAny: () => false,
hasAll: () => false,
} as never);
const file = new File(["bytes"], "logo.png", { type: "image/png" });
await expect(saveLogo(form({ file }) as FormData)).resolves.toEqual({
success: false,
error: "Authenticated staff changed during logo mutation",
});
expect(auditedBrandExecute).not.toHaveBeenCalled();
});
it("keeps every listed legacy action as a thin shared-service wrapper", () => {
@@ -218,7 +290,7 @@ describe("Content legacy wrappers", () => {
const source = readFileSync(path, "utf8");
expect(
source.includes("contentMutationService") ||
source.includes("executeLegacyBrandAssetMutation") ||
source.includes("contentProductionMutationAdapter") ||
source.includes('from "./banners"'),
path,
).toBe(true);