216 lines
6.3 KiB
TypeScript
216 lines
6.3 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { PERMS } from "@/lib/permission-slugs";
|
|
import type { HousekeepingCapabilityContext } from "../../../foundation/contracts";
|
|
import { CONTENT_ROUTE_IDS } from "../routes";
|
|
import {
|
|
type ContentQueryData,
|
|
type ContentQueryInput,
|
|
createContentQuery,
|
|
} from "./content-queries";
|
|
import {
|
|
CONTENT_QUERY_DEFINITIONS,
|
|
loadProductionContentQuery,
|
|
} from "./content-queries-production";
|
|
|
|
const queryMocks = vi.hoisted(() => ({
|
|
execute: vi.fn(),
|
|
join: vi.fn((chunks: unknown[], separator: unknown) => ({
|
|
chunks,
|
|
separator,
|
|
})),
|
|
raw: vi.fn((statement: string) => ({ statement })),
|
|
tagged: vi.fn((strings: TemplateStringsArray, ...values: unknown[]) => ({
|
|
strings: Array.from(strings),
|
|
values,
|
|
})),
|
|
}));
|
|
vi.mock("drizzle-orm", () => ({
|
|
sql: Object.assign(queryMocks.tagged, {
|
|
join: queryMocks.join,
|
|
raw: queryMocks.raw,
|
|
}),
|
|
}));
|
|
vi.mock("@/lib/db", () => ({ db: { execute: queryMocks.execute } }));
|
|
|
|
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)),
|
|
};
|
|
}
|
|
|
|
const ready: ContentQueryData = {
|
|
kind: "editorial",
|
|
items: [
|
|
{
|
|
id: "18446744073709551615",
|
|
title: "Published article",
|
|
status: "published",
|
|
href: "/ase/content/editorial/articles/18446744073709551615",
|
|
},
|
|
],
|
|
total: 1,
|
|
partialDependencies: [],
|
|
};
|
|
|
|
describe("Content query", () => {
|
|
it("reports totals beyond the former 500-row adapter cap", async () => {
|
|
const pageRows = Array.from({ length: 25 }, (_, index) => ({
|
|
id: index + 1,
|
|
title: `Article ${index + 1}`,
|
|
status: "published",
|
|
updated_at: null,
|
|
}));
|
|
queryMocks.execute
|
|
.mockResolvedValueOnce([[{ total: 600 }]])
|
|
.mockResolvedValueOnce([pageRows]);
|
|
const result = await loadProductionContentQuery({
|
|
routeId: "content.editorial.articles",
|
|
params: {},
|
|
list: { search: "", pageSize: 25, offset: 0 },
|
|
});
|
|
expect(result.total).toBe(600);
|
|
expect(result.items).toHaveLength(25);
|
|
expect(queryMocks.execute).toHaveBeenCalledTimes(2);
|
|
const pageQuery = queryMocks.execute.mock.calls[1]?.[0];
|
|
expect(JSON.stringify(pageQuery)).toContain("25");
|
|
expect(JSON.stringify(pageQuery)).toContain("0");
|
|
});
|
|
|
|
it("binds search, limit, and offset into the bounded page query", async () => {
|
|
queryMocks.execute
|
|
.mockResolvedValueOnce([[{ total: 1 }]])
|
|
.mockResolvedValueOnce([
|
|
[{ id: 9, title: "Launch", status: "published", updated_at: null }],
|
|
]);
|
|
await loadProductionContentQuery({
|
|
routeId: "content.editorial.articles",
|
|
params: {},
|
|
list: { search: "Launch", pageSize: 7, offset: 14 },
|
|
});
|
|
const serialized = JSON.stringify(queryMocks.execute.mock.calls);
|
|
expect(serialized).toContain("%launch%");
|
|
expect(serialized).toContain("7");
|
|
expect(serialized).toContain("14");
|
|
});
|
|
|
|
it("searches only projected aliases for routes without descriptions", async () => {
|
|
queryMocks.execute
|
|
.mockResolvedValueOnce([[{ total: 0 }]])
|
|
.mockResolvedValueOnce([[]]);
|
|
await loadProductionContentQuery({
|
|
routeId: "content.engagement.events",
|
|
params: {},
|
|
list: { search: "launch", pageSize: 25, offset: 0 },
|
|
});
|
|
const serialized = JSON.stringify(queryMocks.execute.mock.calls.slice(-2));
|
|
expect(serialized).toContain("COALESCE(title");
|
|
expect(serialized).not.toContain("COALESCE(description");
|
|
});
|
|
|
|
it("never selects email template bodies into summary query results", () => {
|
|
const emailTemplates =
|
|
CONTENT_QUERY_DEFINITIONS["content.help.email-templates"];
|
|
expect(emailTemplates.statement).not.toMatch(/\bbody\b|private_payload/iu);
|
|
});
|
|
|
|
it("fails closed before production adapters are invoked", async () => {
|
|
const load = vi.fn(async () => ready);
|
|
const query = createContentQuery({ load });
|
|
|
|
const result = await query.run(context([]), {
|
|
routeId: "content.editorial.articles",
|
|
list: { search: "", pageSize: 25, offset: 0 },
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
ok: false,
|
|
error: { code: "FORBIDDEN" },
|
|
});
|
|
expect(load).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("bounds list input, preserves canonical BIGINT strings, and returns real adapter data", async () => {
|
|
const load = vi.fn(async (_input: ContentQueryInput) => ready);
|
|
const query = createContentQuery({ load });
|
|
|
|
const result = await query.run(context([PERMS.NEWS_VIEW]), {
|
|
routeId: "content.editorial.articles",
|
|
list: {
|
|
search: " launch ",
|
|
pageSize: 1000,
|
|
offset: 999_999,
|
|
},
|
|
});
|
|
|
|
expect(load).toHaveBeenCalledWith({
|
|
routeId: "content.editorial.articles",
|
|
params: {},
|
|
list: { search: "launch", pageSize: 100, offset: 100_000 },
|
|
});
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
data: {
|
|
items: [{ id: "18446744073709551615" }],
|
|
total: 1,
|
|
partialDependencies: [],
|
|
},
|
|
});
|
|
});
|
|
|
|
it("rejects a fake partial result and maps complete adapter failure", async () => {
|
|
const malformed = createContentQuery({
|
|
load: async () =>
|
|
({
|
|
kind: "media",
|
|
items: [],
|
|
total: 0,
|
|
partialDependencies: ["imaginary"],
|
|
}) as ContentQueryData,
|
|
});
|
|
const unavailable = createContentQuery({
|
|
load: async () => {
|
|
throw new Error("database unavailable");
|
|
},
|
|
});
|
|
|
|
expect(
|
|
await malformed.run(context([PERMS.PAGES_VIEW]), {
|
|
routeId: "content.media.library",
|
|
}),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: { code: "DEPENDENCY_UNAVAILABLE" },
|
|
});
|
|
expect(
|
|
await unavailable.run(context([PERMS.PAGES_VIEW]), {
|
|
routeId: "content.media.library",
|
|
}),
|
|
).toMatchObject({
|
|
ok: false,
|
|
error: { code: "DEPENDENCY_UNAVAILABLE" },
|
|
});
|
|
});
|
|
|
|
it("has an authorized production query path for every Content route", async () => {
|
|
const load = vi.fn(async (input: ContentQueryInput) => ({
|
|
kind: input.routeId.split(".")[1] as ContentQueryData["kind"],
|
|
items: [],
|
|
total: 0,
|
|
partialDependencies: [],
|
|
}));
|
|
const query = createContentQuery({ load });
|
|
const all = context(Object.values(PERMS));
|
|
|
|
for (const routeId of CONTENT_ROUTE_IDS) {
|
|
const result = await query.run(all, { routeId });
|
|
expect(result.ok, routeId).toBe(true);
|
|
}
|
|
expect(load).toHaveBeenCalledTimes(CONTENT_ROUTE_IDS.length);
|
|
});
|
|
});
|