UNPKG

13.8 kBJavaScriptView Raw
1'use strict'
2
3/**
4 * @module nock/intercept
5 */
6
7const { InterceptedRequestRouter } = require('./intercepted_request_router')
8const common = require('./common')
9const { inherits } = require('util')
10const http = require('http')
11const debug = require('debug')('nock.intercept')
12const globalEmitter = require('./global_emitter')
13
14/**
15 * @name NetConnectNotAllowedError
16 * @private
17 * @desc Error trying to make a connection when disabled external access.
18 * @class
19 * @example
20 * nock.disableNetConnect();
21 * http.get('http://zombo.com');
22 * // throw NetConnectNotAllowedError
23 */
24function NetConnectNotAllowedError(host, path) {
25 Error.call(this)
26
27 this.name = 'NetConnectNotAllowedError'
28 this.code = 'ENETUNREACH'
29 this.message = `Nock: Disallowed net connect for "${host}${path}"`
30
31 Error.captureStackTrace(this, this.constructor)
32}
33
34inherits(NetConnectNotAllowedError, Error)
35
36let allInterceptors = {}
37let allowNetConnect
38
39/**
40 * Enabled real request.
41 * @public
42 * @param {String|RegExp} matcher=RegExp.new('.*') Expression to match
43 * @example
44 * // Enables all real requests
45 * nock.enableNetConnect();
46 * @example
47 * // Enables real requests for url that matches google
48 * nock.enableNetConnect('google');
49 * @example
50 * // Enables real requests for url that matches google and amazon
51 * nock.enableNetConnect(/(google|amazon)/);
52 * @example
53 * // Enables real requests for url that includes google
54 * nock.enableNetConnect(host => host.includes('google'));
55 */
56function enableNetConnect(matcher) {
57 if (typeof matcher === 'string') {
58 allowNetConnect = new RegExp(matcher)
59 } else if (matcher instanceof RegExp) {
60 allowNetConnect = matcher
61 } else if (typeof matcher === 'function') {
62 allowNetConnect = { test: matcher }
63 } else {
64 allowNetConnect = /.*/
65 }
66}
67
68function isEnabledForNetConnect(options) {
69 common.normalizeRequestOptions(options)
70
71 const enabled = allowNetConnect && allowNetConnect.test(options.host)
72 debug('Net connect', enabled ? '' : 'not', 'enabled for', options.host)
73 return enabled
74}
75
76/**
77 * Disable all real requests.
78 * @public
79 * @example
80 * nock.disableNetConnect();
81 */
82function disableNetConnect() {
83 allowNetConnect = undefined
84}
85
86function isOn() {
87 return !isOff()
88}
89
90function isOff() {
91 return process.env.NOCK_OFF === 'true'
92}
93
94function addInterceptor(key, interceptor, scope, scopeOptions, host) {
95 if (!(key in allInterceptors)) {
96 allInterceptors[key] = { key, interceptors: [] }
97 }
98 interceptor.__nock_scope = scope
99
100 // We need scope's key and scope options for scope filtering function (if defined)
101 interceptor.__nock_scopeKey = key
102 interceptor.__nock_scopeOptions = scopeOptions
103 // We need scope's host for setting correct request headers for filtered scopes.
104 interceptor.__nock_scopeHost = host
105 interceptor.interceptionCounter = 0
106
107 if (scopeOptions.allowUnmocked) allInterceptors[key].allowUnmocked = true
108
109 allInterceptors[key].interceptors.push(interceptor)
110}
111
112function remove(interceptor) {
113 if (interceptor.__nock_scope.shouldPersist() || --interceptor.counter > 0) {
114 return
115 }
116
117 const { basePath } = interceptor
118 const interceptors =
119 (allInterceptors[basePath] && allInterceptors[basePath].interceptors) || []
120
121 // TODO: There is a clearer way to write that we want to delete the first
122 // matching instance. I'm also not sure why we couldn't delete _all_
123 // matching instances.
124 interceptors.some(function (thisInterceptor, i) {
125 return thisInterceptor === interceptor ? interceptors.splice(i, 1) : false
126 })
127}
128
129function removeAll() {
130 Object.keys(allInterceptors).forEach(function (key) {
131 allInterceptors[key].interceptors.forEach(function (interceptor) {
132 interceptor.scope.keyedInterceptors = {}
133 })
134 })
135 allInterceptors = {}
136}
137
138/**
139 * Return all the Interceptors whose Scopes match against the base path of the provided options.
140 *
141 * @returns {Interceptor[]}
142 */
143function interceptorsFor(options) {
144 common.normalizeRequestOptions(options)
145
146 debug('interceptors for %j', options.host)
147
148 const basePath = `${options.proto}://${options.host}`
149
150 debug('filtering interceptors for basepath', basePath)
151
152 // First try to use filteringScope if any of the interceptors has it defined.
153 for (const { key, interceptors, allowUnmocked } of Object.values(
154 allInterceptors
155 )) {
156 for (const interceptor of interceptors) {
157 const { filteringScope } = interceptor.__nock_scopeOptions
158
159 // If scope filtering function is defined and returns a truthy value then
160 // we have to treat this as a match.
161 if (filteringScope && filteringScope(basePath)) {
162 interceptor.scope.logger('found matching scope interceptor')
163
164 // Keep the filtered scope (its key) to signal the rest of the module
165 // that this wasn't an exact but filtered match.
166 interceptors.forEach(ic => {
167 ic.__nock_filteredScope = ic.__nock_scopeKey
168 })
169 return interceptors
170 }
171 }
172
173 if (common.matchStringOrRegexp(basePath, key)) {
174 if (allowUnmocked && interceptors.length === 0) {
175 debug('matched base path with allowUnmocked (no matching interceptors)')
176 return [
177 {
178 options: { allowUnmocked: true },
179 matchOrigin() {
180 return false
181 },
182 },
183 ]
184 } else {
185 debug(
186 `matched base path (${interceptors.length} interceptor${
187 interceptors.length > 1 ? 's' : ''
188 })`
189 )
190 return interceptors
191 }
192 }
193 }
194
195 return undefined
196}
197
198function removeInterceptor(options) {
199 // Lazily import to avoid circular imports.
200 const Interceptor = require('./interceptor')
201
202 let baseUrl, key, method, proto
203 if (options instanceof Interceptor) {
204 baseUrl = options.basePath
205 key = options._key
206 } else {
207 proto = options.proto ? options.proto : 'http'
208
209 common.normalizeRequestOptions(options)
210 baseUrl = `${proto}://${options.host}`
211 method = (options.method && options.method.toUpperCase()) || 'GET'
212 key = `${method} ${baseUrl}${options.path || '/'}`
213 }
214
215 if (
216 allInterceptors[baseUrl] &&
217 allInterceptors[baseUrl].interceptors.length > 0
218 ) {
219 for (let i = 0; i < allInterceptors[baseUrl].interceptors.length; i++) {
220 const interceptor = allInterceptors[baseUrl].interceptors[i]
221 if (interceptor._key === key) {
222 allInterceptors[baseUrl].interceptors.splice(i, 1)
223 interceptor.scope.remove(key, interceptor)
224 break
225 }
226 }
227
228 return true
229 }
230
231 return false
232}
233// Variable where we keep the ClientRequest we have overridden
234// (which might or might not be node's original http.ClientRequest)
235let originalClientRequest
236
237function ErroringClientRequest(error) {
238 http.OutgoingMessage.call(this)
239 process.nextTick(
240 function () {
241 this.emit('error', error)
242 }.bind(this)
243 )
244}
245
246inherits(ErroringClientRequest, http.ClientRequest)
247
248function overrideClientRequest() {
249 // Here's some background discussion about overriding ClientRequest:
250 // - https://github.com/nodejitsu/mock-request/issues/4
251 // - https://github.com/nock/nock/issues/26
252 // It would be good to add a comment that explains this more clearly.
253 debug('Overriding ClientRequest')
254
255 // ----- Extending http.ClientRequest
256
257 // Define the overriding client request that nock uses internally.
258 function OverriddenClientRequest(...args) {
259 const { options, callback } = common.normalizeClientRequestArgs(...args)
260
261 if (Object.keys(options).length === 0) {
262 // As weird as it is, it's possible to call `http.request` without
263 // options, and it makes a request to localhost or somesuch. We should
264 // support it too, for parity. However it doesn't work today, and fixing
265 // it seems low priority. Giving an explicit error is nicer than
266 // crashing with a weird stack trace. `http[s].request()`, nock's other
267 // client-facing entry point, makes a similar check.
268 // https://github.com/nock/nock/pull/1386
269 // https://github.com/nock/nock/pull/1440
270 throw Error(
271 'Creating a ClientRequest with empty `options` is not supported in Nock'
272 )
273 }
274
275 http.OutgoingMessage.call(this)
276
277 // Filter the interceptors per request options.
278 const interceptors = interceptorsFor(options)
279
280 if (isOn() && interceptors) {
281 debug('using', interceptors.length, 'interceptors')
282
283 // Use filtered interceptors to intercept requests.
284 // TODO: this shouldn't be a class anymore
285 // the overrider explicitly overrides methods and attrs on the request so the `assign` below should be removed.
286 const overrider = new InterceptedRequestRouter({
287 req: this,
288 options,
289 interceptors,
290 })
291 Object.assign(this, overrider)
292
293 if (callback) {
294 this.once('response', callback)
295 }
296 } else {
297 debug('falling back to original ClientRequest')
298
299 // Fallback to original ClientRequest if nock is off or the net connection is enabled.
300 if (isOff() || isEnabledForNetConnect(options)) {
301 originalClientRequest.apply(this, arguments)
302 } else {
303 common.setImmediate(
304 function () {
305 const error = new NetConnectNotAllowedError(
306 options.host,
307 options.path
308 )
309 this.emit('error', error)
310 }.bind(this)
311 )
312 }
313 }
314 }
315 inherits(OverriddenClientRequest, http.ClientRequest)
316
317 // Override the http module's request but keep the original so that we can use it and later restore it.
318 // NOTE: We only override http.ClientRequest as https module also uses it.
319 originalClientRequest = http.ClientRequest
320 http.ClientRequest = OverriddenClientRequest
321
322 debug('ClientRequest overridden')
323}
324
325function restoreOverriddenClientRequest() {
326 debug('restoring overridden ClientRequest')
327
328 // Restore the ClientRequest we have overridden.
329 if (!originalClientRequest) {
330 debug('- ClientRequest was not overridden')
331 } else {
332 http.ClientRequest = originalClientRequest
333 originalClientRequest = undefined
334
335 debug('- ClientRequest restored')
336 }
337}
338
339function isActive() {
340 // If ClientRequest has been overwritten by Nock then originalClientRequest is not undefined.
341 // This means that Nock has been activated.
342 return originalClientRequest !== undefined
343}
344
345function interceptorScopes() {
346 const nestedInterceptors = Object.values(allInterceptors).map(
347 i => i.interceptors
348 )
349 return [].concat(...nestedInterceptors).map(i => i.scope)
350}
351
352function isDone() {
353 return interceptorScopes().every(scope => scope.isDone())
354}
355
356function pendingMocks() {
357 return [].concat(...interceptorScopes().map(scope => scope.pendingMocks()))
358}
359
360function activeMocks() {
361 return [].concat(...interceptorScopes().map(scope => scope.activeMocks()))
362}
363
364function activate() {
365 if (originalClientRequest) {
366 throw new Error('Nock already active')
367 }
368
369 overrideClientRequest()
370
371 // ----- Overriding http.request and https.request:
372
373 common.overrideRequests(function (proto, overriddenRequest, args) {
374 // NOTE: overriddenRequest is already bound to its module.
375
376 const { options, callback } = common.normalizeClientRequestArgs(...args)
377
378 if (Object.keys(options).length === 0) {
379 // As weird as it is, it's possible to call `http.request` without
380 // options, and it makes a request to localhost or somesuch. We should
381 // support it too, for parity. However it doesn't work today, and fixing
382 // it seems low priority. Giving an explicit error is nicer than
383 // crashing with a weird stack trace. `new ClientRequest()`, nock's
384 // other client-facing entry point, makes a similar check.
385 // https://github.com/nock/nock/pull/1386
386 // https://github.com/nock/nock/pull/1440
387 throw Error(
388 'Making a request with empty `options` is not supported in Nock'
389 )
390 }
391
392 // The option per the docs is `protocol`. Its unclear if this line is meant to override that and is misspelled or if
393 // the intend is to explicitly keep track of which module was called using a separate name.
394 // Either way, `proto` is used as the source of truth from here on out.
395 options.proto = proto
396
397 const interceptors = interceptorsFor(options)
398
399 if (isOn() && interceptors) {
400 const matches = interceptors.some(interceptor =>
401 interceptor.matchOrigin(options)
402 )
403 const allowUnmocked = interceptors.some(
404 interceptor => interceptor.options.allowUnmocked
405 )
406
407 if (!matches && allowUnmocked) {
408 let req
409 if (proto === 'https') {
410 const { ClientRequest } = http
411 http.ClientRequest = originalClientRequest
412 req = overriddenRequest(options, callback)
413 http.ClientRequest = ClientRequest
414 } else {
415 req = overriddenRequest(options, callback)
416 }
417 globalEmitter.emit('no match', req)
418 return req
419 }
420
421 // NOTE: Since we already overrode the http.ClientRequest we are in fact constructing
422 // our own OverriddenClientRequest.
423 return new http.ClientRequest(options, callback)
424 } else {
425 globalEmitter.emit('no match', options)
426 if (isOff() || isEnabledForNetConnect(options)) {
427 return overriddenRequest(options, callback)
428 } else {
429 const error = new NetConnectNotAllowedError(options.host, options.path)
430 return new ErroringClientRequest(error)
431 }
432 }
433 })
434}
435
436module.exports = {
437 addInterceptor,
438 remove,
439 removeAll,
440 removeInterceptor,
441 isOn,
442 activate,
443 isActive,
444 isDone,
445 pendingMocks,
446 activeMocks,
447 enableNetConnect,
448 disableNetConnect,
449 restoreOverriddenClientRequest,
450 abortPendingRequests: common.removeAllTimers,
451}