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

Webhooks support for subscriber events (#9230)

no issue

Support for http://resthooks.org style webhooks that can be used with Zapier triggers. This can currently be used in two ways:

a) adding a webhook record to the DB manually
b) using the API with password auth and POSTing to /webhooks/ (this is private API so not documented)

⚠️ only _https_ URLs are supported in the webhook `target_url` field 🚨

- add `webhooks` table to store event names and target urls
- add `POST` and `DELETE` endpoints for `/webhooks/`
- configure `subscribers.added` and `subscribers.deleted` events to trigger registered webhooks
This commit is contained in:
Kevin Ansfield 2017-11-21 15:43:14 +00:00 committed by GitHub
parent 9e60ac639b
commit bffb3dbd90
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 753 additions and 26 deletions

View file

@ -29,6 +29,7 @@ var _ = require('lodash'),
uploads = require('./upload'),
exporter = require('../data/export'),
slack = require('./slack'),
webhooks = require('./webhooks'),
http,
addHeaders,
@ -139,6 +140,9 @@ locationHeader = function locationHeader(req, result) {
} else if (result.hasOwnProperty('tags')) {
newObject = result.tags[0];
location = utils.url.urlJoin(apiRoot, 'tags', newObject.id, '/');
} else if (result.hasOwnProperty('webhooks')) {
newObject = result.webhooks[0];
location = utils.url.urlJoin(apiRoot, 'webhooks', newObject.id, '/');
}
}
@ -312,7 +316,8 @@ module.exports = {
slack: slack,
themes: themes,
invites: invites,
redirects: redirects
redirects: redirects,
webhooks: webhooks
};
/**

View file

@ -201,5 +201,9 @@ module.exports = function apiRoutes() {
api.http(api.redirects.upload)
);
// ## Webhooks (RESTHooks)
apiRouter.post('/webhooks', mw.authenticatePrivate, api.http(api.webhooks.add));
apiRouter.del('/webhooks/:id', mw.authenticatePrivate, api.http(api.webhooks.destroy));
return apiRouter;
};

155
core/server/api/webhooks.js Normal file
View file

@ -0,0 +1,155 @@
// # Webhooks API
// RESTful API for creating webhooks
// also known as "REST Hooks", see http://resthooks.org
var Promise = require('bluebird'),
_ = require('lodash'),
https = require('https'),
url = require('url'),
pipeline = require('../utils/pipeline'),
apiUtils = require('./utils'),
models = require('../models'),
errors = require('../errors'),
logging = require('../logging'),
i18n = require('../i18n'),
docName = 'webhooks',
webhooks;
// TODO: Use the request util. Do we want retries here?
function makeRequest(webhook, payload, options) {
var event = webhook.get('event'),
targetUrl = webhook.get('target_url'),
webhookId = webhook.get('id'),
reqOptions, reqPayload, req;
reqOptions = url.parse(targetUrl);
reqOptions.method = 'POST';
reqOptions.headers = {'Content-Type': 'application/json'};
reqPayload = JSON.stringify(payload);
logging.info('webhook.trigger', event, targetUrl);
req = https.request(reqOptions);
req.write(reqPayload);
req.on('error', function (err) {
// when a webhook responds with a 410 Gone response we should remove the hook
if (err.status === 410) {
logging.info('webhook.destroy (410 response)', event, targetUrl);
return models.Webhook.destroy({id: webhookId}, options);
}
// TODO: use i18n?
logging.error(new errors.GhostError({
err: err,
context: {
id: webhookId,
event: event,
target_url: targetUrl,
payload: payload
}
}));
});
req.end();
}
function makeRequests(webhooksCollection, payload, options) {
_.each(webhooksCollection.models, function (webhook) {
makeRequest(webhook, payload, options);
});
}
/**
* ## Webhook API Methods
*
* **See:** [API Methods](index.js.html#api%20methods)
*/
webhooks = {
/**
* ### Add
* @param {Webhook} object the webhook to create
* @returns {Promise(Webhook)} newly created Webhook
*/
add: function add(object, options) {
var tasks;
/**
* ### Model Query
* Make the call to the Model layer
* @param {Object} options
* @returns {Object} options
*/
function doQuery(options) {
return models.Webhook.getByEventAndTarget(options.data.webhooks[0].event, options.data.webhooks[0].target_url, _.omit(options, ['data']))
.then(function (webhook) {
if (webhook) {
return Promise.reject(new errors.ValidationError({message: i18n.t('errors.api.webhooks.webhookAlreadyExists')}));
}
return models.Webhook.add(options.data.webhooks[0], _.omit(options, ['data']));
})
.then(function onModelResponse(model) {
return {
webhooks: [model.toJSON(options)]
};
});
}
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
apiUtils.validate(docName),
apiUtils.handlePermissions(docName, 'add'),
doQuery
];
// Pipeline calls each task passing the result of one to be the arguments for the next
return pipeline(tasks, object, options);
},
/**
* ## Destroy
*
* @public
* @param {{id, context}} options
* @return {Promise}
*/
destroy: function destroy(options) {
var tasks;
/**
* ### Delete Webhook
* Make the call to the Model layer
* @param {Object} options
*/
function doQuery(options) {
return models.Webhook.destroy(options).return(null);
}
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
apiUtils.validate(docName, {opts: apiUtils.idDefaultOptions}),
apiUtils.handlePermissions(docName, 'destroy'),
doQuery
];
// Pipeline calls each task passing the result of one to be the arguments for the next
return pipeline(tasks, options);
},
trigger: function trigger(event, payload, options) {
var tasks;
function doQuery(options) {
return models.Webhook.findAllByEvent(event, options);
}
tasks = [
doQuery,
_.partialRight(makeRequests, payload, options)
];
return pipeline(tasks, options);
}
};
module.exports = webhooks;

