import { describe, expect, it } from "vitest"; import { ApiError } from "../../common/errors.js"; import { DEFAULT_PAGE_SIZE, decodeCursor, encodeCursor, paginate, parsePagination } from "../pagination.js"; describe("pagination", () => { it("defaults: limit 50, no cursor", () => { const p = parsePagination({}); expect(p.limit).toBe(DEFAULT_PAGE_SIZE); expect(p.cursor).toBeUndefined(); }); it("caps limit at 200 and rejects garbage", () => { expect(parsePagination({ limit: "99999" }).limit).toBe(200); expect(parsePagination({ limit: 5 }).limit).toBe(5); expect(() => parsePagination({ limit: "abc" })).toThrowError(ApiError); expect(() => parsePagination({ limit: "0" })).toThrowError(ApiError); expect(() => parsePagination({ limit: "-3" })).toThrowError(ApiError); }); it("encodes and decodes cursors losslessly", () => { const c = encodeCursor("row-123", "position-xyz"); expect(typeof c).toBe("string"); expect(decodeCursor(c)).toEqual({ id: "row-123", position: "position-xyz" }); }); it("rejects malformed cursors with VALIDATION_ERROR", () => { expect(() => decodeCursor("%%%not-base64-json")).toThrowError(ApiError); expect(() => decodeCursor(Buffer.from(JSON.stringify({ id: 42 })).toString("base64url"))).toThrowError( ApiError ); }); it("paginate: fetches limit+1, drops the extra, encodes nextCursor from the last kept row", async () => { const rows = Array.from({ length: 8 }, (_, i) => ({ id: `r${i}`, position: `p${i}`, n: i })); const query = async ( _where: Record, cursor: { id: string; position: string } | undefined, limit: number ) => { // emulate DB: stable order, exclusive cursor on position const start = cursor ? rows.findIndex((r) => r.position === cursor.position) + 1 : 0; return rows.slice(start, start + limit); }; const page1 = await paginate(query, { limit: 5 }); expect(page1.data).toHaveLength(5); expect(page1.nextCursor).not.toBeNull(); const decoded = decodeCursor(page1.nextCursor!); expect(decoded).toEqual({ id: "r4", position: "p4" }); const page2 = await paginate(query, { cursor: decoded, limit: 5 }); expect(page2.data).toHaveLength(3); expect(page2.nextCursor).toBeNull(); }); it("paginate: exact page boundary → nextCursor null (no extra fetch)", async () => { const rows = Array.from({ length: 5 }, (_, i) => ({ id: `r${i}`, position: `p${i}` })); const query = async ( _where: Record, cursor: { id: string; position: string } | undefined, limit: number ) => { const start = cursor ? rows.findIndex((r) => r.position === cursor.position) + 1 : 0; return rows.slice(start, start + limit); }; const page = await paginate(query, { limit: 5 }); expect(page.data).toHaveLength(5); expect(page.nextCursor).toBeNull(); }); it("paginate: empty result → empty data, null cursor", async () => { const page = await paginate(async () => [], { limit: 50 }); expect(page.data).toEqual([]); expect(page.nextCursor).toBeNull(); }); });