From 0ad93c3df2ed7c6fbc358724bfa8b7b11f702f3d Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Mon, 14 Mar 2016 17:39:00 +0000 Subject: [PATCH] Rewrite DB update to be explicit refs #6301 - Replace builder & automated database upgrade with a set of explicit tasks - Ensure the tasks can only happen if they need to - Remove some duplicate code between fixture & db upgrades (more to do) - Add tests --- .../004/01-add-tour-column-to-users.js | 18 + .../02-add-sortorder-column-to-poststags.js | 18 + .../004/03-add-many-columns-to-clients.js | 21 + .../004/04-add-clienttrusteddomains-table.js | 13 + .../004/05-drop-unique-on-clients-secret.js | 18 + core/server/data/migration/004/index.js | 12 + core/server/data/migration/builder.js | 91 -- core/server/data/migration/fixtures/update.js | 40 +- core/server/data/migration/index.js | 11 +- core/server/data/migration/update.js | 80 +- core/server/data/schema/versioning.js | 38 +- core/test/unit/migration_fixture_spec.js | 515 +++++------ core/test/unit/migration_spec.js | 824 +++++++++++++++++- core/test/unit/versioning_spec.js | 30 + 14 files changed, 1288 insertions(+), 441 deletions(-) create mode 100644 core/server/data/migration/004/01-add-tour-column-to-users.js create mode 100644 core/server/data/migration/004/02-add-sortorder-column-to-poststags.js create mode 100644 core/server/data/migration/004/03-add-many-columns-to-clients.js create mode 100644 core/server/data/migration/004/04-add-clienttrusteddomains-table.js create mode 100644 core/server/data/migration/004/05-drop-unique-on-clients-secret.js create mode 100644 core/server/data/migration/004/index.js delete mode 100644 core/server/data/migration/builder.js diff --git a/core/server/data/migration/004/01-add-tour-column-to-users.js b/core/server/data/migration/004/01-add-tour-column-to-users.js new file mode 100644 index 0000000000..6312cf4b3b --- /dev/null +++ b/core/server/data/migration/004/01-add-tour-column-to-users.js @@ -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); + } + }); + } + }); +}; diff --git a/core/server/data/migration/004/02-add-sortorder-column-to-poststags.js b/core/server/data/migration/004/02-add-sortorder-column-to-poststags.js new file mode 100644 index 0000000000..5ac02ae875 --- /dev/null +++ b/core/server/data/migration/004/02-add-sortorder-column-to-poststags.js @@ -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); + } + }); + } + }); +}; diff --git a/core/server/data/migration/004/03-add-many-columns-to-clients.js b/core/server/data/migration/004/03-add-many-columns-to-clients.js new file mode 100644 index 0000000000..d990bcc880 --- /dev/null +++ b/core/server/data/migration/004/03-add-many-columns-to-clients.js @@ -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); + } + }); + }); + } + }); +}; diff --git a/core/server/data/migration/004/04-add-clienttrusteddomains-table.js b/core/server/data/migration/004/04-add-clienttrusteddomains-table.js new file mode 100644 index 0000000000..a43a542361 --- /dev/null +++ b/core/server/data/migration/004/04-add-clienttrusteddomains-table.js @@ -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); + } + }); +}; diff --git a/core/server/data/migration/004/05-drop-unique-on-clients-secret.js b/core/server/data/migration/004/05-drop-unique-on-clients-secret.js new file mode 100644 index 0000000000..f84209e38c --- /dev/null +++ b/core/server/data/migration/004/05-drop-unique-on-clients-secret.js @@ -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); + } + }); + } + }); +}; diff --git a/core/server/data/migration/004/index.js b/core/server/data/migration/004/index.js new file mode 100644 index 0000000000..72970d5801 --- /dev/null +++ b/core/server/data/migration/004/index.js @@ -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') +]; diff --git a/core/server/data/migration/builder.js b/core/server/data/migration/builder.js deleted file mode 100644 index c100822bb3..0000000000 --- a/core/server/data/migration/builder.js +++ /dev/null @@ -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 -}; diff --git a/core/server/data/migration/fixtures/update.js b/core/server/data/migration/fixtures/update.js index 20850d690b..5a5ebb2313 100644 --- a/core/server/data/migration/fixtures/update.js +++ b/core/server/data/migration/fixtures/update.js @@ -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 () { - logInfo('Updating fixtures to', version); - return sequence(require('./' + version), modelOptions, logInfo); + ops.push(function runVersionTasks() { + logInfo('Updating fixtures to ', version); + return sequence(tasks, modelOptions, logInfo); }); } - }); + + return ops; + }, []); return sequence(ops, modelOptions, logInfo); }; diff --git a/core/server/data/migration/index.js b/core/server/data/migration/index.js index bce7341cb7..fe57190d9a 100644 --- a/core/server/data/migration/index.js +++ b/core/server/data/migration/index.js @@ -44,22 +44,21 @@ init = function (tablesOnly) { return versioning.getDatabaseVersion().then(function (databaseVersion) { var defaultVersion = versioning.getDefaultDatabaseVersion(); + // Update goes first, to allow for FORCE_MIGRATION + // 2. The database exists but is out of date if (databaseVersion < defaultVersion || process.env.FORCE_MIGRATION) { - // 2. The database exists but is out of date // 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); }); }; diff --git a/core/server/data/migration/update.js b/core/server/data/migration/update.js index 9aa9dea64f..e7ab9056d5 100644 --- a/core/server/data/migration/update.js +++ b/core/server/data/migration/update.js @@ -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)); - // execute the commands in sequence - if (!_.isEmpty(migrateOps)) { - logInfo('Running migrations'); + return migrateOps; + }, []); - return sequence(migrateOps); - } - }); + // execute the commands in sequence + if (!_.isEmpty(migrateOps)) { + logInfo('Running migrations'); + } + + 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(); }); }; diff --git a/core/server/data/schema/versioning.js b/core/server/data/schema/versioning.js index d7283fa04d..0360e1edc3 100644 --- a/core/server/data/schema/versioning.js +++ b/core/server/data/schema/versioning.js @@ -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 }; diff --git a/core/test/unit/migration_fixture_spec.js b/core/test/unit/migration_fixture_spec.js index 91f13b49ce..21608f752c 100644 --- a/core/test/unit/migration_fixture_spec.js +++ b/core/test/unit/migration_fixture_spec.js @@ -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,275 +106,277 @@ describe('Fixtures', function () { }).catch(done); }); - describe('01-move-jquery-with-alert', function () { - it('tries to move jQuery to ghost_foot', function (done) { - var logStub = sandbox.stub(), - settingsOneStub = sandbox.stub(models.Settings, 'findOne').returns(Promise.resolve({ - attributes: {value: ''} - })), - settingsEditStub = sandbox.stub(models.Settings, 'edit').returns(Promise.resolve()); + describe('Tasks:', function () { + describe('01-move-jquery-with-alert', function () { + it('tries to move jQuery to ghost_foot', function (done) { + var logStub = sandbox.stub(), + settingsOneStub = sandbox.stub(models.Settings, 'findOne').returns(Promise.resolve({ + attributes: {value: ''} + })), + settingsEditStub = sandbox.stub(models.Settings, 'edit').returns(Promise.resolve()); - fixtures004[0]({}, logStub).then(function () { - settingsOneStub.calledOnce.should.be.true(); - settingsOneStub.calledWith('ghost_foot').should.be.true(); - settingsEditStub.calledOnce.should.be.true(); - logStub.calledOnce.should.be.true(); + fixtures004[0]({}, logStub).then(function () { + settingsOneStub.calledOnce.should.be.true(); + settingsOneStub.calledWith('ghost_foot').should.be.true(); + settingsEditStub.calledOnce.should.be.true(); + logStub.calledOnce.should.be.true(); - done(); + done(); + }); + }); + + it('does not move jQuery to ghost_foot if it is already there', function (done) { + var logStub = sandbox.stub(), + settingsOneStub = sandbox.stub(models.Settings, 'findOne').returns(Promise.resolve({ + attributes: { + value: '\n' + + '\n\n' + } + })), + settingsEditStub = sandbox.stub(models.Settings, 'edit').returns(Promise.resolve()); + + fixtures004[0]({}, logStub).then(function () { + settingsOneStub.calledOnce.should.be.true(); + settingsOneStub.calledWith('ghost_foot').should.be.true(); + settingsEditStub.calledOnce.should.be.false(); + logStub.called.should.be.false(); + + done(); + }).catch(done); + }); + + it('tried to move jQuery AND add a privacy message if any privacy settings are on', function (done) { + configUtils.set({privacy: {useGoogleFonts: false}}); + var logStub = sandbox.stub(), + settingsOneStub = sandbox.stub(models.Settings, 'findOne').returns(Promise.resolve({ + attributes: {value: ''} + })), + settingsEditStub = sandbox.stub(models.Settings, 'edit').returns(Promise.resolve()), + notificationsAddStub = sandbox.stub(notifications, 'add').returns(Promise.resolve()); + + fixtures004[0]({}, logStub).then(function () { + settingsOneStub.calledOnce.should.be.true(); + settingsOneStub.calledWith('ghost_foot').should.be.true(); + settingsEditStub.calledOnce.should.be.true(); + notificationsAddStub.calledOnce.should.be.true(); + logStub.calledTwice.should.be.true(); + + done(); + }).catch(done); }); }); - it('does not move jQuery to ghost_foot if it is already there', function (done) { - var logStub = sandbox.stub(), - settingsOneStub = sandbox.stub(models.Settings, 'findOne').returns(Promise.resolve({ - attributes: { - value: '\n' - + '\n\n' - } - })), - settingsEditStub = sandbox.stub(models.Settings, 'edit').returns(Promise.resolve()); + describe('02-update-private-setting-type', function () { + it('tries to update setting type correctly', function (done) { + var logStub = sandbox.stub(), + settingsOneStub = sandbox.stub(models.Settings, 'findOne').returns(Promise.resolve({})), + settingsEditStub = sandbox.stub(models.Settings, 'edit').returns(Promise.resolve()); - fixtures004[0]({}, logStub).then(function () { - settingsOneStub.calledOnce.should.be.true(); - settingsOneStub.calledWith('ghost_foot').should.be.true(); - settingsEditStub.calledOnce.should.be.false(); - logStub.called.should.be.false(); + fixtures004[1]({}, logStub).then(function () { + settingsOneStub.calledOnce.should.be.true(); + settingsOneStub.calledWith('isPrivate').should.be.true(); + settingsEditStub.calledOnce.should.be.true(); + settingsEditStub.calledWith({key: 'isPrivate', type: 'private'}).should.be.true(); + logStub.calledOnce.should.be.true(); + sinon.assert.callOrder(settingsOneStub, logStub, settingsEditStub); - done(); - }).catch(done); + done(); + }).catch(done); + }); }); - it('tried to move jQuery AND add a privacy message if any privacy settings are on', function (done) { - configUtils.set({privacy: {useGoogleFonts: false}}); - var logStub = sandbox.stub(), - settingsOneStub = sandbox.stub(models.Settings, 'findOne').returns(Promise.resolve({ - attributes: {value: ''} - })), - settingsEditStub = sandbox.stub(models.Settings, 'edit').returns(Promise.resolve()), - notificationsAddStub = sandbox.stub(notifications, 'add').returns(Promise.resolve()); + describe('03-update-password-setting-type', function () { + it('tries to update setting type correctly', function (done) { + var logStub = sandbox.stub(), + settingsOneStub = sandbox.stub(models.Settings, 'findOne').returns(Promise.resolve({})), + settingsEditStub = sandbox.stub(models.Settings, 'edit').returns(Promise.resolve()); - fixtures004[0]({}, logStub).then(function () { - settingsOneStub.calledOnce.should.be.true(); - settingsOneStub.calledWith('ghost_foot').should.be.true(); - settingsEditStub.calledOnce.should.be.true(); - notificationsAddStub.calledOnce.should.be.true(); - logStub.calledTwice.should.be.true(); + fixtures004[2]({}, logStub).then(function () { + settingsOneStub.calledOnce.should.be.true(); + settingsOneStub.calledWith('password').should.be.true(); + settingsEditStub.calledOnce.should.be.true(); + settingsEditStub.calledWith({key: 'password', type: 'private'}).should.be.true(); + logStub.calledOnce.should.be.true(); + sinon.assert.callOrder(settingsOneStub, logStub, settingsEditStub); - done(); - }).catch(done); - }); - }); - - describe('02-update-private-setting-type', function () { - it('tries to update setting type correctly', function (done) { - var logStub = sandbox.stub(), - settingsOneStub = sandbox.stub(models.Settings, 'findOne').returns(Promise.resolve({})), - settingsEditStub = sandbox.stub(models.Settings, 'edit').returns(Promise.resolve()); - - fixtures004[1]({}, logStub).then(function () { - settingsOneStub.calledOnce.should.be.true(); - settingsOneStub.calledWith('isPrivate').should.be.true(); - settingsEditStub.calledOnce.should.be.true(); - settingsEditStub.calledWith({key: 'isPrivate', type: 'private'}).should.be.true(); - logStub.calledOnce.should.be.true(); - sinon.assert.callOrder(settingsOneStub, logStub, settingsEditStub); - - done(); - }).catch(done); - }); - }); - - describe('03-update-password-setting-type', function () { - it('tries to update setting type correctly', function (done) { - var logStub = sandbox.stub(), - settingsOneStub = sandbox.stub(models.Settings, 'findOne').returns(Promise.resolve({})), - settingsEditStub = sandbox.stub(models.Settings, 'edit').returns(Promise.resolve()); - - fixtures004[2]({}, logStub).then(function () { - settingsOneStub.calledOnce.should.be.true(); - settingsOneStub.calledWith('password').should.be.true(); - settingsEditStub.calledOnce.should.be.true(); - settingsEditStub.calledWith({key: 'password', type: 'private'}).should.be.true(); - logStub.calledOnce.should.be.true(); - sinon.assert.callOrder(settingsOneStub, logStub, settingsEditStub); - - done(); - }).catch(done); - }); - }); - - describe('04-update-ghost-admin-client', function () { - it('tries to update client correctly', function (done) { - var logStub = sandbox.stub(), - clientOneStub = sandbox.stub(models.Client, 'findOne').returns(Promise.resolve({})), - clientEditStub = sandbox.stub(models.Client, 'edit').returns(Promise.resolve()); - - fixtures004[3]({}, logStub).then(function () { - clientOneStub.calledOnce.should.be.true(); - clientOneStub.calledWith({slug: 'ghost-admin'}).should.be.true(); - clientEditStub.calledOnce.should.be.true(); - logStub.calledOnce.should.be.true(); - sinon.assert.callOrder(clientOneStub, logStub, clientEditStub); - - done(); - }).catch(done); - }); - }); - - describe('05-add-ghost-frontend-client', function () { - it('tries to add client correctly', function (done) { - var logStub = sandbox.stub(), - clientOneStub = sandbox.stub(models.Client, 'findOne').returns(Promise.resolve()), - clientAddStub = sandbox.stub(models.Client, 'add').returns(Promise.resolve()); - - fixtures004[4]({}, logStub).then(function () { - clientOneStub.calledOnce.should.be.true(); - clientOneStub.calledWith({slug: 'ghost-frontend'}).should.be.true(); - clientAddStub.calledOnce.should.be.true(); - logStub.calledOnce.should.be.true(); - sinon.assert.callOrder(clientOneStub, logStub, clientAddStub); - - done(); - }).catch(done); - }); - }); - - describe('06-clean-broken-tags', function () { - it('tries to clean broken tags correctly', function (done) { - var logStub = sandbox.stub(), - tagObjStub = { - get: sandbox.stub().returns(',hello'), - save: sandbox.stub().returns(Promise.resolve) - }, - tagCollStub = {each: sandbox.stub().callsArgWith(0, tagObjStub)}, - tagAllStub = sandbox.stub(models.Tag, 'findAll').returns(Promise.resolve(tagCollStub)); - - fixtures004[5]({}, logStub).then(function () { - tagAllStub.calledOnce.should.be.true(); - tagCollStub.each.calledOnce.should.be.true(); - tagObjStub.get.calledOnce.should.be.true(); - tagObjStub.get.calledWith('name').should.be.true(); - tagObjStub.save.calledOnce.should.be.true(); - tagObjStub.save.calledWith({name: 'hello'}).should.be.true(); - logStub.calledOnce.should.be.true(); - sinon.assert.callOrder(tagAllStub, tagCollStub.each, tagObjStub.get, tagObjStub.save, logStub); - - done(); - }).catch(done); + done(); + }).catch(done); + }); }); - it('tries can handle tags which end up empty', function (done) { - var logStub = sandbox.stub(), - tagObjStub = { - get: sandbox.stub().returns(','), - save: sandbox.stub().returns(Promise.resolve) - }, - tagCollStub = {each: sandbox.stub().callsArgWith(0, tagObjStub)}, - tagAllStub = sandbox.stub(models.Tag, 'findAll').returns(Promise.resolve(tagCollStub)); + describe('04-update-ghost-admin-client', function () { + it('tries to update client correctly', function (done) { + var logStub = sandbox.stub(), + clientOneStub = sandbox.stub(models.Client, 'findOne').returns(Promise.resolve({})), + clientEditStub = sandbox.stub(models.Client, 'edit').returns(Promise.resolve()); - fixtures004[5]({}, logStub).then(function () { - tagAllStub.calledOnce.should.be.true(); - tagCollStub.each.calledOnce.should.be.true(); - tagObjStub.get.calledOnce.should.be.true(); - tagObjStub.get.calledWith('name').should.be.true(); - tagObjStub.save.calledOnce.should.be.true(); - tagObjStub.save.calledWith({name: 'tag'}).should.be.true(); - logStub.calledOnce.should.be.true(); - sinon.assert.callOrder(tagAllStub, tagCollStub.each, tagObjStub.get, tagObjStub.save, logStub); + fixtures004[3]({}, logStub).then(function () { + clientOneStub.calledOnce.should.be.true(); + clientOneStub.calledWith({slug: 'ghost-admin'}).should.be.true(); + clientEditStub.calledOnce.should.be.true(); + logStub.calledOnce.should.be.true(); + sinon.assert.callOrder(clientOneStub, logStub, clientEditStub); - done(); - }).catch(done); + done(); + }).catch(done); + }); }); - it('tries only changes a tag if necessary', function (done) { - var logStub = sandbox.stub(), - tagObjStub = { - get: sandbox.stub().returns('hello'), - save: sandbox.stub().returns(Promise.resolve) - }, - tagCollStub = {each: sandbox.stub().callsArgWith(0, tagObjStub)}, - tagAllStub = sandbox.stub(models.Tag, 'findAll').returns(Promise.resolve(tagCollStub)); + describe('05-add-ghost-frontend-client', function () { + it('tries to add client correctly', function (done) { + var logStub = sandbox.stub(), + clientOneStub = sandbox.stub(models.Client, 'findOne').returns(Promise.resolve()), + clientAddStub = sandbox.stub(models.Client, 'add').returns(Promise.resolve()); - fixtures004[5]({}, logStub).then(function () { - tagAllStub.calledOnce.should.be.true(); - tagCollStub.each.calledOnce.should.be.true(); - tagObjStub.get.calledOnce.should.be.true(); - tagObjStub.get.calledWith('name').should.be.true(); - tagObjStub.save.called.should.be.false(); - logStub.calledOnce.should.be.false(); - sinon.assert.callOrder(tagAllStub, tagCollStub.each, tagObjStub.get); + fixtures004[4]({}, logStub).then(function () { + clientOneStub.calledOnce.should.be.true(); + clientOneStub.calledWith({slug: 'ghost-frontend'}).should.be.true(); + clientAddStub.calledOnce.should.be.true(); + logStub.calledOnce.should.be.true(); + sinon.assert.callOrder(clientOneStub, logStub, clientAddStub); - done(); - }).catch(done); - }); - }); - - describe('07-add-post-tag-order', function () { - it('calls load on each post', function (done) { - var logStub = sandbox.stub(), - postObjStub = { - load: sandbox.stub().returnsThis() - }, - postCollStub = {mapThen: sandbox.stub().callsArgWith(0, postObjStub)}, - postAllStub = sandbox.stub(models.Post, 'findAll').returns(Promise.resolve(postCollStub)); - - fixtures004[6]({}, logStub).then(function () { - postAllStub.calledOnce.should.be.true(); - postCollStub.mapThen.calledOnce.should.be.true(); - postObjStub.load.calledOnce.should.be.true(); - postObjStub.load.calledWith(['tags']).should.be.true(); - logStub.calledOnce.should.be.true(); - sinon.assert.callOrder(logStub, postAllStub, postCollStub.mapThen, postObjStub.load); - - done(); - }).catch(done); + done(); + }).catch(done); + }); }); - it('tries to add order to posts_tags', function (done) { - var logStub = sandbox.stub(), - postObjStub = { - load: sandbox.stub().returnsThis(), - related: sandbox.stub().returnsThis(), - tags: sandbox.stub().returnsThis(), - each: sandbox.stub().callsArgWith(0, {id: 5}), - updatePivot: sandbox.stub().returns(Promise.resolve()) - }, - postCollStub = {mapThen: sandbox.stub().returns([postObjStub])}, - postAllStub = sandbox.stub(models.Post, 'findAll').returns(Promise.resolve(postCollStub)); + describe('06-clean-broken-tags', function () { + it('tries to clean broken tags correctly', function (done) { + var logStub = sandbox.stub(), + tagObjStub = { + get: sandbox.stub().returns(',hello'), + save: sandbox.stub().returns(Promise.resolve) + }, + tagCollStub = {each: sandbox.stub().callsArgWith(0, tagObjStub)}, + tagAllStub = sandbox.stub(models.Tag, 'findAll').returns(Promise.resolve(tagCollStub)); - fixtures004[6]({}, logStub).then(function () { - postAllStub.calledOnce.should.be.true(); - postCollStub.mapThen.calledOnce.should.be.true(); - postObjStub.load.called.should.be.false(); - postObjStub.related.calledOnce.should.be.true(); - postObjStub.each.calledOnce.should.be.true(); - postObjStub.tags.calledOnce.should.be.true(); - postObjStub.updatePivot.calledOnce.should.be.true(); - logStub.calledThrice.should.be.true(); - sinon.assert.callOrder( - logStub, postAllStub, postCollStub.mapThen, postObjStub.related, postObjStub.each, - logStub, postObjStub.tags, postObjStub.updatePivot, logStub - ); + fixtures004[5]({}, logStub).then(function () { + tagAllStub.calledOnce.should.be.true(); + tagCollStub.each.calledOnce.should.be.true(); + tagObjStub.get.calledOnce.should.be.true(); + tagObjStub.get.calledWith('name').should.be.true(); + tagObjStub.save.calledOnce.should.be.true(); + tagObjStub.save.calledWith({name: 'hello'}).should.be.true(); + logStub.calledOnce.should.be.true(); + sinon.assert.callOrder(tagAllStub, tagCollStub.each, tagObjStub.get, tagObjStub.save, logStub); - done(); - }).catch(done); + done(); + }).catch(done); + }); + + it('tries can handle tags which end up empty', function (done) { + var logStub = sandbox.stub(), + tagObjStub = { + get: sandbox.stub().returns(','), + save: sandbox.stub().returns(Promise.resolve) + }, + tagCollStub = {each: sandbox.stub().callsArgWith(0, tagObjStub)}, + tagAllStub = sandbox.stub(models.Tag, 'findAll').returns(Promise.resolve(tagCollStub)); + + fixtures004[5]({}, logStub).then(function () { + tagAllStub.calledOnce.should.be.true(); + tagCollStub.each.calledOnce.should.be.true(); + tagObjStub.get.calledOnce.should.be.true(); + tagObjStub.get.calledWith('name').should.be.true(); + tagObjStub.save.calledOnce.should.be.true(); + tagObjStub.save.calledWith({name: 'tag'}).should.be.true(); + logStub.calledOnce.should.be.true(); + sinon.assert.callOrder(tagAllStub, tagCollStub.each, tagObjStub.get, tagObjStub.save, logStub); + + done(); + }).catch(done); + }); + + it('tries only changes a tag if necessary', function (done) { + var logStub = sandbox.stub(), + tagObjStub = { + get: sandbox.stub().returns('hello'), + save: sandbox.stub().returns(Promise.resolve) + }, + tagCollStub = {each: sandbox.stub().callsArgWith(0, tagObjStub)}, + tagAllStub = sandbox.stub(models.Tag, 'findAll').returns(Promise.resolve(tagCollStub)); + + fixtures004[5]({}, logStub).then(function () { + tagAllStub.calledOnce.should.be.true(); + tagCollStub.each.calledOnce.should.be.true(); + tagObjStub.get.calledOnce.should.be.true(); + tagObjStub.get.calledWith('name').should.be.true(); + tagObjStub.save.called.should.be.false(); + logStub.calledOnce.should.be.false(); + sinon.assert.callOrder(tagAllStub, tagCollStub.each, tagObjStub.get); + + done(); + }).catch(done); + }); }); - }); - describe('08-add-post-fixture', function () { - it('tries to add a new post fixture correctly', function (done) { - var logStub = sandbox.stub(), - postOneStub = sandbox.stub(models.Post, 'findOne').returns(Promise.resolve()), - postAddStub = sandbox.stub(models.Post, 'add').returns(Promise.resolve()); + describe('07-add-post-tag-order', function () { + it('calls load on each post', function (done) { + var logStub = sandbox.stub(), + postObjStub = { + load: sandbox.stub().returnsThis() + }, + postCollStub = {mapThen: sandbox.stub().callsArgWith(0, postObjStub)}, + postAllStub = sandbox.stub(models.Post, 'findAll').returns(Promise.resolve(postCollStub)); - fixtures004[7]({}, logStub).then(function () { - postOneStub.calledOnce.should.be.true(); - logStub.calledOnce.should.be.true(); - postAddStub.calledOnce.should.be.true(); - sinon.assert.callOrder(postOneStub, logStub, postAddStub); + fixtures004[6]({}, logStub).then(function () { + postAllStub.calledOnce.should.be.true(); + postCollStub.mapThen.calledOnce.should.be.true(); + postObjStub.load.calledOnce.should.be.true(); + postObjStub.load.calledWith(['tags']).should.be.true(); + logStub.calledOnce.should.be.true(); + sinon.assert.callOrder(logStub, postAllStub, postCollStub.mapThen, postObjStub.load); - done(); - }).catch(done); + done(); + }).catch(done); + }); + + it('tries to add order to posts_tags', function (done) { + var logStub = sandbox.stub(), + postObjStub = { + load: sandbox.stub().returnsThis(), + related: sandbox.stub().returnsThis(), + tags: sandbox.stub().returnsThis(), + each: sandbox.stub().callsArgWith(0, {id: 5}), + updatePivot: sandbox.stub().returns(Promise.resolve()) + }, + postCollStub = {mapThen: sandbox.stub().returns([postObjStub])}, + postAllStub = sandbox.stub(models.Post, 'findAll').returns(Promise.resolve(postCollStub)); + + fixtures004[6]({}, logStub).then(function () { + postAllStub.calledOnce.should.be.true(); + postCollStub.mapThen.calledOnce.should.be.true(); + postObjStub.load.called.should.be.false(); + postObjStub.related.calledOnce.should.be.true(); + postObjStub.each.calledOnce.should.be.true(); + postObjStub.tags.calledOnce.should.be.true(); + postObjStub.updatePivot.calledOnce.should.be.true(); + logStub.calledThrice.should.be.true(); + sinon.assert.callOrder( + logStub, postAllStub, postCollStub.mapThen, postObjStub.related, postObjStub.each, + logStub, postObjStub.tags, postObjStub.updatePivot, logStub + ); + + done(); + }).catch(done); + }); + }); + + describe('08-add-post-fixture', function () { + it('tries to add a new post fixture correctly', function (done) { + var logStub = sandbox.stub(), + postOneStub = sandbox.stub(models.Post, 'findOne').returns(Promise.resolve()), + postAddStub = sandbox.stub(models.Post, 'add').returns(Promise.resolve()); + + fixtures004[7]({}, logStub).then(function () { + postOneStub.calledOnce.should.be.true(); + logStub.calledOnce.should.be.true(); + postAddStub.calledOnce.should.be.true(); + sinon.assert.callOrder(postOneStub, logStub, postAddStub); + + done(); + }).catch(done); + }); }); }); }); diff --git a/core/test/unit/migration_spec.js b/core/test/unit/migration_spec.js index 223a965639..46930d91d6 100644 --- a/core/test/unit/migration_spec.js +++ b/core/test/unit/migration_spec.js @@ -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); + }); }); }); diff --git a/core/test/unit/versioning_spec.js b/core/test/unit/versioning_spec.js index f97d2aeb8f..dd092fa552 100644 --- a/core/test/unit/versioning_spec.js +++ b/core/test/unit/versioning_spec.js @@ -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(); + }); + }); });