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 +71 -2
- package/index.js +2 -0
- package/lib/back.js +6 -13
- package/lib/common.js +76 -152
- package/lib/create_response.js +1 -1
- package/lib/debug.js +1 -1
- package/lib/global_emitter.js +1 -1
- package/lib/handle-request.js +125 -0
- package/lib/intercept.js +43 -215
- package/lib/interceptor.js +123 -139
- package/lib/interceptors/builtin.js +65 -0
- package/lib/interceptors/undici.js +78 -0
- package/lib/match_body.js +12 -11
- package/lib/playback_interceptor.js +149 -182
- package/lib/recorder.js +77 -91
- package/lib/scope.js +11 -22
- package/lib/stringify.js +62 -0
- package/lib/utils/node/index.js +33 -0
- package/package.json +14 -15
- package/types/index.d.ts +56 -35
- package/lib/intercepted_request_router.js +0 -343
- package/lib/socket.js +0 -108
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
6
|
-
const
|
|
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 {
|
|
44
|
+
* @param {ArrayBuffer} buffer
|
|
46
45
|
* @returns {boolean}
|
|
47
46
|
*/
|
|
48
47
|
function isUtf8Representable(buffer) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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(
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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 {
|
|
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(
|
|
81
|
-
const
|
|
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}${
|
|
87
|
-
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
|
|
104
|
-
return contentEncoding
|
|
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
|
|
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
|
|
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
|
-
|
|
645
|
+
decompressRequestBody,
|
|
646
|
+
convertHeadersToRaw,
|
|
723
647
|
}
|
package/lib/create_response.js
CHANGED
package/lib/debug.js
CHANGED
package/lib/global_emitter.js
CHANGED
|
@@ -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
|