import { sortClause, SORTABLE_KEYS, eventSearchClause, clampLimit, clampOffset, pageCount, windowParams, DEFAULT_LIMIT, } from "./query.js"; describe("sortClause", () => { it("maps known keys and directions", () => { expect(sortClause("size", "asc")).toBe('ORDER BY "size" ASC NULLS LAST'); expect(sortClause("sender", "desc")).toBe("ORDER BY sender DESC NULLS LAST"); }); it("falls back to date DESC for unknown/missing keys", () => { expect(sortClause(undefined, undefined)).toBe("ORDER BY date DESC NULLS LAST"); expect(sortClause("nope", "asc")).toBe("ORDER BY date ASC NULLS LAST"); }); it("never interpolates an attacker-supplied sort key (injection safety)", () => { const clause = sortClause("date; DROP TABLE event; --", "asc"); expect(clause).toBe("ORDER BY date ASC NULLS LAST"); expect(clause).not.toMatch(/DROP TABLE/); }); it("only honors exactly 'asc' for direction, otherwise DESC", () => { expect(sortClause("date", "ASC")).toContain("ASC"); expect(sortClause("date", "ascending")).toContain("DESC"); expect(sortClause("date", "; DELETE")).toContain("DESC"); }); it("exposes the sortable keys", () => { expect(SORTABLE_KEYS).toEqual( expect.arrayContaining(["agreementUuid", "eventUuid", "sender", "date", "size"]), ); }); }); describe("eventSearchClause", () => { it("references the given bind index and only that index (no interpolated value)", () => { const c = eventSearchClause(3); expect(c).toContain("$3"); expect(c).toContain("ILIKE"); // no other positional params leak in expect(c.match(/\$\d+/g).every((p) => p === "$3")).toBe(true); }); }); describe("clampLimit / clampOffset", () => { it("clamps limit into [1, 200] with a default", () => { expect(clampLimit(undefined)).toBe(DEFAULT_LIMIT); expect(clampLimit("abc")).toBe(DEFAULT_LIMIT); expect(clampLimit("0")).toBe(1); expect(clampLimit("-5")).toBe(1); expect(clampLimit("9999")).toBe(200); expect(clampLimit("75")).toBe(75); }); it("clamps offset to a non-negative integer", () => { expect(clampOffset(undefined)).toBe(0); expect(clampOffset("-3")).toBe(0); expect(clampOffset("abc")).toBe(0); expect(clampOffset("40")).toBe(40); }); }); describe("pageCount", () => { it("computes ceil(total/perPage), at least 1", () => { expect(pageCount(0, 50)).toBe(1); expect(pageCount(50, 50)).toBe(1); expect(pageCount(51, 50)).toBe(2); expect(pageCount(200, 50)).toBe(4); }); }); describe("windowParams", () => { it("defaults to a 30-day window ending today (UTC)", () => { const { from, to } = windowParams({}); const days = (new Date(`${to}T00:00:00Z`) - new Date(`${from}T00:00:00Z`)) / 86_400_000; expect(days).toBe(29); expect(to).toMatch(/^\d{4}-\d{2}-\d{2}$/); }); it("honors valid from/to", () => { expect(windowParams({ from: "2026-01-01", to: "2026-01-31" })).toEqual({ from: "2026-01-01", to: "2026-01-31", }); }); it("ignores invalid dates and collapses inverted ranges", () => { expect(windowParams({ from: "not-a-date", to: "2026-02-10" }).to).toBe("2026-02-10"); expect(windowParams({ from: "2026-05-01", to: "2026-01-01" })).toEqual({ from: "2026-01-01", to: "2026-01-01", }); }); it("caps windows wider than ~a year", () => { const { from, to } = windowParams({ from: "2000-01-01", to: "2026-01-01" }); const days = (new Date(`${to}T00:00:00Z`) - new Date(`${from}T00:00:00Z`)) / 86_400_000; expect(days).toBeLessThanOrEqual(366); }); });