nock 14.0.0-beta.9 → 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
 
@@ -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,
@@ -16,8 +16,9 @@ const responseStatusCodesWithoutBody = [204, 205, 304]
16
16
 
17
17
  /**
18
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
23
  message.statusCode || 200,
23
24
  )
@@ -27,6 +28,7 @@ function createResponse(message) {
27
28
  message.on('data', chunk => controller.enqueue(chunk))
28
29
  message.on('end', () => controller.close())
29
30
  message.on('error', error => controller.error(error))
31
+ signal.addEventListener('abort', () => message.destroy(signal.reason))
30
32
  },
31
33
  cancel() {
32
34
  message.destroy()
package/lib/intercept.js CHANGED
@@ -11,7 +11,6 @@ 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
14
  const {
16
15
  default: nodeInterceptors,
17
16
  } = require('@mswjs/interceptors/presets/node')
@@ -20,7 +19,7 @@ const { once } = require('events')
20
19
 
21
20
  const interceptor = new BatchInterceptor({
22
21
  name: 'nock-interceptor',
23
- interceptors: [...nodeInterceptors, new FetchInterceptor()],
22
+ interceptors: nodeInterceptors,
24
23
  })
25
24
  let isNockActive = false
26
25
 
@@ -400,15 +399,7 @@ function activate() {
400
399
  globalEmitter.emit('no match', nockRequest)
401
400
  } else {
402
401
  nockRequest.on('response', nockResponse => {
403
- // TODO: Consider put empty headers object as default when create the ClientRequest
404
- if (nockResponse.req.headers) {
405
- // forward Nock request headers to the MSW request
406
- Object.entries(nockResponse.req.headers).map(([k, v]) =>
407
- mswRequest.headers.set(k, v),
408
- )
409
- }
410
-
411
- const response = createResponse(nockResponse)
402
+ const response = createResponse(nockResponse, mswRequest.signal)
412
403
  controller.respondWith(response)
413
404
  })
414
405
 
package/lib/recorder.js CHANGED
@@ -6,22 +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')
9
+ const { EventEmitter } = require('stream')
10
+ const { gzipSync, brotliCompressSync, deflateSync } = require('zlib')
11
11
  const {
12
12
  default: nodeInterceptors,
13
13
  } = require('@mswjs/interceptors/presets/node')
14
- const { EventEmitter } = require('stream')
15
-
16
14
  const SEPARATOR = '\n<<<<<<-- cut here -->>>>>>\n'
17
15
  let recordingInProgress = false
18
16
  let outputs = []
19
17
 
20
- // TODO: Consider use one BatchInterceptor (and not one for intercept and one for record)
21
- const interceptor = new BatchInterceptor({
22
- name: 'nock-interceptor',
23
- interceptors: [...nodeInterceptors, new FetchInterceptor()],
24
- })
18
+ // TODO: don't reuse the nodeInterceptors, create new ones.
19
+ const clientRequestInterceptor = nodeInterceptors[0]
20
+ const fetchRequestInterceptor = nodeInterceptors[2]
25
21
 
26
22
  function getScope(options) {
27
23
  const { proto, host, port } = common.normalizeRequestOptions(options)
@@ -227,109 +223,137 @@ function record(recOptions) {
227
223
  restoreOverriddenClientRequest()
228
224
 
229
225
  // We override the requests so that we can save information on them before executing.
230
- interceptor.apply()
231
- interceptor.on(
232
- 'request',
233
- async function ({ request: mswRequest, requestId }) {
234
- const request = mswRequest.clone()
235
- const { options } = common.normalizeClientRequestArgs(request.url)
236
- options.method = request.method
237
- const proto = options.protocol.slice(0, -1)
238
-
239
- // Node 0.11 https.request calls http.request -- don't want to record things
240
- // twice.
241
- /* istanbul ignore if */
242
- if (options._recording) {
243
- return
244
- }
245
- options._recording = true
246
-
247
- const req = new EventEmitter()
248
- req.on('response', function () {
249
- debug(thisRecordingId, 'intercepting', proto, 'request to record')
250
-
251
- // Intercept "res.once('end', ...)"-like event
252
- interceptor.once(
253
- 'response',
254
- async function ({ response: mswResponse }) {
255
- const response = mswResponse.clone()
256
- debug(thisRecordingId, proto, 'intercepted request ended')
257
-
258
- let reqheaders
259
- // Ignore request headers completely unless it was explicitly enabled by the user (see README)
260
- if (enableReqHeadersRecording) {
261
- // We never record user-agent headers as they are worse than useless -
262
- // they actually make testing more difficult without providing any benefit (see README)
263
- reqheaders = Object.fromEntries(request.headers.entries())
264
- common.deleteHeadersField(reqheaders, 'user-agent')
265
- }
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
+ })
266
234
 
267
- const headers = Object.fromEntries(response.headers.entries())
268
- const res = {
269
- statusCode: response.status,
270
- headers,
271
- rawHeaders: headers,
272
- }
235
+ async function recordRequest(mswRequest) {
236
+ const request = mswRequest.clone()
237
+ const { options } = common.normalizeClientRequestArgs(request.url)
238
+ options.method = request.method
239
+ const proto = options.protocol.slice(0, -1)
240
+
241
+ // Node 0.11 https.request calls http.request -- don't want to record things
242
+ // twice.
243
+ /* istanbul ignore if */
244
+ if (options._recording) {
245
+ return
246
+ }
247
+ options._recording = true
273
248
 
274
- const generateFn = outputObjects
275
- ? generateRequestAndResponseObject
276
- : generateRequestAndResponse
277
- let out = generateFn({
278
- req: options,
279
- bodyChunks: [Buffer.from(await request.arrayBuffer())],
280
- options,
281
- res,
282
- dataChunks: [Buffer.from(await response.arrayBuffer())],
283
- reqheaders,
284
- })
285
-
286
- debug('out:', out)
287
-
288
- // Check that the request was made during the current recording.
289
- // If it hasn't then skip it. There is no other simple way to handle
290
- // this as it depends on the timing of requests and responses. Throwing
291
- // will make some recordings/unit tests fail randomly depending on how
292
- // fast/slow the response arrived.
293
- // If you are seeing this error then you need to make sure that all
294
- // the requests made during a single recording session finish before
295
- // ending the same recording session.
296
- if (thisRecordingId !== currentRecordingId) {
297
- debug('skipping recording of an out-of-order request', out)
298
- return
299
- }
249
+ const req = new EventEmitter()
250
+ req.on('response', function () {
251
+ debug(thisRecordingId, 'intercepting', proto, 'request to record')
300
252
 
301
- outputs.push(out)
302
-
303
- if (!dontPrint) {
304
- if (useSeparator) {
305
- if (typeof out !== 'string') {
306
- out = JSON.stringify(out, null, 2)
307
- }
308
- logging(SEPARATOR + out + SEPARATOR)
309
- } else {
310
- logging(out)
311
- }
312
- }
313
- },
314
- )
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
+ }
315
275
 
316
- debug('finished setting up intercepting')
276
+ await recordResponse(new Response(body, response))
277
+ })
317
278
 
