/** * Property-based tests using fast-check. * * Covers: * - decodeCursor(encodeCursor(s)) === s for all non-empty strings and numbers * - decodeKeysetCursor(encodeKeysetCursor(keys)) deep-equals keys * - decodePageToken(encodePageToken(payload)) deep-equals payload * - paginateArray always returns ≤ limit items * - offsetToPage(pageToOffset(p, l), l) === p for all positive p and l */ import { describe, it } from "@suaveplan/testing/runner"; import * as fc from "fast-check"; import { decodeCursor, encodeCursor } from "../src/cursor/cursor-pagination.js"; import { decodeKeysetCursor, encodeKeysetCursor, } from "../src/keyset/keyset-pagination.js"; import { offsetToPage, pageToOffset, paginateArray, } from "../src/offset/offset.js"; import { decodePageToken, encodePageToken } from "../src/tokens/page-tokens.js"; import type { KeysetSortKey, PageTokenPayload, } from "../src/types/pagination.types.js"; // --------------------------------------------------------------------------- // Cursor encode/decode round-trip // --------------------------------------------------------------------------- describe("property: cursor encode/decode round-trip", () => { it("round-trips any non-empty string", { timeout: 10000 }, () => { fc.assert( fc.property(fc.string({ minLength: 1 }), (s) => { return decodeCursor(encodeCursor(s)) === s; }), { numRuns: 200 } ); }); it("round-trips any safe integer", { timeout: 10000 }, () => { fc.assert( fc.property(fc.integer(), (n) => { return decodeCursor(encodeCursor(n)) === n; }), { numRuns: 200 } ); }); }); // --------------------------------------------------------------------------- // Keyset cursor round-trip // --------------------------------------------------------------------------- describe("property: keyset cursor encode/decode round-trip", () => { const keysetArb: fc.Arbitrary = fc.array( fc.record({ field: fc.string({ minLength: 1, maxLength: 20 }), direction: fc.constantFrom("asc" as const, "desc" as const), value: fc.oneof( fc.string({ minLength: 0, maxLength: 50 }), fc.integer() ), }), { minLength: 1, maxLength: 5 } ); it("round-trips arbitrary keyset keys", { timeout: 10000 }, () => { fc.assert( fc.property(keysetArb, (keys) => { const decoded = decodeKeysetCursor(encodeKeysetCursor(keys)); return JSON.stringify(decoded) === JSON.stringify(keys); }), { numRuns: 200 } ); }); }); // --------------------------------------------------------------------------- // Page token round-trip // --------------------------------------------------------------------------- describe("property: page token encode/decode round-trip", () => { const payloadArb: fc.Arbitrary = fc.record( { v: fc.oneof(fc.string({ minLength: 1 }), fc.integer()), s: fc.option(fc.string({ minLength: 1 }), { nil: undefined }), d: fc.option(fc.constantFrom("asc" as const, "desc" as const), { nil: undefined, }), f: fc.option(fc.string({ minLength: 1 }), { nil: undefined }), }, { withDeletedKeys: false } ); it("round-trips arbitrary page token payloads", { timeout: 10000 }, () => { fc.assert( fc.property(payloadArb, (payload) => { const filtered: PageTokenPayload = { v: payload.v }; if (payload.s !== undefined) filtered.s = payload.s; if (payload.d !== undefined) filtered.d = payload.d; if (payload.f !== undefined) filtered.f = payload.f; const decoded = decodePageToken(encodePageToken(filtered)); return JSON.stringify(decoded) === JSON.stringify(filtered); }), { numRuns: 200 } ); }); }); // --------------------------------------------------------------------------- // paginateArray: always returns ≤ limit items // --------------------------------------------------------------------------- describe("property: paginateArray respects limit", () => { it("always returns ≤ limit items", { timeout: 10000 }, () => { fc.assert( fc.property( fc.array(fc.integer(), { minLength: 0, maxLength: 100 }), fc.integer({ min: 1, max: 20 }), fc.integer({ min: 1, max: 10 }), (items, page, limit) => { const res = paginateArray(items, page, limit); return res.data.length <= limit; } ), { numRuns: 300 } ); }); }); // --------------------------------------------------------------------------- // offsetToPage / pageToOffset round-trip // --------------------------------------------------------------------------- describe("property: offsetToPage(pageToOffset(p, l), l) === p", () => { it("round-trips for positive page and limit", { timeout: 10000 }, () => { fc.assert( fc.property( fc.integer({ min: 1, max: 10000 }), fc.integer({ min: 1, max: 1000 }), (page, limit) => { return ( offsetToPage(pageToOffset(page, limit), limit) === page ); } ), { numRuns: 300 } ); }); });