fix(housekeeping): harden poll response paging
This commit is contained in:
1 parent
cbc3075e5a
commit
de94b47cae
2 files changed
+111
-13
No files matched your search
@@ -6,6 +6,7 @@ import {
|
||||
type ContentQueryData,
|
||||
type ContentQueryInput,
|
||||
createContentQuery,
|
||||
type NormalizedContentQueryInput,
|
||||
} from "./content-queries";
|
||||
import {
|
||||
CONTENT_QUERY_DEFINITIONS,
|
||||
@@ -56,6 +57,11 @@ const ready: ContentQueryData = {
|
||||
total: 1,
|
||||
partialDependencies: [],
|
||||
};
|
||||
const normalizedResponsePage = {
|
||||
responseQuestionId: "",
|
||||
responsePageSize: 25,
|
||||
responseOffset: 0,
|
||||
} as const;
|
||||
|
||||
const pollDetail = {
|
||||
kind: "poll-detail" as const,
|
||||
@@ -102,7 +108,7 @@ describe("Content query", () => {
|
||||
const result = await loadProductionContentQuery({
|
||||
routeId: "content.editorial.articles",
|
||||
params: {},
|
||||
list: { search: "", pageSize: 25, offset: 0 },
|
||||
list: { ...normalizedResponsePage, search: "", pageSize: 25, offset: 0 },
|
||||
});
|
||||
expect(result.total).toBe(600);
|
||||
expect(result.items).toHaveLength(25);
|
||||
@@ -121,7 +127,12 @@ describe("Content query", () => {
|
||||
await loadProductionContentQuery({
|
||||
routeId: "content.editorial.articles",
|
||||
params: {},
|
||||
list: { search: "Launch", pageSize: 7, offset: 14 },
|
||||
list: {
|
||||
...normalizedResponsePage,
|
||||
search: "Launch",
|
||||
pageSize: 7,
|
||||
offset: 14,
|
||||
},
|
||||
});
|
||||
const serialized = JSON.stringify(queryMocks.execute.mock.calls);
|
||||
expect(serialized).toContain("%launch%");
|
||||
@@ -136,7 +147,12 @@ describe("Content query", () => {
|
||||
await loadProductionContentQuery({
|
||||
routeId: "content.engagement.events",
|
||||
params: {},
|
||||
list: { search: "launch", pageSize: 25, offset: 0 },
|
||||
list: {
|
||||
...normalizedResponsePage,
|
||||
search: "launch",
|
||||
pageSize: 25,
|
||||
offset: 0,
|
||||
},
|
||||
});
|
||||
const serialized = JSON.stringify(queryMocks.execute.mock.calls.slice(-2));
|
||||
expect(serialized).toContain("COALESCE(title");
|
||||
@@ -171,7 +187,7 @@ describe("Content query", () => {
|
||||
const result = await loadProductionContentQuery({
|
||||
routeId: "content.editorial.article-detail",
|
||||
params: { id: "7" },
|
||||
list: { search: "", pageSize: 25, offset: 0 },
|
||||
list: { ...normalizedResponsePage, search: "", pageSize: 25, offset: 0 },
|
||||
});
|
||||
|
||||
expect(result.items[0]?.privatePayload).toEqual({
|
||||
@@ -208,7 +224,13 @@ describe("Content query", () => {
|
||||
const result = await loadProductionContentQuery({
|
||||
routeId: "content.engagement.events",
|
||||
params: {},
|
||||
list: { search: "", status: "published", pageSize: 25, offset: 0 },
|
||||
list: {
|
||||
...normalizedResponsePage,
|
||||
search: "",
|
||||
status: "published",
|
||||
pageSize: 25,
|
||||
offset: 0,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.items[0]?.privatePayload).toEqual({
|
||||
@@ -245,7 +267,7 @@ describe("Content query", () => {
|
||||
const result = await loadProductionContentQuery({
|
||||
routeId: "content.engagement.event-create",
|
||||
params: {},
|
||||
list: { search: "", pageSize: 25, offset: 0 },
|
||||
list: { ...normalizedResponsePage, search: "", pageSize: 25, offset: 0 },
|
||||
});
|
||||
|
||||
expect(result.items[0]).toMatchObject({
|
||||
@@ -335,7 +357,7 @@ describe("Content query", () => {
|
||||
const result = await loadProductionContentQuery({
|
||||
routeId: "content.engagement.event-detail",
|
||||
params: { id: "11" },
|
||||
list: { search: "", pageSize: 25, offset: 0 },
|
||||
list: { ...normalizedResponsePage, search: "", pageSize: 25, offset: 0 },
|
||||
});
|
||||
|
||||
expect(result.items).toHaveLength(1);
|
||||
@@ -436,6 +458,16 @@ describe("Content query", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("requires response-page defaults in normalized adapter inputs", () => {
|
||||
const incomplete: NormalizedContentQueryInput = {
|
||||
routeId: "content.engagement.events",
|
||||
params: {},
|
||||
// @ts-expect-error Normalized adapter inputs must always include response paging.
|
||||
list: { search: "", pageSize: 25, offset: 0 },
|
||||
};
|
||||
expect(incomplete.routeId).toBe("content.engagement.events");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["mismatched id", "8", pollDetail],
|
||||
[
|
||||
@@ -543,6 +575,64 @@ describe("Content query", () => {
|
||||
}),
|
||||
).toMatchObject({ ok: true });
|
||||
});
|
||||
|
||||
it.each([
|
||||
["an offset at the response total", 1, 1, 1],
|
||||
["a response page extending beyond its total", 3, 2, 2],
|
||||
])("fails closed for %s", async (_label, total, offset, itemCount) => {
|
||||
const items = Array.from({ length: itemCount }, (_, index) => ({
|
||||
id: String(index + 1),
|
||||
questionId: "22",
|
||||
userId: String(index + 1),
|
||||
username: null,
|
||||
answer: "Text",
|
||||
createdAt: "2026-09-02T18:00:00.000Z",
|
||||
}));
|
||||
const query = createContentQuery({
|
||||
load: async () => ({
|
||||
kind: "engagement" as const,
|
||||
items: [
|
||||
{
|
||||
id: "7",
|
||||
title: "Poll",
|
||||
privatePayload: {
|
||||
...pollDetail,
|
||||
questions: [
|
||||
{
|
||||
...pollDetail.questions[0],
|
||||
id: "22",
|
||||
type: "text" as const,
|
||||
options: [],
|
||||
answerCount: total,
|
||||
choiceResults: [],
|
||||
},
|
||||
],
|
||||
textResponses: {
|
||||
questionId: "22",
|
||||
items,
|
||||
total,
|
||||
pageSize: 25,
|
||||
offset,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
partialDependencies: [],
|
||||
}),
|
||||
});
|
||||
|
||||
expect(
|
||||
await query.run(context([PERMS.POLLS_VIEW, PERMS.POLLS_EDIT]), {
|
||||
routeId: "content.engagement.poll-detail",
|
||||
params: { id: "7" },
|
||||
}),
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "DEPENDENCY_UNAVAILABLE" },
|
||||
});
|
||||
});
|
||||
|
||||
it("fails closed when an article detail payload is incomplete", async () => {
|
||||
const query = createContentQuery({
|
||||
load: async () => ({
|
||||
@@ -716,7 +806,7 @@ describe("Content query", () => {
|
||||
const result = await loadProductionContentQuery({
|
||||
routeId: "content.engagement.event-create",
|
||||
params: {},
|
||||
list: { search: "", pageSize: 25, offset: 0 },
|
||||
list: { ...normalizedResponsePage, search: "", pageSize: 25, offset: 0 },
|
||||
});
|
||||
|
||||
expect(result.items).toHaveLength(30);
|
||||
@@ -754,7 +844,12 @@ describe("Content query", () => {
|
||||
loadProductionContentQuery({
|
||||
routeId: "content.engagement.event-detail",
|
||||
params: { id: "11" },
|
||||
list: { search: "", pageSize: 25, offset: 0 },
|
||||
list: {
|
||||
...normalizedResponsePage,
|
||||
search: "",
|
||||
pageSize: 25,
|
||||
offset: 0,
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow("event type limit");
|
||||
|
||||
@@ -784,7 +879,7 @@ describe("Content query", () => {
|
||||
await loadProductionContentQuery({
|
||||
routeId: "content.engagement.event-detail",
|
||||
params: { id: "11" },
|
||||
list: { search: "", pageSize: 25, offset: 0 },
|
||||
list: { ...normalizedResponsePage, search: "", pageSize: 25, offset: 0 },
|
||||
});
|
||||
|
||||
const sql = JSON.stringify(queryMocks.execute.mock.calls);
|
||||
|
||||
@@ -417,6 +417,9 @@ export function isContentPollDetailPayload(
|
||||
!safeCount(responses.offset) ||
|
||||
responses.items.length > responses.pageSize ||
|
||||
responses.total < responses.items.length ||
|
||||
(responses.items.length > 0 &&
|
||||
(responses.offset >= responses.total ||
|
||||
responses.items.length > responses.total - responses.offset)) ||
|
||||
!responses.items.every(isContentPollTextResponsePayload)
|
||||
)
|
||||
return false;
|
||||
@@ -458,9 +461,9 @@ export interface NormalizedContentQueryInput {
|
||||
status?: string;
|
||||
pageSize: number;
|
||||
offset: number;
|
||||
responseQuestionId?: string;
|
||||
responsePageSize?: number;
|
||||
responseOffset?: number;
|
||||
responseQuestionId: string;
|
||||
responsePageSize: number;
|
||||
responseOffset: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user