@mswjs/interceptors 0.22.2 → 0.22.4

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.
@@ -0,0 +1,149 @@
1
+ import { vi, it, expect } from 'vitest'
2
+ import { createProxy } from './createProxy'
3
+
4
+ it('does not interfere with default constructors', () => {
5
+ const ProxyClass = createProxy(
6
+ class {
7
+ constructor(public name: string) {}
8
+ },
9
+ {}
10
+ )
11
+
12
+ const instance = new ProxyClass('John')
13
+ expect(instance.name).toBe('John')
14
+ })
15
+
16
+ it('does not interfere with default getters', () => {
17
+ const proxy = createProxy({ foo: 'initial' }, {})
18
+ expect(proxy.foo).toBe('initial')
19
+ })
20
+
21
+ it('does not interfere with default setters', () => {
22
+ const proxy = createProxy({ foo: 'initial' }, {})
23
+ proxy.foo = 'next'
24
+
25
+ expect(proxy.foo).toBe('next')
26
+ })
27
+
28
+ it('does not interfere with default methods', () => {
29
+ const proxy = createProxy({ getValue: () => 'initial' }, {})
30
+ expect(proxy.getValue()).toBe('initial')
31
+ })
32
+
33
+ it('does not interfere with existing descriptors', () => {
34
+ const target = {} as { foo: string; bar: number }
35
+ let internalBar = 0
36
+
37
+ Object.defineProperties(target, {
38
+ foo: {
39
+ get: () => 'initial',
40
+ },
41
+ bar: {
42
+ set: (value) => {
43
+ internalBar = value + 10
44
+ },
45
+ },
46
+ })
47
+
48
+ const proxy = createProxy(target, {
49
+ getProperty(data, next) {
50
+ return next()
51
+ },
52
+ })
53
+ expect(proxy.foo).toBe('initial')
54
+
55
+ proxy.bar = 5
56
+ expect(proxy.bar).toBeUndefined()
57
+ expect(internalBar).toBe(15)
58
+ })
59
+
60
+ it('infer prototype descriptors', () => {
61
+ class Child {
62
+ ok: boolean
63
+
64
+ set status(nextStatus: number) {
65
+ this.ok = nextStatus >= 200 && nextStatus < 300
66
+ }
67
+ }
68
+
69
+ Object.defineProperties(Child.prototype, {
70
+ status: { enumerable: true },
71
+ })
72
+
73
+ const scope = {} as { child: typeof Child }
74
+
75
+ Object.defineProperty(scope, 'child', {
76
+ enumerable: true,
77
+ value: Child,
78
+ })
79
+
80
+ const ProxyClass = createProxy(scope.child, {})
81
+ const instance = new ProxyClass()
82
+
83
+ instance.status = 201
84
+ expect(instance.ok).toBe(true)
85
+ })
86
+
87
+ it('spies on the constructor', () => {
88
+ const OriginalClass = class {
89
+ constructor(public name: string, public age: number) {}
90
+ }
91
+
92
+ const constructorCall = vi.fn<
93
+ [ConstructorParameters<typeof OriginalClass>, Function],
94
+ typeof OriginalClass
95
+ >((args, next) => next())
96
+
97
+ const ProxyClass = createProxy(OriginalClass, {
98
+ constructorCall,
99
+ })
100
+
101
+ new ProxyClass('John', 32)
102
+
103
+ expect(constructorCall).toHaveBeenCalledTimes(1)
104
+ expect(constructorCall).toHaveBeenCalledWith(
105
+ ['John', 32],
106
+ expect.any(Function)
107
+ )
108
+ })
109
+
110
+ it('spies on property getters', () => {
111
+ const getProperty = vi.fn((args, next) => next())
112
+ const proxy = createProxy({ foo: 'initial' }, { getProperty })
113
+
114
+ proxy.foo
115
+
116
+ expect(getProperty).toHaveBeenCalledTimes(1)
117
+ expect(getProperty).toHaveBeenCalledWith(['foo', proxy], expect.any(Function))
118
+ })
119
+
120
+ it('spies on property setters', () => {
121
+ const setProperty = vi.fn((args, next) => next())
122
+ const proxy = createProxy({ foo: 'initial' }, { setProperty })
123
+
124
+ proxy.foo = 'next'
125
+
126
+ expect(setProperty).toHaveBeenCalledTimes(1)
127
+ expect(setProperty).toHaveBeenCalledWith(
128
+ ['foo', 'next'],
129
+ expect.any(Function)
130
+ )
131
+ })
132
+
133
+ it('spies on method calls', () => {
134
+ const methodCall = vi.fn((args, next) => next())
135
+ const proxy = createProxy(
136
+ {
137
+ greet: (name: string) => `hello ${name}`,
138
+ },
139
+ { methodCall }
140
+ )
141
+
142
+ proxy.greet('Clair')
143
+
144
+ expect(methodCall).toHaveBeenCalledTimes(1)
145
+ expect(methodCall).toHaveBeenCalledWith(
146
+ ['greet', ['Clair']],
147
+ expect.any(Function)
148
+ )
149
+ })
@@ -9,8 +9,8 @@ export interface ProxyOptions<Target extends Record<string, any>> {
9
9
 
10
10
  setProperty?(
11
11
  data: [propertyName: string | symbol, nextValue: unknown],
12
- next: NextFunction<void>
13
- ): void
12
+ next: NextFunction<boolean>
13
+ ): boolean
14
14
 
