mirror of
https://github.com/TryGhost/Ghost.git
synced 2023-12-13 21:00:40 +01:00
post-scheduling
refs #6413 - PUT endpoint to publish a post/page for the scheduler - fn endpoint to get all scheduled posts (with from/to query params) for the scheduler - hardcoded permission handling for scheduler client - fix event bug: unscheduled - basic structure for scheduling - post scheduling basics - offer easy option to change adapter - integrate the default scheduler adapter - update scheduled posts when blog TZ changes - safety check before scheduler can publish a post (not allowed to publish in the future or past) - add force flag to allow publishing in the past - invalidate cache header for /schedules/posts/:id
This commit is contained in:
parent
1b98d80a73
commit
1421c92ba5
33 changed files with 1737 additions and 78 deletions
|
@ -7,12 +7,12 @@
|
|||
var _ = require('lodash'),
|
||||
Promise = require('bluebird'),
|
||||
config = require('../config'),
|
||||
// Include Endpoints
|
||||
configuration = require('./configuration'),
|
||||
db = require('./db'),
|
||||
mail = require('./mail'),
|
||||
notifications = require('./notifications'),
|
||||
posts = require('./posts'),
|
||||
schedules = require('./schedules'),
|
||||
roles = require('./roles'),
|
||||
settings = require('./settings'),
|
||||
tags = require('./tags'),
|
||||
|
@ -60,6 +60,7 @@ cacheInvalidationHeader = function cacheInvalidationHeader(req, result) {
|
|||
var parsedUrl = req._parsedUrl.pathname.replace(/^\/|\/$/g, '').split('/'),
|
||||
method = req.method,
|
||||
endpoint = parsedUrl[0],
|
||||
subdir = parsedUrl[1],
|
||||
jsonResult = result.toJSON ? result.toJSON() : result,
|
||||
INVALIDATE_ALL = '/*',
|
||||
post,
|
||||
|
@ -67,6 +68,9 @@ cacheInvalidationHeader = function cacheInvalidationHeader(req, result) {
|
|||
wasPublishedUpdated;
|
||||
|
||||
if (['POST', 'PUT', 'DELETE'].indexOf(method) > -1) {
|
||||
if (endpoint === 'schedules' && subdir === 'posts') {
|
||||
return INVALIDATE_ALL;
|
||||
}
|
||||
if (['settings', 'users', 'db', 'tags'].indexOf(endpoint) > -1) {
|
||||
return INVALIDATE_ALL;
|
||||
} else if (endpoint === 'posts') {
|
||||
|
@ -213,7 +217,8 @@ http = function http(apiMethod) {
|
|||
var object = req.body,
|
||||
options = _.extend({}, req.file, req.query, req.params, {
|
||||
context: {
|
||||
user: ((req.user && req.user.id) || (req.user && req.user.id === 0)) ? req.user.id : null
|
||||
user: ((req.user && req.user.id) || (req.user && req.user.id === 0)) ? req.user.id : null,
|
||||
client: (req.client && req.client.slug) ? req.client.slug : null
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -257,6 +262,7 @@ module.exports = {
|
|||
mail: mail,
|
||||
notifications: notifications,
|
||||
posts: posts,
|
||||
schedules: schedules,
|
||||
roles: roles,
|
||||
settings: settings,
|
||||
tags: tags,
|
||||
|
|
85
core/server/api/schedules.js
Normal file
85
core/server/api/schedules.js
Normal file
|
@ -0,0 +1,85 @@
|
|||
var _ = require('lodash'),
|
||||
Promise = require('bluebird'),
|
||||
moment = require('moment'),
|
||||
config = require('../config'),
|
||||
pipeline = require(config.paths.corePath + '/server/utils/pipeline'),
|
||||
dataProvider = require(config.paths.corePath + '/server/models'),
|
||||
i18n = require(config.paths.corePath + '/server/i18n'),
|
||||
errors = require(config.paths.corePath + '/server/errors'),
|
||||
apiPosts = require(config.paths.corePath + '/server/api/posts'),
|
||||
utils = require('./utils');
|
||||
|
||||
/**
|
||||
* publish a scheduled post
|
||||
*
|
||||
* object.force: you can force publishing a post in the past (for example if your service was down)
|
||||
*/
|
||||
exports.publishPost = function publishPost(object, options) {
|
||||
if (_.isEmpty(options)) {
|
||||
options = object || {};
|
||||
object = {};
|
||||
}
|
||||
|
||||
var post, publishedAtMoment,
|
||||
publishAPostBySchedulerToleranceInMinutes = config.times.publishAPostBySchedulerToleranceInMinutes;
|
||||
|
||||
// CASE: only the scheduler client is allowed to publish (hardcoded because of missing client permission system)
|
||||
if (!options.context || !options.context.client || options.context.client !== 'ghost-scheduler') {
|
||||
return Promise.reject(new errors.NoPermissionError(i18n.t('errors.permissions.noPermissionToAction')));
|
||||
}
|
||||
|
||||
options.context = {internal: true};
|
||||
|
||||
return pipeline([
|
||||
utils.validate('posts', {opts: utils.idDefaultOptions}),
|
||||
function (cleanOptions) {
|
||||
cleanOptions.status = 'scheduled';
|
||||
|
||||
return apiPosts.read(cleanOptions)
|
||||
.then(function (result) {
|
||||
post = result.posts[0];
|
||||
publishedAtMoment = moment(post.published_at);
|
||||
|
||||
if (publishedAtMoment.diff(moment(), 'minutes') > publishAPostBySchedulerToleranceInMinutes) {
|
||||
return Promise.reject(new errors.NotFoundError(i18n.t('errors.api.job.notFound')));
|
||||
}
|
||||
|
||||
if (publishedAtMoment.diff(moment(), 'minutes') < publishAPostBySchedulerToleranceInMinutes * -1 && object.force !== true) {
|
||||
return Promise.reject(new errors.NotFoundError(i18n.t('errors.api.job.publishInThePast')));
|
||||
}
|
||||
|
||||
return apiPosts.edit({posts: [{status: 'published'}]}, _.pick(cleanOptions, ['context', 'id']));
|
||||
});
|
||||
}
|
||||
], options);
|
||||
};
|
||||
|
||||
/**
|
||||
* get all scheduled posts/pages
|
||||
* permission check not needed, because route is not exposed
|
||||
*/
|
||||
exports.getScheduledPosts = function readPosts(options) {
|
||||
options = options || {};
|
||||
options.context = {internal: true};
|
||||
|
||||
return pipeline([
|
||||
utils.validate('posts', {opts: ['from', 'to']}),
|
||||
function (cleanOptions) {
|
||||
cleanOptions.filter = 'status:scheduled';
|
||||
cleanOptions.columns = ['id', 'published_at', 'created_at'];
|
||||
|
||||
if (cleanOptions.from) {
|
||||
cleanOptions.filter += '+created_at:>=\'' + moment(cleanOptions.from).format('YYYY-MM-DD HH:mm:ss') + '\'';
|
||||
}
|
||||
|
||||
if (cleanOptions.to) {
|
||||
cleanOptions.filter += '+created_at:<=\'' + moment(cleanOptions.to).format('YYYY-MM-DD HH:mm:ss') + '\'';
|
||||
}
|
||||
|
||||
return dataProvider.Post.findAll(cleanOptions)
|
||||
.then(function (result) {
|
||||
return Promise.resolve({posts: result.models});
|
||||
});
|
||||
}
|
||||
], options);
|
||||
};
|
|
@ -43,6 +43,7 @@ utils = {
|
|||
*/
|
||||
return function doValidate() {
|
||||
var object, options, permittedOptions;
|
||||
|
||||
if (arguments.length === 2) {
|
||||
object = arguments[0];
|
||||
options = _.clone(arguments[1]) || {};
|
||||
|
@ -114,6 +115,8 @@ utils = {
|
|||
slug: {isSlug: true},
|
||||
page: {matches: /^\d+$/},
|
||||
limit: {matches: /^\d+|all$/},
|
||||
from: {isDate: true},
|
||||
to: {isDate: true},
|
||||
fields: {matches: /^[\w, ]+$/},
|
||||
order: {matches: /^[a-z0-9_,\. ]+$/i},
|
||||
name: {}
|
||||
|
|
|
@ -91,10 +91,13 @@ ConfigManager.prototype.init = function (rawConfig) {
|
|||
*/
|
||||
ConfigManager.prototype.set = function (config) {
|
||||
var localPath = '',
|
||||
defaultStorage = 'local-file-store',
|
||||
defaultStorageAdapter = 'local-file-store',
|
||||
defaultSchedulingAdapter = 'SchedulingDefault',
|
||||
activeStorageAdapter,
|
||||
activePostSchedulingAdapter,
|
||||
contentPath,
|
||||
activeStorage,
|
||||
storagePath,
|
||||
postSchedulingPath,
|
||||
subdir,
|
||||
assetHash;
|
||||
|
||||
|
@ -142,18 +145,28 @@ ConfigManager.prototype.set = function (config) {
|
|||
assetHash = this._config.assetHash ||
|
||||
(crypto.createHash('md5').update(packageInfo.version + Date.now()).digest('hex')).substring(0, 10);
|
||||
|
||||
// Protect against accessing a non-existent object.
|
||||
// This ensures there's always at least a storage object
|
||||
// because it's referenced in multiple places.
|
||||
// read storage adapter from config file or attach default adapter
|
||||
this._config.storage = this._config.storage || {};
|
||||
activeStorage = this._config.storage.active || defaultStorage;
|
||||
activeStorageAdapter = this._config.storage.active || defaultStorageAdapter;
|
||||
|
||||
if (activeStorage === defaultStorage) {
|
||||
// read scheduling adapter(s) from config file or attach default adapter
|
||||
this._config.scheduling = this._config.scheduling || {};
|
||||
this._config.scheduling.postScheduling = this._config.scheduling.postScheduling || {};
|
||||
activePostSchedulingAdapter = this._config.scheduling.postScheduling.active || defaultSchedulingAdapter;
|
||||
|
||||
// we support custom adapters located in content folder
|
||||
if (activeStorageAdapter === defaultStorageAdapter) {
|
||||
storagePath = path.join(corePath, '/server/storage/');
|
||||
} else {
|
||||
storagePath = path.join(contentPath, 'storage');
|
||||
}
|
||||
|
||||
if (activePostSchedulingAdapter === defaultSchedulingAdapter) {
|
||||
postSchedulingPath = path.join(corePath, '/server/scheduling/');
|
||||
} else {
|
||||
postSchedulingPath = path.join(contentPath, '/scheduling/');
|
||||
}
|
||||
|
||||
_.merge(this._config, {
|
||||
ghostVersion: packageInfo.version,
|
||||
paths: {
|
||||
|
@ -163,7 +176,7 @@ ConfigManager.prototype.set = function (config) {
|
|||
configExample: path.join(appRoot, 'config.example.js'),
|
||||
corePath: corePath,
|
||||
|
||||
storage: path.join(storagePath, activeStorage),
|
||||
storage: path.join(storagePath, activeStorageAdapter),
|
||||
|
||||
contentPath: contentPath,
|
||||
themePath: path.resolve(contentPath, 'themes'),
|
||||
|
@ -179,8 +192,14 @@ ConfigManager.prototype.set = function (config) {
|
|||
availableApps: this._config.paths.availableApps || {},
|
||||
clientAssets: path.join(corePath, '/built/assets/')
|
||||
},
|
||||
scheduling: {
|
||||
postScheduling: {
|
||||
active: activePostSchedulingAdapter,
|
||||
path: postSchedulingPath
|
||||
}
|
||||
},
|
||||
storage: {
|
||||
active: activeStorage
|
||||
active: activeStorageAdapter
|
||||
},
|
||||
theme: {
|
||||
// normalise the URL by removing any trailing slash
|
||||
|
@ -216,7 +235,11 @@ ConfigManager.prototype.set = function (config) {
|
|||
deprecatedItems: ['updateCheck', 'mail.fromaddress'],
|
||||
// create a hash for cache busting assets
|
||||
assetHash: assetHash,
|
||||
preloadHeaders: this._config.preloadHeaders || false
|
||||
preloadHeaders: this._config.preloadHeaders || false,
|
||||
times: {
|
||||
cannotScheduleAPostBeforeInMinutes: 2,
|
||||
publishAPostBySchedulerToleranceInMinutes: 2
|
||||
}
|
||||
});
|
||||
|
||||
// Also pass config object to
|
||||
|
|
11
core/server/errors/incorrect-usage.js
Normal file
11
core/server/errors/incorrect-usage.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
function IncorrectUsage(message, context) {
|
||||
this.name = 'IncorrectUsage';
|
||||
this.stack = new Error().stack;
|
||||
this.statusCode = 400;
|
||||
this.errorType = this.name;
|
||||
this.message = message;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
IncorrectUsage.prototype = Object.create(Error.prototype);
|
||||
module.exports = IncorrectUsage;
|
|
@ -19,6 +19,7 @@ var _ = require('lodash'),
|
|||
TooManyRequestsError = require('./too-many-requests-error'),
|
||||
TokenRevocationError = require('./token-revocation-error'),
|
||||
VersionMismatchError = require('./version-mismatch-error'),
|
||||
IncorrectUsage = require('./incorrect-usage'),
|
||||
i18n = require('../i18n'),
|
||||
config,
|
||||
errors,
|
||||
|
@ -447,3 +448,4 @@ module.exports.MethodNotAllowedError = MethodNotAllowedError;
|
|||
module.exports.TooManyRequestsError = TooManyRequestsError;
|
||||
module.exports.TokenRevocationError = TokenRevocationError;
|
||||
module.exports.VersionMismatchError = VersionMismatchError;
|
||||
module.exports.IncorrectUsage = IncorrectUsage;
|
||||
|
|
|
@ -6,8 +6,17 @@ var events = require('events'),
|
|||
EventRegistry = function () {
|
||||
events.EventEmitter.call(this);
|
||||
};
|
||||
|
||||
util.inherits(EventRegistry, events.EventEmitter);
|
||||
|
||||
EventRegistry.prototype.onMany = function (arr, onEvent) {
|
||||
var self = this;
|
||||
|
||||
arr.forEach(function (eventName) {
|
||||
self.on(eventName, onEvent);
|
||||
});
|
||||
};
|
||||
|
||||
EventRegistryInstance = new EventRegistry();
|
||||
EventRegistryInstance.setMaxListeners(100);
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
// Module dependencies
|
||||
var express = require('express'),
|
||||
_ = require('lodash'),
|
||||
uuid = require('node-uuid'),
|
||||
Promise = require('bluebird'),
|
||||
i18n = require('./i18n'),
|
||||
|
@ -18,6 +19,7 @@ var express = require('express'),
|
|||
xmlrpc = require('./data/xml/xmlrpc'),
|
||||
slack = require('./data/slack'),
|
||||
GhostServer = require('./ghost-server'),
|
||||
scheduling = require('./scheduling'),
|
||||
validateThemes = require('./utils/validate-themes'),
|
||||
|
||||
dbHash;
|
||||
|
@ -47,6 +49,8 @@ function initDbHashAndFirstRun() {
|
|||
// Sets up the express server instances, runs init on a bunch of stuff, configures views, helpers, routes and more
|
||||
// Finally it returns an instance of GhostServer
|
||||
function init(options) {
|
||||
var ghostServer = null;
|
||||
|
||||
// ### Initialisation
|
||||
// The server and its dependencies require a populated config
|
||||
// It returns a promise that is resolved when the application
|
||||
|
@ -108,6 +112,14 @@ function init(options) {
|
|||
});
|
||||
|
||||
return new GhostServer(parentApp);
|
||||
}).then(function (_ghostServer) {
|
||||
ghostServer = _ghostServer;
|
||||
|
||||
// scheduling can trigger api requests, that's why we initialize the module after the ghost server creation
|
||||
// scheduling module can create x schedulers with different adapters
|
||||
return scheduling.init(_.extend(config.scheduling, {apiUrl: config.url + config.urlFor('api')}));
|
||||
}).then(function () {
|
||||
return ghostServer;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -7,19 +7,20 @@
|
|||
// allowed to access data via the API.
|
||||
var _ = require('lodash'),
|
||||
bookshelf = require('bookshelf'),
|
||||
moment = require('moment'),
|
||||
Promise = require('bluebird'),
|
||||
uuid = require('node-uuid'),
|
||||
config = require('../../config'),
|
||||
db = require('../../data/db'),
|
||||
errors = require('../../errors'),
|
||||
filters = require('../../filters'),
|
||||
moment = require('moment'),
|
||||
Promise = require('bluebird'),
|
||||
schema = require('../../data/schema'),
|
||||
utils = require('../../utils'),
|
||||
labs = require('../../utils/labs'),
|
||||
uuid = require('node-uuid'),
|
||||
validation = require('../../data/validation'),
|
||||
plugins = require('../plugins'),
|
||||
i18n = require('../../i18n'),
|
||||
|
||||
ghostBookshelf,
|
||||
proto;
|
||||
|
||||
|
@ -217,6 +218,10 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
|
|||
// Get a specific updated attribute value
|
||||
updated: function updated(attr) {
|
||||
return this.updatedAttributes()[attr];
|
||||
},
|
||||
|
||||
hasDateChanged: function (attr) {
|
||||
return moment(this.get(attr)).diff(moment(this.updated(attr))) !== 0;
|
||||
}
|
||||
}, {
|
||||
// ## Data Utility Functions
|
||||
|
|
|
@ -2,7 +2,7 @@ var config = require('../../config'),
|
|||
events = require(config.paths.corePath + '/server/events'),
|
||||
models = require(config.paths.corePath + '/server/models'),
|
||||
errors = require(config.paths.corePath + '/server/errors'),
|
||||
Promise = require('bluebird'),
|
||||
sequence = require(config.paths.corePath + '/server/utils/sequence'),
|
||||
moment = require('moment-timezone');
|
||||
|
||||
/**
|
||||
|
@ -36,27 +36,29 @@ events.on('settings.activeTimezone.edited', function (settingModel) {
|
|||
return;
|
||||
}
|
||||
|
||||
return Promise.mapSeries(results.map(function (post) {
|
||||
var newPublishedAtMoment = moment(post.get('published_at')).add(timezoneOffset, 'minutes');
|
||||
return sequence(results.map(function (post) {
|
||||
return function reschedulePostIfPossible() {
|
||||
var newPublishedAtMoment = moment(post.get('published_at')).add(timezoneOffset, 'minutes');
|
||||
|
||||
/**
|
||||
* CASE:
|
||||
* - your configured TZ is GMT+01:00
|
||||
* - now is 10AM +01:00 (9AM UTC)
|
||||
* - your post should be published 8PM +01:00 (7PM UTC)
|
||||
* - you reconfigure your blog TZ to GMT+08:00
|
||||
* - now is 5PM +08:00 (9AM UTC)
|
||||
* - if we don't change the published_at, 7PM + 8 hours === next day 5AM
|
||||
* - so we update published_at to 7PM - 480minutes === 11AM UTC
|
||||
* - 11AM UTC === 7PM +08:00
|
||||
*/
|
||||
if (newPublishedAtMoment.isBefore(moment().add(5, 'minutes'))) {
|
||||
post.set('status', 'draft');
|
||||
} else {
|
||||
post.set('published_at', newPublishedAtMoment.toDate());
|
||||
}
|
||||
/**
|
||||
* CASE:
|
||||
* - your configured TZ is GMT+01:00
|
||||
* - now is 10AM +01:00 (9AM UTC)
|
||||
* - your post should be published 8PM +01:00 (7PM UTC)
|
||||
* - you reconfigure your blog TZ to GMT+08:00
|
||||
* - now is 5PM +08:00 (9AM UTC)
|
||||
* - if we don't change the published_at, 7PM + 8 hours === next day 5AM
|
||||
* - so we update published_at to 7PM - 480minutes === 11AM UTC
|
||||
* - 11AM UTC === 7PM +08:00
|
||||
*/
|
||||
if (newPublishedAtMoment.isBefore(moment().add(5, 'minutes'))) {
|
||||
post.set('status', 'draft');
|
||||
} else {
|
||||
post.set('published_at', newPublishedAtMoment.toDate());
|
||||
}
|
||||
|
||||
return models.Post.edit(post.toJSON(), {id: post.id, context: {internal: true}}).reflect();
|
||||
return models.Post.edit(post.toJSON(), {id: post.id, context: {internal: true}}).reflect();
|
||||
};
|
||||
})).each(function (result) {
|
||||
if (!result.isFulfilled()) {
|
||||
errors.logError(result.reason());
|
||||
|
|
|
@ -36,7 +36,7 @@ Client = ghostBookshelf.Model.extend({
|
|||
// whitelists for the `options` hash argument on methods, by method name.
|
||||
// these are the only options that can be passed to Bookshelf / Knex.
|
||||
validOptions = {
|
||||
findOne: ['withRelated']
|
||||
findOne: ['columns', 'withRelated']
|
||||
};
|
||||
|
||||
if (validOptions[methodName]) {
|
||||
|
|
|
@ -60,7 +60,8 @@ Post = ghostBookshelf.Model.extend({
|
|||
model.wasPublished = model.updated('status') === 'published';
|
||||
model.wasScheduled = model.updated('status') === 'scheduled';
|
||||
model.resourceTypeChanging = model.get('page') !== model.updated('page');
|
||||
model.needsReschedule = model.get('published_at') !== model.updated('published_at');
|
||||
model.publishedAtHasChanged = model.hasDateChanged('published_at');
|
||||
model.needsReschedule = model.publishedAtHasChanged && model.isScheduled;
|
||||
|
||||
// Handle added and deleted for post -> page or page -> post
|
||||
if (model.resourceTypeChanging) {
|
||||
|
@ -100,7 +101,7 @@ Post = ghostBookshelf.Model.extend({
|
|||
}
|
||||
|
||||
// CASE: from scheduled to something
|
||||
if (model.wasScheduled && !model.isScheduled) {
|
||||
if (model.wasScheduled && !model.isScheduled && !model.isPublished) {
|
||||
model.emitChange('unscheduled');
|
||||
}
|
||||
} else {
|
||||
|
@ -137,13 +138,23 @@ Post = ghostBookshelf.Model.extend({
|
|||
// Variables to make the slug checking more readable
|
||||
newTitle = this.get('title'),
|
||||
newStatus = this.get('status'),
|
||||
olderStatus = this.previous('status'),
|
||||
prevTitle = this._previousAttributes.title,
|
||||
prevSlug = this._previousAttributes.slug,
|
||||
tagsToCheck = this.get('tags'),
|
||||
publishedAt = this.get('published_at'),
|
||||
publishedAtHasChanged = this.hasDateChanged('published_at'),
|
||||
tags = [];
|
||||
|
||||
// both page and post can get scheduled
|
||||
// CASE: disallow published -> scheduled
|
||||
// @TODO: remove when we have versioning based on updated_at
|
||||
if (newStatus !== olderStatus && newStatus === 'scheduled' && olderStatus === 'published') {
|
||||
return Promise.reject(new errors.ValidationError(
|
||||
i18n.t('errors.models.post.isAlreadyPublished', {key: 'status'})
|
||||
));
|
||||
}
|
||||
|
||||
// CASE: both page and post can get scheduled
|
||||
if (newStatus === 'scheduled') {
|
||||
if (!publishedAt) {
|
||||
return Promise.reject(new errors.ValidationError(
|
||||
|
@ -153,13 +164,12 @@ Post = ghostBookshelf.Model.extend({
|
|||
return Promise.reject(new errors.ValidationError(
|
||||
i18n.t('errors.models.post.valueCannotBeBlank', {key: 'published_at'})
|
||||
));
|
||||
} else if (moment(publishedAt).isBefore(moment())) {
|
||||
// CASE: to schedule/reschedule a post, a minimum diff of x minutes is needed (default configured is 2minutes)
|
||||
} else if (publishedAtHasChanged && moment(publishedAt).isBefore(moment().add(config.times.cannotScheduleAPostBeforeInMinutes, 'minutes'))) {
|
||||
return Promise.reject(new errors.ValidationError(
|
||||
i18n.t('errors.models.post.expectedPublishedAtInFuture')
|
||||
));
|
||||
} else if (moment(publishedAt).isBefore(moment().add(5, 'minutes'))) {
|
||||
return Promise.reject(new errors.ValidationError(
|
||||
i18n.t('errors.models.post.expectedPublishedAtInFuture')
|
||||
i18n.t('errors.models.post.expectedPublishedAtInFuture', {
|
||||
cannotScheduleAPostBeforeInMinutes: config.times.cannotScheduleAPostBeforeInMinutes
|
||||
})
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -447,11 +457,11 @@ Post = ghostBookshelf.Model.extend({
|
|||
// the status provided.
|
||||
if (options.status && options.status !== 'all') {
|
||||
// make sure that status is valid
|
||||
options.status = _.includes(['published', 'draft'], options.status) ? options.status : 'published';
|
||||
options.status = _.includes(['published', 'draft', 'scheduled'], options.status) ? options.status : 'published';
|
||||
options.where.statements.push({prop: 'status', op: '=', value: options.status});
|
||||
delete options.status;
|
||||
} else if (options.status === 'all') {
|
||||
options.where.statements.push({prop: 'status', op: 'IN', value: ['published', 'draft']});
|
||||
options.where.statements.push({prop: 'status', op: 'IN', value: ['published', 'draft', 'scheduled']});
|
||||
delete options.status;
|
||||
}
|
||||
|
||||
|
|
|
@ -44,6 +44,9 @@ apiRoutes = function apiRoutes(middleware) {
|
|||
router.put('/posts/:id', authenticatePrivate, api.http(api.posts.edit));
|
||||
router.del('/posts/:id', authenticatePrivate, api.http(api.posts.destroy));
|
||||
|
||||
// ## Schedules
|
||||
router.put('/schedules/posts/:id', [middleware.api.authenticateClient, middleware.api.authenticateUser], api.http(api.schedules.publishPost));
|
||||
|
||||
// ## Settings
|
||||
router.get('/settings', authenticatePrivate, api.http(api.settings.browse));
|
||||
router.get('/settings/:key', authenticatePrivate, api.http(api.settings.read));
|
||||
|
|
8
core/server/scheduling/SchedulingBase.js
Normal file
8
core/server/scheduling/SchedulingBase.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
function SchedulingBase() {
|
||||
Object.defineProperty(this, 'requiredFns', {
|
||||
value: ['schedule', 'unschedule', 'reschedule', 'run'],
|
||||
writable: false
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = SchedulingBase;
|
192
core/server/scheduling/SchedulingDefault.js
Normal file
192
core/server/scheduling/SchedulingDefault.js
Normal file
|
@ -0,0 +1,192 @@
|
|||
var util = require('util'),
|
||||
moment = require('moment'),
|
||||
request = require('superagent'),
|
||||
SchedulingBase = require(__dirname + '/SchedulingBase'),
|
||||
errors = require(__dirname + '/../errors');
|
||||
|
||||
/**
|
||||
* allJobs is a sorted list by time attribute
|
||||
*/
|
||||
function SchedulingDefault(options) {
|
||||
SchedulingBase.call(this, options);
|
||||
|
||||
this.runTimeoutInMs = 1000 * 60 * 5;
|
||||
this.offsetInMinutes = 10;
|
||||
this.beforePingInMs = -50;
|
||||
|
||||
this.allJobs = {};
|
||||
this.deletedJobs = {};
|
||||
}
|
||||
|
||||
util.inherits(SchedulingDefault, SchedulingBase);
|
||||
|
||||
/**
|
||||
* add to list
|
||||
*/
|
||||
SchedulingDefault.prototype.schedule = function (object) {
|
||||
this._addJob(object);
|
||||
};
|
||||
|
||||
/**
|
||||
* remove from list
|
||||
* add to list
|
||||
*/
|
||||
SchedulingDefault.prototype.reschedule = function (object) {
|
||||
this._deleteJob({time: object.extra.oldTime, url: object.url});
|
||||
this._addJob(object);
|
||||
};
|
||||
|
||||
/**
|
||||
* remove from list
|
||||
* deletion happens right before execution
|
||||
*/
|
||||
SchedulingDefault.prototype.unschedule = function (object) {
|
||||
this._deleteJob(object);
|
||||
};
|
||||
|
||||
/**
|
||||
* check if there are new jobs which needs to be published in the next x minutes
|
||||
* because allJobs is a sorted list, we don't have to iterate over all jobs, just until the offset is too big
|
||||
*/
|
||||
SchedulingDefault.prototype.run = function () {
|
||||
var self = this,
|
||||
timeout = null;
|
||||
|
||||
timeout = setTimeout(function () {
|
||||
var times = Object.keys(self.allJobs),
|
||||
nextJobs = {};
|
||||
|
||||
times.every(function (time) {
|
||||
if (moment(Number(time)).diff(moment(), 'minutes') <= self.offsetInMinutes) {
|
||||
nextJobs[time] = self.allJobs[time];
|
||||
delete self.allJobs[time];
|
||||
return true;
|
||||
}
|
||||
|
||||
// break!
|
||||
return false;
|
||||
});
|
||||
|
||||
clearTimeout(timeout);
|
||||
self._execute(nextJobs);
|
||||
|
||||
// recursive!
|
||||
self.run();
|
||||
}, self.runTimeoutInMs);
|
||||
};
|
||||
|
||||
/**
|
||||
* each timestamp key entry can have multiple jobs
|
||||
*/
|
||||
SchedulingDefault.prototype._addJob = function (object) {
|
||||
var timestamp = moment(object.time).valueOf(),
|
||||
keys = [],
|
||||
sortedJobs = {},
|
||||
instantJob = {},
|
||||
i = 0;
|
||||
|
||||
// CASE: should have been already pinged or should be pinged soon
|
||||
if (moment(timestamp).diff(moment(), 'minutes') < this.offsetInMinutes) {
|
||||
instantJob[timestamp] = [object];
|
||||
this._execute(instantJob);
|
||||
return;
|
||||
}
|
||||
|
||||
// CASE: are there jobs already scheduled for the same time?
|
||||
if (!this.allJobs[timestamp]) {
|
||||
this.allJobs[timestamp] = [];
|
||||
}
|
||||
|
||||
this.allJobs[timestamp].push(object);
|
||||
|
||||
keys = Object.keys(this.allJobs);
|
||||
keys.sort();
|
||||
|
||||
for (i = 0; i < keys.length; i = i + 1) {
|
||||
sortedJobs[keys[i]] = this.allJobs[keys[i]];
|
||||
}
|
||||
|
||||
this.allJobs = sortedJobs;
|
||||
};
|
||||
|
||||
SchedulingDefault.prototype._deleteJob = function (object) {
|
||||
this.deletedJobs[object.url + '_' + moment(object.time).valueOf()] = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* ping jobs
|
||||
* setTimeout is not accurate, but we can live with that fact and use setImmediate feature to qualify
|
||||
* we don't want to use process.nextTick, this would block any I/O operation
|
||||
*/
|
||||
SchedulingDefault.prototype._execute = function (jobs) {
|
||||
var keys = Object.keys(jobs),
|
||||
self = this;
|
||||
|
||||
keys.forEach(function (timestamp) {
|
||||
var timeout = null,
|
||||
diff = moment(Number(timestamp)).diff(moment());
|
||||
|
||||
// awake a little before
|
||||
timeout = setTimeout(function () {
|
||||
clearTimeout(timeout);
|
||||
|
||||
(function retry() {
|
||||
var immediate = setImmediate(function () {
|
||||
clearImmediate(immediate);
|
||||
|
||||
if (moment().diff(moment(Number(timestamp))) <= self.beforePingInMs) {
|
||||
return retry();
|
||||
}
|
||||
|
||||
var toExecute = jobs[timestamp];
|
||||
delete jobs[timestamp];
|
||||
|
||||
toExecute.forEach(function (job) {
|
||||
var deleteKey = job.url + '_' + moment(job.time).valueOf();
|
||||
|
||||
if (self.deletedJobs[deleteKey]) {
|
||||
delete self.deletedJobs[deleteKey];
|
||||
return;
|
||||
}
|
||||
|
||||
self._pingUrl(job);
|
||||
});
|
||||
});
|
||||
})();
|
||||
}, diff - 200);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* if we detect to publish a post in the past (case blog is down)
|
||||
* we add a force flag
|
||||
*/
|
||||
SchedulingDefault.prototype._pingUrl = function (object) {
|
||||
var url = object.url,
|
||||
time = object.time,
|
||||
httpMethod = object.extra.httpMethod,
|
||||
req = request[httpMethod.toLowerCase()](url);
|
||||
|
||||
if (moment(time).isBefore(moment())) {
|
||||
if (httpMethod === 'GET') {
|
||||
req.query('force=true');
|
||||
} else {
|
||||
req.send({
|
||||
force: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
req.end(function (err, response) {
|
||||
if (err) {
|
||||
// CASE: post/page was deleted already
|
||||
if (response && response.status === 404) {
|
||||
return;
|
||||
}
|
||||
|
||||
errors.logError(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = SchedulingDefault;
|
12
core/server/scheduling/index.js
Normal file
12
core/server/scheduling/index.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
var _ = require('lodash'),
|
||||
postScheduling = require(__dirname + '/post-scheduling');
|
||||
|
||||
/**
|
||||
* scheduling modules:
|
||||
* - post scheduling: publish posts/pages when scheduled
|
||||
*/
|
||||
exports.init = function init(options) {
|
||||
options = options || {};
|
||||
|
||||
return postScheduling.init(_.pick(options, 'postScheduling', 'apiUrl'));
|
||||
};
|
99
core/server/scheduling/post-scheduling/index.js
Normal file
99
core/server/scheduling/post-scheduling/index.js
Normal file
|
@ -0,0 +1,99 @@
|
|||
var Promise = require('bluebird'),
|
||||
moment = require('moment'),
|
||||
utils = require(__dirname + '/../utils'),
|
||||
events = require(__dirname + '/../../events'),
|
||||
errors = require(__dirname + '/../../errors'),
|
||||
models = require(__dirname + '/../../models'),
|
||||
schedules = require(__dirname + '/../../api/schedules'),
|
||||
_private = {};
|
||||
|
||||
_private.normalize = function normalize(options) {
|
||||
var object = options.object,
|
||||
apiUrl = options.apiUrl,
|
||||
client = options.client;
|
||||
|
||||
return {
|
||||
time: object.get('published_at'),
|
||||
url: apiUrl + '/schedules/posts/' + object.get('id') + '?client_id=' + client.get('slug') + '&client_secret=' + client.get('secret'),
|
||||
extra: {
|
||||
httpMethod: 'PUT',
|
||||
oldTime: object.updated('published_at') || null
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
_private.loadClient = function loadClient() {
|
||||
return models.Client.findOne({slug: 'ghost-scheduler'}, {columns: ['slug', 'secret']});
|
||||
};
|
||||
|
||||
_private.loadScheduledPosts = function () {
|
||||
return schedules.getScheduledPosts({
|
||||
from: moment().subtract(7, 'days').startOf('day').toDate(),
|
||||
to: moment().endOf('day').toDate()
|
||||
}).then(function (result) {
|
||||
return result.posts || [];
|
||||
});
|
||||
};
|
||||
|
||||
exports.init = function init(options) {
|
||||
options = options || {};
|
||||
|
||||
var config = options.postScheduling,
|
||||
apiUrl = options.apiUrl,
|
||||
adapter = null,
|
||||
client = null;
|
||||
|
||||
if (!config) {
|
||||
return Promise.reject(new errors.IncorrectUsage('post-scheduling: no config was provided'));
|
||||
}
|
||||
|
||||
if (!apiUrl) {
|
||||
return Promise.reject(new errors.IncorrectUsage('post-scheduling: no apiUrl was provided'));
|
||||
}
|
||||
|
||||
return _private.loadClient()
|
||||
.then(function (_client) {
|
||||
client = _client;
|
||||
|
||||
return utils.createAdapter(config);
|
||||
})
|
||||
.then(function (_adapter) {
|
||||
adapter = _adapter;
|
||||
|
||||
return _private.loadScheduledPosts();
|
||||
})
|
||||
.then(function (scheduledPosts) {
|
||||
if (!scheduledPosts.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
scheduledPosts.forEach(function (object) {
|
||||
adapter.reschedule(_private.normalize({object: object, apiUrl: apiUrl, client: client}));
|
||||
});
|
||||
})
|
||||
.then(function () {
|
||||
adapter.run();
|
||||
})
|
||||
.then(function () {
|
||||
events.onMany([
|
||||
'post.scheduled',
|
||||
'page.scheduled'
|
||||
], function (object) {
|
||||
adapter.schedule(_private.normalize({object: object, apiUrl: apiUrl, client: client}));
|
||||
});
|
||||
|
||||
events.onMany([
|
||||
'post.rescheduled',
|
||||
'page.rescheduled'
|
||||
], function (object) {
|
||||
adapter.reschedule(_private.normalize({object: object, apiUrl: apiUrl, client: client}));
|
||||
});
|
||||
|
||||
events.onMany([
|
||||
'post.unscheduled',
|
||||
'page.unscheduled'
|
||||
], function (object) {
|
||||
adapter.unschedule(_private.normalize({object: object, apiUrl: apiUrl, client: client}));
|
||||
});
|
||||
});
|
||||
};
|
54
core/server/scheduling/utils.js
Normal file
54
core/server/scheduling/utils.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
var _ = require('lodash'),
|
||||
Promise = require('bluebird'),
|
||||
SchedulingBase = require(__dirname + '/SchedulingBase'),
|
||||
errors = require(__dirname + '/../errors');
|
||||
|
||||
exports.createAdapter = function (options) {
|
||||
options = options || {};
|
||||
|
||||
var adapter = null,
|
||||
activeAdapter = options.active,
|
||||
path = options.path;
|
||||
|
||||
if (!activeAdapter) {
|
||||
return Promise.reject(new errors.IncorrectUsage('Please provide an active adapter.'));
|
||||
}
|
||||
|
||||
/**
|
||||
* CASE: active adapter is a npm module
|
||||
*/
|
||||
try {
|
||||
adapter = new (require(activeAdapter))(options);
|
||||
} catch (err) {
|
||||
if (err.code !== 'MODULE_NOT_FOUND') {
|
||||
return Promise.reject(new errors.IncorrectUsage(err.message));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CASE: active adapter is located in specific ghost path
|
||||
*/
|
||||
try {
|
||||
adapter = adapter || new (require(path + activeAdapter))(options);
|
||||
} catch (err) {
|
||||
if (err.code === 'MODULE_NOT_FOUND') {
|
||||
return Promise.reject(new errors.IncorrectUsage('MODULE_NOT_FOUND', activeAdapter));
|
||||
}
|
||||
|
||||
return Promise.reject(new errors.IncorrectUsage(err.message));
|
||||
}
|
||||
|
||||
if (!(adapter instanceof SchedulingBase)) {
|
||||
return Promise.reject(new errors.IncorrectUsage('Your adapter does not inherit from the SchedulingBase.'));
|
||||
}
|
||||
|
||||
if (!adapter.requiredFns) {
|
||||
return Promise.reject(new errors.IncorrectUsage('Your adapter does not provide the minimum required functions.'));
|
||||
}
|
||||
|
||||
if (_.xor(adapter.requiredFns, Object.keys(_.pick(Object.getPrototypeOf(adapter), adapter.requiredFns))).length) {
|
||||
return Promise.reject(new errors.IncorrectUsage('Your adapter does not provide the minimum required functions.'));
|
||||
}
|
||||
|
||||
return Promise.resolve(adapter);
|
||||
};
|
|
@ -206,7 +206,8 @@
|
|||
"post": {
|
||||
"untitled": "(Untitled)",
|
||||
"valueCannotBeBlank": "Value in {key} cannot be blank.",
|
||||
"expectedPublishedAtInFuture": "Expected published_at to be in the future.",
|
||||
"isAlreadyPublished": "Your post is already published, please reload your page.",
|
||||
"expectedPublishedAtInFuture": "Date must be at least {cannotScheduleAPostBeforeInMinutes} minutes in the future.",
|
||||
"noUserFound": "No user found",
|
||||
"notEnoughPermission": "You do not have permission to perform this action",
|
||||
"tagUpdates": {
|
||||
|
@ -266,7 +267,7 @@
|
|||
},
|
||||
"permissions": {
|
||||
"noActionsMapFound": {
|
||||
"error": "No actions map found, please call permissions.init() before use."
|
||||
"error": "No actions map found, ensure you have loaded permissions into database and then call permissions.init() before use."
|
||||
},
|
||||
"applyStatusRules": {
|
||||
"error": "You do not have permission to retrieve {docName} with that status"
|
||||
|
@ -323,6 +324,10 @@
|
|||
"posts": {
|
||||
"postNotFound": "Post not found."
|
||||
},
|
||||
"job": {
|
||||
"notFound": "Job not found.",
|
||||
"publishInThePast": "Use the force flag to publish a post in the past."
|
||||
},
|
||||
"settings": {
|
||||
"problemFindingSetting": "Problem finding setting: {key}",
|
||||
"accessCoreSettingFromExtReq": "Attempted to access core setting from external request",
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
var Promise = require('bluebird');
|
||||
|
||||
/**
|
||||
* expects an array of functions returning a promise
|
||||
*/
|
||||
function sequence(tasks /* Any Arguments */) {
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
|
||||
return Promise.reduce(tasks, function (results, task) {
|
||||
return task.apply(this, args).then(function (result) {
|
||||
results.push(result);
|
||||
|
||||
return results;
|
||||
});
|
||||
}, []);
|
||||
|
|
|
@ -94,7 +94,7 @@ describe('Post API', function () {
|
|||
var jsonResponse = res.body;
|
||||
should.exist(jsonResponse.posts);
|
||||
testUtils.API.checkResponse(jsonResponse, 'posts');
|
||||
jsonResponse.posts.should.have.length(8);
|
||||
jsonResponse.posts.should.have.length(9);
|
||||
testUtils.API.checkResponse(jsonResponse.posts[0], 'post');
|
||||
testUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
|
||||
done();
|
||||
|
@ -166,6 +166,28 @@ describe('Post API', function () {
|
|||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can retrieve just scheduled posts', function (done) {
|
||||
request.get(testUtils.API.getApiQuery('posts/?status=scheduled'))
|
||||
.set('Authorization', 'Bearer ' + accesstoken)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(200)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
var jsonResponse = res.body;
|
||||
should.exist(jsonResponse.posts);
|
||||
testUtils.API.checkResponse(jsonResponse, 'posts');
|
||||
jsonResponse.posts.should.have.length(1);
|
||||
testUtils.API.checkResponse(jsonResponse.posts[0], 'post');
|
||||
testUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ## Read
|
||||
|
|
|
@ -2,8 +2,7 @@
|
|||
var testUtils = require('../../utils'),
|
||||
should = require('should'),
|
||||
_ = require('lodash'),
|
||||
|
||||
// Stuff we are testing
|
||||
errors = require('../../../server/errors'),
|
||||
PostAPI = require('../../../server/api/posts');
|
||||
|
||||
describe('Post API', function () {
|
||||
|
@ -183,10 +182,10 @@ describe('Post API', function () {
|
|||
should.exist(results);
|
||||
testUtils.API.checkResponse(results, 'posts');
|
||||
should.exist(results.posts);
|
||||
results.posts.length.should.eql(5);
|
||||
results.posts[0].status.should.eql('draft');
|
||||
testUtils.API.checkResponse(results.posts[0], 'post');
|
||||
|
||||
// DataGenerator creates 6 posts by default + 2 static pages
|
||||
results.posts.length.should.eql(6);
|
||||
testUtils.API.checkResponse(results.posts[0], 'post');
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
@ -245,7 +244,7 @@ describe('Post API', function () {
|
|||
it('can fetch all posts for an author', function (done) {
|
||||
PostAPI.browse({context: {user: 1}, status: 'all', filter: 'author:joe-bloggs', include: 'author'}).then(function (results) {
|
||||
should.exist(results.posts);
|
||||
results.posts.length.should.eql(5);
|
||||
results.posts.length.should.eql(6);
|
||||
|
||||
_.each(results.posts, function (post) {
|
||||
post.author.slug.should.eql('joe-bloggs');
|
||||
|
@ -342,7 +341,7 @@ describe('Post API', function () {
|
|||
it('can order posts using asc', function (done) {
|
||||
var posts, expectedTitles;
|
||||
|
||||
posts = _(testUtils.DataGenerator.Content.posts).reject('page').reject({status: 'scheduled'}).value();
|
||||
posts = _(testUtils.DataGenerator.Content.posts).reject('page').value();
|
||||
expectedTitles = _(posts).map('title').sortBy().value();
|
||||
|
||||
PostAPI.browse({context: {user: 1}, status: 'all', order: 'title asc', fields: 'title'}).then(function (results) {
|
||||
|
@ -358,7 +357,7 @@ describe('Post API', function () {
|
|||
it('can order posts using desc', function (done) {
|
||||
var posts, expectedTitles;
|
||||
|
||||
posts = _(testUtils.DataGenerator.Content.posts).reject('page').reject({status: 'scheduled'}).value();
|
||||
posts = _(testUtils.DataGenerator.Content.posts).reject('page').value();
|
||||
expectedTitles = _(posts).map('title').sortBy().reverse().value();
|
||||
|
||||
PostAPI.browse({context: {user: 1}, status: 'all', order: 'title DESC', fields: 'title'}).then(function (results) {
|
||||
|
@ -374,7 +373,7 @@ describe('Post API', function () {
|
|||
it('can order posts and filter disallowed attributes', function (done) {
|
||||
var posts, expectedTitles;
|
||||
|
||||
posts = _(testUtils.DataGenerator.Content.posts).reject('page').reject({status: 'scheduled'}).value();
|
||||
posts = _(testUtils.DataGenerator.Content.posts).reject('page').value();
|
||||
expectedTitles = _(posts).map('title').sortBy().value();
|
||||
|
||||
PostAPI.browse({context: {user: 1}, status: 'all', order: 'bunny DESC, title ASC', fields: 'title'}).then(function (results) {
|
||||
|
@ -586,6 +585,24 @@ describe('Post API', function () {
|
|||
});
|
||||
|
||||
describe('Edit', function () {
|
||||
it('can edit own post', function (done) {
|
||||
PostAPI.edit({posts:[{status: 'test'}]}, {context: {user: 1}, id: 1}).then(function (results) {
|
||||
should.exist(results.posts);
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('cannot edit others post', function (done) {
|
||||
testUtils.fixtures.insertOne('users', 'createUser', 4)
|
||||
.then(function (result) {
|
||||
PostAPI.edit({posts: [{status: 'test'}]}, {context: {user: result[0]}, id: 1}).catch(function (err) {
|
||||
should.exist(err);
|
||||
(err instanceof errors.NoPermissionError).should.eql(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// These tests are for #6920
|
||||
it('should update post & not delete tags with `tags` not included', function (done) {
|
||||
var options = {context: {user: 1}, id: 1},
|
||||
|
|
451
core/test/integration/api/api_schedules_spec.js
Normal file
451
core/test/integration/api/api_schedules_spec.js
Normal file
|
@ -0,0 +1,451 @@
|
|||
/*globals describe, it, after, before, beforeEach, afterEach */
|
||||
|
||||
var should = require('should'),
|
||||
moment = require('moment'),
|
||||
Promise = require('bluebird'),
|
||||
testUtils = require('../../utils'),
|
||||
config = require(__dirname + '/../../../server/config'),
|
||||
sequence = require(config.paths.corePath + '/server/utils/sequence'),
|
||||
errors = require(config.paths.corePath + '/server/errors'),
|
||||
api = require(config.paths.corePath + '/server/api'),
|
||||
models = require(config.paths.corePath + '/server/models');
|
||||
|
||||
describe('Schedules API', function () {
|
||||
var scope = {posts: []};
|
||||
|
||||
after(function (done) {
|
||||
testUtils.teardown(done);
|
||||
});
|
||||
|
||||
describe('fn: getScheduledPosts', function () {
|
||||
before(function (done) {
|
||||
sequence([
|
||||
testUtils.teardown,
|
||||
testUtils.setup('clients', 'users:roles', 'perms:post', 'perms:init')
|
||||
]).then(function () {
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
describe('success', function () {
|
||||
before(function (done) {
|
||||
scope.posts.push(testUtils.DataGenerator.forKnex.createPost({
|
||||
created_by: testUtils.users.ids.editor,
|
||||
author_id: testUtils.users.ids.editor,
|
||||
published_by: testUtils.users.ids.editor,
|
||||
created_at: moment().add(2, 'days').set('hours', 8).toDate(),
|
||||
published_at: moment().add(5, 'days').toDate(),
|
||||
status: 'scheduled',
|
||||
slug: '2'
|
||||
}));
|
||||
|
||||
scope.posts.push(testUtils.DataGenerator.forKnex.createPost({
|
||||
created_by: testUtils.users.ids.owner,
|
||||
author_id: testUtils.users.ids.owner,
|
||||
published_by: testUtils.users.ids.owner,
|
||||
created_at: moment().add(2, 'days').set('hours', 12).toDate(),
|
||||
published_at: moment().add(5, 'days').toDate(),
|
||||
status: 'scheduled',
|
||||
page: 1,
|
||||
slug: '5'
|
||||
}));
|
||||
|
||||
scope.posts.push(testUtils.DataGenerator.forKnex.createPost({
|
||||
created_by: testUtils.users.ids.author,
|
||||
author_id: testUtils.users.ids.author,
|
||||
published_by: testUtils.users.ids.author,
|
||||
created_at: moment().add(5, 'days').set('hours', 6).toDate(),
|
||||
published_at: moment().add(10, 'days').toDate(),
|
||||
status: 'scheduled',
|
||||
slug: '1'
|
||||
}));
|
||||
|
||||
scope.posts.push(testUtils.DataGenerator.forKnex.createPost({
|
||||
created_by: testUtils.users.ids.owner,
|
||||
author_id: testUtils.users.ids.owner,
|
||||
published_by: testUtils.users.ids.owner,
|
||||
created_at: moment().add(6, 'days').set('hours', 10).set('minutes', 0).toDate(),
|
||||
published_at: moment().add(7, 'days').toDate(),
|
||||
status: 'scheduled',
|
||||
slug: '3'
|
||||
}));
|
||||
|
||||
scope.posts.push(testUtils.DataGenerator.forKnex.createPost({
|
||||
created_by: testUtils.users.ids.owner,
|
||||
author_id: testUtils.users.ids.owner,
|
||||
published_by: testUtils.users.ids.owner,
|
||||
created_at: moment().add(6, 'days').set('hours', 11).toDate(),
|
||||
published_at: moment().add(8, 'days').toDate(),
|
||||
status: 'scheduled',
|
||||
slug: '4'
|
||||
}));
|
||||
|
||||
scope.posts.push(testUtils.DataGenerator.forKnex.createPost({
|
||||
created_by: testUtils.users.ids.owner,
|
||||
author_id: testUtils.users.ids.owner,
|
||||
published_by: testUtils.users.ids.owner,
|
||||
status: 'draft',
|
||||
slug: '6'
|
||||
}));
|
||||
|
||||
Promise.all(scope.posts.map(function (post) {
|
||||
return models.Post.add(post, {context: {internal: true}, importing: true});
|
||||
})).then(function () {
|
||||
return done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
after(function () {
|
||||
scope.posts = [];
|
||||
});
|
||||
|
||||
it('all', function (done) {
|
||||
api.schedules.getScheduledPosts()
|
||||
.then(function (result) {
|
||||
result.posts.length.should.eql(5);
|
||||
Object.keys(result.posts[0].toJSON()).should.eql(['id', 'published_at', 'created_at', 'author', 'url']);
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('for specific datetime', function (done) {
|
||||
api.schedules.getScheduledPosts({
|
||||
from: moment().add(2, 'days').startOf('day').toDate(),
|
||||
to: moment().add(2, 'days').endOf('day').toDate()
|
||||
}).then(function (result) {
|
||||
result.posts.length.should.eql(2);
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('for specific datetime', function (done) {
|
||||
api.schedules.getScheduledPosts({
|
||||
from: moment().add(2, 'days').startOf('day').toDate(),
|
||||
to: moment().add(2, 'days').set('hours', 8).toDate()
|
||||
}).then(function (result) {
|
||||
result.posts.length.should.eql(1);
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('for specific date', function (done) {
|
||||
api.schedules.getScheduledPosts({
|
||||
from: moment().add(5, 'days').startOf('day').toDate(),
|
||||
to: moment().add(6, 'days').endOf('day').toDate()
|
||||
}).then(function (result) {
|
||||
result.posts.length.should.eql(3);
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('for specific date', function (done) {
|
||||
api.schedules.getScheduledPosts({
|
||||
from: moment().add(6, 'days').set('hours', 10).set('minutes', 30).toDate(),
|
||||
to: moment().add(6, 'days').endOf('day').toDate()
|
||||
}).then(function (result) {
|
||||
result.posts.length.should.eql(1);
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('for specific date', function (done) {
|
||||
api.schedules.getScheduledPosts({
|
||||
from: moment().add(1, 'days').toDate()
|
||||
}).then(function (result) {
|
||||
result.posts.length.should.eql(5);
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error', function () {
|
||||
it('from is invalid', function (done) {
|
||||
api.schedules.getScheduledPosts({
|
||||
from: 'bee'
|
||||
}).catch(function (err) {
|
||||
should.exist(err);
|
||||
(err instanceof errors.ValidationError).should.eql(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fn: publishPost', function () {
|
||||
var originalCannotScheduleAPostBeforeInMinutes;
|
||||
|
||||
beforeEach(function (done) {
|
||||
originalCannotScheduleAPostBeforeInMinutes = config.times.cannotScheduleAPostBeforeInMinutes;
|
||||
|
||||
// we can insert published_at less then 5minutes
|
||||
config.times.cannotScheduleAPostBeforeInMinutes = -15;
|
||||
|
||||
sequence([
|
||||
testUtils.teardown,
|
||||
testUtils.setup('clients', 'users:roles', 'perms:post', 'perms:init')
|
||||
]).then(function () {
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
after(function () {
|
||||
config.times.cannotScheduleAPostBeforeInMinutes = originalCannotScheduleAPostBeforeInMinutes;
|
||||
});
|
||||
|
||||
describe('success', function () {
|
||||
beforeEach(function (done) {
|
||||
scope.posts.push(testUtils.DataGenerator.forKnex.createPost({
|
||||
created_by: testUtils.users.ids.author,
|
||||
author_id: testUtils.users.ids.author,
|
||||
published_by: testUtils.users.ids.author,
|
||||
published_at: moment().toDate(),
|
||||
status: 'scheduled',
|
||||
slug: 'first'
|
||||
}));
|
||||
|
||||
scope.posts.push(testUtils.DataGenerator.forKnex.createPost({
|
||||
created_by: testUtils.users.ids.author,
|
||||
author_id: testUtils.users.ids.author,
|
||||
published_by: testUtils.users.ids.author,
|
||||
published_at: moment().add(30, 'seconds').toDate(),
|
||||
status: 'scheduled',
|
||||
slug: 'second'
|
||||
}));
|
||||
|
||||
scope.posts.push(testUtils.DataGenerator.forKnex.createPost({
|
||||
created_by: testUtils.users.ids.author,
|
||||
author_id: testUtils.users.ids.author,
|
||||
published_by: testUtils.users.ids.author,
|
||||
published_at: moment().subtract(30, 'seconds').toDate(),
|
||||
status: 'scheduled',
|
||||
slug: 'third'
|
||||
}));
|
||||
|
||||
scope.posts.push(testUtils.DataGenerator.forKnex.createPost({
|
||||
created_by: testUtils.users.ids.author,
|
||||
author_id: testUtils.users.ids.author,
|
||||
published_by: testUtils.users.ids.author,
|
||||
published_at: moment().subtract(10, 'minute').toDate(),
|
||||
status: 'scheduled',
|
||||
slug: 'fourth'
|
||||
}));
|
||||
|
||||
Promise.all(scope.posts.map(function (post) {
|
||||
return models.Post.add(post, {context: {internal: true}});
|
||||
})).then(function (result) {
|
||||
// returns id 1 and 2, but hard to check, because PG returns a different order
|
||||
result.length.should.eql(4);
|
||||
return done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
scope.posts = [];
|
||||
});
|
||||
|
||||
it('client with specific perms has access to publish post', function (done) {
|
||||
api.schedules.publishPost({id: 1, context: {client: 'ghost-scheduler'}})
|
||||
.then(function (result) {
|
||||
result.posts[0].id.should.eql(1);
|
||||
result.posts[0].status.should.eql('published');
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('can publish with tolerance (30 seconds in the future)', function (done) {
|
||||
api.schedules.publishPost({id: 2, context: {client: 'ghost-scheduler'}})
|
||||
.then(function (result) {
|
||||
result.posts[0].id.should.eql(2);
|
||||
result.posts[0].status.should.eql('published');
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('can publish with tolerance (30seconds in the past)', function (done) {
|
||||
api.schedules.publishPost({id: 3, context: {client: 'ghost-scheduler'}})
|
||||
.then(function (result) {
|
||||
result.posts[0].id.should.eql(3);
|
||||
result.posts[0].status.should.eql('published');
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('can publish a post in the past with force flag', function (done) {
|
||||
api.schedules.publishPost({force: true}, {id: 4, context: {client: 'ghost-scheduler'}})
|
||||
.then(function (result) {
|
||||
result.posts[0].id.should.eql(4);
|
||||
result.posts[0].status.should.eql('published');
|
||||
done();
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error', function () {
|
||||
beforeEach(function (done) {
|
||||
scope.posts.push(testUtils.DataGenerator.forKnex.createPost({
|
||||
created_by: testUtils.users.ids.author,
|
||||
author_id: testUtils.users.ids.author,
|
||||
published_by: testUtils.users.ids.author,
|
||||
published_at: moment().add(2, 'days').toDate(),
|
||||
status: 'scheduled',
|
||||
slug: 'first'
|
||||
}));
|
||||
|
||||
scope.posts.push(testUtils.DataGenerator.forKnex.createPost({
|
||||
created_by: testUtils.users.ids.author,
|
||||
author_id: testUtils.users.ids.author,
|
||||
published_by: testUtils.users.ids.author,
|
||||
published_at: moment().add(2, 'days').toDate(),
|
||||
status: 'draft',
|
||||
slug: 'second'
|
||||
}));
|
||||
|
||||
scope.posts.push(testUtils.DataGenerator.forKnex.createPost({
|
||||
created_by: testUtils.users.ids.author,
|
||||
author_id: testUtils.users.ids.author,
|
||||
published_by: testUtils.users.ids.author,
|
||||
published_at: moment().add(4, 'minutes').toDate(),
|
||||
status: 'scheduled',
|
||||
slug: 'third'
|
||||
}));
|
||||
|
||||
scope.posts.push(testUtils.DataGenerator.forKnex.createPost({
|
||||
created_by: testUtils.users.ids.author,
|
||||
author_id: testUtils.users.ids.author,
|
||||
published_by: testUtils.users.ids.author,
|
||||
published_at: moment().subtract(4, 'minutes').toDate(),
|
||||
status: 'scheduled',
|
||||
slug: 'fourth'
|
||||
}));
|
||||
|
||||
Promise.all(scope.posts.map(function (post) {
|
||||
return models.Post.add(post, {context: {internal: true}});
|
||||
})).then(function (result) {
|
||||
result.length.should.eql(4);
|
||||
return done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
scope.posts = [];
|
||||
});
|
||||
|
||||
it('ghost admin has no access', function (done) {
|
||||
api.schedules.publishPost({id: 1, context: {client: 'ghost-admin'}})
|
||||
.then(function () {
|
||||
done(new Error('expected NoPermissionError'));
|
||||
})
|
||||
.catch(function (err) {
|
||||
should.exist(err);
|
||||
(err instanceof errors.NoPermissionError).should.eql(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('owner has no access (this is how it is right now!)', function (done) {
|
||||
api.schedules.publishPost({id: 2, context: {user: testUtils.users.ids.author}})
|
||||
.then(function () {
|
||||
done(new Error('expected NoPermissionError'));
|
||||
})
|
||||
.catch(function (err) {
|
||||
should.exist(err);
|
||||
(err instanceof errors.NoPermissionError).should.eql(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('other user has no access', function (done) {
|
||||
testUtils.fixtures.insertOne('users', 'createUser', 4)
|
||||
.then(function (result) {
|
||||
api.schedules.publishPost({id: 1, context: {user: result[0]}})
|
||||
.then(function () {
|
||||
done(new Error('expected NoPermissionError'));
|
||||
})
|
||||
.catch(function (err) {
|
||||
should.exist(err);
|
||||
(err instanceof errors.NoPermissionError).should.eql(true);
|
||||
done();
|
||||
});
|
||||
})
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('invalid params', function (done) {
|
||||
api.schedules.publishPost({id: 'bla', context: {client: 'ghost-scheduler'}})
|
||||
.then(function () {
|
||||
done(new Error('expected ValidationError'));
|
||||
})
|
||||
.catch(function (err) {
|
||||
should.exist(err);
|
||||
(err instanceof errors.ValidationError).should.eql(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('post does not exist', function (done) {
|
||||
api.schedules.publishPost({id: 10, context: {client: 'ghost-scheduler'}})
|
||||
.then(function () {
|
||||
done(new Error('expected ValidationError'));
|
||||
})
|
||||
.catch(function (err) {
|
||||
should.exist(err);
|
||||
(err instanceof errors.NotFoundError).should.eql(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('publish at a wrong time', function (done) {
|
||||
api.schedules.publishPost({id: 1, context: {client: 'ghost-scheduler'}})
|
||||
.then(function () {
|
||||
done(new Error('expected ValidationError'));
|
||||
})
|
||||
.catch(function (err) {
|
||||
should.exist(err);
|
||||
(err instanceof errors.NotFoundError).should.eql(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('publish at a wrong time', function (done) {
|
||||
api.schedules.publishPost({id: 3, context: {client: 'ghost-scheduler'}})
|
||||
.then(function () {
|
||||
done(new Error('expected ValidationError'));
|
||||
})
|
||||
.catch(function (err) {
|
||||
should.exist(err);
|
||||
(err instanceof errors.NotFoundError).should.eql(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('publish at a wrong time', function (done) {
|
||||
api.schedules.publishPost({id: 4, context: {client: 'ghost-scheduler'}})
|
||||
.then(function () {
|
||||
done(new Error('expected ValidationError'));
|
||||
})
|
||||
.catch(function (err) {
|
||||
should.exist(err);
|
||||
(err instanceof errors.NotFoundError).should.eql(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('publish, but status is draft', function (done) {
|
||||
api.schedules.publishPost({id: 2, context: {client: 'ghost-scheduler'}})
|
||||
.then(function () {
|
||||
done(new Error('expected ValidationError'));
|
||||
})
|
||||
.catch(function (err) {
|
||||
should.exist(err);
|
||||
(err instanceof errors.NotFoundError).should.eql(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -238,7 +238,7 @@ describe('Post Model', function () {
|
|||
paginationResult.meta.pagination.page.should.equal(1);
|
||||
paginationResult.meta.pagination.limit.should.equal('all');
|
||||
paginationResult.meta.pagination.pages.should.equal(1);
|
||||
paginationResult.posts.length.should.equal(107);
|
||||
paginationResult.posts.length.should.equal(108);
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
|
@ -573,6 +573,27 @@ describe('Post Model', function () {
|
|||
}).catch(done);
|
||||
});
|
||||
|
||||
it('scheduled -> scheduled with unchanged published_at', function (done) {
|
||||
PostModel.findOne({status: 'scheduled'}).then(function (results) {
|
||||
var post;
|
||||
|
||||
should.exist(results);
|
||||
post = results.toJSON();
|
||||
post.status.should.equal('scheduled');
|
||||
|
||||
return PostModel.edit({
|
||||
status: 'scheduled'
|
||||
}, _.extend({}, context, {id: post.id}));
|
||||
}).then(function (edited) {
|
||||
should.exist(edited);
|
||||
edited.attributes.status.should.equal('scheduled');
|
||||
eventSpy.callCount.should.eql(1);
|
||||
eventSpy.firstCall.calledWith('post.edited').should.be.true();
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('published -> scheduled and expect update of published_at', function (done) {
|
||||
var postId = 1;
|
||||
|
||||
|
@ -587,16 +608,13 @@ describe('Post Model', function () {
|
|||
status: 'scheduled',
|
||||
published_at: moment().add(1, 'day').toDate()
|
||||
}, _.extend({}, context, {id: postId}));
|
||||
}).then(function (edited) {
|
||||
should.exist(edited);
|
||||
edited.attributes.status.should.equal('scheduled');
|
||||
eventSpy.callCount.should.eql(3);
|
||||
eventSpy.firstCall.calledWith('post.unpublished').should.be.true();
|
||||
eventSpy.secondCall.calledWith('post.scheduled').should.be.true();
|
||||
eventSpy.thirdCall.calledWith('post.edited').should.be.true();
|
||||
|
||||
}).then(function () {
|
||||
done(new Error('change status from published to scheduled is not allowed right now!'));
|
||||
}).catch(function (err) {
|
||||
should.exist(err);
|
||||
(err instanceof errors.ValidationError).should.eql(true);
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('can convert draft post to page and back', function (done) {
|
||||
|
|
33
core/test/unit/api/index_spec.js
Normal file
33
core/test/unit/api/index_spec.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*globals describe, it */
|
||||
var should = require('should'),
|
||||
rewire = require('rewire'),
|
||||
config = rewire('../../../server/config'),
|
||||
api = rewire(config.paths.corePath + '/server/api');
|
||||
|
||||
describe('API: index', function () {
|
||||
describe('fn: cacheInvalidationHeader', function () {
|
||||
it('/schedules/posts should invalidate cache', function () {
|
||||
var cacheInvalidationHeader = api.__get__('cacheInvalidationHeader'),
|
||||
result = cacheInvalidationHeader({
|
||||
_parsedUrl: {
|
||||
pathname: '/schedules/posts/1'
|
||||
},
|
||||
method: 'PUT'
|
||||
}, {});
|
||||
|
||||
result.should.eql('/*');
|
||||
});
|
||||
|
||||
it('/schedules/something should NOT invalidate cache', function () {
|
||||
var cacheInvalidationHeader = api.__get__('cacheInvalidationHeader'),
|
||||
result = cacheInvalidationHeader({
|
||||
_parsedUrl: {
|
||||
pathname: '/schedules/something'
|
||||
},
|
||||
method: 'PUT'
|
||||
}, {});
|
||||
|
||||
should.not.exist(result);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -156,9 +156,20 @@ describe('API Utils', function () {
|
|||
}).catch(done);
|
||||
});
|
||||
|
||||
it('should reject if invalid options are passed', function (done) {
|
||||
it('should reject if limit is invalid', function (done) {
|
||||
apiUtils.validate('test', {opts: apiUtils.browseDefaultOptions})(
|
||||
{context: 'internal', include: 'stuff', page: 1, limit: 'none'}
|
||||
{limit: 'none'}
|
||||
).then(function () {
|
||||
done(new Error('Should have thrown a validation error'));
|
||||
}).catch(function (err) {
|
||||
err.should.have.property('errorType', 'ValidationError');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject if from is invalid', function (done) {
|
||||
apiUtils.validate('test', {opts: ['from']})(
|
||||
{from: true}
|
||||
).then(function () {
|
||||
done(new Error('Should have thrown a validation error'));
|
||||
}).catch(function (err) {
|
||||
|
|
286
core/test/unit/scheduling/SchedulingDefault_spec.js
Normal file
286
core/test/unit/scheduling/SchedulingDefault_spec.js
Normal file
|
@ -0,0 +1,286 @@
|
|||
/*globals describe, it, before, afterEach*/
|
||||
var config = require(__dirname + '/../../../server/config'),
|
||||
moment = require('moment'),
|
||||
_ = require('lodash'),
|
||||
should = require('should'),
|
||||
express = require('express'),
|
||||
bodyParser = require('body-parser'),
|
||||
http = require('http'),
|
||||
sinon = require('sinon');
|
||||
|
||||
describe('Scheduling Default Adapter', function () {
|
||||
var scope = {};
|
||||
|
||||
before(function () {
|
||||
scope.SchedulingDefault = require(config.paths.corePath + '/server/scheduling/SchedulingDefault');
|
||||
scope.adapter = new scope.SchedulingDefault();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
scope.adapter.allJobs = {};
|
||||
});
|
||||
|
||||
describe('success', function () {
|
||||
it('addJob (schedule)', function () {
|
||||
sinon.stub(scope.adapter, 'run');
|
||||
sinon.stub(scope.adapter, '_execute');
|
||||
|
||||
var dates = [
|
||||
moment().add(1, 'day').subtract(30, 'seconds').toDate(),
|
||||
moment().add(7, 'minutes').toDate(),
|
||||
|
||||
// over 10minutes offset
|
||||
moment().add(12, 'minutes').toDate(),
|
||||
moment().add(20, 'minutes').toDate(),
|
||||
moment().add(15, 'minutes').toDate(),
|
||||
moment().add(15, 'minutes').add(10, 'seconds').toDate(),
|
||||
moment().add(15, 'minutes').subtract(30, 'seconds').toDate(),
|
||||
moment().add(50, 'seconds').toDate()
|
||||
];
|
||||
|
||||
dates.forEach(function (time) {
|
||||
scope.adapter._addJob({
|
||||
time: time,
|
||||
url: 'something'
|
||||
});
|
||||
});
|
||||
|
||||
// 2 jobs get immediately executed
|
||||
should.not.exist(scope.adapter.allJobs[moment(dates[1]).valueOf()]);
|
||||
should.not.exist(scope.adapter.allJobs[moment(dates[7]).valueOf()]);
|
||||
scope.adapter._execute.calledTwice.should.eql(true);
|
||||
|
||||
Object.keys(scope.adapter.allJobs).length.should.eql(dates.length - 2);
|
||||
Object.keys(scope.adapter.allJobs).should.eql([
|
||||
moment(dates[2]).valueOf().toString(),
|
||||
moment(dates[6]).valueOf().toString(),
|
||||
moment(dates[4]).valueOf().toString(),
|
||||
moment(dates[5]).valueOf().toString(),
|
||||
moment(dates[3]).valueOf().toString(),
|
||||
moment(dates[0]).valueOf().toString()
|
||||
]);
|
||||
|
||||
scope.adapter.run.restore();
|
||||
scope.adapter._execute.restore();
|
||||
});
|
||||
|
||||
it('run', function (done) {
|
||||
var timestamps = _.map(_.range(1000), function (i) {
|
||||
return moment().add(i, 'seconds').valueOf();
|
||||
}),
|
||||
allJobs = {};
|
||||
|
||||
sinon.stub(scope.adapter, '_execute', function (nextJobs) {
|
||||
Object.keys(nextJobs).length.should.eql(182);
|
||||
Object.keys(scope.adapter.allJobs).length.should.eql(1000 - 182);
|
||||
scope.adapter._execute.restore();
|
||||
done();
|
||||
});
|
||||
|
||||
timestamps.forEach(function (timestamp) {
|
||||
allJobs[timestamp] = [{url: 'xxx'}];
|
||||
});
|
||||
|
||||
scope.adapter.allJobs = allJobs;
|
||||
scope.adapter.runTimeoutInMs = 1000;
|
||||
scope.adapter.offsetInMinutes = 2;
|
||||
scope.adapter.run();
|
||||
});
|
||||
|
||||
it('execute', function (done) {
|
||||
var pinged = 0,
|
||||
jobs = 3,
|
||||
timestamps = _.map(_.range(jobs), function (i) {
|
||||
return moment().add(1, 'seconds').add(i * 100, 'milliseconds').valueOf();
|
||||
}),
|
||||
nextJobs = {};
|
||||
|
||||
sinon.stub(scope.adapter, 'run');
|
||||
sinon.stub(scope.adapter, '_pingUrl', function () {
|
||||
pinged = pinged + 1;
|
||||
});
|
||||
|
||||
timestamps.forEach(function (timestamp) {
|
||||
nextJobs[timestamp] = [{url: 'xxx'}];
|
||||
});
|
||||
|
||||
scope.adapter._execute(nextJobs);
|
||||
|
||||
(function retry() {
|
||||
if (pinged !== jobs) {
|
||||
return setTimeout(retry, 100);
|
||||
}
|
||||
|
||||
scope.adapter.run.restore();
|
||||
scope.adapter._pingUrl.restore();
|
||||
done();
|
||||
})();
|
||||
});
|
||||
|
||||
it('delete job (unschedule)', function (done) {
|
||||
sinon.stub(scope.adapter, 'run');
|
||||
sinon.stub(scope.adapter, '_pingUrl');
|
||||
|
||||
// add 3 jobs to delete
|
||||
var jobs = {};
|
||||
jobs[moment().add(500, 'milliseconds').valueOf()] = [{url: '/first', time: 1234}];
|
||||
jobs[moment().add(550, 'milliseconds').valueOf()] = [{url: '/first', time: 1235}];
|
||||
jobs[moment().add(600, 'milliseconds').valueOf()] = [{url: '/second', time: 1236}];
|
||||
|
||||
_.map(jobs, function (value) {
|
||||
scope.adapter._deleteJob(value[0]);
|
||||
});
|
||||
|
||||
// add another, which will be pinged
|
||||
jobs[moment().add(650, 'milliseconds').valueOf()] = [{url: '/third', time: 1237}];
|
||||
|
||||
// simulate execute is called
|
||||
scope.adapter._execute(jobs);
|
||||
|
||||
(function retry() {
|
||||
if (!scope.adapter._pingUrl.called) {
|
||||
return setTimeout(retry, 10);
|
||||
}
|
||||
|
||||
Object.keys(scope.adapter.deletedJobs).length.should.eql(0);
|
||||
scope.adapter._pingUrl.calledOnce.should.eql(true);
|
||||
|
||||
scope.adapter.run.restore();
|
||||
scope.adapter._pingUrl.restore();
|
||||
done();
|
||||
})();
|
||||
});
|
||||
|
||||
it('pingUrl (PUT)', function (done) {
|
||||
var app = express(),
|
||||
server = http.createServer(app),
|
||||
wasPinged = false,
|
||||
reqBody;
|
||||
|
||||
app.use(bodyParser.json());
|
||||
|
||||
app.put('/ping', function (req, res) {
|
||||
wasPinged = true;
|
||||
reqBody = req.body;
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
server.listen(1111);
|
||||
|
||||
scope.adapter._pingUrl({
|
||||
url: 'http://localhost:1111/ping',
|
||||
time: moment().add(1, 'second').valueOf(),
|
||||
extra: {
|
||||
httpMethod: 'PUT'
|
||||
}
|
||||
});
|
||||
|
||||
(function retry() {
|
||||
if (wasPinged) {
|
||||
should.not.exist(reqBody.force);
|
||||
return server.close(done);
|
||||
}
|
||||
|
||||
setTimeout(retry, 100);
|
||||
})();
|
||||
});
|
||||
|
||||
it('pingUrl (GET)', function (done) {
|
||||
var app = express(),
|
||||
server = http.createServer(app),
|
||||
wasPinged = false,
|
||||
reqQuery;
|
||||
|
||||
app.get('/ping', function (req, res) {
|
||||
wasPinged = true;
|
||||
reqQuery = req.query;
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
server.listen(1111);
|
||||
|
||||
scope.adapter._pingUrl({
|
||||
url: 'http://localhost:1111/ping',
|
||||
time: moment().add(1, 'second').valueOf(),
|
||||
extra: {
|
||||
httpMethod: 'GET'
|
||||
}
|
||||
});
|
||||
|
||||
(function retry() {
|
||||
if (wasPinged) {
|
||||
should.not.exist(reqQuery.force);
|
||||
return server.close(done);
|
||||
}
|
||||
|
||||
setTimeout(retry, 100);
|
||||
})();
|
||||
});
|
||||
|
||||
it('pingUrl (PUT, and detect publish in the past)', function (done) {
|
||||
var app = express(),
|
||||
server = http.createServer(app),
|
||||
wasPinged = false,
|
||||
reqBody;
|
||||
|
||||
app.use(bodyParser.json());
|
||||
|
||||
app.put('/ping', function (req, res) {
|
||||
wasPinged = true;
|
||||
reqBody = req.body;
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
server.listen(1111);
|
||||
|
||||
scope.adapter._pingUrl({
|
||||
url: 'http://localhost:1111/ping',
|
||||
time: moment().subtract(10, 'minutes').valueOf(),
|
||||
extra: {
|
||||
httpMethod: 'PUT'
|
||||
}
|
||||
});
|
||||
|
||||
(function retry() {
|
||||
if (wasPinged) {
|
||||
should.exist(reqBody.force);
|
||||
return server.close(done);
|
||||
}
|
||||
|
||||
setTimeout(retry, 100);
|
||||
})();
|
||||
});
|
||||
|
||||
it('pingUrl (GET, and detect publish in the past)', function (done) {
|
||||
var app = express(),
|
||||
server = http.createServer(app),
|
||||
wasPinged = false,
|
||||
reqQuery;
|
||||
|
||||
app.get('/ping', function (req, res) {
|
||||
wasPinged = true;
|
||||
reqQuery = req.query;
|
||||
res.sendStatus(200);
|
||||
});
|
||||
|
||||
server.listen(1111);
|
||||
|
||||
scope.adapter._pingUrl({
|
||||
url: 'http://localhost:1111/ping',
|
||||
time: moment().subtract(10, 'minutes').valueOf(),
|
||||
extra: {
|
||||
httpMethod: 'GET'
|
||||
}
|
||||
});
|
||||
|
||||
(function retry() {
|
||||
if (wasPinged) {
|
||||
should.exist(reqQuery.force);
|
||||
return server.close(done);
|
||||
}
|
||||
|
||||
setTimeout(retry, 100);
|
||||
})();
|
||||
});
|
||||
});
|
||||
});
|
33
core/test/unit/scheduling/index_spec.js
Normal file
33
core/test/unit/scheduling/index_spec.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*globals describe, it, before, after*/
|
||||
|
||||
var sinon = require('sinon'),
|
||||
rewire = require('rewire'),
|
||||
/*jshint unused:false*/
|
||||
should = require('should'),
|
||||
Promise = require('bluebird'),
|
||||
config = require(__dirname + '/../../../server/config'),
|
||||
postScheduling = require(__dirname + '/../../../server/scheduling/post-scheduling');
|
||||
|
||||
describe('Scheduling', function () {
|
||||
var scope = {};
|
||||
|
||||
before(function () {
|
||||
sinon.stub(postScheduling, 'init').returns(Promise.resolve());
|
||||
scope.scheduling = rewire(config.paths.corePath + '/server/scheduling');
|
||||
});
|
||||
|
||||
after(function () {
|
||||
postScheduling.init.restore();
|
||||
});
|
||||
|
||||
describe('success', function () {
|
||||
it('ensure post scheduling init is called', function (done) {
|
||||
scope.scheduling.init({
|
||||
postScheduling: {}
|
||||
}).then(function () {
|
||||
postScheduling.init.calledOnce.should.eql(true);
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
});
|
||||
});
|
113
core/test/unit/scheduling/post-scheduling/index_spec.js
Normal file
113
core/test/unit/scheduling/post-scheduling/index_spec.js
Normal file
|
@ -0,0 +1,113 @@
|
|||
/*globals describe, it, beforeEach, afterEach*/
|
||||
|
||||
var should = require('should'),
|
||||
sinon = require('sinon'),
|
||||
Promise = require('bluebird'),
|
||||
config = require(__dirname + '/../../../../server/config'),
|
||||
testUtils = require(config.paths.corePath + '/test/utils'),
|
||||
errors = require(config.paths.corePath + '/server/errors'),
|
||||
events = require(config.paths.corePath + '/server/events'),
|
||||
models = require(config.paths.corePath + '/server/models'),
|
||||
api = require(config.paths.corePath + '/server/api'),
|
||||
schedulingUtils = require(config.paths.corePath + '/server/scheduling/utils'),
|
||||
SchedulingDefault = require(config.paths.corePath + '/server/scheduling/SchedulingDefault'),
|
||||
postScheduling = require(config.paths.corePath + '/server/scheduling/post-scheduling');
|
||||
|
||||
describe('Scheduling: Post Scheduling', function () {
|
||||
var scope = {
|
||||
events: {},
|
||||
scheduledPosts: [],
|
||||
apiUrl: 'localhost:1111/',
|
||||
client: null,
|
||||
post: null
|
||||
};
|
||||
|
||||
beforeEach(testUtils.setup());
|
||||
|
||||
beforeEach(function () {
|
||||
scope.client = models.Client.forge(testUtils.DataGenerator.forKnex.createClient({slug: 'ghost-scheduler'}));
|
||||
scope.post = models.Post.forge(testUtils.DataGenerator.forKnex.createPost({id: 1337, markdown: 'something'}));
|
||||
|
||||
scope.adapter = new SchedulingDefault();
|
||||
|
||||
sinon.stub(api.schedules, 'getScheduledPosts', function () {
|
||||
return Promise.resolve({posts: scope.scheduledPosts});
|
||||
});
|
||||
|
||||
sinon.stub(events, 'onMany', function (events, stubDone) {
|
||||
events.forEach(function (event) {
|
||||
scope.events[event] = stubDone;
|
||||
});
|
||||
});
|
||||
|
||||
sinon.stub(schedulingUtils, 'createAdapter').returns(Promise.resolve(scope.adapter));
|
||||
|
||||
models.Client.findOne = function () {
|
||||
return Promise.resolve(scope.client);
|
||||
};
|
||||
|
||||
sinon.spy(scope.adapter, 'schedule');
|
||||
sinon.spy(scope.adapter, 'reschedule');
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
scope.adapter.schedule.reset();
|
||||
schedulingUtils.createAdapter.restore();
|
||||
scope.adapter.schedule.restore();
|
||||
scope.adapter.reschedule.restore();
|
||||
events.onMany.restore();
|
||||
api.schedules.getScheduledPosts.restore();
|
||||
testUtils.teardown(done);
|
||||
});
|
||||
|
||||
describe('fn:init', function () {
|
||||
describe('success', function () {
|
||||
it('will be scheduled', function (done) {
|
||||
postScheduling.init({
|
||||
apiUrl: scope.apiUrl,
|
||||
postScheduling: {}
|
||||
}).then(function () {
|
||||
scope.events['post.scheduled'](scope.post);
|
||||
scope.adapter.schedule.called.should.eql(true);
|
||||
|
||||
scope.adapter.schedule.calledWith({
|
||||
time: scope.post.get('published_at'),
|
||||
url: scope.apiUrl + '/schedules/posts/' + scope.post.get('id') + '?client_id=' + scope.client.get('slug') + '&client_secret=' + scope.client.get('secret'),
|
||||
extra: {
|
||||
httpMethod: 'PUT',
|
||||
oldTime: null
|
||||
}
|
||||
}).should.eql(true);
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('will load scheduled posts from database', function (done) {
|
||||
scope.scheduledPosts = [
|
||||
models.Post.forge(testUtils.DataGenerator.forKnex.createPost({status: 'scheduled'})),
|
||||
models.Post.forge(testUtils.DataGenerator.forKnex.createPost({status: 'scheduled'}))
|
||||
];
|
||||
|
||||
postScheduling.init({
|
||||
apiUrl: scope.apiUrl,
|
||||
postScheduling: {}
|
||||
}).then(function () {
|
||||
scope.adapter.reschedule.calledTwice.should.eql(true);
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error', function () {
|
||||
it('no url passed', function (done) {
|
||||
postScheduling.init()
|
||||
.catch(function (err) {
|
||||
should.exist(err);
|
||||
(err instanceof errors.IncorrectUsage).should.eql(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
85
core/test/unit/scheduling/utils_spec.js
Normal file
85
core/test/unit/scheduling/utils_spec.js
Normal file
|
@ -0,0 +1,85 @@
|
|||
/*globals describe, it*/
|
||||
|
||||
var should = require('should'),
|
||||
fs = require('fs'),
|
||||
config = require(__dirname + '/../../../server/config'),
|
||||
errors = require(config.paths.corePath + '/server/errors'),
|
||||
schedulingUtils = require(config.paths.corePath + '/server/scheduling/utils');
|
||||
|
||||
describe('Scheduling: utils', function () {
|
||||
describe('success', function () {
|
||||
it('create good adapter', function (done) {
|
||||
schedulingUtils.createAdapter({
|
||||
active: __dirname + '/../../../server/scheduling/SchedulingDefault'
|
||||
}).then(function (adapter) {
|
||||
should.exist(adapter);
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('create good adapter', function (done) {
|
||||
var jsFile = '' +
|
||||
'var util = require(\'util\');' +
|
||||
'var SchedulingBase = require(__dirname + \'/../../../server/scheduling/SchedulingBase\');' +
|
||||
'var AnotherAdapter = function (){ SchedulingBase.call(this); };' +
|
||||
'util.inherits(AnotherAdapter, SchedulingBase);' +
|
||||
'AnotherAdapter.prototype.run = function (){};' +
|
||||
'AnotherAdapter.prototype.schedule = function (){};' +
|
||||
'AnotherAdapter.prototype.reschedule = function (){};' +
|
||||
'AnotherAdapter.prototype.unschedule = function (){};' +
|
||||
'module.exports = AnotherAdapter';
|
||||
|
||||
fs.writeFileSync(__dirname + '/another-scheduler.js', jsFile);
|
||||
schedulingUtils.createAdapter({
|
||||
active: 'another-scheduler',
|
||||
path: __dirname + '/'
|
||||
}).then(function (adapter) {
|
||||
should.exist(adapter);
|
||||
done();
|
||||
}).finally(function () {
|
||||
fs.unlinkSync(__dirname + '/another-scheduler.js');
|
||||
}).catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error', function () {
|
||||
it('create without adapter path', function (done) {
|
||||
schedulingUtils.createAdapter()
|
||||
.catch(function (err) {
|
||||
should.exist(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('create with unknown adapter', function (done) {
|
||||
schedulingUtils.createAdapter({
|
||||
active: '/follow/the/heart'
|
||||
}).catch(function (err) {
|
||||
should.exist(err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('create with adapter, but missing fn\'s', function (done) {
|
||||
var jsFile = '' +
|
||||
'var util = require(\'util\');' +
|
||||
'var SchedulingBase = require(__dirname + \'/../../../server/scheduling/SchedulingBase\');' +
|
||||
'var BadAdapter = function (){ SchedulingBase.call(this); };' +
|
||||
'util.inherits(BadAdapter, SchedulingBase);' +
|
||||
'BadAdapter.prototype.schedule = function (){};' +
|
||||
'module.exports = BadAdapter';
|
||||
|
||||
fs.writeFileSync(__dirname + '/bad-adapter.js', jsFile);
|
||||
|
||||
schedulingUtils.createAdapter({
|
||||
active: __dirname + '/bad-adapter'
|
||||
}).catch(function (err) {
|
||||
should.exist(err);
|
||||
(err instanceof errors.IncorrectUsage).should.eql(true);
|
||||
done();
|
||||
}).finally(function () {
|
||||
fs.unlinkSync(__dirname + '/bad-adapter.js');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,7 +1,9 @@
|
|||
var _ = require('lodash'),
|
||||
uuid = require('node-uuid'),
|
||||
moment = require('moment'),
|
||||
globalUtils = require('../../../server/utils'),
|
||||
DataGenerator = {};
|
||||
|
||||
/*jshint quotmark:false*/
|
||||
// jscs:disable validateQuoteMarks, requireCamelCaseOrUpperCaseIdentifiers
|
||||
DataGenerator.Content = {
|
||||
|
@ -58,7 +60,8 @@ DataGenerator.Content = {
|
|||
title: "This is a scheduled post!!",
|
||||
slug: "scheduled-post",
|
||||
markdown: "<h1>Welcome to my invisible post!</h1>",
|
||||
status: "scheduled"
|
||||
status: "scheduled",
|
||||
published_at: moment().add(2, 'days').toDate()
|
||||
}
|
||||
],
|
||||
|
||||
|
@ -276,6 +279,7 @@ DataGenerator.forKnex = (function () {
|
|||
|
||||
return _.defaults(newObj, {
|
||||
uuid: uuid.v4(),
|
||||
title: 'title',
|
||||
status: 'published',
|
||||
html: overrides.markdown,
|
||||
language: 'en_US',
|
||||
|
@ -320,6 +324,19 @@ DataGenerator.forKnex = (function () {
|
|||
});
|
||||
}
|
||||
|
||||
function createClient(overrides) {
|
||||
overrides = overrides || {};
|
||||
|
||||
var newObj = _.cloneDeep(overrides),
|
||||
basics = createBasic(newObj);
|
||||
|
||||
return _.defaults(newObj, {
|
||||
secret: 'not_available',
|
||||
type: 'ua',
|
||||
status: 'enabled'
|
||||
}, basics);
|
||||
}
|
||||
|
||||
function createGenericUser(uniqueInteger) {
|
||||
return createUser({
|
||||
name: 'Joe Bloggs',
|
||||
|
@ -405,7 +422,8 @@ DataGenerator.forKnex = (function () {
|
|||
];
|
||||
|
||||
clients = [
|
||||
createBasic({name: 'Ghost Admin', slug: 'ghost-admin', secret: 'not_available', type: 'ua', status: 'enabled'})
|
||||
createClient({name: 'Ghost Admin', slug: 'ghost-admin', type: 'ua'}),
|
||||
createClient({name: 'Ghost Scheduler', slug: 'ghost-scheduler', type: 'web'})
|
||||
];
|
||||
|
||||
roles_users = [
|
||||
|
@ -440,6 +458,7 @@ DataGenerator.forKnex = (function () {
|
|||
createGenericPost: createGenericPost,
|
||||
createTag: createBasic,
|
||||
createUser: createUser,
|
||||
createClient: createClient,
|
||||
createGenericUser: createGenericUser,
|
||||
createBasic: createBasic,
|
||||
createRole: createBasic,
|
||||
|
|
|
@ -2,6 +2,7 @@ var Promise = require('bluebird'),
|
|||
_ = require('lodash'),
|
||||
fs = require('fs-extra'),
|
||||
path = require('path'),
|
||||
Module = require('module'),
|
||||
uuid = require('node-uuid'),
|
||||
db = require('../../server/data/db'),
|
||||
migration = require('../../server/data/migration/'),
|
||||
|
@ -20,8 +21,11 @@ var Promise = require('bluebird'),
|
|||
fixtures,
|
||||
getFixtureOps,
|
||||
toDoList,
|
||||
originalRequireFn,
|
||||
postsInserted = 0,
|
||||
|
||||
mockNotExistingModule,
|
||||
unmockNotExistingModule,
|
||||
teardown,
|
||||
setup,
|
||||
doAuth,
|
||||
|
@ -38,7 +42,7 @@ fixtures = {
|
|||
return Promise.resolve(db.knex('posts').insert(posts));
|
||||
},
|
||||
|
||||
insertPostsAndTags: function insertPosts() {
|
||||
insertPostsAndTags: function insertPostsAndTags() {
|
||||
return Promise.resolve(db.knex('posts').insert(DataGenerator.forKnex.posts)).then(function () {
|
||||
return db.knex('tags').insert(DataGenerator.forKnex.tags);
|
||||
}).then(function () {
|
||||
|
@ -403,8 +407,7 @@ toDoList = {
|
|||
roles: function insertRoles() { return fixtures.insertRoles(); },
|
||||
tag: function insertTag() { return fixtures.insertOne('tags', 'createTag'); },
|
||||
subscriber: function insertSubscriber() { return fixtures.insertOne('subscribers', 'createSubscriber'); },
|
||||
|
||||
posts: function insertPosts() { return fixtures.insertPostsAndTags(); },
|
||||
posts: function insertPostsAndTags() { return fixtures.insertPostsAndTags(); },
|
||||
'posts:mu': function insertMultiAuthorPosts() { return fixtures.insertMultiAuthorPosts(); },
|
||||
tags: function insertMoreTags() { return fixtures.insertMoreTags(); },
|
||||
apps: function insertApps() { return fixtures.insertApps(); },
|
||||
|
@ -447,6 +450,7 @@ getFixtureOps = function getFixtureOps(toDos) {
|
|||
fixtureOps.push(function initDB() {
|
||||
return migration.init(tablesOnly);
|
||||
});
|
||||
|
||||
delete toDos.default;
|
||||
delete toDos.init;
|
||||
}
|
||||
|
@ -495,7 +499,7 @@ setup = function setup() {
|
|||
Models.init();
|
||||
|
||||
if (done) {
|
||||
return initFixtures.apply(self, args).then(function () {
|
||||
initFixtures.apply(self, args).then(function () {
|
||||
done();
|
||||
}).catch(done);
|
||||
} else {
|
||||
|
@ -590,6 +594,25 @@ teardown = function teardown(done) {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* offer helper functions for mocking
|
||||
* we start with a small function set to mock non existent modules
|
||||
*/
|
||||
originalRequireFn = Module.prototype.require;
|
||||
mockNotExistingModule = function mockNotExistingModule(modulePath, module) {
|
||||
Module.prototype.require = function (path) {
|
||||
if (path.match(modulePath)) {
|
||||
return module;
|
||||
}
|
||||
|
||||
return originalRequireFn.apply(this, arguments);
|
||||
};
|
||||
};
|
||||
|
||||
unmockNotExistingModule = function unmockNotExistingModule() {
|
||||
Module.prototype.require = originalRequireFn;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
teardown: teardown,
|
||||
setup: setup,
|
||||
|
@ -597,6 +620,9 @@ module.exports = {
|
|||
login: login,
|
||||
togglePermalinks: togglePermalinks,
|
||||
|
||||
mockNotExistingModule: mockNotExistingModule,
|
||||
unmockNotExistingModule: unmockNotExistingModule,
|
||||
|
||||
initFixtures: initFixtures,
|
||||
initData: initData,
|
||||
clearData: clearData,
|
||||
|
|
|
@ -66,6 +66,7 @@
|
|||
"semver": "5.1.0",
|
||||
"showdown-ghost": "0.3.6",
|
||||
"sqlite3": "3.1.4",
|
||||
"superagent": "1.8.3",
|
||||
"unidecode": "0.1.8",
|
||||
"validator": "5.4.0",
|
||||
"xml": "1.0.1"
|
||||
|
|
Loading…
Reference in a new issue