import { describe, it } from "node:test"; import assert from "node:assert/strict"; import { Readable } from "node:stream"; import { PostgresInterface } from "../../../src/db/postgres/PostgresInterface.js"; import type { WrappedPgClient } from "../../../src/db/postgres/pool.js"; import type { SqlCode } from "../../../src/db/postgres/sql.js"; // The postgres backend has no CI database, so these tests drive the real // PostgresInterface.export/import code paths against an in-memory fake client. // They verify control flow (NDJSON shape, meta.source, filtering, note // skipping, counts, insert dispatch, abort -> rollback); the SQL-arg forms are // the same {uuid}/{text}/{jsonb}/{date} patterns the pg backend's own CRUD // already exercises against real Postgres. interface Rows { users?: Record[]; abodes?: Record[]; residents?: Record[]; apikeys?: Record[]; } class FakePg implements WrappedPgClient { readonly = false; inserts: { table: string; vars: unknown[] }[] = []; committed = false; rolledBack = false; #rows: Rows; constructor(rows: Rows = {}) { this.#rows = rows; } async destroy(): Promise {} async all(stmt: SqlCode): Promise { const s = stmt._sql; if (s.includes('FROM "users"')) return (this.#rows.users ?? []) as R[]; if (s.includes('FROM "abodes"')) return (this.#rows.abodes ?? []) as R[]; if (s.includes('FROM "residents"')) return (this.#rows.residents ?? []) as R[]; if (s.includes('FROM "apikeys"')) return (this.#rows.apikeys ?? []) as R[]; return [] as R[]; } async get(stmt: SqlCode): Promise { const rows = await this.all(stmt); return rows[0] ?? null; } async run(stmt: SqlCode): Promise<{ changes: number }> { const table = stmt._sql.match(/INSERT INTO "(\w+)"/)?.[1] ?? "?"; this.inserts.push({ table, vars: stmt._vars }); return { changes: 1 }; } async multi(fn: (tx: WrappedPgClient) => Promise): Promise { try { const r = await fn(this); this.committed = true; return r; } catch (e) { this.rolledBack = true; throw e; } } async rethrow(fn: () => Promise): Promise { return fn(); } } const iso = "2026-01-02T03:04:05.000Z"; function seededRows(): Rows { return { users: [ { uid: "11111111-1111-1111-1111-111111111111", email: "u1@test.example", name: "User One", flags: {}, created_at: new Date(iso), updated_at: new Date(iso), }, ], abodes: [ { aid: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa1", name: "Abode One", created_at: new Date(iso), created_by: "11111111-1111-1111-1111-111111111111", updated_at: new Date(iso), updated_by: null, }, { aid: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa2", name: "Abode Two", created_at: new Date(iso), created_by: null, updated_at: new Date(iso), updated_by: null, }, ], residents: [ { uid: "11111111-1111-1111-1111-111111111111", aid: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa1", flags: {}, created_at: new Date(iso), created_by: null, updated_at: new Date(iso), updated_by: null, }, ], apikeys: [ { uid: "11111111-1111-1111-1111-111111111111", kid: "kkkkkkkk-kkkk-kkkk-kkkk-kkkkkkkkkkk1", name: "key one", permissions: {}, created_at: new Date(iso), expires_at: null, }, ], }; } async function streamToString(s: NodeJS.ReadableStream): Promise { let out = ""; for await (const chunk of s) out += chunk; return out; } function parseLines(ndjson: string): { kind: string; data: any }[] { return ndjson .split("\n") .filter(Boolean) .map((l) => JSON.parse(l)); } function line(kind: string, data: unknown): string { return JSON.stringify({ kind, data }) + "\n"; } describe("postgres export", () => { it("streams meta + the four supported kinds, never note", async () => { const db = new PostgresInterface(new FakePg(seededRows())); const lines = parseLines(await streamToString(db.export())); const meta = lines.find((l) => l.kind === "meta"); assert.ok(meta); assert.equal(meta!.data.source, "postgres"); assert.equal(meta!.data.v, 1); const kinds = new Set(lines.map((l) => l.kind)); assert.ok(kinds.has("user")); assert.ok(kinds.has("abode")); assert.ok(kinds.has("resident")); assert.ok(kinds.has("apikey")); assert.ok(!kinds.has("note"), "note is unsupported on postgres"); }); it("applies the kinds filter", async () => { const db = new PostgresInterface(new FakePg(seededRows())); const lines = parseLines( await streamToString(db.export({ filter: { kinds: ["abode"] } })), ); const kinds = new Set( lines.filter((l) => l.kind !== "meta").map((l) => l.kind), ); assert.deepEqual(kinds, new Set(["abode"])); }); it("applies the abodes allowlist to abode/resident records", async () => { const db = new PostgresInterface(new FakePg(seededRows())); const lines = parseLines( await streamToString( db.export({ filter: { abodes: ["aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa1"] }, }), ), ); const abodeAids = lines .filter((l) => l.kind === "abode") .map((l) => l.data.aid); assert.deepEqual(abodeAids, ["aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa1"]); }); }); describe("postgres import", () => { it("dispatches inserts per kind, skips note, and commits", async () => { const fake = new FakePg(); const db = new PostgresInterface(fake); const source = Readable.from([ line("meta", { v: 1 }), line("user", { uid: "11111111-1111-1111-1111-111111111111", email: "u1@test.example", name: "User One", flags: {}, created_at: iso, updated_at: iso, }), line("abode", { aid: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa1", name: "Abode One", created_at: iso, created_by: "11111111-1111-1111-1111-111111111111", updated_at: iso, updated_by: null, }), line("apikey", { uid: "11111111-1111-1111-1111-111111111111", kid: "kkkkkkkk-kkkk-kkkk-kkkk-kkkkkkkkkkk1", name: "key one", permissions: {}, created_at: iso, expires_at: null, }), // note lines are silently skipped on postgres line("note", { nid: "nnnnnnnn-nnnn-nnnn-nnnn-nnnnnnnnnnn1", aid: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaa1", name: "Note", content: "x", properties: {}, created_at: iso, created_by: null, updated_at: iso, updated_by: null, }), ]); const result = await db.import(source); assert.deepEqual(result.counts, { user: 1, abode: 1, apikey: 1 }); assert.ok(fake.committed); assert.deepEqual(fake.inserts.map((i) => i.table).sort(), [ "abodes", "apikeys", "users", ]); assert.ok(!fake.inserts.some((i) => i.table === "notes"), "no note insert"); // apikey gets a freshly-minted token (never exported) const apikeyInsert = fake.inserts.find((i) => i.table === "apikeys")!; assert.ok( apikeyInsert.vars.some( (v) => typeof v === "string" && v.startsWith("at_"), ), "apikey insert carries a fresh token", ); }); it("rejects and rolls back when the stream carries an error sentinel", async () => { const fake = new FakePg(); const db = new PostgresInterface(fake); const source = Readable.from([ line("meta", { v: 1 }), line("user", { uid: "11111111-1111-1111-1111-111111111111", email: "u1@test.example", name: "User One", flags: {}, created_at: iso, updated_at: iso, }), line("error", { message: "boom" }), ]); await assert.rejects(() => db.import(source), /boom/); assert.ok(fake.rolledBack); assert.ok(!fake.committed); }); it("rejects and rolls back on signal abort", async () => { const fake = new FakePg(); const db = new PostgresInterface(fake); const ac = new AbortController(); const source = Readable.from( (async function* () { yield line("meta", { v: 1 }); yield line("user", { uid: "11111111-1111-1111-1111-111111111111", email: "u1@test.example", name: "User One", flags: {}, created_at: iso, updated_at: iso, }); ac.abort(); await new Promise((r) => setTimeout(r, 1000)); })(), ); await assert.rejects(() => db.import(source, { signal: ac.signal })); assert.ok(fake.rolledBack); assert.ok(!fake.committed); }); });