View file

@ -0,0 +1,21 @@
'use strict';
const logging = require('../../../../logging'),
commands = require('../../../schema').commands,
table = 'webhooks',
message = 'Adding table: ' + table;
module.exports = function addWebhooksTable(options) {
let transacting = options.transacting;
return transacting.schema.hasTable(table)
.then(function (exists) {
if (exists) {
logging.warn(message);
return Promise.resolve();
}
logging.info(message);
return commands.createTable(table, transacting);
});
};

View file

@ -421,6 +421,16 @@
"name": "Upload redirects",
"action_type": "upload",
"object_type": "redirect"
},
{
"name": "Add webhooks",
"action_type": "add",
"object_type": "webhook"
},
{
"name": "Delete webhooks",
"action_type": "destroy",
"object_type": "webhook"
}
]
},
@ -472,7 +482,8 @@
"client": "all",
"subscriber": "all",
"invite": "all",
"redirect": "all"
"redirect": "all",
"webhook": "all"
},
"Editor": {
"post": "all",

View file

@ -239,5 +239,14 @@ module.exports = {
lastRequest: {type: 'bigInteger'},
lifetime: {type: 'bigInteger'},
count: {type: 'integer'}
},
webhooks: {
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
event: {type: 'string', maxlength: 50, nullable: false, validations: {isLowercase: true}},
target_url: {type: 'string', maxlength: 2000, nullable: false},
created_at: {type: 'dateTime', nullable: false},
created_by: {type: 'string', maxlength: 24, nullable: false},
updated_at: {type: 'dateTime', nullable: true},
updated_by: {type: 'string', maxlength: 24, nullable: true}
}
};

View file

