nock 14.0.12 → 15.0.0-beta.10

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
@@ -53,6 +53,7 @@ For instance, if a module performs HTTP requests to a CouchDB server or makes HT
53
53
  - [Request Headers Matching](#request-headers-matching)
54
54
  - [Optional Requests](#optional-requests)
55
55
  - [Allow **unmocked** requests on a mocked hostname](#allow-unmocked-requests-on-a-mocked-hostname)
56
+ - [Passthrough requests](#passthrough-requests)
56
57
  - [Expectations](#expectations)
57
58
  - [.isDone()](#isdone)
58
59
  - [.cleanAll()](#cleanall)
@@ -911,6 +912,44 @@ const scope = nock('http://my.existing.service.com', { allowUnmocked: true })
911
912
 
912
913
  > Note: When applying `{allowUnmocked: true}`, if the request is made to the real server, no interceptor is removed.
913
914
 
915
+ ### Passthrough requests
916
+
917
+ For more granular control over which requests should hit the real server, you can use `passthrough()`. While `allowUnmocked` lets through any request that doesn't match an interceptor, `passthrough()` lets you specify exactly which requests go through the HTTP stack.
918
+
919
+ ```js
920
+ const scope = nock('http://my.existing.service.com')
921
+ .get('/mock')
922
+ .reply(200, { users: ['fake'] })
923
+ .post('/live')
924
+ .passthrough()
925
+
926
+ // GET /mock => goes through nock
927
+ // POST /live => actually makes request to the server
928
+ ```
929
+
930
+ You can use `passthrough()` with query strings, header matching, and other interceptor features:
931
+
932
+ ```js
933
+ nock('http://my.existing.service.com')
934
+ .get('/api/data')
935
+ .query({ live: 'true' })
936
+ .matchHeader('authorization', /^Bearer /)
937
+ .passthrough()
938
+ ```
939
+
940
+ Like other interceptors, passthrough interceptors are consumed when matched. They work with modifiers like `times()`, `persist()`, and `optionally()`:
941
+
942
+ ```js
943
+ // Passthrough the first 3 requests, then fail
944
+ nock('http://example.com').get('/').times(3).passthrough()
945
+
946
+ // Passthrough indefinitely
947
+ nock('http://example.com').persist().get('/').passthrough()
948
+
949
+ // Don't fail scope.done() if this is never called
950
+ nock('http://example.com').get('/maybe').optionally().passthrough()
951
+ ```
952
+
914
953
  ## Expectations
915
954
 
916
955
  Every time an HTTP request is performed for a scope that is mocked, Nock expects to find a handler for it. If it doesn't, it will throw an error.
@@ -1311,7 +1350,7 @@ nock.removeInterceptor({
1311
1350
  hostname: 'localhost',
1312
1351
  path: '/mockedResource',
1313
1352
  // method defaults to `GET`
1314
- // proto defaullts to `http`
1353
+ // proto defaults to `http`
1315
1354
  })
1316
1355
  ```
1317
1356
 
@@ -1353,9 +1392,39 @@ A scope emits the following events:
1353
1392
  You can also listen for no match events like this:
1354
1393
 
1355
1394
  ```js
1356
- nock.emitter.on('no match', req => {})
1395
+ nock.emitter.on('no match', (req, interceptorResults) => {
1396
+ console.log('Request did not match any interceptors:', req.url)
1397
+
1398
+ if (interceptorResults && interceptorResults.length > 0) {
1399
+ interceptorResults.forEach(({ interceptor, reasons }) => {
1400
+ console.log(
1401
+ 'Interceptor:',
1402
+ interceptor.method,
1403
+ interceptor.basePath + interceptor.path,
1404
+ )
1405
+ console.log('Reasons:', reasons)
1406
+ })
1407
+ }
1408
+ })
1357
1409
  ```
1358
1410
 
1411
+ The callback receives two parameters:
1412
+
1413
+ - `req` - The request object that didn't match
1414
+ - `interceptorResults` - An array of objects containing detailed information about each interceptor that was tested.
1415
+ Each object contains:
1416
+ - `interceptor` - The interceptor that was tested against the request
1417
+ - `reasons` - An array of strings describing why the request didn't match this interceptor
1418
+ > ⚠️ **Experimental**: The structure and format of the detailed mismatch information may change in future versions as we gather user feedback and refine the API. The feature itself is stable and ready for use and we're seeking community input on the API design before marking it stable.
1419
+
1420
+ Common mismatch reasons include:
1421
+
1422
+ - **Method mismatch**: `"Method mismatch: expected GET, got POST"`
1423
+ - **Path mismatch**: `"Path mismatch: expected /api/users, got /api/posts"`
1424
+ - **Header mismatch**: `"Header mismatch: expected authorization to match Bearer token, got null"`
1425
+ - **Body mismatch**: `"Body mismatch: expected "expected body", got actual body"`
1426
+ - **Query mismatch**: `"query matching failed"`
1427
+
1359
1428
  ## Nock Back
1360
1429
 
1361
1430
  Fixture recording support and playback.
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 { getGetRequestBody } = 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
+ getGetRequestBody,
45
47
  })
46
48
 
47
49
  // We always activate Nock on import, overriding the globals.
package/lib/back.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
- const assert = require('assert')
3
+ const fs = require('node:fs')
4
+ const assert = require('node:assert')
4
5
  const recorder = require('./recorder')
5
6
  const {
6
7
  activate,
@@ -10,19 +11,11 @@ const {
10
11
  } = require('./intercept')
11
12
  const { loadDefs, define } = require('./scope')
12
13
  const { back: debug } = require('./debug')
13
- const { format } = require('util')
14
- const path = require('path')
14
+ const { format } = require('node:util')
15
+ const path = require('node:path')
15
16
 
16
17
  let _mode = null
17
18
 
18
- let fs
19
-
20
- try {
21
- fs = require('fs')
22
- } catch (err) {
23
- // do nothing, probably in browser
24
- }
25
-
26
19
  /**
27
20
  * nock the current function with the fixture given
28
21
  *
@@ -198,7 +191,7 @@ const update = {
198
191
  return context
199
192
  },
200
193
 
201
- finish: function (fixture, options, context) {
194
+ finish: function (fixture, options) {
202
195
  let outputs = recorder.outputs()
203
196
 
204
197
  if (typeof options.afterRecord === 'function') {
@@ -271,7 +264,7 @@ function load(fixture, options) {
271
264
  return context
272
265
  }
273
266
 
274
- function removeFixture(fixture, options) {
267
+ function removeFixture(fixture) {
275
268
  const context = {
276
269
  scopes: [],
277
270
  assertScopesFinished: function () {},
package/lib/common.js CHANGED
@@ -1,10 +1,9 @@
1
1
  'use strict'
2
2
 
3
3
  const { common: debug } = require('./debug')
4
- const timers = require('timers')
5
- const url = require('url')
6
- const util = require('util')
7
- const http = require('http')
4
+ const timers = require('node:timers')
5
+ const util = require('node:util')
6
+ const zlib = require('node: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
 
@@ -320,7 +326,7 @@ function forEachHeader(rawHeaders, callback) {
320
326
  function percentDecode(str) {
321
327
  try {
322
328
  return decodeURIComponent(str.replace(/\+/g, ' '))
323
- } catch (e) {
329
+ } catch {
324
330
  return str
325
331
  }
326
332
  }
@@ -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
  *
@@ -526,7 +473,6 @@ const wrapTimer =
526
473
  (callback, ...timerArgs) => {
527
474
  const cb = (...callbackArgs) => {
528
475
  try {
529
- // eslint-disable-next-line n/no-callback-literal
530
476
  callback(...callbackArgs)
531
477
  } finally {
532
478
  ids.delete(id)
@@ -551,59 +497,6 @@ function removeAllTimers() {
551
497
  clearTimer(clearImmediate, immediates)
552
498
  }
553
499
 
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
500
  /**
608
501
  * Returns true if the given value is a plain object and not an Array.
609
502
  * @param {*} value
@@ -682,8 +575,6 @@ const expand = input => {
682
575
  } else {
683
576
  resultPtr[part] = {}
684
577
  }
685
- } else if (typeof resultPtr[part] !== 'object') {
686
- return undefined
687
578
  }
688
579
  resultPtr = resultPtr[part]
689
580
  }
@@ -692,6 +583,40 @@ const expand = input => {
692
583
  return result
693
584
  }
694
585
 
586
+ /**
587
+ * @param {ArrayBuffer} buffer
588
+ * @param {string} contentEncoding
589
+ */
590
+ function decompressRequestBody(buffer, contentEncoding) {
591
+ const encodings = contentEncoding
592
+ .toLowerCase()
593
+ .split(',')
594
+ .map(coding => coding.trim())
595
+
596
+ for (const encoding of encodings) {
597
+ if (encoding === 'gzip') {
598
+ return zlib.gunzipSync(buffer)
599
+ } else if (encoding === 'deflate') {
600
+ return zlib.inflateSync(buffer)
601
+ } else if (encoding === 'br') {
602
+ return zlib.brotliDecompressSync(buffer)
603
+ }
604
+ }
605
+
606
+ return buffer
607
+ }
608
+
609
+ /**
610
+ * @param {Headers} headers
611
+ */
612
+ function convertHeadersToRaw(headers) {
613
+ const rawHeaders = []
614
+ for (const [name, value] of headers.entries()) {
615
+ rawHeaders.push(name, value)
616
+ }
617
+ return rawHeaders
618
+ }
619
+
695
620
  module.exports = {
696
621
  contentEncoding,
697
622
  dataEqual,
@@ -706,11 +631,9 @@ module.exports = {
706
631
  isContentEncoded,
707
632
  isJSONContent,
708
633
  isPlainObject,
709
- isRequestDestroyed,
710
634
  isStream,
711
635
  isUtf8Representable,
712
636
  matchStringOrRegexp,
713
- normalizeClientRequestArgs,
714
637
  normalizeOrigin,
715
638
  normalizeRequestOptions,
716
639
  percentDecode,
@@ -719,5 +642,6 @@ module.exports = {
719
642
  setImmediate,
720
643
  setTimeout,
721
644
  stringifyRequest,
722
- convertFetchRequestToClientRequest,
645
+ decompressRequestBody,
646
+ convertHeadersToRaw,
723
647
  }
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { STATUS_CODES } = require('http')
3
+ const { STATUS_CODES } = require('node:http')
4
4
 
5
5
  /**
6
6
  * Creates a Fetch API `Response` instance from the given
package/lib/debug.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { debuglog } = require('util')
3
+ const { debuglog } = require('node:util')
4
4
 
5
5
  module.exports.back = debuglog('nock:back')
6
6
  module.exports.common = debuglog('nock:common')
@@ -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,125 @@
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 matchResults = []
38
+ const matchedInterceptor = interceptors.find(i => {
39
+ const reasons = i.match(request, requestBodyString)
40
+ if (reasons.length > 0) {
41
+ matchResults.push({ interceptor: i, reasons })
42
+ return false
43
+ } else {
44
+ return true
45
+ }
46
+ })
47
+
48
+ if (matchedInterceptor) {
49
+ matchedInterceptor.scope.logger(
50
+ 'interceptor identified, starting mocking',
51
+ )
52
+
53
+ matchedInterceptor.markConsumed()
54
+
55
+ if (matchedInterceptor.isPassthrough) {
56
+ return
57
+ }
58
+
59
+ const response = await playbackInterceptor({
60
+ decompressedRequest: request,
61
+ requestBodyString,
62
+ interceptor: matchedInterceptor,
63
+ requestBodyIsUtf8Representable,
64
+ })
65
+
66
+ return response
67
+ } else {
68
+ globalEmitter.emit(
69
+ 'no match',
70
+ request,
71
+ matchResults.sort((a, b) => a.reasons.length - b.reasons.length),
72
+ )
73
+
74
+ // Try to find a hostname match that allows unmocked.
75
+ const allowUnmocked = interceptors.some(
76
+ i => i.matchHostName(url.hostname) && i.options.allowUnmocked,
77
+ )
78
+
79
+ if (!allowUnmocked) {
80
+ const body = JSON.stringify({
81
+ code: 'ERR_NOCK_NO_MATCH',
82
+ message: `Nock: No match for request ${common.stringifyRequest(request, requestBodyString)}`,
83
+ })
84
+ return new Response(body, {
85
+ status: 501,
86
+ headers: { 'Content-Type': 'application/json' },
87
+ })
88
+ }
89
+ }
90
+ }
91
+ } else {
92
+ globalEmitter.emit('no match', request)
93
+ // Remove http(s):// for backward compatibility until we decide this Error.
94
+ const normalizedUrl = common
95
+ .normalizeOrigin(url)
96
+ .replace(`${url.protocol}//`, '')
97
+ if (isOn() && !isEnabledForNetConnect(normalizedUrl)) {
98
+ throw new NetConnectNotAllowedError(normalizedUrl, url.pathname)
99
+ }
100
+ }
101
+ }
102
+
103
+ /**
104
+ * @name NetConnectNotAllowedError
105
+ * @private
106
+ * @desc Error trying to make a connection when disabled external access.
107
+ * @class
108
+ * @example
109
+ * nock.disableNetConnect();
110
+ * http.get('http://zombo.com');
111
+ * // throw NetConnectNotAllowedError
112
+ */
113
+ function NetConnectNotAllowedError(host, path) {
114
+ Error.call(this)
115
+
116
+ this.name = 'NetConnectNotAllowedError'
117
+ this.code = 'ENETUNREACH'
118
+ this.message = `Nock: Disallowed net connect for "${host}${path}"`
119
+
120
+ Error.captureStackTrace(this, this.constructor)
121
+ }
122
+
123
+ inherits(NetConnectNotAllowedError, Error)
124
+
125
+ module.exports = handleRequest