├── test ├── data │ └── hello.txt ├── request-test.js ├── simple-test.js └── many-test.js ├── .gitignore ├── .travis.yml ├── package.json ├── LICENSE ├── README.md └── lib ├── request.js └── hock.js /test/data/hello.txt: -------------------------------------------------------------------------------- 1 | this 2 | is 3 | my 4 | sample 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules 3 | .DS_Store 4 | .*.sw[op] 5 | .idea 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - 8 5 | - 10 6 | - 12 7 | - 13 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hock", 3 | "description": "A mocking server for HTTP requests", 4 | "version": "1.4.1", 5 | "author": "Maciej Małecki ", 6 | "contributors": [ 7 | { 8 | "name": "Ken Perkins", 9 | "email": "ken.perkins@rackspace.com" 10 | } 11 | ], 12 | "license": "MIT", 13 | "repository": { 14 | "type": "git", 15 | "url": "http://github.com/mmalecki/hock.git" 16 | }, 17 | "keywords": [ 18 | "mock", 19 | "http", 20 | "test" 21 | ], 22 | "devDependencies": { 23 | "mocha": "^7.1.1", 24 | "request": "^2.88.2", 25 | "should": "^13.2.3", 26 | "should-http": "^0.1.1" 27 | }, 28 | "main": "./lib/hock", 29 | "scripts": { 30 | "test": "MOCK=on mocha --reporter spec -t 4000 test/*-test.js" 31 | }, 32 | "engines": { 33 | "node": ">=0.8.x" 34 | }, 35 | "dependencies": { 36 | "deep-equal": "^2.0.2", 37 | "url-equal": "0.1.2-1" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Maciej Małecki, Ken Perkins. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /test/request-test.js: -------------------------------------------------------------------------------- 1 | var Request = require('../lib/request'), 2 | should = require('should'); 3 | 4 | describe('Request unit tests', function () { 5 | describe("matching", function () { 6 | 7 | it('should work with defined headers in the incoming request', function () { 8 | var request = new Request(new Object(), { 9 | method: 'GET', 10 | url: '/lowercasetest', 11 | headers: { 'foo-type': 'artischocke' } 12 | }); 13 | 14 | request.isMatch({ 15 | method: 'GET', 16 | url: '/lowercasetest', 17 | headers: { 'foo-type': 'artischocke' } 18 | }).should.equal(true); 19 | }); 20 | 21 | it('should work with defined headers in the incoming request', function () { 22 | var request = new Request(new Object(), { 23 | method: 'GET', 24 | url: '/lowercasetest', 25 | headers: { 'foo-type': 'artischocke' } 26 | }); 27 | 28 | request.isMatch({ 29 | method: 'GET', 30 | url: '/lowercasetest', 31 | headers: {} 32 | }).should.equal(false); 33 | }); 34 | 35 | it('should accept any order of query strings', function () { 36 | var request = new Request(new Object(), { 37 | method: 'GET', 38 | url: '/?foo=bar&bar=foo' 39 | }); 40 | 41 | request.isMatch({ 42 | method: 'GET', 43 | url: '/?bar=foo&foo=bar' 44 | }).should.equal(true); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hock [![Build Status](https://secure.travis-ci.org/mmalecki/hock.png?branch=master)](http://travis-ci.org/mmalecki/hock) 2 | 3 | An HTTP mocking server with an API based on [Nock](https://github.com/nock/nock). 4 | 5 | ## Overview 6 | 7 | Hock is an HTTP mocking server with an API designed to closely match that of Nock. The key difference between Nock and Hock is that nock works by overriding `http.clientRequest`, allowing requests to be intercepted before they go over the wire. 8 | 9 | Hock is designed as a fully functioning HTTP service. You enqueue requests and responses in a similar fashion to Nock: 10 | ```Javascript 11 | 12 | const http = require('http') 13 | const hock = require('hock') 14 | const request = require('request') 15 | 16 | const mock = hock.createHock() 17 | mock 18 | .get('/some/url') 19 | .reply(200, 'Hello!') 20 | 21 | const server = http.createServer(mock.handler) 22 | server.listen(1337, () => { 23 | request('http://localhost:1337/some/url', (err, res, body) => { 24 | console.log(body); 25 | }) 26 | }) 27 | ``` 28 | 29 | ## HTTP Methods 30 | 31 | Hock supports the 5 primary HTTP methods at this time: 32 | 33 | * GET 34 | * POST 35 | * PUT 36 | * PATCH 37 | * DELETE 38 | * HEAD 39 | * COPY 40 | * OPTIONS 41 | 42 | ```Javascript 43 | // Returns a hock Request object 44 | const req = hockServer.get(url, requestHeaders) 45 | ``` 46 | ```Javascript 47 | // Returns a hock Request object 48 | const req = hockServer.delete(url, requestHeaders) 49 | ``` 50 | ```Javascript 51 | // Returns a hock Request object 52 | const req = hockServer.post(url, body, requestHeaders) 53 | ``` 54 | ```Javascript 55 | // Returns a hock Request object 56 | const req = hockServer.put(url, body, requestHeaders) 57 | ``` 58 | ```Javascript 59 | // Returns a hock Request object 60 | const req = hockServer.head(url, requestHeaders) 61 | ``` 62 | 63 | ## Request Object 64 | 65 | All of these methods return an instance of a `Request`, a hock object which contains all of the state for a mocked request. To define the response and enqueue into the `hockServer`, call either `reply` or `replyWithFile` on the `Request` object: 66 | 67 | ```Javascript 68 | // returns the current hockServer instance 69 | req.reply(statusCode, body, responseHeaders); 70 | ``` 71 | 72 | ```Javascript 73 | // returns the current hockServer instance 74 | req.replyWithFile(statusCode, filePath, responseHeaders); 75 | ``` 76 | 77 | You can optionally send a ReadableStream with reply, for example testing with large responses without having to use a file on disk: 78 | 79 | ```Javascript 80 | // returns the current hockServer instance 81 | req.reply(statusCode, new RandomStream(10000), responseHeaders); 82 | ``` 83 | 84 | You can also provide functions instead of concrete values. These functions will be called with the matching incoming http request, and it useful in cases where the response body or headers need to be constructed based on the incoming request data: 85 | 86 | ```Javascript 87 | // returns the current hockServer instance 88 | req.reply( 89 | statusCode, 90 | function replyWithBody(request) { 91 | return body; 92 | }, 93 | function replyWithHeaders(request) { 94 | return responseHeaders; 95 | } 96 | ); 97 | ``` 98 | 99 | ## Multiple matching requests 100 | 101 | You can optionally tell hock to match multiple requests for the same route: 102 | 103 | ```Javascript 104 | hockServer 105 | .put('/path/one', { 106 | foo: 1, 107 | bar: { 108 | baz: true 109 | biz: 'asdf1234' 110 | } 111 | }) 112 | .min(4) 113 | .max(10) 114 | .reply(202, { 115 | status: 'OK' 116 | }) 117 | ``` 118 | 119 | Call `many` if you need to handle at least one, possibly 120 | many requests: 121 | 122 | ```Javascript 123 | hockServer 124 | .put('/path/one', { 125 | foo: 1, 126 | bar: { 127 | baz: true 128 | biz: 'asdf1234' 129 | } 130 | }) 131 | .many() // min 1, max Unlimited 132 | .reply(202, { 133 | status: 'OK' 134 | }) 135 | ``` 136 | 137 | Provide custom min and max options to `many`: 138 | 139 | ```Javascript 140 | hockServer 141 | .put('/path/one', { 142 | foo: 1, 143 | bar: { 144 | baz: true 145 | biz: 'asdf1234' 146 | } 147 | }) 148 | .many({ 149 | min: 4, 150 | max: 10 151 | }) 152 | .reply(202, { 153 | status: 'OK' 154 | }) 155 | ``` 156 | 157 | Set infinite number of requests with `max(Infinity)`: 158 | 159 | ```Javascript 160 | hockServer 161 | .put('/path/one', { 162 | foo: 1, 163 | bar: { 164 | baz: true 165 | biz: 'asdf1234' 166 | } 167 | }) 168 | .max(Infinity) 169 | .reply(202, { 170 | status: 'OK' 171 | }) 172 | ``` 173 | 174 | If you don't care how many or how few requests are served, you can use `any`: 175 | 176 | ```Javascript 177 | hockServer 178 | .put('/path/one', { 179 | foo: 1, 180 | bar: { 181 | baz: true 182 | biz: 'asdf1234' 183 | } 184 | }) 185 | .any() // equivalent to min(0), max(Infinity) 186 | .reply(202, { 187 | status: 'OK' 188 | }) 189 | ``` 190 | ### hockServer.done() with many 191 | 192 | `hockServer.done()` will verify the number of requests fits within the 193 | minimum and maximum constraints specified by `min`, `max`, `many` or `any`: 194 | 195 | ```js 196 | hockServer.get('/').min(2) 197 | request.get('/', () => { 198 | hockServer.done((err) => { 199 | console.error(err) // error, only made one request 200 | }) 201 | }) 202 | ``` 203 | 204 | If the number of requests doesn't verify and you don't supply a callback 205 | to `hockServer.done()` it will throw! 206 | 207 | ## Chaining requests 208 | 209 | As the `reply` and `replyWithFile` methods return the current hockServer, you can chain them together: 210 | 211 | ```Javascript 212 | 213 | hockServer 214 | .put('/path/one', { 215 | foo: 1, 216 | bar: { 217 | baz: true 218 | biz: 'asdf1234' 219 | } 220 | }) 221 | .reply(202, { 222 | status: 'OK' 223 | }) 224 | .get('/my/file/should/be/here') 225 | .replyWithFile(200, __dirname + '/foo.jpg'); 226 | 227 | ``` 228 | ## Matching requests 229 | 230 | When a request comes in, hock iterates through the queue in a First-in-first-out approach, so long as the request matches. The criteria for matching is based on the method and the url, and additionally the request body if the request is a `PUT`, `PATCH`, or `POST`. If you specify request headers, they will also be matched against before sending the response. 231 | 232 | ## Path filtering 233 | 234 | You can filter paths using regex or a custom function, this is useful for things like timestamps that get appended to urls from clients. 235 | 236 | ```Javascript 237 | 238 | hockServer 239 | .filteringPathRegEx(/timestamp=[^&]*/g, 'timestamp=123') 240 | .get('/url?timestamp=123') 241 | .reply(200, 'Hi!'); 242 | ``` 243 | ```Javascript 244 | hockServer 245 | .filteringPath(function (p) { 246 | return '/url?timestamp=XXX'; 247 | }) 248 | .get('/url?timestamp=XXX') 249 | .reply(200, 'Hi!'); 250 | -------------------------------------------------------------------------------- /lib/request.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'), 4 | qs = require('querystring'), 5 | url_ = require('url'), 6 | Stream = require('stream'), 7 | urlEqual = require('url-equal'), 8 | deepEqual = require('deep-equal'); 9 | 10 | // From Nock 11 | function isStream(obj) { 12 | return obj && 13 | (typeof a !== 'string') && 14 | (! Buffer.isBuffer(obj)) && 15 | typeof obj.setEncoding === 'function'; 16 | } 17 | 18 | function bodyEqual(a, b) { 19 | if (typeof a === 'object' && typeof b === 'object' && a !== null && b !== null) { 20 | return deepEqual(a, b); 21 | } 22 | else { 23 | return a === b; 24 | } 25 | } 26 | 27 | function createResponse(request, response) { 28 | var self = this; 29 | 30 | var headers = typeof this.response.headers === 'function' ? this.response.headers(request) : this.response.headers || this._defaultReplyHeaders; 31 | this.response.body = typeof this.response.body === 'function' ? this.response.body(request) : this.response.body; 32 | 33 | response.writeHead(this.response.statusCode, headers); 34 | 35 | if (isStream(this.response.body)) { 36 | var readStream = this.response.body; 37 | 38 | if (this._maxRequests > 1) { 39 | // Because we need to respond with this body more than once, if it is a stream, 40 | // we make a buffer copy and use that as the body for future responses. 41 | var data = []; 42 | 43 | readStream.on('readable', function () { 44 | var chunk; 45 | while (null !== (chunk = readStream.read())) { 46 | data.push(chunk); 47 | response.write(chunk); 48 | } 49 | }); 50 | readStream.on('end', function(){ 51 | self.response.body = Buffer.concat(data); 52 | response.end(); 53 | }); 54 | } 55 | else { 56 | readStream.pipe(response); 57 | } 58 | } 59 | else if ((typeof this.response.body === 'object') && !Buffer.isBuffer(this.response.body)) { 60 | response.end(JSON.stringify(this.response.body)); 61 | } 62 | else { 63 | response.end(this.response.body); 64 | } 65 | 66 | return this.shouldPrune(); 67 | }; 68 | 69 | /** 70 | * Request class 71 | * 72 | * @description This is the Request class for Hock. It represents a single request, 73 | * and the response to that request 74 | * 75 | * @param {object} parent the hock server this request belongs to 76 | * @param {object} options 77 | * @param {String} options.url the route for the current request i.e. /foo/bar 78 | * @param {String|object} [options.body] optional request body 79 | * 80 | * @param {object} [options.headers] optional request headers 81 | * @param {object} [options.method] the method for the request (default=GET) 82 | * 83 | * @type {Function} 84 | */ 85 | var Request = module.exports = function (parent, options) { 86 | var self = this; 87 | 88 | this.method = options.method || 'GET'; 89 | this.url = options.url; 90 | this.body = options.body || ''; 91 | this.headers = options.headers || {}; 92 | 93 | Object.keys(this.headers).forEach(function (key) { 94 | self.headers[key.toLowerCase()] = self.headers[key]; 95 | if (key.toLowerCase() !== key) { 96 | delete self.headers[key]; 97 | } 98 | }); 99 | 100 | this._defaultReplyHeaders = {}; 101 | 102 | this._parent = parent; 103 | this._minRequests = 1; 104 | this._maxRequests = 1; 105 | this._count = 0; 106 | this._delay = 0; 107 | }; 108 | 109 | /** 110 | * Request.reply 111 | * 112 | * @description provide the mocked reply for the current request 113 | * 114 | * @param {Number} [statusCode] Status Code for the response (200) 115 | * @param {String|object} [body] The body for the response 116 | * @param {object} [headers] Headers for the response 117 | * @returns {*} 118 | */ 119 | Request.prototype.reply = function (statusCode, body, headers) { 120 | this.response = { 121 | statusCode: statusCode || 200, 122 | body: body || '', 123 | headers: headers 124 | }; 125 | 126 | this._parent.enqueue(this); 127 | 128 | return this._parent; 129 | }; 130 | 131 | /** 132 | * Request.replyWithFile 133 | * 134 | * @description provide the mocked reply for the current request based on an input file 135 | * 136 | * @param {Number} statusCode Status Code for the response (200) 137 | * @param {String} filePath The path of the file to respond with 138 | * @param {object} [headers] Headers for the response 139 | * @returns {*} 140 | */ 141 | Request.prototype.replyWithFile = function (statusCode, filePath, headers) { 142 | this.response = { 143 | statusCode: statusCode || 200, 144 | body: fs.createReadStream(filePath), 145 | headers: headers 146 | }; 147 | 148 | this._parent.enqueue(this); 149 | 150 | return this._parent; 151 | 152 | }; 153 | 154 | /** 155 | * Request.many 156 | * 157 | * @decsription allow a request to match multiple queries at the same url. 158 | * 159 | * @param {object} [options] (default={min: 1, max: infinity}) 160 | * @param {object} [options.min] minimum requests to be matched 161 | * @param {object} [options.max] max requests to be matched, must be >= min. 162 | * @returns {Request} 163 | */ 164 | Request.prototype.many = function(options) { 165 | options = options || { 166 | min: 1, 167 | max: Infinity 168 | }; 169 | 170 | if (typeof options.min === 'number') { 171 | this._minRequests = options.min; 172 | if (this._minRequests > this._maxRequests) { 173 | this._maxRequests = this._minRequests; 174 | } 175 | } 176 | 177 | if (typeof options.max === 'number') { 178 | this._maxRequests = options.max; 179 | } 180 | 181 | return this; 182 | }; 183 | 184 | /** 185 | * Request.min 186 | * 187 | * @description convenience function to provide a number for minimum requests 188 | * 189 | * @param {Number} number the value for min 190 | * @returns {Request} 191 | */ 192 | Request.prototype.min = function(number) { 193 | return this.many({ min: number }); 194 | }; 195 | 196 | /** 197 | * Request.max 198 | * 199 | * @description convenience function to provide a number for maximum requests 200 | * 201 | * @param {Number} number the value for max 202 | * @returns {Request} 203 | */ 204 | Request.prototype.max = function (number) { 205 | return this.many({ max: number }); 206 | }; 207 | 208 | /** 209 | * Request.once 210 | * 211 | * @description convenience function to set min, max to 1 212 | * 213 | * @returns {Request} 214 | */ 215 | Request.prototype.once = function() { 216 | return this.many({ min: 1, max: 1 }); 217 | }; 218 | 219 | /** 220 | * Request.twice 221 | * 222 | * @description convenience function to set min, max to 2 223 | * 224 | * @returns {Request} 225 | */ 226 | Request.prototype.twice = function() { 227 | return this.many({ min: 1, max: 2 }); 228 | } 229 | 230 | /** 231 | * Request.any 232 | * 233 | * @description convenience function to set min 0, max to Infinity 234 | * 235 | * @returns {Request} 236 | */ 237 | Request.prototype.any = function() { 238 | return this.many({ min: 0, max: Infinity }); 239 | } 240 | 241 | /** 242 | * Request.delay 243 | * 244 | * @description Delays the requests by number of ms 245 | * 246 | * @returns {boolean} 247 | */ 248 | Request.prototype.delay = function(ms) { 249 | this._delay = ms; 250 | return this; 251 | }; 252 | 253 | /** 254 | * Request.isMatch 255 | * 256 | * @description identify if the current request matches the provided request 257 | * 258 | * @param {object} request The request from the Hock server 259 | * 260 | * @returns {boolean|*} 261 | */ 262 | Request.prototype.isMatch = function(request) { 263 | var self = this; 264 | 265 | if (this._parent._pathFilter) { 266 | request.url = this._parent._pathFilter(request.url); 267 | } 268 | 269 | if (request.method === 'GET' || request.method === 'DELETE') { 270 | return this.method === request.method && urlEqual(this.url, request.url) && 271 | checkHeaders(); 272 | } 273 | else { 274 | let body = request.body; 275 | if (this._requestFilter) { 276 | body = this._requestFilter(request.body); 277 | } 278 | 279 | if (typeof this.body === 'object') { 280 | try { 281 | body = JSON.parse(body) 282 | } 283 | catch (err) { 284 | } 285 | } 286 | 287 | return this.method === request.method && urlEqual(this.url, request.url) && 288 | bodyEqual(this.body, body) && checkHeaders(); 289 | 290 | } 291 | 292 | function checkHeaders() { 293 | var match = true; 294 | Object.keys(self.headers).forEach(function (key) { 295 | if (self.headers[key] && self.headers[key] !== request.headers[key]) { 296 | match = false; 297 | } 298 | }); 299 | 300 | return match; 301 | } 302 | }; 303 | 304 | /** 305 | * Request.sendResponse 306 | * 307 | * @description send the response to the provided Hock response. Applies a delay if it was set 308 | * 309 | * @param {object} request The request object from the hock server 310 | * @param {object} response The response object from the hock server 311 | */ 312 | Request.prototype.sendResponse = function(request, response) { 313 | var self = this; 314 | this._count++; 315 | 316 | if (this._delay > 0) { 317 | setTimeout(function() { 318 | createResponse.call(self, request, response); 319 | }, this._delay); 320 | } 321 | else { 322 | createResponse.call(this, request, response); 323 | } 324 | 325 | return this.shouldPrune(); 326 | } 327 | 328 | /** 329 | * Request.isDone 330 | * 331 | * @description Identify if the current request has met its min and max requirements 332 | * 333 | * @returns {boolean} 334 | */ 335 | Request.prototype.isDone = function() { 336 | return !(this._count >= this._minRequests && this._count <= this._maxRequests); 337 | }; 338 | 339 | /** 340 | * Request.shouldPrune 341 | * 342 | * @description Identify if the request has met its max requirement 343 | * 344 | * @returns {boolean} 345 | */ 346 | Request.prototype.shouldPrune = function() { 347 | return this._count >= this._maxRequests; 348 | }; 349 | 350 | /** 351 | * Request.toJSON 352 | * 353 | * @returns {{method: *, url: *, body: *, headers: *, stats: {count: number, min: (Function|*|Object), max: *, isValid: *, shouldPrune: *}}} 354 | */ 355 | Request.prototype.toJSON = function() { 356 | return { 357 | method: this.method, 358 | url: this.url, 359 | body: this.body, 360 | headers: this.headers, 361 | stats: { 362 | count: this._count, 363 | min: this._minRequests, 364 | max: this._maxRequests, 365 | isDone: this.isDone(), 366 | shouldPrune: this.shouldPrune() 367 | } 368 | }; 369 | }; 370 | -------------------------------------------------------------------------------- /test/simple-test.js: -------------------------------------------------------------------------------- 1 | var http = require('http'), 2 | url = require('url'), 3 | should = require('should'), 4 | shouldHttp = require('should-http'), 5 | request = require('request'), 6 | hock = require('../'); 7 | 8 | var PORT = 5678; 9 | 10 | describe('Hock HTTP Tests', function() { 11 | 12 | var hockInstance; 13 | var httpServer; 14 | 15 | describe("with available ports", function() { 16 | before(function(done) { 17 | hockInstance = hock.createHock(); 18 | httpServer = http.createServer(hockInstance.handler).listen(PORT, function(err) { 19 | should.not.exist(err); 20 | should.exist(hockInstance); 21 | 22 | done(); 23 | }); 24 | }); 25 | 26 | it('should correctly respond to an HTTP GET request', function(done) { 27 | hockInstance 28 | .get('/url') 29 | .reply(200, { 'hock': 'ok' }); 30 | 31 | request('http://localhost:' + PORT + '/url', function(err, res, body) { 32 | should.not.exist(err); 33 | should.exist(res); 34 | res.statusCode.should.equal(200); 35 | JSON.parse(body).should.eql({ 'hock': 'ok' }); 36 | done(); 37 | 38 | }); 39 | }); 40 | 41 | it('should correctly respond to an HTTP POST request', function (done) { 42 | hockInstance 43 | .post('/post', { 'hock': 'post' }) 44 | .reply(201, { 'hock': 'created' }); 45 | 46 | request({ 47 | uri: 'http://localhost:' + PORT + '/post', 48 | method: 'POST', 49 | json: { 50 | 'hock': 'post' 51 | } 52 | }, function (err, res, body) { 53 | should.not.exist(err); 54 | should.exist(res); 55 | res.statusCode.should.equal(201); 56 | body.should.eql({ 'hock': 'created' }); 57 | done(); 58 | }); 59 | }); 60 | 61 | it('should correctly respond to an HTTP PUT request', function (done) { 62 | hockInstance 63 | .put('/put', { 'hock': 'put' }) 64 | .reply(204, { 'hock': 'updated' }); 65 | 66 | request({ 67 | uri: 'http://localhost:' + PORT + '/put', 68 | method: 'PUT', 69 | json: { 70 | 'hock': 'put' 71 | } 72 | }, function (err, res, body) { 73 | should.not.exist(err); 74 | should.exist(res); 75 | res.statusCode.should.equal(204); 76 | should.not.exist(body); 77 | done(); 78 | }); 79 | }); 80 | 81 | it('should correctly respond to an HTTP PATCH request', function (done) { 82 | hockInstance 83 | .patch('/patch', { 'hock': 'patch' }) 84 | .reply(204, { 'hock': 'updated' }); 85 | 86 | request({ 87 | uri: 'http://localhost:' + PORT + '/patch', 88 | method: 'PATCH', 89 | json: { 90 | 'hock': 'patch' 91 | } 92 | }, function (err, res, body) { 93 | should.not.exist(err); 94 | should.exist(res); 95 | res.statusCode.should.equal(204); 96 | should.not.exist(body); 97 | done(); 98 | }); 99 | }); 100 | 101 | it('should correctly respond to an HTTP DELETE request', function (done) { 102 | hockInstance 103 | .delete('/delete') 104 | .reply(202, { 'hock': 'deleted' }); 105 | 106 | request({ 107 | uri: 'http://localhost:' + PORT + '/delete', 108 | method: 'DELETE' 109 | }, function (err, res, body) { 110 | should.not.exist(err); 111 | should.exist(res); 112 | res.statusCode.should.equal(202); 113 | should.exist(body); 114 | JSON.parse(body).should.eql({ 'hock': 'deleted' }); 115 | done(); 116 | }); 117 | }); 118 | 119 | it('should correctly respond to an HTTP HEAD request', function (done) { 120 | hockInstance 121 | .head('/head') 122 | .reply(200, '', { 'Content-Type': 'plain/text' }); 123 | 124 | request({ 125 | uri: 'http://localhost:' + PORT + '/head', 126 | method: 'HEAD' 127 | }, function (err, res, body) { 128 | should.not.exist(err); 129 | should.exist(res); 130 | res.statusCode.should.equal(200); 131 | should.exist(body); 132 | body.should.equal(''); 133 | res.should.have.header('content-type', 'plain/text'); 134 | done(); 135 | }); 136 | }); 137 | 138 | it('should correctly respond to an HTTP COPY request', function(done) { 139 | hockInstance 140 | .copy('/copysrc') 141 | .reply(204); 142 | 143 | request({ 144 | uri: 'http://localhost:' + PORT + '/copysrc', 145 | method: 'COPY' 146 | }, function(err, res, body) { 147 | should.not.exist(err); 148 | should.exist(res); 149 | res.statusCode.should.equal(204); 150 | body.should.equal(''); 151 | done(); 152 | }); 153 | }); 154 | 155 | it('unmatched requests should throw', function () { 156 | hockInstance 157 | .head('/head') 158 | .reply(200, '', { 'Content-Type': 'plain/text' }); 159 | 160 | (function() { 161 | hockInstance.done(); 162 | }).should.throw(); 163 | }); 164 | 165 | it('unmatched requests should call done callback with err', function (done) { 166 | hockInstance 167 | .head('/head') 168 | .reply(200, '', { 'Content-Type': 'plain/text' }) 169 | .done(function(err) { 170 | should.exist(err); 171 | done(); 172 | }); 173 | }); 174 | 175 | it('should work with a delay configured', function(done) { 176 | const DELAY = 500 177 | 178 | hockInstance 179 | .get('/url') 180 | .delay(DELAY) 181 | .reply(200, { 'hock': 'ok' }); 182 | 183 | const start = Date.now() 184 | request('http://localhost:' + PORT + '/url', function(err, res, body) { 185 | should.not.exist(err); 186 | should.exist(res); 187 | res.statusCode.should.equal(200); 188 | JSON.parse(body).should.eql({ 'hock': 'ok' }); 189 | (Date.now() - start).should.be.aboveOrEqual(DELAY); 190 | done(); 191 | 192 | }); 193 | }); 194 | 195 | it('should work with response body function', function(done) { 196 | hockInstance 197 | .get('/url?key=value') 198 | .reply(200, function(request) { 199 | const query = url.parse(request.url, true).query; 200 | return { 'hock': 'ok', key: query.key }; 201 | }); 202 | 203 | request('http://localhost:' + PORT + '/url?key=value', function(err, res, body) { 204 | should.not.exist(err); 205 | should.exist(res); 206 | res.statusCode.should.equal(200); 207 | JSON.parse(body).should.eql({ 'hock': 'ok', key: 'value' }); 208 | 209 | done(); 210 | }); 211 | }); 212 | 213 | it('should work with response header function', function(done) { 214 | hockInstance 215 | .get('/url?key=value') 216 | .reply(200, { 'hock': 'ok' }, function(request) { 217 | const query = url.parse(request.url, true).query; 218 | return { 219 | 'x-request-key': query.key, 220 | }; 221 | }); 222 | 223 | request('http://localhost:' + PORT + '/url?key=value', function(err, res, body) { 224 | should.not.exist(err); 225 | should.exist(res); 226 | res.statusCode.should.equal(200); 227 | 228 | res.headers['x-request-key'].should.eql('value'); 229 | 230 | done(); 231 | }); 232 | }); 233 | 234 | after(function (done) { 235 | httpServer.close(done); 236 | }); 237 | }); 238 | 239 | describe("dynamic path replacing / filtering", function() { 240 | before(function(done) { 241 | hockInstance = hock.createHock(); 242 | httpServer = http.createServer(hockInstance.handler).listen(PORT, function(err) { 243 | should.not.exist(err); 244 | should.exist(hockInstance); 245 | 246 | done(); 247 | }); 248 | }); 249 | 250 | it('should correctly use regex', function(done) { 251 | hockInstance 252 | .filteringPathRegEx(/password=[^&]*/g, 'password=XXX') 253 | .get('/url?password=XXX') 254 | .reply(200, { 'hock': 'ok' }); 255 | 256 | request('http://localhost:' + PORT + '/url?password=artischocko', function(err, res, body) { 257 | should.not.exist(err); 258 | should.exist(res); 259 | res.statusCode.should.equal(200); 260 | JSON.parse(body).should.eql({ 'hock': 'ok' }); 261 | done(); 262 | 263 | }); 264 | }); 265 | 266 | it('should correctly use functions', function(done) { 267 | hockInstance 268 | .filteringPath(function (p) { 269 | p.should.equal('/url?password=artischocko'); 270 | return '/url?password=XXX'; 271 | }) 272 | .get('/url?password=XXX') 273 | .reply(200, { 'hock': 'ok' }); 274 | 275 | request('http://localhost:' + PORT + '/url?password=artischocko', function(err, res, body) { 276 | should.not.exist(err); 277 | should.exist(res); 278 | res.statusCode.should.equal(200); 279 | JSON.parse(body).should.eql({ 'hock': 'ok' }); 280 | done(); 281 | 282 | }); 283 | }); 284 | 285 | after(function(done) { 286 | httpServer.close(done); 287 | }); 288 | }); 289 | 290 | describe("test if route exists", function() { 291 | before(function(done) { 292 | hockInstance = hock.createHock(); 293 | httpServer = http.createServer(hockInstance.handler).listen(PORT, function (err) { 294 | should.not.exist(err); 295 | should.exist(hockInstance); 296 | 297 | done(); 298 | }); 299 | }); 300 | 301 | it('should allow testing for url', function(done) { 302 | hockInstance 303 | .get('/url?password=foo') 304 | .reply(200, { 'hock': 'ok' }) 305 | .get('/arti') 306 | .reply(200, { 'hock': 'ok' }); 307 | 308 | hockInstance.hasRoute('GET', '/url?password=foo').should.equal(true); 309 | hockInstance.hasRoute('GET', '/arti').should.equal(true); 310 | hockInstance.hasRoute('GET', '/notexist').should.equal(false); 311 | done(); 312 | }); 313 | 314 | it('matches the header', function(done) { 315 | hockInstance 316 | .get('/url?password=foo') 317 | .reply(200, { 'hock': 'ok' }) 318 | .get('/artischocko', { 'foo-type': 'artischocke' }) 319 | .reply(200, { 'hock': 'ok' }); 320 | 321 | hockInstance 322 | .hasRoute('GET', '/bla?password=foo', null, { 'content-type': 'plain/text' }) 323 | .should.equal(false); 324 | hockInstance 325 | .hasRoute('GET', '/artischocko', null, { 'foo-type': 'artischocke' }) 326 | .should.equal(true); 327 | 328 | done(); 329 | }); 330 | 331 | it('matches the body', function(done) { 332 | hockInstance 333 | .get('/url?password=foo') 334 | .reply(200, { 'hock': 'ok' }) 335 | .post('/artischocko', 'enteente') 336 | .reply(200, { 'hock': 'ok' }); 337 | 338 | hockInstance.hasRoute('GET', '/bla?password=foo', 'testing').should.equal(false); 339 | hockInstance.hasRoute('POST', '/artischocko', 'enteente').should.equal(true); 340 | 341 | done(); 342 | }); 343 | 344 | it('matches different order of querystring parameters', function(done) { 345 | hockInstance 346 | .get('/url?user=foo&pass=bar') 347 | .reply(200, { 'hock': 'ok' }); 348 | 349 | hockInstance.hasRoute('GET', '/url?pass=bar&user=foo').should.equal(true); 350 | done(); 351 | }); 352 | 353 | after(function(done) { 354 | httpServer.close(done); 355 | }); 356 | }); 357 | }); 358 | -------------------------------------------------------------------------------- /lib/hock.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var qs = require('querystring'), 4 | url_ = require('url'), 5 | http = require('http'), 6 | util = require('util'), 7 | events = require('events').EventEmitter, 8 | Request = require('./request'), 9 | deepEqual = require('deep-equal'), 10 | urlEqual = require('url-equal'); 11 | 12 | /** 13 | * Hock class 14 | * 15 | * @description This is the main class for Hock. It handles creation 16 | * of the underlying webserver, and enqueing all of the requests. 17 | * 18 | * @param {object} [options] options for your Hock server 19 | * @param {boolean} [options.throwOnUnmatched] Tell Hock to throw if 20 | * receiving a request without a match (Default=true) 21 | * 22 | * @type {Function} 23 | */ 24 | var Hock = module.exports = function (options) { 25 | options = options || {}; 26 | this._throwOnUnmatched = (typeof options.throwOnUnmatched === 'boolean' ? options.throwOnUnmatched : true); 27 | this._assertions = []; 28 | this.handler = Hock.prototype.handler.bind(this); 29 | events.EventEmitter.call(this); 30 | }; 31 | util.inherits(Hock, events.EventEmitter); 32 | 33 | /** 34 | * Hock.enqueue 35 | * 36 | * @description enqueue a request into the queue 37 | * 38 | * @param {object} request The request to enter in the hock queue 39 | * @param request 40 | */ 41 | Hock.prototype.enqueue = function (request) { 42 | if (this._requestFilter) { 43 | request._requestFilter = this._requestFilter; 44 | } 45 | 46 | if (this._defaultReplyHeaders) { 47 | request._defaultReplyHeaders = this._defaultReplyHeaders; 48 | } 49 | 50 | this._assertions.push(request); 51 | }; 52 | 53 | /** 54 | * Hock.hasRoute 55 | * 56 | * @description test if there is a request on the assertions queue 57 | * 58 | * @param {String} method the method of the request to match 59 | * @param {String} url the route of the request to match 60 | * @param {String} [body] optionally - use if you set a body 61 | * @param {object} [headers] optionally - use if you set a header 62 | * @returns {Boolean} 63 | */ 64 | Hock.prototype.hasRoute = function (method, url, body, headers) { 65 | if (!body) { 66 | body = ''; 67 | } 68 | 69 | if (!headers) { 70 | headers = {}; 71 | } 72 | 73 | return this._assertions.some(function(request) { 74 | return request.isMatch({ 75 | url, 76 | method, 77 | body, 78 | headers, 79 | }) 80 | }) 81 | }; 82 | 83 | /** 84 | * Hock.done 85 | * 86 | * @description Throw an error if there are unprocessed requests in the assertions queue. 87 | * If there are unfinsihed requests, i.e. min: 2, max 4 with a count of 2, that request will be 88 | * ignored for the purposes of throwing an error. 89 | * 90 | */ 91 | Hock.prototype.done = function (cb) { 92 | var err; 93 | 94 | if (this._assertions.length) { 95 | this._assertions = this._assertions.filter(function(request) { 96 | return request.isDone(); 97 | }); 98 | 99 | if (this._assertions.length) { 100 | err = new Error('Unprocessed Requests in Assertions Queue: \n' + JSON.stringify(this._assertions.map(function (item) { 101 | return item.method + ' ' + item.url; 102 | }))); 103 | } 104 | } 105 | 106 | if (!err) { 107 | return cb && cb(); 108 | } 109 | 110 | if (!cb) { 111 | throw err; 112 | } 113 | 114 | return cb(err); 115 | 116 | }; 117 | 118 | /** 119 | * Hock.get 120 | * 121 | * @description enqueue a GET request into the assertion queue 122 | * 123 | * @param {String} url the route of the request to match 124 | * @param {object} [headers] optionally match the request headers 125 | * @returns {Request} 126 | */ 127 | Hock.prototype.get = function (url, headers) { 128 | return new Request(this, { 129 | method: 'GET', 130 | url: url, 131 | headers: headers || {} 132 | }); 133 | }; 134 | 135 | /** 136 | * Hock.head 137 | * 138 | * @description enqueue a HEAD request into the assertion queue 139 | * 140 | * @param {String} url the route of the request to match 141 | * @param {object} [headers] optionally match the request headers 142 | * @returns {Request} 143 | */ 144 | Hock.prototype.head = function (url, headers) { 145 | return new Request(this, { 146 | method: 'HEAD', 147 | url: url, 148 | headers: headers || {} 149 | }); 150 | }; 151 | 152 | /** 153 | * Hock.put 154 | * 155 | * @description enqueue a PUT request into the assertion queue 156 | * 157 | * @param {String} url the route of the request to match 158 | * @param {object|String} [body] the request body (if any) of the request to match 159 | * @param {object} [headers] optionally match the request headers 160 | * @returns {Request} 161 | */ 162 | Hock.prototype.put = function (url, body, headers) { 163 | return new Request(this, { 164 | method: 'PUT', 165 | url: url, 166 | body: body || '', 167 | headers: headers || {} 168 | }); 169 | }; 170 | 171 | /** 172 | * Hock.patch 173 | * 174 | * @description enqueue a PATCH request into the assertion queue 175 | * 176 | * @param {String} url the route of the request to match 177 | * @param {object|String} [body] the request body (if any) of the request to match 178 | * @param {object} [headers] optionally match the request headers 179 | * @returns {Request} 180 | */ 181 | Hock.prototype.patch = function (url, body, headers) { 182 | return new Request(this, { 183 | method: 'PATCH', 184 | url: url, 185 | body: body || '', 186 | headers: headers || {} 187 | }); 188 | }; 189 | 190 | /** 191 | * Hock.post 192 | * 193 | * @description enqueue a POST request into the assertion queue 194 | * 195 | * @param {String} url the route of the request to match 196 | * @param {object|String} [body] the request body (if any) of the request to match 197 | * @param {object} [headers] optionally match the request headers 198 | * @returns {Request} 199 | */ 200 | Hock.prototype.post = function (url, body, headers) { 201 | return new Request(this, { 202 | method: 'POST', 203 | url: url, 204 | body: body || '', 205 | headers: headers || {} 206 | }); 207 | }; 208 | 209 | /** 210 | * Hock.delete 211 | * 212 | * @description enqueue a DELETE request into the assertion queue 213 | * 214 | * @param {String} url the route of the request to match 215 | * @param {object|String} [body] the request body (if any) of the request to match 216 | * @param {object} [headers] optionally match the request headers 217 | * @returns {Request} 218 | */ 219 | Hock.prototype.delete = function (url, body, headers) { 220 | return new Request(this, { 221 | method: 'DELETE', 222 | url: url, 223 | body: body || '', 224 | headers: headers || {} 225 | }); 226 | }; 227 | 228 | /** 229 | * Hock.copy 230 | * 231 | * @description enqueue a COPY request into the assertion queue 232 | * 233 | * @param {String} url the route of the request to match 234 | * @param {object|String} [body] the request body (if any) of the request to match 235 | * @param {object} [headers] optionally match the request headers 236 | * @returns {Request} 237 | */ 238 | Hock.prototype.copy = function (url, body, headers) { 239 | return new Request(this, { 240 | method: 'COPY', 241 | url: url, 242 | headers: headers || {} 243 | }); 244 | }; 245 | 246 | /** 247 | * Hock.options 248 | * 249 | * @description enqueue a OPTIONS request into the assertion queue 250 | * 251 | * @param {String} url the route of the request to match 252 | * @param {object} [headers] optionally match the request headers 253 | * @returns {Request} 254 | */ 255 | Hock.prototype.options = function (url, headers) { 256 | return new Request(this, { 257 | method: 'OPTIONS', 258 | url: url, 259 | headers: headers || {} 260 | }); 261 | }; 262 | 263 | /** 264 | * Hock.filteringRequestBody 265 | * 266 | * @description Provide a function to Hock to filter the request body 267 | * 268 | * @param {function} filter the function to filter on 269 | * 270 | * @returns {Hock} 271 | */ 272 | Hock.prototype.filteringRequestBody = function (filter) { 273 | this._requestFilter = filter; 274 | return this; 275 | }; 276 | 277 | /** 278 | * Hock.filteringRequestBodyRegEx 279 | * 280 | * @description match incoming requests, and replace the body based on 281 | * a regular expression match 282 | * 283 | * @param {RegEx} source The source regular expression 284 | * @param {string} replace What to replace the source with 285 | * 286 | * @returns {Hock} 287 | */ 288 | Hock.prototype.filteringRequestBodyRegEx = function (source, replace) { 289 | this._requestFilter = function (path) { 290 | if (path) { 291 | path = path.replace(source, replace); 292 | } 293 | return path; 294 | }; 295 | 296 | return this; 297 | }; 298 | 299 | /** 300 | * Hock.filteringPath 301 | * 302 | * @description Provide a function to Hock to filter request path 303 | * 304 | * @param {function} filter the function to filter on 305 | * 306 | * @returns {Hock} 307 | */ 308 | Hock.prototype.filteringPath = function (filter) { 309 | this._pathFilter = filter; 310 | return this; 311 | }; 312 | 313 | /** 314 | * Hock.filteringPathRegEx 315 | * 316 | * @description match incoming requests, and replace the path based on 317 | * a regular expression match 318 | * 319 | * @param {RegEx} source The source regular expression 320 | * @param {string} replace What to replace the source with 321 | * 322 | * @returns {Hock} 323 | */ 324 | Hock.prototype.filteringPathRegEx = function (source, replace) { 325 | this._pathFilter = function (path) { 326 | if (path) { 327 | path = path.replace(source, replace); 328 | } 329 | return path; 330 | }; 331 | 332 | return this; 333 | }; 334 | 335 | /** 336 | * Hock.clearBodyFilter 337 | * 338 | * @description clear the body request filter, if any 339 | * 340 | * @returns {Hock} 341 | */ 342 | Hock.prototype.clearBodyFilter = function () { 343 | delete this._requestFilter; 344 | return this; 345 | } 346 | 347 | /** 348 | * Hock.defaultReplyHeaders 349 | * 350 | * @description set standard headers for all responses 351 | * 352 | * @param {object} headers the list of headers to send by default 353 | * 354 | * @returns {Hock} 355 | */ 356 | Hock.prototype.defaultReplyHeaders = function (headers) { 357 | this._defaultReplyHeaders = headers; 358 | return this; 359 | }; 360 | 361 | /** 362 | * Hock.handler 363 | * 364 | * @description Handle incoming requests 365 | * 366 | * @returns {Function} 367 | * @private 368 | */ 369 | Hock.prototype.handler = function (req, res) { 370 | var self = this; 371 | 372 | var matchIndex = null; 373 | 374 | req.body = ''; 375 | 376 | req.on('data', function (data) { 377 | req.body += data.toString(); 378 | }); 379 | 380 | req.on('end', function () { 381 | 382 | const matchIndex = self._assertions.findIndex(assertion => assertion.isMatch(req)); 383 | 384 | if (matchIndex === -1) { 385 | if (self._throwOnUnmatched) { 386 | throw new Error('No Match For: ' + req.method + ' ' + req.url); 387 | } 388 | 389 | console.error('No Match For: ' + req.method + ' ' + req.url); 390 | if (req.method === 'PUT' || req.method === 'PATCH' || req.method === 'POST') { 391 | console.error(req.body); 392 | } 393 | res.writeHead(500, { 'Content-Type': 'text/plain' }); 394 | res.end('No Matching Response!\n'); 395 | } 396 | else { 397 | if (self._assertions[matchIndex].sendResponse(req, res)) { 398 | self._assertions.splice(matchIndex, 1)[0]; 399 | } 400 | if (self._assertions.length === 0) self.emit('done'); 401 | } 402 | }); 403 | }; 404 | 405 | /** 406 | * exports.createHock 407 | * 408 | * @description static method for creating your hock server 409 | * 410 | * @param {object} [options] options for your Hock server 411 | * @param {Number} [options.port] port number for your Hock server 412 | * @param {boolean} [options.throwOnUnmatched] Tell Hock to throw if 413 | * receiving a request without a match (Default=true) 414 | * 415 | * @returns {Hock} 416 | */ 417 | var createHock = function(options) { 418 | return new Hock(options); 419 | }; 420 | 421 | module.exports = createHock; 422 | module.exports.createHock = createHock; 423 | -------------------------------------------------------------------------------- /test/many-test.js: -------------------------------------------------------------------------------- 1 | var http = require('http'), 2 | should = require('should'), 3 | request = require('request'), 4 | util = require('util'), 5 | crypto = require('crypto'), 6 | hock = require('../'); 7 | 8 | var PORT = 5678; 9 | 10 | describe('Hock Multiple Request Tests', function () { 11 | 12 | var hockInstance; 13 | var httpServer; 14 | 15 | describe("With minimum requests", function () { 16 | beforeEach(function (done) { 17 | hockInstance = hock.createHock(); 18 | httpServer = http.createServer(hockInstance.handler).listen(PORT, function(err) { 19 | should.not.exist(err); 20 | should.exist(hockInstance); 21 | 22 | done(); 23 | }); 24 | }); 25 | 26 | it('should succeed with once', function (done) { 27 | hockInstance 28 | .get('/url') 29 | .once() 30 | .reply(200, { 'hock': 'ok' }); 31 | 32 | request('http://localhost:' + PORT + '/url', function (err, res, body) { 33 | should.not.exist(err); 34 | should.exist(res); 35 | res.statusCode.should.equal(200); 36 | JSON.parse(body).should.eql({ 'hock': 'ok' }); 37 | hockInstance.done(); 38 | done(); 39 | }); 40 | }); 41 | 42 | it('should fail with min: 2 and a single request', function (done) { 43 | hockInstance 44 | .get('/url') 45 | .min(2) 46 | .reply(200, { 'hock': 'ok' }); 47 | 48 | request('http://localhost:' + PORT + '/url', function (err, res, body) { 49 | should.not.exist(err); 50 | should.exist(res); 51 | res.statusCode.should.equal(200); 52 | JSON.parse(body).should.eql({ 'hock': 'ok' }); 53 | (function() { 54 | hockInstance.done(); 55 | }).should.throw(); 56 | done(); 57 | }); 58 | }); 59 | 60 | it('should succeed with min:2 and 2 requests', function (done) { 61 | hockInstance 62 | .get('/url') 63 | .min(2) 64 | .reply(200, { 'hock': 'ok' }); 65 | 66 | request('http://localhost:' + PORT + '/url', function (err, res, body) { 67 | should.not.exist(err); 68 | should.exist(res); 69 | res.statusCode.should.equal(200); 70 | JSON.parse(body).should.eql({ 'hock': 'ok' }); 71 | 72 | request('http://localhost:' + PORT + '/url', function (err, res, body) { 73 | should.not.exist(err); 74 | should.exist(res); 75 | res.statusCode.should.equal(200); 76 | JSON.parse(body).should.eql({ 'hock': 'ok' }); 77 | 78 | hockInstance.done(); 79 | done(); 80 | }); 81 | }); 82 | }); 83 | 84 | it('should succeed with max:2 and 1 request', function (done) { 85 | hockInstance 86 | .get('/url') 87 | .max(2) 88 | .reply(200, { 'hock': 'ok' }); 89 | 90 | request('http://localhost:' + PORT + '/url', function (err, res, body) { 91 | should.not.exist(err); 92 | should.exist(res); 93 | res.statusCode.should.equal(200); 94 | JSON.parse(body).should.eql({ 'hock': 'ok' }); 95 | 96 | hockInstance.done(); 97 | done(); 98 | }); 99 | }); 100 | 101 | it('should succeed with max:2 and 2 requests', function (done) { 102 | hockInstance 103 | .get('/url') 104 | .max(2) 105 | .reply(200, { 'hock': 'ok' }); 106 | 107 | request('http://localhost:' + PORT + '/url', function (err, res, body) { 108 | should.not.exist(err); 109 | should.exist(res); 110 | res.statusCode.should.equal(200); 111 | JSON.parse(body).should.eql({ 'hock': 'ok' }); 112 | 113 | request('http://localhost:' + PORT + '/url', function (err, res, body) { 114 | should.not.exist(err); 115 | should.exist(res); 116 | res.statusCode.should.equal(200); 117 | JSON.parse(body).should.eql({ 'hock': 'ok' }); 118 | 119 | hockInstance.done(); 120 | done(); 121 | }); 122 | }); 123 | }); 124 | 125 | it('should succeed with min:2, max:3 and 2 requests', function (done) { 126 | hockInstance 127 | .get('/url') 128 | .min(2) 129 | .max(3) 130 | .reply(200, { 'hock': 'ok' }); 131 | 132 | request('http://localhost:' + PORT + '/url', function (err, res, body) { 133 | should.not.exist(err); 134 | should.exist(res); 135 | res.statusCode.should.equal(200); 136 | JSON.parse(body).should.eql({ 'hock': 'ok' }); 137 | 138 | request('http://localhost:' + PORT + '/url', function (err, res, body) { 139 | should.not.exist(err); 140 | should.exist(res); 141 | res.statusCode.should.equal(200); 142 | JSON.parse(body).should.eql({ 'hock': 'ok' }); 143 | 144 | hockInstance.done(); 145 | done(); 146 | }); 147 | }); 148 | }); 149 | 150 | it('should succeed with min:2, max:Infinity and 2 requests', function (done) { 151 | hockInstance 152 | .get('/url') 153 | .min(2) 154 | .max(Infinity) 155 | .reply(200, { 'hock': 'ok' }); 156 | 157 | request('http://localhost:' + PORT + '/url', function (err, res, body) { 158 | should.not.exist(err); 159 | should.exist(res); 160 | res.statusCode.should.equal(200); 161 | JSON.parse(body).should.eql({ 'hock': 'ok' }); 162 | 163 | request('http://localhost:' + PORT + '/url', function (err, res, body) { 164 | should.not.exist(err); 165 | should.exist(res); 166 | res.statusCode.should.equal(200); 167 | JSON.parse(body).should.eql({ 'hock': 'ok' }); 168 | 169 | hockInstance.done(); 170 | done(); 171 | }); 172 | }); 173 | }); 174 | 175 | it('should succeed with 2 different routes with different min, max values', function (done) { 176 | hockInstance 177 | .get('/url') 178 | .min(2) 179 | .max(3) 180 | .reply(200, { 'hock': 'ok' }) 181 | .get('/asdf') 182 | .once() 183 | .reply(200, { 'hock': 'ok' }); 184 | 185 | request('http://localhost:' + PORT + '/url', function (err, res, body) { 186 | should.not.exist(err); 187 | should.exist(res); 188 | res.statusCode.should.equal(200); 189 | JSON.parse(body).should.eql({ 'hock': 'ok' }); 190 | 191 | request('http://localhost:' + PORT + '/asdf', function (err, res, body) { 192 | should.not.exist(err); 193 | should.exist(res); 194 | res.statusCode.should.equal(200); 195 | JSON.parse(body).should.eql({ 'hock': 'ok' }); 196 | 197 | request('http://localhost:' + PORT + '/url', function (err, res, body) { 198 | should.not.exist(err); 199 | should.exist(res); 200 | res.statusCode.should.equal(200); 201 | JSON.parse(body).should.eql({ 'hock': 'ok' }); 202 | 203 | hockInstance.done(); 204 | done(); 205 | }); 206 | }); 207 | }); 208 | }); 209 | 210 | describe('min() and max() with replyWithFile', function () { 211 | it('should succeed with a single call', function (done) { 212 | hockInstance 213 | .get('/url') 214 | .replyWithFile(200, process.cwd() + '/test/data/hello.txt'); 215 | 216 | request('http://localhost:' + PORT + '/url', function (err, res, body) { 217 | should.not.exist(err); 218 | should.exist(res); 219 | res.statusCode.should.equal(200); 220 | body.should.equal('this\nis\nmy\nsample\n'); 221 | hockInstance.done(function (err) { 222 | should.not.exist(err); 223 | done(); 224 | }); 225 | }); 226 | }); 227 | 228 | it('should succeed with a multiple calls', function (done) { 229 | hockInstance 230 | .get('/url') 231 | .twice() 232 | .replyWithFile(200, process.cwd() + '/test/data/hello.txt'); 233 | 234 | request('http://localhost:' + PORT + '/url', function (err, res, body) { 235 | should.not.exist(err); 236 | should.exist(res); 237 | res.statusCode.should.equal(200); 238 | body.should.equal('this\nis\nmy\nsample\n'); 239 | 240 | request('http://localhost:' + PORT + '/url', function (err, res, body) { 241 | should.not.exist(err); 242 | should.exist(res); 243 | res.statusCode.should.equal(200); 244 | body.should.equal('this\nis\nmy\nsample\n'); 245 | hockInstance.done(function (err) { 246 | should.not.exist(err); 247 | done(); 248 | }); 249 | }); 250 | }); 251 | }); 252 | }); 253 | 254 | describe('min() and max() with reply (with stream)', function () { 255 | 256 | var Readable = require('stream').Readable; 257 | 258 | function RandomStream(size, opt) { 259 | Readable.call(this, opt); 260 | this.lenToGenerate = size; 261 | } 262 | 263 | util.inherits(RandomStream, Readable); 264 | 265 | RandomStream.prototype._read = function(size) { 266 | if (!size) { 267 | size = 1024; // default size 268 | } 269 | var ready = true; 270 | while (ready) { // only cont while push returns true 271 | if (size > this.lenToGenerate) { // only this left 272 | size = this.lenToGenerate; 273 | } 274 | if (size) { 275 | ready = this.push(crypto.randomBytes(size)); 276 | this.lenToGenerate -= size; 277 | } 278 | // when done, push null and exit loop 279 | if (!this.lenToGenerate) { 280 | this.push(null); 281 | ready = false; 282 | } 283 | } 284 | }; 285 | 286 | var streamLen = 10000000; // 10Mb 287 | 288 | // NOTE: We need to specify encoding: null in requests below to ensure that the response is 289 | // not encoded as a utf8 string (we want the binary contents from the readstream returned.) 290 | 291 | it('should succeed with a single call', function (done) { 292 | hockInstance 293 | .get('/url') 294 | .reply(200, new RandomStream(streamLen)); 295 | 296 | request({'url': 'http://localhost:' + PORT + '/url', 'encoding': null}, function (err, res, body) { 297 | should.not.exist(err); 298 | should.exist(res); 299 | res.statusCode.should.equal(200); 300 | body.length.should.equal(streamLen); 301 | hockInstance.done(function (err) { 302 | should.not.exist(err); 303 | done(); 304 | }); 305 | }); 306 | }); 307 | 308 | it('should succeed with a multiple calls', function (done) { 309 | hockInstance 310 | .get('/url') 311 | .twice() 312 | .reply(200, new RandomStream(streamLen)); 313 | 314 | request({'url': 'http://localhost:' + PORT + '/url', 'encoding': null}, function (err, res, body) { 315 | should.not.exist(err); 316 | should.exist(res); 317 | res.statusCode.should.equal(200); 318 | body.length.should.equal(streamLen); 319 | 320 | request({'url': 'http://localhost:' + PORT + '/url', 'encoding': null}, function (err, res, body) { 321 | should.not.exist(err); 322 | should.exist(res); 323 | res.statusCode.should.equal(200); 324 | body.length.should.equal(streamLen); 325 | hockInstance.done(function (err) { 326 | should.not.exist(err); 327 | done(); 328 | }); 329 | }); 330 | }); 331 | }); 332 | 333 | it('should have matching body with multiple calls', function (done) { 334 | hockInstance 335 | .get('/url') 336 | .twice() 337 | .reply(200, new RandomStream(1000)); 338 | 339 | request({'url': 'http://localhost:' + PORT + '/url', 'encoding': null}, function (err, res, body1) { 340 | should.not.exist(err); 341 | should.exist(res); 342 | res.statusCode.should.equal(200); 343 | body1.length.should.equal(1000); 344 | 345 | request({'url': 'http://localhost:' + PORT + '/url', 'encoding': null}, function (err, res, body2) { 346 | should.not.exist(err); 347 | should.exist(res); 348 | res.statusCode.should.equal(200); 349 | body2.length.should.equal(1000); 350 | body1.toString().should.equal(body2.toString()); 351 | hockInstance.done(function (err) { 352 | should.not.exist(err); 353 | done(); 354 | }); 355 | }); 356 | }); 357 | }); 358 | }); 359 | 360 | describe('many()', function() { 361 | 362 | it('should fail with no requests', function (done) { 363 | hockInstance 364 | .get('/url') 365 | .many() 366 | .reply(200, { 'hock': 'ok' }); 367 | 368 | (function() { 369 | hockInstance.done(); 370 | }).should.throw(); 371 | done(); 372 | }) 373 | 374 | it('should succeed with many requests', function (done) { 375 | hockInstance 376 | .get('/url') 377 | .many() 378 | .reply(200, { 'hock': 'ok' }) 379 | 380 | request('http://localhost:' + PORT + '/url', function (err, res, body) { 381 | should.not.exist(err); 382 | should.exist(res); 383 | res.statusCode.should.equal(200); 384 | JSON.parse(body).should.eql({ 'hock': 'ok' }); 385 | 386 | request('http://localhost:' + PORT + '/url', function (err, res, body) { 387 | should.not.exist(err); 388 | should.exist(res); 389 | res.statusCode.should.equal(200); 390 | JSON.parse(body).should.eql({ 'hock': 'ok' }); 391 | 392 | request('http://localhost:' + PORT + '/url', function (err, res, body) { 393 | should.not.exist(err); 394 | should.exist(res); 395 | res.statusCode.should.equal(200); 396 | JSON.parse(body).should.eql({ 'hock': 'ok' }); 397 | 398 | hockInstance.done(); 399 | done(); 400 | }); 401 | }); 402 | }); 403 | }); 404 | }); 405 | 406 | 407 | describe('any', function() { 408 | it('should succeed with no requests', function (done) { 409 | hockInstance 410 | .get('/url') 411 | .any() 412 | .reply(200, { 'hock': 'ok' }) 413 | .done(); 414 | done(); 415 | }) 416 | 417 | it('should succeed with many requests', function (done) { 418 | hockInstance 419 | .get('/url') 420 | .any() 421 | .reply(200, { 'hock': 'ok' }); 422 | 423 | request('http://localhost:' + PORT + '/url', function (err, res, body) { 424 | should.not.exist(err); 425 | should.exist(res); 426 | res.statusCode.should.equal(200); 427 | JSON.parse(body).should.eql({ 'hock': 'ok' }); 428 | 429 | request('http://localhost:' + PORT + '/url', function (err, res, body) { 430 | should.not.exist(err); 431 | should.exist(res); 432 | res.statusCode.should.equal(200); 433 | JSON.parse(body).should.eql({ 'hock': 'ok' }); 434 | 435 | request('http://localhost:' + PORT + '/url', function (err, res, body) { 436 | should.not.exist(err); 437 | should.exist(res); 438 | res.statusCode.should.equal(200); 439 | JSON.parse(body).should.eql({ 'hock': 'ok' }); 440 | 441 | hockInstance.done(); 442 | done(); 443 | }); 444 | }); 445 | }); 446 | }); 447 | }); 448 | 449 | afterEach(function (done) { 450 | httpServer.close(done); 451 | }); 452 | }); 453 | }); 454 | --------------------------------------------------------------------------------