feat(smartmigration): add initial smartmigration package with MongoDB and S3 migration runner
This commit is contained in:
124
test/test.run.mongo.ts
Normal file
124
test/test.run.mongo.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { makeTestDb } from './helpers/services.js';
|
||||
import type * as smartdata from '@push.rocks/smartdata';
|
||||
import { SmartMigration, SmartMigrationError } from '../ts/index.js';
|
||||
|
||||
// Shared db across the file (see test.mongoledger.ts for the rationale).
|
||||
// Each test uses a unique ledgerName for isolation.
|
||||
let db: smartdata.SmartdataDb;
|
||||
let cleanup: () => Promise<void>;
|
||||
|
||||
tap.test('setup: connect shared db', async () => {
|
||||
const r = await makeTestDb('run_mongo');
|
||||
db = r.db;
|
||||
cleanup = r.cleanup;
|
||||
});
|
||||
|
||||
tap.test('run: applies all steps from scratch', async () => {
|
||||
const m = new SmartMigration({ targetVersion: '2.0.0', db, ledgerName: 'scratch' });
|
||||
const log: string[] = [];
|
||||
m
|
||||
.step('a').from('1.0.0').to('1.1.0').up(async () => { log.push('a'); })
|
||||
.step('b').from('1.1.0').to('1.5.0').up(async () => { log.push('b'); })
|
||||
.step('c').from('1.5.0').to('2.0.0').up(async () => { log.push('c'); });
|
||||
|
||||
const r = await m.run();
|
||||
expect(r.stepsApplied).toHaveLength(3);
|
||||
expect(log).toEqual(['a', 'b', 'c']);
|
||||
expect(r.currentVersionAfter).toEqual('2.0.0');
|
||||
expect(r.wasUpToDate).toBeFalse();
|
||||
expect(r.targetVersion).toEqual('2.0.0');
|
||||
});
|
||||
|
||||
tap.test('run: second run is a no-op', async () => {
|
||||
const m = new SmartMigration({ targetVersion: '2.0.0', db, ledgerName: 'idempotent' });
|
||||
const log: string[] = [];
|
||||
m
|
||||
.step('a').from('1.0.0').to('1.1.0').up(async () => { log.push('a'); })
|
||||
.step('b').from('1.1.0').to('2.0.0').up(async () => { log.push('b'); });
|
||||
|
||||
const r1 = await m.run();
|
||||
expect(r1.stepsApplied).toHaveLength(2);
|
||||
expect(log).toEqual(['a', 'b']);
|
||||
|
||||
// Re-run with a freshly-constructed runner using the same ledger.
|
||||
const m2 = new SmartMigration({ targetVersion: '2.0.0', db, ledgerName: 'idempotent' });
|
||||
m2
|
||||
.step('a').from('1.0.0').to('1.1.0').up(async () => { log.push('a-again'); })
|
||||
.step('b').from('1.1.0').to('2.0.0').up(async () => { log.push('b-again'); });
|
||||
const r2 = await m2.run();
|
||||
expect(r2.wasUpToDate).toBeTrue();
|
||||
expect(r2.stepsApplied).toHaveLength(0);
|
||||
expect(log).toEqual(['a', 'b']); // no new entries
|
||||
});
|
||||
|
||||
tap.test('run: failing step throws and persists failure', async () => {
|
||||
const m = new SmartMigration({ targetVersion: '2.0.0', db, ledgerName: 'failure' });
|
||||
let aCalled = false;
|
||||
m
|
||||
.step('a').from('1.0.0').to('1.1.0').up(async () => { aCalled = true; })
|
||||
.step('b').from('1.1.0').to('2.0.0').up(async () => { throw new Error('boom'); });
|
||||
|
||||
let caught: SmartMigrationError | undefined;
|
||||
try {
|
||||
await m.run();
|
||||
} catch (err) {
|
||||
caught = err as SmartMigrationError;
|
||||
}
|
||||
expect(caught).toBeInstanceOf(SmartMigrationError);
|
||||
expect(caught!.code).toEqual('STEP_FAILED');
|
||||
expect(aCalled).toBeTrue();
|
||||
|
||||
// Ledger should be at 1.1.0 (after step a, before failed step b).
|
||||
const current = await m.getCurrentVersion();
|
||||
expect(current).toEqual('1.1.0');
|
||||
});
|
||||
|
||||
tap.test('run: ctx exposes db, mongo, and step metadata', async () => {
|
||||
const m = new SmartMigration({ targetVersion: '1.1.0', db, ledgerName: 'ctx' });
|
||||
let observed: { hasDb: boolean; hasMongo: boolean; stepId: string; from: string; to: string } | null = null;
|
||||
m.step('check-ctx').from('1.0.0').to('1.1.0').up(async (ctx) => {
|
||||
observed = {
|
||||
hasDb: !!ctx.db,
|
||||
hasMongo: !!ctx.mongo,
|
||||
stepId: ctx.step.id,
|
||||
from: ctx.step.fromVersion,
|
||||
to: ctx.step.toVersion,
|
||||
};
|
||||
// exercise raw mongo
|
||||
await ctx.mongo!.collection('users_ctx_test').insertOne({ name: 'alice' });
|
||||
});
|
||||
await m.run();
|
||||
expect(observed).not.toBeNull();
|
||||
expect(observed!.hasDb).toBeTrue();
|
||||
expect(observed!.hasMongo).toBeTrue();
|
||||
expect(observed!.stepId).toEqual('check-ctx');
|
||||
expect(observed!.from).toEqual('1.0.0');
|
||||
expect(observed!.to).toEqual('1.1.0');
|
||||
// verify the data the step wrote
|
||||
const found = await db.mongoDb.collection('users_ctx_test').findOne({ name: 'alice' });
|
||||
expect(found).not.toBeNull();
|
||||
});
|
||||
|
||||
tap.test('run: ctx.startSession works inside a step', async () => {
|
||||
const m = new SmartMigration({ targetVersion: '1.1.0', db, ledgerName: 'session' });
|
||||
let sessionWasUsed = false;
|
||||
m.step('with-session').from('1.0.0').to('1.1.0').up(async (ctx) => {
|
||||
const session = ctx.startSession();
|
||||
try {
|
||||
sessionWasUsed = !!session.id;
|
||||
await session.endSession();
|
||||
} catch (err) {
|
||||
await session.endSession();
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
await m.run();
|
||||
expect(sessionWasUsed).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('cleanup: close shared db', async () => {
|
||||
await cleanup();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
Reference in New Issue
Block a user