import { ModuleDatabaseError, type ModuleDatabaseExecutor, type ModuleDatabaseParameter, } from "@ekklesia/contracts"; import { describe, expect, it } from "vitest"; import { findEkklesiaDatabaseError, sql } from "../src/index.js"; describe("the raw Module Database ABI v1 SQL tag", () => { it("keeps static SQL unchanged when there are no interpolations", () => { expect(sql`SELECT id FROM calendar_events ORDER BY id`).toEqual({ text: "SELECT id FROM calendar_events ORDER BY id", values: [], }); }); it("turns every interpolation into an ordered PostgreSQL parameter", () => { const id = "event-1"; const active = true; expect(sql`SELECT * FROM calendar_events WHERE id = ${id} AND active = ${active}`).toEqual({ text: "SELECT * FROM calendar_events WHERE id = $1 AND active = $2", values: [id, active], }); }); it("gives repeated values separate positional parameters", () => { const value = "same"; expect(sql`SELECT ${value}, ${value}, ${value}`).toEqual({ text: "SELECT $1, $2, $3", values: [value, value, value], }); }); it("keeps SQL-injection-sensitive strings bound instead of joining them into SQL", () => { const attack = "x'; DROP TABLE calendar_events; --"; const query = sql`SELECT * FROM calendar_events WHERE title = ${attack}`; expect(query.text).toBe("SELECT * FROM calendar_events WHERE title = $1"); expect(query.text).not.toContain(attack); expect(query.values).toEqual([attack]); }); it("preserves bigint, byte arrays, and PostgreSQL arrays as ABI values", () => { const bytes = new Uint8Array([0, 127, 255]); const identifiers = [1n, 2n, null] as const; const query = sql`SELECT ${9_007_199_254_740_993n}, ${bytes}, ${identifiers}`; expect(query.values).toEqual([9_007_199_254_740_993n, bytes, identifiers]); expect(query.values[1]).toBe(bytes); expect(query.values[2]).toBe(identifiers); }); it("does not stringify unsupported values before the ABI executor rejects them", async () => { const invalid = new Date("2026-01-02T03:04:05.000Z"); const query = sql`SELECT ${invalid as unknown as ModuleDatabaseParameter}`; expect(query.values[0]).toBe(invalid); const executor = { abiVersion: 1, async query(request: { readonly values?: readonly ModuleDatabaseParameter[] }) { if (request.values?.some((value) => value instanceof Date)) { throw new ModuleDatabaseError("MODULE_DATABASE_PARAMETER_INVALID"); } throw new Error("Expected the fake ABI validator to reject the value."); }, async transaction() { throw new Error("not used"); }, } as unknown as ModuleDatabaseExecutor; await expect(executor.query(query)).rejects.toMatchObject({ code: "MODULE_DATABASE_PARAMETER_INVALID", name: "ModuleDatabaseError", }); }); }); describe("findEkklesiaDatabaseError", () => { it("finds a stable ABI error through wrappers without parsing messages", () => { const databaseError = new ModuleDatabaseError("MODULE_DATABASE_QUERY_FAILED", { sqlState: "23505", }); const wrapped = new Error("wrapper", { cause: new Error("second", { cause: databaseError }) }); expect(findEkklesiaDatabaseError(wrapped)).toBe(databaseError); expect(findEkklesiaDatabaseError(wrapped)).toMatchObject({ code: "MODULE_DATABASE_QUERY_FAILED", sqlState: "23505", }); }); it("terminates safely for cause cycles and unrelated errors", () => { const cyclic = new Error("cyclic"); cyclic.cause = cyclic; expect(findEkklesiaDatabaseError(cyclic)).toBeUndefined(); expect(findEkklesiaDatabaseError("not an error")).toBeUndefined(); }); });