0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-06 22:40:14 -05:00

Refactored scheduling index files into class/initializer pattern

refs https://github.com/TryGhost/Team/issues/694

- This refactor is not ideal but moves us closer to the desired form of class with injectable (and testable) parameters. Allowed to refactor the test slightly so at least we can check if schedulerd  subscribed events work and if they trigger the adapter with correct data
- Ideally the api/model calls shoudl be abstracted away as well, but that's for another time
- Also got rid of completely pointless "adapters/scheduling" unit test. All it was checking was if the "init" method was called int the passe in object
This commit is contained in:
Naz 2021-05-25 22:32:31 +04:00
parent 9ae55eecfb
commit e370d33378
5 changed files with 179 additions and 225 deletions

View file

@ -1,50 +1,16 @@
const Promise = require('bluebird');
const moment = require('moment');
const localUtils = require('../utils');
const events = require('../../../lib/common/events');
const errors = require('@tryghost/errors');
const urlUtils = require('../../../../shared/url-utils');
const getSignedAdminToken = require('./scheduling-auth-token');
const localUtils = require('../utils');
const PostScheduler = require('./post-scheduler');
const getSchedulerIntegration = require('./scheduler-intergation');
const _private = {};
const SCHEDULED_RESOURCES = ['post', 'page'];
/**
* @description Normalize model data into scheduler notation.
* @param {Object} options
* @return {Object}
*/
_private.normalize = function normalize({model, apiUrl, resourceType, integration}, event = '') {
const resource = `${resourceType}s`;
const publishedAt = (event === 'unscheduled') ? model.previous('published_at') : model.get('published_at');
const signedAdminToken = getSignedAdminToken({
publishedAt,
apiUrl,
key: {
id: integration.api_keys[0].id,
secret: integration.api_keys[0].secret
}
});
let url = `${urlUtils.urlJoin(apiUrl, 'schedules', resource, model.get('id'))}/?token=${signedAdminToken}`;
return {
// NOTE: The scheduler expects a unix timestamp.
time: moment(publishedAt).valueOf(),
url: url,
extra: {
httpMethod: 'PUT',
oldTime: model.previous('published_at') ? moment(model.previous('published_at')).valueOf() : null
}
};
};
/**
* @description Load all scheduled posts/pages from database.
* @return {Promise}
*/
_private.loadScheduledResources = async function () {
const loadScheduledResources = async function () {
const api = require('../../../api');
const SCHEDULED_RESOURCES = ['post', 'page'];
// Fetches all scheduled resources(posts/pages) with default API
const results = await Promise.mapSeries(SCHEDULED_RESOURCES, async (resourceType) => {
const result = await api.schedules.getScheduled.query({
@ -63,66 +29,25 @@ _private.loadScheduledResources = async function () {
}, {});
};
/**
* @description Initialise post scheduling.
* @param {Object} options
* @param {string} options.apiUrl -
* @return {*}
*/
exports.init = async function init(options = {}) {
const {apiUrl} = options;
let adapter = null;
let integration = null;
if (!Object.keys(options).length) {
return Promise.reject(new errors.IncorrectUsageError({message: 'post-scheduling: no config was provided'}));
}
if (!apiUrl) {
return Promise.reject(new errors.IncorrectUsageError({message: 'post-scheduling: no apiUrl was provided'}));
}
integration = await getSchedulerIntegration();
adapter = await localUtils.createAdapter();
const init = async (options) => {
const integration = await getSchedulerIntegration();
const adapter = await localUtils.createAdapter();
let scheduledResources;
if (!adapter.rescheduleOnBoot) {
scheduledResources = [];
} else {
scheduledResources = await _private.loadScheduledResources();
scheduledResources = await loadScheduledResources();
}
if (Object.keys(scheduledResources).length) {
// Reschedules all scheduled resources on boot
// NOTE: We are using reschedule, because custom scheduling adapter could use a database, which needs to be updated
// and not an in-process implementation!
Object.keys(scheduledResources).forEach((resourceType) => {
scheduledResources[resourceType].forEach((model) => {
adapter.unschedule(_private.normalize({model, apiUrl, integration, resourceType}, 'unscheduled'), {bootstrap: true});
adapter.schedule(_private.normalize({model, apiUrl, integration, resourceType}));
});
});
}
adapter.run();
SCHEDULED_RESOURCES.forEach((resource) => {
events.on(`${resource}.scheduled`, (model) => {
adapter.schedule(_private.normalize({model, apiUrl, integration, resourceType: resource}));
});
/** We want to do reschedule as (unschedule + schedule) due to how token(+url) is generated
* We want to first remove existing schedule by generating a matching token(+url)
* followed by generating a new token(+url) for the new schedule
*/
events.on(`${resource}.rescheduled`, (model) => {
adapter.unschedule(_private.normalize({model, apiUrl, integration, resourceType: resource}, 'unscheduled'));
adapter.schedule(_private.normalize({model, apiUrl, integration, resourceType: resource}));
});
events.on(`${resource}.unscheduled`, (model) => {
adapter.unschedule(_private.normalize({model, apiUrl, integration, resourceType: resource}, 'unscheduled'));
});
return new PostScheduler({
apiUrl: options.apiUrl,
integration,
adapter,
scheduledResources,
events
});
};
module.exports = init;

View file

@ -0,0 +1,78 @@
const moment = require('moment');
const errors = require('@tryghost/errors');
const urlUtils = require('../../../../shared/url-utils');
const getSignedAdminToken = require('./scheduling-auth-token');
class PostScheduler {
constructor({apiUrl, integration, adapter, scheduledResources, events} = {}) {
if (!apiUrl) {
throw new errors.IncorrectUsageError({message: 'post-scheduling: no apiUrl was provided'});
}
if (Object.keys(scheduledResources).length) {
// Reschedules all scheduled resources on boot
// NOTE: We are using reschedule, because custom scheduling adapter could use a database, which needs to be updated
// and not an in-process implementation!
Object.keys(scheduledResources).forEach((resourceType) => {
scheduledResources[resourceType].forEach((model) => {
adapter.unschedule(this.normalize({model, apiUrl, integration, resourceType}, 'unscheduled'), {bootstrap: true});
adapter.schedule(this.normalize({model, apiUrl, integration, resourceType}));
});
});
}
adapter.run();
const SCHEDULED_RESOURCES = ['post', 'page'];
SCHEDULED_RESOURCES.forEach((resource) => {
events.on(`${resource}.scheduled`, (model) => {
adapter.schedule(this.normalize({model, apiUrl, integration, resourceType: resource}));
});
/** We want to do reschedule as (unschedule + schedule) due to how token(+url) is generated
* We want to first remove existing schedule by generating a matching token(+url)
* followed by generating a new token(+url) for the new schedule
*/
events.on(`${resource}.rescheduled`, (model) => {
adapter.unschedule(this.normalize({model, apiUrl, integration, resourceType: resource}, 'unscheduled'));
adapter.schedule(this.normalize({model, apiUrl, integration, resourceType: resource}));
});
events.on(`${resource}.unscheduled`, (model) => {
adapter.unschedule(this.normalize({model, apiUrl, integration, resourceType: resource}, 'unscheduled'));
});
});
}
/**
* @description Normalize model data into scheduler notation.
* @param {Object} options
* @return {Object}
*/
normalize({model, apiUrl, resourceType, integration}, event = '') {
const resource = `${resourceType}s`;
const publishedAt = (event === 'unscheduled') ? model.previous('published_at') : model.get('published_at');
const signedAdminToken = getSignedAdminToken({
publishedAt,
apiUrl,
key: {
id: integration.api_keys[0].id,
secret: integration.api_keys[0].secret
}
});
let url = `${urlUtils.urlJoin(apiUrl, 'schedules', resource, model.get('id'))}/?token=${signedAdminToken}`;
return {
// NOTE: The scheduler expects a unix timestamp.
time: moment(publishedAt).valueOf(),
url: url,
extra: {
httpMethod: 'PUT',
oldTime: model.previous('published_at') ? moment(model.previous('published_at')).valueOf() : null
}
};
}
}
module.exports = PostScheduler;

View file

@ -1,29 +0,0 @@
const should = require('should');
const sinon = require('sinon');
const rewire = require('rewire');
const Promise = require('bluebird');
const postScheduling = require('../../../../core/server/adapters/scheduling/post-scheduling');
describe('Scheduling', function () {
const scope = {};
before(function () {
sinon.stub(postScheduling, 'init').returns(Promise.resolve());
scope.scheduling = rewire('../../../../core/server/adapters/scheduling');
});
after(function () {
sinon.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);
});
});
});

View file

@ -1,104 +0,0 @@
const errors = require('@tryghost/errors');
const should = require('should');
const sinon = require('sinon');
const Promise = require('bluebird');
const moment = require('moment');
const testUtils = require('../../../../utils');
const models = require('../../../../../core/server/models');
const events = require('../../../../../core/server/lib/common/events');
const api = require('../../../../../core/server/api');
const schedulingUtils = require('../../../../../core/server/adapters/scheduling/utils');
const SchedulingDefault = require('../../../../../core/server/adapters/scheduling/SchedulingDefault');
const postScheduling = require('../../../../../core/server/adapters/scheduling/post-scheduling');
const urlUtils = require('../../../../../core/shared/url-utils');
// NOTE: to be unskiped and corrected once default scheduler code is migrated
describe.skip('Scheduling: Post Scheduling', function () {
const scope = {
events: {},
scheduledPosts: [],
apiUrl: 'localhost:1111/',
client: null,
post: null
};
before(function () {
models.init();
});
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,
mobiledoc: testUtils.DataGenerator.markdownToMobiledoc('something')
}));
scope.adapter = new SchedulingDefault();
sinon.stub(api.schedules, 'getScheduledPosts').callsFake(function () {
return Promise.resolve({posts: scope.scheduledPosts});
});
sinon.stub(schedulingUtils, 'createAdapter').returns(Promise.resolve(scope.adapter));
sinon.stub(models.Client, 'findOne').callsFake(function () {
return Promise.resolve(scope.client);
});
sinon.spy(scope.adapter, 'schedule');
sinon.spy(scope.adapter, 'reschedule');
});
afterEach(function () {
sinon.restore();
});
describe('fn:init', function () {
describe('success', function () {
it('will be scheduled', function (done) {
postScheduling.init({
apiUrl: scope.apiUrl
}).then(function () {
scope.events['post.scheduled'](scope.post);
scope.adapter.schedule.called.should.eql(true);
scope.adapter.schedule.calledWith({
time: moment(scope.post.get('published_at')).valueOf(),
url: urlUtils.urlJoin(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
}).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.IncorrectUsageError).should.eql(true);
done();
});
});
});
});
});

