2
1
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2023-12-13 21:00:40 +01:00

Added HTTP BREAD for integrations resource (#9985)

refs #9865

* Added generic messaging for resource not found
* Ensured integration model uses transaction for writes
* Created POST /integrations endpoint
* Created GET /integrations/:id endpoint
* Created GET /integrations endpoint
* Created PUT /integrations/:id endpoint
* Created DELETE /integrations/:id endpoint
This commit is contained in:
Fabien O'Carroll 2018-10-18 20:03:56 +07:00 committed by Kevin Ansfield
parent da2c292f64
commit 17feb14e4a
10 changed files with 641 additions and 0 deletions

View file

@ -6,6 +6,10 @@ module.exports = {
return shared.http;
},
get integrations() {
return shared.pipeline(require('./integrations'), localUtils);
},
// @TODO: transform
get session() {
return require('./session');

View file

@ -0,0 +1,145 @@
const common = require('../../lib/common');
const models = require('../../models');
module.exports = {
docName: 'integrations',
browse: {
permissions: true,
options: [
'include',
'limit'
],
validation: {
options: {
include: {
values: ['api_keys', 'webhooks']
}
}
},
query({options}) {
return models.Integration.findPage(options);
}
},
read: {
permissions: true,
data: [
'id'
],
options: [
'include'
],
validation: {
data: {
id: {
required: true
}
},
options: {
include: {
values: ['api_keys', 'webhooks']
}
}
},
query({data, options}) {
return models.Integration.findOne(data, Object.assign(options, {require: true}))
.catch(models.Integration.NotFoundError, () => {
throw new common.errors.NotFoundError({
message: common.i18n.t('errors.api.resource.resourceNotFound', {
resource: 'Integration'
})
});
});
}
},
edit: {
permissions: true,
data: [
'name',
'icon_image',
'description',
'webhooks'
],
options: [
'id',
'include'
],
validation: {
options: {
id: {
required: true
},
include: {
values: ['api_keys', 'webhooks']
}
}
},
query({data, options}) {
return models.Integration.edit(data, Object.assign(options, {require: true}))
.catch(models.Integration.NotFoundError, () => {
throw new common.errors.NotFoundError({
message: common.i18n.t('errors.api.resource.resourceNotFound', {
resource: 'Integration'
})
});
});
}
},
add: {
statusCode: 201,
permissions: true,
data: [
'name',
'icon_image',
'description',
'webhooks'
],
options: [
'include'
],
validation: {
data: {
name: {
required: true
}
},
options: {
include: {
values: ['api_keys', 'webhooks']
}
}
},
query({data, options}) {
const dataWithApiKeys = Object.assign({
api_keys: [
{type: 'content'},
{type: 'admin'}
]
}, data);
return models.Integration.add(dataWithApiKeys, options);
}
},
destroy: {
statusCode: 204,
permissions: true,
options: [
'id'
],
validation: {
options: {
id: {
required: true
}
}
},
query({options}) {
return models.Integration.destroy(Object.assign(options, {require: true}))
.catch(models.Integration.NotFoundError, () => {
throw new common.errors.NotFoundError({
message: common.i18n.t('errors.api.resource.resourceNotFound', {
resource: 'Integration'
})
});
});
}
}
};

View file

@ -1,4 +1,7 @@
module.exports = {
get integrations() {
return require('./integrations');
},
get pages() {
return require('./pages');
},

View file

@ -0,0 +1,15 @@
const _ = require('lodash');
const debug = require('ghost-ignition').debug('api:v2:utils:serializers:input:integrations');
module.exports = {
add(apiConfig, frame) {
debug('add');
frame.data = _.pick(frame.data.integrations[0], apiConfig.data);
},
edit(apiConfig, frame) {
debug('edit');
frame.data = _.pick(frame.data.integrations[0], apiConfig.data);
}
};

View file

@ -1,4 +1,8 @@
module.exports = {
get integrations() {
return require('./integrations');
},
get pages() {
return require('./pages');
},

View file

@ -0,0 +1,34 @@
const debug = require('ghost-ignition').debug('api:v2:utils:serializers:output:integrations');
module.exports = {
browse({data, meta}, apiConfig, frame) {
debug('browse');
frame.response = {
integrations: data.map(model => model.toJSON(frame.options)),
meta
};
},
read(model, apiConfig, frame) {
debug('read');
frame.response = {
integrations: [model.toJSON(frame.options)]
};
},
add(model, apiConfig, frame) {
debug('add');
frame.response = {
integrations: [model.toJSON(frame.options)]
};
},
edit(model, apiConfig, frame) {
debug('edit');
frame.response = {
integrations: [model.toJSON(frame.options)]
};
}
};

View file

@ -10,6 +10,44 @@ const Integration = ghostBookshelf.Model.extend({
webhooks: 'webhooks'
},
add(data, options) {
const addIntegration = () => {
return ghostBookshelf.Model.add.call(this, data, options)
.then(({id}) => {
return this.findOne({id}, options);
});
};
if (!options.transacting) {
return ghostBookshelf.transaction((transacting) => {
options.transacting = transacting;
return addIntegration();
});
}
return addIntegration();
},
edit(data, options) {
const editIntegration = () => {
return ghostBookshelf.Model.edit.call(this, data, options)
.then(({id}) => {
return this.findOne({id}, options);
});
};
if (!options.transacting) {
return ghostBookshelf.transaction((transacting) => {
options.transacting = transacting;
return editIntegration();
});
}
return editIntegration();
},
onSaving(newIntegration, attr, options) {
if (this.hasChanged('slug') || !this.get('slug')) {
// Pass the new slug through the generator to strip illegal characters, detect duplicates

View file

@ -357,6 +357,9 @@
"missingFile": "Please select a JSON file.",
"invalidFile": "Please select a valid JSON file to import."
},
"resource": {
"resourceNotFound": "{resource} not found."
},
"routes": {
"missingFile": "Please select a YAML file.",
"invalidFile": "Please select a valid YAML file to import."

View file

@ -33,6 +33,14 @@ module.exports = function apiRoutes() {
router.put('/posts/:id', mw.authAdminAPI, apiv2.http(apiv2.posts.edit));
router.del('/posts/:id', mw.authAdminAPI, apiv2.http(apiv2.posts.destroy));
// # Integrations
router.get('/integrations', mw.authAdminAPI, apiv2.http(apiv2.integrations.browse));
router.get('/integrations/:id', mw.authAdminAPI, apiv2.http(apiv2.integrations.read));
router.post('/integrations', mw.authAdminAPI, apiv2.http(apiv2.integrations.add));
router.put('/integrations/:id', mw.authAdminAPI, apiv2.http(apiv2.integrations.edit));
router.del('/integrations/:id', mw.authAdminAPI, apiv2.http(apiv2.integrations.destroy));
// ## Schedules
router.put('/schedules/posts/:id', [
auth.authenticate.authenticateClient,

View file

@ -0,0 +1,387 @@
const should = require('should');
const supertest = require('supertest');
const config = require('../../../../../../core/server/config');
const testUtils = require('../../../../utils');
const localUtils = require('./utils');
const ghost = testUtils.startGhost;
describe('Integrations API', function () {
let request;
before(function () {
return ghost()
.then(() => {
request = supertest.agent(config.get('url'));
})
.then(() => {
return localUtils.doAuth(request, 'integrations');
});
});
const findBy = (prop, val) => object => object[prop] === val;
describe('POST /integrations/', function () {
it('Can successfully create a single integration with auto generated content and admin api key', function (done) {
request.post(localUtils.API.getApiQuery('integrations/'))
.set('Origin', config.get('url'))
.send({
integrations: [{
name: 'Dis-Integrate!!'
}]
})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(201)
.end(function (err, {body}) {
if (err) {
return done(err);
}
should.equal(body.integrations.length, 1);
const [integration] = body.integrations;
should.equal(integration.name, 'Dis-Integrate!!');
should.equal(integration.api_keys.length, 2);
const contentApiKey = integration.api_keys.find(findBy('type', 'content'));
should.equal(contentApiKey.integration_id, integration.id);
const adminApiKey = integration.api_keys.find(findBy('type', 'admin'));
should.equal(adminApiKey.integration_id, integration.id);
done();
});
});
it('Can successfully create a single integration with a webhook', function (done) {
request.post(localUtils.API.getApiQuery('integrations/'))
.set('Origin', config.get('url'))
.send({
integrations: [{
name: 'Integratatron4000',
webhooks: [{
event: 'something',
target_url: 'http://example.com',
}]
}]
})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(201)
.end(function (err, {body}) {
if (err) {
return done(err);
}
should.equal(body.integrations.length, 1);
const [integration] = body.integrations;
should.equal(integration.name, 'Integratatron4000');
should.equal(integration.webhooks.length, 1);
const webhook = integration.webhooks[0];
should.equal(webhook.integration_id, integration.id);
done();
});
});
});
describe('GET /integrations/:id', function () {
it('Can successfully get a created integration', function (done) {
request.post(localUtils.API.getApiQuery('integrations/'))
.set('Origin', config.get('url'))
.send({
integrations: [{
name: 'Interrogation Integration'
}]
})
.expect(201)
.end(function (err, {body}) {
if (err) {
return done(err);
}
const [createdIntegration] = body.integrations;
request.get(localUtils.API.getApiQuery(`integrations/${createdIntegration.id}/`))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, {body}) {
if (err) {
return done(err);
}
should.equal(body.integrations.length, 1);
const [integration] = body.integrations;
should.equal(integration.id, createdIntegration.id);
should.equal(integration.name, createdIntegration.name);
should.equal(integration.slug, createdIntegration.slug);
should.equal(integration.description, createdIntegration.description);
should.equal(integration.icon_image, createdIntegration.icon_image);
done();
});
});
});
it('Will 404 if the integration does not exist', function (done) {
request.get(localUtils.API.getApiQuery(`integrations/012345678901234567890123/`))
.set('Origin', config.get('url'))
.expect(404)
.end(done);
});
});
describe('GET /integrations/', function () {
it('Can successfully get *all* created integrations with api_keys', function (done) {
request.post(localUtils.API.getApiQuery('integrations/'))
.set('Origin', config.get('url'))
.send({
integrations: [{
name: 'Integrate with this!'
}]
})
.expect(201)
.end(function (err) {
if (err) {
return done(err);
}
request.post(localUtils.API.getApiQuery('integrations/'))
.set('Origin', config.get('url'))
.send({
integrations: [{
name: 'Winter-(is)-great'
}]
})
.expect(201)
.end(function (err) {
if (err) {
return done(err);
}
request.get(localUtils.API.getApiQuery(`integrations/?include=api_keys&limit=all`))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, {body}) {
if (err) {
return done(err);
}
// This is the only page
should.equal(body.meta.pagination.page, 1);
should.equal(body.meta.pagination.pages, 1);
should.equal(body.meta.pagination.next, null);
should.equal(body.meta.pagination.prev, null);
body.integrations.forEach(integration => {
should.exist(integration.api_keys);
});
done();
});
});
});
});
});
describe('PUT /integrations/:id', function () {
it('Can successfully edit a created integration', function (done) {
request.post(localUtils.API.getApiQuery('integrations/'))
.set('Origin', config.get('url'))
.send({
integrations: [{
name: 'Rubbish Integration Name'
}]
})
.expect(201)
.end(function (err, {body}) {
if (err) {
return done(err);
}
const [createdIntegration] = body.integrations;
request.put(localUtils.API.getApiQuery(`integrations/${createdIntegration.id}/`))
.set('Origin', config.get('url'))
.send({
integrations: [{
name: 'Awesome Integration Name',
description: 'Finally got round to writing this...'
}]
})
.expect(200)
.end(function (err) {
if (err) {
return done(err);
}
request.get(localUtils.API.getApiQuery(`integrations/${createdIntegration.id}/`))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, {body}) {
if (err) {
return done(err);
}
const [updatedIntegration] = body.integrations;
should.equal(updatedIntegration.id, createdIntegration.id);
should.equal(updatedIntegration.name, 'Awesome Integration Name');
should.equal(updatedIntegration.description, 'Finally got round to writing this...');
done();
});
});
});
});
it('Can successfully add and delete a created integrations webhooks', function (done) {
request.post(localUtils.API.getApiQuery('integrations/'))
.set('Origin', config.get('url'))
.send({
integrations: [{
name: 'Webhook-less Integration',
}]
})
.expect(201)
.end(function (err, {body}) {
if (err) {
return done(err);
}
const [createdIntegration] = body.integrations;
request.put(localUtils.API.getApiQuery(`integrations/${createdIntegration.id}/`))
.set('Origin', config.get('url'))
.send({
integrations: [{
webhooks: [{
event: 'somestuff',
target_url: 'http://example.com'
}]
}]
})
.expect(200)
.end(function (err) {
if (err) {
return done(err);
}
request.get(localUtils.API.getApiQuery(`integrations/${createdIntegration.id}/?include=webhooks`))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, {body}) {
if (err) {
return done(err);
}
const [updatedIntegration] = body.integrations;
should.equal(updatedIntegration.webhooks.length, 1);
const webhook = updatedIntegration.webhooks[0];
should.equal(webhook.integration_id, updatedIntegration.id);
request.put(localUtils.API.getApiQuery(`integrations/${createdIntegration.id}/`))
.set('Origin', config.get('url'))
.send({
integrations: [{
webhooks: []
}]
})
.expect(200)
.end(function (err) {
if (err) {
return done(err);
}
request.get(localUtils.API.getApiQuery(`integrations/${createdIntegration.id}/?include=webhooks`))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.end(function (err, {body}) {
if (err) {
return done(err);
}
const [updatedIntegration] = body.integrations;
should.equal(updatedIntegration.webhooks.length, 0);
done();
});
});
});
});
});
});
it('Will 404 if the integration does not exist', function (done) {
request.put(localUtils.API.getApiQuery(`integrations/012345678901234567890123/`))
.set('Origin', config.get('url'))
.send({
integrations: [{
name: 'This better not work'
}]
})
.expect(404)
.end(done);
});
});
describe('DELETE /integrations/:id', function () {
it('Can succesfully delete a created integration', function (done) {
request.post(localUtils.API.getApiQuery('integrations/'))
.set('Origin', config.get('url'))
.send({
integrations: [{
name: 'Short Lived Integration'
}]
})
.expect(201)
.end(function (err, {body}) {
if (err) {
return done(err);
}
const [createdIntegration] = body.integrations;
request.del(localUtils.API.getApiQuery(`integrations/${createdIntegration.id}/`))
.set('Origin', config.get('url'))
.expect(204)
.end(function (err) {
if (err) {
return done(err);
}
request.get(localUtils.API.getApiQuery(`integrations/${createdIntegration.id}/`))
.set('Origin', config.get('url'))
.expect(404)
.end(done);
});
});
});
it('Will 404 if the integration does not exist', function (done) {
request.del(localUtils.API.getApiQuery(`integrations/012345678901234567890123/`))
.set('Origin', config.get('url'))
.expect(404)
.end(done);
});
it('Will delete the associated api_keys and webhooks', function () {
/**
* @TODO
*
* We do not have the /apikeys or /webhooks endpoints yet
* This will be manually tested by egg before merging
*/
});
});
});