itns-sidechain/test/migrations-test.js

807 lines
19 KiB
JavaScript

'use strict';
const assert = require('bsert');
const fs = require('bfile');
const Logger = require('blgr');
const MigrationState = require('../lib/migrations/state');
const AbstractMigration = require('../lib/migrations/migration');
const {
Migrator,
types
} = require('../lib/migrations/migrator');
const {
DB_FLAG_ERROR,
MockChainDB,
migrationError,
mockLayout
} = require('./util/migrations');
const {rimraf, testdir} = require('./util/common');
class MockMigration1 extends AbstractMigration {
async check() {
return types.MIGRATE;
}
async migrate() {
return true;
}
static info() {
return {
name: 'mock migration 1',
description: 'mock description 1'
};
}
}
class MockMigration2 extends AbstractMigration {
async check() {
return types.MIGRATE;
}
async migrate() {
return true;
}
static info() {
return {
name: 'mock migration 2',
description: 'mock description 2'
};
}
}
describe('Migrations', function() {
describe('Migrate flag', function() {
let location, defaultOptions;
async function initDB() {
const db = new MockChainDB(defaultOptions);
await db.open();
await db.close();
}
beforeEach(async () => {
location = testdir('migration');
defaultOptions = {
prefix: location,
memory: false,
migrations: {
0: MockMigration1
}
};
await fs.mkdirp(location);
await initDB();
});
afterEach(async () => {
await rimraf(location);
});
it('should do nothing w/o new migrations & w/o flag', async () => {
const db = new MockChainDB({
...defaultOptions
});
await db.open();
await db.close();
});
it('should fail with new migration & w/o flag', async () => {
const migrations = {
0: MockMigration1,
1: MockMigration2
};
const db = new MockChainDB({
...defaultOptions,
migrations
});
const error = migrationError(migrations, [1], DB_FLAG_ERROR);
await assert.rejects(async () => {
await db.open();
}, {
message: error
});
});
it('should do nothing w/o new migrations & with correct flag', async () => {
const db = new MockChainDB({
...defaultOptions,
migrateFlag: 0
});
await db.open();
await db.close();
});
it('should do run migrations with correct flag', async () => {
let migrated = false;
const migrations = {
0: MockMigration1,
1: class extends AbstractMigration {
async check() {
return types.MIGRATE;
}
async migrate() {
migrated = true;
}
}
};
const db = new MockChainDB({
...defaultOptions,
migrateFlag: 1,
migrations
});
await db.open();
assert.strictEqual(migrated, true);
await db.close();
});
it('should fail w/o new migration & with incorrect flag', async () => {
const db = new MockChainDB({
...defaultOptions,
migrateFlag: 1
});
await assert.rejects(async () => {
await db.open();
}, {
message: 'Migrate flag 1 does not match last ID: 0'
});
});
it('should fail with new migrations & incorrect flag', async () => {
const migrations = {
0: MockMigration1,
1: MockMigration2
};
const db = new MockChainDB({
...defaultOptions,
migrateFlag: 0,
migrations
});
const error = migrationError(migrations, [1], DB_FLAG_ERROR);
let err;
try {
await db.open();
} catch (e) {
err = e;
}
assert(err);
assert.strictEqual(err.message, error);
});
});
describe('Running migrations', function() {
let location, defaultOptions;
async function initDB() {
const db = new MockChainDB(defaultOptions);
await db.open();
await db.close();
}
beforeEach(async () => {
location = testdir('migration');
defaultOptions = {
prefix: location,
memory: false
};
await fs.mkdirp(location);
await initDB();
});
afterEach(async () => {
await rimraf(location);
});
it('should initialize fresh migration state', async () => {
await rimraf(location);
const db = new MockChainDB({
...defaultOptions,
logger: getLogger({
warning: (msg) => {
throw new Error(`Unexpected warning: ${msg}`);
}
})
});
await db.open();
const state = await getMigrationState(db);
assert.strictEqual(state.inProgress, false);
assert.strictEqual(state.nextMigration, 0);
await db.close();
});
it('should ignore migration flag on non-existent db', async () => {
await rimraf(location);
let warning = null;
const db = new MockChainDB({
...defaultOptions,
migrations: { 0: MockMigration1 },
migrateFlag: 0,
logger: getLogger({
warning: (msg) => {
warning = msg;
}
})
});
await db.open();
assert.strictEqual(warning, 'Fresh start, ignoring migration flag.');
const state = await getMigrationState(db);
assert.strictEqual(state.inProgress, false);
assert.strictEqual(state.nextMigration, 1);
await db.close();
});
it('should throw if migrateFlag is incorrect', async () => {
const db = new MockChainDB({
...defaultOptions,
migrateFlag: 0
});
await assert.rejects(async () => {
await db.open();
}, {
message: 'Migrate flag 0 does not match last ID: -1'
});
});
it('should fail if there are available migrations', async () => {
const migrations = { 0: MockMigration1 };
const db = new MockChainDB({
...defaultOptions,
migrations
});
const error = migrationError(migrations, [0], DB_FLAG_ERROR);
await assert.rejects(async () => {
await db.open();
}, {
message: error
});
});
it('should fail if there are available migrations with id', async () => {
const migrations = {
0: class extends AbstractMigration {
async check() {}
async migrate() {}
static info() {
return {
name: 'mig1',
description: 'desc1'
};
}
},
1: class extends AbstractMigration {
async check() {}
async migrate() {}
static info() {
return {
name: 'mig2',
description: 'desc2'
};
}
}
};
const db = new MockChainDB({
...defaultOptions,
migrateFlag: 0,
migrations: migrations
});
const error = migrationError(migrations, [0, 1], DB_FLAG_ERROR);
await assert.rejects(async () => {
await db.open();
}, {
message: error
});
});
it('should migrate', async () => {
let migrated1 = false;
let migrated2 = false;
const db = new MockChainDB({
...defaultOptions,
migrateFlag: 1,
migrations: {
0: class extends AbstractMigration {
async check() {
return types.MIGRATE;
}
async migrate() {
migrated1 = true;
}
},
1: class extends AbstractMigration {
async check() {
return types.MIGRATE;
}
async migrate() {
migrated2 = true;
}
}
}
});
await db.open();
const state = await getMigrationState(db);
assert.strictEqual(state.nextMigration, 2);
assert.strictEqual(migrated1, true);
assert.strictEqual(migrated2, true);
await db.close();
});
it('should show in progress migration status', async () => {
let migrated1 = false;
let migrated2 = false;
const db = new MockChainDB({
...defaultOptions,
migrateFlag: 1,
migrations: {
0: class extends AbstractMigration {
async check() {
return types.MIGRATE;
}
async migrate() {
if (!migrated1) {
migrated1 = true;
throw new Error('in progress error1');
}
}
},
1: class extends AbstractMigration {
async check() {
return types.MIGRATE;
}
async migrate() {
if (!migrated2) {
migrated2 = true;
throw new Error('in progress error2');
}
}
}
}
});
await assert.rejects(async () => {
await db.open();
}, {
message: 'in progress error1'
});
await db.close();
{
// check the state is correct.
await db.db.open();
const state = await getMigrationState(db);
assert.strictEqual(state.inProgress, true);
assert.strictEqual(state.nextMigration, 0);
await db.db.close();
}
await assert.rejects(async () => {
await db.open();
}, {
message: 'in progress error2'
});
await db.close();
{
// check the state is correct.
await db.db.open();
const state = await getMigrationState(db);
assert.strictEqual(state.inProgress, true);
assert.strictEqual(state.nextMigration, 1);
await db.db.close();
}
await db.open();
const state = await getMigrationState(db);
assert.strictEqual(state.inProgress, false);
assert.strictEqual(state.nextMigration, 2);
assert.strictEqual(migrated1, true);
assert.strictEqual(migrated2, true);
await db.close();
});
it('should fake migrate if it does not apply', async () => {
let migrate = false;
const db = new MockChainDB({
...defaultOptions,
migrateFlag: 0,
migrations: {
0: class extends AbstractMigration {
async check() {
return types.FAKE_MIGRATE;
}
async migrate() {
migrate = true;
}
}
}
});
await db.open();
const state = await getMigrationState(db);
assert.strictEqual(state.inProgress, false);
assert.strictEqual(state.nextMigration, 1);
assert.strictEqual(migrate, false);
await db.close();
});
it('should skip migrations if it cant be fullfilled', async () => {
let checked = 0;
let showedWarning = 0;
let migrated = 0;
const db = new MockChainDB({
...defaultOptions,
migrateFlag: 0,
migrations: {
0: class extends AbstractMigration {
async check() {
checked++;
return types.SKIP;
}
async warning() {
showedWarning++;
}
async migrate() {
migrated++;
}
}
}
});
await db.open();
const state = await getMigrationState(db);
assert.strictEqual(state.inProgress, false);
assert.strictEqual(state.nextMigration, 1);
assert.deepStrictEqual(state.skipped, [0]);
assert.strictEqual(checked, 1);
assert.strictEqual(showedWarning, 1);
assert.strictEqual(migrated, 0);
await db.close();
db.options.migrateFlag = -1;
await db.open();
const state2 = await getMigrationState(db);
assert.strictEqual(state2.inProgress, false);
assert.strictEqual(state2.nextMigration, 1);
assert.deepStrictEqual(state2.skipped, [0]);
assert.strictEqual(checked, 1);
assert.strictEqual(showedWarning, 2);
assert.strictEqual(migrated, 0);
await db.close();
});
it('should fail with unknown migration type', async () => {
const db = new MockChainDB({
...defaultOptions,
migrateFlag: 0,
migrations: {
0: class extends AbstractMigration {
async check() {
return -1;
}
}
}
});
await assert.rejects(async () => {
await db.open();
}, {
message: 'Unknown migration type.'
});
});
it('should allow nextMigration modification, skip next', async () => {
let migrated1 = false;
let migrated2 = false;
const db = new MockChainDB({
...defaultOptions,
migrateFlag: 1,
migrations: {
0: class extends AbstractMigration {
async check() {
return types.MIGRATE;
}
async migrate(b, ctx) {
migrated1 = true;
ctx.state.nextMigration = 2;
}
},
1: class extends AbstractMigration {
async check() {
return types.MIGRATE;
}
async migrate() {
migrated2 = true;
}
}
}
});
await db.open();
assert.strictEqual(migrated1, true);
assert.strictEqual(migrated2, false);
await db.close();
});
it('should store inProgressData in version 1 and clean up', async () => {
let thrown1 = false;
let migrated1 = false;
let data1 = null;
let thrown3 = false;
let migrated3 = false;
let data3 = null;
let migrated4 = false;
let data4 = null;
const db = new MockChainDB({
...defaultOptions,
migrateFlag: 4,
migrations: {
0: class extends AbstractMigration {
async check() {
return types.MIGRATE;
}
async migrate(b, ctx) {
ctx.state.version = 0;
}
},
1: class extends AbstractMigration {
async check() {
return types.MIGRATE;
}
async migrate(b, ctx) {
if (!thrown1) {
// This wont be saved, state.version = 0.
ctx.state.inProgressData = Buffer.from('data1');
await ctx.saveState();
thrown1 = true;
throw new Error('error1');
}
migrated1 = true;
data1 = ctx.state.inProgressData;
}
},
2: class extends AbstractMigration {
async check() {
return types.MIGRATE;
}
async migrate(b, ctx) {
ctx.state.version = 1;
}
},
3: class extends AbstractMigration {
async check() {
return types.MIGRATE;
}
async migrate(b, ctx) {
if (!thrown3) {
ctx.state.inProgressData = Buffer.from('data3');
await ctx.saveState();
thrown3 = true;
throw new Error('error3');
}
migrated3 = true;
data3 = ctx.state.inProgressData;
}
},
4: class extends AbstractMigration {
async check() {
return types.MIGRATE;
}
async migrate(b, ctx) {
migrated4 = true;
data4 = ctx.state.inProgressData;
}
}
},
logger: getLogger()
});
for (let i = 0; i < 2; i++) {
try {
await db.open();
} catch (e) {
await db.close();
}
}
await db.open();
assert.strictEqual(migrated1, true);
assert.strictEqual(data1.length, 0);
assert.strictEqual(migrated3, true);
assert.bufferEqual(data3, Buffer.from('data3'));
assert.strictEqual(migrated4, true);
assert.strictEqual(data4.length, 0);
await db.close();
});
});
describe('Options', function() {
it('should fail w/o db && ldb', () => {
const errors = [
{ opts: null, err: 'Migration options are required.' },
{ opts: {}, err: 'options.db is required.' },
{ opts: { db: {} }, err: 'options.ldb is required.' },
{
opts: { db: 1, ldb: {} },
err: 'options.db needs to be an object.'
},
{
opts: { db: {}, ldb: 1 },
err: 'options.ldb needs to be an object.'
}
];
for (const {opts, err} of errors)
assert.throws(() => new Migrator(opts), { message: err });
});
});
describe('MigrationState', function() {
it('should clone the state', () => {
const state1 = new MigrationState();
state1.inProgress = 1;
state1.nextMigration = 3;
state1.skipped = [1, 2];
state1.inProgressData = Buffer.from('data');
const state2 = state1.clone();
assert.notEqual(state2.skipped, state1.skipped);
assert.deepStrictEqual(state2, state1);
});
it('should not encode progress data if version is 0', () => {
const state = new MigrationState();
state.version = 0;
state.inProgressData = Buffer.from('data');
const encoded = state.encode();
const decoded = MigrationState.decode(encoded);
assert.strictEqual(decoded.inProgressData.length, 0);
});
it('should encode progress data if version is not 0', () => {
const state = new MigrationState();
state.version = 1;
state.inProgressData = Buffer.from('data');
const encoded = state.encode();
const decoded = MigrationState.decode(encoded);
assert.bufferEqual(decoded.inProgressData, state.inProgressData);
});
});
describe('AbstractMigration', function() {
let logger = null;
function context(ctx) {
return {warning: () => ctx};
}
const baseMethods = ['check', 'migrate'];
beforeEach(() => {
logger = Logger.global;
Logger.global = {context};
});
afterEach(() => {
Logger.global = logger;
});
it('construct with custom logger', async () => {
const migration = new AbstractMigration({logger: {context}});
assert(migration.logger);
assert(migration.logger.warning);
assert.strictEqual(migration.logger.warning(), 'migration');
});
it('should have method: warning', () => {
let logged;
const migration = new AbstractMigration({
logger: {
context: () => {
return {
warning: () => {
logged = true;
}
};
}
}
});
assert(migration.warning);
migration.warning();
assert.strictEqual(logged, true);
});
for (const method of baseMethods) {
it(`should have unimplemented method: ${method}`, async () => {
const migration = new AbstractMigration({logger: {context}});
assert(migration[method]);
await assert.rejects(async () => {
await migration[method]();
}, {
name: 'Error',
message: 'Abstract method.'
});
});
}
});
});
const nop = () => {};
function getLogger(opts = {}) {
return {
debug: nop,
info: nop,
warning: nop,
error: nop,
context: () => {
return {
debug: opts.debug || nop,
info: opts.info || nop,
warning: opts.warning || nop,
error: opts.error || nop
};
}
};
}
async function getMigrationState(db) {
const raw = await db.db.get(mockLayout.M.encode());
return MigrationState.decode(raw);
}