Files
Epicnabbo-Catalogus-Updated…/src/features/housekeeping/domains/content/content-providers.test.ts
T

164 lines
5.0 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { PERMS } from "@/lib/permission-slugs";
import {
anyCapability,
type HousekeepingCapabilityContext,
} from "../../foundation/contracts";
import { CONTENT_INBOX_SOURCE_IDS, createContentInboxSources } from "./inbox";
import {
CONTENT_SEARCH_PROVIDER_IDS,
createContentSearchProviders,
} from "./search";
import { CONTENT_WIDGET_IDS, createContentWidgets } from "./widgets";
function context(granted: readonly string[]): HousekeepingCapabilityContext {
const permissions = new Set(granted);
return {
actor: { id: 42, 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 search providers", () => {
it("uses the four exact IDs, filters item capabilities, and caps results at 25", async () => {
const visible = anyCapability(PERMS.NEWS_VIEW);
const hidden = anyCapability(PERMS.EVENTS_VIEW);
const candidates = Array.from({ length: 30 }, (_, index) => ({
id: `article-${index}`,
title: `Article ${index}`,
href: `/ase/content/editorial/articles/${index + 1}`,
capability: index === 0 ? hidden : visible,
}));
const providerAdapters = {
articles: vi.fn(async () => candidates),
events: vi.fn(async () => []),
media: vi.fn(async () => []),
help: vi.fn(async () => []),
};
const providers = createContentSearchProviders(providerAdapters);
expect(CONTENT_SEARCH_PROVIDER_IDS).toEqual([
"content.articles",
"content.events",
"content.media",
"content.help",
]);
expect(providers.map((provider) => provider.id)).toEqual(
CONTENT_SEARCH_PROVIDER_IDS,
);
const result = await providers[0].search(context([PERMS.NEWS_VIEW]), {
term: " launch ",
limit: 999,
});
expect(providerAdapters.articles).toHaveBeenCalledWith(
expect.anything(),
"launch",
25,
);
expect(result.ok).toBe(true);
if (!result.ok) return;
expect(result.data).toHaveLength(25);
expect(result.data.some((item) => item.id === "article-0")).toBe(false);
});
it.each([
"/ase/content/%2e%2e/system",
"/ase/content/%252e%252e/system",
"/ase/content/%252f..%252fsystem",
"/ase/content/%255c..%255csystem",
"https://example.test/ase/content/editorial",
"//example.test/ase/content/editorial",
])(
"rejects normalized and double-encoded traversal href %s",
async (href) => {
const adapters = {
articles: async () => [
{
id: "unsafe",
title: "Unsafe",
href,
capability: anyCapability(PERMS.NEWS_VIEW),
},
],
events: async () => [],
media: async () => [],
help: async () => [],
};
const [provider] = createContentSearchProviders(adapters);
const result = await provider.search(context([PERMS.NEWS_VIEW]), {
term: "",
limit: 25,
});
expect(result).toMatchObject({ ok: true, data: [] });
},
);
});
describe("Content inbox and widgets", () => {
it("provides capability-selective publication and attention sources", async () => {
const publication = vi.fn(async () => []);
const attention = vi.fn(async () => []);
const sources = createContentInboxSources({ publication, attention });
expect(CONTENT_INBOX_SOURCE_IDS).toEqual([
"content.publication",
"content.attention",
]);
const controller = new AbortController();
const news = context([PERMS.NEWS_VIEW]);
await sources[0].getItems(news, controller.signal);
const forbidden = await sources[1].getItems(news, controller.signal);
expect(publication).toHaveBeenCalledTimes(1);
expect(attention).not.toHaveBeenCalled();
expect(forbidden).toMatchObject({
ok: false,
error: { code: "FORBIDDEN" },
});
});
it("does not advertise media permissions for an event-and-poll attention source", async () => {
const attention = vi.fn(async () => []);
const [, source] = createContentInboxSources({
publication: async () => [],
attention,
});
const result = await source.getItems(
context([PERMS.PAGES_VIEW, PERMS.BANNERS_VIEW]),
new AbortController().signal,
);
expect(result).toMatchObject({ ok: false, error: { code: "FORBIDDEN" } });
expect(attention).not.toHaveBeenCalled();
});
it("keeps editorial mandatory and media/localization optional without preview DB imports", async () => {
const adapters = {
editorial: vi.fn(async () => ({ drafts: 2, scheduled: 1 })),
media: vi.fn(async () => ({ items: 4 })),
localization: vi.fn(async () => ({ stores: 3, pending: 0 })),
};
const widgets = createContentWidgets(adapters);
expect(CONTENT_WIDGET_IDS).toEqual([
"content.editorial-summary",
"content.media-summary",
"content.localization-summary",
]);
expect(widgets.map((widget) => widget.kind)).toEqual([
"mandatory",
"optional",
"optional",
]);
const result = await widgets[0].load(
context([PERMS.NEWS_VIEW]),
new AbortController().signal,
);
expect(result).toMatchObject({
ok: true,
data: { drafts: 2, scheduled: 1 },
});
});
});