import { describe, expect, test } from "bun:test"; import { buildScanItemsWhere, parseScanItemsQuery, parseScanLimit } from "../scan"; describe("parseScanLimit", () => { test("accepts positive integers and nullish/empty as no-limit", () => { expect(parseScanLimit(5)).toEqual({ ok: true, value: 5 }); expect(parseScanLimit(1)).toEqual({ ok: true, value: 1 }); expect(parseScanLimit(10_000)).toEqual({ ok: true, value: 10_000 }); expect(parseScanLimit(null)).toEqual({ ok: true, value: null }); expect(parseScanLimit(undefined)).toEqual({ ok: true, value: null }); expect(parseScanLimit("")).toEqual({ ok: true, value: null }); }); test("coerces numeric strings (env var path) but rejects garbage", () => { expect(parseScanLimit("7")).toEqual({ ok: true, value: 7 }); expect(parseScanLimit("abc")).toEqual({ ok: false }); expect(parseScanLimit("12abc")).toEqual({ ok: false }); }); test("rejects the footguns that would silently disable the cap", () => { // NaN: processed >= NaN never trips → cap never fires. expect(parseScanLimit(Number.NaN)).toEqual({ ok: false }); // Negative: off-by-one bugs in Math.min(limit, total). expect(parseScanLimit(-1)).toEqual({ ok: false }); expect(parseScanLimit(0)).toEqual({ ok: false }); // Float: Math.min is fine but percentage math breaks on non-integers. expect(parseScanLimit(1.5)).toEqual({ ok: false }); // Infinity is technically a number but has no business as a cap. expect(parseScanLimit(Number.POSITIVE_INFINITY)).toEqual({ ok: false }); }); }); describe("parseScanItemsQuery", () => { test("normalizes default filters and pagination", () => { const q = parseScanItemsQuery({}); expect(q).toEqual({ offset: 0, limit: 50, search: "", status: "all", type: "all", source: "all", }); }); test("clamps limit and offset, trims and lowercases values", () => { const q = parseScanItemsQuery({ offset: "-12", limit: "5000", q: " The Wire ", status: "SCANNED", type: "EPISODE", source: "WEBHOOK", }); expect(q).toEqual({ offset: 0, limit: 200, search: "The Wire", status: "scanned", type: "episode", source: "webhook", }); }); test("falls back to all for unknown enum values", () => { const q = parseScanItemsQuery({ status: "zzz", type: "cartoon", source: "mqtt" }); expect(q.status).toBe("all"); expect(q.type).toBe("all"); expect(q.source).toBe("all"); }); }); describe("buildScanItemsWhere", () => { test("builds combined where clause + args in stable order", () => { const where = buildScanItemsWhere({ offset: 0, limit: 50, search: "blade", status: "scanned", type: "movie", source: "webhook", }); expect(where.sql).toBe( "WHERE scan_status = ? AND lower(type) = ? AND ingest_source = ? AND (lower(name) LIKE ? OR lower(file_path) LIKE ?)", ); expect(where.args).toEqual(["scanned", "movie", "webhook", "%blade%", "%blade%"]); }); test("returns empty where when all filters are broad", () => { const where = buildScanItemsWhere({ offset: 0, limit: 50, search: "", status: "all", type: "all", source: "all", }); expect(where.sql).toBe(""); expect(where.args).toEqual([]); }); });