/** * Unit test for `runPgDump` in backup.service.ts. * * Regression: the dump promise must resolve once BOTH (a) the child process * exits 0 and (b) the output file is fully flushed — regardless of which event * fires first. The original implementation attached the file's `finish` * listener *inside* the child `close` handler, but `stdout.pipe(out)` * auto-ends the file when the child's stdout closes, so `finish` frequently * fired before the listener was attached → the promise hung forever (observed * end-to-end against a real pg_dump). * * We drive the spawn with `node` instead of `pg_dump` (injected via opts) so * the test is deterministic and needs no database. */ import { readFileSync, mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { runPgDump } from '@/lib/services/backup.service'; describe('runPgDump', () => { let dir: string; beforeEach(() => { dir = mkdtempSync(path.join(tmpdir(), 'pn-pgdump-')); }); afterEach(() => { rmSync(dir, { recursive: true, force: true }); }); it('resolves and writes the child stdout to the output file on exit 0', async () => { const out = path.join(dir, 'a.dump'); await runPgDump('ignored://url', out, { command: process.execPath, buildArgs: () => ['-e', 'process.stdout.write("DUMP-CONTENTS-1234")'], }); expect(readFileSync(out, 'utf8')).toBe('DUMP-CONTENTS-1234'); }); it('rejects with the captured stderr when the child exits non-zero', async () => { const out = path.join(dir, 'b.dump'); await expect( runPgDump('ignored://url', out, { command: process.execPath, buildArgs: () => ['-e', 'process.stderr.write("kaboom"); process.exit(3)'], }), ).rejects.toThrow(/exited 3.*kaboom/s); }); it('rejects when the command cannot be spawned', async () => { const out = path.join(dir, 'c.dump'); await expect( runPgDump('ignored://url', out, { command: '/nonexistent/definitely-not-a-real-binary-xyz', buildArgs: () => [], }), ).rejects.toBeTruthy(); }); });