318
- // We override both the http and the https modules; when we are
319
- // serializing the request, we need to know which was called.
320
- // By stuffing the state, we can make sure that nock records
321
- // the intended protocol.
322
- if (proto === 'https') {
323
- options.proto = 'https'
279
+ // Intercept "res.once('end', ...)"-like event
280
+ async function recordResponse(mswResponse) {
281
+ const response = mswResponse.clone()
282
+ debug(thisRecordingId, proto, 'intercepted request ended')
283
+
284
+ let reqheaders
285
+ // Ignore request headers completely unless it was explicitly enabled by the user (see README)
286
+ if (enableReqHeadersRecording) {
287
+ // We never record user-agent headers as they are worse than useless -
288
+ // they actually make testing more difficult without providing any benefit (see README)
289
+ reqheaders = Object.fromEntries(request.headers.entries())
290
+ common.deleteHeadersField(reqheaders, 'user-agent')
324
291
  }
325
- })
326
292
 
327
- // This is a massive change, we are trying to change minimum code, so we emit end event here
328
- // because mswjs take care for these events
329
- // TODO: refactor the recorder, we no longer need all the listeners and can just record the request we get from MSW
330
- req.emit('response')
331
- },
332
- )
293
+ const headers = Object.fromEntries(response.headers.entries())
294
+ const res = {
295
+ statusCode: response.status,
296
+ headers,
297
+ rawHeaders: headers,
298
+ }
299
+
300
+ const generateFn = outputObjects
301
+ ? generateRequestAndResponseObject
302
+ : generateRequestAndResponse
303
+ let out = generateFn({
304
+ req: options,
305
+ bodyChunks: [Buffer.from(await request.arrayBuffer())],
306
+ options,
307
+ res,
308
+ dataChunks: [Buffer.from(await response.arrayBuffer())],
309
+ reqheaders,
310
+ })
311
+
312
+ debug('out:', out)
313
+
314
+ // Check that the request was made during the current recording.
315
+ // If it hasn't then skip it. There is no other simple way to handle
316
+ // this as it depends on the timing of requests and responses. Throwing
317
+ // will make some recordings/unit tests fail randomly depending on how
318
+ // fast/slow the response arrived.
319
+ // If you are seeing this error then you need to make sure that all
320
+ // the requests made during a single recording session finish before
321
+ // ending the same recording session.
322
+ if (thisRecordingId !== currentRecordingId) {
323
+ debug('skipping recording of an out-of-order request', out)
324
+ return
325
+ }
326
+
327
+ outputs.push(out)
328
+
329
+ if (!dontPrint) {
330
+ if (useSeparator) {
331
+ if (typeof out !== 'string') {
332
+ out = JSON.stringify(out, null, 2)
333
+ }
334
+ logging(SEPARATOR + out + SEPARATOR)
335
+ } else {
336
+ logging(out)
337
+ }
338
+ }
339
+ }
340
+
341
+ debug('finished setting up intercepting')
342
+
343
+ // We override both the http and the https modules; when we are
344
+ // serializing the request, we need to know which was called.
345
+ // By stuffing the state, we can make sure that nock records
346
+ // the intended protocol.
347
+ if (proto === 'https') {
348
+ options.proto = 'https'
349
+ }
350
+ })
351
+
352
+ // This is a massive change, we are trying to change minimum code, so we emit end event here
353
+ // because mswjs take care for these events
354
+ // TODO: refactor the recorder, we no longer need all the listeners and can just record the request we get from MSW
355
+ req.emit('response')
356
+ }
333
357
  }
334
358
 
335
359
  // Restore *all* the overridden http/https modules' properties.
@@ -339,7 +363,8 @@ function restore() {
339
363
  'restoring all the overridden http/https properties',
340
364
  )
341
365
 
342
- interceptor.dispose()
366
+ clientRequestInterceptor.dispose()
367
+ fetchRequestInterceptor.dispose()
343
368
  restoreOverriddenClientRequest()
344
369
  recordingInProgress = false
345
370
  }
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "testing",
8
8
  "isolation"
9
9
  ],
10
- "version": "14.0.0-beta.9",
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.2",
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
  }