const _ = require('lodash'); const Promise = require('bluebird'); const should = require('should'); const rewire = require('rewire'); const sinon = require('sinon'); const moment = require('moment'); const uuid = require('uuid'); const testUtils = require('../../utils'); const configUtils = require('../../utils/configUtils'); const packageInfo = require('../../../package.json'); const api = require('../../../core/server/api').v2; let updateCheck = rewire('../../../core/server/update-check'); let NotificationsAPI = rewire('../../../core/server/api/v2/notifications'); describe('Update Check', function () { beforeEach(function () { updateCheck = rewire('../../../core/server/update-check'); NotificationsAPI = rewire('../../../core/server/api/v2/notifications'); }); afterEach(function () { sinon.restore(); configUtils.restore(); }); after(testUtils.teardownDb); describe('fn: updateCheck', function () { let updateCheckRequestSpy; let updateCheckResponseSpy; let updateCheckErrorSpy; beforeEach(testUtils.teardownDb); beforeEach(testUtils.setup('roles', 'owner')); beforeEach(function () { updateCheckRequestSpy = sinon.stub().returns(Promise.resolve()); updateCheckResponseSpy = sinon.stub().returns(Promise.resolve()); updateCheckErrorSpy = sinon.stub(); updateCheck.__set__('updateCheckRequest', updateCheckRequestSpy); updateCheck.__set__('updateCheckResponse', updateCheckResponseSpy); updateCheck.__set__('updateCheckError', updateCheckErrorSpy); updateCheck.__set__('allowedCheckEnvironments', ['development', 'production', 'testing', 'testing-mysql']); }); it('update check was never executed', function (done) { const readStub = sinon.stub().resolves({ settings: [{ value: null }] }); sinon.stub(api, 'settings').get(() => ({ read: readStub })); updateCheck() .then(function () { updateCheckRequestSpy.calledOnce.should.eql(true); updateCheckResponseSpy.calledOnce.should.eql(true); updateCheckErrorSpy.called.should.eql(false); done(); }) .catch(done); }); it('update check won\'t happen if it\'s too early', function (done) { const readStub = sinon.stub().resolves({ settings: [{ value: moment().add('10', 'minutes').unix() }] }); sinon.stub(api, 'settings').get(() => ({ read: readStub })); updateCheck() .then(function () { updateCheckRequestSpy.calledOnce.should.eql(false); updateCheckResponseSpy.calledOnce.should.eql(false); updateCheckErrorSpy.called.should.eql(false); done(); }) .catch(done); }); it('update check will happen if it\'s time to check', function (done) { const readStub = sinon.stub().resolves({ settings: [{ value: moment().subtract('10', 'minutes').unix() }] }); sinon.stub(api, 'settings').get(() => ({ read: readStub })); updateCheck() .then(function () { updateCheckRequestSpy.calledOnce.should.eql(true); updateCheckResponseSpy.calledOnce.should.eql(true); updateCheckErrorSpy.called.should.eql(false); done(); }) .catch(done); }); }); describe('fn: updateCheckData', function () { let environmentsOrig; before(function () { configUtils.set('privacy:useUpdateCheck', true); }); after(function () { configUtils.restore(); }); beforeEach(testUtils.teardownDb); beforeEach(testUtils.setup('roles', 'owner', 'settings', 'posts', 'perms:setting', 'perms:user', 'perms:init')); it('should report the correct data', function (done) { const updateCheckData = updateCheck.__get__('updateCheckData'); updateCheckData().then(function (data) { should.exist(data); data.ghost_version.should.equal(packageInfo.version); data.node_version.should.equal(process.versions.node); data.env.should.equal(process.env.NODE_ENV); data.database_type.should.match(/sqlite3|mysql/); data.blog_id.should.be.a.String(); data.blog_id.should.not.be.empty(); data.theme.should.be.equal('casper'); data.blog_created_at.should.be.a.Number(); data.user_count.should.be.above(0); data.post_count.should.be.above(0); data.npm_version.should.be.a.String(); data.npm_version.should.not.be.empty(); done(); }).catch(done); }); }); describe('fn: createCustomNotification', function () { let currentVersionOrig; before(function () { currentVersionOrig = updateCheck.__get__('ghostVersion.original'); updateCheck.__set__('ghostVersion.original', '0.9.0'); }); after(function () { updateCheck.__set__('ghostVersion.original', currentVersionOrig); }); beforeEach(testUtils.teardownDb); beforeEach(testUtils.setup('settings', 'roles', 'owner', 'perms:setting', 'perms:notification', 'perms:user', 'perms:init')); beforeEach(function () { return api.notifications.destroyAll(testUtils.context.internal); }); it('should create a release notification for target version', function (done) { const createCustomNotification = updateCheck.__get__('createCustomNotification'); const notification = { id: 1, custom: 0, messages: [{ id: uuid.v4(), version: '0.9.x', content: '

Hey there! This is for 0.9.0 version

', dismissible: true, top: true }] }; NotificationsAPI.__set__('ghostVersion.full', '0.8.1'); createCustomNotification(notification).then(function () { return api.notifications.browse(testUtils.context.internal); }).then(function (results) { should.exist(results); should.exist(results.notifications); results.notifications.length.should.eql(1); const targetNotification = _.find(results.notifications, {id: notification.messages[0].id}); should.exist(targetNotification); targetNotification.dismissible.should.eql(notification.messages[0].dismissible); targetNotification.id.should.eql(notification.messages[0].id); targetNotification.top.should.eql(notification.messages[0].top); targetNotification.type.should.eql('info'); targetNotification.message.should.eql(notification.messages[0].content); done(); }).catch(done); }); it('release notification version format is wrong', function (done) { const createCustomNotification = updateCheck.__get__('createCustomNotification'); const notification = { id: 1, custom: 0, messages: [{ id: uuid.v4(), version: '0.9.x', content: '

Hey there! This is for 0.9 version

', dismissible: true, top: true }] }; NotificationsAPI.__set__('ghostVersion.full', '0.8.1'); createCustomNotification(notification).then(function () { return api.notifications.browse(testUtils.context.internal); }).then(function (results) { should.exist(results); should.exist(results.notifications); results.notifications.length.should.eql(0); done(); }).catch(done); }); it('blog version format is wrong', function (done) { const createCustomNotification = updateCheck.__get__('createCustomNotification'); const notification = { id: 1, custom: 0, messages: [{ id: uuid.v4(), version: '0.9.x', content: '

Hey there! This is for 0.9.0 version

', dismissible: true, top: true }] }; NotificationsAPI.__set__('ghostVersion.full', '0.8'); createCustomNotification(notification).then(function () { return api.notifications.browse(testUtils.context.internal); }).then(function (results) { should.exist(results); should.exist(results.notifications); results.notifications.length.should.eql(0); done(); }).catch(done); }); it('should create a custom notification', function (done) { const createCustomNotification = updateCheck.__get__('createCustomNotification'); const notification = { id: 1, custom: 1, messages: [{ id: uuid.v4(), version: 'custom1', content: '

How about migrating your blog?

', dismissible: false, top: true, type: 'warn' }] }; createCustomNotification(notification).then(function () { return api.notifications.browse(testUtils.context.internal); }).then(function (results) { should.exist(results); should.exist(results.notifications); results.notifications.length.should.eql(1); const targetNotification = _.find(results.notifications, {id: notification.messages[0].id}); should.exist(targetNotification); targetNotification.dismissible.should.eql(notification.messages[0].dismissible); targetNotification.top.should.eql(notification.messages[0].top); targetNotification.type.should.eql(notification.messages[0].type); done(); }).catch(done); }); it('should not add duplicates', function (done) { const createCustomNotification = updateCheck.__get__('createCustomNotification'); const notification = { id: 1, custom: 1, messages: [{ id: uuid.v4(), version: 'custom1', content: '

How about migrating your blog?

', dismissible: false, top: true, type: 'warn' }] }; createCustomNotification(notification) .then(function () { return api.notifications.browse(testUtils.context.internal); }) .then(function (results) { should.exist(results); should.exist(results.notifications); results.notifications.length.should.eql(1); }) .then(function () { return createCustomNotification(notification); }) .then(function () { return api.notifications.browse(testUtils.context.internal); }) .then(function (results) { should.exist(results); should.exist(results.notifications); results.notifications.length.should.eql(1); done(); }) .catch(done); }); }); describe('fn: updateCheckResponse', function () { beforeEach(testUtils.teardownDb); beforeEach(testUtils.setup('roles', 'settings', 'perms:setting', 'perms:init')); it('receives a notifications with messages', function (done) { const updateCheckResponse = updateCheck.__get__('updateCheckResponse'); const createNotificationSpy = sinon.spy(); const message = { id: uuid.v4(), version: '^0.11.11', content: 'Test', dismissible: true, top: true }; updateCheck.__set__('createCustomNotification', createNotificationSpy); updateCheckResponse({version: '0.11.12', messages: [message]}) .then(function () { createNotificationSpy.callCount.should.eql(1); done(); }) .catch(done); }); it('receives multiple notifications', function (done) { const updateCheckResponse = updateCheck.__get__('updateCheckResponse'); const createNotificationSpy = sinon.spy(); const message1 = { id: uuid.v4(), version: '^0.11.11', content: 'Test1', dismissible: true, top: true }; const message2 = { id: uuid.v4(), version: '^0', content: 'Test2', dismissible: true, top: false }; const notifications = [ {version: '0.11.12', messages: [message1]}, {version: 'custom1', messages: [message2]} ]; updateCheck.__set__('createCustomNotification', createNotificationSpy); updateCheckResponse(notifications) .then(function () { createNotificationSpy.callCount.should.eql(2); done(); }) .catch(done); }); it('ignores some custom notifications which are not marked as group', function (done) { const updateCheckResponse = updateCheck.__get__('updateCheckResponse'); const createNotificationSpy = sinon.spy(); const message1 = { id: uuid.v4(), version: '^0.11.11', content: 'Test1', dismissible: true, top: true }; const message2 = { id: uuid.v4(), version: '^0', content: 'Test2', dismissible: true, top: false }; const message3 = { id: uuid.v4(), version: '^0', content: 'Test2', dismissible: true, top: false }; const notifications = [ {version: '0.11.12', messages: [message1]}, {version: 'all1', messages: [message2], custom: 1}, {version: 'migration1', messages: [message3], custom: 1} ]; updateCheck.__set__('createCustomNotification', createNotificationSpy); updateCheckResponse(notifications) .then(function () { createNotificationSpy.callCount.should.eql(2); done(); }) .catch(done); }); it('group matches', function (done) { const updateCheckResponse = updateCheck.__get__('updateCheckResponse'); const createNotificationSpy = sinon.spy(); const message1 = { id: uuid.v4(), version: '^0.11.11', content: 'Test1', dismissible: true, top: true }; const message2 = { id: uuid.v4(), version: '^0', content: 'Test2', dismissible: true, top: false }; const message3 = { id: uuid.v4(), version: '^0', content: 'Test2', dismissible: true, top: false }; const notifications = [ {version: '0.11.12', messages: [message1], custom: 0}, {version: 'all1', messages: [message2], custom: 1}, {version: 'migration1', messages: [message3], custom: 1} ]; updateCheck.__set__('createCustomNotification', createNotificationSpy); configUtils.set({notificationGroups: ['migration']}); updateCheckResponse(notifications) .then(function () { createNotificationSpy.callCount.should.eql(3); done(); }) .catch(done); }); it('single custom notification received, group matches', function (done) { const updateCheckResponse = updateCheck.__get__('updateCheckResponse'); const createNotificationSpy = sinon.spy(); const message1 = { id: uuid.v4(), version: '^0.11.11', content: 'Custom', dismissible: true, top: true }; const notifications = [ {version: 'something', messages: [message1], custom: 1} ]; updateCheck.__set__('createCustomNotification', createNotificationSpy); configUtils.set({notificationGroups: ['something']}); updateCheckResponse(notifications) .then(function () { createNotificationSpy.callCount.should.eql(1); done(); }) .catch(done); }); it('single custom notification received, group does not match', function (done) { const updateCheckResponse = updateCheck.__get__('updateCheckResponse'); const createNotificationSpy = sinon.spy(); const message1 = { id: uuid.v4(), version: '^0.11.11', content: 'Custom', dismissible: true, top: true }; const notifications = [ {version: 'something', messages: [message1], custom: 1} ]; updateCheck.__set__('createCustomNotification', createNotificationSpy); configUtils.set({notificationGroups: ['migration']}); updateCheckResponse(notifications) .then(function () { createNotificationSpy.callCount.should.eql(0); done(); }) .catch(done); }); }); describe('fn: updateCheckRequest', function () { beforeEach(function () { configUtils.set('privacy:useUpdateCheck', true); }); afterEach(function () { configUtils.restore(); }); it('[default]', function () { const updateCheckRequest = updateCheck.__get__('updateCheckRequest'); const updateCheckDataSpy = sinon.stub(); let hostname; let reqObj; const data = { ghost_version: '0.11.11', blog_id: 'something', npm_version: 'something' }; updateCheck.__set__('request', function (_hostname, _reqObj) { hostname = _hostname; reqObj = _reqObj; return Promise.resolve({ statusCode: 200, body: {version: 'something'} }); }); updateCheck.__set__('updateCheckData', updateCheckDataSpy); updateCheckDataSpy.returns(Promise.resolve(data)); return updateCheckRequest() .then(function () { hostname.should.eql('https://updates.ghost.org'); should.exist(reqObj.headers['Content-Length']); reqObj.body.should.eql(data); reqObj.json.should.eql(true); }); }); it('privacy flag is used', function () { const updateCheckRequest = updateCheck.__get__('updateCheckRequest'); const updateCheckDataSpy = sinon.stub(); let reqObj; let hostname; configUtils.set({ privacy: { useUpdateCheck: false } }); updateCheck.__set__('request', function (_hostname, _reqObj) { hostname = _hostname; reqObj = _reqObj; return Promise.resolve({ statusCode: 200, body: {version: 'something'} }); }); updateCheck.__set__('updateCheckData', updateCheckDataSpy); updateCheckDataSpy.returns(Promise.resolve({ ghost_version: '0.11.11', blog_id: 'something', npm_version: 'something' })); return updateCheckRequest() .then(function () { hostname.should.eql('https://updates.ghost.org'); reqObj.query.should.eql({ ghost_version: '0.11.11' }); should.not.exist(reqObj.body); reqObj.json.should.eql(true); should.not.exist(reqObj.headers['Content-Length']); }); }); it('received 500 from the service', function () { const updateCheckRequest = updateCheck.__get__('updateCheckRequest'); const updateCheckDataSpy = sinon.stub(); let reqObj; let hostname; updateCheck.__set__('request', function (_hostname, _reqObj) { hostname = _hostname; reqObj = _reqObj; return Promise.reject({ statusCode: 500, message: 'something went wrong' }); }); updateCheck.__set__('updateCheckData', updateCheckDataSpy); updateCheckDataSpy.returns(Promise.resolve({ ghost_version: '0.11.11', blog_id: 'something', npm_version: 'something' })); return updateCheckRequest() .then(function () { throw new Error('Should fail.'); }) .catch(function (err) { err.message.should.eql('something went wrong'); }); }); it('received 404 from the service', function () { const updateCheckRequest = updateCheck.__get__('updateCheckRequest'); const updateCheckDataSpy = sinon.stub(); let reqObj; let hostname; updateCheck.__set__('request', function (_hostname, _reqObj) { hostname = _hostname; reqObj = _reqObj; return Promise.reject({ statusCode: 404, response: { body: { errors: [{detail: 'No Notifications available.'}] } } }); }); updateCheck.__set__('updateCheckData', updateCheckDataSpy); updateCheckDataSpy.returns(Promise.resolve({ ghost_version: '0.11.11', blog_id: 'something', npm_version: 'something' })); return updateCheckRequest() .then(function () { hostname.should.eql('https://updates.ghost.org'); }); }); it('custom url', function () { const updateCheckRequest = updateCheck.__get__('updateCheckRequest'); const updateCheckDataSpy = sinon.stub(); let reqObj; let hostname; configUtils.set({ updateCheck: { url: 'http://localhost:3000' } }); updateCheck.__set__('request', function (_hostname, _reqObj) { hostname = _hostname; reqObj = _reqObj; return Promise.resolve({ statusCode: 200, body: { version: 'something' } }); }); updateCheck.__set__('updateCheckData', updateCheckDataSpy); updateCheckDataSpy.returns(Promise.resolve({ ghost_version: '0.11.11', blog_id: 'something', npm_version: 'something' })); return updateCheckRequest() .then(function () { hostname.should.eql('http://localhost:3000'); }); }); }); });