@mswjs/interceptors 0.31.1 → 0.32.1

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 (69) hide show
  1. package/README.md +56 -39
  2. package/lib/node/RemoteHttpInterceptor.d.ts +1 -2
  3. package/lib/node/RemoteHttpInterceptor.js +11 -11
  4. package/lib/node/RemoteHttpInterceptor.mjs +5 -5
  5. package/lib/node/{chunk-LTEXDYJ6.js → chunk-2COJKQQB.js} +3 -3
  6. package/lib/node/chunk-3OJLYEWA.mjs +963 -0
  7. package/lib/node/chunk-3OJLYEWA.mjs.map +1 -0
  8. package/lib/node/chunk-5JMJ55U7.js +963 -0
  9. package/lib/node/chunk-5JMJ55U7.js.map +1 -0
  10. package/lib/node/{chunk-E4AC7YAC.js → chunk-BFLYGQ6D.js} +4 -2
  11. package/lib/node/{chunk-KSHIDGUL.mjs → chunk-DV4PBH4D.mjs} +3 -3
  12. package/lib/node/{chunk-OUWBQF3Z.mjs → chunk-KWV3JXSI.mjs} +14 -14
  13. package/lib/node/chunk-KWV3JXSI.mjs.map +1 -0
  14. package/lib/node/{chunk-6FRASLM3.mjs → chunk-PNWPIDEL.mjs} +2 -2
  15. package/lib/node/{chunk-APT7KA3B.js → chunk-PYD4E2EJ.js} +13 -13
  16. package/lib/node/{chunk-Q7POAM5N.mjs → chunk-TGTPXCLF.mjs} +3 -1
  17. package/lib/node/{chunk-MQJ3JOOK.js → chunk-UXCYRE4F.js} +14 -14
  18. package/lib/node/chunk-UXCYRE4F.js.map +1 -0
  19. package/lib/node/index.js +3 -3
  20. package/lib/node/index.mjs +2 -2
  21. package/lib/node/interceptors/ClientRequest/index.d.ts +83 -14
  22. package/lib/node/interceptors/ClientRequest/index.js +4 -4
  23. package/lib/node/interceptors/ClientRequest/index.mjs +3 -3
  24. package/lib/node/interceptors/XMLHttpRequest/index.js +4 -4
  25. package/lib/node/interceptors/XMLHttpRequest/index.mjs +3 -3
  26. package/lib/node/interceptors/fetch/index.js +10 -10
  27. package/lib/node/interceptors/fetch/index.mjs +2 -2
  28. package/lib/node/presets/node.d.ts +2 -3
  29. package/lib/node/presets/node.js +6 -6
  30. package/lib/node/presets/node.mjs +4 -4
  31. package/package.json +2 -2
  32. package/src/interceptors/ClientRequest/MockHttpSocket.ts +595 -0
  33. package/src/interceptors/ClientRequest/agents.ts +78 -0
  34. package/src/interceptors/ClientRequest/index.test.ts +14 -12
  35. package/src/interceptors/ClientRequest/index.ts +200 -41
  36. package/src/interceptors/ClientRequest/utils/normalizeClientRequestArgs.test.ts +78 -98
  37. package/src/interceptors/ClientRequest/utils/normalizeClientRequestArgs.ts +40 -22
  38. package/src/interceptors/Socket/MockSocket.test.ts +264 -0
  39. package/src/interceptors/Socket/MockSocket.ts +59 -0
  40. package/src/interceptors/Socket/utils/baseUrlFromConnectionOptions.ts +26 -0
  41. package/src/interceptors/Socket/utils/normalizeSocketWriteArgs.test.ts +52 -0
  42. package/src/interceptors/Socket/utils/normalizeSocketWriteArgs.ts +33 -0
  43. package/src/interceptors/Socket/utils/parseRawHeaders.ts +10 -0
  44. package/lib/node/chunk-IS3CIGXU.js +0 -909
  45. package/lib/node/chunk-IS3CIGXU.js.map +0 -1
  46. package/lib/node/chunk-MQJ3JOOK.js.map +0 -1
  47. package/lib/node/chunk-OMOWHUE6.mjs +0 -909
  48. package/lib/node/chunk-OMOWHUE6.mjs.map +0 -1
  49. package/lib/node/chunk-OUWBQF3Z.mjs.map +0 -1
  50. package/src/interceptors/ClientRequest/NodeClientRequest.test.ts +0 -206
  51. package/src/interceptors/ClientRequest/NodeClientRequest.ts +0 -680
  52. package/src/interceptors/ClientRequest/http.get.ts +0 -30
  53. package/src/interceptors/ClientRequest/http.request.ts +0 -27
  54. package/src/interceptors/ClientRequest/utils/cloneIncomingMessage.test.ts +0 -26
  55. package/src/interceptors/ClientRequest/utils/cloneIncomingMessage.ts +0 -74
  56. package/src/interceptors/ClientRequest/utils/createRequest.test.ts +0 -144
  57. package/src/interceptors/ClientRequest/utils/createRequest.ts +0 -51
  58. package/src/interceptors/ClientRequest/utils/createResponse.test.ts +0 -53
  59. package/src/interceptors/ClientRequest/utils/createResponse.ts +0 -55
  60. package/src/interceptors/ClientRequest/utils/normalizeClientRequestEndArgs.test.ts +0 -41
  61. package/src/interceptors/ClientRequest/utils/normalizeClientRequestEndArgs.ts +0 -53
  62. package/src/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.test.ts +0 -36
  63. package/src/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.ts +0 -39
  64. /package/lib/node/{chunk-LTEXDYJ6.js.map → chunk-2COJKQQB.js.map} +0 -0
  65. /package/lib/node/{chunk-E4AC7YAC.js.map → chunk-BFLYGQ6D.js.map} +0 -0
  66. /package/lib/node/{chunk-KSHIDGUL.mjs.map → chunk-DV4PBH4D.mjs.map} +0 -0
  67. /package/lib/node/{chunk-6FRASLM3.mjs.map → chunk-PNWPIDEL.mjs.map} +0 -0
  68. /package/lib/node/{chunk-APT7KA3B.js.map → chunk-PYD4E2EJ.js.map} +0 -0
  69. /package/lib/node/{chunk-Q7POAM5N.mjs.map → chunk-TGTPXCLF.mjs.map} +0 -0
