173 lines
4.5 KiB
TypeScript
173 lines
4.5 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { PERMS } from "@/lib/permission-slugs";
|
|
import type { HousekeepingCapabilityContext } from "../../foundation/contracts";
|
|
import { loadContentInboxItems } from "./inbox-production";
|
|
import { loadContentSearchCandidates } from "./search-production";
|
|
import { loadContentWidget } from "./widgets-production";
|
|
|
|
const { run } = vi.hoisted(() => ({ run: vi.fn() }));
|
|
|
|
vi.mock("./queries/content-queries", () => ({
|
|
contentQuery: { run },
|
|
}));
|
|
|
|
const context = {
|
|
actor: { id: 42, username: "operator", rank: 7 },
|
|
isSuperAdmin: false,
|
|
has: () => true,
|
|
hasAny: () => true,
|
|
hasAll: () => true,
|
|
} satisfies HousekeepingCapabilityContext;
|
|
|
|
function capability(granted: readonly string[]): HousekeepingCapabilityContext {
|
|
const permissions = new Set(granted);
|
|
return {
|
|
...context,
|
|
has: (slug) => permissions.has(slug),
|
|
hasAny: (...slugs) => slugs.some((slug) => permissions.has(slug)),
|
|
hasAll: (...slugs) => slugs.every((slug) => permissions.has(slug)),
|
|
};
|
|
}
|
|
|
|
function result(
|
|
routeId: string,
|
|
total: number,
|
|
items: readonly Record<string, unknown>[] = [],
|
|
) {
|
|
return {
|
|
ok: true as const,
|
|
data: {
|
|
kind: routeId.split(".")[1],
|
|
items,
|
|
total,
|
|
partialDependencies: [],
|
|
},
|
|
correlationId: "provider-production",
|
|
};
|
|
}
|
|
|
|
describe("Content production providers", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
run.mockImplementation(async (_context, input) =>
|
|
result(input.routeId, input.routeId.includes("articles") ? 7 : 3),
|
|
);
|
|
});
|
|
|
|
it("returns only truthful persisted counts from editorial and localization widgets", async () => {
|
|
const signal = new AbortController().signal;
|
|
await expect(
|
|
loadContentWidget("editorial", context, signal),
|
|
).resolves.toEqual({ articles: 7 });
|
|
await expect(
|
|
loadContentWidget("localization", context, signal),
|
|
).resolves.toEqual({ stores: 3 });
|
|
});
|
|
|
|
it("loads real search candidates from bounded query routes", async () => {
|
|
run.mockImplementation(async (_context, input) =>
|
|
result(input.routeId, 1, [
|
|
{
|
|
id: "9",
|
|
title: "Launch",
|
|
description: "Published",
|
|
href: "/ase/content/editorial/articles/9",
|
|
},
|
|
]),
|
|
);
|
|
|
|
const candidates = await loadContentSearchCandidates(
|
|
"articles",
|
|
context,
|
|
"launch",
|
|
25,
|
|
);
|
|
expect(candidates).toEqual([
|
|
expect.objectContaining({
|
|
id: "content.editorial.articles:9",
|
|
href: "/ase/content/editorial/articles/9",
|
|
}),
|
|
]);
|
|
expect(run).toHaveBeenCalledWith(
|
|
context,
|
|
expect.objectContaining({
|
|
list: { search: "launch", pageSize: 25, offset: 0 },
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("builds publication inbox items only from real query rows", async () => {
|
|
run.mockImplementation(async (_context, input) =>
|
|
result(input.routeId, 1, [
|
|
{
|
|
id: "5",
|
|
title: "Release",
|
|
status: "published",
|
|
updatedAt: new Date().toISOString(),
|
|
href: "/ase/content/editorial/articles/5",
|
|
},
|
|
]),
|
|
);
|
|
|
|
const items = await loadContentInboxItems(
|
|
"publication",
|
|
context,
|
|
new AbortController().signal,
|
|
);
|
|
expect(items).toEqual([]);
|
|
});
|
|
|
|
it("loads only capability-matched media counts", async () => {
|
|
const result = await loadContentWidget(
|
|
"media",
|
|
capability([PERMS.BANNERS_VIEW]),
|
|
new AbortController().signal,
|
|
);
|
|
expect(result).toEqual({ banners: 3 });
|
|
expect(run).toHaveBeenCalledTimes(1);
|
|
expect(run).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({ routeId: "content.media.banners" }),
|
|
);
|
|
});
|
|
|
|
it("does not invent a zero when a widget dependency is unavailable", async () => {
|
|
run.mockResolvedValueOnce({
|
|
ok: false,
|
|
error: { code: "DEPENDENCY_UNAVAILABLE", messageKey: "dependency" },
|
|
correlationId: "unavailable",
|
|
});
|
|
await expect(
|
|
loadContentWidget("editorial", context, new AbortController().signal),
|
|
).rejects.toThrow("Content widget query unavailable");
|
|
});
|
|
|
|
it("emits only actionable publication statuses", async () => {
|
|
run.mockResolvedValueOnce(
|
|
result("content.editorial.articles", 2, [
|
|
{
|
|
id: "draft",
|
|
title: "Draft",
|
|
status: "draft",
|
|
updatedAt: new Date().toISOString(),
|
|
href: "/ase/content/editorial/articles/draft",
|
|
},
|
|
{
|
|
id: "published",
|
|
title: "Published",
|
|
status: "published",
|
|
updatedAt: new Date().toISOString(),
|
|
href: "/ase/content/editorial/articles/published",
|
|
},
|
|
]),
|
|
);
|
|
const items = await loadContentInboxItems(
|
|
"publication",
|
|
context,
|
|
new AbortController().signal,
|
|
);
|
|
expect(items.map((item) => item.itemId)).toEqual(["draft"]);
|
|
expect(items[0]?.state).toBe("draft");
|
|
});
|
|
});
|