From 6af825322b9a360c00658262ea106f56589a0368 Mon Sep 17 00:00:00 2001 From: Nik Krimm Date: Fri, 2 May 2025 17:08:27 -0500 Subject: [PATCH] Improve clarity of test names. --- README.md | 65 +++- test/decorateUserResHeaders.js | 145 +++++---- test/defineMultipleProxyHandlers.js | 98 ++++--- test/handleProxyError.js | 150 +++++----- test/middlewareCompatibility.js | 168 +++++------ test/resolveProxyReqPath.js | 119 ++++---- test/streaming.js | 56 ++-- test/userResDecorator.js | 440 ++++++++++++---------------- 8 files changed, 618 insertions(+), 623 deletions(-) diff --git a/README.md b/README.md index acd2e47e..d0fff853 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# express-http-proxy [![NPM version](https://badge.fury.io/js/express-http-proxy.svg)](http://badge.fury.io/js/express-http-proxy) [![Build Status](https://travis-ci.org/villadora/express-http-proxy.svg?branch=master)](https://travis-ci.org/villadora/express-http-proxy) +# express-http-proxy [![NPM version](https://badge.fury.io/js/express-http-proxy.svg)](http://badge.fury.io/js/express-http-proxy) [![Build Status](https://travis-ci.org/villadora/express-http-proxy.svg?branch=master)](https://travis-ci.org/villadora/express-http-proxy) Express middleware to proxy request to another host and pass response back to original caller. @@ -24,6 +24,51 @@ var app = require('express')(); app.use('/proxy', proxy('www.google.com')); ``` +### 30k view + +The proxy middleware: +* proxies request to your server to an arbitrary server, and +* provide hooks to decorate and filter requests to the proxy target, and +* provide hooks you to decorate and filter proxy responses before returning them to the client. + +``` + +Client Express App Proxy Middleware Target Server + | | | | + | HTTP Request | | | + |-------------------------->| | | + | | Request | | + | |--------------------------->| | + | | | +------------------------+ | + | | | | Request Preprocessing | | + | | | | 1. filter requests | | + | | | | 2. resolve proxy host | | + | | | | 3. decorate proxy opts | | + | | | | 4. decorate proxy req | | + | | | | 5. resolve req path | | + | | | +------------------------+ | + | | | Forwarded Request | + | | |---------------------------->| + | | | | + | | | Response with Headers | + | | |<----------------------------| + | | | | + | | | +------------------------+ | + | | | | Response Processing | | + | | | | 1. skip to next? | | + | | | | 2. copy proxy headers | | + | | | | 3. decorate headers | | + | | | | 4. decorate response | | + | | | +------------------------+ | + | | | | + | | Modified Response | | + | |<---------------------------| | + | Final Response | | | + |<--------------------------| | | + | | | | + +``` + ### Streaming Proxy requests and user responses are piped/streamed/chunked by default. @@ -74,7 +119,7 @@ function selectProxyHost() { app.use('/', proxy(selectProxyHost)); ``` -Notie: Host is only the host name. Any params after in url will be ignored. For ``http://google.com/myPath`, ``myPath`` will be ignored because the host name is ``google.com``. +Notie: Host is only the host name. Any params after in url will be ignored. For ``http://google.com/myPath`, ``myPath`` will be ignored because the host name is ``google.com``. See ``proxyReqPathResolver`` for more detailed path information. @@ -172,17 +217,17 @@ Promise form: ```js app.use(proxy('localhost:12346', { - filter: function (req, res) { - return new Promise(function (resolve) { + filter: function (req, res) { + return new Promise(function (resolve) { resolve(req.method === 'GET'); - }); + }); } })); ``` Note that in the previous example, `resolve(false)` will execute the happy path for filter here (skipping the rest of the proxy, and calling `next()`). -`reject()` will also skip the rest of proxy and call `next()`. +`reject()` will also skip the rest of proxy and call `next()`. #### userResDecorator (was: intercept) (supports Promise) @@ -276,8 +321,8 @@ first request. When a `userResHeaderDecorator` is defined, the return of this method will replace (rather than be merged on to) the headers for `userRes`. -> Note that by default, headers from the PROXY response CLOBBER all headers that may have previously been set on the userResponse. -> Authors have the option of constructing any combination of proxyRes and userRes headers in the `userResHeaderDecorator`. +> Note that by default, headers from the PROXY response CLOBBER all headers that may have previously been set on the userResponse. +> Authors have the option of constructing any combination of proxyRes and userRes headers in the `userResHeaderDecorator`. > Check the tests for this method for examples. @@ -608,9 +653,9 @@ app.use('/', proxy('internalhost.example.com', { | 1.6.0 | Do gzip and gunzip aysyncronously. Test and documentation improvements, dependency updates. | | 1.5.1 | Fixes bug in stringifying debug messages. | | 1.5.0 | Fixes bug in `filter` signature. Fix bug in skipToNextHandler, add expressHttpProxy value to user res when skipped. Add tests for host as ip address. | -| 1.4.0 | DEPRECATED. Critical bug in the `filter` api.| +| 1.4.0 | DEPRECATED. Critical bug in the `filter` api.| | 1.3.0 | DEPRECATED. Critical bug in the `filter` api. `filter` now supports Promises. Update linter to eslint. | -| 1.2.0 | Auto-stream when no decorations are made to req/res. Improved docs, fixes issues in maybeSkipToNexthandler, allow authors to manage error handling. | +| 1.2.0 | Auto-stream when no decorations are made to req/res. Improved docs, fixes issues in maybeSkipToNexthandler, allow authors to manage error handling. | | 1.1.0 | Add step to allow response headers to be modified. | 1.0.7 | Update dependencies. Improve docs on promise rejection. Fix promise rejection on body limit. Improve debug output. | | 1.0.6 | Fixes preserveHostHdr not working, skip userResDecorator on 304, add maybeSkipToNext, test improvements and cleanup. | diff --git a/test/decorateUserResHeaders.js b/test/decorateUserResHeaders.js index 660a0b9e..a25b7af7 100644 --- a/test/decorateUserResHeaders.js +++ b/test/decorateUserResHeaders.js @@ -4,18 +4,11 @@ var assert = require('assert'); var express = require('express'); var request = require('supertest'); var proxy = require('../'); -var proxyTarget = require('./support/proxyTarget'); var TIMEOUT = require('./constants'); -describe('when userResHeaderDecorator is defined', function () { - this.timeout(TIMEOUT.STANDARD); - +describe('userResHeaderDecorator', function () { var app; - var serverReference; - - afterEach(function () { - serverReference.close(); - }); + var serverReference; beforeEach(function () { app = express(); @@ -32,80 +25,84 @@ describe('when userResHeaderDecorator is defined', function () { serverReference.close(); }); - it('can delete a header', function (done) { - app.use('/proxy', proxy('http://127.0.0.1:12345', { - userResHeaderDecorator: function (headers /*, userReq, userRes, proxyReq, proxyRes */) { - delete headers['x-my-secret-header']; - return headers; - } - })); + describe('header modification', function () { + it('can remove headers from the response', function (done) { + app.use('/proxy', proxy('http://127.0.0.1:12345', { + userResHeaderDecorator: function (headers) { + delete headers['x-my-secret-header']; + return headers; + } + })); - app.use(function (req, res) { - res.sendStatus(200); + request(app) + .get('/proxy') + .expect(function (res) { + assert(Object.keys(res.headers).indexOf('x-my-not-so-secret-header') > -1); + assert(Object.keys(res.headers).indexOf('x-my-secret-header') === -1); + }) + .end(done); }); - request(app) - .get('/proxy') - .expect(function (res) { - assert(Object.keys(res.headers).indexOf('x-my-not-so-secret-header') > -1); - assert(Object.keys(res.headers).indexOf('x-my-secret-header') === -1); - }) - .end(done); - }); - - it('provides an interface for updating headers', function (done) { - app.use('/proxy', proxy('http://127.0.0.1:12345', { - userResHeaderDecorator: function (headers /*, userReq, userRes, proxyReq, proxyRes */) { - headers.boltedonheader = 'franky'; - return headers; - } - })); + it('can add new headers to the response', function (done) { + app.use('/proxy', proxy('http://127.0.0.1:12345', { + userResHeaderDecorator: function (headers) { + headers.boltedonheader = 'franky'; + return headers; + } + })); - app.use(function (req, res) { - res.sendStatus(200); + request(app) + .get('/proxy') + .expect(function (res) { + assert(res.headers.boltedonheader === 'franky'); + }) + .end(done); }); - - request(app) - .get('/proxy') - .expect(function (res) { - assert(res.headers.boltedonheader === 'franky'); - }) - .end(done); }); - it('author has option to copy proxyResponse headers to userResponse', function (done) { - app.use('/proxy', proxy('http://127.0.0.1:12345', { - userResHeaderDecorator: function (headers, userReq) { // proxyReq - // Copy specific headers from the proxy request to the user response - // - // We can copy them to new name - if (userReq.headers['x-custom-header']) { - headers['x-proxied-custom-header'] = userReq.headers['x-custom-header']; - } - if (userReq.headers['x-user-agent']) { - headers['x-proxied-user-agent'] = userReq.headers['x-user-agent']; + describe('header proxying', function () { + it('can copy request headers to response with new names', function (done) { + app.use('/proxy', proxy('http://127.0.0.1:12345', { + userResHeaderDecorator: function (headers, userReq) { + if (userReq.headers['x-custom-header']) { + headers['x-proxied-custom-header'] = userReq.headers['x-custom-header']; + } + if (userReq.headers['x-user-agent']) { + headers['x-proxied-user-agent'] = userReq.headers['x-user-agent']; + } + return headers; } + })); - // We can copy them to the same name - headers['x-copied-header-1'] = userReq.headers['x-copied-header-1']; - headers['x-copied-header-2'] = userReq.headers['x-copied-header-2']; - return headers; - } - })); + request(app) + .get('/proxy') + .set('x-custom-header', 'custom-value') + .set('x-user-agent', 'test-agent') + .expect(function (res) { + assert.equal(res.headers['x-proxied-custom-header'], 'custom-value'); + assert.equal(res.headers['x-proxied-user-agent'], 'test-agent'); + }) + .end(done); + }); - request(app) - .get('/proxy') - .set('x-custom-header', 'custom-value') - .set('x-user-agent', 'test-agent') - .set('x-copied-header-1', 'value1') - .set('x-copied-header-2', 'value2') - .expect(function (res) { - // Verify the original headers were proxied to the response - assert.equal(res.headers['x-proxied-custom-header'], 'custom-value'); - assert.equal(res.headers['x-proxied-user-agent'], 'test-agent'); - assert.equal(res.headers['x-copied-header-1'], 'value1'); - assert.equal(res.headers['x-copied-header-2'], 'value2'); - }) - .end(done); + it('can copy request headers to response with same names', function (done) { + app.use('/proxy', proxy('http://127.0.0.1:12345', { + userResHeaderDecorator: function (headers, userReq) { + headers['x-copied-header-1'] = userReq.headers['x-copied-header-1']; + headers['x-copied-header-2'] = userReq.headers['x-copied-header-2']; + return headers; + } + })); + + request(app) + .get('/proxy') + .set('x-copied-header-1', 'value1') + .set('x-copied-header-2', 'value2') + .expect(function (res) { + assert.equal(res.headers['x-copied-header-1'], 'value1'); + assert.equal(res.headers['x-copied-header-2'], 'value2'); + }) + .end(done); + }); }); }); diff --git a/test/defineMultipleProxyHandlers.js b/test/defineMultipleProxyHandlers.js index 852d8995..e2f0658b 100644 --- a/test/defineMultipleProxyHandlers.js +++ b/test/defineMultipleProxyHandlers.js @@ -5,10 +5,9 @@ var express = require('express'); var http = require('http'); var startProxyTarget = require('./support/proxyTarget'); var proxy = require('../'); -var proxyTarget = require('./support/proxyTarget'); var TIMEOUT = require('./constants'); -function fakeProxyServer({path, port, response}) { +function createProxyServer({ path, port, response }) { var proxyRouteFn = [{ method: 'get', path: path, @@ -23,11 +22,18 @@ function fakeProxyServer({path, port, response}) { function simulateUserRequest() { return new Promise(function (resolve, reject) { - - var req = http.request({ hostname: 'localhost', port: 8308, path: '/' }, function (res) { + var req = http.request({ + hostname: 'localhost', + port: 8308, + path: '/' + }, function (res) { var chunks = []; - res.on('data', function (chunk) { chunks.push(chunk.toString()); }); - res.on('end', function () { resolve(chunks); }); + res.on('data', function (chunk) { + chunks.push(chunk.toString()); + }); + res.on('end', function () { + resolve(chunks); + }); }); req.on('error', function (e) { @@ -35,56 +41,66 @@ function simulateUserRequest() { }); req.end(); - }) + }); } -describe('handle multiple proxies in the same runtime', function () { +describe('multiple proxy handlers', function () { this.timeout(TIMEOUT.MEDIUM); var server; - var targetServer, targetServer2; + var primaryProxyServer; + var fallbackProxyServer; beforeEach(function () { - targetServer = fakeProxyServer({path:'/', port: '8309', response: '8309_response'}); - targetServer2 = fakeProxyServer({path: '/', port: '8310', response: '8310_response'}); + primaryProxyServer = createProxyServer({ + path: '/', + port: '8309', + response: 'primary_response' + }); + fallbackProxyServer = createProxyServer({ + path: '/', + port: '8310', + response: 'fallback_response' + }); }); afterEach(function () { server.close(); - targetServer.close(); - targetServer2.close(); + primaryProxyServer.close(); + fallbackProxyServer.close(); }); + describe('when multiple proxies are defined for the same route', function () { + describe('proxy selection behavior', function () { + it('should use the first proxy when it succeeds', function (done) { + var app = express(); + app.use(proxy('http://localhost:8309', {})); + app.use(proxy('http://localhost:8310', {})); + server = app.listen(8308); - describe("When two distinct proxies are defined for the global route", () => { - afterEach(() => server.close()) + simulateUserRequest() + .then(function (res) { + assert.equal(res[0], 'primary_response', 'Should use primary proxy response'); + done(); + }) + .catch(done); + }); - it('the first proxy definition should be used if it succeeds', function (done) { - var app = express(); - app.use(proxy('http://localhost:8309', {})); - app.use(proxy('http://localhost:8310', {})); - server = app.listen(8308) - simulateUserRequest() - .then(function (res) { - assert.equal(res[0], '8309_response'); - done(); - }) - .catch(done); - }); + it('should use the fallback proxy when primary skips to next', function (done) { + var app = express(); + app.use(proxy('http://localhost:8309', { + skipToNextHandlerFilter: () => true // Force skip to next handler + })); + app.use(proxy('http://localhost:8310', {})); + server = app.listen(8308); - it('the fall through definition should be used if the prior skipsToNext', function (done) { - var app = express(); - app.use(proxy('http://localhost:8309', { - skipToNextHandlerFilter: () => { return true } // no matter what, reject this proxy request, and call next() - })); - app.use(proxy('http://localhost:8310')) - server = app.listen(8308) - simulateUserRequest() - .then(function (res) { - assert.equal(res[0], '8310_response'); - done(); - }) - .catch(done); + simulateUserRequest() + .then(function (res) { + assert.equal(res[0], 'fallback_response', 'Should use fallback proxy response'); + done(); + }) + .catch(done); + }); }); - }) + }); }); diff --git a/test/handleProxyError.js b/test/handleProxyError.js index 34a71c0e..f5a127e1 100644 --- a/test/handleProxyError.js +++ b/test/handleProxyError.js @@ -5,24 +5,30 @@ var express = require('express'); var request = require('supertest'); var proxy = require('../'); var proxyTarget = require('../test/support/proxyTarget'); -var proxyRouteFn = [{ - method: 'get', - path: '/:errorCode', - fn: function (req, res) { - var errorCode = req.params.errorCode; - if (errorCode === 'timeout') { - return res.status(504).send('mock timeout'); +var TIMEOUT = require('./constants'); + +function createErrorProxyServer() { + return proxyTarget(12346, 100, [{ + method: 'get', + path: '/:errorCode', + fn: function (req, res) { + var errorCode = req.params.errorCode; + if (errorCode === 'timeout') { + return res.status(504).send('mock timeout'); + } + return res.status(parseInt(errorCode)).send('test case error'); } - return res.status(parseInt(errorCode)).send('test case error'); - } -}]; + }]); +} -describe('error handling can be over-ridden by user', function () { - var app = express(); +describe('proxy error handling', function () { + this.timeout(TIMEOUT.STANDARD); + + var app; var proxyServer; beforeEach(function () { - proxyServer = proxyTarget(12346, 100, proxyRouteFn); + proxyServer = createErrorProxyServer(); app = express(); }); @@ -30,12 +36,11 @@ describe('error handling can be over-ridden by user', function () { proxyServer.close(); }); - describe('when user provides a null function', function () { - - describe('when author sets a timeout that fires', function () { - it('passes 504 directly to client', function (done) { + describe('when using default error handling', function () { + describe('timeout scenarios', function () { + it('should pass 504 timeout directly to client when emitted by proxy', function (done) { app.use(proxy('localhost:12346', { - timeout: 1, + timeout: 1 })); request(app) @@ -46,36 +51,38 @@ describe('error handling can be over-ridden by user', function () { }); }); - it('passes status code (e.g. 504) directly to the client', function (done) { - app.use(proxy('localhost:12346')); - request(app) - .get('/504') - .expect(504) - .expect(function (res) { - assert(res.text === 'test case error'); - return res; - }) - .end(done); - }); + describe('error status codes', function () { + it('should pass 504 gateway timeout directly to client when emitted by the proxy', function (done) { + app.use(proxy('localhost:12346')); + request(app) + .get('/504') + .expect(504) + .expect(function (res) { + assert(res.text === 'test case error'); + return res; + }) + .end(done); + }); - it('passes status code (e.g. 500) back to the client', function (done) { - app.use(proxy('localhost:12346')); - request(app) - .get('/500') - .expect(500) - .end(function (err, res) { - assert(res.text === 'test case error'); - done(); - }); + it('should pass 500 server error directly to client when emitted by the proxy', function (done) { + app.use(proxy('localhost:12346')); + request(app) + .get('/500') + .expect(500) + .end(function (err, res) { + assert(res.text === 'test case error'); + done(); + }); + }); }); }); - describe('when user provides a handler function', function () { - var intentionallyWeirdStatusCode = 399; - var intentionallyQuirkyStatusMessage = 'monkey skunky'; + describe('when using custom error handling', function () { + describe('timeout scenarios', function () { + it('should allow custom handling of timeout errors', function (done) { + var customStatusCode = 399; + var customStatusMessage = 'custom timeout message'; - describe('when author sets a timeout that fires', function () { - it('allows author to skip handling and handle in application step', function (done) { app.use(proxy('localhost:12346', { timeout: 1, proxyErrorHandler: function (err, res, next) { @@ -83,54 +90,45 @@ describe('error handling can be over-ridden by user', function () { } })); - app.use(function (err, req, res, next) { // eslint-disable-line no-unused-vars + app.use(function (err, req, res, next) { if (err.code === 'ECONNRESET') { - res.status(intentionallyWeirdStatusCode).send(intentionallyQuirkyStatusMessage); + res.status(customStatusCode).send(customStatusMessage); } }); request(app) .get('/200') .expect(function (res) { - assert(res.text === intentionallyQuirkyStatusMessage); + assert(res.text === customStatusMessage); return res; }) - .expect(intentionallyWeirdStatusCode) + .expect(customStatusCode) .end(done); }); }); - it('allows authors to sub in their own handling', function (done) { - app.use(proxy('localhost:12346', { - timeout: 1, - proxyErrorHandler: function (err, res, next) { - switch (err && err.code) { - case 'ECONNRESET': { return res.status(405).send('504 became 405'); } - case 'ECONNREFUSED': { return res.status(200).send('gotcher back'); } - default: { next(err); } + describe('error transformation', function () { + it('should allow transforming error responses', function (done) { + app.use(proxy('localhost:12346', { + timeout: 1, + proxyErrorHandler: function (err, res, next) { + switch (err && err.code) { + case 'ECONNRESET': { return res.status(405).send('504 became 405'); } + case 'ECONNREFUSED': { return res.status(200).send('gotcher back'); } + default: { next(err); } + } } - } })); - - request(app) - .get('/timeout') - .expect(405) - .expect(function (res) { - assert(res.text === '504 became 405'); - return res; - }) - .end(done); - }); + })); - it('passes status code (e.g. 500) back to the client', function (done) { - app.use(proxy('localhost:12346')); - request(app) - .get('/500') - .expect(500) - .end(function (err, res) { - assert(res.text === 'test case error'); - done(); - }); + request(app) + .get('/timeout') + .expect(405) + .expect(function (res) { + assert(res.text === '504 became 405'); + return res; + }) + .end(done); + }); }); }); - }); diff --git a/test/middlewareCompatibility.js b/test/middlewareCompatibility.js index 435dd831..568298b7 100644 --- a/test/middlewareCompatibility.js +++ b/test/middlewareCompatibility.js @@ -6,105 +6,109 @@ var request = require('supertest'); var bodyParser = require('body-parser'); var proxy = require('../'); var proxyTarget = require('../test/support/proxyTarget'); +var TIMEOUT = require('./constants'); - -var proxyRouteFn = [{ - method: 'post', - path: '/poster', - fn: function (req, res) { - res.send(req.body); - } -}]; +function createProxyServer() { + return proxyTarget(12346, 100, [{ + method: 'post', + path: '/poster', + fn: function (req, res) { + res.send(req.body); + } + }]); +} describe('middleware compatibility', function () { + this.timeout(TIMEOUT.STANDARD); + var proxyServer; beforeEach(function () { - proxyServer = proxyTarget(12346, 100, proxyRouteFn); + proxyServer = createProxyServer(); }); afterEach(function () { proxyServer.close(); }); - it('should use req.body if defined', function (done) { - var app = express(); - - // Simulate another middleware that puts req stream into the body + describe('request body handling', function () { + it('should use req.body when defined by previous middleware', function (done) { + var app = express(); - app.use(function (req, res, next) { - var received = []; - req.on('data', function onData(chunk) { - if (!chunk) { return; } - received.push(chunk); + // Simulate middleware that processes request body + app.use(function (req, res, next) { + var received = []; + req.on('data', function onData(chunk) { + if (!chunk) { return; } + received.push(chunk); + }); + req.on('end', function onEnd() { + received = Buffer.concat(received).toString('utf8'); + req.body = JSON.parse(received); + req.body.foo = 1; + next(); + }); }); - req.on('end', function onEnd() { - received = Buffer.concat(received).toString('utf8'); - req.body = JSON.parse(received); - req.body.foo = 1; - next(); - }); - }); - app.use(proxy('localhost:12346', { - userResDecorator: function (rsp, data, req) { - assert(req.body); - assert.equal(req.body.foo, 1); - assert.equal(req.body.mypost, 'hello'); - return data; - } - })); + app.use(proxy('localhost:12346', { + userResDecorator: function (rsp, data, req) { + assert(req.body); + assert.equal(req.body.foo, 1); + assert.equal(req.body.mypost, 'hello'); + return data; + } + })); - request(app) - .post('/poster') - .send({ mypost: 'hello' }) - .expect(function (res) { - assert.equal(res.body.foo, 1); - assert.equal(res.body.mypost, 'hello'); - }) - .end(done); - }); + request(app) + .post('/poster') + .send({ mypost: 'hello' }) + .expect(function (res) { + assert.equal(res.body.foo, 1); + assert.equal(res.body.mypost, 'hello'); + }) + .end(done); + }); - it('should stringify req.body when it is a json body so it is written to proxy request', function (done) { - var app = express(); - app.use(bodyParser.json()); - app.use(bodyParser.urlencoded({ - extended: false - })); - app.use(proxy('localhost:12346')); - request(app) - .post('/poster') - .send({ - mypost: 'hello', - doorknob: 'wrect' - }) - .expect(function (res) { - assert.equal(res.body.doorknob, 'wrect'); - assert.equal(res.body.mypost, 'hello'); - }) - .end(done); - }); + it('should stringify JSON body for proxy request', function (done) { + var app = express(); + app.use(bodyParser.json()); + app.use(bodyParser.urlencoded({ + extended: false + })); + app.use(proxy('localhost:12346')); + request(app) + .post('/poster') + .send({ + mypost: 'hello', + doorknob: 'wrect' + }) + .expect(function (res) { + assert.equal(res.body.doorknob, 'wrect'); + assert.equal(res.body.mypost, 'hello'); + }) + .end(done); + }); - it('should convert req.body to a Buffer when reqAsBuffer is set', function (done) { - var app = express(); - app.use(bodyParser.json()); - app.use(bodyParser.urlencoded({ - extended: false - })); - app.use(proxy('localhost:12346', { - reqAsBuffer: true - })); - request(app) - .post('/poster') - .send({ - mypost: 'hello', - doorknob: 'wrect' - }) - .expect(function (res) { - assert.equal(res.body.doorknob, 'wrect'); - assert.equal(res.body.mypost, 'hello'); - }) - .end(done); + it('should convert body to Buffer when reqAsBuffer is set', function (done) { + var app = express(); + app.use(bodyParser.json()); + app.use(bodyParser.urlencoded({ + extended: false + })); + app.use(proxy('localhost:12346', { + reqAsBuffer: true + })); + request(app) + .post('/poster') + .send({ + mypost: 'hello', + doorknob: 'wrect' + }) + .expect(function (res) { + assert.equal(res.body.doorknob, 'wrect'); + assert.equal(res.body.mypost, 'hello'); + }) + .end(done); + }); }); - }); diff --git a/test/resolveProxyReqPath.js b/test/resolveProxyReqPath.js index d02c80c1..59654b83 100644 --- a/test/resolveProxyReqPath.js +++ b/test/resolveProxyReqPath.js @@ -7,92 +7,84 @@ var expect = require('chai').expect; var express = require('express'); var request = require('supertest'); var proxy = require('../'); +var TIMEOUT = require('./constants'); +describe('proxy path resolution', function () { + this.timeout(TIMEOUT.STANDARD); -describe('resolveProxyReqPath', function () { var container; beforeEach(function () { container = new ScopeContainer(); }); - var tests = [ - { - resolverType: 'undefined', - resolverFn: undefined, - data: [ - { url: 'http://localhost:12345', parsed: '/' }, - { url: 'http://g.com/123?45=67', parsed: '/123?45=67' } - ] - }, - { - resolverType: 'a syncronous function', - resolverFn: function () { return 'the craziest thing'; }, - data: [ - { url: 'http://localhost:12345', parsed: 'the craziest thing' }, - { url: 'http://g.com/123?45=67', parsed: 'the craziest thing' } - ] - }, - { - resolverType: 'a Promise', - resolverFn: function () { - return new Promise(function (resolve) { - resolve('the craziest think'); - }); + describe('path resolver types', function () { + var testCases = [ + { + name: 'default resolver', + resolverFn: undefined, + testData: [ + { url: 'http://localhost:12345', expected: '/' }, + { url: 'http://g.com/123?45=67', expected: '/123?45=67' } + ] }, - data: [ - { url: 'http://localhost:12345', parsed: 'the craziest think' }, - { url: 'http://g.com/123?45=67', parsed: 'the craziest think' } - ] - } - ]; - - describe('when proxyReqPathResolver', function () { + { + name: 'synchronous resolver', + resolverFn: function () { return 'custom/path'; }, + testData: [ + { url: 'http://localhost:12345', expected: 'custom/path' }, + { url: 'http://g.com/123?45=67', expected: 'custom/path' } + ] + }, + { + name: 'asynchronous resolver', + resolverFn: function () { + return new Promise(function (resolve) { + resolve('async/path'); + }); + }, + testData: [ + { url: 'http://localhost:12345', expected: 'async/path' }, + { url: 'http://g.com/123?45=67', expected: 'async/path' } + ] + } + ]; - tests.forEach(function (test) { - describe('is ' + test.resolverType, function () { - describe('it returns a promise which resolves a container with expected url', function () { - test.data.forEach(function (data) { - it(data.url, function (done) { - container.user.req = { url: data.url }; - container.options.proxyReqPathResolver = test.resolverFn; - var r = resolveProxyReqPath(container); + testCases.forEach(function (testCase) { + describe('when using ' + testCase.name, function () { + testCase.testData.forEach(function (data) { + it('should resolve ' + data.url + ' to ' + data.expected, function (done) { + container.user.req = { url: data.url }; + container.options.proxyReqPathResolver = testCase.resolverFn; + var result = resolveProxyReqPath(container); - assert(r instanceof Promise, 'Expect resolver to return a thennable'); + assert(result instanceof Promise, 'Resolver should return a Promise'); - r.then(function (container) { - var response; - try { - response = container.proxy.reqBuilder.path; - if (!response) { - throw new Error('reqBuilder.url is undefined'); - } - } catch (e) { - done(e); - } - expect(response).to.equal(data.parsed); - done(); - }); - }); + result.then(function (container) { + var resolvedPath = container.proxy.reqBuilder.path; + if (!resolvedPath) { + return done(new Error('reqBuilder.path is undefined')); + } + expect(resolvedPath).to.equal(data.expected); + done(); + }).catch(done); }); }); }); }); - }); - describe('testing example code in docs', function () { - it('works as advertised', function (done) { + describe('path transformation examples', function () { + it('should transform paths as shown in documentation', function (done) { var proxyTarget = require('../test/support/proxyTarget'); - var proxyRouteFn = [{ + var proxyServer = proxyTarget(12345, 100, [{ method: 'get', path: '/tent', fn: function (req, res) { res.send(req.url); } - }]; + }]); - var proxyServer = proxyTarget(12345, 100, proxyRouteFn); var app = express(); app.use(proxy('localhost:12345', { proxyReqPathResolver: function (req) { @@ -105,12 +97,11 @@ describe('resolveProxyReqPath', function () { request(app) .get('/test?a=1&b=2&c=3') - .end(function (err, res) { - assert.equal(res.text, '/tent?a=1&b=2&c=3'); + .expect('/tent?a=1&b=2&c=3') + .end(function (err) { proxyServer.close(); done(err); }); }); }); - }); diff --git a/test/streaming.js b/test/streaming.js index dc571eae..35466549 100644 --- a/test/streaming.js +++ b/test/streaming.js @@ -5,7 +5,6 @@ var express = require('express'); var http = require('http'); var startProxyTarget = require('./support/proxyTarget'); var proxy = require('../'); -var proxyTarget = require('./support/proxyTarget'); var TIMEOUT = require('./constants'); function chunkingProxyServer() { @@ -46,11 +45,11 @@ function startLocalServer(proxyOptions) { return app.listen(8308); } -describe('streams / piped requests', function () { +describe('streaming behavior / piped requests', function () { this.timeout(TIMEOUT.MEDIUM); var server; - var targetServer; + var targetServer; beforeEach(function () { targetServer = chunkingProxyServer(); @@ -61,33 +60,34 @@ describe('streams / piped requests', function () { targetServer.close(); }); - describe('when streaming options are truthy', function () { + describe('when streaming is enabled', function () { var TEST_CASES = [{ - name: 'vanilla, no options defined', + name: 'with default options', options: {} }, { - name: 'proxyReqOptDecorator is defined', - options: { proxyReqOptDecorator: function (reqBuilder) { return reqBuilder; } } + name: 'with synchronous proxyReqOptDecorator', + options: { + proxyReqOptDecorator: function (reqBuilder) { + return reqBuilder; + } + } }, { - //// Keep around this case for manually testing that this for sure fails for a few cycles. 2018 NMK - //name: 'proxyReqOptDecorator never returns', - //options: { proxyReqOptDecorator: function () { return new Promise(function () {}); } } - //}, { - - name: 'proxyReqOptDecorator is a Promise', - options: { proxyReqOptDecorator: function (reqBuilder) { return Promise.resolve(reqBuilder); } } + name: 'with asynchronous proxyReqOptDecorator', + options: { + proxyReqOptDecorator: function (reqBuilder) { + return Promise.resolve(reqBuilder); + } + } }]; TEST_CASES.forEach(function (testCase) { describe(testCase.name, function () { - it('chunks are received without any buffering, e.g. before request end', function (done) { + it('should receive response in chunks before request completion', function (done) { server = startLocalServer(testCase.options); simulateUserRequest() .then(function (res) { - // Assume that if I'm getting a chunked response, it will be an array of length > 1; - - assert(res instanceof Array, 'res is an Array'); - assert.equal(res.length, 4); + assert(res instanceof Array, 'Response should be an array of chunks'); + assert.equal(res.length, 4, 'Response should contain exactly 4 chunks'); done(); }) .catch(done); @@ -96,23 +96,25 @@ describe('streams / piped requests', function () { }); }); - describe('when streaming options are falsey', function () { + describe('when streaming is disabled', function () { var TEST_CASES = [{ - name: 'skipToNextHandler is defined', - options: { skipToNextHandlerFilter: function () { return false; } } + name: 'with skipToNextHandler filter', + options: { + skipToNextHandlerFilter: function () { + return false; + } + } }]; TEST_CASES.forEach(function (testCase) { describe(testCase.name, function () { - it('response arrives in one large chunk', function (done) { + it('should receive response as a single chunk', function (done) { server = startLocalServer(testCase.options); simulateUserRequest() .then(function (res) { - // Assume that if I'm getting a un-chunked response, it will be an array of length = 1; - - assert(res instanceof Array); - assert.equal(res.length, 1); + assert(res instanceof Array, 'Response should be an array'); + assert.equal(res.length, 1, 'Response should be a single chunk'); done(); }) .catch(done); diff --git a/test/userResDecorator.js b/test/userResDecorator.js index 9440482a..4d1a1304 100644 --- a/test/userResDecorator.js +++ b/test/userResDecorator.js @@ -7,8 +7,9 @@ var proxy = require('../'); var proxyTarget = require('./support/proxyTarget'); var TIMEOUT = require('./constants'); +describe('response decoration', function () { describe('userResDecorator', function () { - this.timeout(TIMEOUT.QUICK); + this.timeout(TIMEOUT.EXTENDED); var proxyServer; beforeEach(function () { @@ -19,25 +20,173 @@ describe('userResDecorator', function () { await proxyServer.close(); }); - describe('when handling a 304', function () { - this.timeout(10000); + describe('userResDecorator', function () { + this.timeout(TIMEOUT.EXTENDED); + + + describe('response transformation', function () { + it('should transform JSON response', function (done) { + var app = express(); + var handler = { + method: 'get', + path: '/data', + fn: function(req, res) { + res.json({ origin: '127.0.0.1' }); + } + }; + proxyServer.close(); + proxyServer = proxyTarget(12345, 100, [handler]); + + app.use(proxy('localhost:12345', { + userResDecorator: function (proxyRes, proxyResData) { + var data = JSON.parse(proxyResData.toString('utf8')); + data.intercepted = true; + return JSON.stringify(data); + } + })); + + request(app) + .get('/data') + .end(function (err, res) { + if (err) { return done(err); } + assert(res.body.intercepted); + done(); + }); + }); - var app; - var slowTarget; - var serverReference; + it('should transform HTML response', function (done) { + var app = express(); + var handler = { + method: 'get', + path: '/html', + fn: function(req, res) { + res.send('Test page'); + } + }; + proxyServer.close(); + proxyServer = proxyTarget(12345, 100, [handler]); + + app.use(proxy('localhost:12345', { + userResDecorator: function (targetResponse, data) { + data = data.toString().replace('DOCTYPE', 'WINNING'); + return data; + } + })); + + request(app) + .get('/html') + .end(function (err, res) { + if (err) { return done(err); } + assert(res.text.indexOf('WINNING') > -1); + done(); + }); + }); + }); - beforeEach(function () { - app = express(); - slowTarget = express(); - slowTarget.use(function (req, res) { res.sendStatus(304); }); - serverReference = slowTarget.listen(12346); + describe('response header modification', function () { + it('should modify custom headers [deviant case, supported by pass-by-reference atm]', function (done) { + var app = express(); + app.use(proxy('localhost:12345', { + userResDecorator: function (rsp, data, req, res) { + res.set('x-wombat-alliance', 'mammels'); + res.set('content-type', 'wiki/wiki'); + return data; + } + })); + + request(app) + .get('/get') + .end(function (err, res) { + if (err) { return done(err); } + assert.equal(res.headers['content-type'], 'wiki/wiki'); + assert.equal(res.headers['x-wombat-alliance'], 'mammels'); + done(); + }); + }); }); - afterEach(function () { - serverReference.close(); + describe('asynchronous decoration', function () { + it('should support Promise-based decoration', function (done) { + var app = express(); + var handler = { + method: 'get', + path: '/promise', + fn: function(req, res) { + res.json({ origin: '127.0.0.1' }); + } + }; + proxyServer.close(); + proxyServer = proxyTarget(12345, 100, [handler]); + + app.use(proxy('localhost:12345', { + userResDecorator: function (proxyRes, proxyResData) { + return new Promise(function (resolve) { + const decoratedResponse = JSON.parse(proxyResData.toString()); + decoratedResponse.funkyMessage = 'oi io oo ii'; + setTimeout(function () { + resolve(JSON.stringify(decoratedResponse)); + }, 200); + }); + } + })); + + request(app) + .get('/promise') + .end(function (err, res) { + if (err) { return done(err); } + assert.equal(res.body.origin, '127.0.0.1'); + assert.equal(res.body.funkyMessage, 'oi io oo ii'); + done(); + }); + }); + }); + + describe('redirect handling', function () { + it('should modify redirect location', function (done) { + function redirectingServer(port, origin) { + var app = express(); + app.get('/', function (req, res) { + res.status(302); + res.location(origin + '/proxied/redirect/url'); + res.send(); + }); + return app.listen(port); + } + + var redirectingServerPort = 8012; + var redirectingServerOrigin = ['http://localhost', redirectingServerPort].join(':'); + var server = redirectingServer(redirectingServerPort, redirectingServerOrigin); + var proxyApp = express(); + var preferredPort = 3000; + + proxyApp.use(proxy(redirectingServerOrigin, { + userResDecorator: function (rsp, data, req, res) { + var proxyReturnedLocation = res.getHeaders ? res.getHeaders().location : res._headers.location; + res.location(proxyReturnedLocation.replace(redirectingServerPort, preferredPort)); + return data; + } + })); + + request(proxyApp) + .get('/') + .expect(function (res) { + res.headers.location.match(/localhost:3000/); + }) + .end(function () { + server.close(); + done(); + }); + }); }); + }); + + describe('special status codes', function () { + it('should skip decoration for 304 responses', function (done) { + var app = express(); + var slowTarget = express(); + slowTarget.use(function (req, res) { res.sendStatus(304); }); + var serverReference = slowTarget.listen(12346); - it('skips any handling', function (done) { app.use('/proxy', proxy('http://127.0.0.1:12346', { userResDecorator: function (/*res*/) { throw new Error('expected to never get here because this step should be skipped for 304'); @@ -47,181 +196,42 @@ describe('userResDecorator', function () { request(app) .get('/proxy') .expect(304) - .end(done); - }); - }); - - it('has access to original response', function (done) { - var app = express(); - app.use(proxy('localhost:12345', { - userResDecorator: function (proxyRes, proxyResData) { - assert(proxyRes.connection); - assert(proxyRes.socket); - assert(proxyRes.headers); - assert(proxyRes.headers['content-type']); - return proxyResData; - } - })); - - request(app).get('/get').end(done); - }); - - it('works with promises', function (done) { - var app = express(); - var handler = { - method: 'get', - path: '/promise', - fn: function(req, res) { - res.json({ origin: '127.0.0.1' }); - } - }; - proxyServer.close(); - proxyServer = proxyTarget(12345, 100, [handler]); - - app.use(proxy('localhost:12345', { - userResDecorator: function (proxyRes, proxyResData) { - return new Promise(function (resolve) { - const decoratedResponse = JSON.parse(proxyResData.toString()); - decoratedResponse.funkyMessage = 'oi io oo ii'; - setTimeout(function () { - resolve(JSON.stringify(decoratedResponse)); - }, 200); + .end(function (err) { + serverReference.close(); + done(err); }); - } - })); - - request(app) - .get('/promise') - .end(function (err, res) { - if (err) { return done(err); } - assert.equal(res.body.origin, '127.0.0.1'); - assert.equal(res.body.funkyMessage, 'oi io oo ii'); - done(); - }); - }); - - it('can modify the response data', function (done) { - var app = express(); - var handler = { - method: 'get', - path: '/data', - fn: function(req, res) { - res.json({ origin: '127.0.0.1' }); - } - }; - proxyServer.close(); - proxyServer = proxyTarget(12345, 100, [handler]); - - app.use(proxy('localhost:12345', { - userResDecorator: function (proxyRes, proxyResData) { - var data = JSON.parse(proxyResData.toString('utf8')); - data.intercepted = true; - return JSON.stringify(data); - } - })); - - request(app) - .get('/data') - .end(function (err, res) { - if (err) { return done(err); } - assert(res.body.intercepted); - done(); - }); - }); - - it('can modify the response headers, [deviant case, supported by pass-by-reference atm]', function (done) { - var app = express(); - app.use(proxy('localhost:12345', { - userResDecorator: function (rsp, data, req, res) { - res.set('x-wombat-alliance', 'mammels'); - res.set('content-type', 'wiki/wiki'); - return data; - } - })); - - request(app) - .get('/get') - .end(function (err, res) { - if (err) { return done(err); } - assert.equal(res.headers['content-type'], 'wiki/wiki'); - assert.equal(res.headers['x-wombat-alliance'], 'mammels'); - done(); - }); - }); - - it('can mutuate an html response', function (done) { - var app = express(); - var handler = { - method: 'get', - path: '/html', - fn: function(req, res) { - res.send('Oh, hey there'); - } - }; - proxyServer.close(); - proxyServer = proxyTarget(12345, 100, [handler]); - - app.use(proxy('localhost:12345', { - userResDecorator: function (rsp, data) { - data = data.toString().replace('Oh', 'Hey'); - return data; - } - })); - - request(app) - .get('/html') - .end(function (err, res) { - if (err) { return done(err); } - assert.equal(res.status, 200, 'Response should have 200 status code'); - assert.equal(res.type, 'text/html', 'Response should be HTML content type'); - assert(res.text.includes('Hey'), 'Response should contain the modified text'); - assert(!res.text.includes('Oh, hey there'), 'Response should not contain the original text'); - done(); - }); + }); }); - it('can change the location of a redirect', function (done) { - function redirectingServer(port, origin) { + describe('response access', function () { + it('should have access to original response properties', function (done) { var app = express(); - app.get('/', function (req, res) { - res.status(302); - res.location(origin + '/proxied/redirect/url'); - res.send(); - }); - return app.listen(port); - } - - var redirectingServerPort = 8012; - var redirectingServerOrigin = ['http://localhost', redirectingServerPort].join(':'); - - var server = redirectingServer(redirectingServerPort, redirectingServerOrigin); - - var proxyApp = express(); - var preferredPort = 3000; - - proxyApp.use(proxy(redirectingServerOrigin, { - userResDecorator: function (rsp, data, req, res) { - var proxyReturnedLocation = res.getHeaders ? res.getHeaders().location : res._headers.location; - res.location(proxyReturnedLocation.replace(redirectingServerPort, preferredPort)); - return data; - } - })); + app.use(proxy('localhost:12345', { + userResDecorator: function (proxyRes, proxyResData) { + assert(proxyRes.connection); + assert(proxyRes.socket); + assert(proxyRes.headers); + assert(proxyRes.headers['content-type']); + return proxyResData; + } + })); - request(proxyApp) - .get('/') - .expect(function (res) { - res.headers.location.match(/localhost:3000/); - }) - .end(function () { - server.close(); - done(); - }); + request(app).get('/get').then(() => done()); + }); }); - describe('when userResDecorator returns a Promise', function () { - this.timeout(TIMEOUT.EXTENDED); // give it some extra time to get response + describe('external response handling', function () { + + /* + Github provided a unique situation where the encoding was different than + utf-8 when we didn't explicitly ask for utf-8. This test helped sort out + the issue, and even though its a little too on the nose for a specific + case, it seems worth keeping around to ensure we don't regress on this + issue. + */ - it('is able to read and manipulate the response', function (done) { + it('should handle GitHub response encoding correctly', function (done) { + this.timeout(15000); var app = express(); app.use(proxy('https://github.com/villadora/express-http-proxy', { userResDecorator: function (targetResponse, data) { @@ -242,71 +252,3 @@ describe('userResDecorator', function () { }); }); -describe('test userResDecorator on html response from mock server', function () { - var proxyServer; - - beforeEach(function () { - var handler = { - method: 'get', - path: '/html', - fn: function(req, res) { - res.send('Test page'); - } - }; - proxyServer = proxyTarget(12345, 100, [handler]); - }); - - afterEach(function () { - proxyServer.close(); - }); - - it('is able to read and manipulate the response', function (done) { - var app = express(); - app.use(proxy('localhost:12345', { - userResDecorator: function (targetResponse, data) { - data = data.toString().replace('DOCTYPE', 'WINNING'); - return data; - } - })); - - request(app) - .get('/html') - .end(function (err, res) { - if (err) { return done(err); } - assert(res.text.indexOf('WINNING') > -1); - done(); - }); - }); -}); - -describe('test userResDecorator on html response from github', function () { - - /* - Github provided a unique situation where the encoding was different than - utf-8 when we didn't explicitly ask for utf-8. This test helped sort out - the issue, and even though its a little too on the nose for a specific - case, it seems worth keeping around to ensure we don't regress on this - issue. - */ - - it('is able to read and manipulate the response', function (done) { - this.timeout(15000); // give it some extra time to get response - var app = express(); - app.use(proxy('https://github.com/villadora/express-http-proxy', { - userResDecorator: function (targetResponse, data) { - data = data.toString().replace('DOCTYPE', 'WINNING'); - assert(data !== ''); - return data; - } - })); - - request(app) - .get('/html') - .end(function (err, res) { - if (err) { return done(err); } - assert(res.text.indexOf('WINNING') > -1); - done(); - }); - }); -}); -