diff --git a/common/locales/en/api-v3.json b/common/locales/en/api-v3.json index afb7b17f00..9dcec829a9 100644 --- a/common/locales/en/api-v3.json +++ b/common/locales/en/api-v3.json @@ -148,5 +148,7 @@ "noSudoAccess": "You don't have sudo access.", "couponCodeRequired": "The coupon code is required.", "eventRequired": "\"req.params.event\" is required.", - "countRequired": "\"req.query.count\" is required." + "countRequired": "\"req.query.count\" is required.", + "invalidUrl": "invalid url", + "invalidEnabled": "the \"enabled\" parameter should be a boolean" } diff --git a/common/script/index.js b/common/script/index.js index ee7206f3b1..10540046c4 100644 --- a/common/script/index.js +++ b/common/script/index.js @@ -118,6 +118,9 @@ import purchase from './ops/purchase'; import purchaseHourglass from './ops/hourglassPurchase'; import readCard from './ops/readCard'; import openMysteryItem from './ops/openMysteryItem'; +import addWebhook from './ops/addWebhook'; +import updateWebhook from './ops/updateWebhook'; +import deleteWebhook from './ops/deleteWebhook'; api.ops = { scoreTask, @@ -137,6 +140,9 @@ api.ops = { purchaseHourglass, readCard, openMysteryItem, + addWebhook, + updateWebhook, + deleteWebhook, }; import handleTwoHanded from './fns/handleTwoHanded'; diff --git a/common/script/ops/addWebhook.js b/common/script/ops/addWebhook.js index 99eaf49bba..0300afef28 100644 --- a/common/script/ops/addWebhook.js +++ b/common/script/ops/addWebhook.js @@ -1,15 +1,22 @@ import refPush from '../libs/refPush'; +import validator from 'validator'; +import i18n from '../i18n'; +import { + NotFound, + BadRequest, +} from '../libs/errors'; -module.exports = function(user, req, cb) { +module.exports = function(user, req) { var wh; wh = user.preferences.webhooks; - refPush(wh, { + + if(!validator.isURL(req.body.url)) throw new BadRequest(i18n.t('invalidUrl', req.language)); + if(!validator.isBoolean(req.body.enabled)) throw new BadRequest(i18n.t('invalidEnabled', req.language)); + + user.markModified('preferences.webhooks'); + + return refPush(wh, { url: req.body.url, - enabled: req.body.enabled || true, - id: req.body.id + enabled: req.body.enabled, }); - if (typeof user.markModified === "function") { - user.markModified('preferences.webhooks'); - } - return typeof cb === "function" ? cb(null, user.preferences.webhooks) : void 0; }; diff --git a/common/script/ops/deleteWebhook.js b/common/script/ops/deleteWebhook.js index a187f08770..45a5c4388c 100644 --- a/common/script/ops/deleteWebhook.js +++ b/common/script/ops/deleteWebhook.js @@ -1,7 +1,5 @@ -module.exports = function(user, req, cb) { + +module.exports = function(user, req) { delete user.preferences.webhooks[req.params.id]; - if (typeof user.markModified === "function") { - user.markModified('preferences.webhooks'); - } - return typeof cb === "function" ? cb(null, user.preferences.webhooks) : void 0; + user.markModified('preferences.webhooks'); }; diff --git a/common/script/ops/updateWebhook.js b/common/script/ops/updateWebhook.js index e2775a40b4..27098c4c4a 100644 --- a/common/script/ops/updateWebhook.js +++ b/common/script/ops/updateWebhook.js @@ -1,9 +1,16 @@ import _ from 'lodash'; +import validator from 'validator'; +import i18n from '../i18n'; +import { + NotFound, + BadRequest, +} from '../libs/errors'; -module.exports = function(user, req, cb) { - _.merge(user.preferences.webhooks[req.params.id], req.body); - if (typeof user.markModified === "function") { - user.markModified('preferences.webhooks'); - } - return typeof cb === "function" ? cb(null, user.preferences.webhooks) : void 0; +module.exports = function(user, req) { + if(!validator.isURL(req.body.url)) throw new BadRequest(i18n.t('invalidUrl', req.language)); + if(!validator.isBoolean(req.body.enabled)) throw new BadRequest(i18n.t('invalidEnabled', req.language)); + + user.markModified('preferences.webhooks'); + user.preferences.webhooks[req.params.id].url = req.body.url; + user.preferences.webhooks[req.params.id].enabled = req.body.enabled; }; diff --git a/tasks/gulp-tests.js b/tasks/gulp-tests.js index 09a974c7f5..f8472bbf9c 100644 --- a/tasks/gulp-tests.js +++ b/tasks/gulp-tests.js @@ -358,6 +358,10 @@ gulp.task('test:api-v3:integration', (done) => { pipe(runner); }); +gulp.task('test:api-v3:integration:watch', () => { + gulp.watch(['website/src/controllers/api-v3/**/*', 'test/api/v3/integration/**/*', 'common/script/ops/*'], ['test:api-v3:integration']); +}); + gulp.task('test:api-v3:integration:separate-server', (done) => { let runner = exec( testBin('mocha test/api/v3/integration --recursive', 'LOAD_SERVER=0'), diff --git a/test/api/v3/integration/user/DELETE-user_delete_webhook.test.js b/test/api/v3/integration/user/DELETE-user_delete_webhook.test.js new file mode 100644 index 0000000000..46844dd855 --- /dev/null +++ b/test/api/v3/integration/user/DELETE-user_delete_webhook.test.js @@ -0,0 +1,23 @@ +import { + generateUser, +} from '../../../../helpers/api-integration/v3'; + +let user; +let endpoint = '/user/webhook'; + +describe('DELETE /user/webhook', () => { + beforeEach(async () => { + user = await generateUser(); + }); + + it('succeeds', async () => { + let id = 'some-id'; + user.preferences.webhooks[id] = { url: 'http://some-url.com', enabled: true }; + await user.sync(); + expect(user.preferences.webhooks).to.eql({}); + let response = await user.del(`${endpoint}/${id}`); + expect(response).to.eql({}); + await user.sync(); + expect(user.preferences.webhooks).to.eql({}); + }); +}); diff --git a/test/api/v3/integration/user/POST-user_add_webhook.test.js b/test/api/v3/integration/user/POST-user_add_webhook.test.js new file mode 100644 index 0000000000..d13f15baa4 --- /dev/null +++ b/test/api/v3/integration/user/POST-user_add_webhook.test.js @@ -0,0 +1,29 @@ +import { + generateUser, + translate as t, +} from '../../../../helpers/api-integration/v3'; + +let user; +let endpoint = '/user/webhook'; + +describe('POST /user/webhook', () => { + beforeEach(async () => { + user = await generateUser(); + }); + + it('validates', async () => { + await expect(user.post(endpoint, { enabled: true })).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('invalidUrl'), + }); + }); + + it('successfully adds the webhook', async () => { + expect(user.preferences.webhooks).to.eql({}); + let response = await user.post(endpoint, { enabled: true, url: 'http://some-url.com'}); + expect(response.id).to.exist; + await user.sync(); + expect(user.preferences.webhooks).to.not.eql({}); + }); +}); diff --git a/test/api/v3/integration/user/PUT-user_update_webhook.test.js b/test/api/v3/integration/user/PUT-user_update_webhook.test.js new file mode 100644 index 0000000000..715070c991 --- /dev/null +++ b/test/api/v3/integration/user/PUT-user_update_webhook.test.js @@ -0,0 +1,32 @@ +import { + generateUser, + translate as t, +} from '../../../../helpers/api-integration/v3'; + +let user; +let url = 'http://new-url.com'; +let enabled = true; + +describe('PUT /user/webhook/:id', () => { + beforeEach(async () => { + user = await generateUser(); + }); + + it('validation fails', async () => { + await expect(user.put('/user/webhook/some-id'), { enabled: true }).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('invalidUrl'), + }); + }); + + it('succeeds', async () => { + let response = await user.post('/user/webhook', { enabled: true, url: 'http://some-url.com'}); + await user.sync(); + expect(user.preferences.webhooks[response.id].url).to.not.eql(url); + let response2 = await user.put(`/user/webhook/${response.id}`, {url, enabled}); + expect(response2).to.eql({}); + await user.sync(); + expect(user.preferences.webhooks[response.id].url).to.eql(url); + }); +}); diff --git a/test/common/ops/addWebhook.test.js b/test/common/ops/addWebhook.test.js new file mode 100644 index 0000000000..11d26e622b --- /dev/null +++ b/test/common/ops/addWebhook.test.js @@ -0,0 +1,57 @@ +import addWebhook from '../../../common/script/ops/addWebhook'; +import { + BadRequest, +} from '../../../common/script/libs/errors'; +import i18n from '../../../common/script/i18n'; +import { + generateUser, +} from '../../helpers/common.helper'; + +describe('shared.ops.addWebhook', () => { + let user; + let req; + + beforeEach(() => { + user = generateUser(); + req = { body: { + enabled: true, + url: 'http://some-url.com', + } }; + }); + + context('adds webhook', () => { + it('validates req.body.url', (done) => { + delete req.body.url; + try { + addWebhook(user, req); + } catch (err) { + expect(err).to.be.an.instanceof(BadRequest); + expect(err.message).to.equal(i18n.t('invalidUrl')); + done(); + } + }); + + it('validates req.body.enabled', (done) => { + delete req.body.enabled; + try { + addWebhook(user, req); + } catch (err) { + expect(err).to.be.an.instanceof(BadRequest); + expect(err.message).to.equal(i18n.t('invalidEnabled')); + done(); + } + }); + + it('calls marksModified()', () => { + user.markModified = sinon.spy(); + addWebhook(user, req); + expect(user.markModified.called).to.eql(true); + }); + + it('succeeds', () => { + expect(user.preferences.webhooks).to.eql({}); + addWebhook(user, req); + expect(user.preferences.webhooks).to.not.eql({}); + }); + }); +}); diff --git a/test/common/ops/deleteWebhook.test.js b/test/common/ops/deleteWebhook.test.js new file mode 100644 index 0000000000..0a3178007a --- /dev/null +++ b/test/common/ops/deleteWebhook.test.js @@ -0,0 +1,20 @@ +import deleteWebhook from '../../../common/script/ops/deleteWebhook'; +import { + generateUser, +} from '../../helpers/common.helper'; + +describe('shared.ops.deleteWebhook', () => { + let user; + let req; + + beforeEach(() => { + user = generateUser(); + req = { params: { id: 'some-id' } }; + }); + + it('succeeds', () => { + user.preferences.webhooks = { 'some-id': {} }; + deleteWebhook(user, req); + expect(user.preferences.webhooks).to.eql({}); + }); +}); diff --git a/test/common/ops/updateWebhook.test.js b/test/common/ops/updateWebhook.test.js new file mode 100644 index 0000000000..43c353626e --- /dev/null +++ b/test/common/ops/updateWebhook.test.js @@ -0,0 +1,42 @@ +import updateWebhook from '../../../common/script/ops/updateWebhook'; +import { + BadRequest, +} from '../../../common/script/libs/errors'; +import i18n from '../../../common/script/i18n'; +import { + generateUser, +} from '../../helpers/common.helper'; + +describe('shared.ops.updateWebhook', () => { + let user; + let req; + let newUrl = 'http://new-url.com'; + + beforeEach(() => { + user = generateUser(); + req = { params: { + id: 'this-id', + }, body: { + url: newUrl, + enabled: true, + } }; + }); + + it('validates body', (done) => { + delete req.body.url; + try { + updateWebhook(user, req); + } catch (err) { + expect(err).to.be.an.instanceof(BadRequest); + expect(err.message).to.equal(i18n.t('invalidUrl')); + done(); + } + }); + + it('succeeds', () => { + let url = 'http://existing-url.com'; + user.preferences.webhooks = { 'this-id': { url } }; + updateWebhook(user, req); + expect(user.preferences.webhooks['this-id'].url).to.eql(newUrl); + }); +}); diff --git a/website/src/controllers/api-v3/user.js b/website/src/controllers/api-v3/user.js index 49d2f2f05d..df2ce1888e 100644 --- a/website/src/controllers/api-v3/user.js +++ b/website/src/controllers/api-v3/user.js @@ -764,4 +764,62 @@ api.userOpenMysteryItem = { }, }; +/** + * @api {post} /user/webhook + * @apiVersion 3.0.0 + * @apiName UserAddWebhook + * @apiGroup User + * @apiSuccess {} + **/ +api.addWebhook = { + method: 'POST', + middlewares: [authWithHeaders()], + url: '/user/webhook', + async handler (req, res) { + let user = res.locals.user; + let result = common.ops.addWebhook(user, req); + await user.save(); + res.respond(200, {id: result.id}); + }, +}; + +/** + * @api {put} /user/webhook/:id + * @apiVersion 3.0.0 + * @apiName UserUpdateWebhook + * @apiGroup User + * @apiSuccess {} + **/ +api.updateWebhook = { + method: 'PUT', + middlewares: [authWithHeaders()], + url: '/user/webhook/:id', + async handler (req, res) { + let user = res.locals.user; + common.ops.updateWebhook(user, req); + await user.save(); + res.respond(200, {}); + }, +}; + +/** + * @api {delete} /user/webhook/:id + * @apiVersion 3.0.0 + * @apiName UserDeleteWebhook + * @apiGroup User + * @apiSuccess {} + **/ +api.deleteWebhook = { + method: 'DELETE', + middlewares: [authWithHeaders()], + url: '/user/webhook/:id', + async handler (req, res) { + let user = res.locals.user; + common.ops.deleteWebhook(user, req); + await user.save(); + res.respond(200, {}); + }, +}; + + module.exports = api;