Ghost/core/test/integration/update_check_spec.js
Katharina Irrgang 5b77f052d9
Update Notification improvements (#9123)
closes #5071

- Remove hardcoded notification in admin controller
  - NOTE: update check notifications are no longer blocking the admin rendering
  - this is one of the most import changes
  - we remove the hardcoded release message
  - we also remove adding a notification manually in here, because this will work differently from now on
    -> you receive a notification (release or custom) in the update check module and this module adds the notification as is to our database

- Change default core settings keys
  - remove displayUpdateNotification
    -> this was used to store the release version number send from the UCS
    -> based on this value, Ghost creates a notification container with self defined values
    -> not needed anymore

- rename seenNotifications to notifications
  -> the new notifications key will hold both
     1. the notification from the USC
     2. the information about if a notification was seen or not
  - this key hold only one release notification
  - and n custom notifications

- Update Check Module: Request to the USC depends on the privacy configuration
  - useUpdateCheck: true -> does a checkin in the USC (exposes data)
  - useUpdateCheck: false -> does only a GET query to the USC (does not expose any data)
  - make the request handling dynamic, so it depends on the flag
  - add an extra logic to be able to define a custom USC endpoint (helpful for testing)
  - add an extra logic to be able to force the request to the service (helpful for testing)

- Update check module: re-work condition when a check should happen
  - only if the env is not correct
  - remove deprecated config.updateCheck
  - remove isPrivacyDisabled check (handled differently now, explained in last commit)

- Update check module: remove `showUpdateNotification` and readability
  - showUpdateNotification was used in the admin controller to fetch the latest release version number from the db
  - no need to check against semver in general, the USC takes care of that (no need to double check)
  - improve readability of `nextUpdateCheck` condition

- Update check module: refactor `updateCheckResponse`
  - remove db call to displayUpdateNotification, not used anymore
  - support receiving multiple custom notifications
  - support custom notification groups
  - the default group is `all` - this will always be consumed
  - groups can be extended via config e.g. `notificationGroups: ['migration']`

- Update check module: refactor createCustomNotification helper
  - get rid of taking over notification duplication handling (this is not the task of the update check module)
  - ensure we have good fallback values for non present attributes in a notification
  - get rid of semver check (happens in the USC) - could be reconsidered later if LTS is gone

- Refactor notification API
  - reason: get rid of in process notification store
    -> this was an object hold in process
    -> everything get's lost after restart
    -> not helpful anymore, because imagine the following case
      -> you get a notification
      -> you store it in process
      -> you mark this notification as seen
      -> you restart Ghost, you will receive the same notification on the next check again
      -> because we are no longer have a separate seen notifications object
  - use database settings key `notification` instead
  - refactor all api endpoints to support reading and storing into the `notifications` object
  - most important: notification deletion happens via a `seen` property (the notification get's physically deleted 3 month automatically)
    -> we have to remember a seen property, because otherwise you don't know which notification was already received/seen

- Add listener to remove seen notifications automatically after 3 month
  - i just decided for 3 month (we can decrease?)
  - at the end it doesn't really matter, as long as the windows is not tooooo short
  - listen on updates for the notifications settings
  - check if notification was seen and is older than 3 month
  - ignore release notification

- Updated our privacy document
- Updated docs.ghost.org for privacy config behaviour
- contains a migration script to remove old settings keys
2018-01-09 15:20:00 +01:00

646 lines
23 KiB
JavaScript

var _ = require('lodash'),
Promise = require('bluebird'),
should = require('should'),
rewire = require('rewire'),
sinon = require('sinon'),
moment = require('moment'),
uuid = require('uuid'),
testUtils = require('../utils'),
configUtils = require('../utils/configUtils'),
packageInfo = require('../../../package'),
updateCheck = rewire('../../server/update-check'),
SettingsAPI = require('../../server/api/settings'),
NotificationsAPI = require('../../server/api/notifications'),
sandbox = sinon.sandbox.create();
describe('Update Check', function () {
beforeEach(function () {
updateCheck = rewire('../../server/update-check');
});
afterEach(function () {
sandbox.restore();
configUtils.restore();
});
describe('fn: updateCheck', function () {
var updateCheckRequestSpy,
updateCheckResponseSpy,
updateCheckErrorSpy;
beforeEach(testUtils.setup('owner', 'posts', 'perms:setting', 'perms:user', 'perms:init'));
afterEach(testUtils.teardown);
beforeEach(function () {
updateCheckRequestSpy = sandbox.stub().returns(Promise.resolve());
updateCheckResponseSpy = sandbox.stub().returns(Promise.resolve());
updateCheckErrorSpy = sandbox.stub();
updateCheck.__set__('updateCheckRequest', updateCheckRequestSpy);
updateCheck.__set__('updateCheckResponse', updateCheckResponseSpy);
updateCheck.__set__('updateCheckError', updateCheckErrorSpy);
updateCheck.__set__('allowedCheckEnvironments', ['development', 'production', 'testing', 'testing-mysql', 'testing-pg']);
});
it('update check was never executed', function (done) {
sandbox.stub(SettingsAPI, 'read').returns(Promise.resolve({
settings: [{
value: null
}]
}));
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) {
sandbox.stub(SettingsAPI, 'read').returns(Promise.resolve({
settings: [{
value: moment().add('10', 'minutes').unix()
}]
}));
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) {
sandbox.stub(SettingsAPI, 'read').returns(Promise.resolve({
settings: [{
value: moment().subtract('10', 'minutes').unix()
}]
}));
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 () {
var environmentsOrig;
before(function () {
configUtils.set('privacy:useUpdateCheck', true);
});
after(function () {
configUtils.restore();
});
beforeEach(testUtils.setup('owner', 'settings', 'posts', 'perms:setting', 'perms:user', 'perms:init'));
afterEach(testUtils.teardown);
it('should report the correct data', function (done) {
var 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.apps.should.be.a.String();
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();
data.lts.should.eql(false);
done();
}).catch(done);
});
});
describe('fn: createCustomNotification', function () {
var currentVersionOrig;
before(function () {
currentVersionOrig = updateCheck.__get__('ghostVersion.original');
updateCheck.__set__('ghostVersion.original', '0.9.0');
});
after(function () {
updateCheck.__set__('ghostVersion.original', currentVersionOrig);
});
beforeEach(testUtils.setup('owner', 'posts', 'settings', 'perms:setting', 'perms:notification', 'perms:user', 'perms:init'));
beforeEach(function () {
return NotificationsAPI.destroyAll(testUtils.context.internal);
});
afterEach(testUtils.teardown);
it('should create a release notification for target version', function (done) {
var createCustomNotification = updateCheck.__get__('createCustomNotification'),
notification = {
id: 1,
custom: 0,
messages: [{
id: uuid.v4(),
version: '0.9.x',
content: '<p>Hey there! This is for 0.9.0 version</p>',
dismissible: true,
top: true
}]
};
createCustomNotification(notification).then(function () {
return NotificationsAPI.browse(testUtils.context.internal);
}).then(function (results) {
should.exist(results);
should.exist(results.notifications);
results.notifications.length.should.eql(1);
var 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('should create a custom notification', function (done) {
var createCustomNotification = updateCheck.__get__('createCustomNotification'),
notification = {
id: 1,
custom: 1,
messages: [{
id: uuid.v4(),
version: 'custom1',
content: '<p>How about migrating your blog?</p>',
dismissible: false,
top: true,
type: 'warn'
}]
};
createCustomNotification(notification).then(function () {
return NotificationsAPI.browse(testUtils.context.internal);
}).then(function (results) {
should.exist(results);
should.exist(results.notifications);
results.notifications.length.should.eql(1);
var 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) {
var createCustomNotification = updateCheck.__get__('createCustomNotification'),
notification = {
id: 1,
custom: 1,
messages: [{
id: uuid.v4(),
version: 'custom1',
content: '<p>How about migrating your blog?</p>',
dismissible: false,
top: true,
type: 'warn'
}]
};
createCustomNotification(notification)
.then(function () {
return NotificationsAPI.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 NotificationsAPI.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.setup('settings', 'perms:setting', 'perms:init'));
afterEach(testUtils.teardown);
it('receives a notifications with messages', function (done) {
var updateCheckResponse = updateCheck.__get__('updateCheckResponse'),
createNotificationSpy = sandbox.spy(),
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) {
var updateCheckResponse = updateCheck.__get__('updateCheckResponse'),
createNotificationSpy = sandbox.spy(),
message1 = {
id: uuid.v4(),
version: '^0.11.11',
content: 'Test1',
dismissible: true,
top: true
},
message2 = {
id: uuid.v4(),
version: '^0',
content: 'Test2',
dismissible: true,
top: false
},
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) {
var updateCheckResponse = updateCheck.__get__('updateCheckResponse'),
createNotificationSpy = sandbox.spy(),
message1 = {
id: uuid.v4(),
version: '^0.11.11',
content: 'Test1',
dismissible: true,
top: true
},
message2 = {
id: uuid.v4(),
version: '^0',
content: 'Test2',
dismissible: true,
top: false
},
message3 = {
id: uuid.v4(),
version: '^0',
content: 'Test2',
dismissible: true,
top: false
},
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) {
var updateCheckResponse = updateCheck.__get__('updateCheckResponse'),
createNotificationSpy = sandbox.spy(),
message1 = {
id: uuid.v4(),
version: '^0.11.11',
content: 'Test1',
dismissible: true,
top: true
},
message2 = {
id: uuid.v4(),
version: '^0',
content: 'Test2',
dismissible: true,
top: false
},
message3 = {
id: uuid.v4(),
version: '^0',
content: 'Test2',
dismissible: true,
top: false
},
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) {
var updateCheckResponse = updateCheck.__get__('updateCheckResponse'),
createNotificationSpy = sandbox.spy(),
message1 = {
id: uuid.v4(),
version: '^0.11.11',
content: 'Custom',
dismissible: true,
top: true
},
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) {
var updateCheckResponse = updateCheck.__get__('updateCheckResponse'),
createNotificationSpy = sandbox.spy(),
message1 = {
id: uuid.v4(),
version: '^0.11.11',
content: 'Custom',
dismissible: true,
top: true
},
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 () {
var updateCheckRequest = updateCheck.__get__('updateCheckRequest'),
updateCheckDataSpy = sandbox.stub(),
hostname,
reqObj,
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 () {
var updateCheckRequest = updateCheck.__get__('updateCheckRequest'),
updateCheckDataSpy = sandbox.stub(),
reqObj,
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 () {
var updateCheckRequest = updateCheck.__get__('updateCheckRequest'),
updateCheckDataSpy = sandbox.stub(),
reqObj,
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 () {
var updateCheckRequest = updateCheck.__get__('updateCheckRequest'),
updateCheckDataSpy = sandbox.stub(),
reqObj,
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 () {
var updateCheckRequest = updateCheck.__get__('updateCheckRequest'),
updateCheckDataSpy = sandbox.stub(),
reqObj,
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');
});
});
});
});