15
15
  getProperty?(
16
16
  data: [propertyName: string | symbol, receiver: Target],
@@ -25,6 +25,7 @@ export function createProxy<Target extends object>(
25
25
  options: ProxyOptions<Target>
26
26
  ): Target {
27
27
  const proxy = new Proxy(target, optionsToProxyHandler(options))
28
+
28
29
  return proxy
29
30
  }
30
31
 
@@ -41,11 +42,31 @@ function optionsToProxyHandler<T extends Record<string, any>>(
41
42
  }
42
43
  }
43
44
 
44
- if (typeof setProperty !== 'undefined') {
45
- handler.set = function (target, propertyName, nextValue, receiver) {
46
- const next = () => Reflect.set(target, propertyName, nextValue, receiver)
47
- return setProperty.call(target, [propertyName, nextValue], next) as any
45
+ handler.set = function (target, propertyName, nextValue, receiver) {
46
+ const next = () => {
47
+ const ownDescriptors = Reflect.getOwnPropertyDescriptor(
48
+ target,
49
+ propertyName
50
+ )
51
+
52
+ if (typeof ownDescriptors?.set !== 'undefined') {
53
+ ownDescriptors.set.apply(target, [nextValue])
54
+ return true
55
+ }
56
+
57
+ return Reflect.defineProperty(target, propertyName, {
58
+ writable: true,
59
+ enumerable: true,
60
+ configurable: true,
61
+ value: nextValue,
62
+ })
63
+ }
64
+
65
+ if (typeof setProperty !== 'undefined') {
66
+ return setProperty.call(target, [propertyName, nextValue], next)
48
67
  }
68
+
69
+ return next()
49
70
  }
50
71
 
51
72
  handler.get = function (target, propertyName, receiver) {
@@ -4,138 +4,132 @@ import { RequestOptions, Agent as HttpsAgent } from 'https'
4
4
  import { getUrlByRequestOptions } from './getUrlByRequestOptions'
5
5
 
6
6
  it('returns a URL based on the basic RequestOptions', () => {
7
- const options: RequestOptions = {
8
- protocol: 'https:',
9
- host: '127.0.0.1',
10
- path: '/resource',
11
- }
12
- const url = getUrlByRequestOptions(options)
13
-
14
- expect(url).toBeInstanceOf(URL)
15
- expect(url).toHaveProperty('port', '')
16
- expect(url).toHaveProperty('href', 'https://127.0.0.1/resource')
7
+ expect(
8
+ getUrlByRequestOptions({
9
+ protocol: 'https:',
10
+ host: '127.0.0.1',
11
+ path: '/resource',
12
+ }).href
13
+ ).toBe('https://127.0.0.1/resource')
17
14
  })
18
15
 
19
16
  it('inherits protocol and port from http.Agent, if set', () => {
20
- const options: RequestOptions = {
21
- host: '127.0.0.1',
22
- path: '/',
23
- agent: new HttpAgent(),
24
- }
25
- const url = getUrlByRequestOptions(options)
26
-
27
- expect(url).toBeInstanceOf(URL)
28
- expect(url).toHaveProperty('protocol', 'http:')
29
- expect(url).toHaveProperty('port', '')
30
- expect(url).toHaveProperty('href', 'http://127.0.0.1/')
17
+ expect(
18
+ getUrlByRequestOptions({
19
+ host: '127.0.0.1',
20
+ path: '/',
21
+ agent: new HttpAgent(),
22
+ }).href
23
+ ).toBe('http://127.0.0.1/')
31
24
  })
32
25
 
33
26
  it('inherits protocol and port from https.Agent, if set', () => {
34
- const options: RequestOptions = {
35
- host: '127.0.0.1',
36
- path: '/',
37
- agent: new HttpsAgent({
38
- port: 3080,
39
- }),
40
- }
41
- const url = getUrlByRequestOptions(options)
42
-
43
- expect(url).toBeInstanceOf(URL)
44
- expect(url).toHaveProperty('protocol', 'https:')
45
- expect(url).toHaveProperty('port', '3080')
46
- expect(url).toHaveProperty('href', 'https://127.0.0.1:3080/')
27
+ expect(
28
+ getUrlByRequestOptions({
29
+ host: '127.0.0.1',
30
+ path: '/',
31
+ agent: new HttpsAgent({
32
+ port: 3080,
33
+ }),
34
+ }).href
35
+ ).toBe('https://127.0.0.1:3080/')
47
36
  })
48
37
 
49
38
  it('resolves protocol to "http" given no explicit protocol and no certificate', () => {
50
- const options: RequestOptions = {
51
- host: '127.0.0.1',
52
- path: '/',
53
- }
54
- const url = getUrlByRequestOptions(options)
55
-
56
- expect(url).toBeInstanceOf(URL)
57
- expect(url).toHaveProperty('protocol', 'http:')
58
- expect(url).toHaveProperty('port', '')
59
- expect(url).toHaveProperty('href', 'http://127.0.0.1/')
39
+ expect(
40
+ getUrlByRequestOptions({
41
+ host: '127.0.0.1',
42
+ path: '/',
43
+ }).href
44
+ ).toBe('http://127.0.0.1/')
60
45
  })
61
46
 
62
47
  it('resolves protocol to "https" given no explicit protocol, but certificate', () => {
63
- const options: RequestOptions = {
64
- host: '127.0.0.1',
65
- path: '/secure',
66
- cert: '<!-- SSL certificate -->',
67
- }
68
- const url = getUrlByRequestOptions(options)
69
-
70
- expect(url).toBeInstanceOf(URL)
71
- expect(url).toHaveProperty('protocol', 'https:')
72
- expect(url).toHaveProperty('port', '')
73
- expect(url).toHaveProperty('href', 'https://127.0.0.1/secure')
48
+ expect(
49
+ getUrlByRequestOptions({
50
+ host: '127.0.0.1',
51
+ path: '/secure',
52
+ cert: '<!-- SSL certificate -->',
53
+ }).href
54
+ ).toBe('https://127.0.0.1/secure')
74
55
  })
75
56
 
76
57
  it('resolves protocol to "https" given no explicit protocol, but port is 443', () => {
77
- const options: RequestOptions = {
78
- host: '127.0.0.1',
79
- port: 443,
80
- path: '/resource',
81
- }
82
- const url = getUrlByRequestOptions(options)
83
-
84
- expect(url).toBeInstanceOf(URL)
85
- expect(url).toHaveProperty('port', '')
86
- expect(url).toHaveProperty('href', 'https://127.0.0.1/resource')
58
+ expect(
59
+ getUrlByRequestOptions({
60
+ host: '127.0.0.1',
61
+ port: 443,
62
+ path: '/resource',
63
+ }).href
64
+ ).toBe('https://127.0.0.1/resource')
87
65
  })
88
66
 
89
67
  it('resolves protocol to "https" given no explicit protocol, but agent port is 443', () => {
90
- const options: RequestOptions = {
91
- host: '127.0.0.1',
92
- agent: new HttpsAgent({
93
- port: 443,
94
- }),
95
- path: '/resource',
96
- }
97
- const url = getUrlByRequestOptions(options)
98
-
99
- expect(url).toBeInstanceOf(URL)
100
- expect(url).toHaveProperty('port', '')
101
- expect(url).toHaveProperty('href', 'https://127.0.0.1/resource')
68
+ expect(
69
+ getUrlByRequestOptions({
70
+ host: '127.0.0.1',
71
+ agent: new HttpsAgent({
72
+ port: 443,
73
+ }),
74
+ path: '/resource',
75
+ }).href
76
+ ).toBe('https://127.0.0.1/resource')
102
77
  })
103
78
 
104
- it('inherits "port" if given', () => {
105
- const options = {
106
- protocol: 'http:',
107
- host: '127.0.0.1',
108
- port: 4002,
109
- path: '/',
110
- }
111
- const url = getUrlByRequestOptions(options)
112
-
113
- expect(url).toBeInstanceOf(URL)
114
- expect(url).toHaveProperty('port', '4002')
115
- expect(url).toHaveProperty('protocol', 'http:')
116
- expect(url).toHaveProperty('href', 'http://127.0.0.1:4002/')
79
+ it('respects explicitly provided port', () => {
80
+ expect(
81
+ getUrlByRequestOptions({
82
+ protocol: 'http:',
83
+ host: '127.0.0.1',
84
+ port: 4002,
85
+ path: '/',
86
+ }).href
87
+ ).toBe('http://127.0.0.1:4002/')
117
88
  })
118
89
 
119
90
  it('inherits "username" and "password"', () => {
120
- const options: RequestOptions = {
91
+ const url = getUrlByRequestOptions({
121
92
  protocol: 'https:',
122
93
  host: '127.0.0.1',
123
94
  path: '/user',
124
95
  auth: 'admin:abc-123',
125
- }
126
- const url = getUrlByRequestOptions(options)
96
+ })
127
97
 
128
98
  expect(url).toBeInstanceOf(URL)
129
99
  expect(url).toHaveProperty('username', 'admin')
130
100
  expect(url).toHaveProperty('password', 'abc-123')
131
- expect(url).toHaveProperty('protocol', 'https:')
132
101
  expect(url).toHaveProperty('href', 'https://admin:abc-123@127.0.0.1/user')
133
102
  })
134
103
 
135
104
  it('resolves hostname to localhost if none provided', () => {
136
- const url = getUrlByRequestOptions({})
105
+ expect(getUrlByRequestOptions({}).hostname).toBe('localhost')
106
+ })
137
107
 
138
- expect(url).toBeInstanceOf(URL)
139
- expect(url).toHaveProperty('protocol', 'http:')
140
- expect(url).toHaveProperty('href', 'http://localhost/')
108
+ it('supports "hostname" instead of "host" and "port"', () => {
109
+ const options: RequestOptions = {
110
+ protocol: 'https:',
111
+ hostname: '127.0.0.1:1234',
112
+ path: '/resource',
113
+ }
114
+
115
+ expect(getUrlByRequestOptions(options).href).toBe(
116
+ 'https://127.0.0.1:1234/resource'
117
+ )
118
+ })
119
+
120
+ it('handles IPv6 hostnames', () => {
121
+ expect(
122
+ getUrlByRequestOptions({
123
+ host: '::1',
124
+ path: '/resource',
125
+ }).href
126
+ ).toBe('http://[::1]/resource')
127
+
128
+ expect(
129
+ getUrlByRequestOptions({
130
+ host: '::1',
131
+ port: 3001,
132
+ path: '/resource',
133
+ }).href
134
+ ).toBe('http://[::1]:3001/resource')
141
135
  })
@@ -15,7 +15,6 @@ export type ResolvedRequestOptions = RequestOptions & RequestSelf
15
15
  export const DEFAULT_PATH = '/'
16
16
  const DEFAULT_PROTOCOL = 'http:'
17
17
  const DEFAULT_HOST = 'localhost'
18
- const DEFAULT_PORT = 80
19
18
  const SSL_PORT = 443
20
19
 
21
20
  function getAgent(
@@ -45,20 +44,45 @@ function getProtocolByRequestOptions(options: ResolvedRequestOptions): string {
45
44
  function getPortByRequestOptions(
46
45
  options: ResolvedRequestOptions
47
46
  ): number | undefined {
47
+ // Use the explicitly provided port.
48
+ if (options.port) {
49
+ return Number(options.port)
50
+ }
51
+
52
+ // Extract the port from the hostname.
53
+ if (options.hostname != null) {
54
+ const [, extractedPort] = options.hostname.match(/:(\d+)$/) || []
55
+
56
+ if (extractedPort != null) {
57
+ return Number(extractedPort)
58
+ }
59
+ }
60
+
61
+ // Otherwise, try to resolve port from the agent.
48
62
  const agent = getAgent(options)
49
- const agentPort =
50
- (agent as HttpsAgent)?.options.port ||
51
- (agent as RequestOptions)?.defaultPort
52
- const optionsPort = options.port
53
-
54
- if (optionsPort || agentPort) {
55
- const explicitPort = optionsPort || agentPort || DEFAULT_PORT
56
- return Number(explicitPort)
63
+
64
+ if ((agent as HttpsAgent)?.options.port) {
65
+ return Number((agent as HttpsAgent).options.port)
57
66
  }
67
+
68
+ if ((agent as RequestOptions)?.defaultPort) {
69
+ return Number((agent as RequestOptions).defaultPort)
70
+ }
71
+
72
+ // Lastly, return undefined indicating that the port
73
+ // must inferred from the protocol. Do not infer it here.
74
+ return undefined
58
75
  }
59
76
 
60
77
  function getHostByRequestOptions(options: ResolvedRequestOptions): string {
61
- return options.hostname || options.host || DEFAULT_HOST
78
+ const { hostname, host } = options
79
+
80
+ // If the hostname is specified, resolve the host from the "host:port" string.
81
+ if (hostname != null) {
82
+ return hostname.replace(/:\d+$/, '')
83
+ }
84
+
85
+ return host || DEFAULT_HOST
62
86
  }
63
87
 
64
88
  function getAuthByRequestOptions(options: ResolvedRequestOptions) {
@@ -77,45 +101,65 @@ function isRawIPv6Address(host: string): boolean {
77
101
  return host.includes(':') && !host.startsWith('[') && !host.endsWith(']')
78
102
  }
79
103
 
104
+ function getHostname(host: string, port?: number): string {
105
+ const portString = typeof port !== 'undefined' ? `:${port}` : ''
106
+
107
+ /**
108
+ * @note As of Node >= 17, hosts (including "localhost") can resolve to IPv6
109
+ * addresses, so construct valid URL by surrounding the IPv6 host with brackets.
110
+ */
111
+ if (isRawIPv6Address(host)) {
112
+ return `[${host}]${portString}`
113
+ }
114
+
115
+ if (typeof port === 'undefined') {
116
+ return host
117
+ }
118
+
119
+ return `${host}${portString}`
120
+ }
121
+
80
122
  /**
81
123
  * Creates a `URL` instance from a given `RequestOptions` object.
82
124
  */
83
125
  export function getUrlByRequestOptions(options: ResolvedRequestOptions): URL {
84
126
  debug('request options', options)
85
127
 
86
- const protocol = getProtocolByRequestOptions(options)
87
- const host = getHostByRequestOptions(options)
88
- const port = getPortByRequestOptions(options)
89
- const path = options.path || DEFAULT_PATH
90
- const auth = getAuthByRequestOptions(options)
128
+ if (options.uri) {
129
+ debug(
130
+ 'constructing url from explicitly provided "options.uri": %s',
131
+ options.uri
132
+ )
133
+ return new URL(options.uri.href)
134
+ }
91
135
 
136
+ debug('figuring out url from request options...')
137
+
138
+ const protocol = getProtocolByRequestOptions(options)
92
139
  debug('protocol', protocol)
140
+
141
+ const host = getHostByRequestOptions(options)
93
142
  debug('host', host)
94
- debug('port', port)
95
- debug('path', path)
96
143
 
97
- /**
98
- * @note As of Node >= 17, hosts (including "localhost") can resolve to IPv6
99
- * addresses, so construct valid URL by surrounding the IPv6 host with brackets.
100
- */
101
- const baseUrl = `${protocol}//${isRawIPv6Address(host) ? `[${host}]` : host}`
102
- debug('base URL:', baseUrl)
144
+ const port = getPortByRequestOptions(options)
145
+ debug('port', port)
103
146
 
104
- const url = options.uri ? new URL(options.uri.href) : new URL(path, baseUrl)
147
+ const hostname = getHostname(host, port)
148
+ debug('hostname', hostname)
105
149
 
106
- if (port) {
107
- debug('detected explicit port', port)
108
- url.port = port.toString()
109
- }
150
+ const path = options.path || DEFAULT_PATH
151
+ debug('path', path)
110
152
 
111
- if (auth) {
112
- debug('resolved auth', auth)
153
+ const credentials = getAuthByRequestOptions(options)
154
+ debug('credentials', credentials)
113
155
 
114
- url.username = auth.username
115
- url.password = auth.password
116
- }
156
+ const authString = credentials
157
+ ? `${credentials.username}:${credentials.password}@`
158
+ : ''
159
+ debug('auth string:', authString)
117
160
 
118
- debug('created URL:', url)
161
+ const url = new URL(`${protocol}//${authString}${hostname}${path}`)
162
+ debug('created url:', url)
119
163
 
120
164
  return url
121
165
  }