Files
TREK/server/tests/unit/db/restore-migrates-forward.test.ts
T

95 lines
3.8 KiB
TypeScript

import { runSchemaBootstrap } from '../../../src/db/orm';
import { Migrator } from '@mikro-orm/migrations';
import { SeedManager } from '@mikro-orm/seeder';
import { MikroORM } from '@mikro-orm/sqlite';
import Database from 'better-sqlite3';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
/**
* Restoring a backup taken on an older release has to bring its schema forward.
*
* That used to fall out of `reinitialize()` re-running `createTables` →
* `runMigrations` → `runSeeds` against the swapped file. Those calls are gone,
* so the guarantee now rests on the schema bootstrap being re-runnable against a
* database that is behind — which is what this covers.
*
* `backup.impl.test.ts` mocks `reinitialize` away, so nothing else asserts it.
*
* Uses the stock driver against a temp file rather than the shared one, because
* the point is to drive a database that is NOT the process singleton.
*/
const MIGRATIONS = path.join(__dirname, '../../../src/db/migrations');
const SEEDERS = path.join(__dirname, '../../../src/db/seeders');
let tmpDir: string | null = null;
function ormFor(dbName: string): Promise<MikroORM> {
return MikroORM.init({
entities: [],
discovery: { warnWhenNoEntities: false },
extensions: [Migrator, SeedManager],
migrations: { path: MIGRATIONS, pathTs: MIGRATIONS, snapshot: false, silent: true },
seeder: { path: SEEDERS, pathTs: SEEDERS, defaultSeeder: 'DatabaseSeeder' },
dbName,
});
}
afterEach(() => {
if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true });
tmpDir = null;
});
describe('a restored database is migrated forward', () => {
it('RESTORE-001: a database stopped part-way through the history reaches head, and seeding survives the replay', async () => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'trek-restore-'));
const dbFile = path.join(tmpDir, 'old-backup.db');
// Stand in for a backup taken on an older release: apply only the first
// slice of the history and stop.
const stale = await ormFor(dbFile);
const staleMigrator = stale.config.getExtension('@mikro-orm/migrator') as Migrator;
const all = await staleMigrator.getPending();
await staleMigrator.up({ to: all[99].name });
await stale.close(true);
const probe = new Database(dbFile);
const tableCount = (sql: string) => (probe.prepare(sql).get() as { c: number }).c;
const applied = tableCount('SELECT COUNT(*) AS c FROM mikro_orm_migrations');
// `document_providers` arrives at step 236, well past where this stopped.
const lateTableBefore = tableCount(
`SELECT COUNT(*) AS c FROM sqlite_master WHERE type = 'table' AND name = 'document_providers'`,
);
probe.close();
expect(applied).toBe(100);
expect(lateTableBefore).toBe(0);
// What reinitialize() does after the file has been swapped in.
const restored = await ormFor(dbFile);
await runSchemaBootstrap(restored);
// Twice: a restore can follow a boot that already bootstrapped, and every
// seeder is written to tolerate that.
await runSchemaBootstrap(restored);
await restored.close(true);
const after = new Database(dbFile);
const count = (sql: string) => (after.prepare(sql).get() as { c: number }).c;
try {
expect(count('SELECT COUNT(*) AS c FROM mikro_orm_migrations')).toBe(all.length);
expect(
count(`SELECT COUNT(*) AS c FROM sqlite_master WHERE type = 'table' AND name = 'document_providers'`),
).toBe(1);
// Seeded exactly once despite the bootstrap running twice.
expect(count('SELECT COUNT(*) AS c FROM categories')).toBe(10);
expect(count('SELECT COUNT(*) AS c FROM schema_version')).toBe(1);
} finally {
after.close();
}
}, 60_000);
});