CI / verify (pull_request) Successful in 58s
CI / infrastructure-smoke (pull_request) Successful in 35s
CI / migration-smoke (pull_request) Successful in 1m53s
CI / image-build (pull_request) Successful in 29s
CI / deploy-smoke (same-origin) (pull_request) Successful in 49s
CI / deploy-smoke (split) (pull_request) Successful in 1m31s
96 lines
3.7 KiB
TypeScript
96 lines
3.7 KiB
TypeScript
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();
|
|
});
|
|
});
|