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.
158 lines
4.6 KiB
TypeScript
158 lines
4.6 KiB
TypeScript
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();
|
|
});
|
|
});
|