0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-17 23:44:39 -05:00

Merge pull request #6609 from ErisDS/new-db-upgrade

Rewrite DB update to be explicit
This commit is contained in:
Sebastian Gierlinger 2016-03-21 16:14:59 +01:00
commit ea9c8235fa
14 changed files with 1288 additions and 441 deletions

View file

@ -0,0 +1,18 @@
var commands = require('../../schema').commands,
db = require('../../db'),
table = 'users',
column = 'tour';
module.exports = function addTourColumnToUsers(logInfo) {
return db.knex.schema.hasTable(table).then(function (exists) {
if (exists) {
return db.knex.schema.hasColumn(table, column).then(function (exists) {
if (!exists) {
logInfo('Adding column: ' + table + '.' + column);
return commands.addColumn(table, column);
}
});
}
});
};

View file

@ -0,0 +1,18 @@
var commands = require('../../schema').commands,
db = require('../../db'),
table = 'posts_tags',
column = 'sort_order';
module.exports = function addSortOrderColumnToPostsTags(logInfo) {
return db.knex.schema.hasTable(table).then(function (exists) {
if (exists) {
return db.knex.schema.hasColumn(table, column).then(function (exists) {
if (!exists) {
logInfo('Adding column: ' + table + '.' + column);
return commands.addColumn(table, column);
}
});
}
});
};

View file

@ -0,0 +1,21 @@
var Promise = require('bluebird'),
commands = require('../../schema').commands,
db = require('../../db'),
table = 'clients',
columns = ['redirection_uri', 'logo', 'status', 'type', 'description'];
module.exports = function addManyColumnsToClients(logInfo) {
return db.knex.schema.hasTable(table).then(function (exists) {
if (exists) {
return Promise.mapSeries(columns, function (column) {
return db.knex.schema.hasColumn(table, column).then(function (exists) {
if (!exists) {
logInfo('Adding column: ' + table + '.' + column);
return commands.addColumn(table, column);
}
});
});
}
});
};

View file

@ -0,0 +1,13 @@
var commands = require('../../schema').commands,
db = require('../../db'),
table = 'client_trusted_domains';
module.exports = function addClientTrustedDomainsTable(logInfo) {
return db.knex.schema.hasTable(table).then(function (exists) {
if (!exists) {
logInfo('Creating table: ' + table);
return commands.createTable(table);
}
});
};

View file

@ -0,0 +1,18 @@
var commands = require('../../schema').commands,
db = require('../../db'),
table = 'clients',
column = 'secret';
module.exports = function dropUniqueOnClientsSecret(logInfo) {
return db.knex.schema.hasTable(table).then(function (exists) {
if (exists) {
return commands.getIndexes(table).then(function (indexes) {
if (indexes.indexOf(table + '_' + column + '_unique') > -1) {
logInfo('Dropping unique on: ' + table + '.' + column);
return commands.dropUnique(table, column);
}
});
}
});
};

View file

@ -0,0 +1,12 @@
module.exports = [
// Added tour column to users
require('./01-add-tour-column-to-users'),
// Added sort_order to posts_tags
require('./02-add-sortorder-column-to-poststags'),
// Added redirection_uri, logo, status, type & description columns to clients
require('./03-add-many-columns-to-clients'),
// Added client_trusted_domains table
require('./04-add-clienttrusteddomains-table'),
// Dropped unique index on client secret
require('./05-drop-unique-on-clients-secret')
];

View file

@ -1,91 +0,0 @@
var _ = require('lodash'),
errors = require('../../errors'),
commands = require('../schema').commands,
schema = require('../schema').tables,
// private
logInfo,
// public
getDeleteCommands,
getAddCommands,
addColumnCommands,
dropColumnCommands,
modifyUniqueCommands;
logInfo = function logInfo(message) {
errors.logInfo('Migrations', message);
};
getDeleteCommands = function getDeleteCommands(oldTables, newTables) {
var deleteTables = _.difference(oldTables, newTables);
return _.map(deleteTables, function (table) {
return function () {
logInfo('Deleting table: ' + table);
return commands.deleteTable(table);
};
});
};
getAddCommands = function getAddCommands(oldTables, newTables) {
var addTables = _.difference(newTables, oldTables);
return _.map(addTables, function (table) {
return function () {
logInfo('Creating table: ' + table);
return commands.createTable(table);
};
});
};
addColumnCommands = function addColumnCommands(table, columns) {
var columnKeys = _.keys(schema[table]),
addColumns = _.difference(columnKeys, columns);
return _.map(addColumns, function (column) {
return function () {
logInfo('Adding column: ' + table + '.' + column);
return commands.addColumn(table, column);
};
});
};
dropColumnCommands = function dropColumnCommands(table, columns) {
var columnKeys = _.keys(schema[table]),
dropColumns = _.difference(columns, columnKeys);
return _.map(dropColumns, function (column) {
return function () {
logInfo('Dropping column: ' + table + '.' + column);
return commands.dropColumn(table, column);
};
});
};
modifyUniqueCommands = function modifyUniqueCommands(table, indexes) {
var columnKeys = _.keys(schema[table]);
return _.map(columnKeys, function (column) {
if (schema[table][column].unique === true) {
if (!_.contains(indexes, table + '_' + column + '_unique')) {
return function () {
logInfo('Adding unique on: ' + table + '.' + column);
return commands.addUnique(table, column);
};
}
} else if (!schema[table][column].unique) {
if (_.contains(indexes, table + '_' + column + '_unique')) {
return function () {
logInfo('Dropping unique on: ' + table + '.' + column);
return commands.dropUnique(table, column);
};
}
}
});
};
module.exports = {
getDeleteCommands: getDeleteCommands,
getAddCommands: getAddCommands,
addColumnCommands: addColumnCommands,
dropColumnCommands: dropColumnCommands,
modifyUniqueCommands: modifyUniqueCommands
};

View file

