mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-03 23:00:14 -05:00
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:
parent
f671f9d2c9
commit
5b77f052d9
14 changed files with 1161 additions and 366 deletions
|
@ -11,9 +11,11 @@ Some official services for Ghost are enabled by default. These services connect
|
||||||
|
|
||||||
### Automatic Update Checks
|
### 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
|
## Third Party Services
|
||||||
|
|
|
@ -1,17 +1,48 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
// # Notifications API
|
// # Notifications API
|
||||||
// RESTful API for creating notifications
|
// RESTful API for creating notifications
|
||||||
var Promise = require('bluebird'),
|
|
||||||
|
const Promise = require('bluebird'),
|
||||||
_ = require('lodash'),
|
_ = require('lodash'),
|
||||||
|
moment = require('moment'),
|
||||||
ObjectId = require('bson-objectid'),
|
ObjectId = require('bson-objectid'),
|
||||||
pipeline = require('../lib/promise/pipeline'),
|
pipeline = require('../lib/promise/pipeline'),
|
||||||
permissions = require('../services/permissions'),
|
permissions = require('../services/permissions'),
|
||||||
canThis = permissions.canThis,
|
|
||||||
localUtils = require('./utils'),
|
localUtils = require('./utils'),
|
||||||
common = require('../lib/common'),
|
common = require('../lib/common'),
|
||||||
settingsAPI = require('./settings'),
|
SettingsAPI = require('./settings'),
|
||||||
// Holds the persistent notifications
|
internalContext = {context: {internal: true}},
|
||||||
notificationsStore = [],
|
canThis = permissions.canThis;
|
||||||
notifications;
|
|
||||||
|
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
|
* ## Notification API Methods
|
||||||
|
@ -27,9 +58,20 @@ notifications = {
|
||||||
*/
|
*/
|
||||||
browse: function browse(options) {
|
browse: function browse(options) {
|
||||||
return canThis(options.context).browse.notification().then(function () {
|
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 () {
|
}, 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 canThis(options.context).add.notification().then(function () {
|
||||||
return options;
|
return options;
|
||||||
}, function () {
|
}, 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
|
* @returns {Object} options
|
||||||
*/
|
*/
|
||||||
function saveNotifications(options) {
|
function saveNotifications(options) {
|
||||||
var defaults = {
|
let defaults = {
|
||||||
dismissible: true,
|
dismissible: true,
|
||||||
location: 'bottom',
|
location: 'bottom',
|
||||||
status: 'alert'
|
status: 'alert',
|
||||||
},
|
|
||||||
addedNotifications = [], existingNotification;
|
|
||||||
|
|
||||||
_.each(options.data.notifications, function (notification) {
|
|
||||||
notification = _.assign(defaults, notification, {
|
|
||||||
id: ObjectId.generate()
|
id: ObjectId.generate()
|
||||||
});
|
},
|
||||||
|
overrides = {
|
||||||
|
seen: false,
|
||||||
|
addedAt: moment().toDate()
|
||||||
|
},
|
||||||
|
notificationsToCheck = options.data.notifications,
|
||||||
|
addedNotifications = [];
|
||||||
|
|
||||||
existingNotification = _.find(notificationsStore, {message: notification.message});
|
return _private.fetchAllNotifications()
|
||||||
|
.then(function (allNotifications) {
|
||||||
|
_.each(notificationsToCheck, function (notification) {
|
||||||
|
let isDuplicate = _.find(allNotifications, {id: notification.id});
|
||||||
|
|
||||||
if (!existingNotification) {
|
if (!isDuplicate) {
|
||||||
notificationsStore.push(notification);
|
addedNotifications.push(_.merge({}, defaults, notification, overrides));
|
||||||
addedNotifications.push(notification);
|
|
||||||
} else {
|
|
||||||
addedNotifications.push(existingNotification);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
let hasReleaseNotification = _.find(notificationsToCheck, {custom: false});
|
||||||
notifications: addedNotifications
|
|
||||||
};
|
// 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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks = [
|
tasks = [
|
||||||
|
@ -124,26 +198,7 @@ notifications = {
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
destroy: function destroy(options) {
|
destroy: function destroy(options) {
|
||||||
var tasks;
|
let 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});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ### Handle Permissions
|
* ### Handle Permissions
|
||||||
|
@ -155,36 +210,47 @@ notifications = {
|
||||||
return canThis(options.context).destroy.notification().then(function () {
|
return canThis(options.context).destroy.notification().then(function () {
|
||||||
return options;
|
return options;
|
||||||
}, function () {
|
}, 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) {
|
function destroyNotification(options) {
|
||||||
var notification = _.find(notificationsStore, function (element) {
|
return _private.fetchAllNotifications()
|
||||||
return element.id === options.id;
|
.then(function (allNotifications) {
|
||||||
});
|
let notificationToMarkAsSeen = _.find(allNotifications, {id: options.id}),
|
||||||
|
notificationToMarkAsSeenIndex = _.findIndex(allNotifications, {id: options.id});
|
||||||
|
|
||||||
if (notification && !notification.dismissible) {
|
if (notificationToMarkAsSeenIndex > -1 && !notificationToMarkAsSeen.dismissible) {
|
||||||
return Promise.reject(
|
return Promise.reject(new common.errors.NoPermissionError({
|
||||||
new common.errors.NoPermissionError({message: common.i18n.t('errors.api.notifications.noPermissionToDismissNotif')})
|
message: common.i18n.t('errors.api.notifications.noPermissionToDismissNotif')
|
||||||
);
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!notification) {
|
if (notificationToMarkAsSeenIndex < 0) {
|
||||||
return Promise.reject(new common.errors.NotFoundError({message: common.i18n.t('errors.api.notifications.notificationDoesNotExist')}));
|
return Promise.reject(new common.errors.NotFoundError({
|
||||||
|
message: common.i18n.t('errors.api.notifications.notificationDoesNotExist')
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationsStore = _.reject(notificationsStore, function (element) {
|
if (notificationToMarkAsSeen.seen) {
|
||||||
return element.id === options.id;
|
return Promise.resolve();
|
||||||
});
|
|
||||||
|
|
||||||
if (notification.custom) {
|
|
||||||
return markAsSeen(notification);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
allNotifications[notificationToMarkAsSeenIndex].seen = true;
|
||||||
|
|
||||||
|
return SettingsAPI.edit({
|
||||||
|
settings: [{
|
||||||
|
key: 'notifications',
|
||||||
|
value: allNotifications
|
||||||
|
}]
|
||||||
|
}, internalContext);
|
||||||
|
})
|
||||||
|
.return();
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks = [
|
tasks = [
|
||||||
localUtils.validate('notifications', {opts: localUtils.idDefaultOptions}),
|
|
||||||
handlePermissions,
|
handlePermissions,
|
||||||
destroyNotification
|
destroyNotification
|
||||||
];
|
];
|
||||||
|
@ -200,9 +266,22 @@ notifications = {
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
destroyAll: function destroyAll(options) {
|
destroyAll: function destroyAll(options) {
|
||||||
return canThis(options.context).destroy.notification().then(function () {
|
return canThis(options.context).destroy.notification()
|
||||||
notificationsStore = [];
|
.then(function () {
|
||||||
return notificationsStore;
|
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) {
|
}, function (err) {
|
||||||
return Promise.reject(new common.errors.NoPermissionError({
|
return Promise.reject(new common.errors.NoPermissionError({
|
||||||
err: err,
|
err: err,
|
||||||
|
|
|
@ -4,6 +4,10 @@
|
||||||
"host": "127.0.0.1",
|
"host": "127.0.0.1",
|
||||||
"port": 2368
|
"port": 2368
|
||||||
},
|
},
|
||||||
|
"updateCheck": {
|
||||||
|
"url": "https://updates.ghost.org",
|
||||||
|
"forceUpdate": false
|
||||||
|
},
|
||||||
"privacy": false,
|
"privacy": false,
|
||||||
"useMinFiles": true,
|
"useMinFiles": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
};
|
|
@ -6,10 +6,7 @@
|
||||||
"next_update_check": {
|
"next_update_check": {
|
||||||
"defaultValue": null
|
"defaultValue": null
|
||||||
},
|
},
|
||||||
"display_update_notification": {
|
"notifications": {
|
||||||
"defaultValue": null
|
|
||||||
},
|
|
||||||
"seen_notifications": {
|
|
||||||
"defaultValue": "[]"
|
"defaultValue": "[]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -544,9 +544,6 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"notices": {
|
"notices": {
|
||||||
"controllers": {
|
|
||||||
"newVersionAvailable": "Ghost {version} is available! Hot Damn. {link} to upgrade."
|
|
||||||
},
|
|
||||||
"index": {
|
"index": {
|
||||||
"welcomeToGhost": "Welcome to Ghost.",
|
"welcomeToGhost": "Welcome to Ghost.",
|
||||||
"youAreRunningUnderEnvironment": "You're running under the <strong> {environment} </strong> environment.",
|
"youAreRunningUnderEnvironment": "You're running under the <strong> {environment} </strong> environment.",
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
// # Update Checking Service
|
// # Update Checking Service
|
||||||
//
|
//
|
||||||
// Makes a request to Ghost.org to check if there is a new version of Ghost available.
|
// 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
|
// - theme - name of the currently active theme
|
||||||
// - apps - names of any active apps
|
// - apps - names of any active apps
|
||||||
|
|
||||||
var crypto = require('crypto'),
|
const crypto = require('crypto'),
|
||||||
exec = require('child_process').exec,
|
exec = require('child_process').exec,
|
||||||
moment = require('moment'),
|
moment = require('moment'),
|
||||||
semver = require('semver'),
|
|
||||||
Promise = require('bluebird'),
|
Promise = require('bluebird'),
|
||||||
_ = require('lodash'),
|
_ = require('lodash'),
|
||||||
url = require('url'),
|
url = require('url'),
|
||||||
|
debug = require('ghost-ignition').debug('update-check'),
|
||||||
api = require('./api'),
|
api = require('./api'),
|
||||||
config = require('./config'),
|
config = require('./config'),
|
||||||
urlService = require('./services/url'),
|
urlService = require('./services/url'),
|
||||||
common = require('./lib/common'),
|
common = require('./lib/common'),
|
||||||
request = require('./lib/request'),
|
request = require('./lib/request'),
|
||||||
currentVersion = require('./lib/ghost-version').full,
|
ghostVersion = require('./lib/ghost-version'),
|
||||||
internal = {context: {internal: true}},
|
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) {
|
function updateCheckError(err) {
|
||||||
if (err.response && err.response.body && typeof err.response.body === 'object') {
|
api.settings.edit({
|
||||||
err = common.errors.utils.deserialize(err.response.body);
|
settings: [{
|
||||||
}
|
key: 'next_update_check',
|
||||||
|
value: nextCheckTimestamp()
|
||||||
api.settings.edit(
|
}]
|
||||||
{settings: [{key: 'next_update_check', value: Math.round(Date.now() / 1000 + 24 * 3600)}]},
|
}, internal);
|
||||||
internal
|
|
||||||
);
|
|
||||||
|
|
||||||
err.context = common.i18n.t('errors.updateCheck.checkingForUpdatesFailed.error');
|
err.context = common.i18n.t('errors.updateCheck.checkingForUpdatesFailed.error');
|
||||||
err.help = common.i18n.t('errors.updateCheck.checkingForUpdatesFailed.help', {url: 'https://docs.ghost.org/v1'});
|
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.
|
* 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}
|
* @return {*|Promise}
|
||||||
*/
|
*/
|
||||||
function createCustomNotification(message) {
|
function createCustomNotification(notification) {
|
||||||
if (!semver.satisfies(currentVersion, message.version)) {
|
if (!notification) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
var notification = {
|
return Promise.each(notification.messages, function (message) {
|
||||||
status: 'alert',
|
let toAdd = {
|
||||||
type: 'info',
|
custom: !!notification.custom,
|
||||||
custom: true,
|
createdAt: moment(notification.created_at).toDate(),
|
||||||
uuid: message.id,
|
status: message.status || 'alert',
|
||||||
dismissible: true,
|
type: message.type || 'info',
|
||||||
|
id: message.id,
|
||||||
|
dismissible: message.hasOwnProperty('dismissible') ? message.dismissible : true,
|
||||||
|
top: !!message.top,
|
||||||
message: message.content
|
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) {
|
debug('Add Custom Notification', toAdd);
|
||||||
var isSeen = _.includes(JSON.parse(seen.settings[0].value || []), notification.id),
|
return api.notifications.add({notifications: [toAdd]}, {context: {internal: true}});
|
||||||
isDuplicate = _.some(all.notifications, {message: notification.message});
|
|
||||||
|
|
||||||
if (!isSeen && !isDuplicate) {
|
|
||||||
return api.notifications.add({notifications: [notification]}, {context: {internal: true}});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCheckData() {
|
function updateCheckData() {
|
||||||
var data = {},
|
let data = {},
|
||||||
mailConfig = config.get('mail');
|
mailConfig = config.get('mail');
|
||||||
|
|
||||||
data.ghost_version = currentVersion;
|
data.ghost_version = ghostVersion.original;
|
||||||
data.node_version = process.versions.node;
|
data.node_version = process.versions.node;
|
||||||
data.env = config.get('env');
|
data.env = config.get('env');
|
||||||
data.database_type = config.get('database').client;
|
data.database_type = config.get('database').client;
|
||||||
|
@ -134,18 +135,52 @@ function updateCheckData() {
|
||||||
}).catch(updateCheckError);
|
}).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() {
|
function updateCheckRequest() {
|
||||||
return updateCheckData()
|
return updateCheckData()
|
||||||
.then(function then(reqData) {
|
.then(function then(reqData) {
|
||||||
return request(checkEndpoint, {
|
let reqObj = {
|
||||||
json: true,
|
timeout: 1000,
|
||||||
body: reqData,
|
headers: {}
|
||||||
headers: {
|
|
||||||
'Content-Length': Buffer.byteLength(JSON.stringify(reqData))
|
|
||||||
},
|
},
|
||||||
timeout: 1000
|
checkEndpoint = config.get('updateCheck:url'),
|
||||||
}).then(function (response) {
|
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;
|
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
|
* Handles the response from the update check
|
||||||
* Does three things with the information received:
|
* Does three things with the information received:
|
||||||
* 1. Updates the time we can next make a check
|
* 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
|
* 2. Create custom notifications is response from UpdateCheck as "messages" array which has the following structure:
|
||||||
* 3. Create custom notifications is response from UpdateCheck as "messages" array which has the following structure:
|
|
||||||
*
|
*
|
||||||
* "messages": [{
|
* "messages": [{
|
||||||
* "id": ed9dc38c-73e5-4d72-a741-22b11f6e151a,
|
* "id": ed9dc38c-73e5-4d72-a741-22b11f6e151a,
|
||||||
* "version": "0.5.x",
|
* "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
|
* @param {Object} response
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
function updateCheckResponse(response) {
|
function updateCheckResponse(response) {
|
||||||
return Promise.all([
|
let notifications = [],
|
||||||
api.settings.edit({settings: [{key: 'next_update_check', value: response.next_check}]}, internal),
|
notificationGroups = (config.get('notificationGroups') || []).concat(['all']);
|
||||||
api.settings.edit({settings: [{key: 'display_update_notification', value: response.version}]}, internal)
|
|
||||||
]).then(function () {
|
|
||||||
var messages = response.messages || [];
|
|
||||||
|
|
||||||
/**
|
debug('Notification Groups', notificationGroups);
|
||||||
* by default the update check service returns messages: []
|
debug('Response Update Check Service', response);
|
||||||
* but the latest release version get's stored anyway, because we adding the `display_update_notification` ^
|
|
||||||
*/
|
return api.settings.edit({settings: [{key: 'next_update_check', value: response.next_check}]}, internal)
|
||||||
return Promise.map(messages, createCustomNotification);
|
.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() {
|
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();
|
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];
|
var nextUpdateCheck = result.settings[0];
|
||||||
|
|
||||||
if (nextUpdateCheck && nextUpdateCheck.value && nextUpdateCheck.value > moment().unix()) {
|
// CASE: Next update check should happen now?
|
||||||
// It's not time to check yet
|
if (!config.get('updateCheck:forceUpdate') && nextUpdateCheck && nextUpdateCheck.value && nextUpdateCheck.value > moment().unix()) {
|
||||||
return; // eslint-disable-line no-useless-return
|
return Promise.resolve();
|
||||||
} else {
|
}
|
||||||
// We need to do a check
|
|
||||||
return updateCheckRequest()
|
return updateCheckRequest()
|
||||||
.then(updateCheckResponse)
|
.then(updateCheckResponse)
|
||||||
.catch(updateCheckError);
|
.catch(updateCheckError);
|
||||||
}
|
})
|
||||||
}).catch(updateCheckError);
|
.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;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = updateCheck;
|
module.exports = updateCheck;
|
||||||
module.exports.showUpdateNotification = showUpdateNotification;
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
var debug = require('ghost-ignition').debug('admin:controller'),
|
'use strict';
|
||||||
_ = require('lodash'),
|
|
||||||
|
const debug = require('ghost-ignition').debug('admin:controller'),
|
||||||
path = require('path'),
|
path = require('path'),
|
||||||
config = require('../../config'),
|
config = require('../../config'),
|
||||||
api = require('../../api'),
|
|
||||||
updateCheck = require('../../update-check'),
|
updateCheck = require('../../update-check'),
|
||||||
common = require('../../lib/common');
|
common = require('../../lib/common');
|
||||||
|
|
||||||
|
@ -12,36 +12,14 @@ var debug = require('ghost-ignition').debug('admin:controller'),
|
||||||
module.exports = function adminController(req, res) {
|
module.exports = function adminController(req, res) {
|
||||||
debug('index called');
|
debug('index called');
|
||||||
|
|
||||||
updateCheck().then(function then() {
|
// run in background, don't block the admin rendering
|
||||||
return updateCheck.showUpdateNotification();
|
updateCheck()
|
||||||
}).then(function then(updateVersion) {
|
.catch(function onError(err) {
|
||||||
if (!updateVersion) {
|
common.logging.error(err);
|
||||||
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}});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}).finally(function noMatterWhat() {
|
|
||||||
var defaultTemplate = config.get('env') === 'production' ? 'default-prod.html' : 'default.html',
|
let defaultTemplate = config.get('env') === 'production' ? 'default-prod.html' : 'default.html',
|
||||||
templatePath = path.resolve(config.get('paths').adminViews, defaultTemplate);
|
templatePath = path.resolve(config.get('paths').adminViews, defaultTemplate);
|
||||||
|
|
||||||
res.sendFile(templatePath);
|
res.sendFile(templatePath);
|
||||||
}).catch(function (err) {
|
|
||||||
common.logging.error(err);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
var should = require('should'),
|
var should = require('should'),
|
||||||
testUtils = require('../../utils'),
|
|
||||||
_ = require('lodash'),
|
_ = require('lodash'),
|
||||||
|
uuid = require('uuid'),
|
||||||
ObjectId = require('bson-objectid'),
|
ObjectId = require('bson-objectid'),
|
||||||
NotificationsAPI = require('../../../server/api/notifications'),
|
testUtils = require('../../utils'),
|
||||||
SettingsAPI = require('../../../server/api/settings');
|
NotificationsAPI = require('../../../server/api/notifications');
|
||||||
|
|
||||||
describe('Notifications API', function () {
|
describe('Notifications API', function () {
|
||||||
// Keep the DB clean
|
// Keep the DB clean
|
||||||
|
@ -13,10 +13,6 @@ describe('Notifications API', function () {
|
||||||
|
|
||||||
should.exist(NotificationsAPI);
|
should.exist(NotificationsAPI);
|
||||||
|
|
||||||
after(function () {
|
|
||||||
return NotificationsAPI.destroyAll(testUtils.context.internal);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can add, adds defaults (internal)', function (done) {
|
it('can add, adds defaults (internal)', function (done) {
|
||||||
var msg = {
|
var msg = {
|
||||||
type: 'info',
|
type: 'info',
|
||||||
|
@ -54,6 +50,7 @@ describe('Notifications API', function () {
|
||||||
notification.dismissible.should.be.true();
|
notification.dismissible.should.be.true();
|
||||||
should.exist(notification.location);
|
should.exist(notification.location);
|
||||||
notification.location.should.equal('bottom');
|
notification.location.should.equal('bottom');
|
||||||
|
notification.id.should.be.a.String();
|
||||||
|
|
||||||
done();
|
done();
|
||||||
}).catch(done);
|
}).catch(done);
|
||||||
|
@ -74,7 +71,7 @@ describe('Notifications API', function () {
|
||||||
should.exist(result.notifications);
|
should.exist(result.notifications);
|
||||||
|
|
||||||
notification = result.notifications[0];
|
notification = result.notifications[0];
|
||||||
notification.id.should.not.equal(msg.id);
|
notification.id.should.be.a.String();
|
||||||
should.exist(notification.status);
|
should.exist(notification.status);
|
||||||
notification.status.should.equal('alert');
|
notification.status.should.equal('alert');
|
||||||
|
|
||||||
|
@ -82,6 +79,32 @@ describe('Notifications API', function () {
|
||||||
}).catch(done);
|
}).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) {
|
it('can browse (internal)', function (done) {
|
||||||
var msg = {
|
var msg = {
|
||||||
type: 'error', // this can be 'error', 'success', 'warn' and 'info'
|
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) {
|
it('can destroy (internal)', function (done) {
|
||||||
var msg = {
|
var msg = {
|
||||||
type: 'error',
|
type: 'error',
|
||||||
|
@ -123,13 +180,13 @@ describe('Notifications API', function () {
|
||||||
NotificationsAPI.add({notifications: [msg]}, testUtils.context.internal).then(function (result) {
|
NotificationsAPI.add({notifications: [msg]}, testUtils.context.internal).then(function (result) {
|
||||||
var notification = result.notifications[0];
|
var notification = result.notifications[0];
|
||||||
|
|
||||||
NotificationsAPI.destroy(
|
NotificationsAPI
|
||||||
_.extend({}, testUtils.context.internal, {id: notification.id})
|
.destroy(_.extend({}, testUtils.context.internal, {id: notification.id}))
|
||||||
).then(function (result) {
|
.then(function (result) {
|
||||||
should.not.exist(result);
|
should.not.exist(result);
|
||||||
|
|
||||||
done();
|
done();
|
||||||
}).catch(done);
|
})
|
||||||
|
.catch(done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -142,22 +199,23 @@ describe('Notifications API', function () {
|
||||||
NotificationsAPI.add({notifications: [msg]}, testUtils.context.internal).then(function (result) {
|
NotificationsAPI.add({notifications: [msg]}, testUtils.context.internal).then(function (result) {
|
||||||
var notification = result.notifications[0];
|
var notification = result.notifications[0];
|
||||||
|
|
||||||
NotificationsAPI.destroy(
|
NotificationsAPI
|
||||||
_.extend({}, testUtils.context.owner, {id: notification.id})
|
.destroy(_.extend({}, testUtils.context.owner, {id: notification.id}))
|
||||||
).then(function (result) {
|
.then(function (result) {
|
||||||
should.not.exist(result);
|
should.not.exist(result);
|
||||||
|
|
||||||
done();
|
done();
|
||||||
}).catch(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 = {
|
var customNotification = {
|
||||||
status: 'alert',
|
status: 'alert',
|
||||||
type: 'info',
|
type: 'info',
|
||||||
location: 'test.to-be-deleted',
|
location: 'test.to-be-deleted',
|
||||||
custom: true,
|
custom: true,
|
||||||
|
id: uuid.v1(),
|
||||||
dismissible: true,
|
dismissible: true,
|
||||||
message: 'Hello, this is dog number 4'
|
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) {
|
NotificationsAPI.add({notifications: [customNotification]}, testUtils.context.internal).then(function (result) {
|
||||||
var notification = result.notifications[0];
|
var notification = result.notifications[0];
|
||||||
|
|
||||||
NotificationsAPI.destroy(
|
return NotificationsAPI.browse(testUtils.context.internal)
|
||||||
_.extend({}, testUtils.context.internal, {id: notification.id})
|
.then(function (response) {
|
||||||
).then(function () {
|
response.notifications.length.should.eql(1);
|
||||||
return SettingsAPI.read(_.extend({key: 'seen_notifications'}, testUtils.context.internal));
|
return NotificationsAPI.destroy(_.extend({}, testUtils.context.internal, {id: notification.id}));
|
||||||
}).then(function (response) {
|
})
|
||||||
should.exist(response);
|
.then(function () {
|
||||||
response.settings[0].value.should.containEql(notification.id);
|
return NotificationsAPI.browse(testUtils.context.internal);
|
||||||
|
})
|
||||||
|
.then(function (response) {
|
||||||
|
response.notifications.length.should.eql(0);
|
||||||
done();
|
done();
|
||||||
}).catch(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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1517,19 +1517,21 @@ describe('Import (new test structure)', function () {
|
||||||
users[1].profile_image.should.eql(exportData.data.users[0].image);
|
users[1].profile_image.should.eql(exportData.data.users[0].image);
|
||||||
// Check feature image is correctly mapped for a tag
|
// Check feature image is correctly mapped for a tag
|
||||||
tags[0].feature_image.should.eql(exportData.data.tags[0].image);
|
tags[0].feature_image.should.eql(exportData.data.tags[0].image);
|
||||||
|
|
||||||
// Check logo image is correctly mapped for a blog
|
// Check logo image is correctly mapped for a blog
|
||||||
settings[6].key.should.eql('logo');
|
settings[5].key.should.eql('logo');
|
||||||
settings[6].value.should.eql('/content/images/2017/05/bloglogo.jpeg');
|
settings[5].value.should.eql('/content/images/2017/05/bloglogo.jpeg');
|
||||||
|
|
||||||
// Check cover image is correctly mapped for a blog
|
// Check cover image is correctly mapped for a blog
|
||||||
settings[7].key.should.eql('cover_image');
|
settings[6].key.should.eql('cover_image');
|
||||||
settings[7].value.should.eql('/content/images/2017/05/blogcover.jpeg');
|
settings[6].value.should.eql('/content/images/2017/05/blogcover.jpeg');
|
||||||
|
|
||||||
// Check default settings locale is not overwritten by defaultLang
|
// Check default settings locale is not overwritten by defaultLang
|
||||||
settings[9].key.should.eql('default_locale');
|
settings[8].key.should.eql('default_locale');
|
||||||
settings[9].value.should.eql('en');
|
settings[8].value.should.eql('en');
|
||||||
|
|
||||||
settings[18].key.should.eql('labs');
|
settings[17].key.should.eql('labs');
|
||||||
settings[18].value.should.eql('{"publicAPI":true}');
|
settings[17].value.should.eql('{"publicAPI":true}');
|
||||||
|
|
||||||
// Check post language is null
|
// Check post language is null
|
||||||
should(firstPost.locale).equal(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
|
// Check feature image is correctly mapped for a tag
|
||||||
tags[0].feature_image.should.eql(exportData.data.tags[0].image);
|
tags[0].feature_image.should.eql(exportData.data.tags[0].image);
|
||||||
// Check logo image is correctly mapped for a blog
|
// Check logo image is correctly mapped for a blog
|
||||||
settings[6].key.should.eql('logo');
|
settings[5].key.should.eql('logo');
|
||||||
settings[6].value.should.eql('/content/images/2017/05/bloglogo.jpeg');
|
settings[5].value.should.eql('/content/images/2017/05/bloglogo.jpeg');
|
||||||
// Check cover image is correctly mapped for a blog
|
// Check cover image is correctly mapped for a blog
|
||||||
settings[7].key.should.eql('cover_image');
|
settings[6].key.should.eql('cover_image');
|
||||||
settings[7].value.should.eql('/content/images/2017/05/blogcover.jpeg');
|
settings[6].value.should.eql('/content/images/2017/05/blogcover.jpeg');
|
||||||
|
|
||||||
// Check default settings locale is not overwritten by defaultLang
|
// Check default settings locale is not overwritten by defaultLang
|
||||||
settings[9].key.should.eql('default_locale');
|
settings[8].key.should.eql('default_locale');
|
||||||
settings[9].value.should.eql('en');
|
settings[8].value.should.eql('en');
|
||||||
|
|
||||||
// Check post language is set to null
|
// Check post language is set to null
|
||||||
should(firstPost.locale).equal(null);
|
should(firstPost.locale).equal(null);
|
||||||
|
|
|
@ -22,7 +22,7 @@ describe('Models: listeners', function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
before(testUtils.teardown);
|
before(testUtils.teardown);
|
||||||
beforeEach(testUtils.setup('owner', 'user-token:0'));
|
beforeEach(testUtils.setup('owner', 'user-token:0', 'settings'));
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
sandbox.stub(common.events, 'on').callsFake(function (eventName, callback) {
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,20 +1,102 @@
|
||||||
var should = require('should'),
|
var _ = require('lodash'),
|
||||||
_ = require('lodash'),
|
Promise = require('bluebird'),
|
||||||
|
should = require('should'),
|
||||||
rewire = require('rewire'),
|
rewire = require('rewire'),
|
||||||
|
sinon = require('sinon'),
|
||||||
|
moment = require('moment'),
|
||||||
uuid = require('uuid'),
|
uuid = require('uuid'),
|
||||||
testUtils = require('../utils'),
|
testUtils = require('../utils'),
|
||||||
configUtils = require('../utils/configUtils'),
|
configUtils = require('../utils/configUtils'),
|
||||||
packageInfo = require('../../../package'),
|
packageInfo = require('../../../package'),
|
||||||
updateCheck = rewire('../../server/update-check'),
|
updateCheck = rewire('../../server/update-check'),
|
||||||
settingsCache = require('../../server/services/settings/cache'),
|
SettingsAPI = require('../../server/api/settings'),
|
||||||
NotificationsAPI = require('../../server/api/notifications');
|
NotificationsAPI = require('../../server/api/notifications'),
|
||||||
|
sandbox = sinon.sandbox.create();
|
||||||
|
|
||||||
describe('Update Check', function () {
|
describe('Update Check', function () {
|
||||||
after(function () {
|
beforeEach(function () {
|
||||||
return NotificationsAPI.destroyAll(testUtils.context.internal);
|
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 () {
|
before(function () {
|
||||||
configUtils.set('privacy:useUpdateCheck', true);
|
configUtils.set('privacy:useUpdateCheck', true);
|
||||||
});
|
});
|
||||||
|
@ -52,156 +134,512 @@ describe('Update Check', function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Custom Notifications', function () {
|
describe('fn: createCustomNotification', function () {
|
||||||
var currentVersionOrig;
|
var currentVersionOrig;
|
||||||
|
|
||||||
before(function () {
|
before(function () {
|
||||||
currentVersionOrig = updateCheck.__get__('currentVersion');
|
currentVersionOrig = updateCheck.__get__('ghostVersion.original');
|
||||||
updateCheck.__set__('currentVersion', '0.9.0');
|
updateCheck.__set__('ghostVersion.original', '0.9.0');
|
||||||
});
|
});
|
||||||
|
|
||||||
after(function () {
|
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(testUtils.setup('owner', 'posts', 'settings', 'perms:setting', 'perms:notification', 'perms:user', 'perms:init'));
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
return NotificationsAPI.destroyAll(testUtils.context.internal);
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(testUtils.teardown);
|
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'),
|
var createCustomNotification = updateCheck.__get__('createCustomNotification'),
|
||||||
message = {
|
notification = {
|
||||||
|
id: 1,
|
||||||
|
custom: 0,
|
||||||
|
messages: [{
|
||||||
id: uuid.v4(),
|
id: uuid.v4(),
|
||||||
version: '0.9.x',
|
version: '0.9.x',
|
||||||
content: '<p>Hey there! This is for 0.9.0 version</p>'
|
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);
|
return NotificationsAPI.browse(testUtils.context.internal);
|
||||||
}).then(function (results) {
|
}).then(function (results) {
|
||||||
should.exist(results);
|
should.exist(results);
|
||||||
should.exist(results.notifications);
|
should.exist(results.notifications);
|
||||||
results.notifications.length.should.be.above(0);
|
results.notifications.length.should.eql(1);
|
||||||
should.exist(_.find(results.notifications, {uuid: message.id}));
|
|
||||||
|
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();
|
done();
|
||||||
}).catch(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'),
|
var createCustomNotification = updateCheck.__get__('createCustomNotification'),
|
||||||
message = {
|
notification = {
|
||||||
|
id: 1,
|
||||||
|
custom: 1,
|
||||||
|
messages: [{
|
||||||
id: uuid.v4(),
|
id: uuid.v4(),
|
||||||
version: '0.5.x',
|
version: 'custom1',
|
||||||
content: '<p>Hey there! This is for 0.5.0 version</p>'
|
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);
|
return NotificationsAPI.browse(testUtils.context.internal);
|
||||||
}).then(function (results) {
|
}).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();
|
done();
|
||||||
}).catch(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 () {
|
describe('fn: updateCheckResponse', function () {
|
||||||
var currentVersionOrig;
|
beforeEach(testUtils.setup('settings', 'perms:setting', 'perms:init'));
|
||||||
|
|
||||||
before(function () {
|
|
||||||
currentVersionOrig = updateCheck.__get__('currentVersion');
|
|
||||||
});
|
|
||||||
|
|
||||||
after(function () {
|
|
||||||
updateCheck.__set__('currentVersion', currentVersionOrig);
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(testUtils.setup('settings', 'perms:setting', 'perms:notification', 'perms:init'));
|
|
||||||
|
|
||||||
afterEach(testUtils.teardown);
|
afterEach(testUtils.teardown);
|
||||||
|
|
||||||
it('should show update notification', function (done) {
|
it('receives a notifications with messages', function (done) {
|
||||||
var showUpdateNotification = updateCheck.__get__('showUpdateNotification');
|
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');
|
updateCheck.__set__('createCustomNotification', createNotificationSpy);
|
||||||
settingsCache.set('display_update_notification', {value: '1.9.0'});
|
|
||||||
|
|
||||||
showUpdateNotification()
|
updateCheckResponse({version: '0.11.12', messages: [message]})
|
||||||
.then(function (result) {
|
.then(function () {
|
||||||
result.should.eql('1.9.0');
|
createNotificationSpy.callCount.should.eql(1);
|
||||||
done();
|
done();
|
||||||
})
|
})
|
||||||
.catch(done);
|
.catch(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show update notification', function (done) {
|
it('receives multiple notifications', function (done) {
|
||||||
var showUpdateNotification = updateCheck.__get__('showUpdateNotification');
|
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');
|
updateCheck.__set__('createCustomNotification', createNotificationSpy);
|
||||||
settingsCache.set('display_update_notification', {value: '2.0.0'});
|
|
||||||
|
|
||||||
showUpdateNotification()
|
updateCheckResponse(notifications)
|
||||||
.then(function (result) {
|
.then(function () {
|
||||||
result.should.eql('2.0.0');
|
createNotificationSpy.callCount.should.eql(2);
|
||||||
done();
|
done();
|
||||||
})
|
})
|
||||||
.catch(done);
|
.catch(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not show update notification: latest minor release is not greater than your Ghost version', function (done) {
|
it('ignores some custom notifications which are not marked as group', function (done) {
|
||||||
var showUpdateNotification = updateCheck.__get__('showUpdateNotification');
|
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');
|
updateCheck.__set__('createCustomNotification', createNotificationSpy);
|
||||||
settingsCache.set('display_update_notification', {value: '1.9.0'});
|
|
||||||
|
|
||||||
showUpdateNotification()
|
updateCheckResponse(notifications)
|
||||||
.then(function (result) {
|
.then(function () {
|
||||||
result.should.eql(false);
|
createNotificationSpy.callCount.should.eql(2);
|
||||||
done();
|
done();
|
||||||
})
|
})
|
||||||
.catch(done);
|
.catch(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not show update notification: latest minor release is not greater than your Ghost version', function (done) {
|
it('group matches', function (done) {
|
||||||
var showUpdateNotification = updateCheck.__get__('showUpdateNotification');
|
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');
|
updateCheck.__set__('createCustomNotification', createNotificationSpy);
|
||||||
settingsCache.set('display_update_notification', {value: '1.9.1'});
|
|
||||||
|
|
||||||
showUpdateNotification()
|
configUtils.set({notificationGroups: ['migration']});
|
||||||
.then(function (result) {
|
|
||||||
result.should.eql(false);
|
updateCheckResponse(notifications)
|
||||||
|
.then(function () {
|
||||||
|
createNotificationSpy.callCount.should.eql(3);
|
||||||
done();
|
done();
|
||||||
})
|
})
|
||||||
.catch(done);
|
.catch(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not show update notification: latest release is a patch', function (done) {
|
it('single custom notification received, group matches', function (done) {
|
||||||
var showUpdateNotification = updateCheck.__get__('showUpdateNotification');
|
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');
|
updateCheck.__set__('createCustomNotification', createNotificationSpy);
|
||||||
settingsCache.set('display_update_notification', {value: '1.9.1'});
|
|
||||||
|
|
||||||
showUpdateNotification()
|
configUtils.set({notificationGroups: ['something']});
|
||||||
.then(function (result) {
|
|
||||||
result.should.eql(false);
|
updateCheckResponse(notifications)
|
||||||
|
.then(function () {
|
||||||
|
createNotificationSpy.callCount.should.eql(1);
|
||||||
done();
|
done();
|
||||||
})
|
})
|
||||||
.catch(done);
|
.catch(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not show update notification: latest release is a patch', function (done) {
|
it('single custom notification received, group does not match', function (done) {
|
||||||
var showUpdateNotification = updateCheck.__get__('showUpdateNotification');
|
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');
|
updateCheck.__set__('createCustomNotification', createNotificationSpy);
|
||||||
settingsCache.set('display_update_notification', {value: '1.9.0'});
|
|
||||||
|
|
||||||
showUpdateNotification()
|
configUtils.set({notificationGroups: ['migration']});
|
||||||
.then(function (result) {
|
|
||||||
result.should.eql(false);
|
updateCheckResponse(notifications)
|
||||||
|
.then(function () {
|
||||||
|
createNotificationSpy.callCount.should.eql(0);
|
||||||
done();
|
done();
|
||||||
})
|
})
|
||||||
.catch(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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
Loading…
Add table
Reference in a new issue