test: port migrate suite + add probe-table and CLI specs
15 vitest specs total: - 9 lifted from authd's tests/db/migrate.test.ts. Specs that referenced the on-disk migrations dir now write fixtures into a per-test tmpdir (package ships no migrations dir of its own). - 3 new specs covering the genesisProbeTable parameter (default, custom table name, opt-out via stampGenesis=false). - 3 new specs covering runMigrateCli (migrate / status / stamp) using PassThrough stream capture.
This commit is contained in:
157
tests/cli.test.ts
Normal file
157
tests/cli.test.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { PassThrough } from 'node:stream';
|
||||
import Database from 'better-sqlite3';
|
||||
import { runMigrateCli } from '../src/cli.ts';
|
||||
|
||||
function captureStreams() {
|
||||
const stdout = new PassThrough();
|
||||
const stderr = new PassThrough();
|
||||
const stdoutBuf: Buffer[] = [];
|
||||
const stderrBuf: Buffer[] = [];
|
||||
stdout.on('data', (c) => stdoutBuf.push(Buffer.isBuffer(c) ? c : Buffer.from(c)));
|
||||
stderr.on('data', (c) => stderrBuf.push(Buffer.isBuffer(c) ? c : Buffer.from(c)));
|
||||
return {
|
||||
stdout,
|
||||
stderr,
|
||||
get stdoutText() {
|
||||
return Buffer.concat(stdoutBuf).toString('utf8');
|
||||
},
|
||||
get stderrText() {
|
||||
return Buffer.concat(stderrBuf).toString('utf8');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('runMigrateCli', () => {
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = mkdtempSync(join(tmpdir(), 'sqlite-migrate-cli-test-'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("command='migrate' calls applyMigrations and writes expected lines", () => {
|
||||
writeFileSync(join(tmpDir, '0001_init.sql'), 'CREATE TABLE x (a INTEGER);');
|
||||
const caps = captureStreams();
|
||||
|
||||
const code = runMigrateCli({
|
||||
openDb: () => new Database(':memory:'),
|
||||
migrationsDir: tmpDir,
|
||||
command: 'migrate',
|
||||
stdout: caps.stdout,
|
||||
stderr: caps.stderr,
|
||||
});
|
||||
|
||||
expect(code).toBe(0);
|
||||
expect(caps.stdoutText).toContain('applied:1');
|
||||
expect(caps.stdoutText).toContain('migrations: 1 applied, 0 pending');
|
||||
});
|
||||
|
||||
it("command='status' emits pending then applied then pending again", () => {
|
||||
writeFileSync(join(tmpDir, '0001_init.sql'), 'CREATE TABLE x (a INTEGER);');
|
||||
|
||||
const sharedDb = new Database(':memory:');
|
||||
const openDb = () => {
|
||||
const proxy = new Proxy(sharedDb, {
|
||||
get(target, prop) {
|
||||
if (prop === 'close') return () => {};
|
||||
const v = (target as any)[prop];
|
||||
return typeof v === 'function' ? v.bind(target) : v;
|
||||
},
|
||||
});
|
||||
return proxy as unknown as Database.Database;
|
||||
};
|
||||
|
||||
const migrateCaps = captureStreams();
|
||||
const migrateCode = runMigrateCli({
|
||||
openDb,
|
||||
migrationsDir: tmpDir,
|
||||
command: 'migrate',
|
||||
stdout: migrateCaps.stdout,
|
||||
stderr: migrateCaps.stderr,
|
||||
});
|
||||
expect(migrateCode).toBe(0);
|
||||
|
||||
const statusCaps = captureStreams();
|
||||
const statusCode = runMigrateCli({
|
||||
openDb,
|
||||
migrationsDir: tmpDir,
|
||||
command: 'status',
|
||||
stdout: statusCaps.stdout,
|
||||
stderr: statusCaps.stderr,
|
||||
});
|
||||
expect(statusCode).toBe(0);
|
||||
expect(statusCaps.stdoutText).toContain('applied:0001');
|
||||
|
||||
writeFileSync(join(tmpDir, '0002_more.sql'), 'CREATE TABLE y (b INTEGER);');
|
||||
const statusCaps2 = captureStreams();
|
||||
const statusCode2 = runMigrateCli({
|
||||
openDb,
|
||||
migrationsDir: tmpDir,
|
||||
command: 'status',
|
||||
stdout: statusCaps2.stdout,
|
||||
stderr: statusCaps2.stderr,
|
||||
});
|
||||
expect(statusCode2).toBe(0);
|
||||
expect(statusCaps2.stdoutText).toContain('pending:0002');
|
||||
|
||||
sharedDb.close();
|
||||
});
|
||||
|
||||
it("command='stamp' marks applied without running body", () => {
|
||||
writeFileSync(
|
||||
join(tmpDir, '0001_init.sql'),
|
||||
'CREATE TABLE only_if_applied (x INTEGER);',
|
||||
);
|
||||
|
||||
const sharedDb = new Database(':memory:');
|
||||
const openDb = () => {
|
||||
const proxy = new Proxy(sharedDb, {
|
||||
get(target, prop) {
|
||||
if (prop === 'close') return () => {};
|
||||
const v = (target as any)[prop];
|
||||
return typeof v === 'function' ? v.bind(target) : v;
|
||||
},
|
||||
});
|
||||
return proxy as unknown as Database.Database;
|
||||
};
|
||||
|
||||
const stampCaps = captureStreams();
|
||||
const code = runMigrateCli({
|
||||
openDb,
|
||||
migrationsDir: tmpDir,
|
||||
command: 'stamp',
|
||||
version: '0001',
|
||||
stdout: stampCaps.stdout,
|
||||
stderr: stampCaps.stderr,
|
||||
});
|
||||
expect(code).toBe(0);
|
||||
expect(stampCaps.stdoutText).toContain('stamped:0001');
|
||||
|
||||
const tableMissing = sharedDb
|
||||
.prepare(
|
||||
`SELECT name FROM sqlite_master WHERE type='table' AND name='only_if_applied'`,
|
||||
)
|
||||
.get();
|
||||
expect(tableMissing).toBeUndefined();
|
||||
|
||||
const noVersionCaps = captureStreams();
|
||||
const noVersionCode = runMigrateCli({
|
||||
openDb,
|
||||
migrationsDir: tmpDir,
|
||||
command: 'stamp',
|
||||
stdout: noVersionCaps.stdout,
|
||||
stderr: noVersionCaps.stderr,
|
||||
});
|
||||
expect(noVersionCode).toBe(1);
|
||||
expect(noVersionCaps.stderrText).toContain('error:');
|
||||
|
||||
sharedDb.close();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user