Files
Epicnabbo-Catalogus-Updated…/src/actions/content-legacy-parity.test.ts
T

348 lines
11 KiB
TypeScript

// @ts-nocheck
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";
import { uploadMedia } from "./admin-media";
import { deleteFavicon, saveFavicon } from "./save-favicon";
import { saveLogo } from "./save-logo";
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 () => ({
before: null,
after: { value: "/api/media/x" },
output: { url: "/api/media/x" },
})),
}));
vi.mock("@/features/housekeeping/domains/content/services/mutations", () => ({
contentMutationService: { execute },
createContentMutationInvocation: (actor, correlationId) => ({
expectedActorId: actor.id,
correlationId,
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",
() => ({
executeLegacyBrandAssetMutation,
}),
);
vi.mock("@/lib/admin/guard", () => ({
requirePermission: vi.fn(),
requireStaff: vi.fn(),
}));
vi.mock("@/lib/safe-action", () => ({
adminAction: (_options: unknown, handler: unknown) => handler,
}));
vi.mock("@/lib/safe-action-shared", () => ({
ActionError: class ActionError extends Error {},
actionOk: (data: unknown = {}) => ({ ok: true, data }),
}));
vi.mock("@/lib/logger", () => ({
logger: { error: vi.fn() },
}));
vi.mock("@/lib/permissions", () => ({
PERMS: {
NEWS_EDIT: "news.edit",
PAGES_EDIT: "pages.edit",
SETTINGS_EDIT: "settings.edit",
SETTINGS_VIEW: "settings.view",
},
}));
vi.mock("@/lib/db", () => ({
db: {
select: vi.fn(() => ({
from: vi.fn(() => ({
where: vi.fn(() => ({ limit: vi.fn(async () => []) })),
})),
})),
insert: vi.fn(() => ({
values: vi.fn(async () => [{ insertId: 1 }]),
})),
},
WebsiteArticles: { id: "id", slug: "slug" },
WebsiteAds: { id: "id" },
WebsiteSetting: { key: "key" },
}));
vi.mock("@/lib/services/staff-activity", () => ({
logStaffActivity: vi.fn(),
}));
vi.mock("@/lib/services/site-settings", () => ({
siteSettings: { get: vi.fn(), reload: vi.fn() },
}));
vi.mock("@/lib/media-storage", () => ({
MEDIA_ROOT: "C:\\media",
resolveMediaPath: vi.fn((name: string) => `C:\\media\\${name}`),
}));
vi.mock("node:fs/promises", () => ({
mkdir: vi.fn(),
writeFile: vi.fn(),
unlink: vi.fn(),
}));
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
vi.mock("next/navigation", () => ({ redirect: vi.fn() }));
const staff = { id: 42, rank: 7, username: "operator" };
const form = (data: Record<string, FormDataEntryValue>) => ({
get: (key: string) => data[key] ?? null,
has: (key: string) => key in data,
});
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" } },
correlationId: "legacy",
});
auditedBrandExecute.mockResolvedValue({
before: null,
after: { value: "/api/media/x" },
output: { url: "/api/media/x" },
});
});
describe("Content legacy wrappers", () => {
it("delegates article creation and preserves redirect ordering", async () => {
await createArticle(
form({
title: "Launch",
shortStory: "Summary",
fullStory: "Body",
image: "/image.png",
}) as FormData,
);
expect(execute).toHaveBeenCalledWith(
expect.objectContaining({ expectedActorId: 42, legacy: true }),
"article.change",
expect.objectContaining({ action: "create", title: "Launch" }),
);
expect(redirect).toHaveBeenCalledWith("/admin/articles");
});
it("delegates ad creation and keeps the legacy void/redirect contract", async () => {
expect(
await createAd(
form({ image: "https://example.test/ad.png" }) as FormData,
),
).toBeUndefined();
expect(execute).toHaveBeenCalledWith(
expect.objectContaining({ expectedActorId: 42, legacy: true }),
"ad.change",
expect.objectContaining({ action: "create" }),
);
expect(redirect).toHaveBeenCalledWith("/admin/ads");
});
it("delegates media and favicon uploads while retaining public result shapes", async () => {
const file = new File(["bytes"], "image.png", { type: "image/png" });
const media = await uploadMedia(form({ file }) as FormData);
const favicon = await saveFavicon(form({ file }) as FormData);
expect(media).toEqual({ ok: true });
expect(favicon).toEqual({ success: true, url: "/api/media/x" });
expect(execute).toHaveBeenCalledWith(
expect.anything(),
"media.upload",
expect.objectContaining({ file }),
);
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 () => {
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(
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 () => {
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();
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("maps a favicon audit partial to the truthful legacy result shape", async () => {
auditedBrandExecute.mockResolvedValueOnce({
before: { value: "/old.ico" },
after: { value: "/api/media/favicon/new.ico" },
output: { url: "/api/media/favicon/new.ico" },
completion: {
status: "partial",
external: "completed",
audit: "unavailable",
},
});
const file = new File(["bytes"], "favicon.png", { type: "image/png" });
await expect(saveFavicon(form({ file }) as FormData)).resolves.toEqual({
success: false,
url: "/api/media/favicon/new.ico",
error:
"Favicon change completed partially; verify storage and audit state",
});
});
it("maps a logo audit partial to the truthful legacy result shape", async () => {
auditedBrandExecute.mockResolvedValueOnce({
before: { value: "/old.png" },
after: { value: "/api/media/logo/new.png" },
output: { url: "/api/media/logo/new.png" },
completion: {
status: "partial",
external: "completed",
audit: "unavailable",
},
});
const file = new File(["bytes"], "logo.png", { type: "image/png" });
await expect(saveLogo(form({ file }) as FormData)).resolves.toEqual({
success: false,
url: "/api/media/logo/new.png",
error: "Logo change completed partially; verify storage and audit state",
});
});
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",
"src/actions/admin-nav-menu.ts",
"src/actions/admin-photos.ts",
"src/actions/admin-tags.ts",
"src/actions/admin-theme.ts",
"src/actions/admin-writeable-boxes.ts",
"src/actions/banners.ts",
"src/actions/events.ts",
"src/actions/polls.ts",
"src/actions/prefixes.ts",
"src/actions/save-favicon.ts",
"src/actions/save-logo.ts",
"src/actions/translations.ts",
"src/actions/emulator.ts",
]) {
const source = readFileSync(path, "utf8");
expect(
source.includes("contentMutationService") ||
source.includes("contentProductionMutationAdapter") ||
source.includes('from "./banners"'),
path,
).toBe(true);
}
});
it("routes the real banner manager through the required compatibility action", () => {
const source = readFileSync(
"src/app/admin/banners/banners-manager.tsx",
"utf8",
);
expect(source).toContain('from "@/actions/admin-banners"');
expect(source).not.toContain('from "@/actions/banners"');
});
});