nock 14.0.4 → 15.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1308,6 +1308,8 @@ Examples:
1308
1308
  nock.removeInterceptor({
1309
1309
  hostname: 'localhost',
1310
1310
  path: '/mockedResource',
1311
+ // method defaults to `GET`
1312
+ // proto defaults to `http`
1311
1313
  })
1312
1314
  ```
1313
1315
 
package/index.js CHANGED
@@ -17,6 +17,7 @@ const {
17
17
  } = require('./lib/intercept')
18
18
  const recorder = require('./lib/recorder')
19
19
  const { Scope, load, loadDefs, define } = require('./lib/scope')
20
+ const { getDecompressedGetBody } = require('./lib/utils/node')
20
21
 
21
22
  module.exports = (basePath, options) => new Scope(basePath, options)
22
23
 
@@ -42,6 +43,7 @@ Object.assign(module.exports, {
42
43
  },
43
44
  restore: recorder.restore,
44
45
  back,
46
+ getDecompressedGetBody,
45
47
  })
46
48
 
47
49
  // We always activate Nock on import, overriding the globals.
package/lib/common.js CHANGED
@@ -2,9 +2,8 @@
2
2
 
3
3
  const { common: debug } = require('./debug')
4
4
  const timers = require('timers')
5
- const url = require('url')
6
5
  const util = require('util')
7
- const http = require('http')
6
+ const zlib = require('zlib')
8
7
 
9
8
  /**
10
9
  * Normalizes the request options so that it always has `host` property.
@@ -42,49 +41,48 @@ function normalizeRequestOptions(options) {
42
41
  * Returns true if the data contained in buffer can be reconstructed
43
42
  * from its utf8 representation.
44
43
  *
45
- * @param {Object} buffer - a Buffer object
44
+ * @param {ArrayBuffer} buffer
46
45
  * @returns {boolean}
47
46
  */
48
47
  function isUtf8Representable(buffer) {
49
- const utfEncodedBuffer = buffer.toString('utf8')
50
- const reconstructedBuffer = Buffer.from(utfEncodedBuffer, 'utf8')
51
- return reconstructedBuffer.equals(buffer)
48
+ try {
49
+ new TextDecoder('utf8', { fatal: true }).decode(buffer)
50
+ return true
51
+ } catch {
52
+ return false
53
+ }
52
54
  }
53
55
 
54
56
  /**
55
57
  * In WHATWG URL vernacular, this returns the origin portion of a URL.
56
58
  * However, the port is not included if it's standard and not already present on the host.
59
+ *
60
+ * @param {URL} url
57
61
  */
58
- function normalizeOrigin(proto, host, port) {
59
- const hostHasPort = host.includes(':')
60
- const portIsStandard =
61
- (proto === 'http' && (port === 80 || port === '80')) ||
62
- (proto === 'https' && (port === 443 || port === '443'))
63
- const portStr = hostHasPort || portIsStandard ? '' : `:${port}`
64
-
65
- return `${proto}://${host}${portStr}`
62
+ function normalizeOrigin(url) {
63
+ // Remove brackets from hostname if IPV6
64
+ const normalizedOrigin = url.hostname.startsWith('[')
65
+ ? `${url.protocol}//${url.hostname.slice(1, -1)}${url.port ? `:${url.port}` : ''}`
66
+ : url.origin
67
+ if (url.port) {
68
+ return normalizedOrigin
69
+ } else {
70
+ return normalizedOrigin + (url.protocol === 'http:' ? ':80' : ':443')
71
+ }
66
72
  }
67
73
 
68
74
  /**
69
75
  * Get high level information about request as string
70
- * @param {Object} options
71
- * @param {string} options.method
72
- * @param {number|string} options.port
73
- * @param {string} options.proto Set internally. always http or https
74
- * @param {string} options.hostname
75
- * @param {string} options.path
76
- * @param {Object} options.headers
76
+ * @param {Request} request
77
77
  * @param {string} body
78
- * @return {string}
79
78
  */
