Files
tickettracker/backend/test/databaseExport.test.ts
T
2026-09-14 08:13:18 +02:00

98 lines
4.0 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import { nextSequenceIdAfterRestore, restoreSqlText, sequenceSetvalSql, sqlLiteral, type QueryExecutor } from "../src/databaseExport.ts";
import { exportTableSql } from "../src/databaseExport.ts";
const jsonbColumn = { dataType: "jsonb", udtName: "jsonb" };
test("sqlLiteral serializes primitive values", () => {
assert.equal(sqlLiteral(null), "NULL");
assert.equal(sqlLiteral(undefined), "NULL");
assert.equal(sqlLiteral("normaler Text"), "'normaler Text'");
assert.equal(sqlLiteral("O'Brien"), "'O''Brien'");
assert.equal(sqlLiteral("erste Zeile\nzweite Zeile"), "'erste Zeile\nzweite Zeile'");
assert.equal(sqlLiteral("Grüße"), "'Grüße'");
assert.equal(sqlLiteral(true), "TRUE");
assert.equal(sqlLiteral(false), "FALSE");
assert.equal(sqlLiteral(0), "0");
assert.equal(sqlLiteral(42), "42");
assert.equal(sqlLiteral(-10), "-10");
assert.equal(sqlLiteral(1.25), "1.25");
});
test("sqlLiteral serializes dates, buffers, objects and arrays", () => {
assert.equal(sqlLiteral(new Date("2026-09-11T10:15:30.000Z")), "'2026-09-11T10:15:30.000Z'");
assert.equal(sqlLiteral(Buffer.from("abc")), "decode('616263', 'hex')");
assert.equal(sqlLiteral({}), "'{}'");
assert.equal(sqlLiteral([]), "'[]'");
assert.equal(sqlLiteral({ foo: "bar" }), `'{"foo":"bar"}'`);
assert.equal(sqlLiteral({ foo: "O'Brien" }), `'{"foo":"O''Brien"}'`);
assert.equal(sqlLiteral({ nested: { enabled: true }, items: [{ id: 1 }] }), `'{"nested":{"enabled":true},"items":[{"id":1}]}'`);
});
test("sqlLiteral serializes JSON columns as valid JSON values", () => {
assert.equal(sqlLiteral({ foo: "bar", enabled: true }, jsonbColumn), `'{"foo":"bar","enabled":true}'`);
assert.equal(sqlLiteral("plain json string", jsonbColumn), `'"plain json string"'`);
assert.equal(sqlLiteral(true, jsonbColumn), "'true'");
});
test("generated table export never contains implicit object stringification", async () => {
const client: QueryExecutor = {
async query<T extends Record<string, unknown>>(text: string): Promise<{ rows: T[]; rowCount: number | null }> {
if (text.includes("information_schema.columns")) {
return {
rowCount: 3,
rows: [
{ columnName: "id", dataType: "bigint", udtName: "int8", ordinalPosition: 1, isNullable: "NO" },
{ columnName: "value", dataType: "jsonb", udtName: "jsonb", ordinalPosition: 2, isNullable: "NO" },
{ columnName: "label", dataType: "text", udtName: "text", ordinalPosition: 3, isNullable: "YES" }
] as T[]
};
}
if (text.includes("pg_get_serial_sequence")) {
return { rowCount: 1, rows: [{ sequenceName: "public.example_id_seq" }] as T[] };
}
return {
rowCount: 1,
rows: [{ id: 4, value: { foo: "bar" }, label: "O'Brien" }] as T[]
};
}
};
const sql = (await exportTableSql(client, "example")).join("\n");
assert.doesNotMatch(sql, /\[object Object\]/);
assert.match(sql, /'\{"foo":"bar"\}'/);
});
test("sequence helper advances after restored max id", () => {
assert.equal(nextSequenceIdAfterRestore(4), 5);
assert.equal(nextSequenceIdAfterRestore(null), 1);
assert.match(sequenceSetvalSql("users", "public.users_id_seq"), /SELECT setval\('public\.users_id_seq'/);
assert.match(sequenceSetvalSql("users", "public.users_id_seq"), /MAX\("id"\) FROM "users"/);
});
test("restoreSqlText removes dump transaction wrapper after export header", () => {
const body = restoreSqlText(`
-- TicketTracker Datenbankexport
-- Erstellt: 2026-09-14T10:00:00.000Z
BEGIN;
SET LOCAL lock_timeout = '10s';
SET LOCAL statement_timeout = '15min';
TRUNCATE TABLE "users", "auth_sessions" RESTART IDENTITY CASCADE;
INSERT INTO "users" ("id", "username") VALUES
(1, 'admin');
COMMIT;
`);
assert.doesNotMatch(body, /^\s*BEGIN\s*;/im);
assert.doesNotMatch(body, /^\s*COMMIT\s*;/im);
assert.doesNotMatch(body, /^\s*SET\s+LOCAL/im);
assert.doesNotMatch(body, /TRUNCATE\s+TABLE/i);
assert.match(body, /INSERT INTO "users"/);
});