UNPKG

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