108 lines
3.5 KiB
JavaScript
108 lines
3.5 KiB
JavaScript
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);
|
|
});
|
|
});
|