├── .gitignore ├── .travis.yml ├── example.js ├── package.json ├── README.md ├── index.js └── test └── webhooks.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | coverage/ 4 | /webHooksDB.json 5 | test/webHooksDB.json 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "6.2.1" 5 | - "7.10" 6 | - "8.4" 7 | 8 | cache: 9 | directories: 10 | - node_modules 11 | 12 | after_success: 13 | - npm run publish-coverage 14 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | // Initialize WebHooks module. 2 | var WebHooks = require('./index') 3 | 4 | var webHooks = new WebHooks({ 5 | db: './webHooksDB.json' // json file that store webhook URLs 6 | }) 7 | 8 | // sync instantation - add a new webhook called 'shortname1' 9 | webHooks.add('shortname1', 'http://127.0.0.1:9000/prova/other_url').then(function () { 10 | // done 11 | }).catch(function (err) { 12 | console.log(err) 13 | }) 14 | 15 | // add another webHook 16 | webHooks.add('shortname2', 'http://127.0.0.1:9000/prova2/').then(function () { 17 | // done 18 | }).catch(function (err) { 19 | console.log(err) 20 | }) 21 | 22 | // remove a single url attached to the given shortname 23 | // webHooks.remove('shortname3', 'http://127.0.0.1:9000/query/').catch(function(err){console.error(err);}); 24 | 25 | // if no url is provided, remove all the urls attached to the given shortname 26 | // webHooks.remove('shortname3').catch(function(err){console.error(err);}); 27 | 28 | // trigger a specific webHook 29 | webHooks.trigger('shortname1', {data: 123}) 30 | webHooks.trigger('shortname2', {data: 123456}, {header: 'header'}) // payload will be sent as POST request with JSON body (Content-Type: application/json) and custom header 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-webhooks", 3 | "version": "1.4.2", 4 | "description": "Create and trigger your own webHooks", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "npm run std && npm run coverage", 8 | "std": "standard", 9 | "coverage": "istanbul cover _mocha -- --timeout=5000 test/**/*.js", 10 | "publish-coverage": "cat ./coverage/lcov.info | coveralls" 11 | }, 12 | "engines": { 13 | "node": ">=6.*.*" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/roccomuso/node-webhooks.git" 18 | }, 19 | "keywords": [ 20 | "webhooks", 21 | "trigger", 22 | "web", 23 | "hooks" 24 | ], 25 | "author": "Rocco Musolino ", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/roccomuso/node-webhooks/issues" 29 | }, 30 | "homepage": "https://github.com/roccomuso/node-webhooks#readme", 31 | "dependencies": { 32 | "bluebird": "^3.5.3", 33 | "debug": "^4.1.1", 34 | "eventemitter2": "^5.0.1", 35 | "jsonfile": "^5.0.0", 36 | "lodash": "^4.17.11", 37 | "request": "^2.88.0" 38 | }, 39 | "devDependencies": { 40 | "chai": "^4.2.0", 41 | "coveralls": "^2.11.16", 42 | "istanbul": "^0.4.5", 43 | "mocha": "^3.4.2", 44 | "mocha-lcov-reporter": "^1.2.0", 45 | "standard": "^10.0.2" 46 | }, 47 | "standard": { 48 | "env": [ 49 | "mocha" 50 | ] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-webhooks [![Build Status](https://travis-ci.org/roccomuso/node-webhooks.svg?branch=master)](https://travis-ci.org/roccomuso/node-webhooks) [![NPM Version](https://img.shields.io/npm/v/node-webhooks.svg)](https://www.npmjs.com/package/node-webhooks) [![Coverage Status](https://coveralls.io/repos/github/roccomuso/node-webhooks/badge.svg?branch=master)](https://coveralls.io/github/roccomuso/node-webhooks?branch=master) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) [![Dependency Status](https://david-dm.org/roccomuso/node-webhooks.png)](https://david-dm.org/roccomuso/node-webhooks) Patreon donate button 2 | 3 | ## What WebHooks are used for 4 | 5 | > Webhooks are "user-defined HTTP callbacks". They are usually triggered by some event, such as pushing code to a repository or a comment being posted to a blog. When that event occurs, the source site makes an HTTP request to the URI configured for the webhook. Users can configure them to cause events on one site to invoke behaviour on another. The action taken may be anything. Common uses are to trigger builds with continuous integration systems or to notify bug tracking systems. Since they use HTTP, they can be integrated into web services without adding new infrastructure. 6 | 7 | ## Install 8 | 9 | npm install node-webhooks --save 10 | 11 | Supporting Node.js 0.12 or above. 12 | 13 | ## How it works 14 | 15 | When a webHook is triggered it will send an HTTPS POST request to the attached URLs, containing a JSON-serialized Update (the one specified when you call the **trigger** method). 16 | 17 | ## Debug 18 | 19 | This module makes use of the popular [debug](https://github.com/visionmedia/debug) package. Use the env variable to enable debug: DEBUG=node-webhooks. 20 | To launch the example and enable debug: DEBUG=node-webhooks node example.js 21 | 22 | ## Usage 23 | 24 | ```javascript 25 | 26 | // Initialize WebHooks module. 27 | var WebHooks = require('node-webhooks') 28 | 29 | // Initialize webhooks module from on-disk database 30 | var webHooks = new WebHooks({ 31 | db: './webHooksDB.json', // json file that store webhook URLs 32 | httpSuccessCodes: [200, 201, 202, 203, 204], //optional success http status codes 33 | }) 34 | 35 | // Alternatively, initialize webhooks module with object; changes will only be 36 | // made in-memory 37 | webHooks = new WebHooks({ 38 | db: {"addPost": ["http://localhost:9100/posts"]}, // just an example 39 | }) 40 | 41 | // sync instantation - add a new webhook called 'shortname1' 42 | webHooks.add('shortname1', 'http://127.0.0.1:9000/prova/other_url').then(function(){ 43 | // done 44 | }).catch(function(err){ 45 | console.log(err) 46 | }) 47 | 48 | // add another webHook 49 | webHooks.add('shortname2', 'http://127.0.0.1:9000/prova2/').then(function(){ 50 | // done 51 | }).catch(function(err){ 52 | console.log(err) 53 | }); 54 | 55 | // remove a single url attached to the given shortname 56 | // webHooks.remove('shortname3', 'http://127.0.0.1:9000/query/').catch(function(err){console.error(err);}) 57 | 58 | // if no url is provided, remove all the urls attached to the given shortname 59 | // webHooks.remove('shortname3').catch(function(err){console.error(err);}) 60 | 61 | // trigger a specific webHook 62 | webHooks.trigger('shortname1', {data: 123}) 63 | webHooks.trigger('shortname2', {data: 123456}, {header: 'header'}) // payload will be sent as POST request with JSON body (Content-Type: application/json) and custom header 64 | 65 | ``` 66 | 67 | ## Available events 68 | 69 | We're using an event emitter library to expose request information on webHook trigger. 70 | 71 | ```javascript 72 | var webHooks = new WebHooks({ 73 | db: WEBHOOKS_DB, 74 | DEBUG: true 75 | }) 76 | 77 | var emitter = webHooks.getEmitter() 78 | 79 | emitter.on('*.success', function (shortname, statusCode, body) { 80 | console.log('Success on trigger webHook' + shortname + 'with status code', statusCode, 'and body', body) 81 | }) 82 | 83 | emitter.on('*.failure', function (shortname, statusCode, body) { 84 | console.error('Error on trigger webHook' + shortname + 'with status code', statusCode, 'and body', body) 85 | }) 86 | ``` 87 | 88 | This makes possible checking if a webHook trigger was successful or not getting request information such as status code or response body. 89 | 90 | The format for the events is built as `eventName.result`. The choosen library `eventemitter2` provides a lot of freedom for listening events. For example: 91 | 92 | - `eventName.success` 93 | - `eventName.failure` 94 | - `eventName.*` 95 | - `*.success` 96 | - `*.*` 97 | 98 | 99 | ## API examples 100 | 101 | webHooks are useful whenever you need to make sure that an external service get updates from your app. 102 | You can easily develop in your APP this kind of webHooks entry-points. 103 | 104 | - GET /api/webhook/get 105 | Return the whole webHook DB file. 106 | 107 | - GET /api/webhook/get/[WebHookShortname] 108 | Return the selected WebHook. 109 | 110 | - POST /api/webhook/add/[WebHookShortname] 111 | Add a new URL for the selected webHook. Requires JSON params: 112 | 113 | - GET /api/webhook/delete/[WebHookShortname] 114 | Remove all the urls attached to the selected webHook. 115 | 116 | - POST /api/webhook/delete/[WebHookShortname] 117 | Remove only one single url attached to the selected webHook. 118 | A json body with the url parameter is required: { "url": "http://..." } 119 | 120 | - POST /api/webhook/trigger/[WebHookShortname] 121 | Trigger a webHook. It requires a JSON body that will be turned over to the webHook URLs. You can also provide custom headers. 122 | 123 | 124 | 125 | ### Author 126 | 127 | Rocco Musolino - [@roccomuso](https://twitter.com/roccomuso) 128 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Author @ Rocco Musolino 3 | 4 | DB Structure Example: 5 | 6 | { 7 | "shortname1": [url1, url2, ...], 8 | "shortname2": [url3, url4, ...], 9 | ... 10 | ... 11 | "shortnameX": [urlZ, ...] 12 | } 13 | 14 | */ 15 | 16 | var debug = require('debug')('node-webhooks') 17 | var Promise = require('bluebird') // for backward compatibility 18 | var _ = require('lodash') 19 | var jsonfile = require('jsonfile') 20 | var fs = require('fs') 21 | var crypto = require('crypto') 22 | var request = require('request') 23 | var events = require('eventemitter2') 24 | 25 | // will contain all the functions. We need to store them to be able to remove the listener callbacks 26 | var _functions = {} 27 | 28 | // WebHooks Class 29 | function WebHooks (options) { 30 | if (typeof options !== 'object') throw new TypeError('Expected an Object') 31 | if (typeof options.db !== 'string' && typeof options.db !== 'object') { 32 | throw new TypeError('db Must be a String path or an object') 33 | } 34 | 35 | this.db = options.db 36 | 37 | // If webhooks data is kept in memory, we skip all disk operations 38 | this.isMemDb = typeof options.db === 'object' 39 | 40 | if (options.hasOwnProperty('httpSuccessCodes')) { 41 | if (!(options.httpSuccessCodes instanceof Array)) throw new TypeError('httpSuccessCodes must be an array') 42 | if (options.httpSuccessCodes.length <= 0) throw new TypeError('httpSuccessCodes must contain at least one http status code') 43 | 44 | this.httpSuccessCodes = options.httpSuccessCodes 45 | } else { 46 | this.httpSuccessCodes = [200] 47 | } 48 | 49 | this.emitter = new events.EventEmitter2({ wildcard: true }) 50 | 51 | var self = this 52 | 53 | if (this.isMemDb) { 54 | debug('setting listeners based on provided configuration object...') 55 | _setListeners(self) 56 | } else { 57 | // sync loading: 58 | try { 59 | fs.accessSync(this.db, fs.R_OK | fs.W_OK) 60 | // DB already exists, set listeners for every URL. 61 | debug('webHook DB loaded, setting listeners...') 62 | _setListeners(self) 63 | } catch (e) { 64 | // DB file not found, initialize it 65 | if (e.hasOwnProperty('code')) { 66 | if (e.code === 'ENOENT') { 67 | // file not found, init DB: 68 | debug('webHook DB init') 69 | _initDB(self.db) 70 | } else console.error(e) 71 | } else console.error(e) 72 | } 73 | } 74 | } 75 | 76 | function _initDB (file) { 77 | // init DB. 78 | var db = {} // init empty db 79 | jsonfile.writeFileSync(file, db, {spaces: 2}) 80 | } 81 | 82 | function _setListeners (self) { 83 | // set Listeners - sync method 84 | 85 | try { 86 | var obj = self.isMemDb ? self.db : jsonfile.readFileSync(self.db) 87 | if (!obj) throw Error('can\'t read webHook DB content') 88 | 89 | for (var key in obj) { 90 | // skip loop if the property is from prototype 91 | if (!obj.hasOwnProperty(key)) continue 92 | 93 | var urls = obj[key] 94 | urls.forEach(function (url) { 95 | var encUrl = crypto.createHash('md5').update(url).digest('hex') 96 | _functions[encUrl] = _getRequestFunction(self, url) 97 | self.emitter.on(key, _functions[encUrl]) 98 | }) 99 | } 100 | } catch (e) { 101 | throw Error(e) 102 | } 103 | 104 | // console.log(_functions[0] == _functions[1]); 105 | // console.log(_functions[1] == _functions[2]); 106 | // console.log(_functions[0] == _functions[2]); 107 | } 108 | 109 | function _getRequestFunction (self, url) { 110 | // return the function then called by the event listener. 111 | var func = function (shortname, jsonData, headersData) { // argument required when eventEmitter.emit() 112 | var obj = {'Content-Type': 'application/json'} 113 | var headers = headersData ? _.merge(obj, headersData) : obj 114 | 115 | debug('POST request to:', url) 116 | // POST request to the instantiated URL with custom headers if provided 117 | request({ 118 | method: 'POST', 119 | uri: url, 120 | strictSSL: false, 121 | headers: headers, 122 | body: JSON.stringify(jsonData) 123 | }, 124 | function (error, response, body) { 125 | var statusCode = response ? response.statusCode : null 126 | body = body || null 127 | debug('Request sent - Server responded with:', statusCode, body) 128 | 129 | if ((error || self.httpSuccessCodes.indexOf(statusCode) === -1)) { 130 | self.emitter.emit(shortname + '.failure', shortname, statusCode, body) 131 | return debug('HTTP failed: ' + error) 132 | } 133 | 134 | self.emitter.emit(shortname + '.success', shortname, statusCode, body) 135 | } 136 | ) 137 | } 138 | 139 | return func 140 | } 141 | 142 | // 'prototype' has improved performances, let's declare the methods 143 | 144 | WebHooks.prototype.trigger = function (shortname, jsonData, headersData) { 145 | // trigger a webHook 146 | this.emitter.emit(shortname, shortname, jsonData, headersData) 147 | } 148 | 149 | WebHooks.prototype.add = function (shortname, url) { // url is required 150 | // add a new webHook. 151 | if (typeof shortname !== 'string') throw new TypeError('shortname required!') 152 | if (typeof url !== 'string') throw new TypeError('Url must be a string') 153 | 154 | var self = this 155 | return new Promise(function (resolve, reject) { 156 | try { 157 | var obj = self.isMemDb ? self.db : jsonfile.readFileSync(self.db) 158 | if (!obj) throw Error('can\'t read webHook DB content') 159 | 160 | var modified = false 161 | var encUrl 162 | if (obj[shortname]) { 163 | // shortname already exists 164 | if (obj[shortname].indexOf(url) === -1) { 165 | // url doesn't exists for given shortname 166 | debug('url added to an existing shortname!') 167 | obj[shortname].push(url) 168 | encUrl = crypto.createHash('md5').update(url).digest('hex') 169 | _functions[encUrl] = _getRequestFunction(self, url) 170 | self.emitter.on(shortname, _functions[encUrl]) 171 | modified = true 172 | } 173 | } else { 174 | // new shortname 175 | debug('new shortname!') 176 | obj[shortname] = [url] 177 | encUrl = crypto.createHash('md5').update(url).digest('hex') 178 | _functions[encUrl] = _getRequestFunction(self, url) 179 | self.emitter.on(shortname, _functions[encUrl]) 180 | modified = true 181 | } 182 | 183 | // actualize DB 184 | if (modified) { 185 | if (!self.isMemDb) jsonfile.writeFileSync(self.db, obj) 186 | resolve(true) 187 | } else resolve(false) 188 | } catch (e) { 189 | reject(e) 190 | } 191 | }) 192 | } 193 | 194 | WebHooks.prototype.remove = function (shortname, url) { // url is optional 195 | // if url exists remove only the url attached to the selected webHook. 196 | // else remove the webHook and all the attached URLs. 197 | if (typeof shortname !== 'string') { 198 | throw new TypeError('shortname required!') 199 | } 200 | var self = this 201 | return new Promise(function (resolve, reject) { 202 | // Basically removeListener will look up the given function by reference, if it found that function it will remove it from the event hander. 203 | try { 204 | if (typeof url !== 'undefined') { 205 | // save in db 206 | _removeUrlFromShortname(self, shortname, url, function (err, done) { 207 | if (err) return reject(err) 208 | if (done) { 209 | // remove only the specified url 210 | var urlKey = crypto.createHash('md5').update(url).digest('hex') 211 | self.emitter.removeListener(shortname, _functions[urlKey]) 212 | delete _functions[urlKey] 213 | resolve(true) 214 | } else resolve(false) 215 | }) 216 | } else { 217 | // remove every event listener attached to the webHook shortname. 218 | self.emitter.removeAllListeners(shortname) 219 | 220 | // delete all the callbacks in _functions for the specified shortname. Let's loop over the url taken from the DB. 221 | var obj = self.isMemDb ? self.db : jsonfile.readFileSync(self.db) 222 | 223 | if (obj.hasOwnProperty(shortname)) { 224 | var urls = obj[shortname] 225 | urls.forEach(function (url) { 226 | var urlKey = crypto.createHash('md5').update(url).digest('hex') 227 | delete _functions[urlKey] 228 | }) 229 | 230 | // save it back to the DB 231 | _removeShortname(self, shortname, function (err) { 232 | if (err) return reject(err) 233 | resolve(true) 234 | }) 235 | } else { 236 | debug('webHook doesn\'t exist') 237 | resolve(false) 238 | } 239 | } 240 | } catch (e) { 241 | reject(e) 242 | } 243 | }) 244 | } 245 | 246 | function _removeUrlFromShortname (self, shortname, url, callback) { 247 | try { 248 | var obj = self.isMemDb ? self.db : jsonfile.readFileSync(self.db) 249 | 250 | var deleted = false 251 | var len = obj[shortname].length 252 | if (obj[shortname].indexOf(url) !== -1) { 253 | obj[shortname].splice(obj[shortname].indexOf(url), 1) 254 | } 255 | if (obj[shortname].length !== len) deleted = true 256 | // save it back to the DB 257 | if (deleted) { 258 | if (!self.isMemDb) jsonfile.writeFileSync(self.db, obj) 259 | debug('url removed from existing shortname') 260 | callback(null, deleted) 261 | } else callback(null, deleted) 262 | } catch (e) { 263 | callback(e, null) 264 | } 265 | } 266 | 267 | function _removeShortname (self, shortname, callback) { 268 | try { 269 | var obj = self.isMemDb ? self.db : jsonfile.readFileSync(self.db) 270 | delete obj[shortname] 271 | // save it back to the DB 272 | if (!self.isMemDb) jsonfile.writeFileSync(self.db, obj) 273 | debug('whole shortname urls removed') 274 | callback(null) 275 | } catch (e) { 276 | callback(e) 277 | } 278 | } 279 | 280 | // async method 281 | WebHooks.prototype.getDB = function () { 282 | // return the whole JSON DB file. 283 | var self = this 284 | return new Promise(function (resolve, reject) { 285 | if (self.isMemDb) resolve(self.db) 286 | jsonfile.readFile(self.db, function (err, obj) { 287 | if (err) { 288 | reject(err) // file not found 289 | } else { 290 | resolve(obj) // file exists 291 | } 292 | }) 293 | }) 294 | } 295 | 296 | // async method 297 | WebHooks.prototype.getWebHook = function (shortname) { 298 | // return the selected WebHook. 299 | var self = this 300 | return new Promise(function (resolve, reject) { 301 | if (self.isMemDb) { 302 | resolve(self.db[shortname] || {}) 303 | } else { 304 | jsonfile.readFile(self.db, function (err, obj) { 305 | if (err) { 306 | reject(err) // file not found 307 | } else { 308 | resolve(obj[shortname] || {}) // file exists 309 | } 310 | }) 311 | } 312 | }) 313 | } 314 | 315 | WebHooks.prototype.getListeners = function () { 316 | return _functions 317 | } 318 | 319 | WebHooks.prototype.getEmitter = function () { 320 | return this.emitter 321 | } 322 | 323 | module.exports = WebHooks 324 | -------------------------------------------------------------------------------- /test/webhooks.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai') 2 | var expect = chai.expect 3 | var should = chai.should() 4 | var debug = require('debug')('test-suite') 5 | var http = require('http') 6 | var fs = require('fs') 7 | var path = require('path') 8 | var WebHooks = require('../index') 9 | var webHooks 10 | var emitter 11 | var DB_FILE = path.join(__dirname, './webHooksDB.json') // json file that store webhook URLs 12 | var DB_OBJECT = { 13 | makeASound: ['http://localhost/beep'], 14 | flashALight: ['http://localhost/blink'] 15 | } 16 | 17 | // IMPLEMENTED TESTS: 18 | 19 | // - spawn a basic server 20 | // - create a webhook istance 21 | // - check wether the json db file exists or not 22 | // - put a webHook 23 | // - add a new URL to the existing webHook 24 | // - call the getWebHook method 25 | // - fire the webHook with no body or headers 26 | // - fire the webHook with custom body. 27 | // - fire the webHook with custom headers. 28 | // - fire the webHook with both body and headers. 29 | // - delete a single webHook URL. 30 | // - fire the webHook and make sure just one URL is called. 31 | // - delete the entire webHook. 32 | // - fire the webHook and make sure no request is dispatched at all. 33 | // - create a new webHook. 34 | // - fire the webHook 1000 times. Expected 1000 REST calls. 35 | 36 | // instantiate a basic web server 37 | var PORT = 8000 38 | var URI = 'http://127.0.0.1:' + PORT 39 | 40 | var OUTCOMES = {} 41 | var LOADTEST = 0 42 | 43 | function handleRequest (request, response) { 44 | debug('called method:', request.method) 45 | debug('called URL:', request.url) 46 | debug('headers:', request.headers) 47 | var body = [] 48 | request.on('data', function (chunk) { 49 | body.push(chunk) 50 | }).on('end', function () { 51 | body = Buffer.concat(body).toString() // as string 52 | OUTCOMES[request.url] = { 53 | headers: request.headers, 54 | body: body 55 | } 56 | if (request.url.indexOf('/2/') !== -1) LOADTEST++ 57 | debug('body:', body) 58 | if (request.url.indexOf('/fail') !== -1) { response.writeHead(400, {'Content-Type': 'application/json'}) } else { 59 | response.writeHead(200, {'Content-Type': 'application/json'}) 60 | } 61 | response.end('Path Hit: ' + request.url) 62 | }) 63 | } 64 | 65 | // Create a server 66 | var server = http.createServer(handleRequest) 67 | 68 | // To verify that basic CRUD operations work correctly both with in-memory and 69 | // on-disk database, we use these functions with the two different settings 70 | // below. 71 | 72 | function addShortnameRequired (done) { 73 | try { 74 | webHooks.add(null, URI + '/1/aaa').then(function () { 75 | done('Error expected') 76 | }).catch(function (err) { 77 | throw new Error(err) 78 | }) 79 | } catch (e) { 80 | expect(e.message).to.equal('shortname required!') 81 | done() 82 | } 83 | } 84 | 85 | function addUrlRequired (done) { 86 | try { 87 | webHooks.add('hei', null).then(function () { 88 | done('Error expected') 89 | }).catch(function (err) { 90 | throw new Error(err) 91 | }) 92 | } catch (e) { 93 | expect(e.message).to.equal('Url must be a string') 94 | done() 95 | } 96 | } 97 | 98 | function removeShortnameRequired (done) { 99 | try { 100 | webHooks.remove(null, 'hei').then(function () { 101 | done('Error expected') 102 | }).catch(function (err) { 103 | throw new Error(err) 104 | }) 105 | } catch (e) { 106 | expect(e.message).to.equal('shortname required!') 107 | done() 108 | } 109 | } 110 | 111 | function getDBReturnsData (done) { 112 | webHooks.getDB().then(function (db) { 113 | should.exist(db) 114 | done() 115 | }).catch(function (e) { 116 | throw e 117 | }) 118 | } 119 | 120 | function addWebhook1 (done) { 121 | webHooks.add('hook1', URI + '/1/aaa').then(function () { 122 | done() 123 | }).catch(function (err) { 124 | throw new Error(err) 125 | }) 126 | } 127 | 128 | function addUrlToHook1 (done) { 129 | webHooks.add('hook1', URI + '/1/bbb').then(function () { 130 | done() 131 | }).catch(function (err) { 132 | throw new Error(err) 133 | }) 134 | } 135 | 136 | function getWebhook1 (done) { 137 | webHooks.getWebHook('hook1').then(function (obj) { 138 | should.exist(obj) 139 | expect(obj.length).to.equal(2) 140 | expect(obj).to.have.members([URI + '/1/aaa', URI + '/1/bbb']) 141 | done() 142 | }).catch(function (err) { 143 | throw new Error(err) 144 | }) 145 | } 146 | 147 | function deleteSingleUrl (done) { 148 | webHooks.remove('hook1', URI + '/1/bbb').then(function (removed) { 149 | expect(removed).to.equal(true) 150 | done() 151 | }).catch(function (err) { 152 | done(err) 153 | }) 154 | } 155 | 156 | function deleteMissingUrl (done) { 157 | webHooks.remove('hook1', URI + '/1/bbb').then(function (removed) { 158 | expect(removed).to.equal(false) 159 | done() 160 | }).catch(function (err) { 161 | done(err) 162 | }) 163 | } 164 | 165 | function deleteMissingHook (done) { 166 | webHooks.remove('not-existing').then(function (removed) { 167 | expect(removed).to.equal(false) 168 | done() 169 | }).catch(function (err) { 170 | done(err) 171 | }) 172 | } 173 | 174 | function deleteHook1 (done) { 175 | webHooks.remove('hook1').then(function (removed) { 176 | expect(removed).to.equal(true) 177 | done() 178 | }).catch(function (err) { 179 | done(err) 180 | }) 181 | } 182 | 183 | describe('Tests >', function () { 184 | before(function (done) { 185 | // Lets start our server 186 | server.listen(PORT, function () { 187 | // Callback triggered when server is successfully listening. Hurray! 188 | debug('Server listening on: http://localhost:%s', PORT) 189 | done() 190 | }) 191 | }) 192 | 193 | it('create a node-webHooks instance with in-memory database', function (done) { 194 | webHooks = new WebHooks({ 195 | db: DB_OBJECT 196 | }) 197 | should.exist(webHooks) 198 | webHooks.should.be.an('object') 199 | done() 200 | }) 201 | 202 | it('add: shortname required (in-memory DB)', addShortnameRequired) 203 | it('add: Url required (in-memory DB)', addUrlRequired) 204 | it('remove: shortname required (in-memory DB)', removeShortnameRequired) 205 | it('getDB() returns data (in-memory DB)', getDBReturnsData) 206 | it('add a webHook called hook1 (in-memory DB)', addWebhook1) 207 | it('add a new URL to the webHook hook1 (in-memory DB)', addUrlToHook1) 208 | it('should get the webHook using the .getWebHook method (in-memory DB)', getWebhook1) 209 | it('should delete a single webHook URL (in-memory DB)', deleteSingleUrl) 210 | it('should return false trying to delete a not existing webHook URL (in-memory DB)', deleteMissingUrl) 211 | it('should return false trying to delete a not existing webHook (in-memory DB)', deleteMissingHook) 212 | it('should delete an entire webHook (in-memory DB)', deleteHook1) 213 | 214 | it('delete old test DB file, if it exists', function (done) { 215 | try { 216 | fs.unlinkSync(DB_FILE) 217 | } catch (e) {} 218 | done() 219 | }) 220 | 221 | it('create a node-webHooks instance with on-disk database', function (done) { 222 | webHooks = new WebHooks({ 223 | db: DB_FILE 224 | }) 225 | should.exist(webHooks) 226 | webHooks.should.be.an('object') 227 | done() 228 | }) 229 | 230 | it('check wether the DB file exists or not', function (done) { 231 | fs.stat(DB_FILE, function (err) { 232 | should.not.exist(err) 233 | done() 234 | }) 235 | }) 236 | 237 | it('add: shortname required (on-disk DB)', addShortnameRequired) 238 | it('add: Url required (on-disk DB)', addUrlRequired) 239 | it('remove: shortname required (on-disk DB)', removeShortnameRequired) 240 | 241 | it('httpSuccessCodes is 200 by default', function (done) { 242 | expect(webHooks.httpSuccessCodes).to.deep.equal([200]) 243 | done() 244 | }) 245 | 246 | it('httpSuccessCodes accepts array only', function (done) { 247 | try { 248 | var a = new WebHooks({ 249 | db: DB_FILE, 250 | httpSuccessCodes: null 251 | }) 252 | done(a + ' should not be possible') 253 | } catch (e) { 254 | expect(e.message).to.equal('httpSuccessCodes must be an array') 255 | done() 256 | } 257 | }) 258 | 259 | it('httpSuccessCodes accepts not empty array only', function (done) { 260 | try { 261 | var b = new WebHooks({ 262 | db: DB_FILE, 263 | httpSuccessCodes: [] 264 | }) 265 | done(b + ' should not be possible') 266 | } catch (e) { 267 | expect(e.message).to.equal('httpSuccessCodes must contain at least one http status code') 268 | done() 269 | } 270 | }) 271 | 272 | it('getDB() returns data (on-disk DB)', getDBReturnsData) 273 | it('add a webHook called hook1 (on-disk DB)', addWebhook1) 274 | it('add a new URL to the webHook hook1 (on-disk DB)', addUrlToHook1) 275 | it('should get the webHook using the .getWebHook method (on-disk DB)', getWebhook1) 276 | 277 | it('should fire the webHook with no body or headers', function (done) { 278 | this.timeout(3000) 279 | webHooks.trigger('hook1') 280 | setTimeout(function () { 281 | debug('OUTCOME-1:', OUTCOMES) 282 | should.exist(OUTCOMES['/1/aaa']) 283 | should.exist(OUTCOMES['/1/bbb']) 284 | expect(OUTCOMES['/1/aaa']).to.have.property('headers') 285 | expect(OUTCOMES['/1/aaa']).to.have.property('body').equal('') 286 | expect(OUTCOMES['/1/bbb']).to.have.property('headers') 287 | expect(OUTCOMES['/1/bbb']).to.have.property('body').equal('') 288 | done() 289 | }, 1000) 290 | }) 291 | 292 | it('should fire the webHook with custom body', function (done) { 293 | this.timeout(3000) 294 | OUTCOMES = {} 295 | webHooks.trigger('hook1', { 296 | hello: 'world' 297 | }) 298 | setTimeout(function () { 299 | debug('OUTCOME-2:', OUTCOMES) 300 | should.exist(OUTCOMES['/1/aaa']) 301 | should.exist(OUTCOMES['/1/bbb']) 302 | expect(OUTCOMES['/1/aaa']).to.have.property('headers') 303 | expect(OUTCOMES['/1/aaa']).to.have.property('body').equal('{"hello":"world"}') 304 | expect(OUTCOMES['/1/bbb']).to.have.property('headers') 305 | expect(OUTCOMES['/1/bbb']).to.have.property('body').equal('{"hello":"world"}') 306 | done() 307 | }, 1000) 308 | }) 309 | 310 | it('should fire the webHook with custom headers', function (done) { 311 | this.timeout(3000) 312 | OUTCOMES = {} 313 | webHooks.trigger('hook1', {}, { 314 | hero: 'hulk' 315 | }) 316 | setTimeout(function () { 317 | debug('OUTCOME-3:', OUTCOMES) 318 | should.exist(OUTCOMES['/1/aaa']) 319 | should.exist(OUTCOMES['/1/bbb']) 320 | expect(OUTCOMES['/1/aaa']).to.have.nested.property('headers.hero').equal('hulk') 321 | expect(OUTCOMES['/1/aaa']).to.have.property('body').equal('{}') 322 | expect(OUTCOMES['/1/bbb']).to.have.nested.property('headers.hero').equal('hulk') 323 | expect(OUTCOMES['/1/bbb']).to.have.property('body').equal('{}') 324 | done() 325 | }, 1000) 326 | }) 327 | 328 | it('should fire the webHook with both custom body and headers', function (done) { 329 | this.timeout(3000) 330 | OUTCOMES = {} 331 | webHooks.trigger('hook1', { 332 | hello: 'rocco' 333 | }, { 334 | hero: 'iron-man' 335 | }) 336 | setTimeout(function () { 337 | debug('OUTCOME-3:', OUTCOMES) 338 | should.exist(OUTCOMES['/1/aaa']) 339 | should.exist(OUTCOMES['/1/bbb']) 340 | expect(OUTCOMES['/1/aaa']).to.have.nested.property('headers.hero').equal('iron-man') 341 | expect(OUTCOMES['/1/aaa']).to.have.property('body').equal('{"hello":"rocco"}') 342 | expect(OUTCOMES['/1/bbb']).to.have.nested.property('headers.hero').equal('iron-man') 343 | expect(OUTCOMES['/1/bbb']).to.have.property('body').equal('{"hello":"rocco"}') 344 | done() 345 | }, 1000) 346 | }) 347 | 348 | it('should delete a single webHook URL (on-disk DB)', deleteSingleUrl) 349 | it('should return false trying to delete a not existing webHook URL (on-disk DB)', deleteMissingUrl) 350 | it('should return false trying to delete a not existing webHook (on-disk DB)', deleteMissingHook) 351 | 352 | it('fire the webHook and make sure just one URL is called', function (done) { 353 | OUTCOMES = {} 354 | webHooks.trigger('hook1') 355 | setTimeout(function () { 356 | should.exist(OUTCOMES['/1/aaa']) 357 | should.not.exist(OUTCOMES['/1/bbb']) 358 | expect(OUTCOMES['/1/aaa']).to.have.property('headers') 359 | expect(OUTCOMES['/1/aaa']).to.have.property('body').equal('') 360 | done() 361 | }, 1000) 362 | }) 363 | 364 | it('should delete an entire webHook (on-disk DB)', deleteHook1) 365 | 366 | it('should fire the deleted webHook and make sure no request is dispatched at all', function (done) { 367 | OUTCOMES = {} 368 | webHooks.trigger('hook1') 369 | setTimeout(function () { 370 | expect(OUTCOMES).to.deep.equal({}) 371 | should.not.exist(OUTCOMES['/1/aaa']) 372 | should.not.exist(OUTCOMES['/1/bbb']) 373 | done() 374 | }, 1000) 375 | }) 376 | 377 | it('should create a new webHook called hook2 for loadtest', function (done) { 378 | webHooks.add('hook2', URI + '/2/aaa').then( 379 | webHooks.add('hook2', URI + '/2/bbb').then(function () { 380 | done() 381 | }) 382 | ).catch(function (err) { 383 | throw new Error(err) 384 | }) 385 | }) 386 | 387 | it('check webHooks were saved successfully using the .getWebHook method', function (done) { 388 | webHooks.getWebHook('hook2').then(function (obj) { 389 | debug('hook2:', obj) 390 | should.exist(obj) 391 | expect(obj.length).to.equal(2) 392 | expect(obj).to.have.members([URI + '/2/aaa', URI + '/2/bbb']) 393 | done() 394 | }).catch(function (err) { 395 | throw new Error(err) 396 | }) 397 | }) 398 | 399 | it('should fire the webHook 1000 times and 2000 REST calls are expected', function (done) { 400 | this.timeout(25 * 1000) 401 | // disabling debug to avoid console flooding 402 | // debug = function() {}; 403 | 404 | for (var i = 1; i <= 1000; i++) { 405 | (function (i) { 406 | webHooks.trigger('hook2', { 407 | i: i 408 | }) 409 | })(i) 410 | } 411 | 412 | var loop = setInterval(function () { 413 | console.log('Got', LOADTEST + '/2000', 'REST calls') 414 | if (LOADTEST === 2000) { 415 | clearInterval(loop) 416 | done() 417 | } 418 | }, 500) 419 | }) 420 | }) 421 | 422 | describe('Events >', function () { 423 | it('Should get the emitter', function (done) { 424 | emitter = webHooks.getEmitter() // get the emitter 425 | should.exist(emitter) 426 | done() 427 | }) 428 | 429 | it('Should get all the listeners func.', function (done) { 430 | should.exist(webHooks.getListeners()) // get the callbacks obj 431 | done() 432 | }) 433 | 434 | it('Should add a new Hook #3', function (done) { 435 | webHooks.add('hook3', URI + '/3/aaa').then(function () { 436 | done() 437 | }).catch(function (err) { 438 | throw new Error(err) 439 | }) 440 | }) 441 | 442 | it('Should catch a specific success event', function (done) { 443 | emitter.on('hook3.failure', function (shortname, stCode, body) { 444 | debug('hook3.failure:', shortname, stCode, body) 445 | done('hook3.failure error: wrong event catched.') 446 | }) 447 | emitter.on('hook3.success', function (shortname, statusCode, body) { 448 | debug('hook3.success:', {shortname: shortname, statusCode: statusCode, body: body}) 449 | should.exist(shortname) 450 | should.exist(statusCode) 451 | should.exist(body) 452 | shortname.should.equal('hook3') 453 | statusCode.should.equal(200) 454 | body.should.equal('Path Hit: /3/aaa') // body response from the server 455 | done() 456 | }) 457 | // fire the hook 458 | webHooks.trigger('hook3', { 459 | header1: 'pippo' 460 | }, { 461 | prop1: 'paperino' 462 | }) 463 | }) 464 | 465 | it('Should remove the specific event listener and fire the hook', function (done) { 466 | this.timeout(4000) 467 | emitter.removeAllListeners('hook3') 468 | emitter.on('hook3.success', function (s, st, body) { 469 | debug('hook3.success error:', s, st, body) 470 | done('error: removed listener should not be called!') 471 | }) 472 | emitter.removeAllListeners('hook3') 473 | webHooks.trigger('hook3') 474 | setTimeout(function () { 475 | done() 476 | }, 2000) 477 | }) 478 | 479 | it('add a failing webHook called hook4', function (done) { 480 | webHooks.add('hook4', URI + '/4/fail').then(function () { 481 | done() 482 | }).catch(function (err) { 483 | throw new Error(err) 484 | }) 485 | }) 486 | 487 | it('Should catch a specific failure event', function (done) { 488 | emitter.on('hook4.success', function () { 489 | done('error: wrong event catched!') 490 | }) 491 | emitter.on('hook4.failure', function (shortname, statusCode, body) { 492 | should.exist(shortname) 493 | should.exist(statusCode) 494 | should.exist(body) 495 | shortname.should.equal('hook4') 496 | statusCode.should.equal(400) 497 | body.should.equal('Path Hit: /4/fail') 498 | done() 499 | }) 500 | // fire the hook 501 | webHooks.trigger('hook4', { 502 | header1: 'foo' 503 | }, { 504 | prop2: 'peterpan' 505 | }) 506 | }) 507 | 508 | it('Should add new hooks for multiple events catch', function (done) { 509 | webHooks.add('hook5', URI + '/5/success').then(function () { 510 | webHooks.add('hook6', URI + '/6/success').then(function () { 511 | webHooks.add('hook7', URI + '/7/fail').then(function () { 512 | webHooks.add('hook8', URI + '/8/fail').then(function () { 513 | done() 514 | }) 515 | }) 516 | }) 517 | }).catch(function (err) { 518 | throw new Error(err) 519 | }) 520 | }) 521 | 522 | it('Should catch all the success events', function (done) { 523 | var got = 0 524 | emitter.on('*.failure', function (shortname, stCode, body) { 525 | debug('error *.failure:', shortname, stCode, body) 526 | done('*.failure error: wrong event catched.') 527 | }) 528 | emitter.on('*.success', function (shortname, statusCode, body) { 529 | debug('captured events:', got) 530 | should.exist(shortname) 531 | should.exist(statusCode) 532 | should.exist(body) 533 | expect(shortname).to.be.oneOf(['hook5', 'hook6']) 534 | statusCode.should.equal(200) 535 | expect(body).to.be.oneOf(['Path Hit: /5/success', 'Path Hit: /6/success']) 536 | ++got 537 | if (got === 2) { 538 | emitter.removeAllListeners('*.success') 539 | emitter.removeAllListeners('*.failure') 540 | done() 541 | } 542 | }) 543 | // fire the hooks 544 | webHooks.trigger('hook5') 545 | webHooks.trigger('hook6') 546 | }) 547 | 548 | it('Should catch all the failure events', function (done) { 549 | var got = 0 550 | emitter.on('*.success', function (shortname, stCode, body) { 551 | debug('error *.success:', shortname, stCode, body) 552 | done('*.success error: wrong event catched.') 553 | }) 554 | emitter.on('*.failure', function (shortname, statusCode, body) { 555 | debug('captured events:', got) 556 | should.exist(shortname) 557 | should.exist(statusCode) 558 | should.exist(body) 559 | expect(shortname).to.be.oneOf(['hook7', 'hook8']) 560 | statusCode.should.equal(400) 561 | expect(body).to.be.oneOf(['Path Hit: /7/fail', 'Path Hit: /8/fail']) 562 | ++got 563 | if (got === 2) { 564 | emitter.removeAllListeners('*.success') 565 | emitter.removeAllListeners('*.failure') 566 | done() 567 | } 568 | }) 569 | // fire the hooks 570 | webHooks.trigger('hook7') 571 | webHooks.trigger('hook8') 572 | }) 573 | 574 | after(function (done) { 575 | // stop the server 576 | server.close(function () { 577 | done() 578 | }) 579 | }) 580 | }) 581 | --------------------------------------------------------------------------------