├── .gitignore ├── Makefile ├── Readme.md ├── index.js ├── lib ├── interaptor.js └── interceptor.js ├── package.json ├── src ├── interaptor.js └── interceptor.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SRC = $(wildcard src/*.js) 2 | LIB = $(SRC:src/%.js=lib/%.js) 3 | 4 | lib: $(LIB) 5 | lib/%.js: src/%.js 6 | @mkdir -p $(@D) 7 | ./node_modules/.bin/babel -L all $< -o $@ 8 | 9 | include node_modules/make-lint-es6/index.mk 10 | 11 | test: lint 12 | @./node_modules/.bin/mocha 13 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # interaptor [![Circle CI](https://circleci.com/gh/vdemedes/interaptor.svg?style=svg)](https://circleci.com/gh/vdemedes/interaptor) 2 | 3 | Intercept HTTP requests for testing purposes. Uses [mitm](https://npmjs.com/package/mitm) under the hood. 4 | 5 | 6 | ### Installation 7 | 8 | ``` 9 | $ npm install interaptor --save 10 | ``` 11 | 12 | 13 | ### Usage 14 | 15 | ```javascript 16 | const intercept = require('interaptor'); 17 | const request = require('request'); 18 | 19 | intercept('api.digitalocean.com') 20 | .get('/v2/droplets') // intercept http://api.digitalocean.com/v2/droplets only 21 | .set('Content-Type', 'application/json') // set Content-Type response header 22 | .set(200) // set response status code 23 | .set('woohoo') // set response body (if object, will be JSON.stringify'ed) 24 | 25 | request('http://api.digitalocean.com/v2/droplets', function (err, res, body) { 26 | // request was not sent to api.digitalocean.com 27 | // request was intercepted by interaptor 28 | 29 | // res.headers['Content-Type'] === 'application/json' 30 | // res.statusCode === 200 31 | // body === 'woohoo' 32 | }); 33 | ``` 34 | 35 | 36 | ### Tests 37 | 38 | [![Circle CI](https://circleci.com/gh/vdemedes/interaptor.svg?style=svg)](https://circleci.com/gh/vdemedes/interaptor) 39 | 40 | ``` 41 | $ make test 42 | ``` 43 | 44 | 45 | ### License 46 | 47 | Interaptor is released under the MIT license. 48 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/interaptor'); 2 | -------------------------------------------------------------------------------- /lib/interaptor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }; 4 | 5 | /** 6 | * Dependencies 7 | */ 8 | 9 | var parse = require('url').parse; 10 | var mitm = require('mitm'); 11 | 12 | var Interceptor = require('./interceptor'); 13 | 14 | /** 15 | * Expose interceptor 16 | */ 17 | 18 | module.exports = function intercept(host) { 19 | // initialize Interaptor 20 | Interaptor.initialize(); 21 | 22 | // return created rule 23 | return Interaptor.create(host); 24 | }; 25 | 26 | /** 27 | * Interceptor 28 | */ 29 | 30 | var Interaptor = (function () { 31 | function Interaptor() { 32 | _classCallCheck(this, Interaptor); 33 | } 34 | 35 | /** 36 | * Initialize mitm 37 | * 38 | * @api private 39 | */ 40 | 41 | Interaptor.initialize = function initialize() { 42 | if (this.mitm) { 43 | return; 44 | }this.mitm = mitm(); 45 | this.mitm.on('connect', this.handleConnect.bind(this)); 46 | this.mitm.on('request', this.handleRequest.bind(this)); 47 | 48 | this.interceptors = []; 49 | }; 50 | 51 | /** 52 | * Find interceptor for request 53 | * 54 | * @param {http.ClientRequest} req 55 | * @api private 56 | */ 57 | 58 | Interaptor.find = function find(req) { 59 | // in node v0.10.x there is no 60 | // - req.method 61 | // - req.url or req.uri.path 62 | var method = (req.method || '').toLowerCase(); 63 | var host = req.host || req.headers.host; 64 | var path = undefined; 65 | 66 | if (req.url) { 67 | var url = parse(req.url); 68 | path = url.pathname; 69 | } else { 70 | path = (req.uri || {}).pathname; 71 | } 72 | 73 | // find matching interceptors 74 | var interceptors = this.interceptors.filter(function (interceptor) { 75 | // if method, host and path match 76 | // interceptor is ok for this request 77 | var methodMatches = interceptor.method && method ? interceptor.method === method : true; 78 | var hostMatches = interceptor.host && host ? interceptor.host === host : true; 79 | var pathMatches = interceptor.path && path ? interceptor.path.test(path) : true; 80 | 81 | return methodMatches && hostMatches && pathMatches; 82 | }); 83 | 84 | // return the first one 85 | return interceptors[0]; 86 | }; 87 | 88 | /** 89 | * Bypass TCP connection, if no interceptors are found 90 | * 91 | * @param {net.Socket} socket 92 | * @param {http.ClientRequest} req 93 | * @api private 94 | */ 95 | 96 | Interaptor.handleConnect = function handleConnect(socket, req) { 97 | var interceptor = this.find(req); 98 | 99 | if (!interceptor) { 100 | return socket.bypass(); 101 | } 102 | }; 103 | 104 | /** 105 | * Execute an interceptor for this request 106 | * 107 | * @param {http.ClientRequest} req 108 | * @param {http.ServerResponse} res 109 | * @api private 110 | */ 111 | 112 | Interaptor.handleRequest = function handleRequest(req, res) { 113 | var interceptor = this.find(req); 114 | 115 | interceptor._handleRequest(req, res); 116 | }; 117 | 118 | /** 119 | * Disable interaptor 120 | * 121 | * @api private 122 | */ 123 | 124 | Interaptor.disable = function disable() { 125 | this.mitm.disable(); 126 | this.mitm = null; 127 | 128 | this.interceptors.length = 0; 129 | }; 130 | 131 | /** 132 | * Create new interceptor for a given host 133 | * 134 | * @param {String} host 135 | * @return {Interceptor} 136 | * @api private 137 | */ 138 | 139 | Interaptor.create = function create(host) { 140 | var interceptor = new Interceptor(this, host); 141 | this.interceptors.push(interceptor); 142 | 143 | return interceptor; 144 | }; 145 | 146 | return Interaptor; 147 | })(); 148 | -------------------------------------------------------------------------------- /lib/interceptor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }; 4 | 5 | var _inherits = function (subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; }; 6 | 7 | /** 8 | * Dependencies 9 | */ 10 | 11 | var EventEmitter = require('events').EventEmitter; 12 | var pathToRegExp = require('path-to-regexp'); 13 | var fetchBody = require('raw-body'); 14 | var stringify = require('json-stringify-safe'); 15 | var methods = require('methods'); 16 | var assert = require('assert'); 17 | var format = require('util').format; 18 | var is = require('is_js'); 19 | 20 | /** 21 | * Interceptor 22 | */ 23 | 24 | var Interceptor = (function (_EventEmitter) { 25 | function Interceptor(interaptor, host) { 26 | _classCallCheck(this, Interceptor); 27 | 28 | _EventEmitter.call(this); 29 | 30 | // keep reference to Interaptor 31 | this._interaptor = interaptor; 32 | 33 | this.host = host; 34 | this._headers = {}; 35 | this._statusCode = 200; 36 | this._body = null; 37 | this._times = 1; // times to intercept, before .disable() 38 | 39 | // values to assert 40 | this._asserts = { 41 | headers: {}, 42 | body: null 43 | }; 44 | 45 | // custom handler functions 46 | this._respondFn = null; 47 | this._assertFn = null; 48 | } 49 | 50 | _inherits(Interceptor, _EventEmitter); 51 | 52 | /** 53 | * Set to intercept requests N times 54 | * 55 | * @example 56 | * times(2) 57 | * 58 | * @api public 59 | */ 60 | 61 | Interceptor.prototype.times = function times(n) { 62 | this._times = n; 63 | 64 | return this; 65 | }; 66 | 67 | /** 68 | * Set headers, status code or body on response 69 | * 70 | * @example 71 | * set('Content-Type', 'application/json') 72 | * set(200) 73 | * set('some response body') 74 | * set(function (req, res) { 75 | * 76 | * }) 77 | * 78 | * @api public 79 | */ 80 | 81 | Interceptor.prototype.set = function set(a, b) { 82 | // header 83 | if (arguments.length === 2) { 84 | var _name = a; 85 | var value = b; 86 | 87 | this._headers[_name] = value; 88 | 89 | return this; 90 | } 91 | 92 | // status code 93 | if (arguments.length === 1 && is.number(a)) { 94 | this._statusCode = a; 95 | 96 | return this; 97 | } 98 | 99 | // function 100 | if (arguments.length === 1 && is['function'](a)) { 101 | this._respondFn = a; 102 | 103 | return this; 104 | } 105 | 106 | // plain body 107 | if (arguments.length === 1 && is.string(a)) { 108 | this._body = a; 109 | 110 | return this; 111 | } 112 | 113 | // JSON body 114 | if (arguments.length === 1 && is.object(a)) { 115 | this._body = stringify(a); 116 | 117 | return this; 118 | } 119 | }; 120 | 121 | /** 122 | * Assert request header and body 123 | * 124 | * @example 125 | * expect('Content-Type', 'application') 126 | * expect('some request body') 127 | * expect(function (req) { 128 | * 129 | * }) 130 | * 131 | * @api public 132 | */ 133 | 134 | Interceptor.prototype.expect = function expect(a, b) { 135 | //header 136 | if (arguments.length === 2) { 137 | var _name2 = a; 138 | var value = b; 139 | 140 | this._asserts.headers[_name2] = value; 141 | 142 | return this; 143 | } 144 | 145 | // status code 146 | if (arguments.length === 1 && is.number(a)) { 147 | this._asserts.statusCode = a; 148 | 149 | return this; 150 | } 151 | 152 | // function 153 | if (arguments.length === 1 && is['function'](a)) { 154 | this._assertFn = a; 155 | 156 | return this; 157 | } 158 | 159 | // plain body 160 | if (arguments.length === 1 && is.string(a)) { 161 | this._asserts.body = a; 162 | 163 | return this; 164 | } 165 | 166 | // JSON body 167 | if (arguments.length === 1 && is.object(a)) { 168 | this._asserts.body = stringify(a); 169 | 170 | return this; 171 | } 172 | }; 173 | 174 | /** 175 | * Disable this rule 176 | * 177 | * @api public 178 | */ 179 | 180 | Interceptor.prototype.disable = function disable() { 181 | var _this = this; 182 | 183 | this._interaptor.interceptors = this._interaptor.interceptors.filter(function (interceptor) { 184 | return interceptor != _this; 185 | }); 186 | this._interaptor = null; 187 | this._headers = null; 188 | this._body = null; 189 | this._asserts = null; 190 | this._respondFn = null; 191 | this._assertFn = null; 192 | }; 193 | 194 | /** 195 | * Handle matching request 196 | * 197 | * @api private 198 | */ 199 | 200 | Interceptor.prototype._handleRequest = function _handleRequest(req, res) { 201 | var _this2 = this; 202 | 203 | // receive request body 204 | fetchBody(req, function (err, body) { 205 | if (err) return _this2._throw(err); 206 | 207 | req.body = body.toString(); 208 | 209 | // run assertions 210 | _this2._assertRequest(req); 211 | 212 | // respond to request 213 | // with defined values 214 | _this2._respondRequest(req, res); 215 | 216 | // disable itself 217 | if (--_this2._times <= 0) _this2.disable(); 218 | }); 219 | }; 220 | 221 | /** 222 | * Run assertions on request 223 | * 224 | * @api private 225 | */ 226 | 227 | Interceptor.prototype._assertRequest = function _assertRequest(req) { 228 | var _this3 = this; 229 | 230 | var asserts = this._asserts; 231 | 232 | // assert headers 233 | Object.keys(asserts.headers).forEach(function (name) { 234 | var expectedValue = asserts.headers[name]; 235 | var actualValue = req.headers[name.toLowerCase()]; 236 | 237 | var error = format('Got %s, expected %s in %s header', actualValue, expectedValue, name); 238 | _this3._assert(actualValue === expectedValue, error); 239 | }); 240 | 241 | // assert body 242 | if (asserts.body) { 243 | var error = format('Got %s, expected %s in request body', req.body, asserts.body); 244 | this._assert(asserts.body === req.body, error); 245 | } 246 | 247 | // custom assert function 248 | if (this._assertFn) { 249 | try { 250 | this._assertFn(req); 251 | } catch (err) { 252 | this._throw(err); 253 | } 254 | } 255 | }; 256 | 257 | /** 258 | * Respond to request with defined values 259 | * 260 | * @api private 261 | */ 262 | 263 | Interceptor.prototype._respondRequest = function _respondRequest(req, res) { 264 | var _this4 = this; 265 | 266 | // set headers 267 | Object.keys(this._headers).forEach(function (name) { 268 | var value = _this4._headers[name]; 269 | 270 | res.setHeader(name, value); 271 | }); 272 | 273 | res.statusCode = this._statusCode; 274 | 275 | if (this._respondFn) { 276 | this._respondFn(req, res); 277 | } 278 | 279 | res.end(this._body); 280 | }; 281 | 282 | /** 283 | * Utility to assert and throw error or emit event 284 | * if there are "error" listeners 285 | * 286 | * @api private 287 | */ 288 | 289 | Interceptor.prototype._assert = function _assert(value, message) { 290 | try { 291 | assert(value, message); 292 | } catch (err) { 293 | this._throw(err); 294 | } 295 | }; 296 | 297 | /** 298 | * Throw error or emit event 299 | * if there are "error" listeners 300 | * 301 | * @api private 302 | */ 303 | 304 | Interceptor.prototype._throw = function _throw(err) { 305 | var listeners = this.listeners('error').length; 306 | if (listeners === 0) throw err; 307 | 308 | this.emit('error', err); 309 | }; 310 | 311 | return Interceptor; 312 | })(EventEmitter); 313 | 314 | // iterate over all HTTP methods 315 | // and create a method on Interceptor 316 | // to set this method and path 317 | methods.forEach(function (method) { 318 | Interceptor.prototype[method] = function (path) { 319 | this.method = method; 320 | this.path = is.regexp(path) ? path : pathToRegExp(path); 321 | 322 | return this; 323 | }; 324 | }); 325 | 326 | /** 327 | * Expose interceptor 328 | */ 329 | 330 | module.exports = Interceptor; 331 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interaptor", 3 | "version": "1.3.1", 4 | "description": "Intercept HTTP requests for testing", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "make test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/vdemedes/interaptor" 12 | }, 13 | "keywords": [ 14 | "intercept", 15 | "mitm", 16 | "http", 17 | "request" 18 | ], 19 | "author": "Vadim Demedes ", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/vdemedes/interaptor/issues" 23 | }, 24 | "files": [ 25 | "index.js", 26 | "lib" 27 | ], 28 | "homepage": "https://github.com/vdemedes/interaptor", 29 | "dependencies": { 30 | "is_js": "^0.7.3", 31 | "json-stringify-safe": "^5.0.0", 32 | "methods": "^1.1.1", 33 | "mitm": "^1.0.3", 34 | "path-to-regexp": "^1.0.3", 35 | "raw-body": "^1.3.3" 36 | }, 37 | "devDependencies": { 38 | "babel": "^5.1.9", 39 | "chai": "^2.2.0", 40 | "make-lint-es6": "^1.0.3", 41 | "mocha": "^2.2.4", 42 | "request": "^2.55.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/interaptor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Dependencies 5 | */ 6 | 7 | const parse = require('url').parse; 8 | const mitm = require('mitm'); 9 | 10 | const Interceptor = require('./interceptor'); 11 | 12 | 13 | /** 14 | * Expose interceptor 15 | */ 16 | 17 | module.exports = function intercept (host) { 18 | // initialize Interaptor 19 | Interaptor.initialize(); 20 | 21 | // return created rule 22 | return Interaptor.create(host); 23 | }; 24 | 25 | 26 | /** 27 | * Interceptor 28 | */ 29 | 30 | class Interaptor { 31 | /** 32 | * Initialize mitm 33 | * 34 | * @api private 35 | */ 36 | 37 | static initialize () { 38 | if (this.mitm) return; 39 | 40 | this.mitm = mitm(); 41 | this.mitm.on('connect', this.handleConnect.bind(this)); 42 | this.mitm.on('request', this.handleRequest.bind(this)); 43 | 44 | this.interceptors = []; 45 | } 46 | 47 | 48 | /** 49 | * Find interceptor for request 50 | * 51 | * @param {http.ClientRequest} req 52 | * @api private 53 | */ 54 | 55 | static find (req) { 56 | // in node v0.10.x there is no 57 | // - req.method 58 | // - req.url or req.uri.path 59 | let method = (req.method || '').toLowerCase(); 60 | let host = req.host || req.headers.host; 61 | let path; 62 | 63 | if (req.url) { 64 | let url = parse(req.url); 65 | path = url.pathname; 66 | } else { 67 | path = (req.uri || {}).pathname; 68 | } 69 | 70 | // find matching interceptors 71 | let interceptors = this.interceptors.filter(interceptor => { 72 | // if method, host and path match 73 | // interceptor is ok for this request 74 | let methodMatches = interceptor.method && method ? (interceptor.method === method) : true; 75 | let hostMatches = interceptor.host && host ? (interceptor.host === host) : true; 76 | let pathMatches = interceptor.path && path ? (interceptor.path.test(path)) : true; 77 | 78 | return methodMatches && hostMatches && pathMatches; 79 | }); 80 | 81 | // return the first one 82 | return interceptors[0]; 83 | } 84 | 85 | 86 | /** 87 | * Bypass TCP connection, if no interceptors are found 88 | * 89 | * @param {net.Socket} socket 90 | * @param {http.ClientRequest} req 91 | * @api private 92 | */ 93 | 94 | static handleConnect (socket, req) { 95 | let interceptor = this.find(req); 96 | 97 | if (!interceptor) return socket.bypass(); 98 | } 99 | 100 | 101 | /** 102 | * Execute an interceptor for this request 103 | * 104 | * @param {http.ClientRequest} req 105 | * @param {http.ServerResponse} res 106 | * @api private 107 | */ 108 | 109 | static handleRequest (req, res) { 110 | let interceptor = this.find(req); 111 | 112 | interceptor._handleRequest(req, res); 113 | } 114 | 115 | 116 | /** 117 | * Disable interaptor 118 | * 119 | * @api private 120 | */ 121 | 122 | static disable () { 123 | this.mitm.disable(); 124 | this.mitm = null; 125 | 126 | this.interceptors.length = 0; 127 | } 128 | 129 | 130 | /** 131 | * Create new interceptor for a given host 132 | * 133 | * @param {String} host 134 | * @return {Interceptor} 135 | * @api private 136 | */ 137 | 138 | static create (host) { 139 | let interceptor = new Interceptor(this, host); 140 | this.interceptors.push(interceptor); 141 | 142 | return interceptor; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/interceptor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Dependencies 5 | */ 6 | 7 | const EventEmitter = require('events').EventEmitter; 8 | const pathToRegExp = require('path-to-regexp'); 9 | const fetchBody = require('raw-body'); 10 | const stringify = require('json-stringify-safe'); 11 | const methods = require('methods'); 12 | const assert = require('assert'); 13 | const format = require('util').format; 14 | const is = require('is_js'); 15 | 16 | 17 | /** 18 | * Interceptor 19 | */ 20 | 21 | class Interceptor extends EventEmitter { 22 | constructor (interaptor, host) { 23 | super(); 24 | 25 | // keep reference to Interaptor 26 | this._interaptor = interaptor; 27 | 28 | this.host = host; 29 | this._headers = {}; 30 | this._statusCode = 200; 31 | this._body = null; 32 | this._times = 1; // times to intercept, before .disable() 33 | 34 | // values to assert 35 | this._asserts = { 36 | headers: {}, 37 | body: null 38 | }; 39 | 40 | // custom handler functions 41 | this._respondFn = null; 42 | this._assertFn = null; 43 | } 44 | 45 | 46 | /** 47 | * Set to intercept requests N times 48 | * 49 | * @example 50 | * times(2) 51 | * 52 | * @api public 53 | */ 54 | 55 | times (n) { 56 | this._times = n; 57 | 58 | return this; 59 | } 60 | 61 | 62 | /** 63 | * Set headers, status code or body on response 64 | * 65 | * @example 66 | * set('Content-Type', 'application/json') 67 | * set(200) 68 | * set('some response body') 69 | * set(function (req, res) { 70 | * 71 | * }) 72 | * 73 | * @api public 74 | */ 75 | 76 | set (a, b) { 77 | // header 78 | if (arguments.length === 2) { 79 | let name = a; 80 | let value = b; 81 | 82 | this._headers[name] = value; 83 | 84 | return this; 85 | } 86 | 87 | // status code 88 | if (arguments.length === 1 && is.number(a)) { 89 | this._statusCode = a; 90 | 91 | return this; 92 | } 93 | 94 | // function 95 | if (arguments.length === 1 && is.function(a)) { 96 | this._respondFn = a; 97 | 98 | return this; 99 | } 100 | 101 | // plain body 102 | if (arguments.length === 1 && is.string(a)) { 103 | this._body = a; 104 | 105 | return this; 106 | } 107 | 108 | // JSON body 109 | if (arguments.length === 1 && is.object(a)) { 110 | this._body = stringify(a); 111 | 112 | return this; 113 | } 114 | } 115 | 116 | 117 | /** 118 | * Assert request header and body 119 | * 120 | * @example 121 | * expect('Content-Type', 'application') 122 | * expect('some request body') 123 | * expect(function (req) { 124 | * 125 | * }) 126 | * 127 | * @api public 128 | */ 129 | 130 | expect (a, b) { 131 | //header 132 | if (arguments.length === 2) { 133 | let name = a; 134 | let value = b; 135 | 136 | this._asserts.headers[name] = value; 137 | 138 | return this; 139 | } 140 | 141 | // status code 142 | if (arguments.length === 1 && is.number(a)) { 143 | this._asserts.statusCode = a; 144 | 145 | return this; 146 | } 147 | 148 | // function 149 | if (arguments.length === 1 && is.function(a)) { 150 | this._assertFn = a; 151 | 152 | return this; 153 | } 154 | 155 | // plain body 156 | if (arguments.length === 1 && is.string(a)) { 157 | this._asserts.body = a; 158 | 159 | return this; 160 | } 161 | 162 | // JSON body 163 | if (arguments.length === 1 && is.object(a)) { 164 | this._asserts.body = stringify(a); 165 | 166 | return this; 167 | } 168 | } 169 | 170 | 171 | /** 172 | * Disable this rule 173 | * 174 | * @api public 175 | */ 176 | 177 | disable () { 178 | this._interaptor.interceptors = this._interaptor.interceptors.filter(interceptor => interceptor != this); 179 | this._interaptor = null; 180 | this._headers = null; 181 | this._body = null; 182 | this._asserts = null; 183 | this._respondFn = null; 184 | this._assertFn = null; 185 | } 186 | 187 | 188 | /** 189 | * Handle matching request 190 | * 191 | * @api private 192 | */ 193 | 194 | _handleRequest (req, res) { 195 | // receive request body 196 | fetchBody(req, (err, body) => { 197 | if (err) return this._throw(err); 198 | 199 | req.body = body.toString(); 200 | 201 | // run assertions 202 | this._assertRequest(req); 203 | 204 | // respond to request 205 | // with defined values 206 | this._respondRequest(req, res); 207 | 208 | // disable itself 209 | if (--this._times <= 0) this.disable(); 210 | }); 211 | } 212 | 213 | 214 | /** 215 | * Run assertions on request 216 | * 217 | * @api private 218 | */ 219 | 220 | _assertRequest (req) { 221 | let asserts = this._asserts; 222 | 223 | // assert headers 224 | Object.keys(asserts.headers).forEach(name => { 225 | let expectedValue = asserts.headers[name]; 226 | let actualValue = req.headers[name.toLowerCase()]; 227 | 228 | let error = format('Got %s, expected %s in %s header', actualValue, expectedValue, name); 229 | this._assert(actualValue === expectedValue, error); 230 | }); 231 | 232 | // assert body 233 | if (asserts.body) { 234 | let error = format('Got %s, expected %s in request body', req.body, asserts.body); 235 | this._assert(asserts.body === req.body, error); 236 | } 237 | 238 | // custom assert function 239 | if (this._assertFn) { 240 | try { 241 | this._assertFn(req); 242 | } catch (err) { 243 | this._throw(err); 244 | } 245 | } 246 | } 247 | 248 | 249 | /** 250 | * Respond to request with defined values 251 | * 252 | * @api private 253 | */ 254 | 255 | _respondRequest (req, res) { 256 | // set headers 257 | Object.keys(this._headers).forEach(name => { 258 | let value = this._headers[name]; 259 | 260 | res.setHeader(name, value); 261 | }); 262 | 263 | res.statusCode = this._statusCode; 264 | 265 | if (this._respondFn) { 266 | this._respondFn(req, res); 267 | } 268 | 269 | res.end(this._body); 270 | } 271 | 272 | 273 | /** 274 | * Utility to assert and throw error or emit event 275 | * if there are "error" listeners 276 | * 277 | * @api private 278 | */ 279 | 280 | _assert (value, message) { 281 | try { 282 | assert(value, message); 283 | } catch (err) { 284 | this._throw(err); 285 | } 286 | } 287 | 288 | 289 | /** 290 | * Throw error or emit event 291 | * if there are "error" listeners 292 | * 293 | * @api private 294 | */ 295 | 296 | _throw (err) { 297 | let listeners = this.listeners('error').length; 298 | if (listeners === 0) throw err; 299 | 300 | this.emit('error', err); 301 | } 302 | } 303 | 304 | 305 | // iterate over all HTTP methods 306 | // and create a method on Interceptor 307 | // to set this method and path 308 | methods.forEach(method => { 309 | Interceptor.prototype[method] = function (path) { 310 | this.method = method; 311 | this.path = is.regexp(path) ? path : pathToRegExp(path); 312 | 313 | return this; 314 | }; 315 | }); 316 | 317 | 318 | /** 319 | * Expose interceptor 320 | */ 321 | 322 | module.exports = Interceptor; 323 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies 3 | */ 4 | 5 | const intercept = require('./'); 6 | const request = require('request'); 7 | const format = require('util').format; 8 | 9 | require('chai').should(); 10 | 11 | 12 | /** 13 | * Tests 14 | */ 15 | 16 | 17 | describe ('interaptor', function () { 18 | itEach (['get', 'post', 'put', 'delete'], 'intercept %s request', function (method, done) { 19 | intercept('example.org') 20 | [method]('/some/path') 21 | .set(200) 22 | .set('intercepted ' + method); 23 | 24 | request({ 25 | url: 'http://example.org/some/path', 26 | method: method 27 | }, function (err, res, body) { 28 | res.statusCode.should.equal(200); 29 | body.should.equal('intercepted ' + method); 30 | 31 | done(err); 32 | }); 33 | }); 34 | 35 | it ('set response code', function (done) { 36 | intercept('example.org') 37 | .get('/some/path') 38 | .set(401); 39 | 40 | request('http://example.org/some/path', function (err, res, body) { 41 | res.statusCode.should.equal(401); 42 | body.should.equal(''); 43 | 44 | done(); 45 | }); 46 | }); 47 | 48 | it ('set response headers', function (done) { 49 | intercept('example.org') 50 | .get('/some/path') 51 | .set('X-Test-Value', '123'); 52 | 53 | request('http://example.org/some/path', function (err, res, body) { 54 | res.statusCode.should.equal(200); 55 | res.headers['x-test-value'].should.equal('123'); 56 | body.should.equal(''); 57 | 58 | done(); 59 | }); 60 | }); 61 | 62 | it ('set response body', function (done) { 63 | intercept('example.org') 64 | .get('/some/path') 65 | .set('intercepted request'); 66 | 67 | request('http://example.org/some/path', function (err, res, body) { 68 | res.statusCode.should.equal(200); 69 | body.should.equal('intercepted request'); 70 | 71 | done(); 72 | }); 73 | }); 74 | 75 | it ('set response body in json', function (done) { 76 | intercept('example.org') 77 | .get('/some/path') 78 | .set({ key: 'value' }); 79 | 80 | request('http://example.org/some/path', function (err, res, body) { 81 | res.statusCode.should.equal(200); 82 | body.should.equal('{"key":"value"}'); 83 | 84 | done(); 85 | }); 86 | }); 87 | 88 | it ('set body in a custom handler function', function (done) { 89 | intercept('example.org') 90 | .get('/some/path') 91 | .set(function (req, res) { 92 | res.write('hello'); 93 | }); 94 | 95 | request('http://example.org/some/path', function (err, res, body) { 96 | res.statusCode.should.equal(200); 97 | body.should.equal('hello'); 98 | 99 | done(); 100 | }); 101 | }); 102 | 103 | it ('assert headers and continue', function (done) { 104 | intercept('example.org') 105 | .get('/some/path') 106 | .expect('X-Test-Flag', 'true'); 107 | 108 | request({ 109 | url: 'http://example.org/some/path', 110 | headers: { 111 | 'X-Test-Flag': 'true' 112 | } 113 | }, done); 114 | }); 115 | 116 | it ('assert headers and fail', function (done) { 117 | intercept('example.org') 118 | .get('/some/path') 119 | .expect('X-Test-Flag', 'false') 120 | .on('error', function (err) { 121 | err.message.should.equal('Got true, expected false in X-Test-Flag header'); 122 | done(); 123 | }); 124 | 125 | request({ 126 | url: 'http://example.org/some/path', 127 | headers: { 128 | 'X-Test-Flag': 'true' 129 | } 130 | }); 131 | }); 132 | 133 | it ('assert body and continue', function (done) { 134 | intercept('example.org') 135 | .post('/some/path') 136 | .expect('some cool body'); 137 | 138 | request({ 139 | url: 'http://example.org/some/path', 140 | method: 'post', 141 | body: 'some cool body' 142 | }, done); 143 | }); 144 | 145 | it ('assert json body and continue', function (done) { 146 | intercept('example.org') 147 | .post('/some/path') 148 | .expect({ key: 'value' }); 149 | 150 | request({ 151 | url: 'http://example.org/some/path', 152 | method: 'post', 153 | json: { key: 'value' } 154 | }, done); 155 | }); 156 | 157 | it ('assert body and fail', function (done) { 158 | intercept('example.org') 159 | .post('/some/path') 160 | .expect('some body') 161 | .on('error', function (err) { 162 | err.message.should.equal('Got some cool body, expected some body in request body'); 163 | done(); 164 | }); 165 | 166 | request({ 167 | url: 'http://example.org/some/path', 168 | method: 'post', 169 | body: 'some cool body' 170 | }); 171 | }); 172 | 173 | it ('assert using custom function and fail', function (done) { 174 | intercept('example.org') 175 | .post('/some/path') 176 | .expect(function (req) { 177 | if (req.body === 'some cool body') { 178 | throw new Error('Some error'); 179 | } 180 | }) 181 | .on('error', function (err) { 182 | err.message.should.equal('Some error'); 183 | done(); 184 | }); 185 | 186 | request({ 187 | url: 'http://example.org/some/path', 188 | method: 'post', 189 | body: 'some cool body' 190 | }); 191 | }); 192 | 193 | it ('assert using custom function and continue', function (done) { 194 | intercept('example.org') 195 | .post('/some/path') 196 | .expect(function (req) { 197 | if (req.body !== 'some cool body') { 198 | throw new Error('Some error'); 199 | } 200 | }); 201 | 202 | request({ 203 | url: 'http://example.org/some/path', 204 | method: 'post', 205 | body: 'some cool body' 206 | }, function () { 207 | done(); 208 | }); 209 | }); 210 | 211 | it ('intercept multiple times', function (done) { 212 | var times = 0; 213 | 214 | intercept('example.org') 215 | .post('/some/path') 216 | .times(2) 217 | .set(function (req, res) { 218 | times++; 219 | 220 | res.write(times.toString()); 221 | }); 222 | 223 | request({ 224 | url: 'http://example.org/some/path', 225 | method: 'post' 226 | }, function (err, res, body) { 227 | res.statusCode.should.equal(200); 228 | body.should.equal('1'); 229 | 230 | request({ 231 | url: 'http://example.org/some/path', 232 | method: 'post' 233 | }, function (err, res, body) { 234 | res.statusCode.should.equal(200); 235 | body.should.equal('2'); 236 | 237 | request({ 238 | url: 'http://example.org/some/path', 239 | method: 'post' 240 | }, function (err, res, body) { 241 | res.statusCode.should.equal(404); 242 | 243 | done(); 244 | }); 245 | }); 246 | }); 247 | }); 248 | 249 | it ('intercept using a regexp path', function (done) { 250 | intercept('example.org') 251 | .post(/something/) 252 | .set('OK'); 253 | 254 | request({ 255 | url: 'http://example.org/something', 256 | method: 'post' 257 | }, function (err, res, body) { 258 | res.statusCode.should.equal(200); 259 | body.should.equal('OK'); 260 | 261 | done(); 262 | }); 263 | }); 264 | 265 | it ('intercept using a route-like path', function (done) { 266 | intercept('example.org') 267 | .post('/items/:id/stuff') 268 | .set('OK'); 269 | 270 | request({ 271 | url: 'http://example.org/items/something/stuff?key=value', 272 | method: 'post' 273 | }, function (err, res, body) { 274 | res.statusCode.should.equal(200); 275 | body.should.equal('OK'); 276 | 277 | done(); 278 | }); 279 | }); 280 | }); 281 | 282 | 283 | /** 284 | * Utilities 285 | */ 286 | 287 | function itEach (arr, desc, test) { 288 | arr.forEach(function (item) { 289 | it (format(desc, item), test.bind(null, item)); 290 | }); 291 | } 292 | --------------------------------------------------------------------------------