@ -4,36 +4,14 @@
// E.g. if we update to version 004, all the tasks in /004/ are executed
var sequence = require('../../../utils/sequence'),
versioning = require('../../schema').versioning,
// Private
getVersionTasks,
modelOptions = {context: {internal: true}},
// Public
update;
/**
* ### Get Version Tasks
* Tries to require a directory matching the version number
*
* This was split from update to make testing easier
*
* @param {String} version
* @param {Function} logInfo
* @returns {Array}
*/
getVersionTasks = function getVersionTasks(version, logInfo) {
var tasks = [];
try {
tasks = require('./' + version);
} catch (e) {
logInfo('No fixture updates found for version', version);
}
return tasks;
};
/**
* ## Update
* Handles doing subsequent updates for versions
@ -43,20 +21,20 @@ getVersionTasks = function getVersionTasks(version, logInfo) {
* @returns {Promise<*>}
*/
update = function update(versions, logInfo) {
var ops = [];
logInfo('Updating fixtures');
versions.forEach(function (version) {
var tasks = getVersionTasks(version, logInfo);
var ops = versions.reduce(function updateToVersion(ops, version) {
var tasks = versioning.getUpdateFixturesTasks(version, logInfo);
if (tasks && tasks.length > 0) {
ops.push(function () {
ops.push(function runVersionTasks() {
logInfo('Updating fixtures to ', version);
return sequence(require('./' + version), modelOptions, logInfo);
return sequence(tasks, modelOptions, logInfo);
});
}
});
return ops;
}, []);
return sequence(ops, modelOptions, logInfo);
};

View file

@ -44,22 +44,21 @@ init = function (tablesOnly) {
return versioning.getDatabaseVersion().then(function (databaseVersion) {
var defaultVersion = versioning.getDefaultDatabaseVersion();
if (databaseVersion < defaultVersion || process.env.FORCE_MIGRATION) {
// Update goes first, to allow for FORCE_MIGRATION
// 2. The database exists but is out of date
if (databaseVersion < defaultVersion || process.env.FORCE_MIGRATION) {
// Migrate to latest version
logInfo('Database upgrade required from version ' + databaseVersion + ' to ' + defaultVersion);
return update(databaseVersion, defaultVersion, logInfo);
}
if (databaseVersion === defaultVersion) {
// 1. The database exists and is up-to-date
} else if (databaseVersion === defaultVersion) {
logInfo('Up to date at version ' + databaseVersion);
// TODO: temporary fix for missing client.secret
return fixClientSecret();
}
if (databaseVersion > defaultVersion) {
// 3. The database exists but the currentVersion setting does not or cannot be understood
} else {
// In this case we don't understand the version because it is too high
errors.logErrorAndExit(
'Your database is not compatible with this version of Ghost',
@ -75,7 +74,7 @@ init = function (tablesOnly) {
}
// 3. The database exists but the currentVersion setting does not or cannot be understood
// In this case the setting was missing or there was some other problem
errors.logErrorAndExit('There is a problem with the database', err.message || err);
errors.logErrorAndExit('There is a problem with the database', err.message);
});
};

View file

@ -1,67 +1,44 @@
// # Update Database
// Handles migrating a database between two different database versions
var _ = require('lodash'),
Promise = require('bluebird'),
backup = require('./backup'),
builder = require('./builder'),
commands = require('../schema').commands,
fixtures = require('./fixtures'),
schema = require('../schema').tables,
sequence = require('../../utils/sequence'),
versioning = require('../schema').versioning,
schemaTables = Object.keys(schema),
updateDatabaseSchema,
// Public
update;
/**
* ### Update Database Schema
* Automatically detect differences between the current DB and the schema, and fix them
* TODO refactor to use explicit instructions, as this has the potential to destroy data
* Fetch the update tasks for each version, and iterate through them in order
*
* @param {Array} versions
* @param {Function} logInfo
* @returns {Promise<*>}
*/
updateDatabaseSchema = function updateDatabaseSchema(logInfo) {
var oldTables,
modifyUniCommands = [],
migrateOps = [];
updateDatabaseSchema = function updateDatabaseSchema(versions, logInfo) {
var migrateOps = versions.reduce(function updateToVersion(migrateOps, version) {
var tasks = versioning.getUpdateDatabaseTasks(version, logInfo);
return commands.getTables().then(function (tables) {
oldTables = tables;
if (!_.isEmpty(oldTables)) {
return commands.checkTables();
if (tasks && tasks.length > 0) {
migrateOps.push(function runVersionTasks() {
logInfo('Updating database to ', version);
return sequence(tasks, logInfo);
});
}
}).then(function () {
migrateOps = migrateOps.concat(builder.getDeleteCommands(oldTables, schemaTables));
migrateOps = migrateOps.concat(builder.getAddCommands(oldTables, schemaTables));
return Promise.all(
_.map(oldTables, function (table) {
return commands.getIndexes(table).then(function (indexes) {
modifyUniCommands = modifyUniCommands.concat(builder.modifyUniqueCommands(table, indexes));
});
})
);
}).then(function () {
return Promise.all(
_.map(oldTables, function (table) {
return commands.getColumns(table).then(function (columns) {
migrateOps = migrateOps.concat(builder.dropColumnCommands(table, columns));
migrateOps = migrateOps.concat(builder.addColumnCommands(table, columns));
});
})
);
}).then(function () {
migrateOps = migrateOps.concat(_.compact(modifyUniCommands));
return migrateOps;
}, []);
// execute the commands in sequence
if (!_.isEmpty(migrateOps)) {
logInfo('Running migrations');
return sequence(migrateOps);
}
});
return sequence(migrateOps, logInfo);
};
/**
@ -80,19 +57,22 @@ update = function update(fromVersion, toVersion, logInfo) {
return versioning.showCannotMigrateError();
}
fromVersion = process.env.FORCE_MIGRATION ? versioning.canMigrateFromVersion : fromVersion;
// Figure out which versions we're updating through.
// This shouldn't include the from/current version (which we're already on)
var versionsToUpdate = versioning.getMigrationVersions(fromVersion, toVersion).slice(1);
return backup(logInfo).then(function () {
return updateDatabaseSchema(logInfo);
return updateDatabaseSchema(versionsToUpdate, logInfo);
}).then(function () {
// Ensure all of the current default settings are created (these are fixtures, so should be inserted first)
return fixtures.ensureDefaultSettings(logInfo);
}).then(function () {
fromVersion = process.env.FORCE_MIGRATION ? versioning.canMigrateFromVersion : fromVersion;
var versions = versioning.getMigrationVersions(fromVersion, toVersion);
// Finally, run any updates to the fixtures, including default settings, that are required
// for anything other than the from/current version (which we're already on)
return fixtures.update(versions.slice(1), logInfo);
// Next, run any updates to the fixtures, including default settings, that are required
return fixtures.update(versionsToUpdate, logInfo);
}).then(function () {
// Finally update the databases current version
// Finally update the database's current version
return versioning.setDatabaseVersion();
});
};

View file

@ -1,4 +1,5 @@
var db = require('../db'),
var path = require('path'),
db = require('../db'),
errors = require('../../errors'),
i18n = require('../../i18n'),
defaultSettings = require('./default-settings'),
@ -72,11 +73,44 @@ function showCannotMigrateError() {
);
}
/**
* ### Get Version Tasks
* Tries to require a directory matching the version number
*
* This was split from update to make testing easier
*
* @param {String} version
* @param {String} relPath
* @param {Function} logInfo
* @returns {Array}
*/
function getVersionTasks(version, relPath, logInfo) {
var tasks = [];
try {
tasks = require(path.join(relPath, version));
} catch (e) {
logInfo('No tasks found for version', version);
}
return tasks;
}
function getUpdateDatabaseTasks(version, logInfo) {
return getVersionTasks(version, '../migration/', logInfo);
}
function getUpdateFixturesTasks(version, logInfo) {
return getVersionTasks(version, '../migration/fixtures/', logInfo);
}
module.exports = {
canMigrateFromVersion: '003',
showCannotMigrateError: showCannotMigrateError,
getDefaultDatabaseVersion: getDefaultDatabaseVersion,
getDatabaseVersion: getDatabaseVersion,
setDatabaseVersion: setDatabaseVersion,
getMigrationVersions: getMigrationVersions
getMigrationVersions: getMigrationVersions,
getUpdateDatabaseTasks: getUpdateDatabaseTasks,
getUpdateFixturesTasks: getUpdateFixturesTasks
};

View file

@ -8,6 +8,7 @@ var should = require('should'),
configUtils = require('../utils/configUtils'),
models = require('../../server/models'),
notifications = require('../../server/api/notifications'),
versioning = require('../../server/data/schema/versioning'),
update = rewire('../../server/data/migration/fixtures/update'),
populate = rewire('../../server/data/migration/fixtures/populate'),
fixtures004 = require('../../server/data/migration/fixtures/004'),
@ -28,37 +29,41 @@ describe('Fixtures', function () {
});
describe('Update fixtures', function () {
it('should call `getVersionTasks` when upgrading from 003 -> 004', function (done) {
it('should call `getUpdateFixturesTasks` when upgrading from 003 -> 004', function (done) {
var logStub = sandbox.stub(),
getVersionTasksStub = sandbox.stub().returns([]),
reset = update.__set__('getVersionTasks', getVersionTasksStub);
getVersionTasksStub = sandbox.stub(versioning, 'getUpdateFixturesTasks').returns([]);
update(['004'], logStub).then(function () {
logStub.calledOnce.should.be.true();
getVersionTasksStub.calledOnce.should.be.true();
reset();
done();
}).catch(done);
});
it('should NOT call `getVersionTasks` when upgrading from 004 -> 004', function (done) {
it('should NOT call `getUpdateFixturesTasks` when upgrading from 004 -> 004', function (done) {
var logStub = sandbox.stub(),
getVersionTasksStub = sandbox.stub().returns(Promise.resolve()),
reset = update.__set__('getVersionTasks', getVersionTasksStub);
getVersionTasksStub = sandbox.stub(versioning, 'getUpdateFixturesTasks').returns([]);
update([], logStub).then(function () {
logStub.calledOnce.should.be.true();
getVersionTasksStub.calledOnce.should.be.false();
reset();
done();
}).catch(done);
});
it('`getVersionTasks` returns empty array if no tasks are found', function () {
var logStub = sandbox.stub();
it('should call tasks in correct order if provided', function (done) {
var logStub = sandbox.stub(),
task1Stub = sandbox.stub().returns(Promise.resolve()),
task2Stub = sandbox.stub().returns(Promise.resolve()),
getVersionTasksStub = sandbox.stub(versioning, 'getUpdateFixturesTasks').returns([task1Stub, task2Stub]);
update.__get__('getVersionTasks')('999', logStub).should.eql([]);
logStub.calledOnce.should.be.true();
update(['000'], logStub).then(function () {
logStub.calledTwice.should.be.true();
getVersionTasksStub.calledOnce.should.be.true();
task1Stub.calledOnce.should.be.true();
task2Stub.calledOnce.should.be.true();
done();
}).catch(done);
});
describe('Update to 004', function () {
@ -101,6 +106,7 @@ describe('Fixtures', function () {
}).catch(done);
});
describe('Tasks:', function () {
describe('01-move-jquery-with-alert', function () {
it('tries to move jQuery to ghost_foot', function (done) {
var logStub = sandbox.stub(),
@ -374,6 +380,7 @@ describe('Fixtures', function () {
});
});
});
});
describe('Populate fixtures', function () {
// This tests that all the models & relations get called correctly

View file

@ -1,18 +1,27 @@
/*globals describe, it, afterEach*/
/*globals describe, it, afterEach, beforeEach*/
var should = require('should'),
sinon = require('sinon'),
rewire = require('rewire'),
_ = require('lodash'),
Promise = require('bluebird'),
crypto = require('crypto'),
fs = require('fs'),
// Stuff we are testing
db = require('../../server/data/db'),
errors = require('../../server/errors/'),
exporter = require('../../server/data/export'),
fixtures = require('../../server/data/migration/fixtures'),
migration = require('../../server/data/migration'),
populate = require('../../server/data/migration/populate'),
schema = require('../../server/data/schema'),
// TODO: can go when fixClientSecret is moved
models = require('../../server/models'),
migration = rewire('../../server/data/migration'),
fixtures = require('../../server/data/migration/fixtures'),
populate = require('../../server/data/migration/populate'),
update = rewire('../../server/data/migration/update'),
updates004 = require('../../server/data/migration/004'),
defaultSettings = schema.defaultSettings,
schemaTables = Object.keys(schema.tables),
@ -167,7 +176,7 @@ describe('Migrations', function () {
settingsStub.calledOnce.should.be.true();
done();
});
}).catch(done);
});
it('should should only create tables, with tablesOnly setting', function (done) {
@ -190,11 +199,812 @@ describe('Migrations', function () {
settingsStub.called.should.be.false();
done();
});
}).catch(done);
});
});
describe('Update', function () {
it('should be tested!');
describe('Update function', function () {
var reset, backupStub, settingsStub, fixturesStub, setDbStub, errorStub, logStub, versionsSpy;
beforeEach(function () {
// Stubs
backupStub = sandbox.stub().returns(new Promise.resolve());
settingsStub = sandbox.stub(fixtures, 'ensureDefaultSettings').returns(new Promise.resolve());
fixturesStub = sandbox.stub(fixtures, 'update').returns(new Promise.resolve());
setDbStub = sandbox.stub(schema.versioning, 'setDatabaseVersion').returns(new Promise.resolve());
errorStub = sandbox.stub(schema.versioning, 'showCannotMigrateError').returns(new Promise.resolve());
logStub = sandbox.stub();
// Spys
versionsSpy = sandbox.spy(schema.versioning, 'getMigrationVersions');
// Internal overrides
reset = update.__set__('backup', backupStub);
});
afterEach(function () {
reset();
});
describe('Pre & post update process', function () {
var updateStub, updateReset;
beforeEach(function () {
// For these tests, stub out the actual update task
updateStub = sandbox.stub().returns(new Promise.resolve());
updateReset = update.__set__('updateDatabaseSchema', updateStub);
});
afterEach(function () {
updateReset();
});
it('should attempt to run the pre & post update tasks correctly', function (done) {
// Execute
update('100', '102', logStub).then(function () {
// Before the update, it does some tasks...
// It should not show an error for these versions
errorStub.called.should.be.false();
// getMigrationVersions should be called with the correct versions
versionsSpy.calledOnce.should.be.true();
versionsSpy.calledWith('100', '102').should.be.true();
// It should attempt to do a backup
backupStub.calledOnce.should.be.true();
// Now it's going to try to actually do the update...
updateStub.calledOnce.should.be.true();
updateStub.calledWith(['101', '102'], logStub).should.be.true();
// And now there are some final tasks to wrap up...
// First, the ensure default settings task
settingsStub.calledOnce.should.be.true();
// Then fixture updates
fixturesStub.calledOnce.should.be.true();
// And finally, set the new DB version
setDbStub.calledOnce.should.be.true();
// Because we stubbed everything, logStub didn't get called
logStub.called.should.be.false();
// Just to be sure, lets assert the call order
sinon.assert.callOrder(
versionsSpy, backupStub, updateStub, settingsStub, fixturesStub, setDbStub
);
done();
}).catch(done);
});
it('should throw error if versions are too old', function (done) {
// Execute
update('000', '002', logStub).then(function () {
// It should show an error for these versions
errorStub.called.should.be.true();
// And so should not do the update...
updateStub.calledOnce.should.be.false();
// Because we stubbed everything, logStub didn't get called
logStub.called.should.be.false();
done();
}).catch(done);
});
it('should upgrade from minimum version, if FORCE_MIGRATION is set', function (done) {
// Setup
process.env.FORCE_MIGRATION = true;
// Execute
update('005', '006', logStub).then(function () {
// It should not show an error for these versions
errorStub.called.should.be.false();
// getMigrationVersions should be called with the correct versions
versionsSpy.calledOnce.should.be.true();
versionsSpy.calledWith('003', '006').should.be.true();
versionsSpy.returned(['003', '004', '005', '006']).should.be.true();
// It should try to do the update
updateStub.calledOnce.should.be.true();
updateStub.calledWith(['004', '005', '006']).should.be.true();
// Because we stubbed everything, logStub didn't get called
logStub.called.should.be.false();
// Restore
delete process.env.FORCE_MIGRATION;
done();
}).catch(done);
});
});
describe('Update to 004', function () {
var knexStub, knexMock;
beforeEach(function () {
knexMock = sandbox.stub().returns({});
knexMock.schema = {
hasTable: sandbox.stub(),
hasColumn: sandbox.stub()
};
// this MUST use sinon, not sandbox, see sinonjs/sinon#781
knexStub = sinon.stub(db, 'knex', {get: function () { return knexMock; }});
});
afterEach(function () {
knexStub.restore();
});
it('should call all the 004 database upgrades', function (done) {
// Setup
var logStub = sandbox.stub();
// stub has table, so that the next action won't happen
knexMock.schema.hasTable.withArgs('users').returns(new Promise.resolve(false));
knexMock.schema.hasTable.withArgs('posts_tags').returns(new Promise.resolve(false));
knexMock.schema.hasTable.withArgs('clients').returns(new Promise.resolve(false));
knexMock.schema.hasTable.withArgs('client_trusted_domains').returns(new Promise.resolve(true));
// Execute
update('003', '004', logStub).then(function () {
errorStub.called.should.be.false();
logStub.calledTwice.should.be.true();
versionsSpy.calledOnce.should.be.true();
versionsSpy.calledWith('003', '004').should.be.true();
versionsSpy.returned(['003', '004']).should.be.true();
knexStub.get.callCount.should.eql(5);
knexMock.schema.hasTable.callCount.should.eql(5);
knexMock.schema.hasColumn.called.should.be.false();
done();
}).catch(done);
});
describe('Tasks:', function () {
describe('01-add-tour-column-to-users', function () {
it('does not try to add a new column if the column already exists', function (done) {
// Setup
var logStub = sandbox.stub(),
addColumnStub = sandbox.stub(schema.commands, 'addColumn');
knexMock.schema.hasTable.withArgs('users').returns(new Promise.resolve(true));
knexMock.schema.hasColumn.withArgs('users', 'tour').returns(new Promise.resolve(true));
// Execute
updates004[0](logStub).then(function () {
knexMock.schema.hasTable.calledOnce.should.be.true();
knexMock.schema.hasTable.calledWith('users').should.be.true();
knexMock.schema.hasColumn.calledOnce.should.be.true();
knexMock.schema.hasColumn.calledWith('users', 'tour').should.be.true();
addColumnStub.called.should.be.false();
logStub.called.should.be.false();
done();
});
});
it('tries to add a new column if table is present but column is not', function (done) {
// Setup
var logStub = sandbox.stub(),
addColumnStub = sandbox.stub(schema.commands, 'addColumn');
knexMock.schema.hasTable.withArgs('users').returns(new Promise.resolve(true));
knexMock.schema.hasColumn.withArgs('users', 'tour').returns(new Promise.resolve(false));
// Execute
updates004[0](logStub).then(function () {
knexMock.schema.hasTable.calledOnce.should.be.true();
knexMock.schema.hasTable.calledWith('users').should.be.true();
knexMock.schema.hasColumn.calledOnce.should.be.true();
knexMock.schema.hasColumn.calledWith('users', 'tour').should.be.true();
addColumnStub.calledOnce.should.be.true();
addColumnStub.calledWith('users', 'tour').should.be.true();
logStub.calledOnce.should.be.true();
done();
});
});
});
describe('02-add-sortorder-column-to-poststags', function () {
it('does not try to add a new column if the column already exists', function (done) {
// Setup
var logStub = sandbox.stub(),
addColumnStub = sandbox.stub(schema.commands, 'addColumn');
knexMock.schema.hasTable.withArgs('posts_tags').returns(new Promise.resolve(true));
knexMock.schema.hasColumn.withArgs('posts_tags', 'sort_order').returns(new Promise.resolve(true));
// Execute
updates004[1](logStub).then(function () {
knexMock.schema.hasTable.calledOnce.should.be.true();
knexMock.schema.hasTable.calledWith('posts_tags').should.be.true();
knexMock.schema.hasColumn.calledOnce.should.be.true();
knexMock.schema.hasColumn.calledWith('posts_tags', 'sort_order').should.be.true();
addColumnStub.called.should.be.false();
logStub.called.should.be.false();
done();
});
});
it('tries to add a new column if table is present but column is not', function (done) {
// Setup
var logStub = sandbox.stub(),
addColumnStub = sandbox.stub(schema.commands, 'addColumn');
knexMock.schema.hasTable.withArgs('posts_tags').returns(new Promise.resolve(true));
knexMock.schema.hasColumn.withArgs('posts_tags', 'sort_order').returns(new Promise.resolve(false));
// Execute
updates004[1](logStub).then(function () {
knexMock.schema.hasTable.calledOnce.should.be.true();
knexMock.schema.hasTable.calledWith('posts_tags').should.be.true();
knexMock.schema.hasColumn.calledOnce.should.be.true();
knexMock.schema.hasColumn.calledWith('posts_tags', 'sort_order').should.be.true();
addColumnStub.calledOnce.should.be.true();
addColumnStub.calledWith('posts_tags', 'sort_order').should.be.true();
logStub.calledOnce.should.be.true();
done();
});
});
});
describe('03-add-many-columns-to-clients', function () {
it('does not try to add new columns if the columns already exist', function (done) {
// Setup
var logStub = sandbox.stub(),
addColumnStub = sandbox.stub(schema.commands, 'addColumn');
knexMock.schema.hasTable.withArgs('clients').returns(new Promise.resolve(true));
knexMock.schema.hasColumn.withArgs('clients', 'redirection_uri').returns(new Promise.resolve(true));
knexMock.schema.hasColumn.withArgs('clients', 'logo').returns(new Promise.resolve(true));
knexMock.schema.hasColumn.withArgs('clients', 'status').returns(new Promise.resolve(true));
knexMock.schema.hasColumn.withArgs('clients', 'type').returns(new Promise.resolve(true));
knexMock.schema.hasColumn.withArgs('clients', 'description').returns(new Promise.resolve(true));
// Execute
updates004[2](logStub).then(function () {
knexMock.schema.hasTable.calledOnce.should.be.true();
knexMock.schema.hasTable.calledWith('clients').should.be.true();
knexMock.schema.hasColumn.callCount.should.eql(5);
knexMock.schema.hasColumn.calledWith('clients', 'redirection_uri').should.be.true();
knexMock.schema.hasColumn.calledWith('clients', 'logo').should.be.true();
knexMock.schema.hasColumn.calledWith('clients', 'status').should.be.true();
knexMock.schema.hasColumn.calledWith('clients', 'type').should.be.true();
knexMock.schema.hasColumn.calledWith('clients', 'description').should.be.true();
addColumnStub.called.should.be.false();
logStub.called.should.be.false();
done();
});
});
it('tries to add new columns if table is present but columns are not', function (done) {
// Setup
var logStub = sandbox.stub(),
addColumnStub = sandbox.stub(schema.commands, 'addColumn');
knexMock.schema.hasTable.withArgs('clients').returns(new Promise.resolve(true));
knexMock.schema.hasColumn.withArgs('clients', 'redirection_uri').returns(new Promise.resolve(false));
knexMock.schema.hasColumn.withArgs('clients', 'logo').returns(new Promise.resolve(false));
knexMock.schema.hasColumn.withArgs('clients', 'status').returns(new Promise.resolve(false));
knexMock.schema.hasColumn.withArgs('clients', 'type').returns(new Promise.resolve(false));
knexMock.schema.hasColumn.withArgs('clients', 'description').returns(new Promise.resolve(false));
// Execute
updates004[2](logStub).then(function () {
knexMock.schema.hasTable.calledOnce.should.be.true();
knexMock.schema.hasTable.calledWith('clients').should.be.true();
knexMock.schema.hasColumn.callCount.should.eql(5);
knexMock.schema.hasColumn.calledWith('clients', 'redirection_uri').should.be.true();
knexMock.schema.hasColumn.calledWith('clients', 'logo').should.be.true();
knexMock.schema.hasColumn.calledWith('clients', 'status').should.be.true();
knexMock.schema.hasColumn.calledWith('clients', 'type').should.be.true();
knexMock.schema.hasColumn.calledWith('clients', 'description').should.be.true();
addColumnStub.callCount.should.eql(5);
addColumnStub.calledWith('clients', 'redirection_uri').should.be.true();
addColumnStub.calledWith('clients', 'logo').should.be.true();
addColumnStub.calledWith('clients', 'status').should.be.true();
addColumnStub.calledWith('clients', 'type').should.be.true();
addColumnStub.calledWith('clients', 'description').should.be.true();
logStub.callCount.should.eql(5);
done();
});
});
it('will only try to add columns that do not exist', function (done) {
// Setup
var logStub = sandbox.stub(),
addColumnStub = sandbox.stub(schema.commands, 'addColumn');
knexMock.schema.hasTable.withArgs('clients').returns(new Promise.resolve(true));
knexMock.schema.hasColumn.withArgs('clients', 'redirection_uri').returns(new Promise.resolve(true));
knexMock.schema.hasColumn.withArgs('clients', 'logo').returns(new Promise.resolve(false));
knexMock.schema.hasColumn.withArgs('clients', 'status').returns(new Promise.resolve(true));
knexMock.schema.hasColumn.withArgs('clients', 'type').returns(new Promise.resolve(false));
knexMock.schema.hasColumn.withArgs('clients', 'description').returns(new Promise.resolve(true));
// Execute
updates004[2](logStub).then(function () {
knexMock.schema.hasTable.calledOnce.should.be.true();
knexMock.schema.hasTable.calledWith('clients').should.be.true();
knexMock.schema.hasColumn.callCount.should.eql(5);
knexMock.schema.hasColumn.calledWith('clients', 'redirection_uri').should.be.true();
knexMock.schema.hasColumn.calledWith('clients', 'logo').should.be.true();
knexMock.schema.hasColumn.calledWith('clients', 'status').should.be.true();
knexMock.schema.hasColumn.calledWith('clients', 'type').should.be.true();
knexMock.schema.hasColumn.calledWith('clients', 'description').should.be.true();
addColumnStub.callCount.should.eql(2);
addColumnStub.calledWith('clients', 'logo').should.be.true();
addColumnStub.calledWith('clients', 'type').should.be.true();
logStub.callCount.should.eql(2);
done();
});
});
});
describe('04-add-clienttrusteddomains-table', function () {
it('does not try to add a new table if the table already exists', function (done) {
// Setup
var logStub = sandbox.stub(),
createTableStub = sandbox.stub(schema.commands, 'createTable');
knexMock.schema.hasTable.withArgs('client_trusted_domains').returns(new Promise.resolve(true));
// Execute
updates004[3](logStub).then(function () {
knexMock.schema.hasTable.calledOnce.should.be.true();
knexMock.schema.hasTable.calledWith('client_trusted_domains').should.be.true();
createTableStub.called.should.be.false();
logStub.called.should.be.false();
done();
});
});
it('tries to add a new table if the table does not exist', function (done) {
// Setup
var logStub = sandbox.stub(),
createTableStub = sandbox.stub(schema.commands, 'createTable');
knexMock.schema.hasTable.withArgs('client_trusted_domains').returns(new Promise.resolve(false));
// Execute
updates004[3](logStub).then(function () {
knexMock.schema.hasTable.calledOnce.should.be.true();
knexMock.schema.hasTable.calledWith('client_trusted_domains').should.be.true();
createTableStub.calledOnce.should.be.true();
createTableStub.calledWith('client_trusted_domains').should.be.true();
logStub.calledOnce.should.be.true();
done();
});
});
});
describe('05-drop-unique-on-clients-secret', function () {
it('does not try to drop unique if the table does not exist', function (done) {
// Setup
var logStub = sandbox.stub(),
getIndexesStub = sandbox.stub(schema.commands, 'getIndexes'),
dropUniqueStub = sandbox.stub(schema.commands, 'dropUnique');
getIndexesStub.withArgs('clients').returns(new Promise.resolve(
['clients_slug_unique', 'clients_name_unique', 'clients_secret_unique'])
);
knexMock.schema.hasTable.withArgs('clients').returns(new Promise.resolve(false));
// Execute
updates004[4](logStub).then(function () {
knexMock.schema.hasTable.calledOnce.should.be.true();
knexMock.schema.hasTable.calledWith('clients').should.be.true();
getIndexesStub.called.should.be.false();
dropUniqueStub.called.should.be.false();
logStub.called.should.be.false();
done();
});
});
it('does not try to drop unique if the index does not exist', function (done) {
// Setup
var logStub = sandbox.stub(),
getIndexesStub = sandbox.stub(schema.commands, 'getIndexes'),
dropUniqueStub = sandbox.stub(schema.commands, 'dropUnique');
knexMock.schema.hasTable.withArgs('clients').returns(new Promise.resolve(true));
getIndexesStub.withArgs('clients').returns(new Promise.resolve(
['clients_slug_unique', 'clients_name_unique'])
);
// Execute
updates004[4](logStub).then(function () {
knexMock.schema.hasTable.calledOnce.should.be.true();
knexMock.schema.hasTable.calledWith('clients').should.be.true();
getIndexesStub.calledOnce.should.be.true();
getIndexesStub.calledWith('clients').should.be.true();
dropUniqueStub.called.should.be.false();
logStub.called.should.be.false();
done();
});
});
it('tries to add a drop unique if table and index both exist', function (done) {
// Setup
var logStub = sandbox.stub(),
getIndexesStub = sandbox.stub(schema.commands, 'getIndexes'),
dropUniqueStub = sandbox.stub(schema.commands, 'dropUnique');
knexMock.schema.hasTable.withArgs('clients').returns(new Promise.resolve(true));
getIndexesStub.withArgs('clients').returns(new Promise.resolve(
['clients_slug_unique', 'clients_name_unique', 'clients_secret_unique'])
);
// Execute
updates004[4](logStub).then(function () {
knexMock.schema.hasTable.calledOnce.should.be.true();
knexMock.schema.hasTable.calledWith('clients').should.be.true();
getIndexesStub.calledOnce.should.be.true();
getIndexesStub.calledWith('clients').should.be.true();
dropUniqueStub.calledOnce.should.be.true();
dropUniqueStub.calledWith('clients', 'secret').should.be.true();
logStub.calledOnce.should.be.true();
done();
});
});
});
});
});
});
describe('Update Database Schema', function () {
it('should not do anything if there are no tasks', function (done) {
var updateDatabaseSchema = update.__get__('updateDatabaseSchema'),
getVersionTasksStub = sandbox.stub(schema.versioning, 'getUpdateDatabaseTasks').returns([]),
logStub = sandbox.stub();
updateDatabaseSchema(['001'], logStub).then(function () {
getVersionTasksStub.calledOnce.should.be.true();
logStub.called.should.be.false();
done();
}).catch(done);
});
it('should call the tasks if they are provided', function (done) {
var updateDatabaseSchema = update.__get__('updateDatabaseSchema'),
task1Stub = sandbox.stub().returns(new Promise.resolve()),
task2Stub = sandbox.stub().returns(new Promise.resolve()),
getVersionTasksStub = sandbox.stub(schema.versioning, 'getUpdateDatabaseTasks').returns([task1Stub, task2Stub]),
logStub = sandbox.stub();
updateDatabaseSchema(['001'], logStub).then(function () {
getVersionTasksStub.calledOnce.should.be.true();
task1Stub.calledOnce.should.be.true();
task2Stub.calledOnce.should.be.true();
logStub.calledTwice.should.be.true();
done();
}).catch(done);
});
});
});
describe('Init', function () {
var defaultVersionStub, databaseVersionStub, logStub, errorStub, updateStub, populateStub, fixSecretStub,
resetLog, resetUpdate, resetPopulate, resetFixSecret;
beforeEach(function () {
defaultVersionStub = sandbox.stub(schema.versioning, 'getDefaultDatabaseVersion');
databaseVersionStub = sandbox.stub(schema.versioning, 'getDatabaseVersion');
errorStub = sandbox.stub(errors, 'logErrorAndExit');
updateStub = sandbox.stub();
populateStub = sandbox.stub();
fixSecretStub = sandbox.stub();
logStub = sandbox.stub();
resetLog = migration.__set__('logInfo', logStub);
resetUpdate = migration.__set__('update', updateStub);
resetPopulate = migration.__set__('populate', populateStub);
resetFixSecret = migration.__set__('fixClientSecret', fixSecretStub);
});
afterEach(function () {
resetLog();
resetUpdate();
resetPopulate();
resetFixSecret();
});
it('should do an UPDATE if default version is higher', function (done) {
// Setup
defaultVersionStub.returns('005');
databaseVersionStub.returns(new Promise.resolve('004'));
// Execute
migration.init().then(function () {
defaultVersionStub.calledOnce.should.be.true();
databaseVersionStub.calledOnce.should.be.true();
logStub.calledOnce.should.be.true();
updateStub.calledOnce.should.be.true();
updateStub.calledWith('004', '005', logStub).should.be.true();
errorStub.called.should.be.false();
populateStub.called.should.be.false();
fixSecretStub.called.should.be.false();
done();
}).catch(done);
});
it('should do an UPDATE if default version is significantly higher', function (done) {
// Setup
defaultVersionStub.returns('010');
databaseVersionStub.returns(new Promise.resolve('004'));
// Execute
migration.init().then(function () {
defaultVersionStub.calledOnce.should.be.true();
databaseVersionStub.calledOnce.should.be.true();
logStub.calledOnce.should.be.true();
updateStub.calledOnce.should.be.true();
updateStub.calledWith('004', '010', logStub).should.be.true();
errorStub.called.should.be.false();
populateStub.called.should.be.false();
fixSecretStub.called.should.be.false();
done();
}).catch(done);
});
it('should do FIX SECRET if versions are the same', function (done) {
// Setup
defaultVersionStub.returns('004');
databaseVersionStub.returns(new Promise.resolve('004'));
// Execute
migration.init().then(function () {
defaultVersionStub.calledOnce.should.be.true();
databaseVersionStub.calledOnce.should.be.true();
logStub.calledOnce.should.be.true();
fixSecretStub.called.should.be.true();
errorStub.called.should.be.false();
updateStub.called.should.be.false();
populateStub.called.should.be.false();
done();
}).catch(done);
});
it('should do an UPDATE even if versions are the same, when FORCE_MIGRATION set', function (done) {
// Setup
defaultVersionStub.returns('004');
databaseVersionStub.returns(new Promise.resolve('004'));
process.env.FORCE_MIGRATION = true;
// Execute
migration.init().then(function () {
defaultVersionStub.calledOnce.should.be.true();
databaseVersionStub.calledOnce.should.be.true();
logStub.calledOnce.should.be.true();
updateStub.calledOnce.should.be.true();
updateStub.calledWith('004', '004', logStub).should.be.true();
errorStub.called.should.be.false();
populateStub.called.should.be.false();
fixSecretStub.called.should.be.false();
delete process.env.FORCE_MIGRATION;
done();
}).catch(done);
});
it('should do a POPULATE if settings table does not exist', function (done) {
// Setup
defaultVersionStub.returns('004');
databaseVersionStub.returns(new Promise.reject(new Error('Settings table does not exist')));
// Execute
migration.init().then(function () {
defaultVersionStub.calledOnce.should.be.true();
databaseVersionStub.calledOnce.should.be.true();
logStub.calledOnce.should.be.true();
populateStub.called.should.be.true();
populateStub.calledWith(logStub, false).should.be.true();
errorStub.called.should.be.false();
updateStub.called.should.be.false();
fixSecretStub.called.should.be.false();
done();
}).catch(done);
});
it('should do a POPULATE with TABLES ONLY if settings table does not exist & tablesOnly is set', function (done) {
// Setup
defaultVersionStub.returns('004');
databaseVersionStub.returns(new Promise.reject(new Error('Settings table does not exist')));
// Execute
migration.init(true).then(function () {
defaultVersionStub.calledOnce.should.be.true();
databaseVersionStub.calledOnce.should.be.true();
logStub.calledOnce.should.be.true();
populateStub.called.should.be.true();
populateStub.calledWith(logStub, true).should.be.true();
errorStub.called.should.be.false();
updateStub.called.should.be.false();
fixSecretStub.called.should.be.false();
done();
}).catch(done);
});
it('should throw an error if the database version is higher than the default', function (done) {
// Setup
defaultVersionStub.returns('004');
databaseVersionStub.returns(new Promise.resolve('010'));
// Execute
migration.init().then(function () {
defaultVersionStub.calledOnce.should.be.true();
databaseVersionStub.calledOnce.should.be.true();
logStub.calledOnce.should.be.false();
errorStub.calledOnce.should.be.true();
populateStub.called.should.be.false();
updateStub.called.should.be.false();
fixSecretStub.called.should.be.false();
done();
}).catch(done);
});
it('should throw an error if the database version returns an error other than settings not existing', function (done) {
// Setup
defaultVersionStub.returns('004');
databaseVersionStub.returns(new Promise.reject(new Error('Something went wrong')));
// Execute
migration.init().then(function () {
databaseVersionStub.calledOnce.should.be.true();
logStub.calledOnce.should.be.false();
errorStub.calledOnce.should.be.true();
defaultVersionStub.calledOnce.should.be.false();
populateStub.called.should.be.false();
updateStub.called.should.be.false();
fixSecretStub.called.should.be.false();
done();
}).catch(done);
});
});
describe('LogInfo', function () {
it('should output an info message prefixed with "Migrations"', function () {
var logInfo = migration.__get__('logInfo'),
errorsStub = sandbox.stub(errors, 'logInfo');
logInfo('Stuff');
errorsStub.calledOnce.should.be.true();
errorsStub.calledWith('Migrations', 'Stuff').should.be.true();
});
});
// TODO: move this to 005!!
describe('FixClientSecret', function () {
var fixClientSecret, queryStub, clientForgeStub, clientEditStub, toStringStub, cryptoStub;
beforeEach(function (done) {
fixClientSecret = migration.__get__('fixClientSecret');
queryStub = {
query: sandbox.stub().returnsThis(),
fetch: sandbox.stub()
};
models.init().then(function () {
toStringStub = {toString: sandbox.stub().returns('TEST')};
cryptoStub = sandbox.stub(crypto, 'randomBytes').returns(toStringStub);
clientForgeStub = sandbox.stub(models.Clients, 'forge').returns(queryStub);
clientEditStub = sandbox.stub(models.Client, 'edit');
done();
});
});
it('should do nothing if there are no incorrect secrets', function (done) {
// Setup
queryStub.fetch.returns(new Promise.resolve({models: []}));
// Execute
fixClientSecret().then(function () {
clientForgeStub.calledOnce.should.be.true();
clientEditStub.called.should.be.false();
toStringStub.toString.called.should.be.false();
cryptoStub.called.should.be.false();
done();
}).catch(done);
});
it('should try to fix any incorrect secrets', function (done) {
// Setup
queryStub.fetch.returns(new Promise.resolve({models: [{id: 1}]}));
// Execute
fixClientSecret().then(function () {
clientForgeStub.calledOnce.should.be.true();
clientEditStub.called.should.be.true();
toStringStub.toString.called.should.be.false();
cryptoStub.called.should.be.false();
done();
}).catch(done);
});
it('should try to create a new secret, if the mode is not testing', function (done) {
// Setup
var envTemp = process.env.NODE_ENV;
process.env.NODE_ENV = 'development';
queryStub.fetch.returns(new Promise.resolve({models: [{id: 1}]}));
// Execute
fixClientSecret().then(function () {
clientForgeStub.calledOnce.should.be.true();
clientEditStub.called.should.be.true();
toStringStub.toString.called.should.be.true();
cryptoStub.calledOnce.should.be.true();
// reset
process.env.NODE_ENV = envTemp;
done();
}).catch(done);
});
});
});

View file

@ -200,4 +200,34 @@ describe('Versioning', function () {
).should.be.true();
});
});
describe('getUpdateTasks', function () {
it('`getUpdateFixturesTasks` returns empty array if no tasks are found', function () {
var logStub = sandbox.stub();
versioning.getUpdateFixturesTasks('999', logStub).should.eql([]);
logStub.calledOnce.should.be.true();
});
it('`getUpdateFixturesTasks` returns 8 items for 004', function () {
var logStub = sandbox.stub();
versioning.getUpdateFixturesTasks('004', logStub).should.be.an.Array().with.lengthOf(8);
logStub.calledOnce.should.be.false();
});
it('`getUpdateDatabaseTasks` returns empty array if no tasks are found', function () {
var logStub = sandbox.stub();
versioning.getUpdateDatabaseTasks('999', logStub).should.eql([]);
logStub.calledOnce.should.be.true();
});
it('`getUpdateDatabaseTasks` returns 5 items for 004', function () {
var logStub = sandbox.stub();
versioning.getUpdateDatabaseTasks('004', logStub).should.be.an.Array().with.lengthOf(5);
logStub.calledOnce.should.be.false();
});
});
});