fix(housekeeping): address task 14 review round 1

This commit is contained in:
Simo committed 2026-08-30 10:53:50 +02:00
1 parent fd68819d9b
commit d09eaa33d6
23 files changed
+2503 -488

No files matched your search

+69 -10
View File
@@ -2,11 +2,12 @@
import { readFileSync } from "node:fs";
import { redirect } from "next/navigation";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { requirePermission } from "@/lib/admin/guard";
import { requirePermission, requireStaff } from "@/lib/admin/guard";
import { createAd } from "./admin-ads";
import { createArticle } from "./admin-articles";
import { uploadMedia } from "./admin-media";
import { saveFavicon } from "./save-favicon";
import { deleteFavicon, saveFavicon } from "./save-favicon";
import { saveLogo } from "./save-logo";
const { execute } = vi.hoisted(() => ({
execute: vi.fn(async () => ({
@@ -15,6 +16,13 @@ const { execute } = vi.hoisted(() => ({
correlationId: "legacy",
})),
}));
const { executeLegacyBrandAssetMutation } = vi.hoisted(() => ({
executeLegacyBrandAssetMutation: vi.fn(async () => ({
before: null,
after: { value: "/api/media/x" },
output: { url: "/api/media/x" },
})),
}));
vi.mock("@/features/housekeeping/domains/content/services/mutations", () => ({
contentMutationService: { execute },
@@ -24,7 +32,16 @@ vi.mock("@/features/housekeeping/domains/content/services/mutations", () => ({
legacy: true,
}),
}));
vi.mock("@/lib/admin/guard", () => ({ requirePermission: vi.fn() }));
vi.mock(
"@/features/housekeeping/domains/content/services/mutation-runtime-external",
() => ({
executeLegacyBrandAssetMutation,
}),
);
vi.mock("@/lib/admin/guard", () => ({
requirePermission: vi.fn(),
requireStaff: vi.fn(),
}));
vi.mock("@/lib/safe-action", () => ({
adminAction: (_options: unknown, handler: unknown) => handler,
}));
@@ -40,6 +57,7 @@ vi.mock("@/lib/permissions", () => ({
NEWS_EDIT: "news.edit",
PAGES_EDIT: "pages.edit",
SETTINGS_EDIT: "settings.edit",
SETTINGS_VIEW: "settings.view",
},
}));
vi.mock("@/lib/db", () => ({
@@ -84,6 +102,7 @@ const form = (data: Record<string, FormDataEntryValue>) => ({
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(requirePermission).mockResolvedValue(staff as never);
vi.mocked(requireStaff).mockResolvedValue(staff as never);
execute.mockResolvedValue({
ok: true,
data: { before: null, after: { id: "1" }, output: { url: "/api/media/x" } },
@@ -111,7 +130,9 @@ describe("Content legacy wrappers", () => {
it("delegates ad creation and keeps the legacy void/redirect contract", async () => {
expect(
await createAd(form({ image: "https://example.test/ad.png" }) as FormData),
await createAd(
form({ image: "https://example.test/ad.png" }) as FormData,
),
).toBeUndefined();
expect(execute).toHaveBeenCalledWith(
expect.objectContaining({ expectedActorId: 42, legacy: true }),
@@ -132,17 +153,51 @@ describe("Content legacy wrappers", () => {
"media.upload",
expect.objectContaining({ file }),
);
expect(execute).toHaveBeenCalledWith(
expect.anything(),
expect(executeLegacyBrandAssetMutation).toHaveBeenCalledWith(
"favicon.save",
expect.objectContaining({ file }),
{ file },
);
});
it("preserves the legacy favicon page gate and establishes a staff logo floor", async () => {
vi.clearAllMocks();
const file = new File(["bytes"], "image.png", { type: "image/png" });
await saveFavicon(form({ file }) as FormData);
await deleteFavicon();
await saveLogo(form({ file }) as FormData);
expect(requirePermission).toHaveBeenNthCalledWith(1, "settings.view");
expect(requirePermission).toHaveBeenNthCalledWith(2, "settings.view");
expect(requirePermission).not.toHaveBeenCalledWith("settings.edit");
expect(requireStaff).toHaveBeenCalledOnce();
expect(
executeLegacyBrandAssetMutation.mock.calls.map(
([operation]) => operation,
),
).toEqual(["favicon.save", "favicon.delete", "logo.save"]);
});
it("does not mutate brand assets when either legacy guard denies access", async () => {
const file = new File(["bytes"], "image.png", { type: "image/png" });
vi.mocked(requirePermission).mockRejectedValueOnce(
new Error("favicon denied"),
);
await expect(saveFavicon(form({ file }) as FormData)).rejects.toThrow(
"favicon denied",
);
expect(executeLegacyBrandAssetMutation).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();
});
it("keeps every listed legacy action as a thin shared-service wrapper", () => {
for (const path of [
"src/actions/admin-ads.ts",
"src/actions/admin-articles.ts",
"src/actions/admin-banners.ts",
"src/actions/admin-email-templates.ts",
"src/actions/admin-help.ts",
"src/actions/admin-media.ts",
@@ -160,9 +215,13 @@ describe("Content legacy wrappers", () => {
"src/actions/translations.ts",
"src/actions/emulator.ts",
]) {
expect(readFileSync(path, "utf8"), path).toContain(
"contentMutationService",
);
const source = readFileSync(path, "utf8");
expect(
source.includes("contentMutationService") ||
source.includes("executeLegacyBrandAssetMutation") ||
source.includes('from "./banners"'),
path,
).toBe(true);
}
});
});