@ -31,7 +31,8 @@ var debug = require('ghost-ignition').debug('boot:init'),
urlService = require('./services/url'),
apps = require('./services/apps'),
xmlrpc = require('./services/xmlrpc'),
slack = require('./services/slack');
slack = require('./services/slack'),
webhooks = require('./services/webhooks');
// ## Initialise Ghost
function init() {
@ -64,9 +65,10 @@ function init() {
xmlrpc.listen(),
// Initialize slack ping
slack.listen(),
// Initialize webhook pings
webhooks.listen(),
// Url Service
urlService.init()
);
}).then(function () {
debug('Apps, XMLRPC, Slack done');

View file

@ -29,7 +29,8 @@ models = [
'subscriber',
'tag',
'user',
'invite'
'invite',
'webhook'
];
function init() {

View file

@ -9,8 +9,10 @@ var ghostBookshelf = require('./base'),
Subscriber = ghostBookshelf.Model.extend({
tableName: 'subscribers',
emitChange: function emitChange(event) {
events.emit('subscriber' + '.' + event, this);
emitChange: function emitChange(event, options) {
options = options || {};
events.emit('subscriber' + '.' + event, this, options);
},
defaults: function defaults() {
@ -19,16 +21,16 @@ Subscriber = ghostBookshelf.Model.extend({
};
},
onCreated: function onCreated(model) {
model.emitChange('added');
onCreated: function onCreated(model, response, options) {
model.emitChange('added', options);
},
onUpdated: function onUpdated(model) {
model.emitChange('edited');
onUpdated: function onUpdated(model, response, options) {
model.emitChange('edited', options);
},
onDestroyed: function onDestroyed(model) {
model.emitChange('deleted');
onDestroyed: function onDestroyed(model, response, options) {
model.emitChange('deleted', options);
}
}, {

View file

@ -0,0 +1,68 @@
var ghostBookshelf = require('./base'),
events = require('../events'),
Promise = require('bluebird'),
Webhook,
Webhooks;
Webhook = ghostBookshelf.Model.extend({
tableName: 'webhooks',
emitChange: function emitChange(event, options) {
options = options || {};
events.emit('webhook' + '.' + event, this, options);
},
onCreated: function onCreated(model, response, options) {
model.emitChange('added', options);
},
onUpdated: function onUpdated(model, response, options) {
model.emitChange('edited', options);
},
onDestroyed: function onDestroyed(model, response, options) {
model.emitChange('deleted', options);
}
}, {
findAllByEvent: function findAllByEvent(event, options) {
var webhooksCollection = Webhooks.forge();
options = this.filterOptions(options, 'findAll');
return webhooksCollection
.query('where', 'event', '=', event)
.fetch(options);
},
getByEventAndTarget: function getByEventAndTarget(event, targetUrl, options) {
options = options || {};
options.require = true;
return Webhooks.forge(options).fetch(options).then(function then(webhooks) {
var webhookWithEventAndTarget = webhooks.find(function findWebhook(webhook) {
return webhook.get('event').toLowerCase() === event.toLowerCase()
&& webhook.get('target_url').toLowerCase() === targetUrl.toLowerCase();
});
if (webhookWithEventAndTarget) {
return webhookWithEventAndTarget;
}
}).catch(function (error) {
if (error.message === 'NotFound' || error.message === 'EmptyResponse') {
return Promise.resolve();
}
return Promise.reject(error);
});
}
});
Webhooks = ghostBookshelf.Collection.extend({
model: Webhook
});
module.exports = {
Webhook: ghostBookshelf.model('Webhook', Webhook),
Webhooks: ghostBookshelf.collection('Webhooks', Webhooks)
};

View file

@ -0,0 +1,57 @@
var _ = require('lodash'),
events = require('../events'),
api = require('../api'),
modelAttrs;
// TODO: this can be removed once all events pass a .toJSON object through
modelAttrs = {
subscriber: ['id', 'name', 'email']
};
// TODO: this works for basic models but we eventually want a full API response
// with embedded models (?include=tags) and so on
function generatePayload(event, model) {
var modelName = event.split('.')[0],
pluralModelName = modelName + 's',
action = event.split('.')[1],
payload = {},
data;
if (action === 'deleted') {
data = {};
modelAttrs[modelName].forEach(function (key) {
if (model._previousAttributes[key] !== undefined) {
data[key] = model._previousAttributes[key];
}
});
} else {
data = model.toJSON();
}
payload[pluralModelName] = [data];
return payload;
}
function listener(event, model, options) {
var payload = generatePayload(event, model);
// avoid triggering webhooks when importing
if (options && options.importing) {
return;
}
api.webhooks.trigger(event, payload, options);
}
// TODO: use a wildcard with the new event emitter or use the webhooks API to
// register listeners only for events that have webhooks
function listen() {
events.on('subscriber.added', _.partial(listener, 'subscriber.added'));
events.on('subscriber.deleted', _.partial(listener, 'subscriber.deleted'));
}
// Public API
module.exports = {
listen: listen
};

View file

@ -417,6 +417,9 @@
},
"notAllowedToInviteOwner": "Not allowed to invite an owner user.",
"notAllowedToInvite": "Not allowed to invite this role."
},
"webhooks": {
"webhookAlreadyExists": "A webhook for requested event with supplied target_url already exists."
}
},
"data": {

View file

@ -0,0 +1,115 @@
var should = require('should'),
supertest = require('supertest'),
testUtils = require('../../../utils'),
config = require('../../../../../core/server/config'),
ghost = testUtils.startGhost,
request;
describe('Webhooks API', function () {
var ghostServer;
describe('As Owner', function () {
var ownerAccessToken = '';
before(function (done) {
// starting ghost automatically populates the db
// TODO: prevent db init, and manage bringing up the DB with fixtures ourselves
ghost().then(function (_ghostServer) {
ghostServer = _ghostServer;
return ghostServer.start();
}).then(function () {
request = supertest.agent(config.get('url'));
}).then(function () {
return testUtils.doAuth(request);
}).then(function (token) {
ownerAccessToken = token;
done();
}).catch(done);
});
after(function () {
return testUtils.clearData()
.then(function () {
return ghostServer.stop();
});
});
describe('Add', function () {
var newWebhook = {
event: 'test.create',
target_url: 'http://example.com/webhooks/test/1'
};
it('creates a new webhook', function (done) {
request.post(testUtils.API.getApiQuery('webhooks/'))
.set('Authorization', 'Bearer ' + ownerAccessToken)
.send({webhooks: [newWebhook]})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(201)
.end(function (err, res) {
if (err) {
return done(err);
}
var jsonResponse = res.body;
should.exist(jsonResponse.webhooks);
testUtils.API.checkResponse(jsonResponse.webhooks[0], 'webhook');
jsonResponse.webhooks[0].event.should.equal(newWebhook.event);
jsonResponse.webhooks[0].target_url.should.equal(newWebhook.target_url);
done();
});
});
});
describe('Delete', function () {
var newWebhook = {
event: 'test.create',
// a different target_url from above is needed to avoid an "already exists" error
target_url: 'http://example.com/webhooks/test/2'
};
it('deletes a webhook', function (done) {
// create the webhook that is to be deleted
request.post(testUtils.API.getApiQuery('webhooks/'))
.set('Authorization', 'Bearer ' + ownerAccessToken)
.send({webhooks: [newWebhook]})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(201)
.end(function (err, res) {
if (err) {
return done(err);
}
var location = res.headers.location;
var jsonResponse = res.body;
should.exist(jsonResponse.webhooks);
testUtils.API.checkResponse(jsonResponse.webhooks[0], 'webhook');
jsonResponse.webhooks[0].event.should.equal(newWebhook.event);
jsonResponse.webhooks[0].target_url.should.equal(newWebhook.target_url);
// begin delete test
request.del(location)
.set('Authorization', 'Bearer ' + ownerAccessToken)
.expect(204)
.end(function (err, res) {
if (err) {
return done(err);
}
res.body.should.be.empty();
done();
});
});
});
});
});
});

View file

@ -0,0 +1,147 @@
var _ = require('lodash'),
should = require('should'),
sinon = require('sinon'),
testUtils = require('../../utils'),
Promise = require('bluebird'),
WebhookAPI = require('../../../server/api/webhooks'),
sandbox = sinon.sandbox.create();
describe('Webhooks API', function () {
beforeEach(testUtils.teardown);
beforeEach(testUtils.setup('webhooks', 'users:roles', 'perms:webhook', 'perms:init'));
afterEach(function () {
sandbox.restore();
});
after(testUtils.teardown);
function checkForErrorType(type, done) {
return function checkForErrorType(error) {
if (Array.isArray(error)) {
error = error[0];
}
if (error.errorType) {
error.errorType.should.eql(type);
done();
} else {
done(error);
}
};
}
describe('Validations', function () {
it('Prevents mixed case event names', function (done) {
WebhookAPI.add({webhooks: [{
event: 'Mixed.Case',
target_url: 'https://example.com/hooks/test'
}]}, testUtils.context.owner)
.then(function () {
done(new Error('Should not allow mixed case event names'));
}).catch(checkForErrorType('ValidationError', done));
});
it('Prevents duplicate event/target pairs', function (done) {
var duplicate = testUtils.DataGenerator.Content.webhooks[0];
WebhookAPI.add({webhooks: [{
event: duplicate.event,
target_url: duplicate.target_url
}]}, testUtils.context.owner)
.then(function () {
done(new Error('Should not allow duplicate event/target'));
}).catch(checkForErrorType('ValidationError', done));
});
});
describe('Permissions', function () {
var firstWebhook = testUtils.DataGenerator.Content.webhooks[0].id;
var newWebhook;
function checkAddResponse(response) {
should.exist(response);
should.exist(response.webhooks);
should.not.exist(response.meta);
response.webhooks.should.have.length(1);
testUtils.API.checkResponse(response.webhooks[0], 'webhook');
response.webhooks[0].created_at.should.be.an.instanceof(Date);
}
beforeEach(function () {
newWebhook = {
event: 'test.added',
target_url: 'https://example.com/webhooks/test-added'
};
});
describe('Owner', function () {
it('Can add', function (done) {
WebhookAPI.add({webhooks: [newWebhook]}, testUtils.context.owner)
.then(function (response) {
checkAddResponse(response);
done();
}).catch(done);
});
it('Can delete', function (done) {
WebhookAPI.destroy(_.extend({}, testUtils.context.owner, {id: firstWebhook}))
.then(function (results) {
should.not.exist(results);
done();
});
});
});
describe('Admin', function () {
it('Can add', function (done) {
WebhookAPI.add({webhooks: [newWebhook]}, testUtils.context.admin)
.then(function (response) {
checkAddResponse(response);
done();
}).catch(done);
});
it('Can delete', function (done) {
WebhookAPI.destroy(_.extend({}, testUtils.context.admin, {id: firstWebhook}))
.then(function (results) {
should.not.exist(results);
done();
});
});
});
describe('Editor', function () {
it('CANNOT add', function (done) {
WebhookAPI.add({webhooks: [newWebhook]}, testUtils.context.editor)
.then(function () {
done(new Error('Editor should not be able to add a webhook'));
}).catch(checkForErrorType('NoPermissionError', done));
});
it('CANNOT delete', function (done) {
WebhookAPI.destroy(_.extend({}, testUtils.context.editor, {id: firstWebhook}))
.then(function () {
done(new Error('Editor should not be able to delete a webhook'));
}).catch(checkForErrorType('NoPermissionError', done));
});
});
describe('Author', function () {
it('CANNOT add', function (done) {
WebhookAPI.add({webhooks: [newWebhook]}, testUtils.context.author)
.then(function () {
done(new Error('Author should not be able to add a webhook'));
}).catch(checkForErrorType('NoPermissionError', done));
});
it('CANNOT delete', function (done) {
WebhookAPI.destroy(_.extend({}, testUtils.context.author, {id: firstWebhook}))
.then(function () {
done(new Error('Editor should not be able to delete a webhook'));
}).catch(checkForErrorType('NoPermissionError', done));
});
});
});
});

View file

@ -224,7 +224,7 @@ describe('Database Migration (special functions)', function () {
result.roles.at(3).get('name').should.eql('Owner');
// Permissions
result.permissions.length.should.eql(51);
result.permissions.length.should.eql(53);
result.permissions.toJSON().should.be.CompletePermissions();
done();

View file

@ -151,19 +151,19 @@ describe('Migration Fixture Utils', function () {
fixtureUtils.addFixturesForRelation(fixtures.relations[0]).then(function (result) {
should.exist(result);
result.should.be.an.Object();
result.should.have.property('expected', 33);
result.should.have.property('done', 33);
result.should.have.property('expected', 34);
result.should.have.property('done', 34);
// Permissions & Roles
permsAllStub.calledOnce.should.be.true();
rolesAllStub.calledOnce.should.be.true();
dataMethodStub.filter.callCount.should.eql(33);
dataMethodStub.filter.callCount.should.eql(34);
dataMethodStub.find.callCount.should.eql(3);
baseUtilAttachStub.callCount.should.eql(33);
baseUtilAttachStub.callCount.should.eql(34);
fromItem.related.callCount.should.eql(33);
fromItem.findWhere.callCount.should.eql(33);
toItem[0].get.callCount.should.eql(66);
fromItem.related.callCount.should.eql(34);
fromItem.findWhere.callCount.should.eql(34);
toItem[0].get.callCount.should.eql(68);
done();
}).catch(done);

View file

@ -19,8 +19,8 @@ var should = require('should'), // jshint ignore:line
// both of which are required for migrations to work properly.
describe('DB version integrity', function () {
// Only these variables should need updating
var currentSchemaHash = '0de1eaa8bc79046a9f43927917c294c3',
currentFixturesHash = 'e2c71e808c3d33660c498d164639dc8c';
var currentSchemaHash = '329f9b498944c459040426e16fc65b11',
currentFixturesHash = '90925e0004a0cedd1e6ea789c81ec67d';
// If this test is failing, then it is likely a change has been made that requires a DB version bump,
// and the values above will need updating as confirmation

View file

@ -0,0 +1,84 @@
var _ = require('lodash'),
should = require('should'),
sinon = require('sinon'),
rewire = require('rewire'),
testUtils = require('../../utils'),
// Stuff we test
webhooks = rewire('../../../server/services/webhooks'),
events = require('../../../server/events'),
sandbox = sinon.sandbox.create();
describe('Webhooks', function () {
var eventStub;
beforeEach(function () {
eventStub = sandbox.stub(events, 'on');
});
afterEach(function () {
sandbox.restore();
});
it('listen() should initialise events correctly', function () {
webhooks.listen();
eventStub.calledTwice.should.be.true();
});
it('listener() with "subscriber.added" event calls api.webhooks.trigger with toJSONified model', function () {
var testSubscriber = _.clone(testUtils.DataGenerator.Content.subscribers[0]),
testModel = {
toJSON: function () {
return testSubscriber;
}
},
apiStub = {
webhooks: {
trigger: sandbox.stub()
}
},
resetWebhooks = webhooks.__set__('api', apiStub),
listener = webhooks.__get__('listener'),
triggerArgs;
listener('subscriber.added', testModel);
apiStub.webhooks.trigger.calledOnce.should.be.true();
triggerArgs = apiStub.webhooks.trigger.getCall(0).args;
triggerArgs[0].should.eql('subscriber.added');
triggerArgs[1].should.deepEqual({
subscribers: [testSubscriber]
});
resetWebhooks();
});
it('listener() with "subscriber.deleted" event calls api.webhooks.trigger with _previousAttributes values', function () {
var testSubscriber = _.clone(testUtils.DataGenerator.Content.subscribers[1]),
testModel = {
_previousAttributes: testSubscriber
},
apiStub = {
webhooks: {
trigger: sandbox.stub()
}
},
resetWebhooks = webhooks.__set__('api', apiStub),
listener = webhooks.__get__('listener'),
triggerArgs;
listener('subscriber.deleted', testModel);
apiStub.webhooks.trigger.calledOnce.should.be.true();
triggerArgs = apiStub.webhooks.trigger.getCall(0).args;
triggerArgs[0].should.eql('subscriber.deleted');
triggerArgs[1].should.deepEqual({
subscribers: [testSubscriber]
});
resetWebhooks();
});
});

View file

@ -53,7 +53,8 @@ var _ = require('lodash'),
notification: ['type', 'message', 'status', 'id', 'dismissible', 'location'],
theme: ['name', 'package', 'active'],
themes: ['themes'],
invites: _(schema.invites).keys().without('token').value()
invites: _(schema.invites).keys().without('token').value(),
webhook: _.keys(schema.webhooks)
};
function getApiQuery(route) {

View file

@ -329,6 +329,19 @@ DataGenerator.Content = {
id: ObjectId.generate(),
email: 'subscriber2@test.com'
}
],
webhooks: [
{
id: ObjectId.generate(),
event: 'subscriber.added',
target_url: 'https://example.com/webhooks/subscriber-added'
},
{
id: ObjectId.generate(),
event: 'subscriber.removed',
target_url: 'https://example.com/webhooks/subscriber-removed'
}
]
};
@ -344,7 +357,8 @@ DataGenerator.forKnex = (function () {
users,
roles_users,
clients,
invites;
invites,
webhooks;
function createBasic(overrides) {
var newObj = _.cloneDeep(overrides);
@ -522,6 +536,20 @@ DataGenerator.forKnex = (function () {
});
}
function createWebhook(overrides) {
var newObj = _.cloneDeep(overrides);
return _.defaults(newObj, {
id: ObjectId.generate(),
event: 'test',
target_url: 'https://example.com/hooks/test',
created_by: DataGenerator.Content.users[0].id,
created_at: new Date(),
updated_by: DataGenerator.Content.users[0].id,
updated_at: new Date()
});
}
posts = [
createPost(DataGenerator.Content.posts[0]),
createPost(DataGenerator.Content.posts[1]),
@ -626,6 +654,11 @@ DataGenerator.forKnex = (function () {
createInvite({email: 'test2@ghost.org', role_id: DataGenerator.Content.roles[2].id})
];
webhooks = [
createWebhook(DataGenerator.Content.webhooks[0]),
createWebhook(DataGenerator.Content.webhooks[1])
];
return {
createPost: createPost,
createGenericPost: createGenericPost,
@ -644,6 +677,7 @@ DataGenerator.forKnex = (function () {
createSubscriber: createBasic,
createInvite: createInvite,
createTrustedDomain: createTrustedDomain,
createWebhook: createWebhook,
invites: invites,
posts: posts,
@ -654,7 +688,8 @@ DataGenerator.forKnex = (function () {
roles: roles,
users: users,
roles_users: roles_users,
clients: clients
clients: clients,
webhooks: webhooks
};
}());

View file

@ -435,6 +435,10 @@ fixtures = {
insertInvites: function insertInvites() {
return db.knex('invites').insert(DataGenerator.forKnex.invites);
},
insertWebhooks: function insertWebhooks() {
return db.knex('webhooks').insert(DataGenerator.forKnex.webhooks);
}
};
@ -540,6 +544,9 @@ toDoList = {
},
themes: function loadThemes() {
return themes.loadAll();
},
webhooks: function insertWebhooks() {
return fixtures.insertWebhooks();
}
};