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
This commit is contained in:
Katharina Irrgang 2018-01-09 15:20:00 +01:00 committed by GitHub
parent f671f9d2c9
commit 5b77f052d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1161 additions and 366 deletions

View File

@ -11,9 +11,11 @@ Some official services for Ghost are enabled by default. These services connect
### Automatic Update Checks
When a new session is started, Ghost pings a Ghost.org endpoint to check if the current version of Ghost is the latest version of Ghost. If an update is available, a notification appears inside Ghost to let you know. Ghost.org collects basic anonymised usage statistics from update check requests.
When a new session is started, Ghost pings a Ghost.org service to check if the current version of Ghost is the latest version of Ghost. If an update is available, a notification on the About Page appears to let you know.
This service can be disabled at any time. All of the information and code related to this service is available in the [update-check.js](https://github.com/TryGhost/Ghost/blob/master/core/server/update-check.js) file.
Ghost will collect basic anonymised usage statistics from your blog before sending the request to the service. You can disable collecting statistics using the [privacy configuration](https://docs.ghost.org/v1/docs/config#section-update-check). You will still receive notifications from the service.
All of the information and code related to this service is available in the [update-check.js](https://github.com/TryGhost/Ghost/blob/master/core/server/update-check.js) file.
## Third Party Services

View File

@ -1,17 +1,48 @@
'use strict';
// # Notifications API
// RESTful API for creating notifications
var Promise = require('bluebird'),
const Promise = require('bluebird'),
_ = require('lodash'),
moment = require('moment'),
ObjectId = require('bson-objectid'),
pipeline = require('../lib/promise/pipeline'),
permissions = require('../services/permissions'),
canThis = permissions.canThis,
localUtils = require('./utils'),
common = require('../lib/common'),
settingsAPI = require('./settings'),
// Holds the persistent notifications
notificationsStore = [],
notifications;
SettingsAPI = require('./settings'),
internalContext = {context: {internal: true}},
canThis = permissions.canThis;
let notifications,
_private = {};
_private.fetchAllNotifications = function fetchAllNotifications() {
let allNotifications;
return SettingsAPI.read(_.merge({key: 'notifications'}, internalContext))
.then(function (response) {
allNotifications = JSON.parse(response.settings[0].value || []);
_.each(allNotifications, function (notification) {
notification.addedAt = moment(notification.addedAt).toDate();
});
return allNotifications;
});
};
_private.publicResponse = function publicResponse(notificationsToReturn) {
_.each(notificationsToReturn, function (notification) {
delete notification.seen;
delete notification.addedAt;
});
return {
notifications: notificationsToReturn
};
};
/**
* ## Notification API Methods
@ -27,9 +58,20 @@ notifications = {
*/
browse: function browse(options) {
return canThis(options.context).browse.notification().then(function () {
return {notifications: notificationsStore};
return _private.fetchAllNotifications()
.then(function (allNotifications) {
allNotifications = _.orderBy(allNotifications, 'addedAt', 'desc');
allNotifications = allNotifications.filter(function (notification) {
return notification.seen !== true;
});
return _private.publicResponse(allNotifications);
});
}, function () {
return Promise.reject(new common.errors.NoPermissionError({message: common.i18n.t('errors.api.notifications.noPermissionToBrowseNotif')}));
return Promise.reject(new common.errors.NoPermissionError({
message: common.i18n.t('errors.api.notifications.noPermissionToBrowseNotif')
}));
});
},
@ -69,7 +111,9 @@ notifications = {
return canThis(options.context).add.notification().then(function () {
return options;
}, function () {
return Promise.reject(new common.errors.NoPermissionError({message: common.i18n.t('errors.api.notifications.noPermissionToAddNotif')}));
return Promise.reject(new common.errors.NoPermissionError({
message: common.i18n.t('errors.api.notifications.noPermissionToAddNotif')
}));
});
}
@ -80,31 +124,61 @@ notifications = {
* @returns {Object} options
*/
function saveNotifications(options) {
var defaults = {
let defaults = {
dismissible: true,
location: 'bottom',
status: 'alert'
},
addedNotifications = [], existingNotification;
_.each(options.data.notifications, function (notification) {
notification = _.assign(defaults, notification, {
status: 'alert',
id: ObjectId.generate()
},
overrides = {
seen: false,
addedAt: moment().toDate()
},
notificationsToCheck = options.data.notifications,
addedNotifications = [];
return _private.fetchAllNotifications()
.then(function (allNotifications) {
_.each(notificationsToCheck, function (notification) {
let isDuplicate = _.find(allNotifications, {id: notification.id});
if (!isDuplicate) {
addedNotifications.push(_.merge({}, defaults, notification, overrides));
}
});
let hasReleaseNotification = _.find(notificationsToCheck, {custom: false});
// CASE: remove any existing release notifications if a new release notification comes in
if (hasReleaseNotification) {
_.remove(allNotifications, function (el) {
return !el.custom;
});
}
// CASE: nothing to add, skip
if (!addedNotifications.length) {
return Promise.resolve();
}
let addedReleaseNotifications = _.filter(addedNotifications, {custom: false});
// CASE: only latest release notification
if (addedReleaseNotifications.length > 1) {
addedNotifications = _.filter(addedNotifications, {custom: true});
addedNotifications.push(_.orderBy(addedReleaseNotifications, 'created_at', 'desc')[0]);
}
return SettingsAPI.edit({
settings: [{
key: 'notifications',
value: allNotifications.concat(addedNotifications)
}]
}, internalContext);
})
.then(function () {
return _private.publicResponse(addedNotifications);
});
existingNotification = _.find(notificationsStore, {message: notification.message});
if (!existingNotification) {
notificationsStore.push(notification);
addedNotifications.push(notification);
} else {
addedNotifications.push(existingNotification);
}
});
return {
notifications: addedNotifications
};
}
tasks = [
@ -124,26 +198,7 @@ notifications = {
* @returns {Promise}
*/
destroy: function destroy(options) {
var tasks;
/**
* Adds the id of notification to "seen_notifications" array.
* @param {Object} notification
* @return {*|Promise}
*/
function markAsSeen(notification) {
var context = {internal: true};
return settingsAPI.read({key: 'seen_notifications', context: context}).then(function then(response) {
var seenNotifications = JSON.parse(response.settings[0].value);
seenNotifications = _.uniqBy(seenNotifications.concat([notification.id]));
return settingsAPI.edit({
settings: [{
key: 'seen_notifications',
value: seenNotifications
}]
}, {context: context});
});
}
let tasks;
/**
* ### Handle Permissions
@ -155,36 +210,47 @@ notifications = {
return canThis(options.context).destroy.notification().then(function () {
return options;
}, function () {
return Promise.reject(new common.errors.NoPermissionError({message: common.i18n.t('errors.api.notifications.noPermissionToDestroyNotif')}));
return Promise.reject(new common.errors.NoPermissionError({
message: common.i18n.t('errors.api.notifications.noPermissionToDestroyNotif')
}));
});
}
function destroyNotification(options) {
var notification = _.find(notificationsStore, function (element) {
return element.id === options.id;
});
return _private.fetchAllNotifications()
.then(function (allNotifications) {
let notificationToMarkAsSeen = _.find(allNotifications, {id: options.id}),
notificationToMarkAsSeenIndex = _.findIndex(allNotifications, {id: options.id});
if (notification && !notification.dismissible) {
return Promise.reject(
new common.errors.NoPermissionError({message: common.i18n.t('errors.api.notifications.noPermissionToDismissNotif')})
);
}
if (notificationToMarkAsSeenIndex > -1 && !notificationToMarkAsSeen.dismissible) {
return Promise.reject(new common.errors.NoPermissionError({
message: common.i18n.t('errors.api.notifications.noPermissionToDismissNotif')
}));
}
if (!notification) {
return Promise.reject(new common.errors.NotFoundError({message: common.i18n.t('errors.api.notifications.notificationDoesNotExist')}));
}
if (notificationToMarkAsSeenIndex < 0) {
return Promise.reject(new common.errors.NotFoundError({
message: common.i18n.t('errors.api.notifications.notificationDoesNotExist')
}));
}
notificationsStore = _.reject(notificationsStore, function (element) {
return element.id === options.id;
});
if (notificationToMarkAsSeen.seen) {
return Promise.resolve();
}
if (notification.custom) {
return markAsSeen(notification);
}
allNotifications[notificationToMarkAsSeenIndex].seen = true;
return SettingsAPI.edit({
settings: [{
key: 'notifications',
value: allNotifications
}]
}, internalContext);
})
.return();
}
tasks = [
localUtils.validate('notifications', {opts: localUtils.idDefaultOptions}),
handlePermissions,
destroyNotification
];
@ -200,15 +266,28 @@ notifications = {
* @returns {Promise}
*/
destroyAll: function destroyAll(options) {
return canThis(options.context).destroy.notification().then(function () {
notificationsStore = [];
return notificationsStore;
}, function (err) {
return Promise.reject(new common.errors.NoPermissionError({
err: err,
context: common.i18n.t('errors.api.notifications.noPermissionToDestroyNotif')
}));
});
return canThis(options.context).destroy.notification()
.then(function () {
return _private.fetchAllNotifications()
.then(function (allNotifications) {
_.each(allNotifications, function (notification) {
notification.seen = true;
});
return SettingsAPI.edit({
settings: [{
key: 'notifications',
value: allNotifications
}]
}, internalContext);
})
.return();
}, function (err) {
return Promise.reject(new common.errors.NoPermissionError({
err: err,
context: common.i18n.t('errors.api.notifications.noPermissionToDestroyNotif')
}));
});
}
};

View File

@ -4,6 +4,10 @@
"host": "127.0.0.1",
"port": 2368
},
"updateCheck": {
"url": "https://updates.ghost.org",
"forceUpdate": false
},
"privacy": false,
"useMinFiles": true,
"paths": {

View File

@ -0,0 +1,67 @@
'use strict';
const _ = require('lodash'),
models = require('../../../../models'),
common = require('../../../../lib/common');
module.exports.config = {
transaction: true
};
module.exports.up = function removeSettingKeys(options) {
let localOptions = _.merge({
context: {internal: true}
}, options);
return models.Settings.findOne({key: 'display_update_notification'}, localOptions)
.then(function (settingsModel) {
if (!settingsModel) {
common.logging.warn('Deleted Settings Key `display_update_notification`.');
return;
}
common.logging.info('Deleted Settings Key `display_update_notification`.');
return models.Settings.destroy({id: settingsModel.id}, localOptions);
})
.then(function () {
return models.Settings.findOne({key: 'seen_notifications'}, localOptions);
})
.then(function (settingsModel) {
if (!settingsModel) {
common.logging.warn('Deleted Settings Key `seen_notifications`.');
return;
}
common.logging.info('Deleted Settings Key `seen_notifications`.');
return models.Settings.destroy({id: settingsModel.id}, localOptions);
});
};
module.exports.down = function addSettingsKeys(options) {
let localOptions = _.merge({
context: {internal: true}
}, options);
return models.Settings.findOne({key: 'display_update_notification'}, localOptions)
.then(function (settingsModel) {
if (settingsModel) {
common.logging.warn('Added Settings Key `display_update_notification`.');
return;
}
common.logging.info('Added Settings Key `display_update_notification`.');
return models.Settings.forge({key: 'display_update_notification'}).save(null, localOptions);
})
.then(function () {
return models.Settings.findOne({key: 'seen_notifications'}, localOptions);
})
.then(function (settingsModel) {
if (settingsModel) {
common.logging.warn('Added Settings Key `seen_notifications`.');
return;
}
common.logging.info('Added Settings Key `seen_notifications`.');
return models.Settings.forge({key: 'seen_notifications', value: '[]'}).save([], localOptions);
});
};

View File

@ -6,10 +6,7 @@
"next_update_check": {
"defaultValue": null
},
"display_update_notification": {
"defaultValue": null
},
"seen_notifications": {
"notifications": {
"defaultValue": "[]"
}
},

View File

@ -120,3 +120,39 @@ common.events.on('settings.active_timezone.edited', function (settingModel, opti
});
});
});
/**
* Remove all notifications, which are seen, older than 3 months.
* No transaction, because notifications are not sensitive and we would have to add `forUpdate`
* to the settings model to create real lock.
*/
common.events.on('settings.notifications.edited', function (settingModel) {
var allNotifications = JSON.parse(settingModel.attributes.value || []),
options = {context: {internal: true}},
skip = true;
allNotifications = allNotifications.filter(function (notification) {
// Do not delete the release notification
if (notification.hasOwnProperty('custom') && !notification.custom) {
return true;
}
if (notification.seen && moment().diff(moment(notification.addedAt), 'month') > 2) {
skip = false;
return false;
}
return true;
});
if (skip) {
return;
}
return models.Settings.edit({
key: 'notifications',
value: JSON.stringify(allNotifications)
}, options).catch(function (err) {
common.errors.logError(err);
});
});

View File

@ -544,9 +544,6 @@
}
},
"notices": {
"controllers": {
"newVersionAvailable": "Ghost {version} is available! Hot Damn. {link} to upgrade."
},
"index": {
"welcomeToGhost": "Welcome to Ghost.",
"youAreRunningUnderEnvironment": "You're running under the <strong> {environment} </strong> environment.",

View File

@ -1,3 +1,5 @@
'use strict';
// # Update Checking Service
//
// Makes a request to Ghost.org to check if there is a new version of Ghost available.
@ -20,31 +22,34 @@
// - theme - name of the currently active theme
// - apps - names of any active apps
var crypto = require('crypto'),
const crypto = require('crypto'),
exec = require('child_process').exec,
moment = require('moment'),
semver = require('semver'),
Promise = require('bluebird'),
_ = require('lodash'),
url = require('url'),
debug = require('ghost-ignition').debug('update-check'),
api = require('./api'),
config = require('./config'),
urlService = require('./services/url'),
common = require('./lib/common'),
request = require('./lib/request'),
currentVersion = require('./lib/ghost-version').full,
ghostVersion = require('./lib/ghost-version'),
internal = {context: {internal: true}},
checkEndpoint = config.get('updateCheckUrl') || 'https://updates.ghost.org';
allowedCheckEnvironments = ['development', 'production'];
function nextCheckTimestamp() {
var now = Math.round(new Date().getTime() / 1000);
return now + (24 * 3600);
}
function updateCheckError(err) {
if (err.response && err.response.body && typeof err.response.body === 'object') {
err = common.errors.utils.deserialize(err.response.body);
}
api.settings.edit(
{settings: [{key: 'next_update_check', value: Math.round(Date.now() / 1000 + 24 * 3600)}]},
internal
);
api.settings.edit({
settings: [{
key: 'next_update_check',
value: nextCheckTimestamp()
}]
}, internal);
err.context = common.i18n.t('errors.updateCheck.checkingForUpdatesFailed.error');
err.help = common.i18n.t('errors.updateCheck.checkingForUpdatesFailed.help', {url: 'https://docs.ghost.org/v1'});
@ -53,40 +58,36 @@ function updateCheckError(err) {
/**
* If the custom message is intended for current version, create and store a custom notification.
* @param {Object} message {id: uuid, version: '0.9.x', content: '' }
* @param {Object} notification
* @return {*|Promise}
*/
function createCustomNotification(message) {
if (!semver.satisfies(currentVersion, message.version)) {
function createCustomNotification(notification) {
if (!notification) {
return Promise.resolve();
}
var notification = {
status: 'alert',
type: 'info',
custom: true,
uuid: message.id,
dismissible: true,
return Promise.each(notification.messages, function (message) {
let toAdd = {
custom: !!notification.custom,
createdAt: moment(notification.created_at).toDate(),
status: message.status || 'alert',
type: message.type || 'info',
id: message.id,
dismissible: message.hasOwnProperty('dismissible') ? message.dismissible : true,
top: !!message.top,
message: message.content
},
getAllNotifications = api.notifications.browse({context: {internal: true}}),
getSeenNotifications = api.settings.read(_.extend({key: 'seen_notifications'}, internal));
};
return Promise.join(getAllNotifications, getSeenNotifications, function joined(all, seen) {
var isSeen = _.includes(JSON.parse(seen.settings[0].value || []), notification.id),
isDuplicate = _.some(all.notifications, {message: notification.message});
if (!isSeen && !isDuplicate) {
return api.notifications.add({notifications: [notification]}, {context: {internal: true}});
}
debug('Add Custom Notification', toAdd);
return api.notifications.add({notifications: [toAdd]}, {context: {internal: true}});
});
}
function updateCheckData() {
var data = {},
let data = {},
mailConfig = config.get('mail');
data.ghost_version = currentVersion;
data.ghost_version = ghostVersion.original;
data.node_version = process.versions.node;
data.env = config.get('env');
data.database_type = config.get('database').client;
@ -134,19 +135,53 @@ function updateCheckData() {
}).catch(updateCheckError);
}
/**
* With the privacy setting `useUpdateCheck` you can control if you want to expose data from your blog to the
* Update Check Service. Enabled or disabled, you will receive the latest notification available from the service.
*/
function updateCheckRequest() {
return updateCheckData()
.then(function then(reqData) {
return request(checkEndpoint, {
json: true,
body: reqData,
headers: {
'Content-Length': Buffer.byteLength(JSON.stringify(reqData))
let reqObj = {
timeout: 1000,
headers: {}
},
timeout: 1000
}).then(function (response) {
return response.body;
});
checkEndpoint = config.get('updateCheck:url'),
checkMethod = config.isPrivacyDisabled('useUpdateCheck') ? 'GET' : 'POST';
if (checkMethod === 'POST') {
reqObj.json = true;
reqObj.body = reqData;
reqObj.headers['Content-Length'] = Buffer.byteLength(JSON.stringify(reqData));
reqObj.headers['Content-Type'] = 'application/json';
} else {
reqObj.json = true;
reqObj.query = {
ghost_version: reqData.ghost_version
};
}
debug('Request Update Check Service', checkEndpoint);
return request(checkEndpoint, reqObj)
.then(function (response) {
return response.body;
})
.catch(function (err) {
// CASE: no notifications available, ignore
if (err.statusCode === 404) {
return {
next_check: nextCheckTimestamp(),
notifications: []
};
}
if (err.response && err.response.body && typeof err.response.body === 'object') {
err = common.errors.utils.deserialize(err.response.body);
}
throw err;
});
});
}
@ -154,65 +189,84 @@ function updateCheckRequest() {
* Handles the response from the update check
* Does three things with the information received:
* 1. Updates the time we can next make a check
* 2. Checks if the version in the response is new, and updates the notification setting
* 3. Create custom notifications is response from UpdateCheck as "messages" array which has the following structure:
* 2. Create custom notifications is response from UpdateCheck as "messages" array which has the following structure:
*
* "messages": [{
* "id": ed9dc38c-73e5-4d72-a741-22b11f6e151a,
* "version": "0.5.x",
* "content": "<p>Hey there! 0.6 is available, visit <a href=\"https://ghost.org/download\">Ghost.org</a> to grab your copy now<!/p>"
* "content": "<p>Hey there! 0.6 is available, visit <a href=\"https://ghost.org/download\">Ghost.org</a> to grab your copy now<!/p>",
* "dismissible": true | false,
* "top": true | false
* ]}
*
* Example for grouped custom notifications in config:
*
* notificationGroups: ['migration', 'something']
*
* 'all' is a reserved name for general custom notifications.
*
* @param {Object} response
* @return {Promise}
*/
function updateCheckResponse(response) {
return Promise.all([
api.settings.edit({settings: [{key: 'next_update_check', value: response.next_check}]}, internal),
api.settings.edit({settings: [{key: 'display_update_notification', value: response.version}]}, internal)
]).then(function () {
var messages = response.messages || [];
let notifications = [],
notificationGroups = (config.get('notificationGroups') || []).concat(['all']);
/**
* by default the update check service returns messages: []
* but the latest release version get's stored anyway, because we adding the `display_update_notification` ^
*/
return Promise.map(messages, createCustomNotification);
});
debug('Notification Groups', notificationGroups);
debug('Response Update Check Service', response);
return api.settings.edit({settings: [{key: 'next_update_check', value: response.next_check}]}, internal)
.then(function () {
// CASE: Update Check Service returns multiple notifications.
if (_.isArray(response)) {
notifications = response;
} else if ((response.hasOwnProperty('notifications') && _.isArray(response.notifications))) {
notifications = response.notifications;
} else {
notifications = [response];
}
// CASE: Hook into received notifications and decide whether you are allowed to receive custom group messages.
if (notificationGroups.length) {
notifications = notifications.filter(function (notification) {
if (!notification.custom) {
return true;
}
return _.includes(notificationGroups.map(function (groupIdentifier) {
if (notification.version.match(new RegExp(groupIdentifier))) {
return true;
}
return false;
}), true) === true;
});
}
return Promise.each(notifications, createCustomNotification);
});
}
function updateCheck() {
if (config.isPrivacyDisabled('useUpdateCheck')) {
// CASE: The check will not happen if your NODE_ENV is not in the allowed defined environments.
if (_.indexOf(allowedCheckEnvironments, process.env.NODE_ENV) === -1) {
return Promise.resolve();
} else {
return api.settings.read(_.extend({key: 'next_update_check'}, internal)).then(function then(result) {
}
return api.settings.read(_.extend({key: 'next_update_check'}, internal))
.then(function then(result) {
var nextUpdateCheck = result.settings[0];
if (nextUpdateCheck && nextUpdateCheck.value && nextUpdateCheck.value > moment().unix()) {
// It's not time to check yet
return; // eslint-disable-line no-useless-return
} else {
// We need to do a check
return updateCheckRequest()
.then(updateCheckResponse)
.catch(updateCheckError);
// CASE: Next update check should happen now?
if (!config.get('updateCheck:forceUpdate') && nextUpdateCheck && nextUpdateCheck.value && nextUpdateCheck.value > moment().unix()) {
return Promise.resolve();
}
}).catch(updateCheckError);
}
}
function showUpdateNotification() {
return api.settings.read(_.extend({key: 'display_update_notification'}, internal)).then(function then(response) {
var display = response.settings[0];
// @TODO: We only show minor/major releases. This is a temporary fix. #5071 is coming soon.
if (display && display.value && currentVersion && semver.gt(display.value, currentVersion) && semver.patch(display.value) === 0) {
return display.value;
}
return false;
});
return updateCheckRequest()
.then(updateCheckResponse)
.catch(updateCheckError);
})
.catch(updateCheckError);
}
module.exports = updateCheck;
module.exports.showUpdateNotification = showUpdateNotification;

View File

@ -1,8 +1,8 @@
var debug = require('ghost-ignition').debug('admin:controller'),
_ = require('lodash'),
'use strict';
const debug = require('ghost-ignition').debug('admin:controller'),
path = require('path'),
config = require('../../config'),
api = require('../../api'),
updateCheck = require('../../update-check'),
common = require('../../lib/common');
@ -12,36 +12,14 @@ var debug = require('ghost-ignition').debug('admin:controller'),
module.exports = function adminController(req, res) {
debug('index called');
updateCheck().then(function then() {
return updateCheck.showUpdateNotification();
}).then(function then(updateVersion) {
if (!updateVersion) {
return;
}
var notification = {
status: 'alert',
type: 'info',
location: 'upgrade.new-version-available',
dismissible: false,
message: common.i18n.t('notices.controllers.newVersionAvailable',
{
version: updateVersion,
link: '<a href="https://docs.ghost.org/docs/upgrade" target="_blank">Click here</a>'
})
};
return api.notifications.browse({context: {internal: true}}).then(function then(results) {
if (!_.some(results.notifications, {message: notification.message})) {
return api.notifications.add({notifications: [notification]}, {context: {internal: true}});
}
// run in background, don't block the admin rendering
updateCheck()
.catch(function onError(err) {
common.logging.error(err);
});
}).finally(function noMatterWhat() {
var defaultTemplate = config.get('env') === 'production' ? 'default-prod.html' : 'default.html',
templatePath = path.resolve(config.get('paths').adminViews, defaultTemplate);
res.sendFile(templatePath);
}).catch(function (err) {
common.logging.error(err);
});
let defaultTemplate = config.get('env') === 'production' ? 'default-prod.html' : 'default.html',
templatePath = path.resolve(config.get('paths').adminViews, defaultTemplate);
res.sendFile(templatePath);
};

View File

@ -1,9 +1,9 @@
var should = require('should'),
testUtils = require('../../utils'),
_ = require('lodash'),
uuid = require('uuid'),
ObjectId = require('bson-objectid'),
NotificationsAPI = require('../../../server/api/notifications'),
SettingsAPI = require('../../../server/api/settings');
testUtils = require('../../utils'),
NotificationsAPI = require('../../../server/api/notifications');
describe('Notifications API', function () {
// Keep the DB clean
@ -13,10 +13,6 @@ describe('Notifications API', function () {
should.exist(NotificationsAPI);
after(function () {
return NotificationsAPI.destroyAll(testUtils.context.internal);
});
it('can add, adds defaults (internal)', function (done) {
var msg = {
type: 'info',
@ -54,6 +50,7 @@ describe('Notifications API', function () {
notification.dismissible.should.be.true();
should.exist(notification.location);
notification.location.should.equal('bottom');
notification.id.should.be.a.String();
done();
}).catch(done);
@ -74,7 +71,7 @@ describe('Notifications API', function () {
should.exist(result.notifications);
notification = result.notifications[0];
notification.id.should.not.equal(msg.id);
notification.id.should.be.a.String();
should.exist(notification.status);
notification.status.should.equal('alert');
@ -82,6 +79,32 @@ describe('Notifications API', function () {
}).catch(done);
});
it('duplicates', function (done) {
var customNotification1 = {
status: 'alert',
type: 'info',
location: 'test.to-be-deleted1',
custom: true,
id: uuid.v1(),
dismissible: true,
message: 'Hello, this is dog number 1'
};
NotificationsAPI
.add({notifications: [customNotification1]}, testUtils.context.internal)
.then(function () {
return NotificationsAPI.add({notifications: [customNotification1]}, testUtils.context.internal);
})
.then(function () {
return NotificationsAPI.browse(testUtils.context.internal);
})
.then(function (response) {
response.notifications.length.should.eql(1);
done();
})
.catch(done);
});
it('can browse (internal)', function (done) {
var msg = {
type: 'error', // this can be 'error', 'success', 'warn' and 'info'
@ -114,6 +137,40 @@ describe('Notifications API', function () {
});
});
it('receive correct order', function (done) {
var customNotification1 = {
status: 'alert',
type: 'info',
custom: true,
id: uuid.v1(),
dismissible: true,
message: '1'
}, customNotification2 = {
status: 'alert',
type: 'info',
custom: true,
id: uuid.v1(),
dismissible: true,
message: '2'
};
NotificationsAPI
.add({notifications: [customNotification1]}, testUtils.context.internal)
.then(function () {
return NotificationsAPI.add({notifications: [customNotification2]}, testUtils.context.internal);
})
.then(function () {
return NotificationsAPI.browse(testUtils.context.internal);
})
.then(function (response) {
response.notifications.length.should.eql(2);
response.notifications[0].message.should.eql('2');
response.notifications[1].message.should.eql('1');
done();
})
.catch(done);
});
it('can destroy (internal)', function (done) {
var msg = {
type: 'error',
@ -123,13 +180,13 @@ describe('Notifications API', function () {
NotificationsAPI.add({notifications: [msg]}, testUtils.context.internal).then(function (result) {
var notification = result.notifications[0];
NotificationsAPI.destroy(
_.extend({}, testUtils.context.internal, {id: notification.id})
).then(function (result) {
should.not.exist(result);
done();
}).catch(done);
NotificationsAPI
.destroy(_.extend({}, testUtils.context.internal, {id: notification.id}))
.then(function (result) {
should.not.exist(result);
done();
})
.catch(done);
});
});
@ -142,22 +199,23 @@ describe('Notifications API', function () {
NotificationsAPI.add({notifications: [msg]}, testUtils.context.internal).then(function (result) {
var notification = result.notifications[0];
NotificationsAPI.destroy(
_.extend({}, testUtils.context.owner, {id: notification.id})
).then(function (result) {
should.not.exist(result);
done();
}).catch(done);
NotificationsAPI
.destroy(_.extend({}, testUtils.context.owner, {id: notification.id}))
.then(function (result) {
should.not.exist(result);
done();
})
.catch(done);
});
});
it('can destroy a custom notification and add its uuid to seen_notifications (owner)', function (done) {
it('ensure notification get\'s removed', function (done) {
var customNotification = {
status: 'alert',
type: 'info',
location: 'test.to-be-deleted',
custom: true,
id: uuid.v1(),
dismissible: true,
message: 'Hello, this is dog number 4'
};
@ -165,16 +223,68 @@ describe('Notifications API', function () {
NotificationsAPI.add({notifications: [customNotification]}, testUtils.context.internal).then(function (result) {
var notification = result.notifications[0];
NotificationsAPI.destroy(
_.extend({}, testUtils.context.internal, {id: notification.id})
).then(function () {
return SettingsAPI.read(_.extend({key: 'seen_notifications'}, testUtils.context.internal));
}).then(function (response) {
should.exist(response);
response.settings[0].value.should.containEql(notification.id);
done();
}).catch(done);
return NotificationsAPI.browse(testUtils.context.internal)
.then(function (response) {
response.notifications.length.should.eql(1);
return NotificationsAPI.destroy(_.extend({}, testUtils.context.internal, {id: notification.id}));
})
.then(function () {
return NotificationsAPI.browse(testUtils.context.internal);
})
.then(function (response) {
response.notifications.length.should.eql(0);
done();
})
.catch(done);
});
});
it('destroy unknown id', function (done) {
NotificationsAPI
.destroy(_.extend({}, testUtils.context.internal, {id: 1}))
.then(function () {
done(new Error('Expected notification error.'));
})
.catch(function (err) {
err.statusCode.should.eql(404);
done();
});
});
it('destroy all', function (done) {
var customNotification1 = {
status: 'alert',
type: 'info',
location: 'test.to-be-deleted1',
custom: true,
id: uuid.v1(),
dismissible: true,
message: 'Hello, this is dog number 1'
}, customNotification2 = {
status: 'alert',
type: 'info',
location: 'test.to-be-deleted2',
custom: true,
id: uuid.v1(),
dismissible: true,
message: 'Hello, this is dog number 2'
};
NotificationsAPI
.add({notifications: [customNotification1]}, testUtils.context.internal)
.then(function () {
return NotificationsAPI.add({notifications: [customNotification2]}, testUtils.context.internal);
})
.then(function () {
return NotificationsAPI.destroyAll(testUtils.context.internal);
})
.then(function () {
return NotificationsAPI.browse(testUtils.context.internal);
})
.then(function (response) {
response.notifications.length.should.eql(0);
done();
})
.catch(done);
});
});

View File

@ -1517,19 +1517,21 @@ describe('Import (new test structure)', function () {
users[1].profile_image.should.eql(exportData.data.users[0].image);
// Check feature image is correctly mapped for a tag
tags[0].feature_image.should.eql(exportData.data.tags[0].image);
// Check logo image is correctly mapped for a blog
settings[6].key.should.eql('logo');
settings[6].value.should.eql('/content/images/2017/05/bloglogo.jpeg');
settings[5].key.should.eql('logo');
settings[5].value.should.eql('/content/images/2017/05/bloglogo.jpeg');
// Check cover image is correctly mapped for a blog
settings[7].key.should.eql('cover_image');
settings[7].value.should.eql('/content/images/2017/05/blogcover.jpeg');
settings[6].key.should.eql('cover_image');
settings[6].value.should.eql('/content/images/2017/05/blogcover.jpeg');
// Check default settings locale is not overwritten by defaultLang
settings[9].key.should.eql('default_locale');
settings[9].value.should.eql('en');
settings[8].key.should.eql('default_locale');
settings[8].value.should.eql('en');
settings[18].key.should.eql('labs');
settings[18].value.should.eql('{"publicAPI":true}');
settings[17].key.should.eql('labs');
settings[17].value.should.eql('{"publicAPI":true}');
// Check post language is null
should(firstPost.locale).equal(null);
@ -1600,15 +1602,15 @@ describe('Import (new test structure)', function () {
// Check feature image is correctly mapped for a tag
tags[0].feature_image.should.eql(exportData.data.tags[0].image);
// Check logo image is correctly mapped for a blog
settings[6].key.should.eql('logo');
settings[6].value.should.eql('/content/images/2017/05/bloglogo.jpeg');
settings[5].key.should.eql('logo');
settings[5].value.should.eql('/content/images/2017/05/bloglogo.jpeg');
// Check cover image is correctly mapped for a blog
settings[7].key.should.eql('cover_image');
settings[7].value.should.eql('/content/images/2017/05/blogcover.jpeg');
settings[6].key.should.eql('cover_image');
settings[6].value.should.eql('/content/images/2017/05/blogcover.jpeg');
// Check default settings locale is not overwritten by defaultLang
settings[9].key.should.eql('default_locale');
settings[9].value.should.eql('en');
settings[8].key.should.eql('default_locale');
settings[8].value.should.eql('en');
// Check post language is set to null
should(firstPost.locale).equal(null);

View File

@ -22,7 +22,7 @@ describe('Models: listeners', function () {
};
before(testUtils.teardown);
beforeEach(testUtils.setup('owner', 'user-token:0'));
beforeEach(testUtils.setup('owner', 'user-token:0', 'settings'));
beforeEach(function () {
sandbox.stub(common.events, 'on').callsFake(function (eventName, callback) {
@ -361,4 +361,70 @@ describe('Models: listeners', function () {
})();
});
});
describe('on notifications changed', function () {
it('nothing to delete', function (done) {
var notifications = JSON.stringify([
{
addedAt: moment().subtract(1, 'week').format(),
seen: true
},
{
addedAt: moment().subtract(2, 'month').format(),
seen: true
},
{
addedAt: moment().subtract(1, 'day').format(),
seen: false
}
]);
models.Settings.edit({key: 'notifications', value: notifications}, testUtils.context.internal)
.then(function () {
eventsToRemember['settings.notifications.edited']({
attributes: {
value: notifications
}
});
return models.Settings.findOne({key: 'notifications'}, testUtils.context.internal);
}).then(function (model) {
JSON.parse(model.get('value')).length.should.eql(3);
done();
}).catch(done);
});
it('expect deletion', function (done) {
var notifications = JSON.stringify([
{
content: 'keep-1',
addedAt: moment().subtract(1, 'week').toDate(),
seen: true
},
{
content: 'delete-me',
addedAt: moment().subtract(3, 'month').toDate(),
seen: true
},
{
content: 'keep-2',
addedAt: moment().subtract(1, 'day').toDate(),
seen: false
}
]);
models.Settings.edit({key: 'notifications', value: notifications}, testUtils.context.internal)
.then(function () {
setTimeout(function () {
return models.Settings.findOne({key: 'notifications'}, testUtils.context.internal)
.then(function (model) {
JSON.parse(model.get('value')).length.should.eql(2);
done();
})
.catch(done);
}, 1000);
})
.catch(done);
});
});
});

View File

@ -1,20 +1,102 @@
var should = require('should'),
_ = require('lodash'),
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'),
settingsCache = require('../../server/services/settings/cache'),
NotificationsAPI = require('../../server/api/notifications');
SettingsAPI = require('../../server/api/settings'),
NotificationsAPI = require('../../server/api/notifications'),
sandbox = sinon.sandbox.create();
describe('Update Check', function () {
after(function () {
return NotificationsAPI.destroyAll(testUtils.context.internal);
beforeEach(function () {
updateCheck = rewire('../../server/update-check');
});
describe('Reporting to UpdateCheck', function () {
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);
});
@ -52,156 +134,512 @@ describe('Update Check', function () {
});
});
describe('Custom Notifications', function () {
describe('fn: createCustomNotification', function () {
var currentVersionOrig;
before(function () {
currentVersionOrig = updateCheck.__get__('currentVersion');
updateCheck.__set__('currentVersion', '0.9.0');
currentVersionOrig = updateCheck.__get__('ghostVersion.original');
updateCheck.__set__('ghostVersion.original', '0.9.0');
});
after(function () {
updateCheck.__set__('currentVersion', currentVersionOrig);
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 custom notification for target version', function (done) {
it('should create a release notification for target version', function (done) {
var createCustomNotification = updateCheck.__get__('createCustomNotification'),
message = {
id: uuid.v4(),
version: '0.9.x',
content: '<p>Hey there! This is for 0.9.0 version</p>'
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(message).then(function () {
createCustomNotification(notification).then(function () {
return NotificationsAPI.browse(testUtils.context.internal);
}).then(function (results) {
should.exist(results);
should.exist(results.notifications);
results.notifications.length.should.be.above(0);
should.exist(_.find(results.notifications, {uuid: message.id}));
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 not create notifications meant for other versions', function (done) {
it('should create a custom notification', function (done) {
var createCustomNotification = updateCheck.__get__('createCustomNotification'),
message = {
id: uuid.v4(),
version: '0.5.x',
content: '<p>Hey there! This is for 0.5.0 version</p>'
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(message).then(function () {
createCustomNotification(notification).then(function () {
return NotificationsAPI.browse(testUtils.context.internal);
}).then(function (results) {
should.not.exist(_.find(results.notifications, {uuid: message.id}));
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('Show notification', function () {
var currentVersionOrig;
before(function () {
currentVersionOrig = updateCheck.__get__('currentVersion');
});
after(function () {
updateCheck.__set__('currentVersion', currentVersionOrig);
});
beforeEach(testUtils.setup('settings', 'perms:setting', 'perms:notification', 'perms:init'));
describe('fn: updateCheckResponse', function () {
beforeEach(testUtils.setup('settings', 'perms:setting', 'perms:init'));
afterEach(testUtils.teardown);
it('should show update notification', function (done) {
var showUpdateNotification = updateCheck.__get__('showUpdateNotification');
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__('currentVersion', '1.7.1');
settingsCache.set('display_update_notification', {value: '1.9.0'});
updateCheck.__set__('createCustomNotification', createNotificationSpy);
showUpdateNotification()
.then(function (result) {
result.should.eql('1.9.0');
updateCheckResponse({version: '0.11.12', messages: [message]})
.then(function () {
createNotificationSpy.callCount.should.eql(1);
done();
})
.catch(done);
});
it('should show update notification', function (done) {
var showUpdateNotification = updateCheck.__get__('showUpdateNotification');
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__('currentVersion', '1.7.1');
settingsCache.set('display_update_notification', {value: '2.0.0'});
updateCheck.__set__('createCustomNotification', createNotificationSpy);
showUpdateNotification()
.then(function (result) {
result.should.eql('2.0.0');
updateCheckResponse(notifications)
.then(function () {
createNotificationSpy.callCount.should.eql(2);
done();
})
.catch(done);
});
it('should not show update notification: latest minor release is not greater than your Ghost version', function (done) {
var showUpdateNotification = updateCheck.__get__('showUpdateNotification');
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__('currentVersion', '1.9.0');
settingsCache.set('display_update_notification', {value: '1.9.0'});
updateCheck.__set__('createCustomNotification', createNotificationSpy);
showUpdateNotification()
.then(function (result) {
result.should.eql(false);
updateCheckResponse(notifications)
.then(function () {
createNotificationSpy.callCount.should.eql(2);
done();
})
.catch(done);
});
it('should not show update notification: latest minor release is not greater than your Ghost version', function (done) {
var showUpdateNotification = updateCheck.__get__('showUpdateNotification');
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__('currentVersion', '1.9.1');
settingsCache.set('display_update_notification', {value: '1.9.1'});
updateCheck.__set__('createCustomNotification', createNotificationSpy);
showUpdateNotification()
.then(function (result) {
result.should.eql(false);
configUtils.set({notificationGroups: ['migration']});
updateCheckResponse(notifications)
.then(function () {
createNotificationSpy.callCount.should.eql(3);
done();
})
.catch(done);
});
it('should not show update notification: latest release is a patch', function (done) {
var showUpdateNotification = updateCheck.__get__('showUpdateNotification');
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__('currentVersion', '1.9.0');
settingsCache.set('display_update_notification', {value: '1.9.1'});
updateCheck.__set__('createCustomNotification', createNotificationSpy);
showUpdateNotification()
.then(function (result) {
result.should.eql(false);
configUtils.set({notificationGroups: ['something']});
updateCheckResponse(notifications)
.then(function () {
createNotificationSpy.callCount.should.eql(1);
done();
})
.catch(done);
});
it('should not show update notification: latest release is a patch', function (done) {
var showUpdateNotification = updateCheck.__get__('showUpdateNotification');
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__('currentVersion', '1.9.1');
settingsCache.set('display_update_notification', {value: '1.9.0'});
updateCheck.__set__('createCustomNotification', createNotificationSpy);
showUpdateNotification()
.then(function (result) {
result.should.eql(false);
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');
});
});
});
});

View File

@ -1,35 +0,0 @@
var should = require('should'), // jshint ignore:line
rewire = require('rewire'),
NotificationAPI = rewire('../../../server/api/notifications');
describe('UNIT: Notification API', function () {
it('ensure non duplicates', function (done) {
var options = {context: {internal: true}},
notifications = [{
type: 'info',
message: 'Hello, this is dog'
}],
notificationStore = NotificationAPI.__get__('notificationsStore');
NotificationAPI.add({notifications: notifications}, options)
.then(function () {
notificationStore.length.should.eql(1);
return NotificationAPI.add({notifications: notifications}, options);
})
.then(function () {
notificationStore.length.should.eql(1);
notifications.push({
type: 'info',
message: 'Hello, this is cat'
});
return NotificationAPI.add({notifications: notifications}, options);
})
.then(function () {
notificationStore.length.should.eql(2);
done();
})
.catch(done);
});
});