@mswjs/interceptors 0.13.1 → 0.13.2
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/package.json +5 -4
- package/src/createInterceptor.ts +100 -0
- package/src/index.ts +5 -0
- package/src/interceptors/ClientRequest/NodeClientRequest.test.ts +283 -0
- package/src/interceptors/ClientRequest/NodeClientRequest.ts +377 -0
- package/src/interceptors/ClientRequest/http.get.ts +32 -0
- package/src/interceptors/ClientRequest/http.request.ts +29 -0
- package/src/interceptors/ClientRequest/index.ts +61 -0
- package/src/interceptors/ClientRequest/utils/bodyBufferToString.test.ts +16 -0
- package/src/interceptors/ClientRequest/utils/bodyBufferToString.ts +7 -0
- package/src/interceptors/ClientRequest/utils/cloneIncomingMessage.test.ts +20 -0
- package/src/interceptors/ClientRequest/utils/cloneIncomingMessage.ts +41 -0
- package/src/interceptors/ClientRequest/utils/concatChunkToBuffer.test.ts +13 -0
- package/src/interceptors/ClientRequest/utils/concatChunkToBuffer.ts +10 -0
- package/src/interceptors/ClientRequest/utils/getIncomingMessageBody.test.ts +44 -0
- package/src/interceptors/ClientRequest/utils/getIncomingMessageBody.ts +38 -0
- package/src/interceptors/ClientRequest/utils/normalizeClientRequestArgs.test.ts +336 -0
- package/src/interceptors/ClientRequest/utils/normalizeClientRequestArgs.ts +205 -0
- package/src/interceptors/ClientRequest/utils/normalizeClientRequestEndArgs.test.ts +40 -0
- package/src/interceptors/ClientRequest/utils/normalizeClientRequestEndArgs.ts +51 -0
- package/src/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.test.ts +35 -0
- package/src/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.ts +36 -0
- package/src/interceptors/XMLHttpRequest/XMLHttpRequestOverride.ts +565 -0
- package/src/interceptors/XMLHttpRequest/index.ts +34 -0
- package/src/interceptors/XMLHttpRequest/polyfills/EventPolyfill.ts +51 -0
- package/src/interceptors/XMLHttpRequest/polyfills/ProgressEventPolyfill.ts +17 -0
- package/src/interceptors/XMLHttpRequest/utils/bufferFrom.test.ts +11 -0
- package/src/interceptors/XMLHttpRequest/utils/bufferFrom.ts +16 -0
- package/src/interceptors/XMLHttpRequest/utils/createEvent.test.ts +27 -0
- package/src/interceptors/XMLHttpRequest/utils/createEvent.ts +41 -0
- package/src/interceptors/fetch/index.ts +89 -0
- package/src/presets/browser.ts +8 -0
- package/src/presets/node.ts +8 -0
- package/src/remote.ts +176 -0
- package/src/utils/cloneObject.test.ts +93 -0
- package/src/utils/cloneObject.ts +34 -0
- package/src/utils/getCleanUrl.test.ts +31 -0
- package/src/utils/getCleanUrl.ts +6 -0
- package/src/utils/getRequestOptionsByUrl.ts +29 -0
- package/src/utils/getUrlByRequestOptions.test.ts +140 -0
- package/src/utils/getUrlByRequestOptions.ts +108 -0
- package/src/utils/isObject.test.ts +19 -0
- package/src/utils/isObject.ts +6 -0
- package/src/utils/parseJson.test.ts +9 -0
- package/src/utils/parseJson.ts +12 -0
- package/src/utils/toIsoResponse.test.ts +39 -0
- package/src/utils/toIsoResponse.ts +14 -0
- package/src/utils/uuid.ts +7 -0
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mswjs/interceptors",
|
|
3
3
|
"description": "Low-level HTTP/HTTPS/XHR/fetch request interception library.",
|
|
4
|
-
"version": "0.13.
|
|
4
|
+
"version": "0.13.2",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"author": "Artem Zakharchenko",
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"engines": {
|
|
10
|
-
"node": ">=12.
|
|
10
|
+
"node": ">=12.x"
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
13
|
"start": "tsc --build -w",
|
|
@@ -19,11 +19,12 @@
|
|
|
19
19
|
"clean": "rimraf lib",
|
|
20
20
|
"build": "yarn clean && tsc --build",
|
|
21
21
|
"prepare": "yarn simple-git-hooks init",
|
|
22
|
-
"prepublishOnly": "yarn
|
|
22
|
+
"prepublishOnly": "yarn build"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"lib",
|
|
26
|
-
"README.md"
|
|
26
|
+
"README.md",
|
|
27
|
+
"src"
|
|
27
28
|
],
|
|
28
29
|
"repository": {
|
|
29
30
|
"type": "git",
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { IncomingMessage } from 'http'
|
|
2
|
+
import { HeadersObject, Headers } from 'headers-utils'
|
|
3
|
+
import { StrictEventEmitter } from 'strict-event-emitter'
|
|
4
|
+
|
|
5
|
+
export type Interceptor = (
|
|
6
|
+
observer: Observer,
|
|
7
|
+
resolver: Resolver
|
|
8
|
+
) => InterceptorCleanupFn
|
|
9
|
+
|
|
10
|
+
export type Observer = StrictEventEmitter<InterceptorEventsMap>
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A side-effect function to restore all the patched modules.
|
|
14
|
+
*/
|
|
15
|
+
export type InterceptorCleanupFn = () => void
|
|
16
|
+
|
|
17
|
+
export type RequestCredentials = 'omit' | 'include' | 'same-origin'
|
|
18
|
+
|
|
19
|
+
export interface IsomorphicRequest {
|
|
20
|
+
id: string
|
|
21
|
+
url: URL
|
|
22
|
+
method: string
|
|
23
|
+
headers: Headers
|
|
24
|
+
/**
|
|
25
|
+
* The value of the request client's "credentials" option
|
|
26
|
+
* or a compatible alternative (i.e. `withCredentials` for `XMLHttpRequest`).
|
|
27
|
+
* Always equals to "omit" in Node.js.
|
|
28
|
+
*/
|
|
29
|
+
credentials: RequestCredentials
|
|
30
|
+
body?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface IsomorphicResponse {
|
|
34
|
+
status: number
|
|
35
|
+
statusText: string
|
|
36
|
+
headers: Headers
|
|
37
|
+
body?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface MockedResponse
|
|
41
|
+
extends Omit<Partial<IsomorphicResponse>, 'headers'> {
|
|
42
|
+
headers?: HeadersObject
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface InterceptorEventsMap {
|
|
46
|
+
request(request: IsomorphicRequest): void
|
|
47
|
+
response(request: IsomorphicRequest, response: IsomorphicResponse): void
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type Resolver = (
|
|
51
|
+
request: IsomorphicRequest,
|
|
52
|
+
ref: IncomingMessage | XMLHttpRequest | Request
|
|
53
|
+
) => MockedResponse | Promise<MockedResponse | void> | void
|
|
54
|
+
|
|
55
|
+
export interface InterceptorOptions {
|
|
56
|
+
modules: Interceptor[]
|
|
57
|
+
resolver: Resolver
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface InterceptorApi {
|
|
61
|
+
/**
|
|
62
|
+
* Apply necessary module patches to provision the interception of requests.
|
|
63
|
+
*/
|
|
64
|
+
apply(): void
|
|
65
|
+
on<Event extends keyof InterceptorEventsMap>(
|
|
66
|
+
event: Event,
|
|
67
|
+
listener: InterceptorEventsMap[Event]
|
|
68
|
+
): void
|
|
69
|
+
/**
|
|
70
|
+
* Restore all applied module patches and disable the interception.
|
|
71
|
+
*/
|
|
72
|
+
restore(): void
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function createInterceptor(options: InterceptorOptions): InterceptorApi {
|
|
76
|
+
const observer = new StrictEventEmitter<InterceptorEventsMap>()
|
|
77
|
+
let cleanupFns: InterceptorCleanupFn[] = []
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
apply() {
|
|
81
|
+
cleanupFns = options.modules.map((interceptor) => {
|
|
82
|
+
return interceptor(observer, options.resolver)
|
|
83
|
+
})
|
|
84
|
+
},
|
|
85
|
+
on(event, listener) {
|
|
86
|
+
observer.addListener(event, listener)
|
|
87
|
+
},
|
|
88
|
+
restore() {
|
|
89
|
+
observer.removeAllListeners()
|
|
90
|
+
|
|
91
|
+
if (cleanupFns.length === 0) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`Failed to restore patched modules: no patches found. Did you forget to run ".apply()"?`
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
cleanupFns.forEach((restore) => restore())
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment node
|
|
3
|
+
*/
|
|
4
|
+
import { EventEmitter } from 'events'
|
|
5
|
+
import * as express from 'express'
|
|
6
|
+
import { ServerApi, createServer } from '@open-draft/test-server'
|
|
7
|
+
import { NodeClientRequest } from './NodeClientRequest'
|
|
8
|
+
import { getIncomingMessageBody } from './utils/getIncomingMessageBody'
|
|
9
|
+
import { normalizeClientRequestArgs } from './utils/normalizeClientRequestArgs'
|
|
10
|
+
|
|
11
|
+
interface ErrorConnectionRefused extends NodeJS.ErrnoException {
|
|
12
|
+
address: string
|
|
13
|
+
port: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let httpServer: ServerApi
|
|
17
|
+
|
|
18
|
+
function waitFor(duration: number): Promise<void> {
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
setTimeout(resolve, duration)
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
beforeAll(async () => {
|
|
25
|
+
httpServer = await createServer((app) => {
|
|
26
|
+
app.post('/comment', (_req, res) => {
|
|
27
|
+
res.status(200).send('original-response')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
app.post('/write', express.text(), (req, res) => {
|
|
31
|
+
res.status(200).send(req.body)
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
afterAll(async () => {
|
|
37
|
+
await httpServer.close()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('gracefully finishes the request when it has a mocked response', (done) => {
|
|
41
|
+
const request = new NodeClientRequest(
|
|
42
|
+
normalizeClientRequestArgs('http:', 'http://any.thing', {
|
|
43
|
+
method: 'PUT',
|
|
44
|
+
}),
|
|
45
|
+
{
|
|
46
|
+
observer: new EventEmitter(),
|
|
47
|
+
resolver() {
|
|
48
|
+
return {
|
|
49
|
+
status: 301,
|
|
50
|
+
headers: {
|
|
51
|
+
'x-custom-header': 'yes',
|
|
52
|
+
},
|
|
53
|
+
body: 'mocked-response',
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
request.on('response', async (response) => {
|
|
60
|
+
// Request must be marked as finished.
|
|
61
|
+
expect(request.finished).toEqual(true)
|
|
62
|
+
expect(request.writableEnded).toEqual(true)
|
|
63
|
+
expect(request.writableFinished).toEqual(true)
|
|
64
|
+
expect(request.writableCorked).toEqual(0)
|
|
65
|
+
|
|
66
|
+
expect(response.statusCode).toEqual(301)
|
|
67
|
+
expect(response.headers).toHaveProperty('x-custom-header', 'yes')
|
|
68
|
+
|
|
69
|
+
const text = await getIncomingMessageBody(response)
|
|
70
|
+
expect(text).toEqual('mocked-response')
|
|
71
|
+
|
|
72
|
+
done()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
request.end()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('responds with a mocked response when requesting an existing hostname', (done) => {
|
|
79
|
+
const request = new NodeClientRequest(
|
|
80
|
+
normalizeClientRequestArgs('http:', httpServer.http.makeUrl('/comment')),
|
|
81
|
+
{
|
|
82
|
+
observer: new EventEmitter(),
|
|
83
|
+
async resolver() {
|
|
84
|
+
await waitFor(250)
|
|
85
|
+
return {
|
|
86
|
+
status: 201,
|
|
87
|
+
body: 'mocked-response',
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
request.on('response', async (response) => {
|
|
94
|
+
expect(response.statusCode).toEqual(201)
|
|
95
|
+
|
|
96
|
+
const text = await getIncomingMessageBody(response)
|
|
97
|
+
expect(text).toEqual('mocked-response')
|
|
98
|
+
|
|
99
|
+
done()
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
request.end()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('performs the request as-is given resolver returned no mocked response', (done) => {
|
|
106
|
+
const request = new NodeClientRequest(
|
|
107
|
+
normalizeClientRequestArgs('http:', httpServer.http.makeUrl('/comment'), {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
}),
|
|
110
|
+
{
|
|
111
|
+
observer: new EventEmitter(),
|
|
112
|
+
resolver() {},
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
request.on('response', async (response) => {
|
|
117
|
+
expect(request.finished).toEqual(true)
|
|
118
|
+
expect(request.writableEnded).toEqual(true)
|
|
119
|
+
|
|
120
|
+
expect(response.statusCode).toEqual(200)
|
|
121
|
+
expect(response.statusMessage).toEqual('OK')
|
|
122
|
+
expect(response.headers).toHaveProperty('x-powered-by', 'Express')
|
|
123
|
+
|
|
124
|
+
const text = await getIncomingMessageBody(response)
|
|
125
|
+
expect(text).toEqual('original-response')
|
|
126
|
+
|
|
127
|
+
done()
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
request.end()
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('emits the ENOTFOUND error connecting to a non-existing hostname given no mocked response', (done) => {
|
|
134
|
+
const request = new NodeClientRequest(
|
|
135
|
+
normalizeClientRequestArgs('http:', 'http://non-existing-url.com'),
|
|
136
|
+
{
|
|
137
|
+
observer: new EventEmitter(),
|
|
138
|
+
resolver() {},
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
request.on('error', (error: NodeJS.ErrnoException) => {
|
|
143
|
+
expect(error.code).toEqual('ENOTFOUND')
|
|
144
|
+
expect(error.syscall).toEqual('getaddrinfo')
|
|
145
|
+
done()
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
request.end()
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test('emits the ECONNREFUSED error connecting to an inactive server given no mocked response', (done) => {
|
|
152
|
+
const request = new NodeClientRequest(
|
|
153
|
+
normalizeClientRequestArgs('http:', 'http://localhost:12345'),
|
|
154
|
+
{
|
|
155
|
+
observer: new EventEmitter(),
|
|
156
|
+
resolver() {},
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
request.on('error', (error: ErrorConnectionRefused) => {
|
|
161
|
+
expect(error.code).toEqual('ECONNREFUSED')
|
|
162
|
+
expect(error.syscall).toEqual('connect')
|
|
163
|
+
expect(error.address).toEqual('127.0.0.1')
|
|
164
|
+
expect(error.port).toEqual(12345)
|
|
165
|
+
|
|
166
|
+
done()
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
request.end()
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test('does not emit ENOTFOUND error connecting to an inactive server given mocked response', (done) => {
|
|
173
|
+
const handleError = jest.fn()
|
|
174
|
+
const request = new NodeClientRequest(
|
|
175
|
+
normalizeClientRequestArgs('http:', 'http://non-existing-url.com'),
|
|
176
|
+
{
|
|
177
|
+
observer: new EventEmitter(),
|
|
178
|
+
async resolver() {
|
|
179
|
+
await waitFor(250)
|
|
180
|
+
return {
|
|
181
|
+
status: 200,
|
|
182
|
+
statusText: 'Works',
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
request.on('error', handleError)
|
|
189
|
+
request.on('response', (response) => {
|
|
190
|
+
expect(handleError).not.toHaveBeenCalled()
|
|
191
|
+
expect(response.statusCode).toEqual(200)
|
|
192
|
+
expect(response.statusMessage).toEqual('Works')
|
|
193
|
+
done()
|
|
194
|
+
})
|
|
195
|
+
request.end()
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
test('does not emit ECONNREFUSED error connecting to an inactive server given mocked response', (done) => {
|
|
199
|
+
const handleError = jest.fn()
|
|
200
|
+
const request = new NodeClientRequest(
|
|
201
|
+
normalizeClientRequestArgs('http:', 'http://localhost:9876'),
|
|
202
|
+
{
|
|
203
|
+
observer: new EventEmitter(),
|
|
204
|
+
async resolver() {
|
|
205
|
+
await waitFor(250)
|
|
206
|
+
return {
|
|
207
|
+
status: 200,
|
|
208
|
+
statusText: 'Works',
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
}
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
request.on('error', handleError)
|
|
215
|
+
request.on('response', (response) => {
|
|
216
|
+
expect(handleError).not.toHaveBeenCalled()
|
|
217
|
+
expect(response.statusCode).toEqual(200)
|
|
218
|
+
expect(response.statusMessage).toEqual('Works')
|
|
219
|
+
done()
|
|
220
|
+
})
|
|
221
|
+
request.end()
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
test('sends the request body to the server given no mocked response', (done) => {
|
|
225
|
+
const request = new NodeClientRequest(
|
|
226
|
+
normalizeClientRequestArgs('http:', httpServer.http.makeUrl('/write'), {
|
|
227
|
+
method: 'POST',
|
|
228
|
+
headers: {
|
|
229
|
+
'Content-Type': 'text/plain',
|
|
230
|
+
},
|
|
231
|
+
}),
|
|
232
|
+
{
|
|
233
|
+
observer: new EventEmitter(),
|
|
234
|
+
resolver() {},
|
|
235
|
+
}
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
request.write('one')
|
|
239
|
+
request.write('two')
|
|
240
|
+
|
|
241
|
+
request.on('response', async (response) => {
|
|
242
|
+
expect(response.statusCode).toEqual(200)
|
|
243
|
+
|
|
244
|
+
const text = await getIncomingMessageBody(response)
|
|
245
|
+
expect(text).toEqual('onetwothree')
|
|
246
|
+
|
|
247
|
+
done()
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
request.end('three')
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
test('does not send request body to the original server given mocked response', (done) => {
|
|
254
|
+
const request = new NodeClientRequest(
|
|
255
|
+
normalizeClientRequestArgs('http:', httpServer.http.makeUrl('/write'), {
|
|
256
|
+
method: 'POST',
|
|
257
|
+
}),
|
|
258
|
+
{
|
|
259
|
+
observer: new EventEmitter(),
|
|
260
|
+
async resolver() {
|
|
261
|
+
await waitFor(200)
|
|
262
|
+
return {
|
|
263
|
+
status: 301,
|
|
264
|
+
body: 'mock created!',
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
}
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
request.write('one')
|
|
271
|
+
request.write('two')
|
|
272
|
+
|
|
273
|
+
request.on('response', async (response) => {
|
|
274
|
+
expect(response.statusCode).toEqual(301)
|
|
275
|
+
|
|
276
|
+
const text = await getIncomingMessageBody(response)
|
|
277
|
+
expect(text).toEqual('mock created!')
|
|
278
|
+
|
|
279
|
+
done()
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
request.end()
|
|
283
|
+
})
|