80
- function stringifyRequest(options, body) {
81
- const { method = 'GET', path = '', port } = options
82
- const origin = normalizeOrigin(options.proto, options.hostname, port)
79
+ function stringifyRequest(request, body) {
80
+ const url = new URL(request.url)
83
81
 
84
82
  const log = {
85
- method,
86
- url: `${origin}${path}`,
87
- headers: options.headers,
83
+ method: request.method,
84
+ url: `${url.origin}${url.pathname}`,
85
+ headers: Object.fromEntries(request.headers.entries()),
88
86
  }
89
87
 
90
88
  if (body) {
@@ -99,14 +97,22 @@ function isContentEncoded(headers) {
99
97
  return typeof contentEncoding === 'string' && contentEncoding !== ''
100
98
  }
101
99
 
100
+ /**
101
+ * @param {Headers} headers
102
+ * @param {'gzip' | 'deflate'} encoder
103
+ * @returns
104
+ */
102
105
  function contentEncoding(headers, encoder) {
103
- const contentEncoding = headers['content-encoding']
104
- return contentEncoding !== undefined && contentEncoding.toString() === encoder
106
+ const contentEncoding = headers.get('content-encoding')
107
+ return contentEncoding?.toString() === encoder
105
108
  }
106
109
 
110
+ /**
111
+ * @param {Headers} headers
112
+ */
107
113
  function isJSONContent(headers) {
108
114
  // https://tools.ietf.org/html/rfc8259
109
- const contentType = String(headers['content-type'] || '').toLowerCase()
115
+ const contentType = String(headers.get('content-type') || '').toLowerCase()
110
116
  return contentType.startsWith('application/json')
111
117
  }
112
118
 
@@ -409,65 +415,6 @@ function isStream(obj) {
409
415
  )
410
416
  }
411
417
 
412
- /**
413
- * Converts the arguments from the various signatures of http[s].request into a standard
414
- * options object and an optional callback function.
415
- *
416
- * https://nodejs.org/api/http.html#http_http_request_url_options_callback
417
- *
418
- * Taken from the beginning of the native `ClientRequest`.
419
- * https://github.com/nodejs/node/blob/908292cf1f551c614a733d858528ffb13fb3a524/lib/_http_client.js#L68
420
- */
421
- function normalizeClientRequestArgs(input, options, cb) {
422
- if (typeof input === 'string') {
423
- input = urlToOptions(new url.URL(input))
424
- } else if (input instanceof url.URL) {
425
- input = urlToOptions(input)
426
- } else {
427
- cb = options
428
- options = input
429
- input = null
430
- }
431
-
432
- if (typeof options === 'function') {
433
- cb = options
434
- options = input || {}
435
- } else {
436
- options = Object.assign(input || {}, options)
437
- }
438
-
439
- return { options, callback: cb }
440
- }
441
-
442
- /**
443
- * Utility function that converts a URL object into an ordinary
444
- * options object as expected by the http.request and https.request APIs.
445
- *
446
- * This was copied from Node's source
447
- * https://github.com/nodejs/node/blob/908292cf1f551c614a733d858528ffb13fb3a524/lib/internal/url.js#L1257
448
- */
449
- function urlToOptions(url) {
450
- const options = {
451
- protocol: url.protocol,
452
- hostname:
453
- typeof url.hostname === 'string' && url.hostname.startsWith('[')
454
- ? url.hostname.slice(1, -1)
455
- : url.hostname,
456
- hash: url.hash,
457
- search: url.search,
458
- pathname: url.pathname,
459
- path: `${url.pathname}${url.search || ''}`,
460
- href: url.href,
461
- }
462
- if (url.port !== '') {
463
- options.port = Number(url.port)
464
- }
465
- if (url.username || url.password) {
466
- options.auth = `${url.username}:${url.password}`
467
- }
468
- return options
469
- }
470
-
471
418
  /**
472
419
  * Determines if request data matches the expected schema.
473
420
  *
@@ -551,59 +498,6 @@ function removeAllTimers() {
551
498
  clearTimer(clearImmediate, immediates)
552
499
  }
553
500
 
554
- /**
555
- * Check if the Client Request has been cancelled.
556
- *
557
- * Until Node 14 is the minimum, we need to look at both flags to see if the request has been cancelled.
558
- * The two flags have the same purpose, but the Node maintainers are migrating from `abort(ed)` to
559
- * `destroy(ed)` terminology, to be more consistent with `stream.Writable`.
560
- * In Node 14.x+, Calling `abort()` will set both `aborted` and `destroyed` to true, however,
561
- * calling `destroy()` will only set `destroyed` to true.
562
- * Falling back on checking if the socket is destroyed to cover the case of Node <14.x where
563
- * `destroy()` is called, but `destroyed` is undefined.
564
- *
565
- * Node Client Request history:
566
- * - `request.abort()`: Added in: v0.3.8, Deprecated since: v14.1.0, v13.14.0
567
- * - `request.aborted`: Added in: v0.11.14, Became a boolean instead of a timestamp: v11.0.0, Not deprecated (yet)
568
- * - `request.destroy()`: Added in: v0.3.0
569
- * - `request.destroyed`: Added in: v14.1.0, v13.14.0
570
- *
571
- * @param {ClientRequest} req
572
- * @returns {boolean}
573
- */
574
- function isRequestDestroyed(req) {
575
- return !!(
576
- req.destroyed === true ||
577
- req.aborted ||
578
- (req.socket && req.socket.destroyed)
579
- )
580
- }
581
-
582
- /**
583
- * @param {Request} request
584
- */
585
- function convertFetchRequestToClientRequest(request) {
586
- const url = new URL(request.url)
587
- const options = {
588
- ...urlToOptions(url),
589
- method: request.method,
590
- host: url.hostname,
591
- port: url.port || (url.protocol === 'https:' ? 443 : 80),
592
- path: url.pathname + url.search,
593
- proto: url.protocol.slice(0, -1),
594
- headers: Object.fromEntries(request.headers.entries()),
595
- }
596
-
597
- // By default, Node adds a host header, but for maximum backward compatibility, we are now removing it.
598
- // However, we need to consider leaving the header and fixing the tests.
599
- if (options.headers.host === options.host) {
600
- const { host, ...restHeaders } = options.headers
601
- options.headers = restHeaders
602
- }
603
-
604
- return new http.ClientRequest(options)
605
- }
606
-
607
501
  /**
608
502
  * Returns true if the given value is a plain object and not an Array.
609
503
  * @param {*} value
@@ -690,6 +584,40 @@ const expand = input => {
690
584
  return result
691
585
  }
692
586
 
587
+ /**
588
+ * @param {ArrayBuffer} buffer
589
+ * @param {string} contentEncoding
590
+ */
591
+ function decompressRequestBody(buffer, contentEncoding) {
592
+ const encodings = contentEncoding
593
+ .toLowerCase()
594
+ .split(',')
595
+ .map(coding => coding.trim())
596
+
597
+ for (const encoding of encodings) {
598
+ if (encoding === 'gzip') {
599
+ return zlib.gunzipSync(buffer)
600
+ } else if (encoding === 'deflate') {
601
+ return zlib.inflateSync(buffer)
602
+ } else if (encoding === 'br') {
603
+ return zlib.brotliDecompressSync(buffer)
604
+ }
605
+ }
606
+
607
+ return buffer
608
+ }
609
+
610
+ /**
611
+ * @param {Headers} headers
612
+ */
613
+ function convertHeadersToRaw(headers) {
614
+ const rawHeaders = []
615
+ for (const [name, value] of headers.entries()) {
616
+ rawHeaders.push(name, value)
617
+ }
618
+ return rawHeaders
619
+ }
620
+
693
621
  module.exports = {
694
622
  contentEncoding,
695
623
  dataEqual,
@@ -704,11 +632,9 @@ module.exports = {
704
632
  isContentEncoded,
705
633
  isJSONContent,
706
634
  isPlainObject,
707
- isRequestDestroyed,
708
635
  isStream,
709
636
  isUtf8Representable,
710
637
  matchStringOrRegexp,
711
- normalizeClientRequestArgs,
712
638
  normalizeOrigin,
713
639
  normalizeRequestOptions,
714
640
  percentDecode,
@@ -717,5 +643,6 @@ module.exports = {
717
643
  setImmediate,
718
644
  setTimeout,
719
645
  stringifyRequest,
720
- convertFetchRequestToClientRequest,
646
+ decompressRequestBody,
647
+ convertHeadersToRaw,
721
648
  }
@@ -1,5 +1,5 @@
1
1
  'use strict'
2
2
 
3
- const { EventEmitter } = require('events')
3
+ const { EventEmitter } = require('node:events')
4
4
 
5
5
  module.exports = new EventEmitter()
@@ -0,0 +1,107 @@
1
+ 'use strict'
2
+ const { inherits } = require('node:util')
3
+ const globalEmitter = require('./global_emitter')
4
+ const common = require('./common')
5
+ const { playbackInterceptor } = require('./playback_interceptor')
6
+
7
+ /**
8
+ * @param {Request} request
9
+ */
10
+ async function handleRequest(request) {
11
+ const {
12
+ interceptorsFor,
13
+ isOn,
14
+ isEnabledForNetConnect,
15
+ } = require('./intercept')
16
+ const url = new URL(request.url)
17
+ const interceptors = interceptorsFor(url)
18
+
19
+ if (isOn() && interceptors) {
20
+ const matches = interceptors.some(interceptor =>
21
+ interceptor.matchOrigin(request),
22
+ )
23
+ const allowUnmocked = interceptors.some(
24
+ interceptor => interceptor.options.allowUnmocked,
25
+ )
26
+ if (!matches && allowUnmocked) {
27
+ globalEmitter.emit('no match', request)
28
+ } else {
29
+ const requestBodyBuffer = await request.clone().arrayBuffer()
30
+ // When request body is a binary buffer we internally use in its hexadecimal representation.
31
+ const requestBodyIsUtf8Representable =
32
+ common.isUtf8Representable(requestBodyBuffer)
33
+ const requestBodyString = Buffer.from(requestBodyBuffer).toString(
34
+ requestBodyIsUtf8Representable ? 'utf8' : 'hex',
35
+ )
36
+
37
+ const matchedInterceptor = interceptors.find(i =>
38
+ i.match(request, requestBodyString),
39
+ )
40
+
41
+ if (matchedInterceptor) {
42
+ matchedInterceptor.scope.logger(
43
+ 'interceptor identified, starting mocking',
44
+ )
45
+
46
+ matchedInterceptor.markConsumed()
47
+
48
+ const response = await playbackInterceptor({
49
+ decompressedRequest: request,
50
+ requestBodyString,
51
+ interceptor: matchedInterceptor,
52
+ requestBodyIsUtf8Representable,
53
+ })
54
+
55
+ return response
56
+ } else {
57
+ globalEmitter.emit('no match', request)
58
+
59
+ // Try to find a hostname match that allows unmocked.
60
+ const allowUnmocked = interceptors.some(
61
+ i => i.matchHostName(url.hostname) && i.options.allowUnmocked,
62
+ )
63
+
64
+ if (!allowUnmocked) {
65
+ const reqStr = common.stringifyRequest(request, requestBodyString)
66
+ const err = new Error(`Nock: No match for request ${reqStr}`)
67
+ err.code = 'ERR_NOCK_NO_MATCH'
68
+ err.statusCode = err.status = 404
69
+ throw err
70
+ }
71
+ }
72
+ }
73
+ } else {
74
+ globalEmitter.emit('no match', request)
75
+ // Remove http(s):// for backward compatibility until we decide this Error.
76
+ const normalizedUrl = common
77
+ .normalizeOrigin(url)
78
+ .replace(`${url.protocol}//`, '')
79
+ if (isOn() && !isEnabledForNetConnect(normalizedUrl)) {
80
+ throw new NetConnectNotAllowedError(normalizedUrl, url.pathname)
81
+ }
82
+ }
83
+ }
84
+
85
+ /**
86
+ * @name NetConnectNotAllowedError
87
+ * @private
88
+ * @desc Error trying to make a connection when disabled external access.
89
+ * @class
90
+ * @example
91
+ * nock.disableNetConnect();
92
+ * http.get('http://zombo.com');
93
+ * // throw NetConnectNotAllowedError
94
+ */
95
+ function NetConnectNotAllowedError(host, path) {
96
+ Error.call(this)
97
+
98
+ this.name = 'NetConnectNotAllowedError'
99
+ this.code = 'ENETUNREACH'
100
+ this.message = `Nock: Disallowed net connect for "${host}${path}"`
101
+
102
+ Error.captureStackTrace(this, this.constructor)
103
+ }
104
+
105
+ inherits(NetConnectNotAllowedError, Error)
106
+
107
+ module.exports = handleRequest