nock 14.0.0-beta.8 → 14.0.0

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
@@ -7,14 +7,6 @@
7
7
 
8
8
  [npmjs]: https://www.npmjs.com/package/nock
9
9
 
10
- > **Notice**
11
- >
12
- > We have introduced experimental support for fetch. Please share your feedback with us. You can install it by:
13
- >
14
- > ```
15
- > npm install --save-dev nock@beta
16
- > ```
17
-
18
10
  HTTP server mocking and expectations library for Node.js
19
11
 
20
12
  Nock can be used to test modules that perform HTTP requests in isolation.
@@ -692,46 +684,17 @@ You are able to specify the number of milliseconds that your reply should be del
692
684
  ```js
693
685
  nock('http://my.server.com')
694
686
  .get('/')
695
- .delay(2000) // 2 seconds delay will be applied to the response header.
687
+ .delay(2000) // 2 seconds delay will be applied to the response body.
696
688
  .reply(200, '<html></html>')
697
689
  ```
698
690
 
699
- `delay(1000)` is an alias for `delayConnection(1000).delayBody(0)`
700
- `delay({ head: 1000, body: 2000 })` is an alias for `delayConnection(1000).delayBody(2000)`
701
- Both of which are covered in detail below.
702
-
703
691
  #### Delay the connection
704
692
 
