diff --git a/core/server/data/migration/fixtures/004/02-update-private-setting-type.js b/core/server/data/migration/fixtures/004/02-update-private-setting-type.js index 5be667b103..25f5776c6d 100644 --- a/core/server/data/migration/fixtures/004/02-update-private-setting-type.js +++ b/core/server/data/migration/fixtures/004/02-update-private-setting-type.js @@ -4,7 +4,7 @@ var models = require('../../../../models'), module.exports = function updatePrivateSetting(options, logInfo) { return models.Settings.findOne('isPrivate').then(function (setting) { - if (setting) { + if (setting && setting.get('type') !== 'private') { logInfo('Update isPrivate setting'); return models.Settings.edit({key: 'isPrivate', type: 'private'}, options); } diff --git a/core/server/data/migration/fixtures/004/03-update-password-setting-type.js b/core/server/data/migration/fixtures/004/03-update-password-setting-type.js index be947dae7c..9da16212e0 100644 --- a/core/server/data/migration/fixtures/004/03-update-password-setting-type.js +++ b/core/server/data/migration/fixtures/004/03-update-password-setting-type.js @@ -4,7 +4,7 @@ var models = require('../../../../models'), module.exports = function updatePasswordSetting(options, logInfo) { return models.Settings.findOne('password').then(function (setting) { - if (setting) { + if (setting && setting.get('type') !== 'private') { logInfo('Update password setting'); return models.Settings.edit({key: 'password', type: 'private'}, options); } diff --git a/core/server/data/migration/fixtures/004/04-update-ghost-admin-client.js b/core/server/data/migration/fixtures/004/04-update-ghost-admin-client.js index 7d18133446..469bbafe39 100644 --- a/core/server/data/migration/fixtures/004/04-update-ghost-admin-client.js +++ b/core/server/data/migration/fixtures/004/04-update-ghost-admin-client.js @@ -9,7 +9,7 @@ var models = require('../../../../models'), module.exports = function updateGhostAdminClient(options, logInfo) { // ghost-admin should already exist from 003 version return models.Client.findOne({slug: adminClient.slug}).then(function (client) { - if (client) { + if (client && (client.get('secret') === 'not_available' || client.get('status') !== 'enabled')) { logInfo('Update ghost-admin client fixture'); return models.Client.edit( _.extend({}, adminClient, {secret: crypto.randomBytes(6).toString('hex')}), diff --git a/core/server/data/migration/fixtures/004/07-add-post-tag-order.js b/core/server/data/migration/fixtures/004/07-add-post-tag-order.js index b2cf057559..895521aaa9 100644 --- a/core/server/data/migration/fixtures/004/07-add-post-tag-order.js +++ b/core/server/data/migration/fixtures/004/07-add-post-tag-order.js @@ -1,39 +1,62 @@ // Add a new order value to posts_tags based on the existing info var models = require('../../../../models'), _ = require('lodash'), - sequence = require('../../../../utils/sequence'); + sequence = require('../../../../utils/sequence'), + migrationHasRunFlag, + modelOptions; + +function loadTagsForEachPost(posts) { + if (!posts) { + return []; + } + return posts.mapThen(function loadTagsForPost(post) { + return post.load(['tags']); + }); +} + +function updatePostTagsSortOrder(post, tagId, order) { + var sortOrder = order; + return function doUpdatePivot() { + return post.tags().updatePivot( + {sort_order: sortOrder}, _.extend({}, modelOptions, {query: {where: {tag_id: tagId}}}) + ); + }; +} + +function buildTagOpsArray(tagOps, post) { + var order = 0; + + return post.related('tags').reduce(function processTag(tagOps, tag) { + if (tag.pivot.get('sort_order') > 0) { + // if any entry in the posts_tags table has already run, we shouldn't run this again + migrationHasRunFlag = true; + } + + tagOps.push(updatePostTagsSortOrder(post, tag.id, order)); + order += 1; + + return tagOps; + }, tagOps); +} + +function processPostsArray(postsArray) { + return postsArray.reduce(buildTagOpsArray, []); +} module.exports = function addPostTagOrder(options, logInfo) { - var tagOps = []; - logInfo('Collecting data on tag order for posts...'); - return models.Post.findAll(_.extend({}, options)).then(function (posts) { - if (posts) { - return posts.mapThen(function (post) { - return post.load(['tags']); - }); - } - return []; - }).then(function (posts) { - _.each(posts, function (post) { - var order = 0; - post.related('tags').each(function (tag) { - tagOps.push((function (order) { - var sortOrder = order; - return function () { - return post.tags().updatePivot( - {sort_order: sortOrder}, _.extend({}, options, {query: {where: {tag_id: tag.id}}}) - ); - }; - }(order))); - order += 1; - }); - }); + modelOptions = options; + migrationHasRunFlag = false; - if (tagOps.length > 0) { - logInfo('Updating order on ' + tagOps.length + ' tag relationships (could take a while)...'); - return sequence(tagOps).then(function () { - logInfo('Tag order successfully updated'); - }); - } - }); + logInfo('Collecting data on tag order for posts...'); + return models.Post.findAll(_.extend({}, modelOptions)) + .then(loadTagsForEachPost) + .then(processPostsArray) + .then(function (tagOps) { + if (tagOps.length > 0 && !migrationHasRunFlag) { + logInfo('Updating order on ' + tagOps.length + ' tag relationships (could take a while)...'); + return sequence(tagOps).then(function () { + logInfo('Tag order successfully updated'); + }); + } + }); }; diff --git a/core/test/unit/migration_fixture_spec.js b/core/test/unit/migration_fixture_spec.js index 21608f752c..e02427019b 100644 --- a/core/test/unit/migration_fixture_spec.js +++ b/core/test/unit/migration_fixture_spec.js @@ -169,16 +169,44 @@ describe('Fixtures', function () { 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({})), + settingObjStub = {get: sandbox.stub()}, + settingsOneStub = sandbox.stub(models.Settings, 'findOne').returns( + Promise.resolve(settingObjStub) + ), 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(); + settingObjStub.get.calledOnce.should.be.true(); + settingObjStub.get.calledWith('type').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); + sinon.assert.callOrder(settingsOneStub, settingObjStub.get, logStub, settingsEditStub); + + done(); + }).catch(done); + }); + + it('does not try to update setting type if it is already set', function (done) { + var logStub = sandbox.stub(), + settingObjStub = {get: sandbox.stub().returns('private')}, + settingsOneStub = sandbox.stub(models.Settings, 'findOne').returns( + Promise.resolve(settingObjStub) + ), + 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(); + settingObjStub.get.calledOnce.should.be.true(); + settingObjStub.get.calledWith('type').should.be.true(); + + settingsEditStub.called.should.be.false(); + logStub.calledOnce.should.be.false(); + + sinon.assert.callOrder(settingsOneStub, settingObjStub.get); done(); }).catch(done); @@ -188,7 +216,10 @@ describe('Fixtures', function () { 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({})), + settingObjStub = {get: sandbox.stub()}, + settingsOneStub = sandbox.stub(models.Settings, 'findOne').returns( + Promise.resolve(settingObjStub) + ), settingsEditStub = sandbox.stub(models.Settings, 'edit').returns(Promise.resolve()); fixtures004[2]({}, logStub).then(function () { @@ -202,20 +233,123 @@ describe('Fixtures', function () { done(); }).catch(done); }); + + it('does not try to update setting type if it is already set', function (done) { + var logStub = sandbox.stub(), + settingObjStub = {get: sandbox.stub().returns('private')}, + settingsOneStub = sandbox.stub(models.Settings, 'findOne').returns( + Promise.resolve(settingObjStub) + ), + 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(); + settingObjStub.get.calledOnce.should.be.true(); + settingObjStub.get.calledWith('type').should.be.true(); + + settingsEditStub.called.should.be.false(); + logStub.calledOnce.should.be.false(); + + sinon.assert.callOrder(settingsOneStub, settingObjStub.get); + + 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({})), + clientObjStub = {get: sandbox.stub()}, + clientOneStub = sandbox.stub(models.Client, 'findOne').returns(Promise.resolve(clientObjStub)), 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(); + clientObjStub.get.calledTwice.should.be.true(); + clientObjStub.get.calledWith('secret').should.be.true(); + clientObjStub.get.calledWith('status').should.be.true(); clientEditStub.calledOnce.should.be.true(); logStub.calledOnce.should.be.true(); - sinon.assert.callOrder(clientOneStub, logStub, clientEditStub); + sinon.assert.callOrder( + clientOneStub, clientObjStub.get, clientObjStub.get, logStub, clientEditStub + ); + + done(); + }).catch(done); + }); + + it('does not try to update client if the secret and status are already correct', function (done) { + var logStub = sandbox.stub(), + clientObjStub = {get: sandbox.stub()}, + clientOneStub = sandbox.stub(models.Client, 'findOne').returns(Promise.resolve(clientObjStub)), + clientEditStub = sandbox.stub(models.Client, 'edit').returns(Promise.resolve()); + + clientObjStub.get.withArgs('secret').returns('abc'); + clientObjStub.get.withArgs('status').returns('enabled'); + + fixtures004[3]({}, logStub).then(function () { + clientOneStub.calledOnce.should.be.true(); + clientOneStub.calledWith({slug: 'ghost-admin'}).should.be.true(); + clientObjStub.get.calledTwice.should.be.true(); + clientObjStub.get.calledWith('secret').should.be.true(); + clientObjStub.get.calledWith('status').should.be.true(); + clientEditStub.called.should.be.false(); + logStub.called.should.be.false(); + sinon.assert.callOrder(clientOneStub, clientObjStub.get, clientObjStub.get); + + done(); + }).catch(done); + }); + + it('tries to update client if secret is correct but status is wrong', function (done) { + var logStub = sandbox.stub(), + clientObjStub = {get: sandbox.stub()}, + clientOneStub = sandbox.stub(models.Client, 'findOne').returns(Promise.resolve(clientObjStub)), + clientEditStub = sandbox.stub(models.Client, 'edit').returns(Promise.resolve()); + + clientObjStub.get.withArgs('secret').returns('abc'); + clientObjStub.get.withArgs('status').returns('development'); + + fixtures004[3]({}, logStub).then(function () { + clientOneStub.calledOnce.should.be.true(); + clientOneStub.calledWith({slug: 'ghost-admin'}).should.be.true(); + clientObjStub.get.calledTwice.should.be.true(); + clientObjStub.get.calledWith('secret').should.be.true(); + clientObjStub.get.calledWith('status').should.be.true(); + + clientEditStub.calledOnce.should.be.true(); + logStub.calledOnce.should.be.true(); + sinon.assert.callOrder( + clientOneStub, clientObjStub.get, clientObjStub.get, logStub, clientEditStub + ); + + done(); + }).catch(done); + }); + + it('tries to update client if status is correct but secret is wrong', function (done) { + var logStub = sandbox.stub(), + clientObjStub = {get: sandbox.stub()}, + clientOneStub = sandbox.stub(models.Client, 'findOne').returns(Promise.resolve(clientObjStub)), + clientEditStub = sandbox.stub(models.Client, 'edit').returns(Promise.resolve()); + + clientObjStub.get.withArgs('secret').returns('not_available'); + clientObjStub.get.withArgs('status').returns('enabled'); + + fixtures004[3]({}, logStub).then(function () { + clientOneStub.calledOnce.should.be.true(); + clientOneStub.calledWith({slug: 'ghost-admin'}).should.be.true(); + clientObjStub.get.calledOnce.should.be.true(); + clientObjStub.get.calledWith('secret').should.be.true(); + + clientEditStub.calledOnce.should.be.true(); + logStub.calledOnce.should.be.true(); + sinon.assert.callOrder( + clientOneStub, clientObjStub.get, logStub, clientEditStub + ); done(); }).catch(done); @@ -238,6 +372,21 @@ describe('Fixtures', function () { done(); }).catch(done); }); + + it('does not try to add client if it already exists', 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.called.should.be.false(); + logStub.called.should.be.false(); + + done(); + }).catch(done); + }); }); describe('06-clean-broken-tags', function () { @@ -287,7 +436,7 @@ describe('Fixtures', function () { }).catch(done); }); - it('tries only changes a tag if necessary', function (done) { + it('does not change tags if not necessary', function (done) { var logStub = sandbox.stub(), tagObjStub = { get: sandbox.stub().returns('hello'), @@ -312,12 +461,12 @@ describe('Fixtures', function () { describe('07-add-post-tag-order', function () { it('calls load on each post', function (done) { - var logStub = sandbox.stub(), - postObjStub = { - load: sandbox.stub().returnsThis() + var postObjStub = { + load: sandbox.stub() }, - postCollStub = {mapThen: sandbox.stub().callsArgWith(0, postObjStub)}, - postAllStub = sandbox.stub(models.Post, 'findAll').returns(Promise.resolve(postCollStub)); + postCollStub = {mapThen: sandbox.stub().callsArgWith(0, postObjStub).returns([])}, + postAllStub = sandbox.stub(models.Post, 'findAll').returns(Promise.resolve(postCollStub)), + logStub = sandbox.stub(); fixtures004[6]({}, logStub).then(function () { postAllStub.calledOnce.should.be.true(); @@ -331,35 +480,194 @@ describe('Fixtures', function () { }).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)); + it('returns early, if no posts are found', function (done) { + var postAllStub = sandbox.stub(models.Post, 'findAll').returns(Promise.resolve()), + logStub = sandbox.stub(); fixtures004[6]({}, logStub).then(function () { + logStub.calledOnce.should.be.true(); + postAllStub.calledOnce.should.be.true(); + sinon.assert.callOrder(logStub, postAllStub); + + done(); + }).catch(done); + }); + + it('executes sequence, if at least one tag is found', function (done) { + var tagOpStub = sandbox.stub().returns(Promise.resolve()), + tagOpsArr = [tagOpStub], + // By stubbing reduce, we can return an array directly without pretending to process tags + postArrayReduceStub = { + reduce: sandbox.stub().returns(tagOpsArr) + }, + // By returning from mapThen, we can skip doing tag.load in this test + postCollStub = {mapThen: sandbox.stub().returns(postArrayReduceStub)}, + postAllStub = sandbox.stub(models.Post, 'findAll').returns(Promise.resolve(postCollStub)), + logStub = sandbox.stub(); + + fixtures004[6]({}, logStub).then(function () { + logStub.calledThrice.should.be.true(); + postAllStub.calledOnce.should.be.true(); + postCollStub.mapThen.calledOnce.should.be.true(); + postArrayReduceStub.reduce.calledOnce.should.be.true(); + tagOpStub.calledOnce.should.be.true(); + + sinon.assert.callOrder( + logStub, postAllStub, postCollStub.mapThen, postArrayReduceStub.reduce, + logStub, tagOpStub, logStub + ); + + done(); + }).catch(done); + }); + + it('executes sequence, if more than one tag is found', function (done) { + var tagOp1Stub = sandbox.stub().returns(Promise.resolve()), + tagOp2Stub = sandbox.stub().returns(Promise.resolve()), + tagOpsArr = [tagOp1Stub, tagOp2Stub], + // By stubbing reduce, we can return an array directly without pretending to process tags + postArrayReduceStub = { + reduce: sandbox.stub().returns(tagOpsArr) + }, + // By returning from mapThen, we can skip doing tag.load in this test + postCollStub = {mapThen: sandbox.stub().returns(postArrayReduceStub)}, + postAllStub = sandbox.stub(models.Post, 'findAll').returns(Promise.resolve(postCollStub)), + logStub = sandbox.stub(); + + fixtures004[6]({}, logStub).then(function () { + logStub.calledThrice.should.be.true(); + postAllStub.calledOnce.should.be.true(); + postCollStub.mapThen.calledOnce.should.be.true(); + postArrayReduceStub.reduce.calledOnce.should.be.true(); + tagOp1Stub.calledOnce.should.be.true(); + tagOp2Stub.calledOnce.should.be.true(); + + sinon.assert.callOrder( + logStub, postAllStub, postCollStub.mapThen, postArrayReduceStub.reduce, + logStub, tagOp1Stub, tagOp2Stub, logStub + ); + + done(); + }).catch(done); + }); + + it('does not execute sequence, if migrationHasRunFlag gets set to true', function (done) { + var tagObjStub = { + pivot: { + // If pivot gets a non-zero, migrationHasRunFlag gets set to true + get: sandbox.stub().returns(1) + } + }, + postObjStub = { + // By returning an array from related, we can use real reduce to simulate a result here + related: sandbox.stub().returns([tagObjStub]) + }, + // By returning from mapThen, we can skip doing tag.load in this test + postCollStub = {mapThen: sandbox.stub().returns([postObjStub])}, + postAllStub = sandbox.stub(models.Post, 'findAll').returns(Promise.resolve(postCollStub)), + logStub = sandbox.stub(); + + fixtures004[6]({}, logStub).then(function () { + logStub.calledOnce.should.be.true(); 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(); + tagObjStub.pivot.get.calledOnce.should.be.true(); + tagObjStub.pivot.get.calledWith('sort_order').should.be.true(); + sinon.assert.callOrder( + logStub, postAllStub, postCollStub.mapThen, postObjStub.related, tagObjStub.pivot.get + ); + + done(); + }).catch(done); + }); + + it('does execute sequence, if migrationHasRunFlag is false', function (done) { + var tagObjStub = { + pivot: { + // If pivot gets a non-zero, migrationHasRunFlag gets set to true + get: sandbox.stub().returns(0) + } + }, + postObjStub = { + // By returning an array from related, we can use real reduce to simulate a result here + related: sandbox.stub().returns([tagObjStub]), + + // Get called when executing the sequence + tags: sandbox.stub().returnsThis(), + updatePivot: sandbox.stub().returns(Promise.resolve()) + }, + // By returning from mapThen, we can skip doing tag.load in this test + postCollStub = {mapThen: sandbox.stub().returns([postObjStub])}, + postAllStub = sandbox.stub(models.Post, 'findAll').returns(Promise.resolve(postCollStub)), + logStub = sandbox.stub(); + + fixtures004[6]({}, logStub).then(function () { + logStub.calledThrice.should.be.true(); + postAllStub.calledOnce.should.be.true(); + postCollStub.mapThen.calledOnce.should.be.true(); + postObjStub.related.calledOnce.should.be.true(); + tagObjStub.pivot.get.calledOnce.should.be.true(); + tagObjStub.pivot.get.calledWith('sort_order').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, postAllStub, postCollStub.mapThen, postObjStub.related, tagObjStub.pivot.get, logStub, postObjStub.tags, postObjStub.updatePivot, logStub ); done(); }).catch(done); }); + + it('tries to add incremental sort_order to posts_tags', function (done) { + var tagObjStub = { + pivot: { + // If pivot gets a non-zero, migrationHasRunFlag gets set to true + get: sandbox.stub().returns(0) + } + }, + postObjStub = { + // By returning an array from related, we can use real reduce to simulate a result here + related: sandbox.stub().returns([tagObjStub, tagObjStub, tagObjStub]), + + // Get called when executing the sequence + tags: sandbox.stub().returnsThis(), + updatePivot: sandbox.stub().returns(Promise.resolve()) + }, + // By returning from mapThen, we can skip doing tag.load in this test + postCollStub = {mapThen: sandbox.stub().returns([postObjStub])}, + postAllStub = sandbox.stub(models.Post, 'findAll').returns(Promise.resolve(postCollStub)), + logStub = sandbox.stub(); + + fixtures004[6]({}, logStub).then(function () { + logStub.calledThrice.should.be.true(); + postAllStub.calledOnce.should.be.true(); + postCollStub.mapThen.calledOnce.should.be.true(); + postObjStub.related.calledOnce.should.be.true(); + tagObjStub.pivot.get.calledThrice.should.be.true(); + + postObjStub.tags.calledThrice.should.be.true(); + postObjStub.updatePivot.calledThrice.should.be.true(); + + postObjStub.updatePivot.firstCall.args[0].should.eql({sort_order: 0}); + postObjStub.updatePivot.secondCall.args[0].should.eql({sort_order: 1}); + postObjStub.updatePivot.thirdCall.args[0].should.eql({sort_order: 2}); + + sinon.assert.callOrder( + logStub, postAllStub, postCollStub.mapThen, postObjStub.related, + tagObjStub.pivot.get, tagObjStub.pivot.get, tagObjStub.pivot.get, + logStub, + postObjStub.tags, postObjStub.updatePivot, + postObjStub.tags, postObjStub.updatePivot, + postObjStub.tags, postObjStub.updatePivot, + logStub + ); + + done(); + }).catch(done); + }); }); describe('08-add-post-fixture', function () { @@ -378,6 +686,20 @@ describe('Fixtures', function () { }).catch(done); }); }); + + it('does not try to add new post fixture if it already exists', 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.called.should.be.false(); + postAddStub.called.should.be.false(); + + done(); + }).catch(done); + }); }); }); });