View file

@ -0,0 +1,84 @@
const errors = require('@tryghost/errors');
const should = require('should');
const sinon = require('sinon');
const Promise = require('bluebird');
const moment = require('moment');
const testUtils = require('../../../../utils');
const models = require('../../../../../core/server/models');
const events = require('../../../../../core/server/lib/common/events');
const schedulingUtils = require('../../../../../core/server/adapters/scheduling/utils');
const SchedulingDefault = require('../../../../../core/server/adapters/scheduling/SchedulingDefault');
const urlUtils = require('../../../../../core/shared/url-utils');
const PostScheduler = require('../../../../../core/server/adapters/scheduling/post-scheduling/post-scheduler');
describe('Scheduling: Post Scheduler', function () {
let adapter;
before(function () {
models.init();
});
beforeEach(function () {
adapter = new SchedulingDefault();
sinon.stub(schedulingUtils, 'createAdapter').returns(Promise.resolve(adapter));
sinon.spy(adapter, 'schedule');
sinon.spy(adapter, 'unschedule');
});
afterEach(function () {
sinon.restore();
});
describe('fn:constructor', function () {
describe('success', function () {
it('will be scheduled', async function () {
const post = models.Post.forge(testUtils.DataGenerator.forKnex.createPost({
id: 1337,
mobiledoc: testUtils.DataGenerator.markdownToMobiledoc('something')
}));
new PostScheduler({
apiUrl: 'localhost:1111/',
integration: {
api_keys: [{
id: 'integrationUniqueId',
secret: 'super-secret'
}]
},
adapter,
scheduledResources: {
posts: []
},
events
});
events.emit('post.scheduled', post);
// let the events bubble up
await Promise.delay(100);
adapter.schedule.called.should.eql(true);
adapter.schedule.calledOnce.should.eql(true);
adapter.schedule.args[0][0].time.should.equal(moment(post.get('published_at')).valueOf());
adapter.schedule.args[0][0].url.should.startWith(urlUtils.urlJoin('localhost:1111/', 'schedules', 'posts', post.get('id'), '?token='));
adapter.schedule.args[0][0].extra.httpMethod.should.eql('PUT');
should.equal(null, adapter.schedule.args[0][0].extra.oldTime);
});
});
describe('error', function () {
it('no apiUrl parameter passed', function () {
try {
new PostScheduler();
throw new Error('should have thrown');
} catch (err) {
(err instanceof errors.IncorrectUsageError).should.eql(true);
}
});
});
});
});