705
- You are able to specify the number of milliseconds that your connection should be idle before it starts to receive the response.
706
-
707
- To simulate a socket timeout, provide a larger value than the timeout setting on the request.
708
-
709
- ```js
710
- nock('http://my.server.com')
711
- .get('/')
712
- .delayConnection(2000) // 2 seconds
713
- .reply(200, '<html></html>')
714
-
715
- req = http.request('http://my.server.com', { timeout: 1000 })
716
- ```
717
-
718
- Nock emits timeout events almost immediately by comparing the requested connection delay to the timeout parameter passed to `http.request()` or `http.ClientRequest#setTimeout()`.
719
- This allows you to test timeouts without using fake timers or slowing down your tests.
720
- If the client chooses to _not_ take an action (e.g. abort the request), the request and response will continue on as normal, after real clock time has passed.
721
-
722
- ##### Technical Details
723
-
724
- Following the `'finish'` event being emitted by `ClientRequest`, Nock will wait for the next event loop iteration before checking if the request has been aborted.
725
- At this point, any connection delay value is compared against any request timeout setting and a [`'timeout'`](https://nodejs.org/api/http.html#http_event_timeout) is emitted when appropriate from the socket and the request objects.
726
- A Node timeout timer is then registered with any connection delay value to delay real time before checking again if the request has been aborted and the [`'response'`](http://nodejs.org/api/http.html#http_event_response) is emitted by the request.
727
-
728
- A similar method, `.socketDelay()` was removed in version 13. It was thought that having two methods so subtlety similar was confusing.
729
- The discussion can be found at https://github.com/nock/nock/pull/1974.
693
+ The `delayConnection` method’s behavior of emitting quick timeout events when the connection delay exceeds the request timeout is now deprecated. Please use the `delay` function instead.
730
694
 
731
695
  #### Delay the response body
732
696
 
733
- You are able to specify the number of milliseconds that the response body should be delayed.
734
- This is the time between the headers being received and the body starting to be received.
697
+ The `delayBody` is now deprecated. Please use the `delay` function instead.
735
698
 
736
699
  ```js
737
700
  nock('http://my.server.com')
package/lib/common.js CHANGED
@@ -518,32 +518,36 @@ function deepEqual(expected, actual) {
518
518
  return expected === actual
519
519
  }
520
520
 
521
- const timeouts = []
522
- const intervals = []
523
- const immediates = []
521
+ const timeouts = new Set()
522
+ const immediates = new Set()
524
523
 
525
524
  const wrapTimer =
526
525
  (timer, ids) =>
527
- (...args) => {
528
- const id = timer(...args)
529
- ids.push(id)
526
+ (callback, ...timerArgs) => {
527
+ const cb = (...callbackArgs) => {
528
+ try {
529
+ // eslint-disable-next-line n/no-callback-literal
530
+ callback(...callbackArgs)
531
+ } finally {
532
+ ids.delete(id)
533
+ }
534
+ }
535
+ const id = timer(cb, ...timerArgs)
536
+ ids.add(id)
530
537
  return id
531
538
  }
532
539
 
533
540
  const setTimeout = wrapTimer(timers.setTimeout, timeouts)
534
- const setInterval = wrapTimer(timers.setInterval, intervals)
535
541
  const setImmediate = wrapTimer(timers.setImmediate, immediates)
536
542
 
537
543
  function clearTimer(clear, ids) {
538
- while (ids.length) {
539
- clear(ids.shift())
540
- }
544
+ ids.forEach(clear)
545
+ ids.clear()
541
546
  }
542
547
 
543
548
  function removeAllTimers() {
544
549
  debug('remove all timers')
545
550
  clearTimer(clearTimeout, timeouts)
546
- clearTimer(clearInterval, intervals)
547
551
  clearTimer(clearImmediate, immediates)
548
552
  }
549
553
 
@@ -576,10 +580,10 @@ function isRequestDestroyed(req) {
576
580
  }
577
581
 
578
582
  /**
579
- * @param {Request} request
583
+ * @param {Request} request
580
584
  */
581
585
  function convertFetchRequestToClientRequest(request) {
582
- const url = new URL(request.url);
586
+ const url = new URL(request.url)
583
587
  const options = {
584
588
  ...urlToOptions(url),
585
589
  method: request.method,
@@ -587,9 +591,9 @@ function convertFetchRequestToClientRequest(request) {
587
591
  port: url.port || (url.protocol === 'https:' ? 443 : 80),
588
592
  path: url.pathname + url.search,
589
593
  proto: url.protocol.slice(0, -1),
590
- headers: Object.fromEntries(request.headers.entries())
591
- };
592
-
594
+ headers: Object.fromEntries(request.headers.entries()),
595
+ }
596
+
593
597
  // By default, Node adds a host header, but for maximum backward compatibility, we are now removing it.
594
598
  // However, we need to consider leaving the header and fixing the tests.
595
599
  if (options.headers.host === options.host) {
@@ -597,7 +601,7 @@ function convertFetchRequestToClientRequest(request) {
597
601
  options.headers = restHeaders
598
602
  }
599
603
 
600
- return new http.ClientRequest(options);
604
+ return new http.ClientRequest(options)
601
605
  }
602
606
 
603
607
  /**
@@ -711,7 +715,6 @@ module.exports = {
711
715
  percentEncode,
712
716
  removeAllTimers,
713
717
  setImmediate,
714
- setInterval,
715
718
  setTimeout,
716
719
  stringifyRequest,
717
720
  convertFetchRequestToClientRequest,
@@ -15,23 +15,25 @@ const { STATUS_CODES } = require('http')
15
15
  const responseStatusCodesWithoutBody = [204, 205, 304]
16
16
 
17
17
  /**
18
- * @param {import('http').IncomingMessage} message
18
+ * @param {import('http').IncomingMessage} message
19
+ * @param {AbortSignal} signal
19
20
  */
20
- function createResponse(message) {
21
+ function createResponse(message, signal) {
21
22
  const responseBodyOrNull = responseStatusCodesWithoutBody.includes(
22
- message.statusCode || 200
23
+ message.statusCode || 200,
23
24
  )
24
25
  ? null
25
26
  : new ReadableStream({
26
- start(controller) {
27
- message.on('data', (chunk) => controller.enqueue(chunk))
28
- message.on('end', () => controller.close())
29
- message.on('error', (error) => controller.error(error))
30
- },
31
- cancel() {
32
- message.destroy()
33
- },
34
- })
27
+ start(controller) {
28
+ message.on('data', chunk => controller.enqueue(chunk))
29
+ message.on('end', () => controller.close())
30
+ message.on('error', error => controller.error(error))
31
+ signal.addEventListener('abort', () => message.destroy(signal.reason))
32
+ },
33
+ cancel() {
34
+ message.destroy()
35
+ },
36
+ })
35
37
 
36
38
  const rawHeaders = new Headers()
37
39
  for (let i = 0; i < message.rawHeaders.length; i += 2) {
package/lib/intercept.js CHANGED
@@ -11,14 +11,15 @@ const http = require('http')
11
11
  const { intercept: debug } = require('./debug')
12
12
  const globalEmitter = require('./global_emitter')
13
13
  const { BatchInterceptor } = require('@mswjs/interceptors')
14
- const { FetchInterceptor } = require('@mswjs/interceptors/fetch')
15
- const { default: nodeInterceptors } = require('@mswjs/interceptors/presets/node')
14
+ const {
15
+ default: nodeInterceptors,
16
+ } = require('@mswjs/interceptors/presets/node')
16
17
  const { createResponse } = require('./create_response')
17
18
  const { once } = require('events')
18
19
 
19
20
  const interceptor = new BatchInterceptor({
20
21
  name: 'nock-interceptor',
21
- interceptors: [...nodeInterceptors, new FetchInterceptor()],
22
+ interceptors: nodeInterceptors,
22
23
  })
23
24
  let isNockActive = false
24
25
 
@@ -194,7 +195,8 @@ function interceptorsFor(options) {
194
195
  ]
195
196
  } else {
196
197
  debug(
197
- `matched base path (${interceptors.length} interceptor${interceptors.length > 1 ? 's' : ''
198
+ `matched base path (${interceptors.length} interceptor${
199
+ interceptors.length > 1 ? 's' : ''
198
200
  })`,
199
201
  )
200
202
  return interceptors
@@ -332,7 +334,7 @@ function restoreOverriddenClientRequest() {
332
334
  if (!originalClientRequest) {
333
335
  debug('- ClientRequest was not overridden')
334
336
  } else {
335
- isNockActive = false;
337
+ isNockActive = false
336
338
  interceptor.dispose()
337
339
  http.ClientRequest = originalClientRequest
338
340
  originalClientRequest = undefined
@@ -371,58 +373,55 @@ function activate() {
371
373
  }
372
374
 
373
375
  overrideClientRequest()
374
- interceptor.apply();
376
+ interceptor.apply()
375
377
  // Force msw to forward Nock's error instead of coerce it into 500 error
376
378
  interceptor.on('unhandledException', ({ controller, error }) => {
377
379
  controller.errorWith(error)
378
380
  })
379
- interceptor.on('request', async function ({ request: mswRequest, controller }) {
380
- const request = mswRequest.clone()
381
- const { options } = common.normalizeClientRequestArgs(request.url)
382
- options.proto = options.protocol.slice(0, -1)
383
- options.method = request.method
384
- const interceptors = interceptorsFor(options)
385
- if (isOn() && interceptors) {
386
- const matches = interceptors.some(interceptor =>
387
- interceptor.matchOrigin(options)
388
- )
389
- const allowUnmocked = interceptors.some(
390
- interceptor => interceptor.options.allowUnmocked
391
- )
381
+ interceptor.on(
382
+ 'request',
383
+ async function ({ request: mswRequest, controller }) {
384
+ const request = mswRequest.clone()
385
+ const { options } = common.normalizeClientRequestArgs(request.url)
386
+ options.proto = options.protocol.slice(0, -1)
387
+ options.method = request.method
388
+ const interceptors = interceptorsFor(options)
389
+ if (isOn() && interceptors) {
390
+ const matches = interceptors.some(interceptor =>
391
+ interceptor.matchOrigin(options),
392
+ )
393
+ const allowUnmocked = interceptors.some(
394
+ interceptor => interceptor.options.allowUnmocked,
395
+ )
392
396
 
393
- const nockRequest = common.convertFetchRequestToClientRequest(request);
394
- if (!matches && allowUnmocked) {
395
- globalEmitter.emit('no match', nockRequest)
397
+ const nockRequest = common.convertFetchRequestToClientRequest(request)
398
+ if (!matches && allowUnmocked) {
399
+ globalEmitter.emit('no match', nockRequest)
400
+ } else {
401
+ nockRequest.on('response', nockResponse => {
402
+ const response = createResponse(nockResponse, mswRequest.signal)
403
+ controller.respondWith(response)
404
+ })
405
+
406
+ const promise = Promise.race([
407
+ // TODO: temp hacky way to handle allowUnmocked in startPlayback
408
+ once(nockRequest, 'real-request'),
409
+ once(nockRequest, 'error'),
410
+ once(nockRequest, 'response'),
411
+ ])
412
+ const buffer = await request.arrayBuffer()
413
+ nockRequest.write(buffer)
414
+ nockRequest.end()
415
+ await promise
416
+ }
396
417
  } else {
397
- nockRequest.on('response', nockResponse => {
398
- // TODO: Consider put empty headers object as default when create the ClientRequest
399
- if (nockResponse.req.headers) {
400
- // forward Nock request headers to the MSW request
401
- Object.entries(nockResponse.req.headers).map(([k, v]) => mswRequest.headers.set(k, v))
402
- }
403
-
404
- const response = createResponse(nockResponse)
405
- controller.respondWith(response)
406
- })
407
-
408
- const promise = Promise.race([
409
- // TODO: temp hacky way to handle allowUnmocked in startPlayback
410
- once(nockRequest, 'real-request'),
411
- once(nockRequest, 'error'),
412
- once(nockRequest, 'response'),
413
- ])
414
- const buffer = await request.arrayBuffer()
415
- nockRequest.write(buffer)
416
- nockRequest.end()
417
- await promise
418
- }
419
- } else {
420
- globalEmitter.emit('no match', options)
421
- if (!(isOff() || isEnabledForNetConnect(options))) {
422
- throw new NetConnectNotAllowedError(options.host, options.path)
418
+ globalEmitter.emit('no match', options)
419
+ if (!(isOff() || isEnabledForNetConnect(options))) {
420
+ throw new NetConnectNotAllowedError(options.host, options.path)
421
+ }
423
422
  }
424
- }
425
- })
423
+ },
424
+ )
426
425
  isNockActive = true
427
426
  }
428
427
 
@@ -1,10 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { request_overrider: debug } = require('./debug')
4
- const {
5
- IncomingMessage,
6
- ClientRequest,
7
- } = require('http')
4
+ const { IncomingMessage, ClientRequest } = require('http')
8
5
  const propagate = require('propagate')
9
6
  const common = require('./common')
10
7
  const globalEmitter = require('./global_emitter')
package/lib/recorder.js CHANGED
@@ -6,20 +6,18 @@ const { inspect } = require('util')
6
6
 
7
7
  const common = require('./common')
8
8
  const { restoreOverriddenClientRequest } = require('./intercept')
9
- const { BatchInterceptor } = require('@mswjs/interceptors')
10
- const { FetchInterceptor } = require('@mswjs/interceptors/fetch')
11
- const { default: nodeInterceptors } = require('@mswjs/interceptors/presets/node')
12
9
  const { EventEmitter } = require('stream')
13
-
10
+ const { gzipSync, brotliCompressSync, deflateSync } = require('zlib')
11
+ const {
12
+ default: nodeInterceptors,
13
+ } = require('@mswjs/interceptors/presets/node')
14
14
  const SEPARATOR = '\n<<<<<<-- cut here -->>>>>>\n'
15
15
  let recordingInProgress = false
16
16
  let outputs = []
17
17
 
18
- // TODO: Consider use one BatchInterceptor (and not one for intercept and one for record)
19
- const interceptor = new BatchInterceptor({
20
- name: 'nock-interceptor',
21
- interceptors: [...nodeInterceptors, new FetchInterceptor()],
22
- })
18
+ // TODO: don't reuse the nodeInterceptors, create new ones.
19
+ const clientRequestInterceptor = nodeInterceptors[0]
20
+ const fetchRequestInterceptor = nodeInterceptors[2]
23
21
 
24
22
  function getScope(options) {
25
23
  const { proto, host, port } = common.normalizeRequestOptions(options)
@@ -225,8 +223,16 @@ function record(recOptions) {
225
223
  restoreOverriddenClientRequest()
226
224
 
227
225
  // We override the requests so that we can save information on them before executing.
228
- interceptor.apply();
229
- interceptor.on('request', async function ({ request: mswRequest, requestId }) {
226
+ clientRequestInterceptor.apply()
227
+ fetchRequestInterceptor.apply()
228
+ clientRequestInterceptor.on('request', async function ({ request }) {
229
+ await recordRequest(request)
230
+ })
231
+ fetchRequestInterceptor.on('request', async function ({ request }) {
232
+ await recordRequest(request)
233
+ })
234
+
235
+ async function recordRequest(mswRequest) {
230
236
  const request = mswRequest.clone()
231
237
  const { options } = common.normalizeClientRequestArgs(request.url)
232
238
  options.method = request.method
@@ -236,16 +242,42 @@ function record(recOptions) {
236
242
  // twice.
237
243
  /* istanbul ignore if */
238
244
  if (options._recording) {
239
- return
245
+ return
240
246
  }
241
247
  options._recording = true
242
248
 
243
- const req = new EventEmitter();
249
+ const req = new EventEmitter()
244
250
  req.on('response', function () {
245
251
  debug(thisRecordingId, 'intercepting', proto, 'request to record')
246
252
 
253
+ clientRequestInterceptor.once('response', async function ({ response }) {
254
+ await recordResponse(response)
255
+ })
256
+ fetchRequestInterceptor.once('response', async function ({ response }) {
257
+ // fetch decompresses the body automatically, so we need to recompress it
258
+ const codings =
259
+ response.headers
260
+ .get('content-encoding')
261
+ ?.toLowerCase()
262
+ .split(',')
263
+ .map(c => c.trim()) || []
264
+
265
+ let body = await response.arrayBuffer()
266
+ for (const coding of codings) {
267
+ if (coding === 'gzip') {
268
+ body = gzipSync(body)
269
+ } else if (coding === 'deflate') {
270
+ body = deflateSync(body)
271
+ } else if (coding === 'br') {
272
+ body = brotliCompressSync(body)
273
+ }
274
+ }
275
+
276
+ await recordResponse(new Response(body, response))
277
+ })
278
+
247
279
  // Intercept "res.once('end', ...)"-like event
248
- interceptor.once('response', async function ({ response: mswResponse }) {
280
+ async function recordResponse(mswResponse) {
249
281
  const response = mswResponse.clone()
250
282
  debug(thisRecordingId, proto, 'intercepted request ended')
251
283
 
@@ -304,7 +336,7 @@ function record(recOptions) {
304
336
  logging(out)
305
337
  }
306
338
  }
307
- })
339
+ }
308
340
 
309
341
  debug('finished setting up intercepting')
310
342
 
@@ -321,7 +353,7 @@ function record(recOptions) {
321
353
  // because mswjs take care for these events
322
354
  // TODO: refactor the recorder, we no longer need all the listeners and can just record the request we get from MSW
323
355
  req.emit('response')
324
- })
356
+ }
325
357
  }
326
358
 
327
359
  // Restore *all* the overridden http/https modules' properties.
@@ -331,7 +363,8 @@ function restore() {
331
363
  'restoring all the overridden http/https properties',
332
364
  )
333
365
 
334
- interceptor.dispose()
366
+ clientRequestInterceptor.dispose()
367
+ fetchRequestInterceptor.dispose()
335
368
  restoreOverriddenClientRequest()
336
369
  recordingInProgress = false
337
370
  }
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "testing",
8
8
  "isolation"
9
9
  ],
10
- "version": "14.0.0-beta.8",
10
+ "version": "14.0.0",
11
11
  "author": "Pedro Teixeira <pedro.teixeira@gmail.com>",
12
12
  "repository": {
13
13
  "type": "git",
@@ -22,7 +22,7 @@
22
22
  "main": "./index.js",
23
23
  "types": "types",
24
24
  "dependencies": {
25
- "@mswjs/interceptors": "^0.33.1",
25
+ "@mswjs/interceptors": "^0.37.3",
26
26
  "json-stringify-safe": "^5.0.1",
27
27
  "propagate": "^2.0.0"
28
28
  },
@@ -48,7 +48,7 @@
48
48
  "prettier": "3.2.5",
49
49
  "proxyquire": "^2.1.0",
50
50
  "rimraf": "^3.0.0",
51
- "semantic-release": "^22.0.5",
51
+ "semantic-release": "^24.1.0",
52
52
  "sinon": "^17.0.1",
53
53
  "sinon-chai": "^3.7.0",
54
54
  "typescript": "^5.0.4"
package/types/index.d.ts CHANGED
@@ -205,8 +205,13 @@ declare namespace nock {
205
205
  thrice(): this
206
206
  optionally(flag?: boolean): this
207
207
 
208
+ delay(opts: number): this
209
+ /** @deprecated use delay(number) instead */
210
+ delay(opts: { head?: number; body?: number }): this
208
211
  delay(opts: number | { head?: number; body?: number }): this
212
+ /** @deprecated use delay function instead */
209
213
  delayBody(timeMs: number): this
214
+ /** @deprecated use delay function instead */
210
215
  delayConnection(timeMs: number): this
211
216
  }
212
217
 
@@ -256,13 +261,13 @@ declare namespace nock {
256
261
  (
257
262
  fixtureName: string,
258
263
  options: BackOptions,
259
- nockedFn: (nockDone: () => Promise<void>) => void,
264
+ nockedFn: (nockDone: () => void) => void,
260
265
  ): void
261
266
  (
262
267
  fixtureName: string,
263
268
  options?: BackOptions,
264
269
  ): Promise<{
265
- nockDone: () => Promise<void>
270
+ nockDone: () => void
266
271
  context: BackContext
267
272
  }>
268
273
  }
@@ -289,7 +294,7 @@ declare namespace nock {
289
294
  interface BackOptions {
290
295
  before?: (def: Definition) => void
291
296
  after?: (scope: Scope) => void
292
- afterRecord?: (defs: Definition[]) => Definition[]
297
+ afterRecord?: (defs: Definition[]) => Definition[] | string
293
298
  recorder?: RecorderOptions
294
299
  }
295
300
  }