UNPKG

13.6 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 debug('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 const overrider = new InterceptedRequestRouter({
285 req: this,
286 options,
287 interceptors,
288 })
289 Object.assign(this, overrider)
290
291 if (callback) {
292 this.once('response', callback)
293 }
294 } else {
295 debug('falling back to original ClientRequest')
296
297 // Fallback to original ClientRequest if nock is off or the net connection is enabled.
298 if (isOff() || isEnabledForNetConnect(options)) {
299 originalClientRequest.apply(this, arguments)
300 } else {
301 common.setImmediate(
302 function() {
303 const error = new NetConnectNotAllowedError(
304 options.host,
305 options.path
306 )
307 this.emit('error', error)
308 }.bind(this)
309 )
310 }
311 }
312 }
313 inherits(OverriddenClientRequest, http.ClientRequest)
314
315 // Override the http module's request but keep the original so that we can use it and later restore it.
316 // NOTE: We only override http.ClientRequest as https module also uses it.
317 originalClientRequest = http.ClientRequest
318 http.ClientRequest = OverriddenClientRequest
319
320 debug('ClientRequest overridden')
321}
322
323function restoreOverriddenClientRequest() {
324 debug('restoring overridden ClientRequest')
325
326 // Restore the ClientRequest we have overridden.
327 if (!originalClientRequest) {
328 debug('- ClientRequest was not overridden')
329 } else {
330 http.ClientRequest = originalClientRequest
331 originalClientRequest = undefined
332
333 debug('- ClientRequest restored')
334 }
335}
336
337function isActive() {
338 // If ClientRequest has been overwritten by Nock then originalClientRequest is not undefined.
339 // This means that Nock has been activated.
340 return originalClientRequest !== undefined
341}
342
343function interceptorScopes() {
344 const nestedInterceptors = Object.values(allInterceptors).map(
345 i => i.interceptors
346 )
347 return [].concat(...nestedInterceptors).map(i => i.scope)
348}
349
350function isDone() {
351 return interceptorScopes().every(scope => scope.isDone())
352}
353
354function pendingMocks() {
355 return [].concat(...interceptorScopes().map(scope => scope.pendingMocks()))
356}
357
358function activeMocks() {
359 return [].concat(...interceptorScopes().map(scope => scope.activeMocks()))
360}
361
362function activate() {
363 if (originalClientRequest) {
364 throw new Error('Nock already active')
365 }
366
367 overrideClientRequest()
368
369 // ----- Overriding http.request and https.request:
370
371 common.overrideRequests(function(proto, overriddenRequest, args) {
372 // NOTE: overriddenRequest is already bound to its module.
373
374 const { options, callback } = common.normalizeClientRequestArgs(...args)
375
376 if (Object.keys(options).length === 0) {
377 // As weird as it is, it's possible to call `http.request` without
378 // options, and it makes a request to localhost or somesuch. We should
379 // support it too, for parity. However it doesn't work today, and fixing
380 // it seems low priority. Giving an explicit error is nicer than
381 // crashing with a weird stack trace. `new ClientRequest()`, nock's
382 // other client-facing entry point, makes a similar check.
383 // https://github.com/nock/nock/pull/1386
384 // https://github.com/nock/nock/pull/1440
385 throw Error(
386 'Making a request with empty `options` is not supported in Nock'
387 )
388 }
389
390 // The option per the docs is `protocol`. Its unclear if this line is meant to override that and is misspelled or if
391 // the intend is to explicitly keep track of which module was called using a separate name.
392 // Either way, `proto` is used as the source of truth from here on out.
393 options.proto = proto
394
395 const interceptors = interceptorsFor(options)
396
397 if (isOn() && interceptors) {
398 const matches = interceptors.some(interceptor =>
399 interceptor.matchOrigin(options)
400 )
401 const allowUnmocked = interceptors.some(
402 interceptor => interceptor.options.allowUnmocked
403 )
404
405 if (!matches && allowUnmocked) {
406 let req
407 if (proto === 'https') {
408 const { ClientRequest } = http
409 http.ClientRequest = originalClientRequest
410 req = overriddenRequest(options, callback)
411 http.ClientRequest = ClientRequest
412 } else {
413 req = overriddenRequest(options, callback)
414 }
415 globalEmitter.emit('no match', req)
416 return req
417 }
418
419 // NOTE: Since we already overrode the http.ClientRequest we are in fact constructing
420 // our own OverriddenClientRequest.
421 return new http.ClientRequest(options, callback)
422 } else {
423 globalEmitter.emit('no match', options)
424 if (isOff() || isEnabledForNetConnect(options)) {
425 return overriddenRequest(options, callback)
426 } else {
427 const error = new NetConnectNotAllowedError(options.host, options.path)
428 return new ErroringClientRequest(error)
429 }
430 }
431 })
432}
433
434module.exports = {
435 addInterceptor,
436 remove,
437 removeAll,
438 removeInterceptor,
439 isOn,
440 activate,
441 isActive,
442 isDone,
443 pendingMocks,
444 activeMocks,
445 enableNetConnect,
446 disableNetConnect,
447 restoreOverriddenClientRequest,
448 abortPendingRequests: common.removeAllTimers,
449}