nock 15.0.0-beta.5 → 15.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
@@ -461,13 +461,14 @@ nock('http://www.google.com')
461
461
  .replyWithError('something awful happened')
462
462
  ```
463
463
 
464
- JSON error responses are allowed too:
464
+ Error objects are allowed too:
465
465
 
466
466
  ```js
467
- nock('http://www.google.com').get('/cat-poems').replyWithError({
468
- message: 'something awful happened',
469
- code: 'AWFUL_ERROR',
470
- })
467
+ nock('http://www.google.com')
468
+ .get('/cat-poems')
469
+ .replyWithError(
470
+ Object.assign(new Error('Connection refused'), { code: 'ECONNREFUSED' }),
471
+ )
471
472
  ```
472
473
 
473
474
  > Note: This will emit an `error` event on the `request` object, not the reply.
@@ -1352,9 +1353,39 @@ A scope emits the following events:
1352
1353
  You can also listen for no match events like this:
1353
1354
 
1354
1355
  ```js
1355
- nock.emitter.on('no match', req => {})
1356
+ nock.emitter.on('no match', (req, interceptorResults) => {
1357
+ console.log('Request did not match any interceptors:', req.url)
1358
+
1359
+ if (interceptorResults && interceptorResults.length > 0) {
1360
+ interceptorResults.forEach(({ interceptor, reasons }) => {
1361
+ console.log(
1362
+ 'Interceptor:',
1363
+ interceptor.method,
1364
+ interceptor.basePath + interceptor.path,
1365
+ )
1366
+ console.log('Reasons:', reasons)
1367
+ })
1368
+ }
1369
+ })
1356
1370
  ```
1357
1371
 
1372
+ The callback receives two parameters:
1373
+
1374
+ - `req` - The request object that didn't match
1375
+ - `interceptorResults` - An array of objects containing detailed information about each interceptor that was tested.
1376
+ Each object contains:
1377
+ - `interceptor` - The interceptor that was tested against the request
1378
+ - `reasons` - An array of strings describing why the request didn't match this interceptor
1379
+ > ⚠️ **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.
1380
+
1381
+ Common mismatch reasons include:
1382
+
1383
+ - **Method mismatch**: `"Method mismatch: expected GET, got POST"`
1384
+ - **Path mismatch**: `"Path mismatch: expected /api/users, got /api/posts"`
1385
+ - **Header mismatch**: `"Header mismatch: expected authorization to match Bearer token, got null"`
1386
+ - **Body mismatch**: `"Body mismatch: expected "expected body", got actual body"`
1387
+ - **Query mismatch**: `"query matching failed"`
1388
+
1358
1389
  ## Nock Back
1359
1390
 
1360
1391
  Fixture recording support and playback.
@@ -34,9 +34,16 @@ async function handleRequest(request) {
34
34
  requestBodyIsUtf8Representable ? 'utf8' : 'hex',
35
35
  )
36
36
 
37
- const matchedInterceptor = interceptors.find(i =>
38
- i.match(request, requestBodyString),
39
- )
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
+ })
40
47
 
41
48
  if (matchedInterceptor) {
42
49
  matchedInterceptor.scope.logger(
@@ -54,7 +61,11 @@ async function handleRequest(request) {
54
61
 
55
62
  return response
56
63
  } else {
57
- globalEmitter.emit('no match', request)
64
+ globalEmitter.emit(
65
+ 'no match',
66
+ request,
67
+ matchResults.sort((a, b) => a.reasons.length - b.reasons.length),
68
+ )
58
69
 
59
70
  // Try to find a hostname match that allows unmocked.
60
71
  const allowUnmocked = interceptors.some(
@@ -224,21 +224,21 @@ module.exports = class Interceptor {
224
224
  /**
225
225
  * @param {Request} request
226
226
  * @param {string} body - string or hex
227
+ * @returns {string[]}
227
228
  */
228
229
  match(request, body) {
229
230
  const url = new URL(request.url)
230
231
  // TODO: fix request log to string
231
232
  this.scope.logger('attempting match %j, body = %j', request, body)
232
233
 
234
+ const mismatches = []
233
235
  let path = url.pathname + url.search
234
- let matches
235
236
  let matchKey
236
237
 
237
238
  if (this.method !== request.method) {
238
- this.scope.logger(
239
- `Method did not match. Request ${request.method} Interceptor ${this.method}`,
240
- )
241
- return false
239
+ const msg = `Method mismatch: expected ${this.method}, got ${request.method}`
240
+ this.scope.logger(msg)
241
+ mismatches.push(msg)
242
242
  }
243
243
 
244
244
  if (this.scope.transformPathFunction) {
@@ -254,12 +254,15 @@ module.exports = class Interceptor {
254
254
  }
255
255
  }
256
256
 
257
- if (
258
- !this.scope.matchHeaders.every(requestMatchesFilter) ||
259
- !this.interceptorMatchHeaders.every(requestMatchesFilter)
260
- ) {
261
- this.scope.logger("headers don't match")
262
- return false
257
+ for (const header of [
258
+ ...this.scope.matchHeaders,
259
+ ...this.interceptorMatchHeaders,
260
+ ]) {
261
+ if (!requestMatchesFilter(header)) {
262
+ const msg = `Header mismatch: expected ${header.name} to match ${header.value}, got ${request.headers.get(header.name)}`
263
+ this.scope.logger(msg)
264
+ mismatches.push(msg)
265
+ }
263
266
  }
264
267
 
265
268
  const reqHeadersMatch = Object.keys(this.reqheaders).every(key =>
@@ -271,18 +274,18 @@ module.exports = class Interceptor {
271
274
  )
272
275
 
273
276
  if (!reqHeadersMatch) {
274
- this.scope.logger("headers don't match")
275
- return false
277
+ const msg = "Request headers don't match"
278
+ this.scope.logger(msg)
279
+ mismatches.push(msg)
276
280
  }
277
281
 
278
282
  if (
279
283
  this.scope.scopeOptions.conditionally &&
280
284
  !this.scope.scopeOptions.conditionally()
281
285
  ) {
282
- this.scope.logger(
283
- 'matching failed because Scope.conditionally() did not validate',
284
- )
285
- return false
286
+ const msg = 'conditionally() did not validate'
287
+ this.scope.logger(msg)
288
+ mismatches.push(msg)
286
289
  }
287
290
 
288
291
  const badHeaders = this.badheaders.filter(header =>
@@ -290,8 +293,9 @@ module.exports = class Interceptor {
290
293
  )
291
294
 
292
295
  if (badHeaders.length) {
293
- this.scope.logger('request contains bad headers', ...badHeaders)
294
- return false
296
+ const msg = `Request contains bad headers: ${badHeaders.join(', ')}`
297
+ this.scope.logger(msg)
298
+ mismatches.push(msg)
295
299
  }
296
300
 
297
301
  // Match query strings when using query()
@@ -302,12 +306,10 @@ module.exports = class Interceptor {
302
306
  const [pathname, search] = path.split('?')
303
307
  const matchQueries = this.matchQuery({ search })
304
308
 
305
- this.scope.logger(
306
- matchQueries ? 'query matching succeeded' : 'query matching failed',
307
- )
308
-
309
309
  if (!matchQueries) {
310
- return false
310
+ const msg = 'query matching failed'
311
+ this.scope.logger(msg)
312
+ mismatches.push(msg)
311
313
  }
312
314
 
313
315
  // If the query string was explicitly checked then subsequent checks against
@@ -325,37 +327,37 @@ module.exports = class Interceptor {
325
327
  matchKey = common.normalizeOrigin(url)
326
328
  }
327
329
 
328
- if (typeof this.uri === 'function') {
329
- matches =
330
- common.matchStringOrRegexp(matchKey, this.basePath) &&
331
- // This is a false positive, as `uri` is not bound to `this`.
332
-
333
- this.uri.call(this, path)
334
- } else {
335
- matches =
336
- common.matchStringOrRegexp(matchKey, this.basePath) &&
337
- common.matchStringOrRegexp(path, this.path)
330
+ if (!common.matchStringOrRegexp(matchKey, this.basePath)) {
331
+ const msg = `Base path mismatch: expected ${this.basePath}, got ${matchKey}`
332
+ this.scope.logger(msg)
333
+ mismatches.push(msg)
338
334
  }
339
335
 
340
- this.scope.logger(`matching ${matchKey}${path} to ${this._key}: ${matches}`)
336
+ if (typeof this.uri === 'function') {
337
+ if (!this.uri.call(this, path)) {
338
+ const msg = `Path function mismatch: expected function to return true for ${path}`
339
+ this.scope.logger(msg)
340
+ mismatches.push(msg)
341
+ }
342
+ } else if (!common.matchStringOrRegexp(path, this.path)) {
343
+ const msg = `Path mismatch: expected ${this.path}, got ${path}`
344
+ this.scope.logger(msg)
345
+ mismatches.push(msg)
346
+ }
341
347
 
342
- if (matches && this._requestBody !== undefined) {
348
+ if (this._requestBody !== undefined) {
343
349
  if (this.scope.transformRequestBodyFunction) {
344
350
  body = this.scope.transformRequestBodyFunction(body, this._requestBody)
345
351
  }
346
352
 
347
- matches = matchBody(request, this._requestBody, body)
348
- if (!matches) {
349
- this.scope.logger(
350
- "bodies don't match: \n",
351
- this._requestBody,
352
- '\n',
353
- body,
354
- )
353
+ if (!matchBody(request, this._requestBody, body)) {
354
+ const msg = `Body mismatch: expected ${stringify(this._requestBody)}, got ${body}`
355
+ this.scope.logger(msg)
356
+ mismatches.push(msg)
355
357
  }
356
358
  }
357
359
 
358
- return matches
360
+ return mismatches
359
361
  }
360
362
 
361
363
  /**
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "testing",
8
8
  "isolation"
9
9
  ],
10
- "version": "15.0.0-beta.5",
10
+ "version": "15.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.39.3",
25
+ "@mswjs/interceptors": "^0.39.5",
26
26
  "json-stringify-safe": "^5.0.1"
27
27
  },
28
28
  "devDependencies": {
@@ -42,10 +42,11 @@
42
42
  "globals": "^16.1.0",
43
43
  "got": "^11.8.6",
44
44
  "jest": "^29.7.0",
45
- "mocha": "^9.1.3",
45
+ "mocha": "^11.7.2",
46
46
  "npm-run-all": "^4.1.5",
47
47
  "nyc": "^15.0.0",
48
48
  "prettier": "3.2.5",
49
+ "proxyquire": "^2.1.0",
49
50
  "rimraf": "^3.0.0",
50
51
  "semantic-release": "^24.1.0",
51
52
  "sinon": "^17.0.1",
package/types/index.d.ts CHANGED
@@ -32,7 +32,7 @@ declare namespace nock {
32
32
  function abortPendingRequests(): void
33
33
 
34
34
  let back: Back
35
- let emitter: NodeJS.EventEmitter
35
+ let emitter: NockEmitter
36
36
  let recorder: Recorder
37
37
 
38
38
  type InterceptFunction = (
@@ -87,6 +87,42 @@ declare namespace nock {
87
87
  | readonly [StatusCode, ReplyBody]
88
88
  | readonly [StatusCode, ReplyBody, ReplyHeaders]
89
89
 
90
+ /**
91
+ * Detailed mismatch information for the 'no match' event
92
+ * @experimental This interface may change in future versions based on community feedback.
93
+ */
94
+ interface InterceptorMatchResult {
95
+ interceptor: Interceptor
96
+ reasons: string[]
97
+ }
98
+
99
+ /**
100
+ * Enhanced global emitter with typed 'no match' event
101
+ */
102
+ interface NockEmitter extends NodeJS.EventEmitter {
103
+ on(event: 'no match', listener: (req: Request) => void): this
104
+ on(
105
+ event: 'no match',
106
+ listener: (
107
+ req: Request,
108
+ interceptorResults?: InterceptorMatchResult[],
109
+ ) => void,
110
+ ): this
111
+ once(event: 'no match', listener: (req: Request) => void): this
112
+ once(
113
+ event: 'no match',
114
+ listener: (
115
+ req: Request,
116
+ interceptorResults?: InterceptorMatchResult[],
117
+ ) => void,
118
+ ): this
119
+ emit(
120
+ event: 'no match',
121
+ req: Request,
122
+ interceptorResults?: InterceptorMatchResult[],
123
+ ): boolean
124
+ }
125
+
90
126
  interface Scope extends NodeJS.EventEmitter {
91
127
  get: InterceptFunction
92
128
  post: InterceptFunction