UNPKG

@mswjs/interceptors

Version:

Low-level HTTP/HTTPS/XHR/fetch request interception library.

492 lines (412 loc) 14.8 kB
import { it, expect } from 'vitest' import { parse } from 'url' import { globalAgent as httpGlobalAgent, RequestOptions } from 'http' import { Agent as HttpsAgent, globalAgent as httpsGlobalAgent } from 'https' import { getUrlByRequestOptions } from '../../../utils/getUrlByRequestOptions' import { normalizeClientRequestArgs } from './normalizeClientRequestArgs' it('handles [string, callback] input', () => { const [url, options, callback] = normalizeClientRequestArgs('https:', [ 'https://mswjs.io/resource', function cb() {}, ]) // URL string must be converted to a URL instance. expect(url.href).toEqual('https://mswjs.io/resource') // Request options must be derived from the URL instance. expect(options).toHaveProperty('method', 'GET') expect(options).toHaveProperty('protocol', 'https:') expect(options).toHaveProperty('hostname', 'mswjs.io') expect(options).toHaveProperty('path', '/resource') // Callback must be preserved. expect(callback?.name).toEqual('cb') }) it('handles [string, RequestOptions, callback] input', () => { const initialOptions = { headers: { 'Content-Type': 'text/plain', }, } const [url, options, callback] = normalizeClientRequestArgs('https:', [ 'https://mswjs.io/resource', initialOptions, function cb() {}, ]) // URL must be created from the string. expect(url.href).toEqual('https://mswjs.io/resource') // Request options must be preserved. expect(options).toHaveProperty('headers', initialOptions.headers) // Callback must be preserved. expect(callback?.name).toEqual('cb') }) it('handles [URL, callback] input', () => { const [url, options, callback] = normalizeClientRequestArgs('https:', [ new URL('https://mswjs.io/resource'), function cb() {}, ]) // URL must be preserved. expect(url.href).toEqual('https://mswjs.io/resource') // Request options must be derived from the URL instance. expect(options.method).toEqual('GET') expect(options.protocol).toEqual('https:') expect(options.hostname).toEqual('mswjs.io') expect(options.path).toEqual('/resource') // Callback must be preserved. expect(callback?.name).toEqual('cb') }) it('handles [Absolute Legacy URL, callback] input', () => { const [url, options, callback] = normalizeClientRequestArgs('https:', [ parse('https://cherry:durian@mswjs.io:12345/resource?apple=banana'), function cb() {}, ]) // URL must be preserved. expect(url.toJSON()).toEqual( new URL( 'https://cherry:durian@mswjs.io:12345/resource?apple=banana' ).toJSON() ) // Request options must be derived from the URL instance. expect(options.method).toEqual('GET') expect(options.protocol).toEqual('https:') expect(options.hostname).toEqual('mswjs.io') expect(options.path).toEqual('/resource?apple=banana') expect(options.port).toEqual(12345) expect(options.auth).toEqual('cherry:durian') // Callback must be preserved. expect(callback?.name).toEqual('cb') }) it('handles [Relative Legacy URL, RequestOptions without path set, callback] input', () => { const [url, options, callback] = normalizeClientRequestArgs('http:', [ parse('/resource?apple=banana'), { host: 'mswjs.io' }, function cb() {}, ]) // Correct WHATWG URL generated. expect(url.toJSON()).toEqual( new URL('http://mswjs.io/resource?apple=banana').toJSON() ) // No path in request options, so legacy url path is copied-in. expect(options.protocol).toEqual('http:') expect(options.host).toEqual('mswjs.io') expect(options.path).toEqual('/resource?apple=banana') // Callback must be preserved. expect(callback?.name).toEqual('cb') }) it('handles [Relative Legacy URL, RequestOptions with path set, callback] input', () => { const [url, options, callback] = normalizeClientRequestArgs('http:', [ parse('/resource?apple=banana'), { host: 'mswjs.io', path: '/other?cherry=durian' }, function cb() {}, ]) // Correct WHATWG URL generated. expect(url.toJSON()).toEqual( new URL('http://mswjs.io/other?cherry=durian').toJSON() ) // Path in request options, so that path is preferred. expect(options.protocol).toEqual('http:') expect(options.host).toEqual('mswjs.io') expect(options.path).toEqual('/other?cherry=durian') // Callback must be preserved. expect(callback?.name).toEqual('cb') }) it('handles [Relative Legacy URL, callback] input', () => { const [url, options, callback] = normalizeClientRequestArgs('http:', [ parse('/resource?apple=banana'), function cb() {}, ]) // Correct WHATWG URL generated. expect(url.toJSON()).toMatch( getUrlByRequestOptions({ path: '/resource?apple=banana' }).toJSON() ) // Check path is in options. expect(options.protocol).toEqual('http:') expect(options.path).toEqual('/resource?apple=banana') // Callback must be preserved. expect(callback).toBeTypeOf('function') expect(callback?.name).toEqual('cb') }) it('handles [Relative Legacy URL] input', () => { const [url, options, callback] = normalizeClientRequestArgs('http:', [ parse('/resource?apple=banana'), ]) // Correct WHATWG URL generated. expect(url.toJSON()).toMatch( getUrlByRequestOptions({ path: '/resource?apple=banana' }).toJSON() ) // Check path is in options. expect(options.protocol).toEqual('http:') expect(options.path).toEqual('/resource?apple=banana') // Callback must be preserved. expect(callback).toBeUndefined() }) it('handles [URL, RequestOptions, callback] input', () => { const [url, options, callback] = normalizeClientRequestArgs('https:', [ new URL('https://mswjs.io/resource'), { agent: false, headers: { 'Content-Type': 'text/plain', }, }, function cb() {}, ]) // URL must be preserved. expect(url.href).toEqual('https://mswjs.io/resource') // Options must be preserved. // `urlToHttpOptions` from `node:url` generates additional // ClientRequest options, some of which are not legally allowed. expect(options).toMatchObject<RequestOptions>({ agent: false, _defaultAgent: httpsGlobalAgent, protocol: url.protocol, method: 'GET', headers: { 'Content-Type': 'text/plain', }, hostname: url.hostname, path: url.pathname, }) // Callback must be preserved. expect(callback).toBeTypeOf('function') expect(callback?.name).toEqual('cb') }) it('handles [URL, RequestOptions] where options have custom "hostname"', () => { const [url, options] = normalizeClientRequestArgs('http:', [ new URL('http://example.com/path-from-url'), { hostname: 'host-from-options.com', }, ]) expect(url.href).toBe('http://host-from-options.com/path-from-url') expect(options).toMatchObject({ hostname: 'host-from-options.com', path: '/path-from-url', }) }) it('handles [URL, RequestOptions] where options contain "host" and "path" and "port"', () => { const [url, options] = normalizeClientRequestArgs('http:', [ new URL('http://example.com/path-from-url?a=b&c=d'), { hostname: 'host-from-options.com', path: '/path-from-options', port: 1234, }, ]) // Must remove the query string since it's not specified in "options.path" expect(url.href).toBe('http://host-from-options.com:1234/path-from-options') expect(options).toMatchObject<RequestOptions>({ hostname: 'host-from-options.com', path: '/path-from-options', port: 1234, }) }) it('handles [URL, RequestOptions] where options contain "path" with query string', () => { const [url, options] = normalizeClientRequestArgs('http:', [ new URL('http://example.com/path-from-url?a=b&c=d'), { path: '/path-from-options?foo=bar&baz=xyz', }, ]) expect(url.href).toBe('http://example.com/path-from-options?foo=bar&baz=xyz') expect(options).toMatchObject<RequestOptions>({ hostname: 'example.com', path: '/path-from-options?foo=bar&baz=xyz', }) }) it('handles [RequestOptions, callback] input', () => { const initialOptions = { method: 'POST', protocol: 'https:', host: 'mswjs.io', /** * @see https://github.com/mswjs/msw/issues/705 */ origin: 'https://mswjs.io', path: '/resource', headers: { 'Content-Type': 'text/plain', }, } const [url, options, callback] = normalizeClientRequestArgs('https:', [ initialOptions, function cb() {}, ]) // URL must be derived from request options. expect(url.href).toEqual('https://mswjs.io/resource') // Request options must be preserved. expect(options).toMatchObject(initialOptions) // Callback must be preserved. expect(callback).toBeTypeOf('function') expect(callback?.name).toEqual('cb') }) it('handles [Empty RequestOptions, callback] input', () => { const [_, options, callback] = normalizeClientRequestArgs('https:', [ {}, function cb() {}, ]) expect(options.protocol).toEqual('https:') // Callback must be preserved expect(callback?.name).toEqual('cb') }) /** * @see https://github.com/mswjs/interceptors/issues/19 */ it('handles [PartialRequestOptions, callback] input', () => { const initialOptions = { method: 'GET', port: '50176', path: '/resource', host: '127.0.0.1', ca: undefined, key: undefined, pfx: undefined, cert: undefined, passphrase: undefined, agent: false, } const [url, options, callback] = normalizeClientRequestArgs('https:', [ initialOptions, function cb() {}, ]) // URL must be derived from request options. expect(url.toJSON()).toEqual( new URL('https://127.0.0.1:50176/resource').toJSON() ) // Request options must be preserved. expect(options).toMatchObject(initialOptions) // Options protocol must be inferred from the request issuing module. expect(options.protocol).toEqual('https:') // Callback must be preserved. expect(callback).toBeTypeOf('function') expect(callback?.name).toEqual('cb') }) it('sets fallback Agent based on the URL protocol', () => { const [url, options] = normalizeClientRequestArgs('https:', [ 'https://github.com', ]) const agent = options.agent as HttpsAgent expect(agent).toBeInstanceOf(HttpsAgent) expect(agent).toHaveProperty('defaultPort', 443) expect(agent).toHaveProperty('protocol', url.protocol) }) it('preserves `requestUnauthorized` option set to undefined', () => { const [, options] = normalizeClientRequestArgs('https:', [ 'https://github.com', { rejectUnauthorized: undefined }, ]) expect(options.rejectUnauthorized).toBe(undefined) expect((options.agent as HttpsAgent).options.rejectUnauthorized).toBe( undefined ) }) it('preserves `requestUnauthorized` option set to true', () => { const [, options] = normalizeClientRequestArgs('https:', [ 'https://github.com', { rejectUnauthorized: true }, ]) expect(options.rejectUnauthorized).toBe(true) expect((options.agent as HttpsAgent).options.rejectUnauthorized).toBe(true) }) it('preserves `requestUnauthorized` option set to false', () => { const [, options] = normalizeClientRequestArgs('https:', [ 'https://github.com', { rejectUnauthorized: false }, ]) expect(options.rejectUnauthorized).toBe(false) expect((options.agent as HttpsAgent).options.rejectUnauthorized).toBe(false) }) it('does not add `rejectUnauthorized` value if not set', () => { const agent = new HttpsAgent() const [, options] = normalizeClientRequestArgs('https:', [ 'https://github.com', ]) expect(options).not.toHaveProperty('rejectUnauthorized') expect((options.agent as HttpsAgent).options).not.toHaveProperty( 'rejectUnauthorized' ) }) it('does not set any fallback Agent given "agent: false" option', () => { const [, options] = normalizeClientRequestArgs('https:', [ 'https://github.com', { agent: false }, ]) expect(options.agent).toEqual(false) }) it('sets the default Agent for HTTP request', () => { const [, options] = normalizeClientRequestArgs('http:', [ 'http://github.com', {}, ]) expect(options._defaultAgent).toEqual(httpGlobalAgent) }) it('sets the default Agent for HTTPS request', () => { const [, options] = normalizeClientRequestArgs('https:', [ 'https://github.com', {}, ]) expect(options._defaultAgent).toEqual(httpsGlobalAgent) }) it('preserves a custom default Agent when set', () => { const [, options] = normalizeClientRequestArgs('https:', [ 'https://github.com', { /** * @note Intentionally incorrect Agent for HTTPS request. */ _defaultAgent: httpGlobalAgent, }, ]) expect(options._defaultAgent).toEqual(httpGlobalAgent) }) it('merges URL-based RequestOptions with the custom RequestOptions', () => { const [url, options] = normalizeClientRequestArgs('https:', [ 'https://github.com/graphql', { method: 'GET', pfx: 'PFX_KEY', }, ]) expect(url.href).toEqual('https://github.com/graphql') // Original request options must be preserved. expect(options.method).toEqual('GET') expect(options.pfx).toEqual('PFX_KEY') // Other options must be inferred from the URL. expect(options.protocol).toEqual(url.protocol) expect(options.hostname).toEqual(url.hostname) expect(options.path).toEqual(url.pathname) }) it('respects custom "options.path" over URL path', () => { const [url, options] = normalizeClientRequestArgs('http:', [ new URL('http://example.com/path-from-url'), { path: '/path-from-options', }, ]) expect(url.href).toBe('http://example.com/path-from-options') expect(options.protocol).toBe('http:') expect(options.hostname).toBe('example.com') expect(options.path).toBe('/path-from-options') }) it('respects custom "options.path" over URL path with query string', () => { const [url, options] = normalizeClientRequestArgs('http:', [ new URL('http://example.com/path-from-url?a=b&c=d'), { path: '/path-from-options', }, ]) // Must replace both the path and the query string. expect(url.href).toBe('http://example.com/path-from-options') expect(options.protocol).toBe('http:') expect(options.hostname).toBe('example.com') expect(options.path).toBe('/path-from-options') }) it('preserves URL query string', () => { const [url, options] = normalizeClientRequestArgs('http:', [ new URL('http://example.com:8080/resource?a=b&c=d'), ]) expect(url.href).toBe('http://example.com:8080/resource?a=b&c=d') expect(options.protocol).toBe('http:') // expect(options.host).toBe('example.com:8080') expect(options.hostname).toBe('example.com') // Query string is a part of the options path. expect(options.path).toBe('/resource?a=b&c=d') expect(options.port).toBe(8080) })