@mswjs/interceptors
Version:
Low-level HTTP/HTTPS/XHR/fetch request interception library.
492 lines (412 loc) • 14.8 kB
text/typescript
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)
})