├── .gitignore ├── .travis.yml ├── README.md ├── index.js ├── lib ├── request.js └── response.js ├── package.json ├── request.js ├── response.js └── tests ├── integration.js ├── post.js ├── req.js └── res.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | node_modules 14 | 15 | npm-debug.log 16 | .vscode -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "12" 4 | - "10" 5 | - "8" 6 | - "6" 7 | - "4" 8 | - "0.12" 9 | - "0.10" 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hammock. [![Build Status](https://travis-ci.org/doanythingfordethklok/hammock.svg?branch=master)](https://travis-ci.org/doanythingfordethklok/hammock) 2 | 3 | Node.js mock / polyfill http object library for http req / res. 4 | 5 | # Motivation 6 | 7 | Polyfill req / res for testing w/o http or for code generation from an existing site. Since the purpose of this lib is mock req/res for testing, you probably want this as a dev dependency. I think I've used it in a production app for something like SSR or something, but it was unusual!!! 8 | 9 | # Install Node (0.10+) 10 | This includes new releases (e.g. 0.10, 0.11, 0.12, iojs, 4, 5, 6, 7, 8, 9, 10, 11) 11 | 12 | ``` 13 | npm install hammock --save-dev 14 | 15 | OR 16 | 17 | yarn add hammock --dev 18 | ``` 19 | 20 | # Install - Node (0.8 compatibility) 21 | 22 | Hammock v3 breaks backwards compatibility for node 0.8 which 23 | will not affect the vast majority of users. For those still 24 | using 0.8, use major version 2. 25 | 26 | ``` 27 | npm install hammock@^2.2.0 --save-dev 28 | 29 | OR 30 | 31 | yarn add hammock@v2.2.0 --dev 32 | ``` 33 | 34 | 35 | # Example 36 | 37 | 38 | ```js 39 | /* Should consider migrating to a factory so that people don't have to guess whether to use new or not */ 40 | var MockRequest = require('hammock').Request, 41 | MockResponse = require('hammock').Response; 42 | 43 | /* Most This is most helpful for GET requests. In future, it would be nice to polyfill body parsing events. */ 44 | var req = new MockRequest({ 45 | url: '/foo', 46 | headers: { host: 'localhost', bar: 'baz' }, 47 | method: 'GET' 48 | }), 49 | res = new MockResponse(); 50 | 51 | res.on('end', function(err, data) { 52 | console.log(data.statusCode); 53 | console.log(util.inspect(data.headers)); 54 | console.log(data.body); 55 | }); 56 | 57 | /* Using pipeline-router / director syntax here, but this should be simple with express. */ 58 | var router = RouterFactory.create({ /* options */ }); 59 | router.dispatch(req, res); 60 | 61 | ``` 62 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | Request: require('./lib/request'), 5 | Response: require('./lib/response') 6 | }; 7 | -------------------------------------------------------------------------------- /lib/request.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Cookies = require('cookies'); 4 | var url = require('url'); 5 | var http = require('http'); 6 | var util = require('util'); 7 | var PassThrough = require('readable-stream').PassThrough; 8 | 9 | // Some versions of node populate _headers while others 10 | // populate a symbol key (node10+) 11 | var outHeadersKey = readOutHeadersKey(); 12 | 13 | /** 14 | * Mock request object for sending to the router without needing a server. 15 | **/ 16 | var MockRequest = function (options) { 17 | if (!(this instanceof MockRequest)) { 18 | return new MockRequest(options); 19 | } 20 | 21 | PassThrough.call(this, options); 22 | 23 | this.body = ''; 24 | this.httpVersion = '1.1'; 25 | this.url = options.url || '/'; 26 | this.query = url.parse(this.url, true).query; 27 | this[outHeadersKey] = null; 28 | this.headers = this._headers = {}; 29 | 30 | // necessary for mocking a real request. 31 | this._headerNames = {}; 32 | this._removedHeader = {}; 33 | 34 | // setHeader now applies toLowerCase the names to mirror native node behaviour 35 | var self = this; 36 | 37 | for(var k in options.headers) { 38 | if (options.headers[k] !== undefined) { 39 | self.setHeader(k, options.headers[k]); 40 | } 41 | } 42 | 43 | this.setHeader('transfer-encoding', 'chunked'); 44 | 45 | this.method = options.method || 'GET'; 46 | this.connection = this.socket = {}; 47 | // this.buffer = []; 48 | 49 | if (options.cookies && !this.getHeader('cookie')) { 50 | var cookieBuffer = []; 51 | 52 | for(var k in options.cookies) { 53 | cookieBuffer.push(k + '=' + options.cookies[k]); 54 | } 55 | 56 | this.setHeader('cookie', cookieBuffer.join(';')); 57 | } 58 | }; 59 | 60 | util.inherits(MockRequest, PassThrough); 61 | 62 | MockRequest.prototype.setHeader = function (name, value, clobber) { 63 | if (http.ClientRequest) { 64 | var ret = http.ClientRequest.prototype.setHeader.call(this, name, value, clobber); 65 | 66 | if (http.ClientRequest.prototype.getHeaders) { 67 | this.headers = http.ClientRequest.prototype.getHeaders.call(this); 68 | } 69 | else { 70 | this.headers = this[outHeadersKey]; 71 | } 72 | 73 | return ret; 74 | } 75 | else if (clobber || !this.headers.hasOwnProperty(name)) { 76 | this.headers[name.toLowerCase()] = value; 77 | } 78 | else { 79 | this.headers[name.toLowerCase()] += ',' + value; 80 | } 81 | }; 82 | 83 | MockRequest.prototype.getHeader = function (name) { 84 | if (http.ClientRequest) { 85 | return http.ClientRequest.prototype.getHeader.call(this, name); 86 | } 87 | else { 88 | return this.headers[name]; 89 | } 90 | }; 91 | 92 | function readOutHeadersKey () { 93 | if (http.ClientRequest && Object.getOwnPropertySymbols) { 94 | var x = {}; 95 | 96 | try { 97 | http.ClientRequest.apply(x); 98 | } 99 | catch (e) { 100 | // This will invariably throw 101 | } 102 | var symbols = Object.getOwnPropertySymbols(x); 103 | 104 | for (var i = 0; i < symbols.length; i++) { 105 | if (symbols[i].toString() == 'Symbol(outHeadersKey)') { 106 | return symbols[i]; 107 | } 108 | } 109 | } 110 | 111 | return '_headers'; 112 | } 113 | 114 | module.exports = MockRequest; 115 | -------------------------------------------------------------------------------- /lib/response.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var events = require('events'); 4 | var util = require('util'); 5 | var http = require('http'); 6 | var Buffer = require('buffer').Buffer; 7 | var Writable = require('readable-stream').Writable; 8 | 9 | // Some versions of node populate _headers while others 10 | // populate a symbol key (node10+) 11 | var outHeadersKey = readOutHeadersKey(); 12 | 13 | /** 14 | * Mock response object for receiving content without a server. 15 | **/ 16 | var MockResponse = function (callback) { 17 | if (!(this instanceof MockResponse)) { 18 | return new MockResponse(callback); 19 | } 20 | 21 | Writable.call(this, { 22 | decodeStrings: false 23 | }); 24 | 25 | this._buffer = []; 26 | this.statusCode = 200; 27 | this[outHeadersKey] = null; 28 | this._headers = {}; 29 | 30 | if (callback) { 31 | var self = this; 32 | var cleanup = function () { 33 | self.removeListener('error', cleanup) 34 | self.removeListener('response', cleanup) 35 | 36 | callback.apply(this, arguments) 37 | } 38 | this.once('error', cleanup); 39 | this.once('response', cleanup); 40 | } 41 | 42 | // necessary for mocking a real request. 43 | this._headerNames = {}; 44 | this._removedHeader = {}; 45 | }; 46 | 47 | util.inherits(MockResponse, Writable); 48 | 49 | MockResponse.prototype._write = function (chunk, enc, cb) { 50 | this._buffer.push(chunk); 51 | cb(null); 52 | }; 53 | MockResponse.prototype.writeHead = function (statusCode, headers) { 54 | var that = this; 55 | 56 | this.statusCode = statusCode; 57 | Object.keys(headers || {}).forEach(function (k) { 58 | that.setHeader(k, headers[k]); 59 | }); 60 | }; 61 | MockResponse.prototype.setHeader = function (name, value, clobber) { 62 | if (http.ServerResponse) { 63 | var ret = http.ServerResponse.prototype.setHeader.call(this, name, value, clobber); 64 | if (http.ServerResponse.prototype.getHeaders) { 65 | this.headers = http.ServerResponse.prototype.getHeaders.call(this); 66 | } else { 67 | this.headers = this[outHeadersKey]; 68 | } 69 | return ret; 70 | } else if (clobber || !this.headers.hasOwnProperty(name)) { 71 | this.headers[name] = value; 72 | } else { 73 | this.headers[name] += ',' + value; 74 | } 75 | }; 76 | MockResponse.prototype.getHeader = function (name) { 77 | if (http.ServerResponse) { 78 | return http.ServerResponse.prototype.getHeader.call(this, name); 79 | } else { 80 | return this.headers[name]; 81 | } 82 | }; 83 | MockResponse.prototype.end = function (str) { 84 | if (this.finished) { 85 | return; 86 | } 87 | 88 | if (str) { 89 | this._buffer.push(str); 90 | } 91 | 92 | var body = this._buildBody(); 93 | var headers; 94 | 95 | if (http.ServerResponse.prototype.getHeaders) { 96 | headers = http.ServerResponse.prototype.getHeaders.call(this); 97 | } else { 98 | headers = this[outHeadersKey]; 99 | } 100 | 101 | this.emit('close'); 102 | this.emit('finish'); 103 | this.emit('end', null, { // deprecate me 104 | statusCode: this.statusCode, 105 | body: body, 106 | headers: headers 107 | }); 108 | this.emit('response', null, { 109 | statusCode: this.statusCode, 110 | body: body, 111 | headers: headers 112 | }); 113 | this.finished = true 114 | }; 115 | 116 | MockResponse.prototype._buildBody = function _buildBody() { 117 | if (this._buffer.length === 0) { 118 | return ''; 119 | } 120 | if (this._buffer.length === 1) { 121 | return this._buffer[0]; 122 | } 123 | 124 | var isBuffers = true; 125 | for (var i = 0; i < this._buffer.length; i++) { 126 | if (!Buffer.isBuffer(this._buffer[i])) { 127 | isBuffers = false; 128 | } 129 | } 130 | 131 | if (!isBuffers) { 132 | return this._buffer.join(''); 133 | } 134 | 135 | return Buffer.concat(this._buffer); 136 | }; 137 | 138 | function readOutHeadersKey() { 139 | if (http.ServerResponse && Object.getOwnPropertySymbols) { 140 | var x = {}; 141 | try { 142 | http.ServerResponse.apply(x); 143 | } catch (e) { 144 | // This will invariably throw 145 | } 146 | var symbols = Object.getOwnPropertySymbols(x); 147 | for (var i = 0; i < symbols.length; i++) { 148 | if (symbols[i].toString() == 'Symbol(outHeadersKey)') { 149 | return symbols[i]; 150 | } 151 | } 152 | } 153 | return '_headers'; 154 | } 155 | 156 | 157 | module.exports = MockResponse; 158 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hammock", 3 | "version": "3.0.1", 4 | "description": "Node.js mock / polyfill http object library for http req / res", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "tape ./tests/*" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git@github.com:doanythingfordethklok/hammock.git" 12 | }, 13 | "keywords": [ 14 | "node", 15 | "mock", 16 | "http", 17 | "request", 18 | "response", 19 | "polyfill" 20 | ], 21 | "dependencies": { 22 | "cookies": "0.7.3", 23 | "readable-stream": "2.3.3" 24 | }, 25 | "devDependencies": { 26 | "nock": "8.2.2", 27 | "request": "2.88.0", 28 | "tape": "4.8.0" 29 | }, 30 | "author": "Tommy Messbauer", 31 | "license": "MIT" 32 | } 33 | -------------------------------------------------------------------------------- /request.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require("./lib/request"); 4 | -------------------------------------------------------------------------------- /response.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require("./lib/response"); 4 | -------------------------------------------------------------------------------- /tests/integration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | 5 | var Buffer = require('buffer').Buffer; 6 | var nock = require('nock'); 7 | var PassThrough = require('readable-stream').PassThrough; 8 | var request = require('request'); 9 | 10 | var MockRequest = require('../index.js').Request; 11 | var MockResponse = require('../index.js').Response; 12 | 13 | test('Integration: MockRequest should pipe through request module', function t (assert) { 14 | nock('http://localhost:5555') 15 | .post('/derp', '{a') 16 | .reply(200, '{b'); 17 | 18 | var req = MockRequest({ 19 | url: 'http://localhost:5555/derp', 20 | method: 'POST' 21 | }); 22 | 23 | var chunks = []; 24 | var p = new PassThrough(); 25 | 26 | p.on('data', function accumulate (d) { 27 | chunks.push(d); 28 | }); 29 | p.on('end', function assertTest () { 30 | var data = Buffer.concat(chunks); 31 | 32 | assert.strictEqual(String(data), '{b'); 33 | assert.end(); 34 | }); 35 | 36 | req.pipe(request.post('http://localhost:5555/derp')) 37 | .pipe(p); 38 | 39 | req.write('{a'); 40 | req.end(); 41 | }); 42 | 43 | test('Integration: response module should pipe to hammock res', function t (assert) { 44 | nock('http://localhost:5555', { 45 | reqHeaders: { 46 | 'x-test-request': 'RequestValue' 47 | } 48 | }).post('/derp', '{a') 49 | .reply(200, '{b', { 'x-test-response': 'ResponseValue' }); 50 | 51 | var req = MockRequest({ 52 | url: 'http://localhost:5555/derp', 53 | method: 'POST', 54 | headers: { 55 | 'x-test-request': 'RequestValue' 56 | } 57 | }); 58 | var res = MockResponse(function onRes (err, response) { 59 | assert.ifError(err); 60 | assert.strictEqual(String(response.body), '{b'); 61 | assert.deepEqual(response.headers, { 62 | 'x-test-response': 'ResponseValue' 63 | }); 64 | assert.end(); 65 | }); 66 | 67 | req.pipe(request.post('http://localhost:5555/derp')) 68 | .pipe(res); 69 | 70 | req.write('{a'); 71 | req.end(); 72 | }); 73 | -------------------------------------------------------------------------------- /tests/post.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | var MockRequest = require('../index.js').Request; 5 | var Cookies = require('cookies'); 6 | 7 | test('Create post request', function (t) { 8 | var req = new MockRequest({ 9 | method: 'POST', 10 | url: '/hello', 11 | headers: { 12 | 'Content-Type': 'text/plain' 13 | }, 14 | cookies: { 15 | foo: 'bar', 16 | bar: 'baz' 17 | } 18 | }); 19 | 20 | t.ok(req, 'Request was created'); 21 | 22 | var cookies = new Cookies(req); 23 | var foo = cookies.get('foo'); 24 | var bar = cookies.get('bar'); 25 | 26 | t.equal(foo, 'bar', 'Cookie (foo) should return the original value.'); 27 | t.equal(bar, 'baz', 'Cookie (bar) should return the original value.'); 28 | 29 | var _body = ''; 30 | 31 | req.on('data', function (body) { 32 | _body += body; 33 | }); 34 | req.once('end', function () { 35 | t.equals(_body.toString(), 'foobar', 'Body should have been pushed from mock req.'); 36 | t.end(); 37 | }); 38 | 39 | req.write('foo'); 40 | req.end('bar'); 41 | }); 42 | -------------------------------------------------------------------------------- /tests/req.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | var MockRequest = require('../index.js').Request; 5 | var Cookies = require('cookies'); 6 | var PassThrough = require('readable-stream').PassThrough; 7 | 8 | test('MockRequest: Create request with cookie', function (t) { 9 | t.plan(3); 10 | 11 | var req = new MockRequest({ 12 | url: '/hello', 13 | cookies: { 14 | foo: 'bar', 15 | bar: 'baz' 16 | } 17 | }); 18 | 19 | t.ok(req, 'Request was created'); 20 | 21 | var cookies = new Cookies(req); 22 | var foo = cookies.get('foo'); 23 | var bar = cookies.get('bar'); 24 | 25 | t.equal(foo, 'bar', 'Cookie (foo) should return the original value.'); 26 | t.equal(bar, 'baz', 'Cookie (bar) should return the original value.'); 27 | t.end(); 28 | }); 29 | 30 | test('MockRequest: can create with cookie header', function (t) { 31 | t.plan(2); 32 | 33 | var req = new MockRequest({ 34 | headers: { 35 | cookie: 's=foo' 36 | } 37 | }); 38 | 39 | t.ok(req); 40 | t.equal(req.headers.cookie, 's=foo'); 41 | 42 | t.end(); 43 | }); 44 | 45 | test('MockRequest: pipe to another stream', function (t) { 46 | t.plan(1); 47 | 48 | var test_string = 'qweqwe'; 49 | var p = new PassThrough(); 50 | 51 | p.on('data', function accumulate (d) { 52 | t.equal(d.toString(), test_string); 53 | }); 54 | p.on('end', function assertTest () { 55 | t.end(); 56 | }); 57 | 58 | var req = new MockRequest({ 59 | headers: { 60 | cookie: 's=foo' 61 | } 62 | }); 63 | 64 | req.pipe(p); 65 | 66 | req.write(test_string); 67 | req.end(); 68 | }); 69 | -------------------------------------------------------------------------------- /tests/res.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'); 4 | var MockResponse = require('../index.js').Response; 5 | var PassThrough = require('readable-stream').PassThrough; 6 | 7 | test('MockResponse: can write to response', function t (assert) { 8 | assert.plan(1); // ensure onResponse is not called multiple times or some other weird condition. 9 | var res = new MockResponse(onResponse); 10 | 11 | res.write('foo'); 12 | res.write('bar'); 13 | res.end(); 14 | 15 | function onResponse (err, resp) { 16 | assert.equal(resp.body, 'foobar'); 17 | assert.end(); 18 | } 19 | }); 20 | 21 | test('MockResponse: can write buffers to response', function t (assert) { 22 | assert.plan(1); // ensure onResponse is not called multiple times or some other weird condition. 23 | var res = new MockResponse(onResponse); 24 | 25 | res.write('foo'); 26 | res.write(new Buffer('bar')); 27 | res.end(); 28 | 29 | function onResponse (err, resp) { 30 | assert.equal(resp.body, 'foobar'); 31 | assert.end(); 32 | } 33 | }); 34 | 35 | test('MockResponse: can write buffer to end()', function t (assert) { 36 | assert.plan(1); // ensure onResponse is not called multiple times or some other weird condition. 37 | var res = new MockResponse(onResponse); 38 | 39 | res.end(new Buffer('foobar')); 40 | 41 | function onResponse (err, resp) { 42 | assert.equal( 43 | resp.body.toString('hex'), new Buffer('foobar').toString('hex') 44 | ); 45 | assert.end(); 46 | } 47 | }); 48 | 49 | test('MockResponse: can write only buffers to response', function t (assert) { 50 | assert.plan(1); // ensure onResponse is not called multiple times or some other weird condition. 51 | var res = new MockResponse(onResponse); 52 | 53 | res.write(new Buffer('foo')); 54 | res.write(new Buffer('bar')); 55 | res.end(); 56 | 57 | function onResponse (err, resp) { 58 | assert.equal( 59 | resp.body.toString('hex'), new Buffer('foobar').toString('hex') 60 | ); 61 | assert.end(); 62 | } 63 | }); 64 | 65 | test('MockResponse: can write only buffers to response with pipe', function t (assert) { 66 | assert.plan(1); // ensure onResponse is not called multiple times or some other weird condition. 67 | var res = new MockResponse(onResponse); 68 | 69 | var stream = new PassThrough(); 70 | 71 | stream.write(new Buffer('foo')); 72 | stream.write(new Buffer('bar')); 73 | stream.pipe(res); 74 | stream.end(); 75 | 76 | function onResponse (err, resp) { 77 | assert.equal( 78 | resp.body.toString('hex'), new Buffer('foobar').toString('hex') 79 | ); 80 | assert.end(); 81 | } 82 | }); 83 | 84 | test('MockResponse: can handle empty responses', function t (assert) { 85 | assert.plan(1); // ensure onResponse is not called multiple times or some other weird condition. 86 | var res = new MockResponse(onResponse); 87 | 88 | var stream = new PassThrough(); 89 | 90 | stream.pipe(res); 91 | stream.end(); 92 | 93 | function onResponse (err, resp) { 94 | assert.deepEqual(resp.body, ''); 95 | assert.end(); 96 | } 97 | }); 98 | 99 | test('MockResponse: set headers', function t (assert) { 100 | assert.plan(2); // ensure onResponse is not called multiple times or some other weird condition. 101 | 102 | var res = new MockResponse(onResponse); 103 | 104 | res.setHeader('x-test', 'sdf'); 105 | res.setHeader('x-test2', 'ertt'); 106 | 107 | var stream = new PassThrough(); 108 | 109 | stream.pipe(res); 110 | stream.end(); 111 | 112 | function onResponse (err, resp) { 113 | assert.deepEqual(resp.headers['x-test'], 'sdf'); 114 | assert.deepEqual(resp.headers['x-test2'], 'ertt'); 115 | assert.end(); 116 | } 117 | }); 118 | 119 | test('MockResponse: pipe to res', function t (assert) { 120 | assert.plan(2); 121 | 122 | var res = MockResponse(function onRes (err, response) { 123 | assert.ifError(err); 124 | assert.strictEqual(String(response.body), '{b'); 125 | assert.end(); 126 | }); 127 | 128 | var writer = new PassThrough(); 129 | 130 | writer.pipe(res); 131 | writer.write('{b'); 132 | writer.end(); 133 | }); 134 | --------------------------------------------------------------------------------