From 60dbe34dbc7da33582ae178ad023a6bf4cb46e7c Mon Sep 17 00:00:00 2001 From: Gregor Martynus <39992+gr2m@users.noreply.github.com> Date: Sun, 28 Nov 2021 15:04:11 -0800 Subject: [PATCH] fix: make all `test_intercpet.js` tests pass again --- lib/intercept.js | 72 +++++++++++++++---- modules/intercept-node-http/index.js | 2 +- .../lib/client-request/index.js | 17 ++++- .../lib/override-requests.js | 29 ++++---- 4 files changed, 91 insertions(+), 29 deletions(-) diff --git a/lib/intercept.js b/lib/intercept.js index 202244311..9bf208c34 100644 --- a/lib/intercept.js +++ b/lib/intercept.js @@ -303,20 +303,34 @@ function activate() { ) if (!matches && allowUnmocked) { - globalEmitter.emit('no match', req) + globalEmitter.emit('no match', overriddenRequest) return request.nockSendRealRequest() } debug('using', interceptors.length, 'interceptors') - // TODO: find out when this is needed and make it work again - // interceptors.forEach(interceptor => { - // this.setHostHeaderUsingInterceptor(interceptor) + // We need to override the host header in case the `filteringScope` option is used. + // + // Example: + // + // ```js + // const scope = nock('https://api.dropbox.com', { + // filteringScope: scope => /^https:\/\/api[0-9]*.dropbox.com/.test(scope), // }) - - const requestBodyBuffers = [] - const requestBodyBuffer = Buffer.concat(requestBodyBuffers) + // .get('/1/metadata/auto/Photos?include_deleted=false&list=true') + // .reply(200) + // ``` + // + // In the above example we want to match requests to any subdomain of `dropbox.com` matching `/api[0-9]*/`. + // But in the actual request, it should look as if the request went to `api.dropbox.com` as defined in the scope. + // + // I guess this is for making work better with recording. ~@gr2m + interceptors.forEach(interceptor => { + setHostHeaderUsingInterceptor(options, overriddenRequest, interceptor) + }) + + const requestBodyBuffer = overriddenRequest.nockGetRequestBodyAsBuffer() // When request body is a binary buffer we internally use in its hexadecimal // representation. const requestBodyIsUtf8Representable = @@ -350,7 +364,12 @@ function activate() { return } - globalEmitter.emit('no match', req, options, requestBodyString) + globalEmitter.emit( + 'no match', + overriddenRequest, + options, + requestBodyString + ) // Try to find a hostname match that allows unmocked. const allowUnmockedForHostName = interceptors.some( @@ -370,15 +389,44 @@ function activate() { globalEmitter.emit('no match', options) - if (isOn() && isEnabledForNetConnect(options)) { - const error = new NetConnectNotAllowedError(options.host, options.path) - return new ErroringClientRequest(error) + if (isOff() || isEnabledForNetConnect(options)) { + return overriddenRequest.nockSendRealRequest() } - return request.nockSendRealRequest() + const error = new NetConnectNotAllowedError(options.host, options.path) + return new ErroringClientRequest(error) }) } +/** + * Set request headers of the given request. This is needed both during the + * routing phase, in case header filters were specified, and during the + * interceptor-playback phase, to correctly pass mocked request headers. + * TODO There are some problems with this; see https://github.com/nock/nock/issues/1718 + */ +function setHostHeaderUsingInterceptor(options, request, interceptor) { + // If a filtered scope is being used we have to use scope's host in the + // header, otherwise 'host' header won't match. + // NOTE: We use lower-case header field names throughout Nock. + const HOST_HEADER = 'host' + if (interceptor.__nock_filteredScope && interceptor.__nock_scopeHost) { + options.headers[HOST_HEADER] = interceptor.__nock_scopeHost + request.setHeader(HOST_HEADER, interceptor.__nock_scopeHost) + } else { + // For all other cases, we always add host header equal to the requested + // host unless it was already defined. + if (options.host && !request.getHeader(HOST_HEADER)) { + let hostHeader = options.host + + if (options.port === 80 || options.port === 443) { + hostHeader = hostHeader.split(':')[0] + } + + request.setHeader(HOST_HEADER, hostHeader) + } + } +} + module.exports = { addInterceptor, remove, diff --git a/modules/intercept-node-http/index.js b/modules/intercept-node-http/index.js index 31e5780ed..0c9e888fc 100644 --- a/modules/intercept-node-http/index.js +++ b/modules/intercept-node-http/index.js @@ -28,7 +28,7 @@ function setupNodeIntercept(onIntercept) { createNockInterceptedClientRequest(onIntercept) http.ClientRequest = NockInterceptedClientRequest - overrideRequests(function (proto, overriddenRequest, args) { + overrideRequests(function (...args) { return new NockInterceptedClientRequest(...args) }) diff --git a/modules/intercept-node-http/lib/client-request/index.js b/modules/intercept-node-http/lib/client-request/index.js index a810c0627..390cf4ec1 100644 --- a/modules/intercept-node-http/lib/client-request/index.js +++ b/modules/intercept-node-http/lib/client-request/index.js @@ -67,7 +67,7 @@ function createNockInterceptedClientRequest(onIntercept) { // override public API methods this.write = (...args) => handleWrite(this, state, ...args) this.end = (...args) => handleEnd(this, state, ...args) - this.flushHeaders = () => handleFlushHeaders(state, this) + this.flushHeaders = () => handleFlushHeaders(this, state) // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expect if (options.headers.expect === '100-continue') { @@ -111,6 +111,9 @@ function createNockInterceptedClientRequest(onIntercept) { state.onResponseCallback ) + // do not call response callback twice + this.removeListener('response', state.onResponseCallback) + propagate(newRequest, this) // TODO: pass raw buffer of request body as received by mocked request @@ -120,6 +123,15 @@ function createNockInterceptedClientRequest(onIntercept) { // the real requests is sent out, and that it's called when // the response is mocked } + + /** + * Expose method to retrieve request body as a buffer for matching purposes + * + * @returns {Buffer} + */ + this.nockGetRequestBodyAsBuffer = function nockGetRequestBodyAsBuffer() { + return Buffer.concat(state.requestBodyBuffers) + } } } @@ -276,10 +288,11 @@ function prepareForIntercept(request, state) { } // set host header - let hostHeader = options.host + let hostHeader = options.host || options.hostname if (options.port === 80 || options.port === 443) { hostHeader = hostHeader.split(':')[0] } + request.setHeader('host', hostHeader) // wait to emit the finish event until we know for sure that the request will be intercepted, diff --git a/modules/intercept-node-http/lib/override-requests.js b/modules/intercept-node-http/lib/override-requests.js index 84a75a658..8669dfb56 100644 --- a/modules/intercept-node-http/lib/override-requests.js +++ b/modules/intercept-node-http/lib/override-requests.js @@ -1,6 +1,8 @@ const http = require('http') const https = require('https') +const normalizeNodeRequestArguments = require('./normalize-request-arguments') + module.exports = overrideRequests /** @@ -17,27 +19,26 @@ module.exports = overrideRequests function overrideRequests(newRequest) { ;['http', 'https'].forEach(function (moduleName) { const module = moduleName === 'http' ? http : https - const overriddenRequest = module.request - const overriddenGet = module.get // https://nodejs.org/api/http.html#http_http_request_url_options_callback - module.request = function (url, options, callback) { - return newRequest(moduleName, overriddenRequest.bind(module), [ - url, - options, - callback, - ]) + module.request = function (...args) { + const { options, callback } = normalizeNodeRequestArguments(...args) + return newRequest(withDefaultProtocol(moduleName, options), callback) } // https://nodejs.org/api/http.html#http_http_get_options_callback - module.get = function (url, options, callback) { - const req = newRequest(moduleName, overriddenGet.bind(module), [ - url, - options, - callback, - ]) + module.get = function (...args) { + const { options, callback } = normalizeNodeRequestArguments(...args) + const req = newRequest(withDefaultProtocol(moduleName, options), callback) req.end() return req } }) } + +function withDefaultProtocol(moduleName, options) { + return { + protocol: moduleName + ':', + ...options, + } +}