feat(housekeeping): define typed poll query contract
This commit is contained in:
1 parent
e69633b753
commit
cbc3075e5a
4 files changed
+432
-8
No files matched your search
@@ -57,6 +57,35 @@ const ready: ContentQueryData = {
|
||||
partialDependencies: [],
|
||||
};
|
||||
|
||||
const pollDetail = {
|
||||
kind: "poll-detail" as const,
|
||||
description: "Complete poll",
|
||||
showResults: true,
|
||||
multipleChoice: false,
|
||||
startsAt: "2026-09-02T18:00:00.000Z",
|
||||
endsAt: null,
|
||||
questionCount: 1,
|
||||
voterCount: 2,
|
||||
answerCount: 2,
|
||||
questions: [
|
||||
{
|
||||
id: "21",
|
||||
question: "Favourite colour?",
|
||||
type: "single" as const,
|
||||
sortOrder: 0,
|
||||
options: ["Red", "Blue"],
|
||||
answerCount: 2,
|
||||
choiceResults: [{ option: "Red", count: 2 }],
|
||||
},
|
||||
],
|
||||
textResponses: {
|
||||
questionId: null,
|
||||
items: [],
|
||||
total: 0,
|
||||
pageSize: 25,
|
||||
offset: 0,
|
||||
},
|
||||
};
|
||||
describe("Content query", () => {
|
||||
beforeEach(() => queryMocks.execute.mockReset());
|
||||
|
||||
@@ -355,7 +384,14 @@ describe("Content query", () => {
|
||||
expect(load).toHaveBeenCalledWith({
|
||||
routeId: "content.editorial.articles",
|
||||
params: {},
|
||||
list: { search: "launch", pageSize: 100, offset: 100_000 },
|
||||
list: {
|
||||
search: "launch",
|
||||
pageSize: 100,
|
||||
offset: 100_000,
|
||||
responseQuestionId: "",
|
||||
responsePageSize: 25,
|
||||
responseOffset: 0,
|
||||
},
|
||||
});
|
||||
expect(result).toMatchObject({
|
||||
ok: true,
|
||||
@@ -367,6 +403,146 @@ describe("Content query", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes bounded poll response-page input before invoking the adapter", async () => {
|
||||
const load = vi.fn(async () => ({
|
||||
kind: "engagement" as const,
|
||||
items: [],
|
||||
total: 0,
|
||||
partialDependencies: [],
|
||||
}));
|
||||
const query = createContentQuery({ load });
|
||||
|
||||
await query.run(context([PERMS.POLLS_VIEW, PERMS.POLLS_EDIT]), {
|
||||
routeId: "content.engagement.poll-detail",
|
||||
params: { id: "7" },
|
||||
list: {
|
||||
responseQuestionId: " 22 ",
|
||||
responsePageSize: 50,
|
||||
responseOffset: 100_000,
|
||||
},
|
||||
});
|
||||
|
||||
expect(load).toHaveBeenCalledWith({
|
||||
routeId: "content.engagement.poll-detail",
|
||||
params: { id: "7" },
|
||||
list: {
|
||||
search: "",
|
||||
pageSize: 25,
|
||||
offset: 0,
|
||||
responseQuestionId: "22",
|
||||
responsePageSize: 50,
|
||||
responseOffset: 100_000,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
["mismatched id", "8", pollDetail],
|
||||
[
|
||||
"too many questions",
|
||||
"7",
|
||||
{ ...pollDetail, questions: Array(101).fill(pollDetail.questions[0]) },
|
||||
],
|
||||
[
|
||||
"invalid response user",
|
||||
"7",
|
||||
{
|
||||
...pollDetail,
|
||||
textResponses: {
|
||||
questionId: "22",
|
||||
items: [
|
||||
{
|
||||
id: "1",
|
||||
questionId: "22",
|
||||
userId: "0",
|
||||
username: null,
|
||||
answer: "Text",
|
||||
createdAt: "2026-09-02T18:00:00.000Z",
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
pageSize: 25,
|
||||
offset: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
] as const)(
|
||||
"fails closed for poll detail with %s",
|
||||
async (_label, itemId, payload) => {
|
||||
const query = createContentQuery({
|
||||
load: async () => ({
|
||||
kind: "engagement" as const,
|
||||
items: [{ id: itemId, title: "Poll", privatePayload: payload }],
|
||||
total: 1,
|
||||
partialDependencies: [],
|
||||
}),
|
||||
});
|
||||
const result = await query.run(
|
||||
context([PERMS.POLLS_VIEW, PERMS.POLLS_EDIT]),
|
||||
{
|
||||
routeId: "content.engagement.poll-detail",
|
||||
params: { id: "7" },
|
||||
},
|
||||
);
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "DEPENDENCY_UNAVAILABLE" },
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("fails closed when poll creation does not return the exact create payload", async () => {
|
||||
const query = createContentQuery({
|
||||
load: async () => ({
|
||||
kind: "engagement" as const,
|
||||
items: [
|
||||
{ id: "7", title: "Poll", privatePayload: { kind: "poll-create" } },
|
||||
],
|
||||
total: 1,
|
||||
partialDependencies: [],
|
||||
}),
|
||||
});
|
||||
expect(
|
||||
await query.run(context([PERMS.POLLS_EDIT]), {
|
||||
routeId: "content.engagement.poll-create",
|
||||
}),
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "DEPENDENCY_UNAVAILABLE" },
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts an empty poll response page past its total", async () => {
|
||||
const query = createContentQuery({
|
||||
load: async () => ({
|
||||
kind: "engagement" as const,
|
||||
items: [
|
||||
{
|
||||
id: "7",
|
||||
title: "Poll",
|
||||
privatePayload: {
|
||||
...pollDetail,
|
||||
textResponses: {
|
||||
questionId: null,
|
||||
items: [],
|
||||
total: 1,
|
||||
pageSize: 25,
|
||||
offset: 100_000,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
partialDependencies: [],
|
||||
}),
|
||||
});
|
||||
expect(
|
||||
await query.run(context([PERMS.POLLS_VIEW, PERMS.POLLS_EDIT]), {
|
||||
routeId: "content.engagement.poll-detail",
|
||||
params: { id: "7" },
|
||||
}),
|
||||
).toMatchObject({ ok: true });
|
||||
});
|
||||
it("fails closed when an article detail payload is incomplete", async () => {
|
||||
const query = createContentQuery({
|
||||
load: async () => ({
|
||||
@@ -417,6 +593,9 @@ describe("Content query", () => {
|
||||
status: `published${"x".repeat(23)}`,
|
||||
pageSize: 25,
|
||||
offset: 0,
|
||||
responseQuestionId: "",
|
||||
responsePageSize: 25,
|
||||
responseOffset: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -488,19 +667,31 @@ describe("Content query", () => {
|
||||
});
|
||||
|
||||
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 load = vi.fn(async (input: ContentQueryInput) => {
|
||||
const isPollCreate = input.routeId === "content.engagement.poll-create";
|
||||
return {
|
||||
kind: input.routeId.split(".")[1] as ContentQueryData["kind"],
|
||||
items: isPollCreate
|
||||
? [
|
||||
{
|
||||
id: "create",
|
||||
title: "Create poll",
|
||||
privatePayload: { kind: "poll-create" },
|
||||
},
|
||||
]
|
||||
: [],
|
||||
total: isPollCreate ? 1 : 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,
|
||||
...(routeId === "content.engagement.event-detail"
|
||||
...(routeId === "content.engagement.event-detail" ||
|
||||
routeId === "content.engagement.poll-detail"
|
||||
? { params: { id: "1" } }
|
||||
: {}),
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user