feat(housekeeping): deliver content vertical
This commit is contained in:
1 parent
bdab924bdf
commit
fd68819d9b
67 files changed
+5742
-1916
No files matched your search
@@ -0,0 +1,149 @@
|
||||
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("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 },
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user