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 }, }); }); });