@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.
Files changed (48) hide show
  1. package/package.json +5 -4
  2. package/src/createInterceptor.ts +100 -0
  3. package/src/index.ts +5 -0
  4. package/src/interceptors/ClientRequest/NodeClientRequest.test.ts +283 -0
  5. package/src/interceptors/ClientRequest/NodeClientRequest.ts +377 -0
  6. package/src/interceptors/ClientRequest/http.get.ts +32 -0
  7. package/src/interceptors/ClientRequest/http.request.ts +29 -0
  8. package/src/interceptors/ClientRequest/index.ts +61 -0
  9. package/src/interceptors/ClientRequest/utils/bodyBufferToString.test.ts +16 -0
  10. package/src/interceptors/ClientRequest/utils/bodyBufferToString.ts +7 -0
  11. package/src/interceptors/ClientRequest/utils/cloneIncomingMessage.test.ts +20 -0
  12. package/src/interceptors/ClientRequest/utils/cloneIncomingMessage.ts +41 -0
  13. package/src/interceptors/ClientRequest/utils/concatChunkToBuffer.test.ts +13 -0
  14. package/src/interceptors/ClientRequest/utils/concatChunkToBuffer.ts +10 -0
  15. package/src/interceptors/ClientRequest/utils/getIncomingMessageBody.test.ts +44 -0
  16. package/src/interceptors/ClientRequest/utils/getIncomingMessageBody.ts +38 -0
  17. package/src/interceptors/ClientRequest/utils/normalizeClientRequestArgs.test.ts +336 -0
  18. package/src/interceptors/ClientRequest/utils/normalizeClientRequestArgs.ts +205 -0
  19. package/src/interceptors/ClientRequest/utils/normalizeClientRequestEndArgs.test.ts +40 -0
  20. package/src/interceptors/ClientRequest/utils/normalizeClientRequestEndArgs.ts +51 -0
  21. package/src/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.test.ts +35 -0
  22. package/src/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.ts +36 -0
  23. package/src/interceptors/XMLHttpRequest/XMLHttpRequestOverride.ts +565 -0
  24. package/src/interceptors/XMLHttpRequest/index.ts +34 -0
  25. package/src/interceptors/XMLHttpRequest/polyfills/EventPolyfill.ts +51 -0
  26. package/src/interceptors/XMLHttpRequest/polyfills/ProgressEventPolyfill.ts +17 -0
  27. package/src/interceptors/XMLHttpRequest/utils/bufferFrom.test.ts +11 -0
  28. package/src/interceptors/XMLHttpRequest/utils/bufferFrom.ts +16 -0
  29. package/src/interceptors/XMLHttpRequest/utils/createEvent.test.ts +27 -0
  30. package/src/interceptors/XMLHttpRequest/utils/createEvent.ts +41 -0
  31. package/src/interceptors/fetch/index.ts +89 -0
  32. package/src/presets/browser.ts +8 -0
  33. package/src/presets/node.ts +8 -0
  34. package/src/remote.ts +176 -0
  35. package/src/utils/cloneObject.test.ts +93 -0
  36. package/src/utils/cloneObject.ts +34 -0
  37. package/src/utils/getCleanUrl.test.ts +31 -0
  38. package/src/utils/getCleanUrl.ts +6 -0
  39. package/src/utils/getRequestOptionsByUrl.ts +29 -0
  40. package/src/utils/getUrlByRequestOptions.test.ts +140 -0
  41. package/src/utils/getUrlByRequestOptions.ts +108 -0
  42. package/src/utils/isObject.test.ts +19 -0
  43. package/src/utils/isObject.ts +6 -0
  44. package/src/utils/parseJson.test.ts +9 -0
  45. package/src/utils/parseJson.ts +12 -0
  46. package/src/utils/toIsoResponse.test.ts +39 -0
  47. package/src/utils/toIsoResponse.ts +14 -0
  48. 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.1",
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.20.0"
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 test && yarn build"
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,5 @@
1
+ export * from './createInterceptor'
2
+ export * from './remote'
3
+
4
+ /* Utils */
5
+ export { getCleanUrl } from './utils/getCleanUrl'
@@ -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
+ })