@@ -1,5 +1,5 @@
1
- import { it, expect, beforeAll, afterAll } from 'vitest'
2
- import http from 'http'
1
+ import { it, expect, beforeAll, afterEach, afterAll } from 'vitest'
2
+ import http from 'node:http'
3
3
  import { HttpServer } from '@open-draft/test-server/http'
4
4
  import { DeferredPromise } from '@open-draft/deferred-promise'
5
5
  import { ClientRequestInterceptor } from '.'
@@ -21,6 +21,10 @@ beforeAll(async () => {
21
21
  await httpServer.listen()
22
22
  })
23
23
 
24
+ afterEach(() => {
25
+ interceptor.removeAllListeners()
26
+ })
27
+
24
28
  afterAll(async () => {
25
29
  interceptor.dispose()
26
30
  await httpServer.close()
@@ -60,25 +64,23 @@ it('forbids calling "respondWith" multiple times for the same request', async ()
60
64
  it('abort the request if the abort signal is emitted', async () => {
61
65
  const requestUrl = httpServer.http.url('/')
62
66
 
63
- const requestEmitted = new DeferredPromise<void>()
64
67
  interceptor.on('request', async function delayedResponse({ request }) {
65
- requestEmitted.resolve()
66
- await sleep(10_000)
68
+ await sleep(1_000)
67
69
  request.respondWith(new Response())
68
70
  })
69
71
 
70
72
  const abortController = new AbortController()
71
73
  const request = http.get(requestUrl, { signal: abortController.signal })
72
74
 
73
- await requestEmitted
74
-
75
75
  abortController.abort()
76
76
 
77
- const requestAborted = new DeferredPromise<void>()
78
- request.on('error', function (err) {
79
- expect(err.name).toEqual('AbortError')
80
- requestAborted.resolve()
77
+ const abortErrorPromise = new DeferredPromise<Error>()
78
+ request.on('error', function (error) {
79
+ abortErrorPromise.resolve(error)
81
80
  })
82
81
 
83
- await requestAborted
82
+ const abortError = await abortErrorPromise
83
+ expect(abortError.name).toEqual('AbortError')
84
+
85
+ expect(request.destroyed).toBe(true)
84
86
  })
@@ -1,61 +1,220 @@
1
- import http from 'http'
2
- import https from 'https'
3
- import type { Emitter } from 'strict-event-emitter'
4
- import { HttpRequestEventMap } from '../../glossary'
1
+ import http from 'node:http'
2
+ import https from 'node:https'
3
+ import { until } from '@open-draft/until'
5
4
  import { Interceptor } from '../../Interceptor'
6
- import { get } from './http.get'
7
- import { request } from './http.request'
8
- import { NodeClientOptions, Protocol } from './NodeClientRequest'
5
+ import type { HttpRequestEventMap } from '../../glossary'
6
+ import {
7
+ kRequestId,
8
+ MockHttpSocketRequestCallback,
9
+ MockHttpSocketResponseCallback,
10
+ } from './MockHttpSocket'
11
+ import { MockAgent, MockHttpsAgent } from './agents'
12
+ import { emitAsync } from '../../utils/emitAsync'
13
+ import { toInteractiveRequest } from '../../utils/toInteractiveRequest'
14
+ import { normalizeClientRequestArgs } from './utils/normalizeClientRequestArgs'
15
+ import { isNodeLikeError } from '../../utils/isNodeLikeError'
16
+ import { createServerErrorResponse } from '../../utils/responseUtils'
9
17
 
10
- export type ClientRequestEmitter = Emitter<HttpRequestEventMap>
11
-
12
- export type ClientRequestModules = Map<Protocol, typeof http | typeof https>
13
-
14
- /**
15
- * Intercept requests made via the `ClientRequest` class.
16
- * Such requests include `http.get`, `https.request`, etc.
17
- */
18
18
  export class ClientRequestInterceptor extends Interceptor<HttpRequestEventMap> {
19
- static interceptorSymbol = Symbol('http')
20
- private modules: ClientRequestModules
19
+ static symbol = Symbol('client-request-interceptor')
21
20
 
22
21
  constructor() {
23
- super(ClientRequestInterceptor.interceptorSymbol)
24
-
25
- this.modules = new Map()
26
- this.modules.set('http', http)
27
- this.modules.set('https', https)
22
+ super(ClientRequestInterceptor.symbol)
28
23
  }
29
24
 
30
25
  protected setup(): void {
31
- const logger = this.logger.extend('setup')
26
+ const { get: originalGet, request: originalRequest } = http
27
+ const { get: originalHttpsGet, request: originalHttpsRequest } = https
28
+
29
+ const onRequest = this.onRequest.bind(this)
30
+ const onResponse = this.onResponse.bind(this)
31
+
32
+ http.request = new Proxy(http.request, {
33
+ apply: (target, thisArg, args: Parameters<typeof http.request>) => {
34
+ const [url, options, callback] = normalizeClientRequestArgs(
35
+ 'http:',
36
+ args
37
+ )
38
+ const mockAgent = new MockAgent({
39
+ customAgent: options.agent,
40
+ onRequest,
41
+ onResponse,
42
+ })
43
+ options.agent = mockAgent
44
+
45
+ return Reflect.apply(target, thisArg, [url, options, callback])
46
+ },
47
+ })
48
+
49
+ http.get = new Proxy(http.get, {
50
+ apply: (target, thisArg, args: Parameters<typeof http.get>) => {
51
+ const [url, options, callback] = normalizeClientRequestArgs(
52
+ 'http:',
53
+ args
54
+ )
55
+
56
+ const mockAgent = new MockAgent({
57
+ customAgent: options.agent,
58
+ onRequest,
59
+ onResponse,
60
+ })
61
+ options.agent = mockAgent
62
+
63
+ return Reflect.apply(target, thisArg, [url, options, callback])
64
+ },
65
+ })
32
66
 
33
- for (const [protocol, requestModule] of this.modules) {
34
- const { request: pureRequest, get: pureGet } = requestModule
67
+ //
68
+ // HTTPS.
69
+ //
35
70
 
36
- this.subscriptions.push(() => {
37
- requestModule.request = pureRequest
38
- requestModule.get = pureGet
71
+ https.request = new Proxy(https.request, {
72
+ apply: (target, thisArg, args: Parameters<typeof https.request>) => {
73
+ const [url, options, callback] = normalizeClientRequestArgs(
74
+ 'https:',
75
+ args
76
+ )
77
+
78
+ const mockAgent = new MockHttpsAgent({
79
+ customAgent: options.agent,
80
+ onRequest,
81
+ onResponse,
82
+ })
83
+ options.agent = mockAgent
84
+
85
+ return Reflect.apply(target, thisArg, [url, options, callback])
86
+ },
87
+ })
88
+
89
+ https.get = new Proxy(https.get, {
90
+ apply: (target, thisArg, args: Parameters<typeof https.get>) => {
91
+ const [url, options, callback] = normalizeClientRequestArgs(
92
+ 'https:',
93
+ args
94
+ )
95
+
96
+ const mockAgent = new MockHttpsAgent({
97
+ customAgent: options.agent,
98
+ onRequest,
99
+ onResponse,
100
+ })
101
+ options.agent = mockAgent
102
+
103
+ return Reflect.apply(target, thisArg, [url, options, callback])
104
+ },
105
+ })
106
+
107
+ this.subscriptions.push(() => {
108
+ http.get = originalGet
109
+ http.request = originalRequest
110
+
111
+ https.get = originalHttpsGet
112
+ https.request = originalHttpsRequest
113
+ })
114
+ }
39
115
 
40
- logger.info('native "%s" module restored!', protocol)
116
+ private onRequest: MockHttpSocketRequestCallback = async ({
117
+ request,
118
+ socket,
119
+ }) => {
120
+ const requestId = Reflect.get(request, kRequestId)
121
+ const { interactiveRequest, requestController } =
122
+ toInteractiveRequest(request)
123
+
124
+ // TODO: Abstract this bit. We are using it everywhere.
125
+ this.emitter.once('request', ({ requestId: pendingRequestId }) => {
126
+ if (pendingRequestId !== requestId) {
127
+ return
128
+ }
129
+
130
+ if (requestController.responsePromise.state === 'pending') {
131
+ this.logger.info(
132
+ 'request has not been handled in listeners, executing fail-safe listener...'
133
+ )
134
+
135
+ requestController.responsePromise.resolve(undefined)
136
+ }
137
+ })
138
+
139
+ const listenerResult = await until(async () => {
140
+ await emitAsync(this.emitter, 'request', {
141
+ requestId,
142
+ request: interactiveRequest,
41
143
  })
42
144
 
43
- const options: NodeClientOptions = {
44
- emitter: this.emitter,
45
- logger: this.logger,
145
+ return await requestController.responsePromise
146
+ })
147
+
148
+ if (listenerResult.error) {
149
+ // Treat thrown Responses as mocked responses.
150
+ if (listenerResult.error instanceof Response) {
151
+ socket.respondWith(listenerResult.error)
152
+ return
153
+ }
154
+
155
+ // Allow mocking Node-like errors.
156
+ if (isNodeLikeError(listenerResult.error)) {
157
+ socket.errorWith(listenerResult.error)
158
+ return
159
+ }
160
+
161
+ // Emit the "unhandledException" event to allow the client
162
+ // to opt-out from the default handling of exceptions
163
+ // as 500 error responses.
164
+ if (this.emitter.listenerCount('unhandledException') > 0) {
165
+ await emitAsync(this.emitter, 'unhandledException', {
166
+ error: listenerResult.error,
167
+ request,
168
+ requestId,
169
+ controller: {
170
+ respondWith: socket.respondWith.bind(socket),
171
+ errorWith: socket.errorWith.bind(socket),
172
+ },
173
+ })
174
+
175
+ // After the listeners are done, if the socket is
176
+ // not connecting anymore, the response was mocked.
177
+ // If the socket has been destroyed, the error was mocked.
178
+ // Treat both as the result of the listener's call.
179
+ if (!socket.connecting || socket.destroyed) {
180
+ return
181
+ }
46
182
  }
47
183
 
48
- // @ts-ignore
49
- requestModule.request =
50
- // Force a line break.
51
- request(protocol, options)
184
+ // Unhandled exceptions in the request listeners are
185
+ // synonymous to unhandled exceptions on the server.
186
+ // Those are represented as 500 error responses.
187
+ socket.respondWith(createServerErrorResponse(listenerResult.error))
188
+ return
189
+ }
52
190
 
53
- // @ts-ignore
54
- requestModule.get =
55
- // Force a line break.
56
- get(protocol, options)
191
+ const mockedResponse = listenerResult.data
57
192
 
58
- logger.info('native "%s" module patched!', protocol)
193
+ if (mockedResponse) {
194
+ /**
195
+ * @note The `.respondWith()` method will handle "Response.error()".
196
+ * Maybe we should make all interceptors do that?
197
+ */
198
+ socket.respondWith(mockedResponse)
199
+ return
59
200
  }
201
+
202
+ socket.passthrough()
203
+ }
204
+
205
+ public onResponse: MockHttpSocketResponseCallback = async ({
206
+ requestId,
207
+ request,
208
+ response,
209
+ isMockedResponse,
210
+ }) => {
211
+ // Return the promise to when all the response event listeners
212
+ // are finished.
213
+ return emitAsync(this.emitter, 'response', {
214
+ requestId,
215
+ request,
216
+ response,
217
+ isMockedResponse,
218
+ })
60
219
  }
61
220
  }