修改后台权限
This commit is contained in:
255
node_modules/@mswjs/interceptors/src/BatchInterceptor.test.ts
generated
vendored
Normal file
255
node_modules/@mswjs/interceptors/src/BatchInterceptor.test.ts
generated
vendored
Normal file
@@ -0,0 +1,255 @@
|
||||
import { vi, it, expect, afterEach } from 'vitest'
|
||||
import { Interceptor } from './Interceptor'
|
||||
import { BatchInterceptor } from './BatchInterceptor'
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
|
||||
it('applies child interceptors', () => {
|
||||
class PrimaryInterceptor extends Interceptor<any> {
|
||||
constructor() {
|
||||
super(Symbol('primary'))
|
||||
}
|
||||
}
|
||||
|
||||
class SecondaryInterceptor extends Interceptor<any> {
|
||||
constructor() {
|
||||
super(Symbol('secondary'))
|
||||
}
|
||||
}
|
||||
|
||||
const instances = {
|
||||
primary: new PrimaryInterceptor(),
|
||||
secondary: new SecondaryInterceptor(),
|
||||
}
|
||||
|
||||
const interceptor = new BatchInterceptor({
|
||||
name: 'batch-apply',
|
||||
interceptors: [instances.primary, instances.secondary],
|
||||
})
|
||||
|
||||
const primaryApplySpy = vi.spyOn(instances.primary, 'apply')
|
||||
const secondaryApplySpy = vi.spyOn(instances.secondary, 'apply')
|
||||
|
||||
interceptor.apply()
|
||||
|
||||
expect(primaryApplySpy).toHaveBeenCalledTimes(1)
|
||||
expect(secondaryApplySpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('proxies event listeners to the interceptors', () => {
|
||||
class PrimaryInterceptor extends Interceptor<{ hello: [string] }> {
|
||||
constructor() {
|
||||
super(Symbol('primary'))
|
||||
}
|
||||
}
|
||||
|
||||
class SecondaryInterceptor extends Interceptor<{
|
||||
goodbye: [string]
|
||||
}> {
|
||||
constructor() {
|
||||
super(Symbol('secondary'))
|
||||
}
|
||||
}
|
||||
|
||||
const instances = {
|
||||
primary: new PrimaryInterceptor(),
|
||||
secondary: new SecondaryInterceptor(),
|
||||
}
|
||||
|
||||
const interceptor = new BatchInterceptor({
|
||||
name: 'batch-proxy',
|
||||
interceptors: [instances.primary, instances.secondary],
|
||||
})
|
||||
|
||||
const helloListener = vi.fn()
|
||||
interceptor.on('hello', helloListener)
|
||||
|
||||
const goodbyeListener = vi.fn()
|
||||
interceptor.on('goodbye', goodbyeListener)
|
||||
|
||||
// Emulate the child interceptor emitting events.
|
||||
instances.primary['emitter'].emit('hello', 'John')
|
||||
instances.secondary['emitter'].emit('goodbye', 'Kate')
|
||||
|
||||
// Must call the batch interceptor listener.
|
||||
expect(helloListener).toHaveBeenCalledTimes(1)
|
||||
expect(helloListener).toHaveBeenCalledWith('John')
|
||||
expect(goodbyeListener).toHaveBeenCalledTimes(1)
|
||||
expect(goodbyeListener).toHaveBeenCalledWith('Kate')
|
||||
})
|
||||
|
||||
it('disposes of child interceptors', async () => {
|
||||
class PrimaryInterceptor extends Interceptor<any> {
|
||||
constructor() {
|
||||
super(Symbol('primary'))
|
||||
}
|
||||
}
|
||||
|
||||
class SecondaryInterceptor extends Interceptor<any> {
|
||||
constructor() {
|
||||
super(Symbol('secondary'))
|
||||
}
|
||||
}
|
||||
|
||||
const instances = {
|
||||
primary: new PrimaryInterceptor(),
|
||||
secondary: new SecondaryInterceptor(),
|
||||
}
|
||||
|
||||
const interceptor = new BatchInterceptor({
|
||||
name: 'batch-dispose',
|
||||
interceptors: [instances.primary, instances.secondary],
|
||||
})
|
||||
|
||||
const primaryDisposeSpy = vi.spyOn(instances.primary, 'dispose')
|
||||
const secondaryDisposeSpy = vi.spyOn(instances.secondary, 'dispose')
|
||||
|
||||
interceptor.apply()
|
||||
interceptor.dispose()
|
||||
|
||||
expect(primaryDisposeSpy).toHaveBeenCalledTimes(1)
|
||||
expect(secondaryDisposeSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('forwards listeners added via "on()"', () => {
|
||||
class FirstInterceptor extends Interceptor<any> {
|
||||
constructor() {
|
||||
super(Symbol('first'))
|
||||
}
|
||||
}
|
||||
class SecondaryInterceptor extends Interceptor<any> {
|
||||
constructor() {
|
||||
super(Symbol('second'))
|
||||
}
|
||||
}
|
||||
|
||||
const firstInterceptor = new FirstInterceptor()
|
||||
const secondInterceptor = new SecondaryInterceptor()
|
||||
|
||||
const interceptor = new BatchInterceptor({
|
||||
name: 'batch',
|
||||
interceptors: [firstInterceptor, secondInterceptor],
|
||||
})
|
||||
|
||||
const listener = vi.fn()
|
||||
interceptor.on('foo', listener)
|
||||
|
||||
expect(firstInterceptor['emitter'].listenerCount('foo')).toBe(1)
|
||||
expect(secondInterceptor['emitter'].listenerCount('foo')).toBe(1)
|
||||
expect(interceptor['emitter'].listenerCount('foo')).toBe(0)
|
||||
})
|
||||
|
||||
it('forwards listeners removal via "off()"', () => {
|
||||
type Events = {
|
||||
foo: []
|
||||
}
|
||||
|
||||
class FirstInterceptor extends Interceptor<Events> {
|
||||
constructor() {
|
||||
super(Symbol('first'))
|
||||
}
|
||||
}
|
||||
class SecondaryInterceptor extends Interceptor<Events> {
|
||||
constructor() {
|
||||
super(Symbol('second'))
|
||||
}
|
||||
}
|
||||
|
||||
const firstInterceptor = new FirstInterceptor()
|
||||
const secondInterceptor = new SecondaryInterceptor()
|
||||
|
||||
const interceptor = new BatchInterceptor({
|
||||
name: 'batch',
|
||||
interceptors: [firstInterceptor, secondInterceptor],
|
||||
})
|
||||
|
||||
const listener = vi.fn()
|
||||
interceptor.on('foo', listener)
|
||||
interceptor.off('foo', listener)
|
||||
|
||||
expect(firstInterceptor['emitter'].listenerCount('foo')).toBe(0)
|
||||
expect(secondInterceptor['emitter'].listenerCount('foo')).toBe(0)
|
||||
})
|
||||
|
||||
it('forwards removal of all listeners by name via ".removeAllListeners()"', () => {
|
||||
type Events = {
|
||||
foo: []
|
||||
bar: []
|
||||
}
|
||||
|
||||
class FirstInterceptor extends Interceptor<Events> {
|
||||
constructor() {
|
||||
super(Symbol('first'))
|
||||
}
|
||||
}
|
||||
class SecondaryInterceptor extends Interceptor<Events> {
|
||||
constructor() {
|
||||
super(Symbol('second'))
|
||||
}
|
||||
}
|
||||
|
||||
const firstInterceptor = new FirstInterceptor()
|
||||
const secondInterceptor = new SecondaryInterceptor()
|
||||
|
||||
const interceptor = new BatchInterceptor({
|
||||
name: 'batch',
|
||||
interceptors: [firstInterceptor, secondInterceptor],
|
||||
})
|
||||
|
||||
const listener = vi.fn()
|
||||
interceptor.on('foo', listener)
|
||||
interceptor.on('foo', listener)
|
||||
interceptor.on('bar', listener)
|
||||
|
||||
expect(firstInterceptor['emitter'].listenerCount('foo')).toBe(2)
|
||||
expect(secondInterceptor['emitter'].listenerCount('foo')).toBe(2)
|
||||
expect(firstInterceptor['emitter'].listenerCount('bar')).toBe(1)
|
||||
expect(secondInterceptor['emitter'].listenerCount('bar')).toBe(1)
|
||||
|
||||
interceptor.removeAllListeners('foo')
|
||||
|
||||
expect(firstInterceptor['emitter'].listenerCount('foo')).toBe(0)
|
||||
expect(secondInterceptor['emitter'].listenerCount('foo')).toBe(0)
|
||||
expect(firstInterceptor['emitter'].listenerCount('bar')).toBe(1)
|
||||
expect(secondInterceptor['emitter'].listenerCount('bar')).toBe(1)
|
||||
})
|
||||
|
||||
it('forwards removal of all listeners via ".removeAllListeners()"', () => {
|
||||
class FirstInterceptor extends Interceptor<any> {
|
||||
constructor() {
|
||||
super(Symbol('first'))
|
||||
}
|
||||
}
|
||||
class SecondaryInterceptor extends Interceptor<any> {
|
||||
constructor() {
|
||||
super(Symbol('second'))
|
||||
}
|
||||
}
|
||||
|
||||
const firstInterceptor = new FirstInterceptor()
|
||||
const secondInterceptor = new SecondaryInterceptor()
|
||||
|
||||
const interceptor = new BatchInterceptor({
|
||||
name: 'batch',
|
||||
interceptors: [firstInterceptor, secondInterceptor],
|
||||
})
|
||||
|
||||
const listener = vi.fn()
|
||||
interceptor.on('foo', listener)
|
||||
interceptor.on('foo', listener)
|
||||
interceptor.on('bar', listener)
|
||||
|
||||
expect(firstInterceptor['emitter'].listenerCount('foo')).toBe(2)
|
||||
expect(secondInterceptor['emitter'].listenerCount('foo')).toBe(2)
|
||||
expect(firstInterceptor['emitter'].listenerCount('bar')).toBe(1)
|
||||
expect(secondInterceptor['emitter'].listenerCount('bar')).toBe(1)
|
||||
|
||||
interceptor.removeAllListeners()
|
||||
|
||||
expect(firstInterceptor['emitter'].listenerCount('foo')).toBe(0)
|
||||
expect(secondInterceptor['emitter'].listenerCount('foo')).toBe(0)
|
||||
expect(firstInterceptor['emitter'].listenerCount('bar')).toBe(0)
|
||||
expect(secondInterceptor['emitter'].listenerCount('bar')).toBe(0)
|
||||
})
|
||||
95
node_modules/@mswjs/interceptors/src/BatchInterceptor.ts
generated
vendored
Normal file
95
node_modules/@mswjs/interceptors/src/BatchInterceptor.ts
generated
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
import { EventMap, Listener } from 'strict-event-emitter'
|
||||
import { Interceptor, ExtractEventNames } from './Interceptor'
|
||||
|
||||
export interface BatchInterceptorOptions<
|
||||
InterceptorList extends ReadonlyArray<Interceptor<any>>
|
||||
> {
|
||||
name: string
|
||||
interceptors: InterceptorList
|
||||
}
|
||||
|
||||
export type ExtractEventMapType<
|
||||
InterceptorList extends ReadonlyArray<Interceptor<any>>
|
||||
> = InterceptorList extends ReadonlyArray<infer InterceptorType>
|
||||
? InterceptorType extends Interceptor<infer EventMap>
|
||||
? EventMap
|
||||
: never
|
||||
: never
|
||||
|
||||
/**
|
||||
* A batch interceptor that exposes a single interface
|
||||
* to apply and operate with multiple interceptors at once.
|
||||
*/
|
||||
export class BatchInterceptor<
|
||||
InterceptorList extends ReadonlyArray<Interceptor<any>>,
|
||||
Events extends EventMap = ExtractEventMapType<InterceptorList>
|
||||
> extends Interceptor<Events> {
|
||||
static symbol: symbol
|
||||
|
||||
private interceptors: InterceptorList
|
||||
|
||||
constructor(options: BatchInterceptorOptions<InterceptorList>) {
|
||||
BatchInterceptor.symbol = Symbol(options.name)
|
||||
super(BatchInterceptor.symbol)
|
||||
this.interceptors = options.interceptors
|
||||
}
|
||||
|
||||
protected setup() {
|
||||
const logger = this.logger.extend('setup')
|
||||
|
||||
logger.info('applying all %d interceptors...', this.interceptors.length)
|
||||
|
||||
for (const interceptor of this.interceptors) {
|
||||
logger.info('applying "%s" interceptor...', interceptor.constructor.name)
|
||||
interceptor.apply()
|
||||
|
||||
logger.info('adding interceptor dispose subscription')
|
||||
this.subscriptions.push(() => interceptor.dispose())
|
||||
}
|
||||
}
|
||||
|
||||
public on<EventName extends ExtractEventNames<Events>>(
|
||||
event: EventName,
|
||||
listener: Listener<Events[EventName]>
|
||||
): this {
|
||||
// Instead of adding a listener to the batch interceptor,
|
||||
// propagate the listener to each of the individual interceptors.
|
||||
for (const interceptor of this.interceptors) {
|
||||
interceptor.on(event, listener)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public once<EventName extends ExtractEventNames<Events>>(
|
||||
event: EventName,
|
||||
listener: Listener<Events[EventName]>
|
||||
): this {
|
||||
for (const interceptor of this.interceptors) {
|
||||
interceptor.once(event, listener)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public off<EventName extends ExtractEventNames<Events>>(
|
||||
event: EventName,
|
||||
listener: Listener<Events[EventName]>
|
||||
): this {
|
||||
for (const interceptor of this.interceptors) {
|
||||
interceptor.off(event, listener)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public removeAllListeners<EventName extends ExtractEventNames<Events>>(
|
||||
event?: EventName | undefined
|
||||
): this {
|
||||
for (const interceptors of this.interceptors) {
|
||||
interceptors.removeAllListeners(event)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
||||
205
node_modules/@mswjs/interceptors/src/Interceptor.test.ts
generated
vendored
Normal file
205
node_modules/@mswjs/interceptors/src/Interceptor.test.ts
generated
vendored
Normal file
@@ -0,0 +1,205 @@
|
||||
import { describe, vi, it, expect, afterEach } from 'vitest'
|
||||
import {
|
||||
Interceptor,
|
||||
getGlobalSymbol,
|
||||
deleteGlobalSymbol,
|
||||
InterceptorReadyState,
|
||||
} from './Interceptor'
|
||||
import { nextTickAsync } from './utils/nextTick'
|
||||
|
||||
const symbol = Symbol('test')
|
||||
|
||||
afterEach(() => {
|
||||
deleteGlobalSymbol(symbol)
|
||||
})
|
||||
|
||||
it('does not set a maximum listeners limit', () => {
|
||||
const interceptor = new Interceptor(symbol)
|
||||
expect(interceptor['emitter'].getMaxListeners()).toBe(0)
|
||||
})
|
||||
|
||||
describe('on()', () => {
|
||||
it('adds a new listener using "on()"', () => {
|
||||
const interceptor = new Interceptor(symbol)
|
||||
expect(interceptor['emitter'].listenerCount('event')).toBe(0)
|
||||
|
||||
const listener = vi.fn()
|
||||
interceptor.on('event', listener)
|
||||
expect(interceptor['emitter'].listenerCount('event')).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('once()', () => {
|
||||
it('calls the listener only once', () => {
|
||||
const interceptor = new Interceptor(symbol)
|
||||
const listener = vi.fn()
|
||||
|
||||
interceptor.once('foo', listener)
|
||||
expect(listener).not.toHaveBeenCalled()
|
||||
|
||||
interceptor['emitter'].emit('foo', 'bar')
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(1)
|
||||
expect(listener).toHaveBeenCalledWith('bar')
|
||||
|
||||
listener.mockReset()
|
||||
|
||||
interceptor['emitter'].emit('foo', 'baz')
|
||||
interceptor['emitter'].emit('foo', 'xyz')
|
||||
expect(listener).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('off()', () => {
|
||||
it('removes a listener using "off()"', () => {
|
||||
const interceptor = new Interceptor(symbol)
|
||||
expect(interceptor['emitter'].listenerCount('event')).toBe(0)
|
||||
|
||||
const listener = vi.fn()
|
||||
interceptor.on('event', listener)
|
||||
expect(interceptor['emitter'].listenerCount('event')).toBe(1)
|
||||
|
||||
interceptor.off('event', listener)
|
||||
expect(interceptor['emitter'].listenerCount('event')).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('persistence', () => {
|
||||
it('stores global reference to the applied interceptor', () => {
|
||||
const interceptor = new Interceptor(symbol)
|
||||
interceptor.apply()
|
||||
|
||||
expect(getGlobalSymbol(symbol)).toEqual(interceptor)
|
||||
})
|
||||
|
||||
it('deletes global reference when the interceptor is disposed', () => {
|
||||
const interceptor = new Interceptor(symbol)
|
||||
|
||||
interceptor.apply()
|
||||
interceptor.dispose()
|
||||
|
||||
expect(getGlobalSymbol(symbol)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('readyState', () => {
|
||||
it('sets the state to "INACTIVE" when the interceptor is created', () => {
|
||||
const interceptor = new Interceptor(symbol)
|
||||
expect(interceptor.readyState).toBe(InterceptorReadyState.INACTIVE)
|
||||
})
|
||||
|
||||
it('leaves state as "INACTIVE" if the interceptor failed the environment check', async () => {
|
||||
class MyInterceptor extends Interceptor<any> {
|
||||
protected checkEnvironment(): boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
const interceptor = new MyInterceptor(symbol)
|
||||
interceptor.apply()
|
||||
|
||||
expect(interceptor.readyState).toBe(InterceptorReadyState.INACTIVE)
|
||||
})
|
||||
|
||||
it('performs state transition when the interceptor is applying', async () => {
|
||||
const interceptor = new Interceptor(symbol)
|
||||
interceptor.apply()
|
||||
|
||||
// The interceptor's state transitions to APPLIED immediately.
|
||||
// The only exception is if something throws during the setup.
|
||||
expect(interceptor.readyState).toBe(InterceptorReadyState.APPLIED)
|
||||
})
|
||||
|
||||
it('performs state transition when disposing of the interceptor', async () => {
|
||||
const interceptor = new Interceptor(symbol)
|
||||
interceptor.apply()
|
||||
interceptor.dispose()
|
||||
|
||||
// The interceptor's state transitions to DISPOSED immediately.
|
||||
// The only exception is if something throws during the teardown.
|
||||
expect(interceptor.readyState).toBe(InterceptorReadyState.DISPOSED)
|
||||
})
|
||||
})
|
||||
|
||||
describe('apply', () => {
|
||||
it('does not apply the same interceptor multiple times', () => {
|
||||
const interceptor = new Interceptor(symbol)
|
||||
const setupSpy = vi.spyOn(
|
||||
interceptor,
|
||||
// @ts-expect-error Protected property spy.
|
||||
'setup'
|
||||
)
|
||||
|
||||
// Intentionally apply the same interceptor multiple times.
|
||||
interceptor.apply()
|
||||
interceptor.apply()
|
||||
interceptor.apply()
|
||||
|
||||
// The "setup" must not be called repeatedly.
|
||||
expect(setupSpy).toHaveBeenCalledTimes(1)
|
||||
|
||||
expect(getGlobalSymbol(symbol)).toEqual(interceptor)
|
||||
})
|
||||
|
||||
it('does not call "apply" if the interceptor fails environment check', () => {
|
||||
class MyInterceptor extends Interceptor<{}> {
|
||||
checkEnvironment() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const interceptor = new MyInterceptor(Symbol('test'))
|
||||
const setupSpy = vi.spyOn(
|
||||
interceptor,
|
||||
// @ts-expect-error Protected property spy.
|
||||
'setup'
|
||||
)
|
||||
interceptor.apply()
|
||||
|
||||
expect(setupSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('proxies listeners from new interceptor to already running interceptor', () => {
|
||||
const firstInterceptor = new Interceptor(symbol)
|
||||
const secondInterceptor = new Interceptor(symbol)
|
||||
|
||||
firstInterceptor.apply()
|
||||
const firstListener = vi.fn()
|
||||
firstInterceptor.on('test', firstListener)
|
||||
|
||||
secondInterceptor.apply()
|
||||
const secondListener = vi.fn()
|
||||
secondInterceptor.on('test', secondListener)
|
||||
|
||||
// Emitting event in the first interceptor will bubble to the second one.
|
||||
firstInterceptor['emitter'].emit('test', 'hello world')
|
||||
|
||||
expect(firstListener).toHaveBeenCalledTimes(1)
|
||||
expect(firstListener).toHaveBeenCalledWith('hello world')
|
||||
|
||||
expect(secondListener).toHaveBeenCalledTimes(1)
|
||||
expect(secondListener).toHaveBeenCalledWith('hello world')
|
||||
|
||||
expect(secondInterceptor['emitter'].listenerCount('test')).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('dispose', () => {
|
||||
it('removes all listeners when the interceptor is disposed', async () => {
|
||||
const interceptor = new Interceptor(symbol)
|
||||
|
||||
interceptor.apply()
|
||||
const listener = vi.fn()
|
||||
interceptor.on('test', listener)
|
||||
interceptor.dispose()
|
||||
|
||||
// Even after emitting an event, the listener must not get called.
|
||||
interceptor['emitter'].emit('test')
|
||||
expect(listener).not.toHaveBeenCalled()
|
||||
|
||||
// The listener must not be called on the next tick either.
|
||||
await nextTickAsync(() => {
|
||||
interceptor['emitter'].emit('test')
|
||||
expect(listener).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
249
node_modules/@mswjs/interceptors/src/Interceptor.ts
generated
vendored
Normal file
249
node_modules/@mswjs/interceptors/src/Interceptor.ts
generated
vendored
Normal file
@@ -0,0 +1,249 @@
|
||||
import { Logger } from '@open-draft/logger'
|
||||
import { Emitter, Listener } from 'strict-event-emitter'
|
||||
|
||||
export type InterceptorEventMap = Record<string, any>
|
||||
export type InterceptorSubscription = () => void
|
||||
|
||||
/**
|
||||
* Request header name to detect when a single request
|
||||
* is being handled by nested interceptors (XHR -> ClientRequest).
|
||||
* Obscure by design to prevent collisions with user-defined headers.
|
||||
* Ideally, come up with the Interceptor-level mechanism for this.
|
||||
* @see https://github.com/mswjs/interceptors/issues/378
|
||||
*/
|
||||
export const INTERNAL_REQUEST_ID_HEADER_NAME =
|
||||
'x-interceptors-internal-request-id'
|
||||
|
||||
export function getGlobalSymbol<V>(symbol: Symbol): V | undefined {
|
||||
return (
|
||||
// @ts-ignore https://github.com/Microsoft/TypeScript/issues/24587
|
||||
globalThis[symbol] || undefined
|
||||
)
|
||||
}
|
||||
|
||||
function setGlobalSymbol(symbol: Symbol, value: any): void {
|
||||
// @ts-ignore
|
||||
globalThis[symbol] = value
|
||||
}
|
||||
|
||||
export function deleteGlobalSymbol(symbol: Symbol): void {
|
||||
// @ts-ignore
|
||||
delete globalThis[symbol]
|
||||
}
|
||||
|
||||
export enum InterceptorReadyState {
|
||||
INACTIVE = 'INACTIVE',
|
||||
APPLYING = 'APPLYING',
|
||||
APPLIED = 'APPLIED',
|
||||
DISPOSING = 'DISPOSING',
|
||||
DISPOSED = 'DISPOSED',
|
||||
}
|
||||
|
||||
export type ExtractEventNames<Events extends Record<string, any>> =
|
||||
Events extends Record<infer EventName, any> ? EventName : never
|
||||
|
||||
export class Interceptor<Events extends InterceptorEventMap> {
|
||||
protected emitter: Emitter<Events>
|
||||
protected subscriptions: Array<InterceptorSubscription>
|
||||
protected logger: Logger
|
||||
|
||||
public readyState: InterceptorReadyState
|
||||
|
||||
constructor(private readonly symbol: symbol) {
|
||||
this.readyState = InterceptorReadyState.INACTIVE
|
||||
|
||||
this.emitter = new Emitter()
|
||||
this.subscriptions = []
|
||||
this.logger = new Logger(symbol.description!)
|
||||
|
||||
// Do not limit the maximum number of listeners
|
||||
// so not to limit the maximum amount of parallel events emitted.
|
||||
this.emitter.setMaxListeners(0)
|
||||
|
||||
this.logger.info('constructing the interceptor...')
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if this interceptor can be applied
|
||||
* in the current environment.
|
||||
*/
|
||||
protected checkEnvironment(): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply this interceptor to the current process.
|
||||
* Returns an already running interceptor instance if it's present.
|
||||
*/
|
||||
public apply(): void {
|
||||
const logger = this.logger.extend('apply')
|
||||
logger.info('applying the interceptor...')
|
||||
|
||||
if (this.readyState === InterceptorReadyState.APPLIED) {
|
||||
logger.info('intercepted already applied!')
|
||||
return
|
||||
}
|
||||
|
||||
const shouldApply = this.checkEnvironment()
|
||||
|
||||
if (!shouldApply) {
|
||||
logger.info('the interceptor cannot be applied in this environment!')
|
||||
return
|
||||
}
|
||||
|
||||
this.readyState = InterceptorReadyState.APPLYING
|
||||
|
||||
// Whenever applying a new interceptor, check if it hasn't been applied already.
|
||||
// This enables to apply the same interceptor multiple times, for example from a different
|
||||
// interceptor, only proxying events but keeping the stubs in a single place.
|
||||
const runningInstance = this.getInstance()
|
||||
|
||||
if (runningInstance) {
|
||||
logger.info('found a running instance, reusing...')
|
||||
|
||||
// Proxy any listeners you set on this instance to the running instance.
|
||||
this.on = (event, listener) => {
|
||||
logger.info('proxying the "%s" listener', event)
|
||||
|
||||
// Add listeners to the running instance so they appear
|
||||
// at the top of the event listeners list and are executed first.
|
||||
runningInstance.emitter.addListener(event, listener)
|
||||
|
||||
// Ensure that once this interceptor instance is disposed,
|
||||
// it removes all listeners it has appended to the running interceptor instance.
|
||||
this.subscriptions.push(() => {
|
||||
runningInstance.emitter.removeListener(event, listener)
|
||||
logger.info('removed proxied "%s" listener!', event)
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
this.readyState = InterceptorReadyState.APPLIED
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.info('no running instance found, setting up a new instance...')
|
||||
|
||||
// Setup the interceptor.
|
||||
this.setup()
|
||||
|
||||
// Store the newly applied interceptor instance globally.
|
||||
this.setInstance()
|
||||
|
||||
this.readyState = InterceptorReadyState.APPLIED
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the module augments and stubs necessary for this interceptor.
|
||||
* This method is not run if there's a running interceptor instance
|
||||
* to prevent instantiating an interceptor multiple times.
|
||||
*/
|
||||
protected setup(): void {}
|
||||
|
||||
/**
|
||||
* Listen to the interceptor's public events.
|
||||
*/
|
||||
public on<EventName extends ExtractEventNames<Events>>(
|
||||
event: EventName,
|
||||
listener: Listener<Events[EventName]>
|
||||
): this {
|
||||
const logger = this.logger.extend('on')
|
||||
|
||||
if (
|
||||
this.readyState === InterceptorReadyState.DISPOSING ||
|
||||
this.readyState === InterceptorReadyState.DISPOSED
|
||||
) {
|
||||
logger.info('cannot listen to events, already disposed!')
|
||||
return this
|
||||
}
|
||||
|
||||
logger.info('adding "%s" event listener:', event, listener)
|
||||
|
||||
this.emitter.on(event, listener)
|
||||
return this
|
||||
}
|
||||
|
||||
public once<EventName extends ExtractEventNames<Events>>(
|
||||
event: EventName,
|
||||
listener: Listener<Events[EventName]>
|
||||
): this {
|
||||
this.emitter.once(event, listener)
|
||||
return this
|
||||
}
|
||||
|
||||
public off<EventName extends ExtractEventNames<Events>>(
|
||||
event: EventName,
|
||||
listener: Listener<Events[EventName]>
|
||||
): this {
|
||||
this.emitter.off(event, listener)
|
||||
return this
|
||||
}
|
||||
|
||||
public removeAllListeners<EventName extends ExtractEventNames<Events>>(
|
||||
event?: EventName
|
||||
): this {
|
||||
this.emitter.removeAllListeners(event)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes of any side-effects this interceptor has introduced.
|
||||
*/
|
||||
public dispose(): void {
|
||||
const logger = this.logger.extend('dispose')
|
||||
|
||||
if (this.readyState === InterceptorReadyState.DISPOSED) {
|
||||
logger.info('cannot dispose, already disposed!')
|
||||
return
|
||||
}
|
||||
|
||||
logger.info('disposing the interceptor...')
|
||||
this.readyState = InterceptorReadyState.DISPOSING
|
||||
|
||||
if (!this.getInstance()) {
|
||||
logger.info('no interceptors running, skipping dispose...')
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the global symbol as soon as possible,
|
||||
// indicating that the interceptor is no longer running.
|
||||
this.clearInstance()
|
||||
|
||||
logger.info('global symbol deleted:', getGlobalSymbol(this.symbol))
|
||||
|
||||
if (this.subscriptions.length > 0) {
|
||||
logger.info('disposing of %d subscriptions...', this.subscriptions.length)
|
||||
|
||||
for (const dispose of this.subscriptions) {
|
||||
dispose()
|
||||
}
|
||||
|
||||
this.subscriptions = []
|
||||
|
||||
logger.info('disposed of all subscriptions!', this.subscriptions.length)
|
||||
}
|
||||
|
||||
this.emitter.removeAllListeners()
|
||||
logger.info('destroyed the listener!')
|
||||
|
||||
this.readyState = InterceptorReadyState.DISPOSED
|
||||
}
|
||||
|
||||
private getInstance(): this | undefined {
|
||||
const instance = getGlobalSymbol<this>(this.symbol)
|
||||
this.logger.info('retrieved global instance:', instance?.constructor?.name)
|
||||
return instance
|
||||
}
|
||||
|
||||
private setInstance(): void {
|
||||
setGlobalSymbol(this.symbol, this)
|
||||
this.logger.info('set global instance!', this.symbol.description)
|
||||
}
|
||||
|
||||
private clearInstance(): void {
|
||||
deleteGlobalSymbol(this.symbol)
|
||||
this.logger.info('cleared global instance!', this.symbol.description)
|
||||
}
|
||||
}
|
||||
7
node_modules/@mswjs/interceptors/src/InterceptorError.ts
generated
vendored
Normal file
7
node_modules/@mswjs/interceptors/src/InterceptorError.ts
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
export class InterceptorError extends Error {
|
||||
constructor(message?: string) {
|
||||
super(message)
|
||||
this.name = 'InterceptorError'
|
||||
Object.setPrototypeOf(this, InterceptorError.prototype)
|
||||
}
|
||||
}
|
||||
251
node_modules/@mswjs/interceptors/src/RemoteHttpInterceptor.ts
generated
vendored
Normal file
251
node_modules/@mswjs/interceptors/src/RemoteHttpInterceptor.ts
generated
vendored
Normal file
@@ -0,0 +1,251 @@
|
||||
import { ChildProcess } from 'child_process'
|
||||
import { HttpRequestEventMap } from './glossary'
|
||||
import { Interceptor } from './Interceptor'
|
||||
import { BatchInterceptor } from './BatchInterceptor'
|
||||
import { ClientRequestInterceptor } from './interceptors/ClientRequest'
|
||||
import { XMLHttpRequestInterceptor } from './interceptors/XMLHttpRequest'
|
||||
import { FetchInterceptor } from './interceptors/fetch'
|
||||
import { handleRequest } from './utils/handleRequest'
|
||||
import { RequestController } from './RequestController'
|
||||
import { FetchResponse } from './utils/fetchUtils'
|
||||
import { isResponseError } from './utils/responseUtils'
|
||||
|
||||
export interface SerializedRequest {
|
||||
id: string
|
||||
url: string
|
||||
method: string
|
||||
headers: Array<[string, string]>
|
||||
credentials: RequestCredentials
|
||||
body: string
|
||||
}
|
||||
|
||||
interface RevivedRequest extends Omit<SerializedRequest, 'url' | 'headers'> {
|
||||
url: URL
|
||||
headers: Headers
|
||||
}
|
||||
|
||||
export interface SerializedResponse {
|
||||
status: number
|
||||
statusText: string
|
||||
headers: Array<[string, string]>
|
||||
body: string
|
||||
}
|
||||
|
||||
export class RemoteHttpInterceptor extends BatchInterceptor<
|
||||
[ClientRequestInterceptor, XMLHttpRequestInterceptor, FetchInterceptor]
|
||||
> {
|
||||
constructor() {
|
||||
super({
|
||||
name: 'remote-interceptor',
|
||||
interceptors: [
|
||||
new ClientRequestInterceptor(),
|
||||
new XMLHttpRequestInterceptor(),
|
||||
new FetchInterceptor(),
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
protected setup() {
|
||||
super.setup()
|
||||
|
||||
let handleParentMessage: NodeJS.MessageListener
|
||||
|
||||
this.on('request', async ({ request, requestId, controller }) => {
|
||||
// Send the stringified intercepted request to
|
||||
// the parent process where the remote resolver is established.
|
||||
const serializedRequest = JSON.stringify({
|
||||
id: requestId,
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
headers: Array.from(request.headers.entries()),
|
||||
credentials: request.credentials,
|
||||
body: ['GET', 'HEAD'].includes(request.method)
|
||||
? null
|
||||
: await request.text(),
|
||||
} as SerializedRequest)
|
||||
|
||||
this.logger.info(
|
||||
'sent serialized request to the child:',
|
||||
serializedRequest
|
||||
)
|
||||
|
||||
process.send?.(`request:${serializedRequest}`)
|
||||
|
||||
const responsePromise = new Promise<void>((resolve) => {
|
||||
handleParentMessage = (message) => {
|
||||
if (typeof message !== 'string') {
|
||||
return resolve()
|
||||
}
|
||||
|
||||
if (message.startsWith(`response:${requestId}`)) {
|
||||
const [, serializedResponse] =
|
||||
message.match(/^response:.+?:(.+)$/) || []
|
||||
|
||||
if (!serializedResponse) {
|
||||
return resolve()
|
||||
}
|
||||
|
||||
const responseInit = JSON.parse(
|
||||
serializedResponse
|
||||
) as SerializedResponse
|
||||
|
||||
const mockedResponse = new FetchResponse(responseInit.body, {
|
||||
url: request.url,
|
||||
status: responseInit.status,
|
||||
statusText: responseInit.statusText,
|
||||
headers: responseInit.headers,
|
||||
})
|
||||
|
||||
/**
|
||||
* @todo Support "errorWith" as well.
|
||||
* This response handling from the child is incomplete.
|
||||
*/
|
||||
|
||||
controller.respondWith(mockedResponse)
|
||||
return resolve()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Listen for the mocked response message from the parent.
|
||||
this.logger.info(
|
||||
'add "message" listener to the parent process',
|
||||
handleParentMessage
|
||||
)
|
||||
process.addListener('message', handleParentMessage)
|
||||
|
||||
return responsePromise
|
||||
})
|
||||
|
||||
this.subscriptions.push(() => {
|
||||
process.removeListener('message', handleParentMessage)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function requestReviver(key: string, value: any) {
|
||||
switch (key) {
|
||||
case 'url':
|
||||
return new URL(value)
|
||||
|
||||
case 'headers':
|
||||
return new Headers(value)
|
||||
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
export interface RemoveResolverOptions {
|
||||
process: ChildProcess
|
||||
}
|
||||
|
||||
export class RemoteHttpResolver extends Interceptor<HttpRequestEventMap> {
|
||||
static symbol = Symbol('remote-resolver')
|
||||
private process: ChildProcess
|
||||
|
||||
constructor(options: RemoveResolverOptions) {
|
||||
super(RemoteHttpResolver.symbol)
|
||||
this.process = options.process
|
||||
}
|
||||
|
||||
protected setup() {
|
||||
const logger = this.logger.extend('setup')
|
||||
|
||||
const handleChildMessage: NodeJS.MessageListener = async (message) => {
|
||||
logger.info('received message from child!', message)
|
||||
|
||||
if (typeof message !== 'string' || !message.startsWith('request:')) {
|
||||
logger.info('unknown message, ignoring...')
|
||||
return
|
||||
}
|
||||
|
||||
const [, serializedRequest] = message.match(/^request:(.+)$/) || []
|
||||
if (!serializedRequest) {
|
||||
return
|
||||
}
|
||||
|
||||
const requestJson = JSON.parse(
|
||||
serializedRequest,
|
||||
requestReviver
|
||||
) as RevivedRequest
|
||||
|
||||
logger.info('parsed intercepted request', requestJson)
|
||||
|
||||
const request = new Request(requestJson.url, {
|
||||
method: requestJson.method,
|
||||
headers: new Headers(requestJson.headers),
|
||||
credentials: requestJson.credentials,
|
||||
body: requestJson.body,
|
||||
})
|
||||
|
||||
const controller = new RequestController(request, {
|
||||
passthrough: () => {},
|
||||
respondWith: async (response) => {
|
||||
if (isResponseError(response)) {
|
||||
this.logger.info('received a network error!', { response })
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
this.logger.info('received mocked response!', { response })
|
||||
|
||||
const responseClone = response.clone()
|
||||
const responseText = await responseClone.text()
|
||||
|
||||
// // Send the mocked response to the child process.
|
||||
const serializedResponse = JSON.stringify({
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: Array.from(response.headers.entries()),
|
||||
body: responseText,
|
||||
} as SerializedResponse)
|
||||
|
||||
this.process.send(
|
||||
`response:${requestJson.id}:${serializedResponse}`,
|
||||
(error) => {
|
||||
if (error) {
|
||||
return
|
||||
}
|
||||
|
||||
// Emit an optimistic "response" event at this point,
|
||||
// not to rely on the back-and-forth signaling for the sake of the event.
|
||||
this.emitter.emit('response', {
|
||||
request,
|
||||
requestId: requestJson.id,
|
||||
response: responseClone,
|
||||
isMockedResponse: true,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
logger.info(
|
||||
'sent serialized mocked response to the parent:',
|
||||
serializedResponse
|
||||
)
|
||||
},
|
||||
errorWith: (reason) => {
|
||||
this.logger.info('request has errored!', { error: reason })
|
||||
throw new Error('Not implemented')
|
||||
},
|
||||
})
|
||||
|
||||
await handleRequest({
|
||||
request,
|
||||
requestId: requestJson.id,
|
||||
controller,
|
||||
emitter: this.emitter,
|
||||
})
|
||||
}
|
||||
|
||||
this.subscriptions.push(() => {
|
||||
this.process.removeListener('message', handleChildMessage)
|
||||
logger.info('removed the "message" listener from the child process!')
|
||||
})
|
||||
|
||||
logger.info('adding a "message" listener to the child process')
|
||||
this.process.addListener('message', handleChildMessage)
|
||||
|
||||
this.process.once('error', () => this.dispose())
|
||||
this.process.once('exit', () => this.dispose())
|
||||
}
|
||||
}
|
||||
104
node_modules/@mswjs/interceptors/src/RequestController.test.ts
generated
vendored
Normal file
104
node_modules/@mswjs/interceptors/src/RequestController.test.ts
generated
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
import { vi, it, expect } from 'vitest'
|
||||
import {
|
||||
RequestController,
|
||||
type RequestControllerSource,
|
||||
} from './RequestController'
|
||||
import { InterceptorError } from './InterceptorError'
|
||||
|
||||
const defaultSource = {
|
||||
passthrough() {},
|
||||
respondWith() {},
|
||||
errorWith() {},
|
||||
} satisfies RequestControllerSource
|
||||
|
||||
it('has a pending state upon construction', () => {
|
||||
const controller = new RequestController(
|
||||
new Request('http://localhost'),
|
||||
defaultSource
|
||||
)
|
||||
|
||||
expect(controller.handled).toBeInstanceOf(Promise)
|
||||
expect(controller.readyState).toBe(RequestController.PENDING)
|
||||
})
|
||||
|
||||
it('handles a request when calling ".respondWith()" with a mocked response', async () => {
|
||||
const respondWith = vi.fn<RequestControllerSource['respondWith']>()
|
||||
const controller = new RequestController(new Request('http://localhost'), {
|
||||
...defaultSource,
|
||||
respondWith,
|
||||
})
|
||||
|
||||
await controller.respondWith(new Response('hello world'))
|
||||
|
||||
expect(controller.readyState).toBe(RequestController.RESPONSE)
|
||||
await expect(controller.handled).resolves.toBeUndefined()
|
||||
|
||||
expect(respondWith).toHaveBeenCalledOnce()
|
||||
const [response] = respondWith.mock.calls[0]
|
||||
|
||||
expect(response).toBeInstanceOf(Response)
|
||||
expect(response.status).toBe(200)
|
||||
await expect(response.text()).resolves.toBe('hello world')
|
||||
})
|
||||
|
||||
it('handles the request when calling ".errorWith()" with an error', async () => {
|
||||
const errorWith = vi.fn<RequestControllerSource['errorWith']>()
|
||||
const controller = new RequestController(new Request('http://localhost'), {
|
||||
...defaultSource,
|
||||
errorWith,
|
||||
})
|
||||
|
||||
const error = new Error('Oops!')
|
||||
await controller.errorWith(error)
|
||||
|
||||
expect(controller.readyState).toBe(RequestController.ERROR)
|
||||
await expect(controller.handled).resolves.toBeUndefined()
|
||||
|
||||
expect(errorWith).toHaveBeenCalledOnce()
|
||||
expect(errorWith).toHaveBeenCalledWith(error)
|
||||
})
|
||||
|
||||
it('handles the request when calling ".errorWith()" with an arbitrary object', async () => {
|
||||
const errorWith = vi.fn<RequestControllerSource['errorWith']>()
|
||||
const controller = new RequestController(new Request('http://localhost'), {
|
||||
...defaultSource,
|
||||
errorWith,
|
||||
})
|
||||
|
||||
const error = { message: 'Oops!' }
|
||||
await controller.errorWith(error)
|
||||
|
||||
expect(controller.readyState).toBe(RequestController.ERROR)
|
||||
await expect(controller.handled).resolves.toBeUndefined()
|
||||
|
||||
expect(errorWith).toHaveBeenCalledOnce()
|
||||
expect(errorWith).toHaveBeenCalledWith(error)
|
||||
})
|
||||
|
||||
it('throws when calling "respondWith" multiple times', async () => {
|
||||
const controller = new RequestController(
|
||||
new Request('http://localhost'),
|
||||
defaultSource
|
||||
)
|
||||
controller.respondWith(new Response('hello world'))
|
||||
|
||||
expect(() => controller.respondWith(new Response('second response'))).toThrow(
|
||||
new InterceptorError(
|
||||
'Failed to respond to the "GET http://localhost/" request with "200 OK": the request has already been handled (2)'
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
it('throws when calling "errorWith" multiple times', async () => {
|
||||
const controller = new RequestController(
|
||||
new Request('http://localhost'),
|
||||
defaultSource
|
||||
)
|
||||
controller.errorWith(new Error('Oops!'))
|
||||
|
||||
expect(() => controller.errorWith(new Error('second error'))).toThrow(
|
||||
new InterceptorError(
|
||||
'Failed to error the "GET http://localhost/" request with "Error: second error": the request has already been handled (3)'
|
||||
)
|
||||
)
|
||||
})
|
||||
109
node_modules/@mswjs/interceptors/src/RequestController.ts
generated
vendored
Normal file
109
node_modules/@mswjs/interceptors/src/RequestController.ts
generated
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
import { DeferredPromise } from '@open-draft/deferred-promise'
|
||||
import { invariant } from 'outvariant'
|
||||
import { InterceptorError } from './InterceptorError'
|
||||
|
||||
export interface RequestControllerSource {
|
||||
passthrough(): void
|
||||
respondWith(response: Response): void
|
||||
errorWith(reason?: unknown): void
|
||||
}
|
||||
|
||||
export class RequestController {
|
||||
static PENDING = 0 as const
|
||||
static PASSTHROUGH = 1 as const
|
||||
static RESPONSE = 2 as const
|
||||
static ERROR = 3 as const
|
||||
|
||||
public readyState: number
|
||||
|
||||
/**
|
||||
* A Promise that resolves when this controller handles a request.
|
||||
* See `controller.readyState` for more information on the handling result.
|
||||
*/
|
||||
public handled: Promise<void>
|
||||
|
||||
constructor(
|
||||
protected readonly request: Request,
|
||||
protected readonly source: RequestControllerSource
|
||||
) {
|
||||
this.readyState = RequestController.PENDING
|
||||
this.handled = new DeferredPromise<void>()
|
||||
}
|
||||
|
||||
get #handled() {
|
||||
return this.handled as DeferredPromise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform this request as-is.
|
||||
*/
|
||||
public async passthrough(): Promise<void> {
|
||||
invariant.as(
|
||||
InterceptorError,
|
||||
this.readyState === RequestController.PENDING,
|
||||
'Failed to passthrough the "%s %s" request: the request has already been handled',
|
||||
this.request.method,
|
||||
this.request.url
|
||||
)
|
||||
|
||||
this.readyState = RequestController.PASSTHROUGH
|
||||
await this.source.passthrough()
|
||||
this.#handled.resolve()
|
||||
}
|
||||
|
||||
/**
|
||||
* Respond to this request with the given `Response` instance.
|
||||
*
|
||||
* @example
|
||||
* controller.respondWith(new Response())
|
||||
* controller.respondWith(Response.json({ id }))
|
||||
* controller.respondWith(Response.error())
|
||||
*/
|
||||
public respondWith(response: Response): void {
|
||||
invariant.as(
|
||||
InterceptorError,
|
||||
this.readyState === RequestController.PENDING,
|
||||
'Failed to respond to the "%s %s" request with "%d %s": the request has already been handled (%d)',
|
||||
this.request.method,
|
||||
this.request.url,
|
||||
response.status,
|
||||
response.statusText || 'OK',
|
||||
this.readyState
|
||||
)
|
||||
|
||||
this.readyState = RequestController.RESPONSE
|
||||
this.#handled.resolve()
|
||||
|
||||
/**
|
||||
* @note Although `source.respondWith()` is potentially asynchronous,
|
||||
* do NOT await it for backward-compatibility. Awaiting it will short-circuit
|
||||
* the request listener invocation as soon as a listener responds to a request.
|
||||
* Ideally, that's what we want, but that's not what we promise the user.
|
||||
*/
|
||||
this.source.respondWith(response)
|
||||
}
|
||||
|
||||
/**
|
||||
* Error this request with the given reason.
|
||||
*
|
||||
* @example
|
||||
* controller.errorWith()
|
||||
* controller.errorWith(new Error('Oops!'))
|
||||
* controller.errorWith({ message: 'Oops!'})
|
||||
*/
|
||||
public errorWith(reason?: unknown): void {
|
||||
invariant.as(
|
||||
InterceptorError,
|
||||
this.readyState === RequestController.PENDING,
|
||||
'Failed to error the "%s %s" request with "%s": the request has already been handled (%d)',
|
||||
this.request.method,
|
||||
this.request.url,
|
||||
reason?.toString(),
|
||||
this.readyState
|
||||
)
|
||||
|
||||
this.readyState = RequestController.ERROR
|
||||
this.source.errorWith(reason)
|
||||
this.#handled.resolve()
|
||||
}
|
||||
}
|
||||
7
node_modules/@mswjs/interceptors/src/createRequestId.test.ts
generated
vendored
Normal file
7
node_modules/@mswjs/interceptors/src/createRequestId.test.ts
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
import { it, expect } from 'vitest'
|
||||
import { createRequestId } from './createRequestId'
|
||||
import { REQUEST_ID_REGEXP } from '../test/helpers'
|
||||
|
||||
it('returns a request ID', () => {
|
||||
expect(createRequestId()).toMatch(REQUEST_ID_REGEXP)
|
||||
})
|
||||
9
node_modules/@mswjs/interceptors/src/createRequestId.ts
generated
vendored
Normal file
9
node_modules/@mswjs/interceptors/src/createRequestId.ts
generated
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Generate a random ID string to represent a request.
|
||||
* @example
|
||||
* createRequestId()
|
||||
* // "f774b6c9c600f"
|
||||
*/
|
||||
export function createRequestId(): string {
|
||||
return Math.random().toString(16).slice(2)
|
||||
}
|
||||
21
node_modules/@mswjs/interceptors/src/getRawRequest.ts
generated
vendored
Normal file
21
node_modules/@mswjs/interceptors/src/getRawRequest.ts
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
const kRawRequest = Symbol('kRawRequest')
|
||||
|
||||
/**
|
||||
* Returns a raw request instance associated with this request.
|
||||
*
|
||||
* @example
|
||||
* interceptor.on('request', ({ request }) => {
|
||||
* const rawRequest = getRawRequest(request)
|
||||
*
|
||||
* if (rawRequest instanceof http.ClientRequest) {
|
||||
* console.log(rawRequest.rawHeaders)
|
||||
* }
|
||||
* })
|
||||
*/
|
||||
export function getRawRequest(request: Request): unknown | undefined {
|
||||
return Reflect.get(request, kRawRequest)
|
||||
}
|
||||
|
||||
export function setRawRequest(request: Request, rawRequest: unknown): void {
|
||||
Reflect.set(request, kRawRequest, rawRequest)
|
||||
}
|
||||
37
node_modules/@mswjs/interceptors/src/glossary.ts
generated
vendored
Normal file
37
node_modules/@mswjs/interceptors/src/glossary.ts
generated
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { RequestController } from './RequestController'
|
||||
|
||||
export const IS_PATCHED_MODULE: unique symbol = Symbol('isPatchedModule')
|
||||
|
||||
/**
|
||||
* @note Export `RequestController` as a type only.
|
||||
* It's never meant to be created in the userland.
|
||||
*/
|
||||
export type { RequestController }
|
||||
|
||||
export type RequestCredentials = 'omit' | 'include' | 'same-origin'
|
||||
|
||||
export type HttpRequestEventMap = {
|
||||
request: [
|
||||
args: {
|
||||
request: Request
|
||||
requestId: string
|
||||
controller: RequestController
|
||||
}
|
||||
]
|
||||
response: [
|
||||
args: {
|
||||
response: Response
|
||||
isMockedResponse: boolean
|
||||
request: Request
|
||||
requestId: string
|
||||
}
|
||||
]
|
||||
unhandledException: [
|
||||
args: {
|
||||
error: unknown
|
||||
request: Request
|
||||
requestId: string
|
||||
controller: RequestController
|
||||
}
|
||||
]
|
||||
}
|
||||
15
node_modules/@mswjs/interceptors/src/index.ts
generated
vendored
Normal file
15
node_modules/@mswjs/interceptors/src/index.ts
generated
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
export * from './glossary'
|
||||
export * from './Interceptor'
|
||||
export * from './BatchInterceptor'
|
||||
export {
|
||||
RequestController,
|
||||
type RequestControllerSource,
|
||||
} from './RequestController'
|
||||
|
||||
/* Utils */
|
||||
export { createRequestId } from './createRequestId'
|
||||
export { getCleanUrl } from './utils/getCleanUrl'
|
||||
export { encodeBuffer, decodeBuffer } from './utils/bufferUtils'
|
||||
export { FetchResponse } from './utils/fetchUtils'
|
||||
export { getRawRequest } from './getRawRequest'
|
||||
export { resolveWebSocketUrl } from './utils/resolveWebSocketUrl'
|
||||
724
node_modules/@mswjs/interceptors/src/interceptors/ClientRequest/MockHttpSocket.ts
generated
vendored
Normal file
724
node_modules/@mswjs/interceptors/src/interceptors/ClientRequest/MockHttpSocket.ts
generated
vendored
Normal file
@@ -0,0 +1,724 @@
|
||||
import net from 'node:net'
|
||||
import {
|
||||
type HeadersCallback,
|
||||
HTTPParser,
|
||||
type RequestHeadersCompleteCallback,
|
||||
type ResponseHeadersCompleteCallback,
|
||||
} from '_http_common'
|
||||
import { STATUS_CODES, IncomingMessage, ServerResponse } from 'node:http'
|
||||
import { Readable } from 'node:stream'
|
||||
import { invariant } from 'outvariant'
|
||||
import { INTERNAL_REQUEST_ID_HEADER_NAME } from '../../Interceptor'
|
||||
import { MockSocket } from '../Socket/MockSocket'
|
||||
import type { NormalizedSocketWriteArgs } from '../Socket/utils/normalizeSocketWriteArgs'
|
||||
import { isPropertyAccessible } from '../../utils/isPropertyAccessible'
|
||||
import { baseUrlFromConnectionOptions } from '../Socket/utils/baseUrlFromConnectionOptions'
|
||||
import { createRequestId } from '../../createRequestId'
|
||||
import { getRawFetchHeaders } from './utils/recordRawHeaders'
|
||||
import { FetchResponse } from '../../utils/fetchUtils'
|
||||
import { setRawRequest } from '../../getRawRequest'
|
||||
import { setRawRequestBodyStream } from '../../utils/node'
|
||||
import { freeParser } from './utils/parserUtils'
|
||||
|
||||
type HttpConnectionOptions = any
|
||||
|
||||
export type MockHttpSocketRequestCallback = (args: {
|
||||
requestId: string
|
||||
request: Request
|
||||
socket: MockHttpSocket
|
||||
}) => void
|
||||
|
||||
export type MockHttpSocketResponseCallback = (args: {
|
||||
requestId: string
|
||||
request: Request
|
||||
response: Response
|
||||
isMockedResponse: boolean
|
||||
socket: MockHttpSocket
|
||||
}) => Promise<void>
|
||||
|
||||
interface MockHttpSocketOptions {
|
||||
connectionOptions: HttpConnectionOptions
|
||||
createConnection: () => net.Socket
|
||||
onRequest: MockHttpSocketRequestCallback
|
||||
onResponse: MockHttpSocketResponseCallback
|
||||
}
|
||||
|
||||
export const kRequestId = Symbol('kRequestId')
|
||||
|
||||
export class MockHttpSocket extends MockSocket {
|
||||
private connectionOptions: HttpConnectionOptions
|
||||
private createConnection: () => net.Socket
|
||||
private baseUrl: URL
|
||||
|
||||
private onRequest: MockHttpSocketRequestCallback
|
||||
private onResponse: MockHttpSocketResponseCallback
|
||||
private responseListenersPromise?: Promise<void>
|
||||
|
||||
private requestRawHeadersBuffer: Array<string> = []
|
||||
private responseRawHeadersBuffer: Array<string> = []
|
||||
private writeBuffer: Array<NormalizedSocketWriteArgs> = []
|
||||
private request?: Request
|
||||
private requestParser: HTTPParser<0>
|
||||
private requestStream?: Readable
|
||||
private shouldKeepAlive?: boolean
|
||||
|
||||
private socketState: 'unknown' | 'mock' | 'passthrough' = 'unknown'
|
||||
private responseParser: HTTPParser<1>
|
||||
private responseStream?: Readable
|
||||
private originalSocket?: net.Socket
|
||||
|
||||
constructor(options: MockHttpSocketOptions) {
|
||||
super({
|
||||
write: (chunk, encoding, callback) => {
|
||||
// Buffer the writes so they can be flushed in case of the original connection
|
||||
// and when reading the request body in the interceptor. If the connection has
|
||||
// been established, no need to buffer the chunks anymore, they will be forwarded.
|
||||
if (this.socketState !== 'passthrough') {
|
||||
this.writeBuffer.push([chunk, encoding, callback])
|
||||
}
|
||||
|
||||
if (chunk) {
|
||||
/**
|
||||
* Forward any writes to the mock socket to the underlying original socket.
|
||||
* This ensures functional duplex connections, like WebSocket.
|
||||
* @see https://github.com/mswjs/interceptors/issues/682
|
||||
*/
|
||||
if (this.socketState === 'passthrough') {
|
||||
this.originalSocket?.write(chunk, encoding, callback)
|
||||
}
|
||||
|
||||
this.requestParser.execute(
|
||||
Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding)
|
||||
)
|
||||
}
|
||||
},
|
||||
read: (chunk) => {
|
||||
if (chunk !== null) {
|
||||
/**
|
||||
* @todo We need to free the parser if the connection has been
|
||||
* upgraded to a non-HTTP protocol. It won't be able to parse data
|
||||
* from that point onward anyway. No need to keep it in memory.
|
||||
*/
|
||||
this.responseParser.execute(
|
||||
Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
this.connectionOptions = options.connectionOptions
|
||||
this.createConnection = options.createConnection
|
||||
this.onRequest = options.onRequest
|
||||
this.onResponse = options.onResponse
|
||||
|
||||
this.baseUrl = baseUrlFromConnectionOptions(this.connectionOptions)
|
||||
|
||||
// Request parser.
|
||||
this.requestParser = new HTTPParser()
|
||||
this.requestParser.initialize(HTTPParser.REQUEST, {})
|
||||
this.requestParser[HTTPParser.kOnHeaders] = this.onRequestHeaders.bind(this)
|
||||
this.requestParser[HTTPParser.kOnHeadersComplete] =
|
||||
this.onRequestStart.bind(this)
|
||||
this.requestParser[HTTPParser.kOnBody] = this.onRequestBody.bind(this)
|
||||
this.requestParser[HTTPParser.kOnMessageComplete] =
|
||||
this.onRequestEnd.bind(this)
|
||||
|
||||
// Response parser.
|
||||
this.responseParser = new HTTPParser()
|
||||
this.responseParser.initialize(HTTPParser.RESPONSE, {})
|
||||
this.responseParser[HTTPParser.kOnHeaders] =
|
||||
this.onResponseHeaders.bind(this)
|
||||
this.responseParser[HTTPParser.kOnHeadersComplete] =
|
||||
this.onResponseStart.bind(this)
|
||||
this.responseParser[HTTPParser.kOnBody] = this.onResponseBody.bind(this)
|
||||
this.responseParser[HTTPParser.kOnMessageComplete] =
|
||||
this.onResponseEnd.bind(this)
|
||||
|
||||
// Once the socket is finished, nothing can write to it
|
||||
// anymore. It has also flushed any buffered chunks.
|
||||
this.once('finish', () => freeParser(this.requestParser, this))
|
||||
|
||||
if (this.baseUrl.protocol === 'https:') {
|
||||
Reflect.set(this, 'encrypted', true)
|
||||
// The server certificate is not the same as a CA
|
||||
// passed to the TLS socket connection options.
|
||||
Reflect.set(this, 'authorized', false)
|
||||
Reflect.set(this, 'getProtocol', () => 'TLSv1.3')
|
||||
Reflect.set(this, 'getSession', () => undefined)
|
||||
Reflect.set(this, 'isSessionReused', () => false)
|
||||
Reflect.set(this, 'getCipher', () => ({
|
||||
name: 'AES256-SHA',
|
||||
standardName: 'TLS_RSA_WITH_AES_256_CBC_SHA',
|
||||
version: 'TLSv1.3',
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
public emit(event: string | symbol, ...args: any[]): boolean {
|
||||
const emitEvent = super.emit.bind(this, event as any, ...args)
|
||||
|
||||
if (this.responseListenersPromise) {
|
||||
this.responseListenersPromise.finally(emitEvent)
|
||||
return this.listenerCount(event) > 0
|
||||
}
|
||||
|
||||
return emitEvent()
|
||||
}
|
||||
|
||||
public destroy(error?: Error | undefined): this {
|
||||
// Destroy the response parser when the socket gets destroyed.
|
||||
// Normally, we should listen to the "close" event but it
|
||||
// can be suppressed by using the "emitClose: false" option.
|
||||
freeParser(this.responseParser, this)
|
||||
|
||||
if (error) {
|
||||
this.emit('error', error)
|
||||
}
|
||||
|
||||
return super.destroy(error)
|
||||
}
|
||||
|
||||
/**
|
||||
* Establish this Socket connection as-is and pipe
|
||||
* its data/events through this Socket.
|
||||
*/
|
||||
public passthrough(): void {
|
||||
this.socketState = 'passthrough'
|
||||
|
||||
if (this.destroyed) {
|
||||
return
|
||||
}
|
||||
|
||||
const socket = this.createConnection()
|
||||
this.originalSocket = socket
|
||||
|
||||
/**
|
||||
* @note Inherit the original socket's connection handle.
|
||||
* Without this, each push to the mock socket results in a
|
||||
* new "connection" listener being added (i.e. buffering pushes).
|
||||
* @see https://github.com/nodejs/node/blob/b18153598b25485ce4f54d0c5cb830a9457691ee/lib/net.js#L734
|
||||
*/
|
||||
if ('_handle' in socket) {
|
||||
Object.defineProperty(this, '_handle', {
|
||||
value: socket._handle,
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
})
|
||||
}
|
||||
|
||||
// The client-facing socket can be destroyed in two ways:
|
||||
// 1. The developer destroys the socket.
|
||||
// 2. The passthrough socket "close" is forwarded to the socket.
|
||||
this.once('close', () => {
|
||||
socket.removeAllListeners()
|
||||
|
||||
// If the closure didn't originate from the passthrough socket, destroy it.
|
||||
if (!socket.destroyed) {
|
||||
socket.destroy()
|
||||
}
|
||||
|
||||
this.originalSocket = undefined
|
||||
})
|
||||
|
||||
this.address = socket.address.bind(socket)
|
||||
|
||||
// Flush the buffered "socket.write()" calls onto
|
||||
// the original socket instance (i.e. write request body).
|
||||
// Exhaust the "requestBuffer" in case this Socket
|
||||
// gets reused for different requests.
|
||||
let writeArgs: NormalizedSocketWriteArgs | undefined
|
||||
let headersWritten = false
|
||||
|
||||
while ((writeArgs = this.writeBuffer.shift())) {
|
||||
if (writeArgs !== undefined) {
|
||||
if (!headersWritten) {
|
||||
const [chunk, encoding, callback] = writeArgs
|
||||
const chunkString = chunk.toString()
|
||||
const chunkBeforeRequestHeaders = chunkString.slice(
|
||||
0,
|
||||
chunkString.indexOf('\r\n') + 2
|
||||
)
|
||||
const chunkAfterRequestHeaders = chunkString.slice(
|
||||
chunk.indexOf('\r\n\r\n')
|
||||
)
|
||||
const rawRequestHeaders = getRawFetchHeaders(this.request!.headers)
|
||||
const requestHeadersString = rawRequestHeaders
|
||||
// Skip the internal request ID deduplication header.
|
||||
.filter(([name]) => {
|
||||
return name.toLowerCase() !== INTERNAL_REQUEST_ID_HEADER_NAME
|
||||
})
|
||||
.map(([name, value]) => `${name}: ${value}`)
|
||||
.join('\r\n')
|
||||
|
||||
// Modify the HTTP request message headers
|
||||
// to reflect any changes to the request headers
|
||||
// from the "request" event listener.
|
||||
const headersChunk = `${chunkBeforeRequestHeaders}${requestHeadersString}${chunkAfterRequestHeaders}`
|
||||
socket.write(headersChunk, encoding, callback)
|
||||
headersWritten = true
|
||||
continue
|
||||
}
|
||||
|
||||
socket.write(...writeArgs)
|
||||
}
|
||||
}
|
||||
|
||||
// Forward TLS Socket properties onto this Socket instance
|
||||
// in the case of a TLS/SSL connection.
|
||||
if (Reflect.get(socket, 'encrypted')) {
|
||||
const tlsProperties = [
|
||||
'encrypted',
|
||||
'authorized',
|
||||
'getProtocol',
|
||||
'getSession',
|
||||
'isSessionReused',
|
||||
'getCipher',
|
||||
]
|
||||
|
||||
tlsProperties.forEach((propertyName) => {
|
||||
Object.defineProperty(this, propertyName, {
|
||||
enumerable: true,
|
||||
get: () => {
|
||||
const value = Reflect.get(socket, propertyName)
|
||||
return typeof value === 'function' ? value.bind(socket) : value
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
socket
|
||||
.on('lookup', (...args) => this.emit('lookup', ...args))
|
||||
.on('connect', () => {
|
||||
this.connecting = socket.connecting
|
||||
this.emit('connect')
|
||||
})
|
||||
.on('secureConnect', () => this.emit('secureConnect'))
|
||||
.on('secure', () => this.emit('secure'))
|
||||
.on('session', (session) => this.emit('session', session))
|
||||
.on('ready', () => this.emit('ready'))
|
||||
.on('drain', () => this.emit('drain'))
|
||||
.on('data', (chunk) => {
|
||||
// Push the original response to this socket
|
||||
// so it triggers the HTTP response parser. This unifies
|
||||
// the handling pipeline for original and mocked response.
|
||||
this.push(chunk)
|
||||
})
|
||||
.on('error', (error) => {
|
||||
Reflect.set(this, '_hadError', Reflect.get(socket, '_hadError'))
|
||||
this.emit('error', error)
|
||||
})
|
||||
.on('resume', () => this.emit('resume'))
|
||||
.on('timeout', () => this.emit('timeout'))
|
||||
.on('prefinish', () => this.emit('prefinish'))
|
||||
.on('finish', () => this.emit('finish'))
|
||||
.on('close', (hadError) => this.emit('close', hadError))
|
||||
.on('end', () => this.emit('end'))
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the given Fetch API `Response` instance to an
|
||||
* HTTP message and push it to the socket.
|
||||
*/
|
||||
public async respondWith(response: Response): Promise<void> {
|
||||
// Ignore the mocked response if the socket has been destroyed
|
||||
// (e.g. aborted or timed out),
|
||||
if (this.destroyed) {
|
||||
return
|
||||
}
|
||||
|
||||
// Prevent recursive calls.
|
||||
invariant(
|
||||
this.socketState !== 'mock',
|
||||
'[MockHttpSocket] Failed to respond to the "%s %s" request with "%s %s": the request has already been handled',
|
||||
this.request?.method,
|
||||
this.request?.url,
|
||||
response.status,
|
||||
response.statusText
|
||||
)
|
||||
|
||||
// Handle "type: error" responses.
|
||||
if (isPropertyAccessible(response, 'type') && response.type === 'error') {
|
||||
this.errorWith(new TypeError('Network error'))
|
||||
return
|
||||
}
|
||||
|
||||
// First, emit all the connection events
|
||||
// to emulate a successful connection.
|
||||
this.mockConnect()
|
||||
this.socketState = 'mock'
|
||||
|
||||
// Flush the write buffer to trigger write callbacks
|
||||
// if it hasn't been flushed already (e.g. someone started reading request stream).
|
||||
this.flushWriteBuffer()
|
||||
|
||||
// Create a `ServerResponse` instance to delegate HTTP message parsing,
|
||||
// Transfer-Encoding, and other things to Node.js internals.
|
||||
const serverResponse = new ServerResponse(new IncomingMessage(this))
|
||||
|
||||
/**
|
||||
* Assign a mock socket instance to the server response to
|
||||
* spy on the response chunk writes. Push the transformed response chunks
|
||||
* to this `MockHttpSocket` instance to trigger the "data" event.
|
||||
* @note Providing the same `MockSocket` instance when creating `ServerResponse`
|
||||
* does not have the same effect.
|
||||
* @see https://github.com/nodejs/node/blob/10099bb3f7fd97bb9dd9667188426866b3098e07/test/parallel/test-http-server-response-standalone.js#L32
|
||||
*/
|
||||
serverResponse.assignSocket(
|
||||
new MockSocket({
|
||||
write: (chunk, encoding, callback) => {
|
||||
this.push(chunk, encoding)
|
||||
callback?.()
|
||||
},
|
||||
read() {},
|
||||
})
|
||||
)
|
||||
|
||||
/**
|
||||
* @note Remove the `Connection` and `Date` response headers
|
||||
* injected by `ServerResponse` by default. Those are required
|
||||
* from the server but the interceptor is NOT technically a server.
|
||||
* It's confusing to add response headers that the developer didn't
|
||||
* specify themselves. They can always add these if they wish.
|
||||
* @see https://www.rfc-editor.org/rfc/rfc9110#field.date
|
||||
* @see https://www.rfc-editor.org/rfc/rfc9110#field.connection
|
||||
*/
|
||||
serverResponse.removeHeader('connection')
|
||||
serverResponse.removeHeader('date')
|
||||
|
||||
const rawResponseHeaders = getRawFetchHeaders(response.headers)
|
||||
|
||||
/**
|
||||
* @note Call `.writeHead` in order to set the raw response headers
|
||||
* in the same case as they were provided by the developer. Using
|
||||
* `.setHeader()`/`.appendHeader()` normalizes header names.
|
||||
*/
|
||||
serverResponse.writeHead(
|
||||
response.status,
|
||||
response.statusText || STATUS_CODES[response.status],
|
||||
rawResponseHeaders
|
||||
)
|
||||
|
||||
// If the developer destroy the socket, gracefully destroy the response.
|
||||
this.once('error', () => {
|
||||
serverResponse.destroy()
|
||||
})
|
||||
|
||||
if (response.body) {
|
||||
try {
|
||||
const reader = response.body.getReader()
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
|
||||
if (done) {
|
||||
serverResponse.end()
|
||||
break
|
||||
}
|
||||
|
||||
serverResponse.write(value)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
serverResponse.destroy()
|
||||
/**
|
||||
* @note Destroy the request socket gracefully.
|
||||
* Response stream errors do NOT produce request errors.
|
||||
*/
|
||||
this.destroy()
|
||||
return
|
||||
}
|
||||
|
||||
serverResponse.destroy()
|
||||
throw error
|
||||
}
|
||||
} else {
|
||||
serverResponse.end()
|
||||
}
|
||||
|
||||
// Close the socket if the connection wasn't marked as keep-alive.
|
||||
if (!this.shouldKeepAlive) {
|
||||
this.emit('readable')
|
||||
|
||||
/**
|
||||
* @todo @fixme This is likely a hack.
|
||||
* Since we push null to the socket, it never propagates to the
|
||||
* parser, and the parser never calls "onResponseEnd" to close
|
||||
* the response stream. We are closing the stream here manually
|
||||
* but that shouldn't be the case.
|
||||
*/
|
||||
this.responseStream?.push(null)
|
||||
this.push(null)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close this socket connection with the given error.
|
||||
*/
|
||||
public errorWith(error?: Error): void {
|
||||
this.destroy(error)
|
||||
}
|
||||
|
||||
private mockConnect(): void {
|
||||
// Calling this method immediately puts the socket
|
||||
// into the connected state.
|
||||
this.connecting = false
|
||||
|
||||
const isIPv6 =
|
||||
net.isIPv6(this.connectionOptions.hostname) ||
|
||||
this.connectionOptions.family === 6
|
||||
const addressInfo = {
|
||||
address: isIPv6 ? '::1' : '127.0.0.1',
|
||||
family: isIPv6 ? 'IPv6' : 'IPv4',
|
||||
port: this.connectionOptions.port,
|
||||
}
|
||||
// Return fake address information for the socket.
|
||||
this.address = () => addressInfo
|
||||
this.emit(
|
||||
'lookup',
|
||||
null,
|
||||
addressInfo.address,
|
||||
addressInfo.family === 'IPv6' ? 6 : 4,
|
||||
this.connectionOptions.host
|
||||
)
|
||||
this.emit('connect')
|
||||
this.emit('ready')
|
||||
|
||||
if (this.baseUrl.protocol === 'https:') {
|
||||
this.emit('secure')
|
||||
this.emit('secureConnect')
|
||||
|
||||
// A single TLS connection is represented by two "session" events.
|
||||
this.emit(
|
||||
'session',
|
||||
this.connectionOptions.session ||
|
||||
Buffer.from('mock-session-renegotiate')
|
||||
)
|
||||
this.emit('session', Buffer.from('mock-session-resume'))
|
||||
}
|
||||
}
|
||||
|
||||
private flushWriteBuffer(): void {
|
||||
for (const writeCall of this.writeBuffer) {
|
||||
if (typeof writeCall[2] === 'function') {
|
||||
writeCall[2]()
|
||||
/**
|
||||
* @note Remove the callback from the write call
|
||||
* so it doesn't get called twice on passthrough
|
||||
* if `request.end()` was called within `request.write()`.
|
||||
* @see https://github.com/mswjs/interceptors/issues/684
|
||||
*/
|
||||
writeCall[2] = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This callback might be called when the request is "slow":
|
||||
* - Request headers were fragmented across multiple TCP packages;
|
||||
* - Request headers were too large to be processed in a single run
|
||||
* (e.g. more than 30 request headers).
|
||||
* @note This is called before request start.
|
||||
*/
|
||||
private onRequestHeaders: HeadersCallback = (rawHeaders) => {
|
||||
this.requestRawHeadersBuffer.push(...rawHeaders)
|
||||
}
|
||||
|
||||
private onRequestStart: RequestHeadersCompleteCallback = (
|
||||
versionMajor,
|
||||
versionMinor,
|
||||
rawHeaders,
|
||||
_,
|
||||
path,
|
||||
__,
|
||||
___,
|
||||
____,
|
||||
shouldKeepAlive
|
||||
) => {
|
||||
this.shouldKeepAlive = shouldKeepAlive
|
||||
|
||||
const url = new URL(path || '', this.baseUrl)
|
||||
const method = this.connectionOptions.method?.toUpperCase() || 'GET'
|
||||
const headers = FetchResponse.parseRawHeaders([
|
||||
...this.requestRawHeadersBuffer,
|
||||
...(rawHeaders || []),
|
||||
])
|
||||
this.requestRawHeadersBuffer.length = 0
|
||||
|
||||
const canHaveBody = method !== 'GET' && method !== 'HEAD'
|
||||
|
||||
// Translate the basic authorization in the URL to the request header.
|
||||
// Constructing a Request instance with a URL containing auth is no-op.
|
||||
if (url.username || url.password) {
|
||||
if (!headers.has('authorization')) {
|
||||
headers.set('authorization', `Basic ${url.username}:${url.password}`)
|
||||
}
|
||||
url.username = ''
|
||||
url.password = ''
|
||||
}
|
||||
|
||||
// Create a new stream for each request.
|
||||
// If this Socket is reused for multiple requests,
|
||||
// this ensures that each request gets its own stream.
|
||||
// One Socket instance can only handle one request at a time.
|
||||
this.requestStream = new Readable({
|
||||
/**
|
||||
* @note Provide the `read()` method so a `Readable` could be
|
||||
* used as the actual request body (the stream calls "read()").
|
||||
* We control the queue in the onRequestBody/End functions.
|
||||
*/
|
||||
read: () => {
|
||||
// If the user attempts to read the request body,
|
||||
// flush the write buffer to trigger the callbacks.
|
||||
// This way, if the request stream ends in the write callback,
|
||||
// it will indeed end correctly.
|
||||
this.flushWriteBuffer()
|
||||
},
|
||||
})
|
||||
|
||||
const requestId = createRequestId()
|
||||
this.request = new Request(url, {
|
||||
method,
|
||||
headers,
|
||||
credentials: 'same-origin',
|
||||
// @ts-expect-error Undocumented Fetch property.
|
||||
duplex: canHaveBody ? 'half' : undefined,
|
||||
body: canHaveBody ? (Readable.toWeb(this.requestStream!) as any) : null,
|
||||
})
|
||||
|
||||
Reflect.set(this.request, kRequestId, requestId)
|
||||
|
||||
// Set the raw `http.ClientRequest` instance on the request instance.
|
||||
// This is useful for cases like getting the raw headers of the request.
|
||||
setRawRequest(this.request, Reflect.get(this, '_httpMessage'))
|
||||
|
||||
// Create a copy of the request body stream and store it on the request.
|
||||
// This is only needed for the consumers who wish to read the request body stream
|
||||
// of requests that cannot have a body per Fetch API specification (i.e. GET, HEAD).
|
||||
setRawRequestBodyStream(this.request, this.requestStream)
|
||||
|
||||
// Skip handling the request that's already being handled
|
||||
// by another (parent) interceptor. For example, XMLHttpRequest
|
||||
// is often implemented via ClientRequest in Node.js (e.g. JSDOM).
|
||||
// In that case, XHR interceptor will bubble down to the ClientRequest
|
||||
// interceptor. No need to try to handle that request again.
|
||||
/**
|
||||
* @fixme Stop relying on the "X-Request-Id" request header
|
||||
* to figure out if one interceptor has been invoked within another.
|
||||
* @see https://github.com/mswjs/interceptors/issues/378
|
||||
*/
|
||||
if (this.request.headers.has(INTERNAL_REQUEST_ID_HEADER_NAME)) {
|
||||
this.passthrough()
|
||||
return
|
||||
}
|
||||
|
||||
this.onRequest({
|
||||
requestId,
|
||||
request: this.request,
|
||||
socket: this,
|
||||
})
|
||||
}
|
||||
|
||||
private onRequestBody(chunk: Buffer): void {
|
||||
invariant(
|
||||
this.requestStream,
|
||||
'Failed to write to a request stream: stream does not exist'
|
||||
)
|
||||
|
||||
this.requestStream.push(chunk)
|
||||
}
|
||||
|
||||
private onRequestEnd(): void {
|
||||
// Request end can be called for requests without body.
|
||||
if (this.requestStream) {
|
||||
this.requestStream.push(null)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This callback might be called when the response is "slow":
|
||||
* - Response headers were fragmented across multiple TCP packages;
|
||||
* - Response headers were too large to be processed in a single run
|
||||
* (e.g. more than 30 response headers).
|
||||
* @note This is called before response start.
|
||||
*/
|
||||
private onResponseHeaders: HeadersCallback = (rawHeaders) => {
|
||||
this.responseRawHeadersBuffer.push(...rawHeaders)
|
||||
}
|
||||
|
||||
private onResponseStart: ResponseHeadersCompleteCallback = (
|
||||
versionMajor,
|
||||
versionMinor,
|
||||
rawHeaders,
|
||||
method,
|
||||
url,
|
||||
status,
|
||||
statusText
|
||||
) => {
|
||||
const headers = FetchResponse.parseRawHeaders([
|
||||
...this.responseRawHeadersBuffer,
|
||||
...(rawHeaders || []),
|
||||
])
|
||||
this.responseRawHeadersBuffer.length = 0
|
||||
|
||||
const response = new FetchResponse(
|
||||
/**
|
||||
* @note The Fetch API response instance exposed to the consumer
|
||||
* is created over the response stream of the HTTP parser. It is NOT
|
||||
* related to the Socket instance. This way, you can read response body
|
||||
* in response listener while the Socket instance delays the emission
|
||||
* of "end" and other events until those response listeners are finished.
|
||||
*/
|
||||
FetchResponse.isResponseWithBody(status)
|
||||
? (Readable.toWeb(
|
||||
(this.responseStream = new Readable({ read() {} }))
|
||||
) as any)
|
||||
: null,
|
||||
{
|
||||
url,
|
||||
status,
|
||||
statusText,
|
||||
headers,
|
||||
}
|
||||
)
|
||||
|
||||
invariant(
|
||||
this.request,
|
||||
'Failed to handle a response: request does not exist'
|
||||
)
|
||||
|
||||
FetchResponse.setUrl(this.request.url, response)
|
||||
|
||||
/**
|
||||
* @fixme Stop relying on the "X-Request-Id" request header
|
||||
* to figure out if one interceptor has been invoked within another.
|
||||
* @see https://github.com/mswjs/interceptors/issues/378
|
||||
*/
|
||||
if (this.request.headers.has(INTERNAL_REQUEST_ID_HEADER_NAME)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.responseListenersPromise = this.onResponse({
|
||||
response,
|
||||
isMockedResponse: this.socketState === 'mock',
|
||||
requestId: Reflect.get(this.request, kRequestId),
|
||||
request: this.request,
|
||||
socket: this,
|
||||
})
|
||||
}
|
||||
|
||||
private onResponseBody(chunk: Buffer) {
|
||||
invariant(
|
||||
this.responseStream,
|
||||
'Failed to write to a response stream: stream does not exist'
|
||||
)
|
||||
|
||||
this.responseStream.push(chunk)
|
||||
}
|
||||
|
||||
private onResponseEnd(): void {
|
||||
// Response end can be called for responses without body.
|
||||
if (this.responseStream) {
|
||||
this.responseStream.push(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
110
node_modules/@mswjs/interceptors/src/interceptors/ClientRequest/agents.ts
generated
vendored
Normal file
110
node_modules/@mswjs/interceptors/src/interceptors/ClientRequest/agents.ts
generated
vendored
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Here's how requests are handled in Node.js:
|
||||
*
|
||||
* 1. http.ClientRequest instance calls `agent.addRequest(request, options, cb)`.
|
||||
* 2. Agent creates a new socket: `agent.createSocket(options, cb)`.
|
||||
* 3. Agent creates a new connection: `agent.createConnection(options, cb)`.
|
||||
*/
|
||||
import net from 'node:net'
|
||||
import http from 'node:http'
|
||||
import https from 'node:https'
|
||||
import {
|
||||
MockHttpSocket,
|
||||
type MockHttpSocketRequestCallback,
|
||||
type MockHttpSocketResponseCallback,
|
||||
} from './MockHttpSocket'
|
||||
|
||||
declare module 'node:http' {
|
||||
interface Agent {
|
||||
options?: http.AgentOptions
|
||||
createConnection(options: any, callback: any): net.Socket
|
||||
}
|
||||
}
|
||||
|
||||
interface MockAgentOptions {
|
||||
customAgent?: http.RequestOptions['agent']
|
||||
onRequest: MockHttpSocketRequestCallback
|
||||
onResponse: MockHttpSocketResponseCallback
|
||||
}
|
||||
|
||||
export class MockAgent extends http.Agent {
|
||||
private customAgent?: http.RequestOptions['agent']
|
||||
private onRequest: MockHttpSocketRequestCallback
|
||||
private onResponse: MockHttpSocketResponseCallback
|
||||
|
||||
constructor(options: MockAgentOptions) {
|
||||
super()
|
||||
this.customAgent = options.customAgent
|
||||
this.onRequest = options.onRequest
|
||||
this.onResponse = options.onResponse
|
||||
}
|
||||
|
||||
public createConnection(options: any, callback: any): net.Socket {
|
||||
const createConnection =
|
||||
this.customAgent instanceof http.Agent
|
||||
? this.customAgent.createConnection
|
||||
: super.createConnection
|
||||
|
||||
const createConnectionOptions =
|
||||
this.customAgent instanceof http.Agent
|
||||
? {
|
||||
...options,
|
||||
...this.customAgent.options,
|
||||
}
|
||||
: options
|
||||
|
||||
const socket = new MockHttpSocket({
|
||||
connectionOptions: options,
|
||||
createConnection: createConnection.bind(
|
||||
this.customAgent || this,
|
||||
createConnectionOptions,
|
||||
callback
|
||||
),
|
||||
onRequest: this.onRequest.bind(this),
|
||||
onResponse: this.onResponse.bind(this),
|
||||
})
|
||||
|
||||
return socket
|
||||
}
|
||||
}
|
||||
|
||||
export class MockHttpsAgent extends https.Agent {
|
||||
private customAgent?: https.RequestOptions['agent']
|
||||
private onRequest: MockHttpSocketRequestCallback
|
||||
private onResponse: MockHttpSocketResponseCallback
|
||||
|
||||
constructor(options: MockAgentOptions) {
|
||||
super()
|
||||
this.customAgent = options.customAgent
|
||||
this.onRequest = options.onRequest
|
||||
this.onResponse = options.onResponse
|
||||
}
|
||||
|
||||
public createConnection(options: any, callback: any): net.Socket {
|
||||
const createConnection =
|
||||
this.customAgent instanceof http.Agent
|
||||
? this.customAgent.createConnection
|
||||
: super.createConnection
|
||||
|
||||
const createConnectionOptions =
|
||||
this.customAgent instanceof http.Agent
|
||||
? {
|
||||
...options,
|
||||
...this.customAgent.options,
|
||||
}
|
||||
: options
|
||||
|
||||
const socket = new MockHttpSocket({
|
||||
connectionOptions: options,
|
||||
createConnection: createConnection.bind(
|
||||
this.customAgent || this,
|
||||
createConnectionOptions,
|
||||
callback
|
||||
),
|
||||
onRequest: this.onRequest.bind(this),
|
||||
onResponse: this.onResponse.bind(this),
|
||||
})
|
||||
|
||||
return socket
|
||||
}
|
||||
}
|
||||
75
node_modules/@mswjs/interceptors/src/interceptors/ClientRequest/index.test.ts
generated
vendored
Normal file
75
node_modules/@mswjs/interceptors/src/interceptors/ClientRequest/index.test.ts
generated
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
import { it, expect, beforeAll, afterEach, afterAll } from 'vitest'
|
||||
import http from 'node:http'
|
||||
import { HttpServer } from '@open-draft/test-server/http'
|
||||
import { DeferredPromise } from '@open-draft/deferred-promise'
|
||||
import { ClientRequestInterceptor } from '.'
|
||||
import { sleep, waitForClientRequest } from '../../../test/helpers'
|
||||
|
||||
const httpServer = new HttpServer((app) => {
|
||||
app.get('/', (_req, res) => {
|
||||
res.status(200).send('/')
|
||||
})
|
||||
app.get('/get', (_req, res) => {
|
||||
res.status(200).send('/get')
|
||||
})
|
||||
})
|
||||
|
||||
const interceptor = new ClientRequestInterceptor()
|
||||
|
||||
beforeAll(async () => {
|
||||
interceptor.apply()
|
||||
await httpServer.listen()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
interceptor.removeAllListeners()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
interceptor.dispose()
|
||||
await httpServer.close()
|
||||
})
|
||||
|
||||
it('abort the request if the abort signal is emitted', async () => {
|
||||
const requestUrl = httpServer.http.url('/')
|
||||
|
||||
interceptor.on('request', async function delayedResponse({ controller }) {
|
||||
await sleep(1_000)
|
||||
controller.respondWith(new Response())
|
||||
})
|
||||
|
||||
const abortController = new AbortController()
|
||||
const request = http.get(requestUrl, { signal: abortController.signal })
|
||||
|
||||
abortController.abort()
|
||||
|
||||
const abortErrorPromise = new DeferredPromise<Error>()
|
||||
request.on('error', function (error) {
|
||||
abortErrorPromise.resolve(error)
|
||||
})
|
||||
|
||||
const abortError = await abortErrorPromise
|
||||
expect(abortError.name).toEqual('AbortError')
|
||||
|
||||
expect(request.destroyed).toBe(true)
|
||||
})
|
||||
|
||||
it('patch the Headers object correctly after dispose and reapply', async () => {
|
||||
interceptor.dispose()
|
||||
interceptor.apply()
|
||||
|
||||
interceptor.on('request', ({ controller }) => {
|
||||
const headers = new Headers({
|
||||
'X-CustoM-HeadeR': 'Yes',
|
||||
})
|
||||
controller.respondWith(new Response(null, { headers }))
|
||||
})
|
||||
|
||||
const request = http.get(httpServer.http.url('/'))
|
||||
const { res } = await waitForClientRequest(request)
|
||||
|
||||
expect(res.rawHeaders).toEqual(
|
||||
expect.arrayContaining(['X-CustoM-HeadeR', 'Yes'])
|
||||
)
|
||||
expect(res.headers['x-custom-header']).toEqual('Yes')
|
||||
})
|
||||
193
node_modules/@mswjs/interceptors/src/interceptors/ClientRequest/index.ts
generated
vendored
Normal file
193
node_modules/@mswjs/interceptors/src/interceptors/ClientRequest/index.ts
generated
vendored
Normal file
@@ -0,0 +1,193 @@
|
||||
import http from 'node:http'
|
||||
import https from 'node:https'
|
||||
import { Interceptor } from '../../Interceptor'
|
||||
import type { HttpRequestEventMap } from '../../glossary'
|
||||
import {
|
||||
kRequestId,
|
||||
MockHttpSocketRequestCallback,
|
||||
MockHttpSocketResponseCallback,
|
||||
} from './MockHttpSocket'
|
||||
import { MockAgent, MockHttpsAgent } from './agents'
|
||||
import { RequestController } from '../../RequestController'
|
||||
import { emitAsync } from '../../utils/emitAsync'
|
||||
import { normalizeClientRequestArgs } from './utils/normalizeClientRequestArgs'
|
||||
import { handleRequest } from '../../utils/handleRequest'
|
||||
import {
|
||||
recordRawFetchHeaders,
|
||||
restoreHeadersPrototype,
|
||||
} from './utils/recordRawHeaders'
|
||||
|
||||
export class ClientRequestInterceptor extends Interceptor<HttpRequestEventMap> {
|
||||
static symbol = Symbol('client-request-interceptor')
|
||||
|
||||
constructor() {
|
||||
super(ClientRequestInterceptor.symbol)
|
||||
}
|
||||
|
||||
protected setup(): void {
|
||||
const {
|
||||
ClientRequest: OriginalClientRequest,
|
||||
get: originalGet,
|
||||
request: originalRequest,
|
||||
} = http
|
||||
const { get: originalHttpsGet, request: originalHttpsRequest } = https
|
||||
|
||||
const onRequest = this.onRequest.bind(this)
|
||||
const onResponse = this.onResponse.bind(this)
|
||||
|
||||
// Support requests performed via the `ClientRequest` constructor directly.
|
||||
http.ClientRequest = new Proxy(http.ClientRequest, {
|
||||
construct: (target, args: Parameters<typeof http.request>) => {
|
||||
const [url, options, callback] = normalizeClientRequestArgs(
|
||||
'http:',
|
||||
args
|
||||
)
|
||||
|
||||
// Create a mock agent instance appropriate for the request protocol.
|
||||
const Agent = options.protocol === 'https:' ? MockHttpsAgent : MockAgent
|
||||
const mockAgent = new Agent({
|
||||
customAgent: options.agent,
|
||||
onRequest,
|
||||
onResponse,
|
||||
})
|
||||
options.agent = mockAgent
|
||||
|
||||
return Reflect.construct(target, [url, options, callback])
|
||||
},
|
||||
})
|
||||
|
||||
http.request = new Proxy(http.request, {
|
||||
apply: (target, thisArg, args: Parameters<typeof http.request>) => {
|
||||
const [url, options, callback] = normalizeClientRequestArgs(
|
||||
'http:',
|
||||
args
|
||||
)
|
||||
const mockAgent = new MockAgent({
|
||||
customAgent: options.agent,
|
||||
onRequest,
|
||||
onResponse,
|
||||
})
|
||||
options.agent = mockAgent
|
||||
|
||||
return Reflect.apply(target, thisArg, [url, options, callback])
|
||||
},
|
||||
})
|
||||
|
||||
http.get = new Proxy(http.get, {
|
||||
apply: (target, thisArg, args: Parameters<typeof http.get>) => {
|
||||
const [url, options, callback] = normalizeClientRequestArgs(
|
||||
'http:',
|
||||
args
|
||||
)
|
||||
|
||||
const mockAgent = new MockAgent({
|
||||
customAgent: options.agent,
|
||||
onRequest,
|
||||
onResponse,
|
||||
})
|
||||
options.agent = mockAgent
|
||||
|
||||
return Reflect.apply(target, thisArg, [url, options, callback])
|
||||
},
|
||||
})
|
||||
|
||||
//
|
||||
// HTTPS.
|
||||
//
|
||||
|
||||
https.request = new Proxy(https.request, {
|
||||
apply: (target, thisArg, args: Parameters<typeof https.request>) => {
|
||||
const [url, options, callback] = normalizeClientRequestArgs(
|
||||
'https:',
|
||||
args
|
||||
)
|
||||
|
||||
const mockAgent = new MockHttpsAgent({
|
||||
customAgent: options.agent,
|
||||
onRequest,
|
||||
onResponse,
|
||||
})
|
||||
options.agent = mockAgent
|
||||
|
||||
return Reflect.apply(target, thisArg, [url, options, callback])
|
||||
},
|
||||
})
|
||||
|
||||
https.get = new Proxy(https.get, {
|
||||
apply: (target, thisArg, args: Parameters<typeof https.get>) => {
|
||||
const [url, options, callback] = normalizeClientRequestArgs(
|
||||
'https:',
|
||||
args
|
||||
)
|
||||
|
||||
const mockAgent = new MockHttpsAgent({
|
||||
customAgent: options.agent,
|
||||
onRequest,
|
||||
onResponse,
|
||||
})
|
||||
options.agent = mockAgent
|
||||
|
||||
return Reflect.apply(target, thisArg, [url, options, callback])
|
||||
},
|
||||
})
|
||||
|
||||
// Spy on `Header.prototype.set` and `Header.prototype.append` calls
|
||||
// and record the raw header names provided. This is to support
|
||||
// `IncomingMessage.prototype.rawHeaders`.
|
||||
recordRawFetchHeaders()
|
||||
|
||||
this.subscriptions.push(() => {
|
||||
http.ClientRequest = OriginalClientRequest
|
||||
|
||||
http.get = originalGet
|
||||
http.request = originalRequest
|
||||
|
||||
https.get = originalHttpsGet
|
||||
https.request = originalHttpsRequest
|
||||
|
||||
restoreHeadersPrototype()
|
||||
})
|
||||
}
|
||||
|
||||
private onRequest: MockHttpSocketRequestCallback = async ({
|
||||
request,
|
||||
socket,
|
||||
}) => {
|
||||
const controller = new RequestController(request, {
|
||||
passthrough() {
|
||||
socket.passthrough()
|
||||
},
|
||||
async respondWith(response) {
|
||||
await socket.respondWith(response)
|
||||
},
|
||||
errorWith(reason) {
|
||||
if (reason instanceof Error) {
|
||||
socket.errorWith(reason)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
await handleRequest({
|
||||
request,
|
||||
requestId: Reflect.get(request, kRequestId),
|
||||
controller,
|
||||
emitter: this.emitter,
|
||||
})
|
||||
}
|
||||
|
||||
public onResponse: MockHttpSocketResponseCallback = async ({
|
||||
requestId,
|
||||
request,
|
||||
response,
|
||||
isMockedResponse,
|
||||
}) => {
|
||||
// Return the promise to when all the response event listeners
|
||||
// are finished.
|
||||
return emitAsync(this.emitter, 'response', {
|
||||
requestId,
|
||||
request,
|
||||
response,
|
||||
isMockedResponse,
|
||||
})
|
||||
}
|
||||
}
|
||||
54
node_modules/@mswjs/interceptors/src/interceptors/ClientRequest/utils/getIncomingMessageBody.test.ts
generated
vendored
Normal file
54
node_modules/@mswjs/interceptors/src/interceptors/ClientRequest/utils/getIncomingMessageBody.test.ts
generated
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
import { it, expect } from 'vitest'
|
||||
import { IncomingMessage } from 'http'
|
||||
import { Socket } from 'net'
|
||||
import * as zlib from 'zlib'
|
||||
import { getIncomingMessageBody } from './getIncomingMessageBody'
|
||||
|
||||
it('returns utf8 string given a utf8 response body', async () => {
|
||||
const utfBuffer = Buffer.from('one')
|
||||
const message = new IncomingMessage(new Socket())
|
||||
|
||||
const pendingResponseBody = getIncomingMessageBody(message)
|
||||
message.emit('data', utfBuffer)
|
||||
message.emit('end')
|
||||
|
||||
expect(await pendingResponseBody).toEqual('one')
|
||||
})
|
||||
|
||||
it('returns utf8 string given a gzipped response body', async () => {
|
||||
const utfBuffer = zlib.gzipSync(Buffer.from('two'))
|
||||
const message = new IncomingMessage(new Socket())
|
||||
message.headers = {
|
||||
'content-encoding': 'gzip',
|
||||
}
|
||||
|
||||
const pendingResponseBody = getIncomingMessageBody(message)
|
||||
message.emit('data', utfBuffer)
|
||||
message.emit('end')
|
||||
|
||||
expect(await pendingResponseBody).toEqual('two')
|
||||
})
|
||||
|
||||
it('returns utf8 string given a gzipped response body with incorrect "content-length"', async () => {
|
||||
const utfBuffer = zlib.gzipSync(Buffer.from('three'))
|
||||
const message = new IncomingMessage(new Socket())
|
||||
message.headers = {
|
||||
'content-encoding': 'gzip',
|
||||
'content-length': '500',
|
||||
}
|
||||
|
||||
const pendingResponseBody = getIncomingMessageBody(message)
|
||||
message.emit('data', utfBuffer)
|
||||
message.emit('end')
|
||||
|
||||
expect(await pendingResponseBody).toEqual('three')
|
||||
})
|
||||
|
||||
it('returns empty string given an empty body', async () => {
|
||||
const message = new IncomingMessage(new Socket())
|
||||
|
||||
const pendingResponseBody = getIncomingMessageBody(message)
|
||||
message.emit('end')
|
||||
|
||||
expect(await pendingResponseBody).toEqual('')
|
||||
})
|
||||
45
node_modules/@mswjs/interceptors/src/interceptors/ClientRequest/utils/getIncomingMessageBody.ts
generated
vendored
Normal file
45
node_modules/@mswjs/interceptors/src/interceptors/ClientRequest/utils/getIncomingMessageBody.ts
generated
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
import { IncomingMessage } from 'http'
|
||||
import { PassThrough } from 'stream'
|
||||
import * as zlib from 'zlib'
|
||||
import { Logger } from '@open-draft/logger'
|
||||
|
||||
const logger = new Logger('http getIncomingMessageBody')
|
||||
|
||||
export function getIncomingMessageBody(
|
||||
response: IncomingMessage
|
||||
): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
logger.info('cloning the original response...')
|
||||
|
||||
// Pipe the original response to support non-clone
|
||||
// "response" input. No need to clone the response,
|
||||
// as we always have access to the full "response" input,
|
||||
// either a clone or an original one (in tests).
|
||||
const responseClone = response.pipe(new PassThrough())
|
||||
const stream =
|
||||
response.headers['content-encoding'] === 'gzip'
|
||||
? responseClone.pipe(zlib.createGunzip())
|
||||
: responseClone
|
||||
|
||||
const encoding = response.readableEncoding || 'utf8'
|
||||
stream.setEncoding(encoding)
|
||||
logger.info('using encoding:', encoding)
|
||||
|
||||
let body = ''
|
||||
|
||||
stream.on('data', (responseBody) => {
|
||||
logger.info('response body read:', responseBody)
|
||||
body += responseBody
|
||||
})
|
||||
|
||||
stream.once('end', () => {
|
||||
logger.info('response body end')
|
||||
resolve(body)
|
||||
})
|
||||
|
||||
stream.once('error', (error) => {
|
||||
logger.info('error while reading response body:', error)
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
427
node_modules/@mswjs/interceptors/src/interceptors/ClientRequest/utils/normalizeClientRequestArgs.test.ts
generated
vendored
Normal file
427
node_modules/@mswjs/interceptors/src/interceptors/ClientRequest/utils/normalizeClientRequestArgs.test.ts
generated
vendored
Normal file
@@ -0,0 +1,427 @@
|
||||
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 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)
|
||||
})
|
||||
268
node_modules/@mswjs/interceptors/src/interceptors/ClientRequest/utils/normalizeClientRequestArgs.ts
generated
vendored
Normal file
268
node_modules/@mswjs/interceptors/src/interceptors/ClientRequest/utils/normalizeClientRequestArgs.ts
generated
vendored
Normal file
@@ -0,0 +1,268 @@
|
||||
import { urlToHttpOptions } from 'node:url'
|
||||
import {
|
||||
Agent as HttpAgent,
|
||||
globalAgent as httpGlobalAgent,
|
||||
IncomingMessage,
|
||||
} from 'node:http'
|
||||
import {
|
||||
RequestOptions,
|
||||
Agent as HttpsAgent,
|
||||
globalAgent as httpsGlobalAgent,
|
||||
} from 'node:https'
|
||||
import {
|
||||
/**
|
||||
* @note Use the Node.js URL instead of the global URL
|
||||
* because environments like JSDOM may override the global,
|
||||
* breaking the compatibility with Node.js.
|
||||
* @see https://github.com/node-fetch/node-fetch/issues/1376#issuecomment-966435555
|
||||
*/
|
||||
URL,
|
||||
Url as LegacyURL,
|
||||
parse as parseUrl,
|
||||
} from 'node:url'
|
||||
import { Logger } from '@open-draft/logger'
|
||||
import {
|
||||
ResolvedRequestOptions,
|
||||
getUrlByRequestOptions,
|
||||
} from '../../../utils/getUrlByRequestOptions'
|
||||
import { cloneObject } from '../../../utils/cloneObject'
|
||||
import { isObject } from '../../../utils/isObject'
|
||||
|
||||
const logger = new Logger('http normalizeClientRequestArgs')
|
||||
|
||||
export type HttpRequestCallback = (response: IncomingMessage) => void
|
||||
|
||||
export type ClientRequestArgs =
|
||||
// Request without any arguments is also possible.
|
||||
| []
|
||||
| [string | URL | LegacyURL, HttpRequestCallback?]
|
||||
| [string | URL | LegacyURL, RequestOptions, HttpRequestCallback?]
|
||||
| [RequestOptions, HttpRequestCallback?]
|
||||
|
||||
function resolveRequestOptions(
|
||||
args: ClientRequestArgs,
|
||||
url: URL
|
||||
): RequestOptions {
|
||||
// Calling `fetch` provides only URL to `ClientRequest`
|
||||
// without any `RequestOptions` or callback.
|
||||
if (typeof args[1] === 'undefined' || typeof args[1] === 'function') {
|
||||
logger.info('request options not provided, deriving from the url', url)
|
||||
return urlToHttpOptions(url)
|
||||
}
|
||||
|
||||
if (args[1]) {
|
||||
logger.info('has custom RequestOptions!', args[1])
|
||||
const requestOptionsFromUrl = urlToHttpOptions(url)
|
||||
|
||||
logger.info('derived RequestOptions from the URL:', requestOptionsFromUrl)
|
||||
|
||||
/**
|
||||
* Clone the request options to lock their state
|
||||
* at the moment they are provided to `ClientRequest`.
|
||||
* @see https://github.com/mswjs/interceptors/issues/86
|
||||
*/
|
||||
logger.info('cloning RequestOptions...')
|
||||
const clonedRequestOptions = cloneObject(args[1])
|
||||
logger.info('successfully cloned RequestOptions!', clonedRequestOptions)
|
||||
|
||||
return {
|
||||
...requestOptionsFromUrl,
|
||||
...clonedRequestOptions,
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('using an empty object as request options')
|
||||
return {} as RequestOptions
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the given `URL` instance with the explicit properties provided
|
||||
* on the `RequestOptions` object. The options object takes precedence,
|
||||
* and will replace URL properties like "host", "path", and "port", if specified.
|
||||
*/
|
||||
function overrideUrlByRequestOptions(url: URL, options: RequestOptions): URL {
|
||||
url.host = options.host || url.host
|
||||
url.hostname = options.hostname || url.hostname
|
||||
url.port = options.port ? options.port.toString() : url.port
|
||||
|
||||
if (options.path) {
|
||||
const parsedOptionsPath = parseUrl(options.path, false)
|
||||
url.pathname = parsedOptionsPath.pathname || ''
|
||||
url.search = parsedOptionsPath.search || ''
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
function resolveCallback(
|
||||
args: ClientRequestArgs
|
||||
): HttpRequestCallback | undefined {
|
||||
return typeof args[1] === 'function' ? args[1] : args[2]
|
||||
}
|
||||
|
||||
export type NormalizedClientRequestArgs = [
|
||||
url: URL,
|
||||
options: ResolvedRequestOptions,
|
||||
callback?: HttpRequestCallback
|
||||
]
|
||||
|
||||
/**
|
||||
* Normalizes parameters given to a `http.request` call
|
||||
* so it always has a `URL` and `RequestOptions`.
|
||||
*/
|
||||
export function normalizeClientRequestArgs(
|
||||
defaultProtocol: string,
|
||||
args: ClientRequestArgs
|
||||
): NormalizedClientRequestArgs {
|
||||
let url: URL
|
||||
let options: ResolvedRequestOptions
|
||||
let callback: HttpRequestCallback | undefined
|
||||
|
||||
logger.info('arguments', args)
|
||||
logger.info('using default protocol:', defaultProtocol)
|
||||
|
||||
// Support "http.request()" calls without any arguments.
|
||||
// That call results in a "GET http://localhost" request.
|
||||
if (args.length === 0) {
|
||||
const url = new URL('http://localhost')
|
||||
const options = resolveRequestOptions(args, url)
|
||||
return [url, options]
|
||||
}
|
||||
|
||||
// Convert a url string into a URL instance
|
||||
// and derive request options from it.
|
||||
if (typeof args[0] === 'string') {
|
||||
logger.info('first argument is a location string:', args[0])
|
||||
|
||||
url = new URL(args[0])
|
||||
logger.info('created a url:', url)
|
||||
|
||||
const requestOptionsFromUrl = urlToHttpOptions(url)
|
||||
logger.info('request options from url:', requestOptionsFromUrl)
|
||||
|
||||
options = resolveRequestOptions(args, url)
|
||||
logger.info('resolved request options:', options)
|
||||
|
||||
callback = resolveCallback(args)
|
||||
}
|
||||
// Handle a given URL instance as-is
|
||||
// and derive request options from it.
|
||||
else if (args[0] instanceof URL) {
|
||||
url = args[0]
|
||||
logger.info('first argument is a URL:', url)
|
||||
|
||||
// Check if the second provided argument is RequestOptions.
|
||||
// If it is, check if "options.path" was set and rewrite it
|
||||
// on the input URL.
|
||||
// Do this before resolving options from the URL below
|
||||
// to prevent query string from being duplicated in the path.
|
||||
if (typeof args[1] !== 'undefined' && isObject<RequestOptions>(args[1])) {
|
||||
url = overrideUrlByRequestOptions(url, args[1])
|
||||
}
|
||||
|
||||
options = resolveRequestOptions(args, url)
|
||||
logger.info('derived request options:', options)
|
||||
|
||||
callback = resolveCallback(args)
|
||||
}
|
||||
// Handle a legacy URL instance and re-normalize from either a RequestOptions object
|
||||
// or a WHATWG URL.
|
||||
else if ('hash' in args[0] && !('method' in args[0])) {
|
||||
const [legacyUrl] = args
|
||||
logger.info('first argument is a legacy URL:', legacyUrl)
|
||||
|
||||
if (legacyUrl.hostname === null) {
|
||||
/**
|
||||
* We are dealing with a relative url, so use the path as an "option" and
|
||||
* merge in any existing options, giving priority to existing options -- i.e. a path in any
|
||||
* existing options will take precedence over the one contained in the url. This is consistent
|
||||
* with the behaviour in ClientRequest.
|
||||
* @see https://github.com/nodejs/node/blob/d84f1312915fe45fe0febe888db692c74894c382/lib/_http_client.js#L122
|
||||
*/
|
||||
logger.info('given legacy URL is relative (no hostname)')
|
||||
|
||||
return isObject(args[1])
|
||||
? normalizeClientRequestArgs(defaultProtocol, [
|
||||
{ path: legacyUrl.path, ...args[1] },
|
||||
args[2],
|
||||
])
|
||||
: normalizeClientRequestArgs(defaultProtocol, [
|
||||
{ path: legacyUrl.path },
|
||||
args[1] as HttpRequestCallback,
|
||||
])
|
||||
}
|
||||
|
||||
logger.info('given legacy url is absolute')
|
||||
|
||||
// We are dealing with an absolute URL, so convert to WHATWG and try again.
|
||||
const resolvedUrl = new URL(legacyUrl.href)
|
||||
|
||||
return args[1] === undefined
|
||||
? normalizeClientRequestArgs(defaultProtocol, [resolvedUrl])
|
||||
: typeof args[1] === 'function'
|
||||
? normalizeClientRequestArgs(defaultProtocol, [resolvedUrl, args[1]])
|
||||
: normalizeClientRequestArgs(defaultProtocol, [
|
||||
resolvedUrl,
|
||||
args[1],
|
||||
args[2],
|
||||
])
|
||||
}
|
||||
// Handle a given "RequestOptions" object as-is
|
||||
// and derive the URL instance from it.
|
||||
else if (isObject(args[0])) {
|
||||
options = { ...(args[0] as any) }
|
||||
logger.info('first argument is RequestOptions:', options)
|
||||
|
||||
// When handling a "RequestOptions" object without an explicit "protocol",
|
||||
// infer the protocol from the request issuing module (http/https).
|
||||
options.protocol = options.protocol || defaultProtocol
|
||||
logger.info('normalized request options:', options)
|
||||
|
||||
url = getUrlByRequestOptions(options)
|
||||
logger.info('created a URL from RequestOptions:', url.href)
|
||||
|
||||
callback = resolveCallback(args)
|
||||
} else {
|
||||
throw new Error(
|
||||
`Failed to construct ClientRequest with these parameters: ${args}`
|
||||
)
|
||||
}
|
||||
|
||||
options.protocol = options.protocol || url.protocol
|
||||
options.method = options.method || 'GET'
|
||||
|
||||
/**
|
||||
* Ensure that the default Agent is always set.
|
||||
* This prevents the protocol mismatch for requests with { agent: false },
|
||||
* where the global Agent is inferred.
|
||||
* @see https://github.com/mswjs/msw/issues/1150
|
||||
* @see https://github.com/nodejs/node/blob/418ff70b810f0e7112d48baaa72932a56cfa213b/lib/_http_client.js#L130
|
||||
* @see https://github.com/nodejs/node/blob/418ff70b810f0e7112d48baaa72932a56cfa213b/lib/_http_client.js#L157-L159
|
||||
*/
|
||||
if (!options._defaultAgent) {
|
||||
logger.info(
|
||||
'has no default agent, setting the default agent for "%s"',
|
||||
options.protocol
|
||||
)
|
||||
|
||||
options._defaultAgent =
|
||||
options.protocol === 'https:' ? httpsGlobalAgent : httpGlobalAgent
|
||||
}
|
||||
|
||||
logger.info('successfully resolved url:', url.href)
|
||||
logger.info('successfully resolved options:', options)
|
||||
logger.info('successfully resolved callback:', callback)
|
||||
|
||||
/**
|
||||
* @note If the user-provided URL is not a valid URL in Node.js,
|
||||
* (e.g. the one provided by the JSDOM polyfills), case it to
|
||||
* string. Otherwise, this throws on Node.js incompatibility
|
||||
* (`ERR_INVALID_ARG_TYPE` on the connection listener)
|
||||
* @see https://github.com/node-fetch/node-fetch/issues/1376#issuecomment-966435555
|
||||
*/
|
||||
if (!(url instanceof URL)) {
|
||||
url = (url as any).toString()
|
||||
}
|
||||
|
||||
return [url, options, callback]
|
||||
}
|
||||
48
node_modules/@mswjs/interceptors/src/interceptors/ClientRequest/utils/parserUtils.ts
generated
vendored
Normal file
48
node_modules/@mswjs/interceptors/src/interceptors/ClientRequest/utils/parserUtils.ts
generated
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { Socket } from 'node:net'
|
||||
import { HTTPParser } from '_http_common'
|
||||
|
||||
/**
|
||||
* @see https://github.com/nodejs/node/blob/f3adc11e37b8bfaaa026ea85c1cf22e3a0e29ae9/lib/_http_common.js#L180
|
||||
*/
|
||||
export function freeParser(parser: HTTPParser<any>, socket?: Socket): void {
|
||||
if (parser._consumed) {
|
||||
parser.unconsume()
|
||||
}
|
||||
|
||||
parser._headers = []
|
||||
parser._url = ''
|
||||
parser.socket = null
|
||||
parser.incoming = null
|
||||
parser.outgoing = null
|
||||
parser.maxHeaderPairs = 2000
|
||||
parser._consumed = false
|
||||
parser.onIncoming = null
|
||||
|
||||
parser[HTTPParser.kOnHeaders] = null
|
||||
parser[HTTPParser.kOnHeadersComplete] = null
|
||||
parser[HTTPParser.kOnMessageBegin] = null
|
||||
parser[HTTPParser.kOnMessageComplete] = null
|
||||
parser[HTTPParser.kOnBody] = null
|
||||
parser[HTTPParser.kOnExecute] = null
|
||||
parser[HTTPParser.kOnTimeout] = null
|
||||
|
||||
parser.remove()
|
||||
parser.free()
|
||||
|
||||
if (socket) {
|
||||
/**
|
||||
* @note Unassigning the socket's parser will fail this assertion
|
||||
* if there's still some data being processed on the socket:
|
||||
* @see https://github.com/nodejs/node/blob/4e1f39b678b37017ac9baa0971e3aeecd3b67b51/lib/_http_client.js#L613
|
||||
*/
|
||||
if (socket.destroyed) {
|
||||
// @ts-expect-error Node.js internals.
|
||||
socket.parser = null
|
||||
} else {
|
||||
socket.once('end', () => {
|
||||
// @ts-expect-error Node.js internals.
|
||||
socket.parser = null
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
258
node_modules/@mswjs/interceptors/src/interceptors/ClientRequest/utils/recordRawHeaders.test.ts
generated
vendored
Normal file
258
node_modules/@mswjs/interceptors/src/interceptors/ClientRequest/utils/recordRawHeaders.test.ts
generated
vendored
Normal file
@@ -0,0 +1,258 @@
|
||||
// @vitest-environment node
|
||||
import { it, expect, afterEach } from 'vitest'
|
||||
import {
|
||||
recordRawFetchHeaders,
|
||||
restoreHeadersPrototype,
|
||||
getRawFetchHeaders,
|
||||
} from './recordRawHeaders'
|
||||
|
||||
const url = 'http://localhost'
|
||||
|
||||
afterEach(() => {
|
||||
restoreHeadersPrototype()
|
||||
})
|
||||
|
||||
it('returns an empty list if no headers were set', () => {
|
||||
expect(getRawFetchHeaders(new Headers())).toEqual([])
|
||||
expect(getRawFetchHeaders(new Headers(undefined))).toEqual([])
|
||||
expect(getRawFetchHeaders(new Headers({}))).toEqual([])
|
||||
expect(getRawFetchHeaders(new Headers([]))).toEqual([])
|
||||
expect(getRawFetchHeaders(new Request(url).headers)).toEqual([])
|
||||
expect(getRawFetchHeaders(new Response().headers)).toEqual([])
|
||||
})
|
||||
|
||||
it('records raw headers (Headers / object as init)', () => {
|
||||
recordRawFetchHeaders()
|
||||
const headers = new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
'X-My-Header': '1',
|
||||
})
|
||||
|
||||
expect(getRawFetchHeaders(headers)).toEqual([
|
||||
['Content-Type', 'application/json'],
|
||||
['X-My-Header', '1'],
|
||||
])
|
||||
expect(Object.fromEntries(headers)).toEqual({
|
||||
'content-type': 'application/json',
|
||||
'x-my-header': '1',
|
||||
})
|
||||
})
|
||||
|
||||
it('records raw headers (Headers / array as init)', () => {
|
||||
recordRawFetchHeaders()
|
||||
const headers = new Headers([['X-My-Header', '1']])
|
||||
|
||||
expect(getRawFetchHeaders(headers)).toEqual([['X-My-Header', '1']])
|
||||
expect(Object.fromEntries(headers)).toEqual({
|
||||
'x-my-header': '1',
|
||||
})
|
||||
})
|
||||
|
||||
it('records raw headers (Headers / Headers as init', () => {
|
||||
recordRawFetchHeaders()
|
||||
const headers = new Headers([['X-My-Header', '1']])
|
||||
|
||||
expect(getRawFetchHeaders(new Headers(headers))).toEqual([
|
||||
['X-My-Header', '1'],
|
||||
])
|
||||
})
|
||||
|
||||
it('records raw headers added via ".set()"', () => {
|
||||
recordRawFetchHeaders()
|
||||
const headers = new Headers([['X-My-Header', '1']])
|
||||
headers.set('X-Another-Header', '2')
|
||||
|
||||
expect(getRawFetchHeaders(headers)).toEqual([
|
||||
['X-My-Header', '1'],
|
||||
['X-Another-Header', '2'],
|
||||
])
|
||||
})
|
||||
|
||||
it('records raw headers added via ".append()"', () => {
|
||||
recordRawFetchHeaders()
|
||||
const headers = new Headers([['X-My-Header', '1']])
|
||||
headers.append('X-My-Header', '2')
|
||||
|
||||
expect(getRawFetchHeaders(headers)).toEqual([
|
||||
['X-My-Header', '1'],
|
||||
['X-My-Header', '2'],
|
||||
])
|
||||
})
|
||||
|
||||
it('deletes the header when called ".delete()"', () => {
|
||||
const headers = new Headers([['X-My-Header', '1']])
|
||||
headers.delete('X-My-Header')
|
||||
|
||||
expect(getRawFetchHeaders(headers)).toEqual([])
|
||||
})
|
||||
|
||||
it('records raw headers (Request / object as init)', () => {
|
||||
recordRawFetchHeaders()
|
||||
const request = new Request(url, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-My-Header': '1',
|
||||
},
|
||||
})
|
||||
|
||||
expect(getRawFetchHeaders(request.headers)).toEqual([
|
||||
['Content-Type', 'application/json'],
|
||||
['X-My-Header', '1'],
|
||||
])
|
||||
})
|
||||
|
||||
it('records raw headers (Request / array as init)', () => {
|
||||
recordRawFetchHeaders()
|
||||
const request = new Request(url, {
|
||||
headers: [['X-My-Header', '1']],
|
||||
})
|
||||
|
||||
expect(getRawFetchHeaders(request.headers)).toEqual([['X-My-Header', '1']])
|
||||
})
|
||||
|
||||
it('records raw headers (Request / Headers as init)', () => {
|
||||
recordRawFetchHeaders()
|
||||
const headers = new Headers([['X-My-Header', '1']])
|
||||
const request = new Request(url, { headers })
|
||||
|
||||
expect(getRawFetchHeaders(request.headers)).toEqual([['X-My-Header', '1']])
|
||||
})
|
||||
|
||||
it('records raw headers (Request / Request as init)', () => {
|
||||
recordRawFetchHeaders()
|
||||
const init = new Request(url, { headers: [['X-My-Header', '1']] })
|
||||
const request = new Request(init)
|
||||
|
||||
expect(getRawFetchHeaders(request.headers)).toEqual([['X-My-Header', '1']])
|
||||
})
|
||||
|
||||
it('preserves headers instanceof (Request / Request as init)', () => {
|
||||
recordRawFetchHeaders()
|
||||
const init = new Request(url, { headers: [['X-My-Header', '1']] })
|
||||
new Request(init)
|
||||
expect(init.headers).toBeInstanceOf(Headers)
|
||||
})
|
||||
|
||||
it('preserves headers instanceof (Request / Request with Headers as init)', () => {
|
||||
recordRawFetchHeaders()
|
||||
const headers = new Headers([['X-My-Header', '1']])
|
||||
const init = new Request(url, { headers })
|
||||
new Request(init)
|
||||
expect(init.headers).toBeInstanceOf(Headers)
|
||||
})
|
||||
|
||||
it('preserves headers instanceof (Response / Response with Headers as init)', () => {
|
||||
recordRawFetchHeaders()
|
||||
const init = { headers: new Headers([['X-My-Header', '1']]) }
|
||||
new Response(url, init)
|
||||
expect(init.headers).toBeInstanceOf(Headers)
|
||||
})
|
||||
|
||||
it('records raw headers (Request / Request+Headers as init)', () => {
|
||||
recordRawFetchHeaders()
|
||||
const init = new Request(url, { headers: [['X-My-Header', '1']] })
|
||||
expect(getRawFetchHeaders(init.headers)).toEqual([['X-My-Header', '1']])
|
||||
|
||||
const request = new Request(init, {
|
||||
headers: new Headers([['X-Another-Header', '2']]),
|
||||
})
|
||||
|
||||
// Must merge the raw headers from the request init
|
||||
// and the request instance itself.
|
||||
expect(getRawFetchHeaders(request.headers)).toEqual([
|
||||
['X-My-Header', '1'],
|
||||
['X-Another-Header', '2'],
|
||||
])
|
||||
})
|
||||
|
||||
it('records raw headers (Response / object as init)', () => {
|
||||
recordRawFetchHeaders()
|
||||
const response = new Response(null, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-My-Header': '1',
|
||||
},
|
||||
})
|
||||
|
||||
expect(getRawFetchHeaders(response.headers)).toEqual([
|
||||
['Content-Type', 'application/json'],
|
||||
['X-My-Header', '1'],
|
||||
])
|
||||
})
|
||||
|
||||
it('records raw headers (Response / array as init)', () => {
|
||||
recordRawFetchHeaders()
|
||||
const response = new Response(null, {
|
||||
headers: [['X-My-Header', '1']],
|
||||
})
|
||||
|
||||
expect(getRawFetchHeaders(response.headers)).toEqual([['X-My-Header', '1']])
|
||||
})
|
||||
|
||||
it('records raw headers (Response / Headers as init)', () => {
|
||||
recordRawFetchHeaders()
|
||||
const headers = new Headers([['X-My-Header', '1']])
|
||||
const response = new Response(null, { headers })
|
||||
|
||||
expect(getRawFetchHeaders(response.headers)).toEqual([['X-My-Header', '1']])
|
||||
})
|
||||
|
||||
it('stops recording once the patches are restored', () => {
|
||||
restoreHeadersPrototype()
|
||||
|
||||
const headers = new Headers({ 'X-My-Header': '1' })
|
||||
// Must return the normalized headers (no access to raw headers).
|
||||
expect(getRawFetchHeaders(headers)).toEqual([['x-my-header', '1']])
|
||||
})
|
||||
|
||||
it('overrides an existing header when calling ".set()"', () => {
|
||||
recordRawFetchHeaders()
|
||||
const headers = new Headers([['a', '1']])
|
||||
expect(headers.get('a')).toBe('1')
|
||||
|
||||
headers.set('a', '2')
|
||||
expect(headers.get('a')).toBe('2')
|
||||
|
||||
const headersClone = new Headers(headers)
|
||||
expect(headersClone.get('a')).toBe('2')
|
||||
})
|
||||
|
||||
it('overrides an existing multi-value header when calling ".set()"', () => {
|
||||
recordRawFetchHeaders()
|
||||
const headers = new Headers([
|
||||
['a', '1'],
|
||||
['a', '2'],
|
||||
])
|
||||
expect(headers.get('a')).toBe('1, 2')
|
||||
|
||||
headers.set('a', '3')
|
||||
expect(headers.get('a')).toBe('3')
|
||||
})
|
||||
|
||||
it('does not throw on using Headers before recording', () => {
|
||||
// If the consumer constructs a Headers instance before
|
||||
// the interceptor is enabled, it will have no internal symbol set.
|
||||
const headers = new Headers()
|
||||
recordRawFetchHeaders()
|
||||
const request = new Request(url, { headers })
|
||||
|
||||
expect(getRawFetchHeaders(request.headers)).toEqual([])
|
||||
|
||||
request.headers.set('X-My-Header', '1')
|
||||
expect(getRawFetchHeaders(request.headers)).toEqual([['X-My-Header', '1']])
|
||||
})
|
||||
|
||||
/**
|
||||
* @see https://github.com/mswjs/interceptors/issues/681
|
||||
*/
|
||||
it('isolates headers between different headers instances', async () => {
|
||||
recordRawFetchHeaders()
|
||||
const original = new Headers()
|
||||
const firstClone = new Headers(original)
|
||||
firstClone.set('Content-Type', 'application/json')
|
||||
const secondClone = new Headers(original)
|
||||
|
||||
expect(original.get('Content-Type')).toBeNull()
|
||||
expect(firstClone.get('Content-Type')).toBe('application/json')
|
||||
expect(secondClone.get('Content-Type')).toBeNull()
|
||||
})
|
||||
262
node_modules/@mswjs/interceptors/src/interceptors/ClientRequest/utils/recordRawHeaders.ts
generated
vendored
Normal file
262
node_modules/@mswjs/interceptors/src/interceptors/ClientRequest/utils/recordRawHeaders.ts
generated
vendored
Normal file
@@ -0,0 +1,262 @@
|
||||
type HeaderTuple = [string, string]
|
||||
type RawHeaders = Array<HeaderTuple>
|
||||
type SetHeaderBehavior = 'set' | 'append'
|
||||
|
||||
const kRawHeaders = Symbol('kRawHeaders')
|
||||
const kRestorePatches = Symbol('kRestorePatches')
|
||||
|
||||
function recordRawHeader(
|
||||
headers: Headers,
|
||||
args: HeaderTuple,
|
||||
behavior: SetHeaderBehavior
|
||||
) {
|
||||
ensureRawHeadersSymbol(headers, [])
|
||||
const rawHeaders = Reflect.get(headers, kRawHeaders) as RawHeaders
|
||||
|
||||
if (behavior === 'set') {
|
||||
// When recording a set header, ensure we remove any matching existing headers.
|
||||
for (let index = rawHeaders.length - 1; index >= 0; index--) {
|
||||
if (rawHeaders[index][0].toLowerCase() === args[0].toLowerCase()) {
|
||||
rawHeaders.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rawHeaders.push(args)
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the raw headers symbol on the given `Headers` instance.
|
||||
* If the symbol already exists, this function does nothing.
|
||||
*/
|
||||
function ensureRawHeadersSymbol(
|
||||
headers: Headers,
|
||||
rawHeaders: RawHeaders
|
||||
): void {
|
||||
if (Reflect.has(headers, kRawHeaders)) {
|
||||
return
|
||||
}
|
||||
|
||||
defineRawHeadersSymbol(headers, rawHeaders)
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the raw headers symbol on the given `Headers` instance.
|
||||
* If the symbol already exists, it gets overridden.
|
||||
*/
|
||||
function defineRawHeadersSymbol(headers: Headers, rawHeaders: RawHeaders) {
|
||||
Object.defineProperty(headers, kRawHeaders, {
|
||||
value: rawHeaders,
|
||||
enumerable: false,
|
||||
// Mark the symbol as configurable so its value can be overridden.
|
||||
// Overrides happen when merging raw headers from multiple sources.
|
||||
// E.g. new Request(new Request(url, { headers }), { headers })
|
||||
configurable: true,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch the global `Headers` class to store raw headers.
|
||||
* This is for compatibility with `IncomingMessage.prototype.rawHeaders`.
|
||||
*
|
||||
* @note Node.js has their own raw headers symbol but it
|
||||
* only records the first header name in case of multi-value headers.
|
||||
* Any other headers are normalized before comparing. This makes it
|
||||
* incompatible with the `rawHeaders` format.
|
||||
*
|
||||
* let h = new Headers()
|
||||
* h.append('X-Custom', 'one')
|
||||
* h.append('x-custom', 'two')
|
||||
* h[Symbol('headers map')] // Map { 'X-Custom' => 'one, two' }
|
||||
*/
|
||||
export function recordRawFetchHeaders() {
|
||||
// Prevent patching the Headers prototype multiple times.
|
||||
if (Reflect.get(Headers, kRestorePatches)) {
|
||||
return Reflect.get(Headers, kRestorePatches)
|
||||
}
|
||||
|
||||
const {
|
||||
Headers: OriginalHeaders,
|
||||
Request: OriginalRequest,
|
||||
Response: OriginalResponse,
|
||||
} = globalThis
|
||||
const { set, append, delete: headersDeleteMethod } = Headers.prototype
|
||||
|
||||
Object.defineProperty(Headers, kRestorePatches, {
|
||||
value: () => {
|
||||
Headers.prototype.set = set
|
||||
Headers.prototype.append = append
|
||||
Headers.prototype.delete = headersDeleteMethod
|
||||
globalThis.Headers = OriginalHeaders
|
||||
|
||||
globalThis.Request = OriginalRequest
|
||||
globalThis.Response = OriginalResponse
|
||||
|
||||
Reflect.deleteProperty(Headers, kRestorePatches)
|
||||
},
|
||||
enumerable: false,
|
||||
/**
|
||||
* @note Mark this property as configurable
|
||||
* so we can delete it using `Reflect.delete` during cleanup.
|
||||
*/
|
||||
configurable: true,
|
||||
})
|
||||
|
||||
Object.defineProperty(globalThis, 'Headers', {
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
value: new Proxy(Headers, {
|
||||
construct(target, args, newTarget) {
|
||||
const headersInit = args[0] || []
|
||||
|
||||
if (
|
||||
headersInit instanceof Headers &&
|
||||
Reflect.has(headersInit, kRawHeaders)
|
||||
) {
|
||||
const headers = Reflect.construct(
|
||||
target,
|
||||
[Reflect.get(headersInit, kRawHeaders)],
|
||||
newTarget
|
||||
)
|
||||
ensureRawHeadersSymbol(headers, [
|
||||
/**
|
||||
* @note Spread the retrieved headers to clone them.
|
||||
* This prevents multiple Headers instances from pointing
|
||||
* at the same internal "rawHeaders" array.
|
||||
*/
|
||||
...Reflect.get(headersInit, kRawHeaders),
|
||||
])
|
||||
return headers
|
||||
}
|
||||
|
||||
const headers = Reflect.construct(target, args, newTarget)
|
||||
|
||||
// Request/Response constructors will set the symbol
|
||||
// upon creating a new instance, using the raw developer
|
||||
// input as the raw headers. Skip the symbol altogether
|
||||
// in those cases because the input to Headers will be normalized.
|
||||
if (!Reflect.has(headers, kRawHeaders)) {
|
||||
const rawHeadersInit = Array.isArray(headersInit)
|
||||
? headersInit
|
||||
: Object.entries(headersInit)
|
||||
ensureRawHeadersSymbol(headers, rawHeadersInit)
|
||||
}
|
||||
|
||||
return headers
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
Headers.prototype.set = new Proxy(Headers.prototype.set, {
|
||||
apply(target, thisArg, args: HeaderTuple) {
|
||||
recordRawHeader(thisArg, args, 'set')
|
||||
return Reflect.apply(target, thisArg, args)
|
||||
},
|
||||
})
|
||||
|
||||
Headers.prototype.append = new Proxy(Headers.prototype.append, {
|
||||
apply(target, thisArg, args: HeaderTuple) {
|
||||
recordRawHeader(thisArg, args, 'append')
|
||||
return Reflect.apply(target, thisArg, args)
|
||||
},
|
||||
})
|
||||
|
||||
Headers.prototype.delete = new Proxy(Headers.prototype.delete, {
|
||||
apply(target, thisArg, args: [string]) {
|
||||
const rawHeaders = Reflect.get(thisArg, kRawHeaders) as RawHeaders
|
||||
|
||||
if (rawHeaders) {
|
||||
for (let index = rawHeaders.length - 1; index >= 0; index--) {
|
||||
if (rawHeaders[index][0].toLowerCase() === args[0].toLowerCase()) {
|
||||
rawHeaders.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Reflect.apply(target, thisArg, args)
|
||||
},
|
||||
})
|
||||
|
||||
Object.defineProperty(globalThis, 'Request', {
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
value: new Proxy(Request, {
|
||||
construct(target, args, newTarget) {
|
||||
const request = Reflect.construct(target, args, newTarget)
|
||||
const inferredRawHeaders: RawHeaders = []
|
||||
|
||||
// Infer raw headers from a `Request` instance used as init.
|
||||
if (typeof args[0] === 'object' && args[0].headers != null) {
|
||||
inferredRawHeaders.push(...inferRawHeaders(args[0].headers))
|
||||
}
|
||||
|
||||
// Infer raw headers from the "headers" init argument.
|
||||
if (typeof args[1] === 'object' && args[1].headers != null) {
|
||||
inferredRawHeaders.push(...inferRawHeaders(args[1].headers))
|
||||
}
|
||||
|
||||
if (inferredRawHeaders.length > 0) {
|
||||
ensureRawHeadersSymbol(request.headers, inferredRawHeaders)
|
||||
}
|
||||
|
||||
return request
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
Object.defineProperty(globalThis, 'Response', {
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
value: new Proxy(Response, {
|
||||
construct(target, args, newTarget) {
|
||||
const response = Reflect.construct(target, args, newTarget)
|
||||
|
||||
if (typeof args[1] === 'object' && args[1].headers != null) {
|
||||
ensureRawHeadersSymbol(
|
||||
response.headers,
|
||||
inferRawHeaders(args[1].headers)
|
||||
)
|
||||
}
|
||||
|
||||
return response
|
||||
},
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
export function restoreHeadersPrototype() {
|
||||
if (!Reflect.get(Headers, kRestorePatches)) {
|
||||
return
|
||||
}
|
||||
|
||||
Reflect.get(Headers, kRestorePatches)()
|
||||
}
|
||||
|
||||
export function getRawFetchHeaders(headers: Headers): RawHeaders {
|
||||
// If the raw headers recording failed for some reason,
|
||||
// use the normalized header entries instead.
|
||||
if (!Reflect.has(headers, kRawHeaders)) {
|
||||
return Array.from(headers.entries())
|
||||
}
|
||||
|
||||
const rawHeaders = Reflect.get(headers, kRawHeaders) as RawHeaders
|
||||
return rawHeaders.length > 0 ? rawHeaders : Array.from(headers.entries())
|
||||
}
|
||||
|
||||
/**
|
||||
* Infers the raw headers from the given `HeadersInit` provided
|
||||
* to the Request/Response constructor.
|
||||
*
|
||||
* If the `init.headers` is a Headers instance, use it directly.
|
||||
* That means the headers were created standalone and already have
|
||||
* the raw headers stored.
|
||||
* If the `init.headers` is a HeadersInit, create a new Headers
|
||||
* instance out of it.
|
||||
*/
|
||||
function inferRawHeaders(headers: HeadersInit): RawHeaders {
|
||||
if (headers instanceof Headers) {
|
||||
return Reflect.get(headers, kRawHeaders) || []
|
||||
}
|
||||
|
||||
return Reflect.get(new Headers(headers), kRawHeaders)
|
||||
}
|
||||
264
node_modules/@mswjs/interceptors/src/interceptors/Socket/MockSocket.test.ts
generated
vendored
Normal file
264
node_modules/@mswjs/interceptors/src/interceptors/Socket/MockSocket.test.ts
generated
vendored
Normal file
@@ -0,0 +1,264 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { Socket } from 'node:net'
|
||||
import { vi, it, expect } from 'vitest'
|
||||
import { MockSocket } from './MockSocket'
|
||||
|
||||
it(`keeps the socket connecting until it's destroyed`, () => {
|
||||
const socket = new MockSocket({
|
||||
write: vi.fn(),
|
||||
read: vi.fn(),
|
||||
})
|
||||
|
||||
expect(socket.connecting).toBe(true)
|
||||
|
||||
socket.destroy()
|
||||
expect(socket.connecting).toBe(false)
|
||||
})
|
||||
|
||||
it('calls the "write" on "socket.write()"', () => {
|
||||
const writeCallback = vi.fn()
|
||||
const socket = new MockSocket({
|
||||
write: writeCallback,
|
||||
read: vi.fn(),
|
||||
})
|
||||
|
||||
socket.write()
|
||||
expect(writeCallback).toHaveBeenCalledWith(undefined, undefined, undefined)
|
||||
})
|
||||
|
||||
it('calls the "write" on "socket.write(chunk)"', () => {
|
||||
const writeCallback = vi.fn()
|
||||
const socket = new MockSocket({
|
||||
write: writeCallback,
|
||||
read: vi.fn(),
|
||||
})
|
||||
|
||||
socket.write('hello')
|
||||
expect(writeCallback).toHaveBeenCalledWith('hello', undefined, undefined)
|
||||
})
|
||||
|
||||
it('calls the "write" on "socket.write(chunk, encoding)"', () => {
|
||||
const writeCallback = vi.fn()
|
||||
const socket = new MockSocket({
|
||||
write: writeCallback,
|
||||
read: vi.fn(),
|
||||
})
|
||||
|
||||
socket.write('hello', 'utf8')
|
||||
expect(writeCallback).toHaveBeenCalledWith('hello', 'utf8', undefined)
|
||||
})
|
||||
|
||||
it('calls the "write" on "socket.write(chunk, encoding, callback)"', () => {
|
||||
const writeCallback = vi.fn()
|
||||
const socket = new MockSocket({
|
||||
write: writeCallback,
|
||||
read: vi.fn(),
|
||||
})
|
||||
|
||||
const callback = vi.fn()
|
||||
socket.write('hello', 'utf8', callback)
|
||||
expect(writeCallback).toHaveBeenCalledWith('hello', 'utf8', callback)
|
||||
})
|
||||
|
||||
it('calls the "write" on "socket.end()"', () => {
|
||||
const writeCallback = vi.fn()
|
||||
const socket = new MockSocket({
|
||||
write: writeCallback,
|
||||
read: vi.fn(),
|
||||
})
|
||||
|
||||
socket.end()
|
||||
expect(writeCallback).toHaveBeenCalledWith(undefined, undefined, undefined)
|
||||
})
|
||||
|
||||
it('calls the "write" on "socket.end(chunk)"', () => {
|
||||
const writeCallback = vi.fn()
|
||||
const socket = new MockSocket({
|
||||
write: writeCallback,
|
||||
read: vi.fn(),
|
||||
})
|
||||
|
||||
socket.end('final')
|
||||
expect(writeCallback).toHaveBeenCalledWith('final', undefined, undefined)
|
||||
})
|
||||
|
||||
it('calls the "write" on "socket.end(chunk, encoding)"', () => {
|
||||
const writeCallback = vi.fn()
|
||||
const socket = new MockSocket({
|
||||
write: writeCallback,
|
||||
read: vi.fn(),
|
||||
})
|
||||
|
||||
socket.end('final', 'utf8')
|
||||
expect(writeCallback).toHaveBeenCalledWith('final', 'utf8', undefined)
|
||||
})
|
||||
|
||||
it('calls the "write" on "socket.end(chunk, encoding, callback)"', () => {
|
||||
const writeCallback = vi.fn()
|
||||
const socket = new MockSocket({
|
||||
write: writeCallback,
|
||||
read: vi.fn(),
|
||||
})
|
||||
|
||||
const callback = vi.fn()
|
||||
socket.end('final', 'utf8', callback)
|
||||
expect(writeCallback).toHaveBeenCalledWith('final', 'utf8', callback)
|
||||
})
|
||||
|
||||
it('calls the "write" on "socket.end()" without any arguments', () => {
|
||||
const writeCallback = vi.fn()
|
||||
const socket = new MockSocket({
|
||||
write: writeCallback,
|
||||
read: vi.fn(),
|
||||
})
|
||||
|
||||
socket.end()
|
||||
expect(writeCallback).toHaveBeenCalledWith(undefined, undefined, undefined)
|
||||
})
|
||||
|
||||
it('emits "finished" on .end() without any arguments', async () => {
|
||||
const finishListener = vi.fn()
|
||||
const socket = new MockSocket({
|
||||
write: vi.fn(),
|
||||
read: vi.fn(),
|
||||
})
|
||||
socket.on('finish', finishListener)
|
||||
socket.end()
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(finishListener).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
it('calls the "read" on "socket.read(chunk)"', () => {
|
||||
const readCallback = vi.fn()
|
||||
const socket = new MockSocket({
|
||||
write: vi.fn(),
|
||||
read: readCallback,
|
||||
})
|
||||
|
||||
socket.push('hello')
|
||||
expect(readCallback).toHaveBeenCalledWith('hello', undefined)
|
||||
})
|
||||
|
||||
it('calls the "read" on "socket.read(chunk, encoding)"', () => {
|
||||
const readCallback = vi.fn()
|
||||
const socket = new MockSocket({
|
||||
write: vi.fn(),
|
||||
read: readCallback,
|
||||
})
|
||||
|
||||
socket.push('world', 'utf8')
|
||||
expect(readCallback).toHaveBeenCalledWith('world', 'utf8')
|
||||
})
|
||||
|
||||
it('calls the "read" on "socket.read(null)"', () => {
|
||||
const readCallback = vi.fn()
|
||||
const socket = new MockSocket({
|
||||
write: vi.fn(),
|
||||
read: readCallback,
|
||||
})
|
||||
|
||||
socket.push(null)
|
||||
expect(readCallback).toHaveBeenCalledWith(null, undefined)
|
||||
})
|
||||
|
||||
it('updates the writable state on "socket.end()"', async () => {
|
||||
const finishListener = vi.fn()
|
||||
const endListener = vi.fn()
|
||||
const socket = new MockSocket({
|
||||
write: vi.fn(),
|
||||
read: vi.fn(),
|
||||
})
|
||||
socket.on('finish', finishListener)
|
||||
socket.on('end', endListener)
|
||||
|
||||
expect(socket.writable).toBe(true)
|
||||
expect(socket.writableEnded).toBe(false)
|
||||
expect(socket.writableFinished).toBe(false)
|
||||
|
||||
socket.write('hello')
|
||||
// Finish the writable stream.
|
||||
socket.end()
|
||||
|
||||
expect(socket.writable).toBe(false)
|
||||
expect(socket.writableEnded).toBe(true)
|
||||
|
||||
// The "finish" event is emitted when writable is done.
|
||||
// I.e. "socket.end()" is called.
|
||||
await vi.waitFor(() => {
|
||||
expect(finishListener).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
expect(socket.writableFinished).toBe(true)
|
||||
})
|
||||
|
||||
it('updates the readable state on "socket.push(null)"', async () => {
|
||||
const endListener = vi.fn()
|
||||
const socket = new MockSocket({
|
||||
write: vi.fn(),
|
||||
read: vi.fn(),
|
||||
})
|
||||
socket.on('end', endListener)
|
||||
|
||||
expect(socket.readable).toBe(true)
|
||||
expect(socket.readableEnded).toBe(false)
|
||||
|
||||
socket.push('hello')
|
||||
socket.push(null)
|
||||
|
||||
expect(socket.readable).toBe(true)
|
||||
expect(socket.readableEnded).toBe(false)
|
||||
|
||||
// Read the data to free the buffer and
|
||||
// make Socket emit "end".
|
||||
socket.read()
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(endListener).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
expect(socket.readable).toBe(false)
|
||||
expect(socket.readableEnded).toBe(true)
|
||||
})
|
||||
|
||||
it('updates the readable/writable state on "socket.destroy()"', async () => {
|
||||
const finishListener = vi.fn()
|
||||
const endListener = vi.fn()
|
||||
const closeListener = vi.fn()
|
||||
const socket = new MockSocket({
|
||||
write: vi.fn(),
|
||||
read: vi.fn(),
|
||||
})
|
||||
socket.on('finish', finishListener)
|
||||
socket.on('end', endListener)
|
||||
socket.on('close', closeListener)
|
||||
|
||||
expect(socket.writable).toBe(true)
|
||||
expect(socket.writableEnded).toBe(false)
|
||||
expect(socket.writableFinished).toBe(false)
|
||||
expect(socket.readable).toBe(true)
|
||||
|
||||
socket.destroy()
|
||||
|
||||
expect(socket.writable).toBe(false)
|
||||
// The ".end()" wasn't called.
|
||||
expect(socket.writableEnded).toBe(false)
|
||||
expect(socket.writableFinished).toBe(false)
|
||||
expect(socket.readable).toBe(false)
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(closeListener).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
// Neither "finish" nor "end" events are emitted
|
||||
// when you destroy the stream. If you want those,
|
||||
// call ".end()", then destroy the stream.
|
||||
expect(finishListener).not.toHaveBeenCalled()
|
||||
expect(endListener).not.toHaveBeenCalled()
|
||||
expect(socket.writableFinished).toBe(false)
|
||||
|
||||
// The "end" event was never emitted so "readableEnded"
|
||||
// remains false.
|
||||
expect(socket.readableEnded).toBe(false)
|
||||
})
|
||||
58
node_modules/@mswjs/interceptors/src/interceptors/Socket/MockSocket.ts
generated
vendored
Normal file
58
node_modules/@mswjs/interceptors/src/interceptors/Socket/MockSocket.ts
generated
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
import net from 'node:net'
|
||||
import {
|
||||
normalizeSocketWriteArgs,
|
||||
type WriteArgs,
|
||||
type WriteCallback,
|
||||
} from './utils/normalizeSocketWriteArgs'
|
||||
|
||||
export interface MockSocketOptions {
|
||||
write: (
|
||||
chunk: Buffer | string,
|
||||
encoding: BufferEncoding | undefined,
|
||||
callback?: WriteCallback
|
||||
) => void
|
||||
|
||||
read: (chunk: Buffer, encoding: BufferEncoding | undefined) => void
|
||||
}
|
||||
|
||||
export class MockSocket extends net.Socket {
|
||||
public connecting: boolean
|
||||
|
||||
constructor(protected readonly options: MockSocketOptions) {
|
||||
super()
|
||||
this.connecting = false
|
||||
this.connect()
|
||||
|
||||
this._final = (callback) => {
|
||||
callback(null)
|
||||
}
|
||||
}
|
||||
|
||||
public connect() {
|
||||
// The connection will remain pending until
|
||||
// the consumer decides to handle it.
|
||||
this.connecting = true
|
||||
return this
|
||||
}
|
||||
|
||||
public write(...args: Array<unknown>): boolean {
|
||||
const [chunk, encoding, callback] = normalizeSocketWriteArgs(
|
||||
args as WriteArgs
|
||||
)
|
||||
this.options.write(chunk, encoding, callback)
|
||||
return true
|
||||
}
|
||||
|
||||
public end(...args: Array<unknown>) {
|
||||
const [chunk, encoding, callback] = normalizeSocketWriteArgs(
|
||||
args as WriteArgs
|
||||
)
|
||||
this.options.write(chunk, encoding, callback)
|
||||
return super.end.apply(this, args as any)
|
||||
}
|
||||
|
||||
public push(chunk: any, encoding?: BufferEncoding): boolean {
|
||||
this.options.read(chunk, encoding)
|
||||
return super.push(chunk, encoding)
|
||||
}
|
||||
}
|
||||
26
node_modules/@mswjs/interceptors/src/interceptors/Socket/utils/baseUrlFromConnectionOptions.ts
generated
vendored
Normal file
26
node_modules/@mswjs/interceptors/src/interceptors/Socket/utils/baseUrlFromConnectionOptions.ts
generated
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
export function baseUrlFromConnectionOptions(options: any): URL {
|
||||
if ('href' in options) {
|
||||
return new URL(options.href)
|
||||
}
|
||||
|
||||
const protocol = options.port === 443 ? 'https:' : 'http:'
|
||||
const host = options.host
|
||||
|
||||
const url = new URL(`${protocol}//${host}`)
|
||||
|
||||
if (options.port) {
|
||||
url.port = options.port.toString()
|
||||
}
|
||||
|
||||
if (options.path) {
|
||||
url.pathname = options.path
|
||||
}
|
||||
|
||||
if (options.auth) {
|
||||
const [username, password] = options.auth.split(':')
|
||||
url.username = username
|
||||
url.password = password
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
52
node_modules/@mswjs/interceptors/src/interceptors/Socket/utils/normalizeSocketWriteArgs.test.ts
generated
vendored
Normal file
52
node_modules/@mswjs/interceptors/src/interceptors/Socket/utils/normalizeSocketWriteArgs.test.ts
generated
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { it, expect } from 'vitest'
|
||||
import { normalizeSocketWriteArgs } from './normalizeSocketWriteArgs'
|
||||
|
||||
it('normalizes .write()', () => {
|
||||
expect(normalizeSocketWriteArgs([undefined])).toEqual([
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
])
|
||||
expect(normalizeSocketWriteArgs([null])).toEqual([null, undefined, undefined])
|
||||
})
|
||||
|
||||
it('normalizes .write(chunk)', () => {
|
||||
expect(normalizeSocketWriteArgs([Buffer.from('hello')])).toEqual([
|
||||
Buffer.from('hello'),
|
||||
undefined,
|
||||
undefined,
|
||||
])
|
||||
expect(normalizeSocketWriteArgs(['hello'])).toEqual([
|
||||
'hello',
|
||||
undefined,
|
||||
undefined,
|
||||
])
|
||||
expect(normalizeSocketWriteArgs([null])).toEqual([null, undefined, undefined])
|
||||
})
|
||||
|
||||
it('normalizes .write(chunk, encoding)', () => {
|
||||
expect(normalizeSocketWriteArgs([Buffer.from('hello'), 'utf8'])).toEqual([
|
||||
Buffer.from('hello'),
|
||||
'utf8',
|
||||
undefined,
|
||||
])
|
||||
})
|
||||
|
||||
it('normalizes .write(chunk, callback)', () => {
|
||||
const callback = () => {}
|
||||
expect(normalizeSocketWriteArgs([Buffer.from('hello'), callback])).toEqual([
|
||||
Buffer.from('hello'),
|
||||
undefined,
|
||||
callback,
|
||||
])
|
||||
})
|
||||
|
||||
it('normalizes .write(chunk, encoding, callback)', () => {
|
||||
const callback = () => {}
|
||||
expect(
|
||||
normalizeSocketWriteArgs([Buffer.from('hello'), 'utf8', callback])
|
||||
).toEqual([Buffer.from('hello'), 'utf8', callback])
|
||||
})
|
||||
33
node_modules/@mswjs/interceptors/src/interceptors/Socket/utils/normalizeSocketWriteArgs.ts
generated
vendored
Normal file
33
node_modules/@mswjs/interceptors/src/interceptors/Socket/utils/normalizeSocketWriteArgs.ts
generated
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
export type WriteCallback = (error?: Error | null) => void
|
||||
|
||||
export type WriteArgs =
|
||||
| [chunk: unknown, callback?: WriteCallback]
|
||||
| [chunk: unknown, encoding: BufferEncoding, callback?: WriteCallback]
|
||||
|
||||
export type NormalizedSocketWriteArgs = [
|
||||
chunk: any,
|
||||
encoding?: BufferEncoding,
|
||||
callback?: WriteCallback,
|
||||
]
|
||||
|
||||
/**
|
||||
* Normalizes the arguments provided to the `Writable.prototype.write()`
|
||||
* and `Writable.prototype.end()`.
|
||||
*/
|
||||
export function normalizeSocketWriteArgs(
|
||||
args: WriteArgs
|
||||
): NormalizedSocketWriteArgs {
|
||||
const normalized: NormalizedSocketWriteArgs = [args[0], undefined, undefined]
|
||||
|
||||
if (typeof args[1] === 'string') {
|
||||
normalized[1] = args[1]
|
||||
} else if (typeof args[1] === 'function') {
|
||||
normalized[2] = args[1]
|
||||
}
|
||||
|
||||
if (typeof args[2] === 'function') {
|
||||
normalized[2] = args[2]
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
116
node_modules/@mswjs/interceptors/src/interceptors/WebSocket/WebSocketClassTransport.ts
generated
vendored
Normal file
116
node_modules/@mswjs/interceptors/src/interceptors/WebSocket/WebSocketClassTransport.ts
generated
vendored
Normal file
@@ -0,0 +1,116 @@
|
||||
import { bindEvent } from './utils/bindEvent'
|
||||
import {
|
||||
StrictEventListenerOrEventListenerObject,
|
||||
WebSocketData,
|
||||
WebSocketTransport,
|
||||
WebSocketTransportEventMap,
|
||||
} from './WebSocketTransport'
|
||||
import { kOnSend, kClose, WebSocketOverride } from './WebSocketOverride'
|
||||
import { CancelableMessageEvent, CloseEvent } from './utils/events'
|
||||
|
||||
/**
|
||||
* Abstraction over the given mock `WebSocket` instance that allows
|
||||
* for controlling that instance (e.g. sending and receiving messages).
|
||||
*/
|
||||
export class WebSocketClassTransport
|
||||
extends EventTarget
|
||||
implements WebSocketTransport
|
||||
{
|
||||
constructor(protected readonly socket: WebSocketOverride) {
|
||||
super()
|
||||
|
||||
// Emit the "close" event on the transport if the close
|
||||
// originates from the WebSocket client. E.g. the application
|
||||
// calls "ws.close()", not the interceptor.
|
||||
this.socket.addEventListener('close', (event) => {
|
||||
this.dispatchEvent(bindEvent(this.socket, new CloseEvent('close', event)))
|
||||
})
|
||||
|
||||
/**
|
||||
* Emit the "outgoing" event on the transport
|
||||
* whenever the WebSocket client sends data ("ws.send()").
|
||||
*/
|
||||
this.socket[kOnSend] = (data) => {
|
||||
this.dispatchEvent(
|
||||
bindEvent(
|
||||
this.socket,
|
||||
// Dispatch this as cancelable because "client" connection
|
||||
// re-creates this message event (cannot dispatch the same event).
|
||||
new CancelableMessageEvent('outgoing', {
|
||||
data,
|
||||
origin: this.socket.url,
|
||||
cancelable: true,
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public addEventListener<EventType extends keyof WebSocketTransportEventMap>(
|
||||
type: EventType,
|
||||
callback: StrictEventListenerOrEventListenerObject<
|
||||
WebSocketTransportEventMap[EventType]
|
||||
> | null,
|
||||
options?: boolean | AddEventListenerOptions
|
||||
): void {
|
||||
return super.addEventListener(type, callback as EventListener, options)
|
||||
}
|
||||
|
||||
public dispatchEvent<EventType extends keyof WebSocketTransportEventMap>(
|
||||
event: WebSocketTransportEventMap[EventType]
|
||||
): boolean {
|
||||
return super.dispatchEvent(event)
|
||||
}
|
||||
|
||||
public send(data: WebSocketData): void {
|
||||
queueMicrotask(() => {
|
||||
if (
|
||||
this.socket.readyState === this.socket.CLOSING ||
|
||||
this.socket.readyState === this.socket.CLOSED
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const dispatchEvent = () => {
|
||||
this.socket.dispatchEvent(
|
||||
bindEvent(
|
||||
/**
|
||||
* @note Setting this event's "target" to the
|
||||
* WebSocket override instance is important.
|
||||
* This way it can tell apart original incoming events
|
||||
* (must be forwarded to the transport) from the
|
||||
* mocked message events like the one below
|
||||
* (must be dispatched on the client instance).
|
||||
*/
|
||||
this.socket,
|
||||
new MessageEvent('message', {
|
||||
data,
|
||||
origin: this.socket.url,
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (this.socket.readyState === this.socket.CONNECTING) {
|
||||
this.socket.addEventListener(
|
||||
'open',
|
||||
() => {
|
||||
dispatchEvent()
|
||||
},
|
||||
{ once: true }
|
||||
)
|
||||
} else {
|
||||
dispatchEvent()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public close(code: number, reason?: string): void {
|
||||
/**
|
||||
* @note Call the internal close method directly
|
||||
* to allow closing the connection with the status codes
|
||||
* that are non-configurable by the user (> 1000 <= 1015).
|
||||
*/
|
||||
this.socket[kClose](code, reason)
|
||||
}
|
||||
}
|
||||
152
node_modules/@mswjs/interceptors/src/interceptors/WebSocket/WebSocketClientConnection.ts
generated
vendored
Normal file
152
node_modules/@mswjs/interceptors/src/interceptors/WebSocket/WebSocketClientConnection.ts
generated
vendored
Normal file
@@ -0,0 +1,152 @@
|
||||
import type { WebSocketData, WebSocketTransport } from './WebSocketTransport'
|
||||
import type { WebSocketEventListener } from './WebSocketOverride'
|
||||
import { bindEvent } from './utils/bindEvent'
|
||||
import { CancelableMessageEvent, CloseEvent } from './utils/events'
|
||||
import { createRequestId } from '../../createRequestId'
|
||||
|
||||
const kEmitter = Symbol('kEmitter')
|
||||
const kBoundListener = Symbol('kBoundListener')
|
||||
|
||||
export interface WebSocketClientEventMap {
|
||||
message: MessageEvent<WebSocketData>
|
||||
close: CloseEvent
|
||||
}
|
||||
|
||||
export abstract class WebSocketClientConnectionProtocol {
|
||||
abstract id: string
|
||||
abstract url: URL
|
||||
public abstract send(data: WebSocketData): void
|
||||
public abstract close(code?: number, reason?: string): void
|
||||
|
||||
public abstract addEventListener<
|
||||
EventType extends keyof WebSocketClientEventMap,
|
||||
>(
|
||||
type: EventType,
|
||||
listener: WebSocketEventListener<WebSocketClientEventMap[EventType]>,
|
||||
options?: AddEventListenerOptions | boolean
|
||||
): void
|
||||
|
||||
public abstract removeEventListener<
|
||||
EventType extends keyof WebSocketClientEventMap,
|
||||
>(
|
||||
event: EventType,
|
||||
listener: WebSocketEventListener<WebSocketClientEventMap[EventType]>,
|
||||
options?: EventListenerOptions | boolean
|
||||
): void
|
||||
}
|
||||
|
||||
/**
|
||||
* The WebSocket client instance represents an incoming
|
||||
* client connection. The user can control the connection,
|
||||
* send and receive events.
|
||||
*/
|
||||
export class WebSocketClientConnection implements WebSocketClientConnectionProtocol {
|
||||
public readonly id: string
|
||||
public readonly url: URL
|
||||
|
||||
private [kEmitter]: EventTarget
|
||||
|
||||
constructor(
|
||||
public readonly socket: WebSocket,
|
||||
private readonly transport: WebSocketTransport
|
||||
) {
|
||||
this.id = createRequestId()
|
||||
this.url = new URL(socket.url)
|
||||
this[kEmitter] = new EventTarget()
|
||||
|
||||
// Emit outgoing client data ("ws.send()") as "message"
|
||||
// events on the "client" connection.
|
||||
this.transport.addEventListener('outgoing', (event) => {
|
||||
const message = bindEvent(
|
||||
this.socket,
|
||||
new CancelableMessageEvent('message', {
|
||||
data: event.data,
|
||||
origin: event.origin,
|
||||
cancelable: true,
|
||||
})
|
||||
)
|
||||
|
||||
this[kEmitter].dispatchEvent(message)
|
||||
|
||||
// This is a bit silly but forward the cancellation state
|
||||
// of the "client" message event to the "outgoing" transport event.
|
||||
// This way, other agens (like "server" connection) can know
|
||||
// whether the client listener has pervented the default.
|
||||
if (message.defaultPrevented) {
|
||||
event.preventDefault()
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Emit the "close" event on the "client" connection
|
||||
* whenever the underlying transport is closed.
|
||||
* @note "client.close()" does NOT dispatch the "close"
|
||||
* event on the WebSocket because it uses non-configurable
|
||||
* close status code. Thus, we listen to the transport
|
||||
* instead of the WebSocket's "close" event.
|
||||
*/
|
||||
this.transport.addEventListener('close', (event) => {
|
||||
this[kEmitter].dispatchEvent(
|
||||
bindEvent(this.socket, new CloseEvent('close', event))
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for the outgoing events from the connected WebSocket client.
|
||||
*/
|
||||
public addEventListener<EventType extends keyof WebSocketClientEventMap>(
|
||||
type: EventType,
|
||||
listener: WebSocketEventListener<WebSocketClientEventMap[EventType]>,
|
||||
options?: AddEventListenerOptions | boolean
|
||||
): void {
|
||||
if (!Reflect.has(listener, kBoundListener)) {
|
||||
const boundListener = listener.bind(this.socket)
|
||||
|
||||
// Store the bound listener on the original listener
|
||||
// so the exact bound function can be accessed in "removeEventListener()".
|
||||
Object.defineProperty(listener, kBoundListener, {
|
||||
value: boundListener,
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
})
|
||||
}
|
||||
|
||||
this[kEmitter].addEventListener(
|
||||
type,
|
||||
Reflect.get(listener, kBoundListener) as EventListener,
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the listener for the given event.
|
||||
*/
|
||||
public removeEventListener<EventType extends keyof WebSocketClientEventMap>(
|
||||
event: EventType,
|
||||
listener: WebSocketEventListener<WebSocketClientEventMap[EventType]>,
|
||||
options?: EventListenerOptions | boolean
|
||||
): void {
|
||||
this[kEmitter].removeEventListener(
|
||||
event,
|
||||
Reflect.get(listener, kBoundListener) as EventListener,
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send data to the connected client.
|
||||
*/
|
||||
public send(data: WebSocketData): void {
|
||||
this.transport.send(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the WebSocket connection.
|
||||
* @param {number} code A status code (see https://www.rfc-editor.org/rfc/rfc6455#section-7.4.1).
|
||||
* @param {string} reason A custom connection close reason.
|
||||
*/
|
||||
public close(code?: number, reason?: string): void {
|
||||
this.transport.close(code, reason)
|
||||
}
|
||||
}
|
||||
252
node_modules/@mswjs/interceptors/src/interceptors/WebSocket/WebSocketOverride.ts
generated
vendored
Normal file
252
node_modules/@mswjs/interceptors/src/interceptors/WebSocket/WebSocketOverride.ts
generated
vendored
Normal file
@@ -0,0 +1,252 @@
|
||||
import { invariant } from 'outvariant'
|
||||
import { DeferredPromise } from '@open-draft/deferred-promise'
|
||||
import type { WebSocketData } from './WebSocketTransport'
|
||||
import { bindEvent } from './utils/bindEvent'
|
||||
import { CloseEvent } from './utils/events'
|
||||
import { resolveWebSocketUrl } from '../../utils/resolveWebSocketUrl'
|
||||
|
||||
export type WebSocketEventListener<
|
||||
EventType extends WebSocketEventMap[keyof WebSocketEventMap] = Event,
|
||||
> = (this: WebSocket, event: EventType) => void
|
||||
|
||||
const WEBSOCKET_CLOSE_CODE_RANGE_ERROR =
|
||||
'InvalidAccessError: close code out of user configurable range'
|
||||
|
||||
export const kPassthroughPromise = Symbol('kPassthroughPromise')
|
||||
export const kOnSend = Symbol('kOnSend')
|
||||
export const kClose = Symbol('kClose')
|
||||
|
||||
export class WebSocketOverride extends EventTarget implements WebSocket {
|
||||
static readonly CONNECTING = 0
|
||||
static readonly OPEN = 1
|
||||
static readonly CLOSING = 2
|
||||
static readonly CLOSED = 3
|
||||
readonly CONNECTING = 0
|
||||
readonly OPEN = 1
|
||||
readonly CLOSING = 2
|
||||
readonly CLOSED = 3
|
||||
|
||||
public url: string
|
||||
public protocol: string
|
||||
public extensions: string
|
||||
public binaryType: BinaryType
|
||||
public readyState: WebSocket['readyState']
|
||||
public bufferedAmount: number
|
||||
|
||||
private _onopen: WebSocketEventListener | null = null
|
||||
private _onmessage: WebSocketEventListener<
|
||||
MessageEvent<WebSocketData>
|
||||
> | null = null
|
||||
private _onerror: WebSocketEventListener | null = null
|
||||
private _onclose: WebSocketEventListener<CloseEvent> | null = null
|
||||
|
||||
private [kPassthroughPromise]: DeferredPromise<boolean>
|
||||
private [kOnSend]?: (data: WebSocketData) => void
|
||||
|
||||
constructor(url: string | URL, protocols?: string | Array<string>) {
|
||||
super()
|
||||
this.url = resolveWebSocketUrl(url)
|
||||
this.protocol = ''
|
||||
this.extensions = ''
|
||||
this.binaryType = 'blob'
|
||||
this.readyState = this.CONNECTING
|
||||
this.bufferedAmount = 0
|
||||
|
||||
this[kPassthroughPromise] = new DeferredPromise<boolean>()
|
||||
|
||||
queueMicrotask(async () => {
|
||||
if (await this[kPassthroughPromise]) {
|
||||
return
|
||||
}
|
||||
|
||||
this.protocol =
|
||||
typeof protocols === 'string'
|
||||
? protocols
|
||||
: Array.isArray(protocols) && protocols.length > 0
|
||||
? protocols[0]
|
||||
: ''
|
||||
|
||||
/**
|
||||
* @note Check that nothing has prevented this connection
|
||||
* (e.g. called `client.close()` in the connection listener).
|
||||
* If the connection has been prevented, never dispatch the open event,.
|
||||
*/
|
||||
if (this.readyState === this.CONNECTING) {
|
||||
this.readyState = this.OPEN
|
||||
this.dispatchEvent(bindEvent(this, new Event('open')))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
set onopen(listener: WebSocketEventListener | null) {
|
||||
this.removeEventListener('open', this._onopen)
|
||||
this._onopen = listener
|
||||
if (listener !== null) {
|
||||
this.addEventListener('open', listener)
|
||||
}
|
||||
}
|
||||
get onopen(): WebSocketEventListener | null {
|
||||
return this._onopen
|
||||
}
|
||||
|
||||
set onmessage(
|
||||
listener: WebSocketEventListener<MessageEvent<WebSocketData>> | null
|
||||
) {
|
||||
this.removeEventListener(
|
||||
'message',
|
||||
this._onmessage as WebSocketEventListener
|
||||
)
|
||||
this._onmessage = listener
|
||||
if (listener !== null) {
|
||||
this.addEventListener('message', listener)
|
||||
}
|
||||
}
|
||||
get onmessage(): WebSocketEventListener<MessageEvent<WebSocketData>> | null {
|
||||
return this._onmessage
|
||||
}
|
||||
|
||||
set onerror(listener: WebSocketEventListener | null) {
|
||||
this.removeEventListener('error', this._onerror)
|
||||
this._onerror = listener
|
||||
if (listener !== null) {
|
||||
this.addEventListener('error', listener)
|
||||
}
|
||||
}
|
||||
get onerror(): WebSocketEventListener | null {
|
||||
return this._onerror
|
||||
}
|
||||
|
||||
set onclose(listener: WebSocketEventListener<CloseEvent> | null) {
|
||||
this.removeEventListener('close', this._onclose as WebSocketEventListener)
|
||||
this._onclose = listener
|
||||
if (listener !== null) {
|
||||
this.addEventListener('close', listener)
|
||||
}
|
||||
}
|
||||
get onclose(): WebSocketEventListener<CloseEvent> | null {
|
||||
return this._onclose
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://websockets.spec.whatwg.org/#ref-for-dom-websocket-send%E2%91%A0
|
||||
*/
|
||||
public send(data: WebSocketData): void {
|
||||
if (this.readyState === this.CONNECTING) {
|
||||
this.close()
|
||||
throw new DOMException('InvalidStateError')
|
||||
}
|
||||
|
||||
// Sending when the socket is about to close
|
||||
// discards the sent data.
|
||||
if (this.readyState === this.CLOSING || this.readyState === this.CLOSED) {
|
||||
return
|
||||
}
|
||||
|
||||
// Buffer the data to send in this even loop
|
||||
// but send it in the next.
|
||||
this.bufferedAmount += getDataSize(data)
|
||||
|
||||
queueMicrotask(() => {
|
||||
// This is a bit optimistic but since no actual data transfer
|
||||
// is involved, all the data will be "sent" on the next tick.
|
||||
this.bufferedAmount = 0
|
||||
|
||||
/**
|
||||
* @note Notify the parent about outgoing data.
|
||||
* This notifies the transport and the connection
|
||||
* listens to the outgoing data to emit the "message" event.
|
||||
*/
|
||||
this[kOnSend]?.(data)
|
||||
})
|
||||
}
|
||||
|
||||
public close(code: number = 1000, reason?: string): void {
|
||||
invariant(code, WEBSOCKET_CLOSE_CODE_RANGE_ERROR)
|
||||
invariant(
|
||||
code === 1000 || (code >= 3000 && code <= 4999),
|
||||
WEBSOCKET_CLOSE_CODE_RANGE_ERROR
|
||||
)
|
||||
|
||||
this[kClose](code, reason)
|
||||
}
|
||||
|
||||
private [kClose](
|
||||
code: number = 1000,
|
||||
reason?: string,
|
||||
wasClean = true
|
||||
): void {
|
||||
/**
|
||||
* @note Move this check here so that even internal closures,
|
||||
* like those triggered by the `server` connection, are not
|
||||
* performed twice.
|
||||
*/
|
||||
if (this.readyState === this.CLOSING || this.readyState === this.CLOSED) {
|
||||
return
|
||||
}
|
||||
|
||||
this.readyState = this.CLOSING
|
||||
|
||||
queueMicrotask(() => {
|
||||
this.readyState = this.CLOSED
|
||||
|
||||
this.dispatchEvent(
|
||||
bindEvent(
|
||||
this,
|
||||
new CloseEvent('close', {
|
||||
code,
|
||||
reason,
|
||||
wasClean,
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
// Remove all event listeners once the socket is closed.
|
||||
this._onopen = null
|
||||
this._onmessage = null
|
||||
this._onerror = null
|
||||
this._onclose = null
|
||||
})
|
||||
}
|
||||
|
||||
public addEventListener<K extends keyof WebSocketEventMap>(
|
||||
type: K,
|
||||
listener: (this: WebSocket, event: WebSocketEventMap[K]) => void,
|
||||
options?: boolean | AddEventListenerOptions
|
||||
): void
|
||||
public addEventListener(
|
||||
type: string,
|
||||
listener: EventListenerOrEventListenerObject,
|
||||
options?: boolean | AddEventListenerOptions
|
||||
): void
|
||||
public addEventListener(
|
||||
type: unknown,
|
||||
listener: unknown,
|
||||
options?: unknown
|
||||
): void {
|
||||
return super.addEventListener(
|
||||
type as string,
|
||||
listener as EventListener,
|
||||
options as AddEventListenerOptions
|
||||
)
|
||||
}
|
||||
|
||||
removeEventListener<K extends keyof WebSocketEventMap>(
|
||||
type: K,
|
||||
callback: EventListenerOrEventListenerObject | null,
|
||||
options?: boolean | EventListenerOptions
|
||||
): void {
|
||||
return super.removeEventListener(type, callback, options)
|
||||
}
|
||||
}
|
||||
|
||||
function getDataSize(data: WebSocketData): number {
|
||||
if (typeof data === 'string') {
|
||||
return data.length
|
||||
}
|
||||
|
||||
if (data instanceof Blob) {
|
||||
return data.size
|
||||
}
|
||||
|
||||
return data.byteLength
|
||||
}
|
||||
420
node_modules/@mswjs/interceptors/src/interceptors/WebSocket/WebSocketServerConnection.ts
generated
vendored
Normal file
420
node_modules/@mswjs/interceptors/src/interceptors/WebSocket/WebSocketServerConnection.ts
generated
vendored
Normal file
@@ -0,0 +1,420 @@
|
||||
import { invariant } from 'outvariant'
|
||||
import {
|
||||
kClose,
|
||||
WebSocketEventListener,
|
||||
WebSocketOverride,
|
||||
} from './WebSocketOverride'
|
||||
import type { WebSocketData } from './WebSocketTransport'
|
||||
import type { WebSocketClassTransport } from './WebSocketClassTransport'
|
||||
import { bindEvent } from './utils/bindEvent'
|
||||
import {
|
||||
CancelableMessageEvent,
|
||||
CancelableCloseEvent,
|
||||
CloseEvent,
|
||||
} from './utils/events'
|
||||
|
||||
const kEmitter = Symbol('kEmitter')
|
||||
const kBoundListener = Symbol('kBoundListener')
|
||||
const kSend = Symbol('kSend')
|
||||
|
||||
export interface WebSocketServerEventMap {
|
||||
open: Event
|
||||
message: MessageEvent<WebSocketData>
|
||||
error: Event
|
||||
close: CloseEvent
|
||||
}
|
||||
|
||||
export abstract class WebSocketServerConnectionProtocol {
|
||||
public abstract connect(): void
|
||||
public abstract send(data: WebSocketData): void
|
||||
public abstract close(): void
|
||||
|
||||
public abstract addEventListener<
|
||||
EventType extends keyof WebSocketServerEventMap,
|
||||
>(
|
||||
event: EventType,
|
||||
listener: WebSocketEventListener<WebSocketServerEventMap[EventType]>,
|
||||
options?: AddEventListenerOptions | boolean
|
||||
): void
|
||||
|
||||
public abstract removeEventListener<
|
||||
EventType extends keyof WebSocketServerEventMap,
|
||||
>(
|
||||
event: EventType,
|
||||
listener: WebSocketEventListener<WebSocketServerEventMap[EventType]>,
|
||||
options?: EventListenerOptions | boolean
|
||||
): void
|
||||
}
|
||||
|
||||
/**
|
||||
* The WebSocket server instance represents the actual production
|
||||
* WebSocket server connection. It's idle by default but you can
|
||||
* establish it by calling `server.connect()`.
|
||||
*/
|
||||
export class WebSocketServerConnection implements WebSocketServerConnectionProtocol {
|
||||
/**
|
||||
* A WebSocket instance connected to the original server.
|
||||
*/
|
||||
private realWebSocket?: WebSocket
|
||||
private mockCloseController: AbortController
|
||||
private realCloseController: AbortController
|
||||
private [kEmitter]: EventTarget
|
||||
|
||||
constructor(
|
||||
private readonly client: WebSocketOverride,
|
||||
private readonly transport: WebSocketClassTransport,
|
||||
private readonly createConnection: () => WebSocket
|
||||
) {
|
||||
this[kEmitter] = new EventTarget()
|
||||
this.mockCloseController = new AbortController()
|
||||
this.realCloseController = new AbortController()
|
||||
|
||||
// Automatically forward outgoing client events
|
||||
// to the actual server unless the outgoing message event
|
||||
// has been prevented. The "outgoing" transport event it
|
||||
// dispatched by the "client" connection.
|
||||
this.transport.addEventListener('outgoing', (event) => {
|
||||
// Ignore client messages if the server connection
|
||||
// hasn't been established yet. Nowhere to forward.
|
||||
if (typeof this.realWebSocket === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
// Every outgoing client message can prevent this forwarding
|
||||
// by preventing the default of the outgoing message event.
|
||||
// This listener will be added before user-defined listeners,
|
||||
// so execute the logic on the next tick.
|
||||
queueMicrotask(() => {
|
||||
if (!event.defaultPrevented) {
|
||||
/**
|
||||
* @note Use the internal send mechanism so consumers can tell
|
||||
* apart direct user calls to `server.send()` and internal calls.
|
||||
* E.g. MSW has to ignore this internal call to log out messages correctly.
|
||||
*/
|
||||
this[kSend](event.data)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.transport.addEventListener(
|
||||
'incoming',
|
||||
this.handleIncomingMessage.bind(this)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The `WebSocket` instance connected to the original server.
|
||||
* Accessing this before calling `server.connect()` will throw.
|
||||
*/
|
||||
public get socket(): WebSocket {
|
||||
invariant(
|
||||
this.realWebSocket,
|
||||
'Cannot access "socket" on the original WebSocket server object: the connection is not open. Did you forget to call `server.connect()`?'
|
||||
)
|
||||
|
||||
return this.realWebSocket
|
||||
}
|
||||
|
||||
/**
|
||||
* Open connection to the original WebSocket server.
|
||||
*/
|
||||
public connect(): void {
|
||||
invariant(
|
||||
!this.realWebSocket || this.realWebSocket.readyState !== WebSocket.OPEN,
|
||||
'Failed to call "connect()" on the original WebSocket instance: the connection already open'
|
||||
)
|
||||
|
||||
const realWebSocket = this.createConnection()
|
||||
|
||||
// Inherit the binary type from the mock WebSocket client.
|
||||
realWebSocket.binaryType = this.client.binaryType
|
||||
|
||||
// Allow the interceptor to listen to when the server connection
|
||||
// has been established. This isn't necessary to operate with the connection
|
||||
// but may be beneficial in some cases (like conditionally adding logging).
|
||||
realWebSocket.addEventListener(
|
||||
'open',
|
||||
(event) => {
|
||||
this[kEmitter].dispatchEvent(
|
||||
bindEvent(this.realWebSocket!, new Event('open', event))
|
||||
)
|
||||
},
|
||||
{ once: true }
|
||||
)
|
||||
|
||||
realWebSocket.addEventListener('message', (event) => {
|
||||
// Dispatch the "incoming" transport event instead of
|
||||
// invoking the internal handler directly. This way,
|
||||
// anyone can listen to the "incoming" event but this
|
||||
// class is the one resulting in it.
|
||||
this.transport.dispatchEvent(
|
||||
bindEvent(
|
||||
this.realWebSocket!,
|
||||
new MessageEvent('incoming', {
|
||||
data: event.data,
|
||||
origin: event.origin,
|
||||
})
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
// Close the original connection when the mock client closes.
|
||||
// E.g. "client.close()" was called. This is never forwarded anywhere.
|
||||
this.client.addEventListener(
|
||||
'close',
|
||||
(event) => {
|
||||
this.handleMockClose(event)
|
||||
},
|
||||
{
|
||||
signal: this.mockCloseController.signal,
|
||||
}
|
||||
)
|
||||
|
||||
// Forward the "close" event to let the interceptor handle
|
||||
// closures initiated by the original server.
|
||||
realWebSocket.addEventListener(
|
||||
'close',
|
||||
(event) => {
|
||||
this.handleRealClose(event)
|
||||
},
|
||||
{
|
||||
signal: this.realCloseController.signal,
|
||||
}
|
||||
)
|
||||
|
||||
realWebSocket.addEventListener('error', () => {
|
||||
const errorEvent = bindEvent(
|
||||
realWebSocket,
|
||||
new Event('error', { cancelable: true })
|
||||
)
|
||||
|
||||
// Emit the "error" event on the `server` connection
|
||||
// to let the interceptor react to original server errors.
|
||||
this[kEmitter].dispatchEvent(errorEvent)
|
||||
|
||||
// If the error event from the original server hasn't been prevented,
|
||||
// forward it to the underlying client.
|
||||
if (!errorEvent.defaultPrevented) {
|
||||
this.client.dispatchEvent(bindEvent(this.client, new Event('error')))
|
||||
}
|
||||
})
|
||||
|
||||
this.realWebSocket = realWebSocket
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for the incoming events from the original WebSocket server.
|
||||
*/
|
||||
public addEventListener<EventType extends keyof WebSocketServerEventMap>(
|
||||
event: EventType,
|
||||
listener: WebSocketEventListener<WebSocketServerEventMap[EventType]>,
|
||||
options?: AddEventListenerOptions | boolean
|
||||
): void {
|
||||
if (!Reflect.has(listener, kBoundListener)) {
|
||||
const boundListener = listener.bind(this.client)
|
||||
|
||||
// Store the bound listener on the original listener
|
||||
// so the exact bound function can be accessed in "removeEventListener()".
|
||||
Object.defineProperty(listener, kBoundListener, {
|
||||
value: boundListener,
|
||||
enumerable: false,
|
||||
})
|
||||
}
|
||||
|
||||
this[kEmitter].addEventListener(
|
||||
event,
|
||||
Reflect.get(listener, kBoundListener) as EventListener,
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the listener for the given event.
|
||||
*/
|
||||
public removeEventListener<EventType extends keyof WebSocketServerEventMap>(
|
||||
event: EventType,
|
||||
listener: WebSocketEventListener<WebSocketServerEventMap[EventType]>,
|
||||
options?: EventListenerOptions | boolean
|
||||
): void {
|
||||
this[kEmitter].removeEventListener(
|
||||
event,
|
||||
Reflect.get(listener, kBoundListener) as EventListener,
|
||||
options
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send data to the original WebSocket server.
|
||||
* @example
|
||||
* server.send('hello')
|
||||
* server.send(new Blob(['hello']))
|
||||
* server.send(new TextEncoder().encode('hello'))
|
||||
*/
|
||||
public send(data: WebSocketData): void {
|
||||
this[kSend](data)
|
||||
}
|
||||
|
||||
private [kSend](data: WebSocketData): void {
|
||||
const { realWebSocket } = this
|
||||
|
||||
invariant(
|
||||
realWebSocket,
|
||||
'Failed to call "server.send()" for "%s": the connection is not open. Did you forget to call "server.connect()"?',
|
||||
this.client.url
|
||||
)
|
||||
|
||||
// Silently ignore writes on the closed original WebSocket.
|
||||
if (
|
||||
realWebSocket.readyState === WebSocket.CLOSING ||
|
||||
realWebSocket.readyState === WebSocket.CLOSED
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// Delegate the send to when the original connection is open.
|
||||
// Unlike the mock, connecting to the original server may take time
|
||||
// so we cannot call this on the next tick.
|
||||
if (realWebSocket.readyState === WebSocket.CONNECTING) {
|
||||
realWebSocket.addEventListener(
|
||||
'open',
|
||||
() => {
|
||||
realWebSocket.send(data)
|
||||
},
|
||||
{ once: true }
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Send the data to the original WebSocket server.
|
||||
realWebSocket.send(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the actual server connection.
|
||||
*/
|
||||
public close(): void {
|
||||
const { realWebSocket } = this
|
||||
|
||||
invariant(
|
||||
realWebSocket,
|
||||
'Failed to close server connection for "%s": the connection is not open. Did you forget to call "server.connect()"?',
|
||||
this.client.url
|
||||
)
|
||||
|
||||
// Remove the "close" event listener from the server
|
||||
// so it doesn't close the underlying WebSocket client
|
||||
// when you call "server.close()". This also prevents the
|
||||
// `close` event on the `server` connection from being dispatched twice.
|
||||
this.realCloseController.abort()
|
||||
|
||||
if (
|
||||
realWebSocket.readyState === WebSocket.CLOSING ||
|
||||
realWebSocket.readyState === WebSocket.CLOSED
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// Close the actual client connection.
|
||||
realWebSocket.close()
|
||||
|
||||
// Dispatch the "close" event on the `server` connection.
|
||||
queueMicrotask(() => {
|
||||
this[kEmitter].dispatchEvent(
|
||||
bindEvent(
|
||||
this.realWebSocket,
|
||||
new CancelableCloseEvent('close', {
|
||||
/**
|
||||
* @note `server.close()` in the interceptor
|
||||
* always results in clean closures.
|
||||
*/
|
||||
code: 1000,
|
||||
cancelable: true,
|
||||
})
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private handleIncomingMessage(event: MessageEvent<WebSocketData>): void {
|
||||
// Clone the event to dispatch it on this class
|
||||
// once again and prevent the "already being dispatched"
|
||||
// exception. Clone it here so we can observe this event
|
||||
// being prevented in the "server.on()" listeners.
|
||||
const messageEvent = bindEvent(
|
||||
event.target,
|
||||
new CancelableMessageEvent('message', {
|
||||
data: event.data,
|
||||
origin: event.origin,
|
||||
cancelable: true,
|
||||
})
|
||||
)
|
||||
|
||||
/**
|
||||
* @note Emit "message" event on the server connection
|
||||
* instance to let the interceptor know about these
|
||||
* incoming events from the original server. In that listener,
|
||||
* the interceptor can modify or skip the event forwarding
|
||||
* to the mock WebSocket instance.
|
||||
*/
|
||||
this[kEmitter].dispatchEvent(messageEvent)
|
||||
|
||||
/**
|
||||
* @note Forward the incoming server events to the client.
|
||||
* Preventing the default on the message event stops this.
|
||||
*/
|
||||
if (!messageEvent.defaultPrevented) {
|
||||
this.client.dispatchEvent(
|
||||
bindEvent(
|
||||
/**
|
||||
* @note Bind the forwarded original server events
|
||||
* to the mock WebSocket instance so it would
|
||||
* dispatch them straight away.
|
||||
*/
|
||||
this.client,
|
||||
// Clone the message event again to prevent
|
||||
// the "already being dispatched" exception.
|
||||
new MessageEvent('message', {
|
||||
data: event.data,
|
||||
origin: event.origin,
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private handleMockClose(_event: Event): void {
|
||||
// Close the original connection if the mock client closes.
|
||||
if (this.realWebSocket) {
|
||||
this.realWebSocket.close()
|
||||
}
|
||||
}
|
||||
|
||||
private handleRealClose(event: CloseEvent): void {
|
||||
// For closures originating from the original server,
|
||||
// remove the "close" listener from the mock client.
|
||||
// original close -> (?) client[kClose]() --X--> "close" (again).
|
||||
this.mockCloseController.abort()
|
||||
|
||||
const closeEvent = bindEvent(
|
||||
this.realWebSocket,
|
||||
new CancelableCloseEvent('close', {
|
||||
code: event.code,
|
||||
reason: event.reason,
|
||||
wasClean: event.wasClean,
|
||||
cancelable: true,
|
||||
})
|
||||
)
|
||||
|
||||
this[kEmitter].dispatchEvent(closeEvent)
|
||||
|
||||
// If the close event from the server hasn't been prevented,
|
||||
// forward the closure to the mock client.
|
||||
if (!closeEvent.defaultPrevented) {
|
||||
// Close the intercepted client forcefully to
|
||||
// allow non-configurable status codes from the server.
|
||||
// If the socket has been closed by now, no harm calling
|
||||
// this again—it will have no effect.
|
||||
this.client[kClose](event.code, event.reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
39
node_modules/@mswjs/interceptors/src/interceptors/WebSocket/WebSocketTransport.ts
generated
vendored
Normal file
39
node_modules/@mswjs/interceptors/src/interceptors/WebSocket/WebSocketTransport.ts
generated
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
import { CloseEvent } from './utils/events'
|
||||
|
||||
export type WebSocketData = string | ArrayBufferLike | Blob | ArrayBufferView
|
||||
|
||||
export type WebSocketTransportEventMap = {
|
||||
incoming: MessageEvent<WebSocketData>
|
||||
outgoing: MessageEvent<WebSocketData>
|
||||
close: CloseEvent
|
||||
}
|
||||
|
||||
export type StrictEventListenerOrEventListenerObject<EventType extends Event> =
|
||||
| ((this: WebSocket, event: EventType) => void)
|
||||
| {
|
||||
handleEvent(this: WebSocket, event: EventType): void
|
||||
}
|
||||
|
||||
export interface WebSocketTransport {
|
||||
addEventListener<EventType extends keyof WebSocketTransportEventMap>(
|
||||
event: EventType,
|
||||
listener: StrictEventListenerOrEventListenerObject<
|
||||
WebSocketTransportEventMap[EventType]
|
||||
> | null,
|
||||
options?: boolean | AddEventListenerOptions
|
||||
): void
|
||||
|
||||
dispatchEvent<EventType extends keyof WebSocketTransportEventMap>(
|
||||
event: WebSocketTransportEventMap[EventType]
|
||||
): boolean
|
||||
|
||||
/**
|
||||
* Send the data from the server to this client.
|
||||
*/
|
||||
send(data: WebSocketData): void
|
||||
|
||||
/**
|
||||
* Close the client connection.
|
||||
*/
|
||||
close(code?: number, reason?: string): void
|
||||
}
|
||||
191
node_modules/@mswjs/interceptors/src/interceptors/WebSocket/index.ts
generated
vendored
Normal file
191
node_modules/@mswjs/interceptors/src/interceptors/WebSocket/index.ts
generated
vendored
Normal file
@@ -0,0 +1,191 @@
|
||||
import { Interceptor } from '../../Interceptor'
|
||||
import {
|
||||
WebSocketClientConnectionProtocol,
|
||||
WebSocketClientConnection,
|
||||
type WebSocketClientEventMap,
|
||||
} from './WebSocketClientConnection'
|
||||
import {
|
||||
WebSocketServerConnectionProtocol,
|
||||
WebSocketServerConnection,
|
||||
type WebSocketServerEventMap,
|
||||
} from './WebSocketServerConnection'
|
||||
import { WebSocketClassTransport } from './WebSocketClassTransport'
|
||||
import {
|
||||
kClose,
|
||||
kPassthroughPromise,
|
||||
WebSocketOverride,
|
||||
} from './WebSocketOverride'
|
||||
import { bindEvent } from './utils/bindEvent'
|
||||
import { hasConfigurableGlobal } from '../../utils/hasConfigurableGlobal'
|
||||
import { emitAsync } from '../../utils/emitAsync'
|
||||
|
||||
export {
|
||||
type WebSocketData,
|
||||
type WebSocketTransport,
|
||||
} from './WebSocketTransport'
|
||||
export {
|
||||
WebSocketClientEventMap,
|
||||
WebSocketClientConnectionProtocol,
|
||||
WebSocketClientConnection,
|
||||
WebSocketServerEventMap,
|
||||
WebSocketServerConnectionProtocol,
|
||||
WebSocketServerConnection,
|
||||
}
|
||||
|
||||
export {
|
||||
CloseEvent,
|
||||
CancelableCloseEvent,
|
||||
CancelableMessageEvent,
|
||||
} from './utils/events'
|
||||
|
||||
export type WebSocketEventMap = {
|
||||
connection: [args: WebSocketConnectionData]
|
||||
}
|
||||
|
||||
export type WebSocketConnectionData = {
|
||||
/**
|
||||
* The incoming WebSocket client connection.
|
||||
*/
|
||||
client: WebSocketClientConnection
|
||||
|
||||
/**
|
||||
* The original WebSocket server connection.
|
||||
*/
|
||||
server: WebSocketServerConnection
|
||||
|
||||
/**
|
||||
* The connection information.
|
||||
*/
|
||||
info: {
|
||||
/**
|
||||
* The protocols supported by the WebSocket client.
|
||||
*/
|
||||
protocols: string | Array<string> | undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercept the outgoing WebSocket connections created using
|
||||
* the global `WebSocket` class.
|
||||
*/
|
||||
export class WebSocketInterceptor extends Interceptor<WebSocketEventMap> {
|
||||
static symbol = Symbol('websocket')
|
||||
|
||||
constructor() {
|
||||
super(WebSocketInterceptor.symbol)
|
||||
}
|
||||
|
||||
protected checkEnvironment(): boolean {
|
||||
return hasConfigurableGlobal('WebSocket')
|
||||
}
|
||||
|
||||
protected setup(): void {
|
||||
const originalWebSocketDescriptor = Object.getOwnPropertyDescriptor(
|
||||
globalThis,
|
||||
'WebSocket'
|
||||
)
|
||||
|
||||
const WebSocketProxy = new Proxy(globalThis.WebSocket, {
|
||||
construct: (
|
||||
target,
|
||||
args: ConstructorParameters<typeof globalThis.WebSocket>,
|
||||
newTarget
|
||||
) => {
|
||||
const [url, protocols] = args
|
||||
|
||||
const createConnection = (): WebSocket => {
|
||||
return Reflect.construct(target, args, newTarget)
|
||||
}
|
||||
|
||||
// All WebSocket instances are mocked and don't forward
|
||||
// any events to the original server (no connection established).
|
||||
// To forward the events, the user must use the "server.send()" API.
|
||||
const socket = new WebSocketOverride(url, protocols)
|
||||
const transport = new WebSocketClassTransport(socket)
|
||||
|
||||
// Emit the "connection" event to the interceptor on the next tick
|
||||
// so the client can modify WebSocket options, like "binaryType"
|
||||
// while the connection is already pending.
|
||||
queueMicrotask(async () => {
|
||||
try {
|
||||
const server = new WebSocketServerConnection(
|
||||
socket,
|
||||
transport,
|
||||
createConnection
|
||||
)
|
||||
|
||||
const hasConnectionListeners =
|
||||
this.emitter.listenerCount('connection') > 0
|
||||
|
||||
// The "globalThis.WebSocket" class stands for
|
||||
// the client-side connection. Assume it's established
|
||||
// as soon as the WebSocket instance is constructed.
|
||||
await emitAsync(this.emitter, 'connection', {
|
||||
client: new WebSocketClientConnection(socket, transport),
|
||||
server,
|
||||
info: {
|
||||
protocols,
|
||||
},
|
||||
})
|
||||
|
||||
if (hasConnectionListeners) {
|
||||
socket[kPassthroughPromise].resolve(false)
|
||||
} else {
|
||||
socket[kPassthroughPromise].resolve(true)
|
||||
|
||||
server.connect()
|
||||
|
||||
// Forward the "open" event from the original server
|
||||
// to the mock WebSocket client in the case of a passthrough connection.
|
||||
server.addEventListener('open', () => {
|
||||
socket.dispatchEvent(bindEvent(socket, new Event('open')))
|
||||
|
||||
// Forward the original connection protocol to the
|
||||
// mock WebSocket client.
|
||||
if (server['realWebSocket']) {
|
||||
socket.protocol = server['realWebSocket'].protocol
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
/**
|
||||
* @note Translate unhandled exceptions during the connection
|
||||
* handling (i.e. interceptor exceptions) as WebSocket connection
|
||||
* closures with error. This prevents from the exceptions occurring
|
||||
* in `queueMicrotask` from being process-wide and uncatchable.
|
||||
*/
|
||||
if (error instanceof Error) {
|
||||
socket.dispatchEvent(new Event('error'))
|
||||
|
||||
// No need to close the connection if it's already being closed.
|
||||
// E.g. the interceptor called `client.close()` and then threw an error.
|
||||
if (
|
||||
socket.readyState !== WebSocket.CLOSING &&
|
||||
socket.readyState !== WebSocket.CLOSED
|
||||
) {
|
||||
socket[kClose](1011, error.message, false)
|
||||
}
|
||||
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return socket
|
||||
},
|
||||
})
|
||||
|
||||
Object.defineProperty(globalThis, 'WebSocket', {
|
||||
value: WebSocketProxy,
|
||||
configurable: true,
|
||||
})
|
||||
|
||||
this.subscriptions.push(() => {
|
||||
Object.defineProperty(
|
||||
globalThis,
|
||||
'WebSocket',
|
||||
originalWebSocketDescriptor!
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
27
node_modules/@mswjs/interceptors/src/interceptors/WebSocket/utils/bindEvent.test.ts
generated
vendored
Normal file
27
node_modules/@mswjs/interceptors/src/interceptors/WebSocket/utils/bindEvent.test.ts
generated
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { it, expect } from 'vitest'
|
||||
import { bindEvent } from './bindEvent'
|
||||
|
||||
it('sets the "target" on the given event', () => {
|
||||
class Target {}
|
||||
const target = new Target()
|
||||
const event = new Event('open')
|
||||
bindEvent(target, event)
|
||||
|
||||
expect(event.type).toBe('open')
|
||||
expect(event.target).toEqual(target)
|
||||
})
|
||||
|
||||
it('overrides existing "target" on the given event', () => {
|
||||
class Target {}
|
||||
const oldTarget = new Target()
|
||||
const newTarget = new Target()
|
||||
const event = new Event('open')
|
||||
bindEvent(oldTarget, event)
|
||||
bindEvent(newTarget, event)
|
||||
|
||||
expect(event.type).toBe('open')
|
||||
expect(event.target).toEqual(newTarget)
|
||||
})
|
||||
21
node_modules/@mswjs/interceptors/src/interceptors/WebSocket/utils/bindEvent.ts
generated
vendored
Normal file
21
node_modules/@mswjs/interceptors/src/interceptors/WebSocket/utils/bindEvent.ts
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
type EventWithTarget<E extends Event, T> = E & { target: T }
|
||||
|
||||
export function bindEvent<E extends Event, T>(
|
||||
target: T,
|
||||
event: E
|
||||
): EventWithTarget<E, T> {
|
||||
Object.defineProperties(event, {
|
||||
target: {
|
||||
value: target,
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
},
|
||||
currentTarget: {
|
||||
value: target,
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
},
|
||||
})
|
||||
|
||||
return event as EventWithTarget<E, T>
|
||||
}
|
||||
101
node_modules/@mswjs/interceptors/src/interceptors/WebSocket/utils/events.test.ts
generated
vendored
Normal file
101
node_modules/@mswjs/interceptors/src/interceptors/WebSocket/utils/events.test.ts
generated
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { CancelableMessageEvent, CloseEvent } from './events'
|
||||
|
||||
describe(CancelableMessageEvent, () => {
|
||||
it('initiates with the right defaults', () => {
|
||||
const event = new CancelableMessageEvent('message', {
|
||||
data: 'hello',
|
||||
})
|
||||
|
||||
expect(event).toBeInstanceOf(MessageEvent)
|
||||
expect(event.type).toBe('message')
|
||||
expect(event.data).toBe('hello')
|
||||
expect(event.cancelable).toBe(false)
|
||||
expect(event.defaultPrevented).toBe(false)
|
||||
})
|
||||
|
||||
it('initiates a cancelable event', () => {
|
||||
const event = new CancelableMessageEvent('message', {
|
||||
data: 'hello',
|
||||
cancelable: true,
|
||||
})
|
||||
|
||||
expect(event).toBeInstanceOf(MessageEvent)
|
||||
expect(event.type).toBe('message')
|
||||
expect(event.data).toBe('hello')
|
||||
expect(event.cancelable).toBe(true)
|
||||
expect(event.defaultPrevented).toBe(false)
|
||||
})
|
||||
|
||||
it('cancels a cancelable event when calling "preventDefault()"', () => {
|
||||
const event = new CancelableMessageEvent('message', {
|
||||
data: 'hello',
|
||||
cancelable: true,
|
||||
})
|
||||
|
||||
expect(event.defaultPrevented).toBe(false)
|
||||
event.preventDefault()
|
||||
expect(event.defaultPrevented).toBe(true)
|
||||
})
|
||||
|
||||
it('does nothing when calling "preventDefault()" on a non-cancelable event', () => {
|
||||
const event = new CancelableMessageEvent('message', {
|
||||
data: 'hello',
|
||||
})
|
||||
|
||||
expect(event.defaultPrevented).toBe(false)
|
||||
event.preventDefault()
|
||||
expect(event.defaultPrevented).toBe(false)
|
||||
})
|
||||
|
||||
it('supports setting the "cancelable" value directly', () => {
|
||||
const event = new CancelableMessageEvent('message', {})
|
||||
/**
|
||||
* @note HappyDOM sets the "cancelable" and "preventDefault"
|
||||
* event properties directly. That's no-op as far as I know
|
||||
* but they do it and we have to account for that.
|
||||
*/
|
||||
event.cancelable = true
|
||||
expect(event.cancelable).toBe(true)
|
||||
})
|
||||
|
||||
it('supports setting the "defaultPrevented" value directly', () => {
|
||||
const event = new CancelableMessageEvent('message', {})
|
||||
/**
|
||||
* @note HappyDOM sets the "cancelable" and "preventDefault"
|
||||
* event properties directly. That's no-op as far as I know
|
||||
* but they do it and we have to account for that.
|
||||
*/
|
||||
event.defaultPrevented = true
|
||||
expect(event.defaultPrevented).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe(CloseEvent, () => {
|
||||
it('initiates with the right defaults', () => {
|
||||
const event = new CloseEvent('close')
|
||||
|
||||
expect(event).toBeInstanceOf(Event)
|
||||
expect(event.type).toBe('close')
|
||||
expect(event.code).toBe(0)
|
||||
expect(event.reason).toBe('')
|
||||
expect(event.wasClean).toBe(false)
|
||||
})
|
||||
|
||||
it('initiates with custom values', () => {
|
||||
const event = new CloseEvent('close', {
|
||||
code: 1003,
|
||||
reason: 'close reason',
|
||||
wasClean: true,
|
||||
})
|
||||
|
||||
expect(event).toBeInstanceOf(Event)
|
||||
expect(event.type).toBe('close')
|
||||
expect(event.code).toBe(1003)
|
||||
expect(event.reason).toBe('close reason')
|
||||
expect(event.wasClean).toBe(true)
|
||||
})
|
||||
})
|
||||
94
node_modules/@mswjs/interceptors/src/interceptors/WebSocket/utils/events.ts
generated
vendored
Normal file
94
node_modules/@mswjs/interceptors/src/interceptors/WebSocket/utils/events.ts
generated
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
const kCancelable = Symbol('kCancelable')
|
||||
const kDefaultPrevented = Symbol('kDefaultPrevented')
|
||||
|
||||
/**
|
||||
* A `MessageEvent` superset that supports event cancellation
|
||||
* in Node.js. It's rather non-intrusive so it can be safely
|
||||
* used in the browser as well.
|
||||
*
|
||||
* @see https://github.com/nodejs/node/issues/51767
|
||||
*/
|
||||
export class CancelableMessageEvent<T = any> extends MessageEvent<T> {
|
||||
[kCancelable]: boolean;
|
||||
[kDefaultPrevented]: boolean
|
||||
|
||||
constructor(type: string, init: MessageEventInit<T>) {
|
||||
super(type, init)
|
||||
this[kCancelable] = !!init.cancelable
|
||||
this[kDefaultPrevented] = false
|
||||
}
|
||||
|
||||
get cancelable() {
|
||||
return this[kCancelable]
|
||||
}
|
||||
|
||||
set cancelable(nextCancelable) {
|
||||
this[kCancelable] = nextCancelable
|
||||
}
|
||||
|
||||
get defaultPrevented() {
|
||||
return this[kDefaultPrevented]
|
||||
}
|
||||
|
||||
set defaultPrevented(nextDefaultPrevented) {
|
||||
this[kDefaultPrevented] = nextDefaultPrevented
|
||||
}
|
||||
|
||||
public preventDefault(): void {
|
||||
if (this.cancelable && !this[kDefaultPrevented]) {
|
||||
this[kDefaultPrevented] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface CloseEventInit extends EventInit {
|
||||
code?: number
|
||||
reason?: string
|
||||
wasClean?: boolean
|
||||
}
|
||||
|
||||
export class CloseEvent extends Event {
|
||||
public code: number
|
||||
public reason: string
|
||||
public wasClean: boolean
|
||||
|
||||
constructor(type: string, init: CloseEventInit = {}) {
|
||||
super(type, init)
|
||||
this.code = init.code === undefined ? 0 : init.code
|
||||
this.reason = init.reason === undefined ? '' : init.reason
|
||||
this.wasClean = init.wasClean === undefined ? false : init.wasClean
|
||||
}
|
||||
}
|
||||
|
||||
export class CancelableCloseEvent extends CloseEvent {
|
||||
[kCancelable]: boolean;
|
||||
[kDefaultPrevented]: boolean
|
||||
|
||||
constructor(type: string, init: CloseEventInit = {}) {
|
||||
super(type, init)
|
||||
this[kCancelable] = !!init.cancelable
|
||||
this[kDefaultPrevented] = false
|
||||
}
|
||||
|
||||
get cancelable() {
|
||||
return this[kCancelable]
|
||||
}
|
||||
|
||||
set cancelable(nextCancelable) {
|
||||
this[kCancelable] = nextCancelable
|
||||
}
|
||||
|
||||
get defaultPrevented() {
|
||||
return this[kDefaultPrevented]
|
||||
}
|
||||
|
||||
set defaultPrevented(nextDefaultPrevented) {
|
||||
this[kDefaultPrevented] = nextDefaultPrevented
|
||||
}
|
||||
|
||||
public preventDefault(): void {
|
||||
if (this.cancelable && !this[kDefaultPrevented]) {
|
||||
this[kDefaultPrevented] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
746
node_modules/@mswjs/interceptors/src/interceptors/XMLHttpRequest/XMLHttpRequestController.ts
generated
vendored
Normal file
746
node_modules/@mswjs/interceptors/src/interceptors/XMLHttpRequest/XMLHttpRequestController.ts
generated
vendored
Normal file
@@ -0,0 +1,746 @@
|
||||
import { invariant } from 'outvariant'
|
||||
import { isNodeProcess } from 'is-node-process'
|
||||
import type { Logger } from '@open-draft/logger'
|
||||
import { concatArrayBuffer } from './utils/concatArrayBuffer'
|
||||
import { createEvent } from './utils/createEvent'
|
||||
import {
|
||||
decodeBuffer,
|
||||
encodeBuffer,
|
||||
toArrayBuffer,
|
||||
} from '../../utils/bufferUtils'
|
||||
import { createProxy } from '../../utils/createProxy'
|
||||
import { isDomParserSupportedType } from './utils/isDomParserSupportedType'
|
||||
import { parseJson } from '../../utils/parseJson'
|
||||
import { createResponse } from './utils/createResponse'
|
||||
import { INTERNAL_REQUEST_ID_HEADER_NAME } from '../../Interceptor'
|
||||
import { createRequestId } from '../../createRequestId'
|
||||
import { getBodyByteLength } from './utils/getBodyByteLength'
|
||||
import { setRawRequest } from '../../getRawRequest'
|
||||
|
||||
const kIsRequestHandled = Symbol('kIsRequestHandled')
|
||||
const IS_NODE = isNodeProcess()
|
||||
const kFetchRequest = Symbol('kFetchRequest')
|
||||
|
||||
/**
|
||||
* An `XMLHttpRequest` instance controller that allows us
|
||||
* to handle any given request instance (e.g. responding to it).
|
||||
*/
|
||||
export class XMLHttpRequestController {
|
||||
public request: XMLHttpRequest
|
||||
public requestId: string
|
||||
public onRequest?: (
|
||||
this: XMLHttpRequestController,
|
||||
args: {
|
||||
request: Request
|
||||
requestId: string
|
||||
}
|
||||
) => Promise<void>
|
||||
public onResponse?: (
|
||||
this: XMLHttpRequestController,
|
||||
args: {
|
||||
response: Response
|
||||
isMockedResponse: boolean
|
||||
request: Request
|
||||
requestId: string
|
||||
}
|
||||
) => void;
|
||||
|
||||
[kIsRequestHandled]: boolean;
|
||||
[kFetchRequest]?: Request
|
||||
private method: string = 'GET'
|
||||
private url: URL = null as any
|
||||
private requestHeaders: Headers
|
||||
private responseBuffer: Uint8Array
|
||||
private events: Map<keyof XMLHttpRequestEventTargetEventMap, Array<Function>>
|
||||
private uploadEvents: Map<
|
||||
keyof XMLHttpRequestEventTargetEventMap,
|
||||
Array<Function>
|
||||
>
|
||||
|
||||
constructor(
|
||||
readonly initialRequest: XMLHttpRequest,
|
||||
public logger: Logger
|
||||
) {
|
||||
this[kIsRequestHandled] = false
|
||||
|
||||
this.events = new Map()
|
||||
this.uploadEvents = new Map()
|
||||
this.requestId = createRequestId()
|
||||
this.requestHeaders = new Headers()
|
||||
this.responseBuffer = new Uint8Array()
|
||||
|
||||
this.request = createProxy(initialRequest, {
|
||||
setProperty: ([propertyName, nextValue], invoke) => {
|
||||
switch (propertyName) {
|
||||
case 'ontimeout': {
|
||||
const eventName = propertyName.slice(
|
||||
2
|
||||
) as keyof XMLHttpRequestEventTargetEventMap
|
||||
|
||||
/**
|
||||
* @note Proxy callbacks to event listeners because JSDOM has trouble
|
||||
* translating these properties to callbacks. It seemed to be operating
|
||||
* on events exclusively.
|
||||
*/
|
||||
this.request.addEventListener(eventName, nextValue as any)
|
||||
|
||||
return invoke()
|
||||
}
|
||||
|
||||
default: {
|
||||
return invoke()
|
||||
}
|
||||
}
|
||||
},
|
||||
methodCall: ([methodName, args], invoke) => {
|
||||
switch (methodName) {
|
||||
case 'open': {
|
||||
const [method, url] = args as [string, string | undefined]
|
||||
|
||||
if (typeof url === 'undefined') {
|
||||
this.method = 'GET'
|
||||
this.url = toAbsoluteUrl(method)
|
||||
} else {
|
||||
this.method = method
|
||||
this.url = toAbsoluteUrl(url)
|
||||
}
|
||||
|
||||
this.logger = this.logger.extend(`${this.method} ${this.url.href}`)
|
||||
this.logger.info('open', this.method, this.url.href)
|
||||
|
||||
return invoke()
|
||||
}
|
||||
|
||||
case 'addEventListener': {
|
||||
const [eventName, listener] = args as [
|
||||
keyof XMLHttpRequestEventTargetEventMap,
|
||||
Function,
|
||||
]
|
||||
|
||||
this.registerEvent(eventName, listener)
|
||||
this.logger.info('addEventListener', eventName, listener)
|
||||
|
||||
return invoke()
|
||||
}
|
||||
|
||||
case 'setRequestHeader': {
|
||||
const [name, value] = args as [string, string]
|
||||
this.requestHeaders.set(name, value)
|
||||
|
||||
this.logger.info('setRequestHeader', name, value)
|
||||
|
||||
return invoke()
|
||||
}
|
||||
|
||||
case 'send': {
|
||||
const [body] = args as [
|
||||
body?: XMLHttpRequestBodyInit | Document | null,
|
||||
]
|
||||
|
||||
this.request.addEventListener('load', () => {
|
||||
if (typeof this.onResponse !== 'undefined') {
|
||||
// Create a Fetch API Response representation of whichever
|
||||
// response this XMLHttpRequest received. Note those may
|
||||
// be either a mocked and the original response.
|
||||
const fetchResponse = createResponse(
|
||||
this.request,
|
||||
/**
|
||||
* The `response` property is the right way to read
|
||||
* the ambiguous response body, as the request's "responseType" may differ.
|
||||
* @see https://xhr.spec.whatwg.org/#the-response-attribute
|
||||
*/
|
||||
this.request.response
|
||||
)
|
||||
|
||||
// Notify the consumer about the response.
|
||||
this.onResponse.call(this, {
|
||||
response: fetchResponse,
|
||||
isMockedResponse: this[kIsRequestHandled],
|
||||
request: fetchRequest,
|
||||
requestId: this.requestId!,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const requestBody =
|
||||
typeof body === 'string' ? encodeBuffer(body) : body
|
||||
|
||||
// Delegate request handling to the consumer.
|
||||
const fetchRequest = this.toFetchApiRequest(requestBody)
|
||||
this[kFetchRequest] = fetchRequest.clone()
|
||||
|
||||
/**
|
||||
* @note Start request handling on the next tick so that the user
|
||||
* could add event listeners for "loadend" before the interceptor fires it.
|
||||
*/
|
||||
queueMicrotask(() => {
|
||||
const onceRequestSettled =
|
||||
this.onRequest?.call(this, {
|
||||
request: fetchRequest,
|
||||
requestId: this.requestId!,
|
||||
}) || Promise.resolve()
|
||||
|
||||
onceRequestSettled.finally(() => {
|
||||
// If the consumer didn't handle the request (called `.respondWith()`) perform it as-is.
|
||||
if (!this[kIsRequestHandled]) {
|
||||
this.logger.info(
|
||||
'request callback settled but request has not been handled (readystate %d), performing as-is...',
|
||||
this.request.readyState
|
||||
)
|
||||
|
||||
/**
|
||||
* @note Set the intercepted request ID on the original request in Node.js
|
||||
* so that if it triggers any other interceptors, they don't attempt
|
||||
* to process it once again.
|
||||
*
|
||||
* For instance, XMLHttpRequest is often implemented via "http.ClientRequest"
|
||||
* and we don't want for both XHR and ClientRequest interceptors to
|
||||
* handle the same request at the same time (e.g. emit the "response" event twice).
|
||||
*/
|
||||
if (IS_NODE) {
|
||||
this.request.setRequestHeader(
|
||||
INTERNAL_REQUEST_ID_HEADER_NAME,
|
||||
this.requestId!
|
||||
)
|
||||
}
|
||||
|
||||
return invoke()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
return invoke()
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Proxy the `.upload` property to gather the event listeners/callbacks.
|
||||
*/
|
||||
define(
|
||||
this.request,
|
||||
'upload',
|
||||
createProxy(this.request.upload, {
|
||||
setProperty: ([propertyName, nextValue], invoke) => {
|
||||
switch (propertyName) {
|
||||
case 'onloadstart':
|
||||
case 'onprogress':
|
||||
case 'onaboart':
|
||||
case 'onerror':
|
||||
case 'onload':
|
||||
case 'ontimeout':
|
||||
case 'onloadend': {
|
||||
const eventName = propertyName.slice(
|
||||
2
|
||||
) as keyof XMLHttpRequestEventTargetEventMap
|
||||
|
||||
this.registerUploadEvent(eventName, nextValue as Function)
|
||||
}
|
||||
}
|
||||
|
||||
return invoke()
|
||||
},
|
||||
methodCall: ([methodName, args], invoke) => {
|
||||
switch (methodName) {
|
||||
case 'addEventListener': {
|
||||
const [eventName, listener] = args as [
|
||||
keyof XMLHttpRequestEventTargetEventMap,
|
||||
Function,
|
||||
]
|
||||
this.registerUploadEvent(eventName, listener)
|
||||
this.logger.info('upload.addEventListener', eventName, listener)
|
||||
|
||||
return invoke()
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
private registerEvent(
|
||||
eventName: keyof XMLHttpRequestEventTargetEventMap,
|
||||
listener: Function
|
||||
): void {
|
||||
const prevEvents = this.events.get(eventName) || []
|
||||
const nextEvents = prevEvents.concat(listener)
|
||||
this.events.set(eventName, nextEvents)
|
||||
|
||||
this.logger.info('registered event "%s"', eventName, listener)
|
||||
}
|
||||
|
||||
private registerUploadEvent(
|
||||
eventName: keyof XMLHttpRequestEventTargetEventMap,
|
||||
listener: Function
|
||||
): void {
|
||||
const prevEvents = this.uploadEvents.get(eventName) || []
|
||||
const nextEvents = prevEvents.concat(listener)
|
||||
this.uploadEvents.set(eventName, nextEvents)
|
||||
|
||||
this.logger.info('registered upload event "%s"', eventName, listener)
|
||||
}
|
||||
|
||||
/**
|
||||
* Responds to the current request with the given
|
||||
* Fetch API `Response` instance.
|
||||
*/
|
||||
public async respondWith(response: Response): Promise<void> {
|
||||
/**
|
||||
* @note Since `XMLHttpRequestController` delegates the handling of the responses
|
||||
* to the "load" event listener that doesn't distinguish between the mocked and original
|
||||
* responses, mark the request that had a mocked response with a corresponding symbol.
|
||||
*
|
||||
* Mark this request as having a mocked response immediately since
|
||||
* calculating request/response total body length is asynchronous.
|
||||
*/
|
||||
this[kIsRequestHandled] = true
|
||||
|
||||
/**
|
||||
* Dispatch request upload events for requests with a body.
|
||||
* @see https://github.com/mswjs/interceptors/issues/573
|
||||
*/
|
||||
if (this[kFetchRequest]) {
|
||||
const totalRequestBodyLength = await getBodyByteLength(
|
||||
this[kFetchRequest]
|
||||
)
|
||||
|
||||
this.trigger('loadstart', this.request.upload, {
|
||||
loaded: 0,
|
||||
total: totalRequestBodyLength,
|
||||
})
|
||||
this.trigger('progress', this.request.upload, {
|
||||
loaded: totalRequestBodyLength,
|
||||
total: totalRequestBodyLength,
|
||||
})
|
||||
this.trigger('load', this.request.upload, {
|
||||
loaded: totalRequestBodyLength,
|
||||
total: totalRequestBodyLength,
|
||||
})
|
||||
|
||||
this.trigger('loadend', this.request.upload, {
|
||||
loaded: totalRequestBodyLength,
|
||||
total: totalRequestBodyLength,
|
||||
})
|
||||
}
|
||||
|
||||
this.logger.info(
|
||||
'responding with a mocked response: %d %s',
|
||||
response.status,
|
||||
response.statusText
|
||||
)
|
||||
|
||||
define(this.request, 'status', response.status)
|
||||
define(this.request, 'statusText', response.statusText)
|
||||
define(this.request, 'responseURL', this.url.href)
|
||||
|
||||
this.request.getResponseHeader = new Proxy(this.request.getResponseHeader, {
|
||||
apply: (_, __, args: [name: string]) => {
|
||||
this.logger.info('getResponseHeader', args[0])
|
||||
|
||||
if (this.request.readyState < this.request.HEADERS_RECEIVED) {
|
||||
this.logger.info('headers not received yet, returning null')
|
||||
|
||||
// Headers not received yet, nothing to return.
|
||||
return null
|
||||
}
|
||||
|
||||
const headerValue = response.headers.get(args[0])
|
||||
this.logger.info(
|
||||
'resolved response header "%s" to',
|
||||
args[0],
|
||||
headerValue
|
||||
)
|
||||
|
||||
return headerValue
|
||||
},
|
||||
})
|
||||
|
||||
this.request.getAllResponseHeaders = new Proxy(
|
||||
this.request.getAllResponseHeaders,
|
||||
{
|
||||
apply: () => {
|
||||
this.logger.info('getAllResponseHeaders')
|
||||
|
||||
if (this.request.readyState < this.request.HEADERS_RECEIVED) {
|
||||
this.logger.info('headers not received yet, returning empty string')
|
||||
|
||||
// Headers not received yet, nothing to return.
|
||||
return ''
|
||||
}
|
||||
|
||||
const headersList = Array.from(response.headers.entries())
|
||||
const allHeaders = headersList
|
||||
.map(([headerName, headerValue]) => {
|
||||
return `${headerName}: ${headerValue}`
|
||||
})
|
||||
.join('\r\n')
|
||||
|
||||
this.logger.info('resolved all response headers to', allHeaders)
|
||||
|
||||
return allHeaders
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Update the response getters to resolve against the mocked response.
|
||||
Object.defineProperties(this.request, {
|
||||
response: {
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
get: () => this.response,
|
||||
},
|
||||
responseText: {
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
get: () => this.responseText,
|
||||
},
|
||||
responseXML: {
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
get: () => this.responseXML,
|
||||
},
|
||||
})
|
||||
|
||||
const totalResponseBodyLength = await getBodyByteLength(response.clone())
|
||||
|
||||
this.logger.info('calculated response body length', totalResponseBodyLength)
|
||||
|
||||
this.trigger('loadstart', this.request, {
|
||||
loaded: 0,
|
||||
total: totalResponseBodyLength,
|
||||
})
|
||||
|
||||
this.setReadyState(this.request.HEADERS_RECEIVED)
|
||||
this.setReadyState(this.request.LOADING)
|
||||
|
||||
const finalizeResponse = () => {
|
||||
this.logger.info('finalizing the mocked response...')
|
||||
|
||||
this.setReadyState(this.request.DONE)
|
||||
|
||||
this.trigger('load', this.request, {
|
||||
loaded: this.responseBuffer.byteLength,
|
||||
total: totalResponseBodyLength,
|
||||
})
|
||||
|
||||
this.trigger('loadend', this.request, {
|
||||
loaded: this.responseBuffer.byteLength,
|
||||
total: totalResponseBodyLength,
|
||||
})
|
||||
}
|
||||
|
||||
if (response.body) {
|
||||
this.logger.info('mocked response has body, streaming...')
|
||||
|
||||
const reader = response.body.getReader()
|
||||
|
||||
const readNextResponseBodyChunk = async () => {
|
||||
const { value, done } = await reader.read()
|
||||
|
||||
if (done) {
|
||||
this.logger.info('response body stream done!')
|
||||
finalizeResponse()
|
||||
return
|
||||
}
|
||||
|
||||
if (value) {
|
||||
this.logger.info('read response body chunk:', value)
|
||||
this.responseBuffer = concatArrayBuffer(this.responseBuffer, value)
|
||||
|
||||
this.trigger('progress', this.request, {
|
||||
loaded: this.responseBuffer.byteLength,
|
||||
total: totalResponseBodyLength,
|
||||
})
|
||||
}
|
||||
|
||||
readNextResponseBodyChunk()
|
||||
}
|
||||
|
||||
readNextResponseBodyChunk()
|
||||
} else {
|
||||
finalizeResponse()
|
||||
}
|
||||
}
|
||||
|
||||
private responseBufferToText(): string {
|
||||
return decodeBuffer(this.responseBuffer)
|
||||
}
|
||||
|
||||
get response(): unknown {
|
||||
this.logger.info(
|
||||
'getResponse (responseType: %s)',
|
||||
this.request.responseType
|
||||
)
|
||||
|
||||
if (this.request.readyState !== this.request.DONE) {
|
||||
return null
|
||||
}
|
||||
|
||||
switch (this.request.responseType) {
|
||||
case 'json': {
|
||||
const responseJson = parseJson(this.responseBufferToText())
|
||||
this.logger.info('resolved response JSON', responseJson)
|
||||
|
||||
return responseJson
|
||||
}
|
||||
|
||||
case 'arraybuffer': {
|
||||
const arrayBuffer = toArrayBuffer(this.responseBuffer)
|
||||
this.logger.info('resolved response ArrayBuffer', arrayBuffer)
|
||||
|
||||
return arrayBuffer
|
||||
}
|
||||
|
||||
case 'blob': {
|
||||
const mimeType =
|
||||
this.request.getResponseHeader('Content-Type') || 'text/plain'
|
||||
const responseBlob = new Blob([this.responseBufferToText()], {
|
||||
type: mimeType,
|
||||
})
|
||||
|
||||
this.logger.info(
|
||||
'resolved response Blob (mime type: %s)',
|
||||
responseBlob,
|
||||
mimeType
|
||||
)
|
||||
|
||||
return responseBlob
|
||||
}
|
||||
|
||||
default: {
|
||||
const responseText = this.responseBufferToText()
|
||||
this.logger.info(
|
||||
'resolving "%s" response type as text',
|
||||
this.request.responseType,
|
||||
responseText
|
||||
)
|
||||
|
||||
return responseText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get responseText(): string {
|
||||
/**
|
||||
* Throw when trying to read the response body as text when the
|
||||
* "responseType" doesn't expect text. This just respects the spec better.
|
||||
* @see https://xhr.spec.whatwg.org/#the-responsetext-attribute
|
||||
*/
|
||||
invariant(
|
||||
this.request.responseType === '' || this.request.responseType === 'text',
|
||||
'InvalidStateError: The object is in invalid state.'
|
||||
)
|
||||
|
||||
if (
|
||||
this.request.readyState !== this.request.LOADING &&
|
||||
this.request.readyState !== this.request.DONE
|
||||
) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const responseText = this.responseBufferToText()
|
||||
this.logger.info('getResponseText: "%s"', responseText)
|
||||
|
||||
return responseText
|
||||
}
|
||||
|
||||
get responseXML(): Document | null {
|
||||
invariant(
|
||||
this.request.responseType === '' ||
|
||||
this.request.responseType === 'document',
|
||||
'InvalidStateError: The object is in invalid state.'
|
||||
)
|
||||
|
||||
if (this.request.readyState !== this.request.DONE) {
|
||||
return null
|
||||
}
|
||||
|
||||
const contentType = this.request.getResponseHeader('Content-Type') || ''
|
||||
|
||||
if (typeof DOMParser === 'undefined') {
|
||||
console.warn(
|
||||
'Cannot retrieve XMLHttpRequest response body as XML: DOMParser is not defined. You are likely using an environment that is not browser or does not polyfill browser globals correctly.'
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
if (isDomParserSupportedType(contentType)) {
|
||||
return new DOMParser().parseFromString(
|
||||
this.responseBufferToText(),
|
||||
contentType
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
public errorWith(error?: Error): void {
|
||||
/**
|
||||
* @note Mark this request as handled even if it received a mock error.
|
||||
* This prevents the controller from trying to perform this request as-is.
|
||||
*/
|
||||
this[kIsRequestHandled] = true
|
||||
this.logger.info('responding with an error')
|
||||
|
||||
this.setReadyState(this.request.DONE)
|
||||
this.trigger('error', this.request)
|
||||
this.trigger('loadend', this.request)
|
||||
}
|
||||
|
||||
/**
|
||||
* Transitions this request's `readyState` to the given one.
|
||||
*/
|
||||
private setReadyState(nextReadyState: number): void {
|
||||
this.logger.info(
|
||||
'setReadyState: %d -> %d',
|
||||
this.request.readyState,
|
||||
nextReadyState
|
||||
)
|
||||
|
||||
if (this.request.readyState === nextReadyState) {
|
||||
this.logger.info('ready state identical, skipping transition...')
|
||||
return
|
||||
}
|
||||
|
||||
define(this.request, 'readyState', nextReadyState)
|
||||
|
||||
this.logger.info('set readyState to: %d', nextReadyState)
|
||||
|
||||
if (nextReadyState !== this.request.UNSENT) {
|
||||
this.logger.info('triggering "readystatechange" event...')
|
||||
|
||||
this.trigger('readystatechange', this.request)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers given event on the `XMLHttpRequest` instance.
|
||||
*/
|
||||
private trigger<
|
||||
EventName extends keyof (XMLHttpRequestEventTargetEventMap & {
|
||||
readystatechange: ProgressEvent<XMLHttpRequestEventTarget>
|
||||
}),
|
||||
>(
|
||||
eventName: EventName,
|
||||
target: XMLHttpRequest | XMLHttpRequestUpload,
|
||||
options?: ProgressEventInit
|
||||
): void {
|
||||
const callback = (target as XMLHttpRequest)[`on${eventName}`]
|
||||
const event = createEvent(target, eventName, options)
|
||||
|
||||
this.logger.info('trigger "%s"', eventName, options || '')
|
||||
|
||||
// Invoke direct callbacks.
|
||||
if (typeof callback === 'function') {
|
||||
this.logger.info('found a direct "%s" callback, calling...', eventName)
|
||||
callback.call(target as XMLHttpRequest, event)
|
||||
}
|
||||
|
||||
// Invoke event listeners.
|
||||
const events =
|
||||
target instanceof XMLHttpRequestUpload ? this.uploadEvents : this.events
|
||||
|
||||
for (const [registeredEventName, listeners] of events) {
|
||||
if (registeredEventName === eventName) {
|
||||
this.logger.info(
|
||||
'found %d listener(s) for "%s" event, calling...',
|
||||
listeners.length,
|
||||
eventName
|
||||
)
|
||||
|
||||
listeners.forEach((listener) => listener.call(target, event))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts this `XMLHttpRequest` instance into a Fetch API `Request` instance.
|
||||
*/
|
||||
private toFetchApiRequest(
|
||||
body: XMLHttpRequestBodyInit | Document | null | undefined
|
||||
): Request {
|
||||
this.logger.info('converting request to a Fetch API Request...')
|
||||
|
||||
// If the `Document` is used as the body of this XMLHttpRequest,
|
||||
// set its inner text as the Fetch API Request body.
|
||||
const resolvedBody =
|
||||
body instanceof Document ? body.documentElement.innerText : body
|
||||
|
||||
const fetchRequest = new Request(this.url.href, {
|
||||
method: this.method,
|
||||
headers: this.requestHeaders,
|
||||
/**
|
||||
* @see https://xhr.spec.whatwg.org/#cross-origin-credentials
|
||||
*/
|
||||
credentials: this.request.withCredentials ? 'include' : 'same-origin',
|
||||
body: ['GET', 'HEAD'].includes(this.method.toUpperCase())
|
||||
? null
|
||||
: resolvedBody,
|
||||
})
|
||||
|
||||
const proxyHeaders = createProxy(fetchRequest.headers, {
|
||||
methodCall: ([methodName, args], invoke) => {
|
||||
// Forward the latest state of the internal request headers
|
||||
// because the interceptor might have modified them
|
||||
// without responding to the request.
|
||||
switch (methodName) {
|
||||
case 'append':
|
||||
case 'set': {
|
||||
const [headerName, headerValue] = args as [string, string]
|
||||
this.request.setRequestHeader(headerName, headerValue)
|
||||
break
|
||||
}
|
||||
|
||||
case 'delete': {
|
||||
const [headerName] = args as [string]
|
||||
console.warn(
|
||||
`XMLHttpRequest: Cannot remove a "${headerName}" header from the Fetch API representation of the "${fetchRequest.method} ${fetchRequest.url}" request. XMLHttpRequest headers cannot be removed.`
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return invoke()
|
||||
},
|
||||
})
|
||||
define(fetchRequest, 'headers', proxyHeaders)
|
||||
setRawRequest(fetchRequest, this.request)
|
||||
|
||||
this.logger.info('converted request to a Fetch API Request!', fetchRequest)
|
||||
|
||||
return fetchRequest
|
||||
}
|
||||
}
|
||||
|
||||
function toAbsoluteUrl(url: string | URL): URL {
|
||||
/**
|
||||
* @note XMLHttpRequest interceptor may run in environments
|
||||
* that implement XMLHttpRequest but don't implement "location"
|
||||
* (for example, React Native). If that's the case, return the
|
||||
* input URL as-is (nothing to be relative to).
|
||||
* @see https://github.com/mswjs/msw/issues/1777
|
||||
*/
|
||||
if (typeof location === 'undefined') {
|
||||
return new URL(url)
|
||||
}
|
||||
|
||||
return new URL(url.toString(), location.href)
|
||||
}
|
||||
|
||||
function define(
|
||||
target: object,
|
||||
property: string | symbol,
|
||||
value: unknown
|
||||
): void {
|
||||
Reflect.defineProperty(target, property, {
|
||||
// Ensure writable properties to allow redefining readonly properties.
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
value,
|
||||
})
|
||||
}
|
||||
121
node_modules/@mswjs/interceptors/src/interceptors/XMLHttpRequest/XMLHttpRequestProxy.ts
generated
vendored
Normal file
121
node_modules/@mswjs/interceptors/src/interceptors/XMLHttpRequest/XMLHttpRequestProxy.ts
generated
vendored
Normal file
@@ -0,0 +1,121 @@
|
||||
import type { Logger } from '@open-draft/logger'
|
||||
import { XMLHttpRequestEmitter } from '.'
|
||||
import { RequestController } from '../../RequestController'
|
||||
import { XMLHttpRequestController } from './XMLHttpRequestController'
|
||||
import { handleRequest } from '../../utils/handleRequest'
|
||||
import { isResponseError } from '../../utils/responseUtils'
|
||||
|
||||
export interface XMLHttpRequestProxyOptions {
|
||||
emitter: XMLHttpRequestEmitter
|
||||
logger: Logger
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a proxied `XMLHttpRequest` class.
|
||||
* The proxied class establishes spies on certain methods,
|
||||
* allowing us to intercept requests and respond to them.
|
||||
*/
|
||||
export function createXMLHttpRequestProxy({
|
||||
emitter,
|
||||
logger,
|
||||
}: XMLHttpRequestProxyOptions) {
|
||||
const XMLHttpRequestProxy = new Proxy(globalThis.XMLHttpRequest, {
|
||||
construct(target, args, newTarget) {
|
||||
logger.info('constructed new XMLHttpRequest')
|
||||
|
||||
const originalRequest = Reflect.construct(
|
||||
target,
|
||||
args,
|
||||
newTarget
|
||||
) as XMLHttpRequest
|
||||
|
||||
/**
|
||||
* @note Forward prototype descriptors onto the proxied object.
|
||||
* XMLHttpRequest is implemented in JSDOM in a way that assigns
|
||||
* a bunch of descriptors, like "set responseType()" on the prototype.
|
||||
* With this propagation, we make sure that those descriptors trigger
|
||||
* when the user operates with the proxied request instance.
|
||||
*/
|
||||
const prototypeDescriptors = Object.getOwnPropertyDescriptors(
|
||||
target.prototype
|
||||
)
|
||||
for (const propertyName in prototypeDescriptors) {
|
||||
Reflect.defineProperty(
|
||||
originalRequest,
|
||||
propertyName,
|
||||
prototypeDescriptors[propertyName]
|
||||
)
|
||||
}
|
||||
|
||||
const xhrRequestController = new XMLHttpRequestController(
|
||||
originalRequest,
|
||||
logger
|
||||
)
|
||||
|
||||
xhrRequestController.onRequest = async function ({ request, requestId }) {
|
||||
const controller = new RequestController(request, {
|
||||
passthrough: () => {
|
||||
this.logger.info(
|
||||
'no mocked response received, performing request as-is...'
|
||||
)
|
||||
},
|
||||
respondWith: async (response) => {
|
||||
if (isResponseError(response)) {
|
||||
this.errorWith(new TypeError('Network error'))
|
||||
return
|
||||
}
|
||||
|
||||
await this.respondWith(response)
|
||||
},
|
||||
errorWith: (reason) => {
|
||||
this.logger.info('request errored!', { error: reason })
|
||||
|
||||
if (reason instanceof Error) {
|
||||
this.errorWith(reason)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
this.logger.info('awaiting mocked response...')
|
||||
|
||||
this.logger.info(
|
||||
'emitting the "request" event for %s listener(s)...',
|
||||
emitter.listenerCount('request')
|
||||
)
|
||||
|
||||
await handleRequest({
|
||||
request,
|
||||
requestId,
|
||||
controller,
|
||||
emitter,
|
||||
})
|
||||
}
|
||||
|
||||
xhrRequestController.onResponse = async function ({
|
||||
response,
|
||||
isMockedResponse,
|
||||
request,
|
||||
requestId,
|
||||
}) {
|
||||
this.logger.info(
|
||||
'emitting the "response" event for %s listener(s)...',
|
||||
emitter.listenerCount('response')
|
||||
)
|
||||
|
||||
emitter.emit('response', {
|
||||
response,
|
||||
isMockedResponse,
|
||||
request,
|
||||
requestId,
|
||||
})
|
||||
}
|
||||
|
||||
// Return the proxied request from the controller
|
||||
// so that the controller can react to the consumer's interactions
|
||||
// with this request (opening/sending/etc).
|
||||
return xhrRequestController.request
|
||||
},
|
||||
})
|
||||
|
||||
return XMLHttpRequestProxy
|
||||
}
|
||||
61
node_modules/@mswjs/interceptors/src/interceptors/XMLHttpRequest/index.ts
generated
vendored
Normal file
61
node_modules/@mswjs/interceptors/src/interceptors/XMLHttpRequest/index.ts
generated
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
import { invariant } from 'outvariant'
|
||||
import { Emitter } from 'strict-event-emitter'
|
||||
import { HttpRequestEventMap, IS_PATCHED_MODULE } from '../../glossary'
|
||||
import { Interceptor } from '../../Interceptor'
|
||||
import { createXMLHttpRequestProxy } from './XMLHttpRequestProxy'
|
||||
import { hasConfigurableGlobal } from '../../utils/hasConfigurableGlobal'
|
||||
|
||||
export type XMLHttpRequestEmitter = Emitter<HttpRequestEventMap>
|
||||
|
||||
export class XMLHttpRequestInterceptor extends Interceptor<HttpRequestEventMap> {
|
||||
static interceptorSymbol = Symbol('xhr')
|
||||
|
||||
constructor() {
|
||||
super(XMLHttpRequestInterceptor.interceptorSymbol)
|
||||
}
|
||||
|
||||
protected checkEnvironment() {
|
||||
return hasConfigurableGlobal('XMLHttpRequest')
|
||||
}
|
||||
|
||||
protected setup() {
|
||||
const logger = this.logger.extend('setup')
|
||||
|
||||
logger.info('patching "XMLHttpRequest" module...')
|
||||
|
||||
const PureXMLHttpRequest = globalThis.XMLHttpRequest
|
||||
|
||||
invariant(
|
||||
!(PureXMLHttpRequest as any)[IS_PATCHED_MODULE],
|
||||
'Failed to patch the "XMLHttpRequest" module: already patched.'
|
||||
)
|
||||
|
||||
globalThis.XMLHttpRequest = createXMLHttpRequestProxy({
|
||||
emitter: this.emitter,
|
||||
logger: this.logger,
|
||||
})
|
||||
|
||||
logger.info(
|
||||
'native "XMLHttpRequest" module patched!',
|
||||
globalThis.XMLHttpRequest.name
|
||||
)
|
||||
|
||||
Object.defineProperty(globalThis.XMLHttpRequest, IS_PATCHED_MODULE, {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
value: true,
|
||||
})
|
||||
|
||||
this.subscriptions.push(() => {
|
||||
Object.defineProperty(globalThis.XMLHttpRequest, IS_PATCHED_MODULE, {
|
||||
value: undefined,
|
||||
})
|
||||
|
||||
globalThis.XMLHttpRequest = PureXMLHttpRequest
|
||||
logger.info(
|
||||
'native "XMLHttpRequest" module restored!',
|
||||
globalThis.XMLHttpRequest.name
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
51
node_modules/@mswjs/interceptors/src/interceptors/XMLHttpRequest/polyfills/EventPolyfill.ts
generated
vendored
Normal file
51
node_modules/@mswjs/interceptors/src/interceptors/XMLHttpRequest/polyfills/EventPolyfill.ts
generated
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
export class EventPolyfill implements Event {
|
||||
readonly NONE = 0
|
||||
readonly CAPTURING_PHASE = 1
|
||||
readonly AT_TARGET = 2
|
||||
readonly BUBBLING_PHASE = 3
|
||||
|
||||
public type: string = ''
|
||||
public srcElement: EventTarget | null = null
|
||||
public target: EventTarget | null
|
||||
public currentTarget: EventTarget | null = null
|
||||
public eventPhase: number = 0
|
||||
public timeStamp: number
|
||||
public isTrusted: boolean = true
|
||||
public composed: boolean = false
|
||||
public cancelable: boolean = true
|
||||
public defaultPrevented: boolean = false
|
||||
public bubbles: boolean = true
|
||||
public lengthComputable: boolean = true
|
||||
public loaded: number = 0
|
||||
public total: number = 0
|
||||
|
||||
cancelBubble: boolean = false
|
||||
returnValue: boolean = true
|
||||
|
||||
constructor(
|
||||
type: string,
|
||||
options?: { target: EventTarget; currentTarget: EventTarget }
|
||||
) {
|
||||
this.type = type
|
||||
this.target = options?.target || null
|
||||
this.currentTarget = options?.currentTarget || null
|
||||
this.timeStamp = Date.now()
|
||||
}
|
||||
|
||||
public composedPath(): EventTarget[] {
|
||||
return []
|
||||
}
|
||||
|
||||
public initEvent(type: string, bubbles?: boolean, cancelable?: boolean) {
|
||||
this.type = type
|
||||
this.bubbles = !!bubbles
|
||||
this.cancelable = !!cancelable
|
||||
}
|
||||
|
||||
public preventDefault() {
|
||||
this.defaultPrevented = true
|
||||
}
|
||||
|
||||
public stopPropagation() {}
|
||||
public stopImmediatePropagation() {}
|
||||
}
|
||||
17
node_modules/@mswjs/interceptors/src/interceptors/XMLHttpRequest/polyfills/ProgressEventPolyfill.ts
generated
vendored
Normal file
17
node_modules/@mswjs/interceptors/src/interceptors/XMLHttpRequest/polyfills/ProgressEventPolyfill.ts
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
import { EventPolyfill } from './EventPolyfill'
|
||||
|
||||
export class ProgressEventPolyfill extends EventPolyfill {
|
||||
readonly lengthComputable: boolean
|
||||
readonly composed: boolean
|
||||
readonly loaded: number
|
||||
readonly total: number
|
||||
|
||||
constructor(type: string, init?: ProgressEventInit) {
|
||||
super(type)
|
||||
|
||||
this.lengthComputable = init?.lengthComputable || false
|
||||
this.composed = init?.composed || false
|
||||
this.loaded = init?.loaded || 0
|
||||
this.total = init?.total || 0
|
||||
}
|
||||
}
|
||||
12
node_modules/@mswjs/interceptors/src/interceptors/XMLHttpRequest/utils/concatArrayBuffer.ts
generated
vendored
Normal file
12
node_modules/@mswjs/interceptors/src/interceptors/XMLHttpRequest/utils/concatArrayBuffer.ts
generated
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Concatenate two `Uint8Array` buffers.
|
||||
*/
|
||||
export function concatArrayBuffer(
|
||||
left: Uint8Array,
|
||||
right: Uint8Array
|
||||
): Uint8Array {
|
||||
const result = new Uint8Array(left.byteLength + right.byteLength)
|
||||
result.set(left, 0)
|
||||
result.set(right, left.byteLength)
|
||||
return result
|
||||
}
|
||||
12
node_modules/@mswjs/interceptors/src/interceptors/XMLHttpRequest/utils/concateArrayBuffer.test.ts
generated
vendored
Normal file
12
node_modules/@mswjs/interceptors/src/interceptors/XMLHttpRequest/utils/concateArrayBuffer.test.ts
generated
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
import { it, expect } from 'vitest'
|
||||
import { concatArrayBuffer } from './concatArrayBuffer'
|
||||
|
||||
const encoder = new TextEncoder()
|
||||
|
||||
it('concatenates two Uint8Array buffers', () => {
|
||||
const result = concatArrayBuffer(
|
||||
encoder.encode('hello'),
|
||||
encoder.encode('world')
|
||||
)
|
||||
expect(result).toEqual(encoder.encode('helloworld'))
|
||||
})
|
||||
26
node_modules/@mswjs/interceptors/src/interceptors/XMLHttpRequest/utils/createEvent.test.ts
generated
vendored
Normal file
26
node_modules/@mswjs/interceptors/src/interceptors/XMLHttpRequest/utils/createEvent.test.ts
generated
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
// @vitest-environment jsdom
|
||||
import { it, expect } from 'vitest'
|
||||
import { createEvent } from './createEvent'
|
||||
import { EventPolyfill } from '../polyfills/EventPolyfill'
|
||||
|
||||
const request = new XMLHttpRequest()
|
||||
request.open('POST', '/user')
|
||||
|
||||
it('returns an EventPolyfill instance with the given target set', () => {
|
||||
const event = createEvent(request, 'my-event')
|
||||
const target = event.target as XMLHttpRequest
|
||||
|
||||
expect(event).toBeInstanceOf(EventPolyfill)
|
||||
expect(target).toBeInstanceOf(XMLHttpRequest)
|
||||
})
|
||||
|
||||
it('returns the ProgressEvent instance', () => {
|
||||
const event = createEvent(request, 'load', {
|
||||
loaded: 100,
|
||||
total: 500,
|
||||
})
|
||||
|
||||
expect(event).toBeInstanceOf(ProgressEvent)
|
||||
expect(event.loaded).toBe(100)
|
||||
expect(event.total).toBe(500)
|
||||
})
|
||||
41
node_modules/@mswjs/interceptors/src/interceptors/XMLHttpRequest/utils/createEvent.ts
generated
vendored
Normal file
41
node_modules/@mswjs/interceptors/src/interceptors/XMLHttpRequest/utils/createEvent.ts
generated
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
import { EventPolyfill } from '../polyfills/EventPolyfill'
|
||||
import { ProgressEventPolyfill } from '../polyfills/ProgressEventPolyfill'
|
||||
|
||||
const SUPPORTS_PROGRESS_EVENT = typeof ProgressEvent !== 'undefined'
|
||||
|
||||
export function createEvent(
|
||||
target: XMLHttpRequest | XMLHttpRequestUpload,
|
||||
type: string,
|
||||
init?: ProgressEventInit
|
||||
): EventPolyfill | ProgressEvent {
|
||||
const progressEvents = [
|
||||
'error',
|
||||
'progress',
|
||||
'loadstart',
|
||||
'loadend',
|
||||
'load',
|
||||
'timeout',
|
||||
'abort',
|
||||
]
|
||||
|
||||
/**
|
||||
* `ProgressEvent` is not supported in React Native.
|
||||
* @see https://github.com/mswjs/interceptors/issues/40
|
||||
*/
|
||||
const ProgressEventClass = SUPPORTS_PROGRESS_EVENT
|
||||
? ProgressEvent
|
||||
: ProgressEventPolyfill
|
||||
|
||||
const event = progressEvents.includes(type)
|
||||
? new ProgressEventClass(type, {
|
||||
lengthComputable: true,
|
||||
loaded: init?.loaded || 0,
|
||||
total: init?.total || 0,
|
||||
})
|
||||
: new EventPolyfill(type, {
|
||||
target,
|
||||
currentTarget: target,
|
||||
})
|
||||
|
||||
return event
|
||||
}
|
||||
49
node_modules/@mswjs/interceptors/src/interceptors/XMLHttpRequest/utils/createResponse.ts
generated
vendored
Normal file
49
node_modules/@mswjs/interceptors/src/interceptors/XMLHttpRequest/utils/createResponse.ts
generated
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
import { FetchResponse } from '../../../utils/fetchUtils'
|
||||
|
||||
/**
|
||||
* Creates a Fetch API `Response` instance from the given
|
||||
* `XMLHttpRequest` instance and a response body.
|
||||
*/
|
||||
export function createResponse(
|
||||
request: XMLHttpRequest,
|
||||
body: BodyInit | null
|
||||
): Response {
|
||||
/**
|
||||
* Handle XMLHttpRequest responses that must have null as the
|
||||
* response body when represented using Fetch API Response.
|
||||
* XMLHttpRequest response will always have an empty string
|
||||
* as the "request.response" in those cases, resulting in an error
|
||||
* when constructing a Response instance.
|
||||
* @see https://github.com/mswjs/interceptors/issues/379
|
||||
*/
|
||||
const responseBodyOrNull = FetchResponse.isResponseWithBody(request.status)
|
||||
? body
|
||||
: null
|
||||
|
||||
return new FetchResponse(responseBodyOrNull, {
|
||||
url: request.responseURL,
|
||||
status: request.status,
|
||||
statusText: request.statusText,
|
||||
headers: createHeadersFromXMLHttpRequestHeaders(
|
||||
request.getAllResponseHeaders()
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
function createHeadersFromXMLHttpRequestHeaders(headersString: string): Headers {
|
||||
const headers = new Headers()
|
||||
|
||||
const lines = headersString.split(/[\r\n]+/)
|
||||
for (const line of lines) {
|
||||
if (line.trim() === '') {
|
||||
continue
|
||||
}
|
||||
|
||||
const [name, ...parts] = line.split(': ')
|
||||
const value = parts.join(': ')
|
||||
|
||||
headers.append(name, value)
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
164
node_modules/@mswjs/interceptors/src/interceptors/XMLHttpRequest/utils/getBodyByteLength.test.ts
generated
vendored
Normal file
164
node_modules/@mswjs/interceptors/src/interceptors/XMLHttpRequest/utils/getBodyByteLength.test.ts
generated
vendored
Normal file
@@ -0,0 +1,164 @@
|
||||
// @vitest-environment node
|
||||
import { it, expect } from 'vitest'
|
||||
import { getBodyByteLength } from './getBodyByteLength'
|
||||
|
||||
const url = 'http://localhost'
|
||||
|
||||
it('returns explicit body length set in the "Content-Length" header', async () => {
|
||||
await expect(
|
||||
getBodyByteLength(new Request(url, { headers: { 'Content-Length': '10' } }))
|
||||
).resolves.toBe(10)
|
||||
|
||||
await expect(
|
||||
getBodyByteLength(
|
||||
new Response('hello', { headers: { 'Content-Length': '5' } })
|
||||
)
|
||||
).resolves.toBe(5)
|
||||
})
|
||||
|
||||
/**
|
||||
* Request.
|
||||
*/
|
||||
|
||||
it('returns 0 for a request with an empty body', async () => {
|
||||
await expect(getBodyByteLength(new Request(url))).resolves.toBe(0)
|
||||
await expect(
|
||||
getBodyByteLength(new Request(url, { method: 'POST', body: null }))
|
||||
).resolves.toBe(0)
|
||||
await expect(
|
||||
getBodyByteLength(new Request(url, { method: 'POST', body: undefined }))
|
||||
).resolves.toBe(0)
|
||||
await expect(
|
||||
getBodyByteLength(new Request(url, { method: 'POST', body: '' }))
|
||||
).resolves.toBe(0)
|
||||
})
|
||||
|
||||
it('calculates body length from the text request body', async () => {
|
||||
await expect(
|
||||
getBodyByteLength(
|
||||
new Request(url, {
|
||||
method: 'POST',
|
||||
body: 'hello world',
|
||||
})
|
||||
)
|
||||
).resolves.toBe(11)
|
||||
})
|
||||
|
||||
it('calculates body length from the URLSearchParams request body', async () => {
|
||||
await expect(
|
||||
getBodyByteLength(
|
||||
new Request(url, {
|
||||
method: 'POST',
|
||||
body: new URLSearchParams([['hello', 'world']]),
|
||||
})
|
||||
)
|
||||
).resolves.toBe(11)
|
||||
})
|
||||
|
||||
it('calculates body length from the Blob request body', async () => {
|
||||
await expect(
|
||||
getBodyByteLength(
|
||||
new Request(url, {
|
||||
method: 'POST',
|
||||
body: new Blob(['hello world']),
|
||||
})
|
||||
)
|
||||
).resolves.toBe(11)
|
||||
})
|
||||
|
||||
it('calculates body length from the ArrayBuffer request body', async () => {
|
||||
await expect(
|
||||
getBodyByteLength(
|
||||
new Request(url, {
|
||||
method: 'POST',
|
||||
body: await new Blob(['hello world']).arrayBuffer(),
|
||||
})
|
||||
)
|
||||
).resolves.toBe(11)
|
||||
})
|
||||
|
||||
it('calculates body length from the FormData request body', async () => {
|
||||
const formData = new FormData()
|
||||
formData.append('hello', 'world')
|
||||
|
||||
await expect(
|
||||
getBodyByteLength(
|
||||
new Request(url, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
)
|
||||
).resolves.toBe(129)
|
||||
})
|
||||
|
||||
it('calculates body length from the ReadableStream request body', async () => {
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(new TextEncoder().encode('hello world'))
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
|
||||
await expect(
|
||||
getBodyByteLength(
|
||||
new Request(url, {
|
||||
method: 'POST',
|
||||
body: stream,
|
||||
// @ts-expect-error Undocumented required Undici property.
|
||||
duplex: 'half',
|
||||
})
|
||||
)
|
||||
).resolves.toBe(11)
|
||||
})
|
||||
|
||||
/**
|
||||
* Response.
|
||||
*/
|
||||
it('returns 0 for a response with an empty body', async () => {
|
||||
await expect(getBodyByteLength(new Response())).resolves.toBe(0)
|
||||
await expect(getBodyByteLength(new Response(null))).resolves.toBe(0)
|
||||
await expect(getBodyByteLength(new Response(undefined))).resolves.toBe(0)
|
||||
await expect(getBodyByteLength(new Response(''))).resolves.toBe(0)
|
||||
})
|
||||
|
||||
it('calculates body length from the text response body', async () => {
|
||||
await expect(getBodyByteLength(new Response('hello world'))).resolves.toBe(11)
|
||||
})
|
||||
|
||||
it('calculates body length from the URLSearchParams response body', async () => {
|
||||
await expect(
|
||||
getBodyByteLength(new Response(new URLSearchParams([['hello', 'world']])))
|
||||
).resolves.toBe(11)
|
||||
})
|
||||
|
||||
it('calculates body length from the Blob response body', async () => {
|
||||
await expect(
|
||||
getBodyByteLength(new Response(new Blob(['hello world'])))
|
||||
).resolves.toBe(11)
|
||||
})
|
||||
|
||||
it('calculates body length from the ArrayBuffer response body', async () => {
|
||||
await expect(
|
||||
getBodyByteLength(
|
||||
new Response(await new Blob(['hello world']).arrayBuffer())
|
||||
)
|
||||
).resolves.toBe(11)
|
||||
})
|
||||
|
||||
it('calculates body length from the FormData response body', async () => {
|
||||
const formData = new FormData()
|
||||
formData.append('hello', 'world')
|
||||
|
||||
await expect(getBodyByteLength(new Response(formData))).resolves.toBe(129)
|
||||
})
|
||||
|
||||
it('calculates body length from the ReadableStream response body', async () => {
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(new TextEncoder().encode('hello world'))
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
|
||||
await expect(getBodyByteLength(new Response(stream))).resolves.toBe(11)
|
||||
})
|
||||
16
node_modules/@mswjs/interceptors/src/interceptors/XMLHttpRequest/utils/getBodyByteLength.ts
generated
vendored
Normal file
16
node_modules/@mswjs/interceptors/src/interceptors/XMLHttpRequest/utils/getBodyByteLength.ts
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Return a total byte length of the given request/response body.
|
||||
* If the `Content-Length` header is present, it will be used as the byte length.
|
||||
*/
|
||||
export async function getBodyByteLength(
|
||||
input: Request | Response
|
||||
): Promise<number> {
|
||||
const explicitContentLength = input.headers.get('content-length')
|
||||
|
||||
if (explicitContentLength != null && explicitContentLength !== '') {
|
||||
return Number(explicitContentLength)
|
||||
}
|
||||
|
||||
const buffer = await input.arrayBuffer()
|
||||
return buffer.byteLength
|
||||
}
|
||||
14
node_modules/@mswjs/interceptors/src/interceptors/XMLHttpRequest/utils/isDomParserSupportedType.ts
generated
vendored
Normal file
14
node_modules/@mswjs/interceptors/src/interceptors/XMLHttpRequest/utils/isDomParserSupportedType.ts
generated
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
export function isDomParserSupportedType(
|
||||
type: string
|
||||
): type is DOMParserSupportedType {
|
||||
const supportedTypes: Array<DOMParserSupportedType> = [
|
||||
'application/xhtml+xml',
|
||||
'application/xml',
|
||||
'image/svg+xml',
|
||||
'text/html',
|
||||
'text/xml',
|
||||
]
|
||||
return supportedTypes.some((supportedType) => {
|
||||
return type.startsWith(supportedType)
|
||||
})
|
||||
}
|
||||
214
node_modules/@mswjs/interceptors/src/interceptors/fetch/index.ts
generated
vendored
Normal file
214
node_modules/@mswjs/interceptors/src/interceptors/fetch/index.ts
generated
vendored
Normal file
@@ -0,0 +1,214 @@
|
||||
import { invariant } from 'outvariant'
|
||||
import { until } from '@open-draft/until'
|
||||
import { DeferredPromise } from '@open-draft/deferred-promise'
|
||||
import { HttpRequestEventMap, IS_PATCHED_MODULE } from '../../glossary'
|
||||
import { Interceptor } from '../../Interceptor'
|
||||
import { RequestController } from '../../RequestController'
|
||||
import { emitAsync } from '../../utils/emitAsync'
|
||||
import { handleRequest } from '../../utils/handleRequest'
|
||||
import { canParseUrl } from '../../utils/canParseUrl'
|
||||
import { createRequestId } from '../../createRequestId'
|
||||
import { createNetworkError } from './utils/createNetworkError'
|
||||
import { followFetchRedirect } from './utils/followRedirect'
|
||||
import { decompressResponse } from './utils/decompression'
|
||||
import { hasConfigurableGlobal } from '../../utils/hasConfigurableGlobal'
|
||||
import { FetchResponse } from '../../utils/fetchUtils'
|
||||
import { setRawRequest } from '../../getRawRequest'
|
||||
import { isResponseError } from '../../utils/responseUtils'
|
||||
|
||||
export class FetchInterceptor extends Interceptor<HttpRequestEventMap> {
|
||||
static symbol = Symbol('fetch')
|
||||
|
||||
constructor() {
|
||||
super(FetchInterceptor.symbol)
|
||||
}
|
||||
|
||||
protected checkEnvironment() {
|
||||
return hasConfigurableGlobal('fetch')
|
||||
}
|
||||
|
||||
protected async setup() {
|
||||
const pureFetch = globalThis.fetch
|
||||
|
||||
invariant(
|
||||
!(pureFetch as any)[IS_PATCHED_MODULE],
|
||||
'Failed to patch the "fetch" module: already patched.'
|
||||
)
|
||||
|
||||
globalThis.fetch = async (input, init) => {
|
||||
const requestId = createRequestId()
|
||||
|
||||
/**
|
||||
* @note Resolve potentially relative request URL
|
||||
* against the present `location`. This is mainly
|
||||
* for native `fetch` in JSDOM.
|
||||
* @see https://github.com/mswjs/msw/issues/1625
|
||||
*/
|
||||
const resolvedInput =
|
||||
typeof input === 'string' &&
|
||||
typeof location !== 'undefined' &&
|
||||
!canParseUrl(input)
|
||||
? new URL(input, location.href)
|
||||
: input
|
||||
|
||||
const request = new Request(resolvedInput, init)
|
||||
|
||||
/**
|
||||
* @note Set the raw request only if a Request instance was provided to fetch.
|
||||
*/
|
||||
if (input instanceof Request) {
|
||||
setRawRequest(request, input)
|
||||
}
|
||||
|
||||
const responsePromise = new DeferredPromise<Response>()
|
||||
|
||||
const controller = new RequestController(request, {
|
||||
passthrough: async () => {
|
||||
this.logger.info('request has not been handled, passthrough...')
|
||||
|
||||
/**
|
||||
* @note Clone the request instance right before performing it.
|
||||
* This preserves any modifications made to the intercepted request
|
||||
* in the "request" listener. This also allows the user to read the
|
||||
* request body in the "response" listener (otherwise "unusable").
|
||||
*/
|
||||
const requestCloneForResponseEvent = request.clone()
|
||||
|
||||
// Perform the intercepted request as-is.
|
||||
const { error: responseError, data: originalResponse } = await until(
|
||||
() => pureFetch(request)
|
||||
)
|
||||
|
||||
if (responseError) {
|
||||
return responsePromise.reject(responseError)
|
||||
}
|
||||
|
||||
this.logger.info('original fetch performed', originalResponse)
|
||||
|
||||
if (this.emitter.listenerCount('response') > 0) {
|
||||
this.logger.info('emitting the "response" event...')
|
||||
|
||||
const responseClone = originalResponse.clone()
|
||||
await emitAsync(this.emitter, 'response', {
|
||||
response: responseClone,
|
||||
isMockedResponse: false,
|
||||
request: requestCloneForResponseEvent,
|
||||
requestId,
|
||||
})
|
||||
}
|
||||
|
||||
// Resolve the response promise with the original response
|
||||
// since the `fetch()` return this internal promise.
|
||||
responsePromise.resolve(originalResponse)
|
||||
},
|
||||
respondWith: async (rawResponse) => {
|
||||
// Handle mocked `Response.error()` (i.e. request errors).
|
||||
if (isResponseError(rawResponse)) {
|
||||
this.logger.info('request has errored!', { response: rawResponse })
|
||||
responsePromise.reject(createNetworkError(rawResponse))
|
||||
return
|
||||
}
|
||||
|
||||
this.logger.info('received mocked response!', {
|
||||
rawResponse,
|
||||
})
|
||||
|
||||
// Decompress the mocked response body, if applicable.
|
||||
const decompressedStream = decompressResponse(rawResponse)
|
||||
const response =
|
||||
decompressedStream === null
|
||||
? rawResponse
|
||||
: new FetchResponse(decompressedStream, rawResponse)
|
||||
|
||||
FetchResponse.setUrl(request.url, response)
|
||||
|
||||
/**
|
||||
* Undici's handling of following redirect responses.
|
||||
* Treat the "manual" redirect mode as a regular mocked response.
|
||||
* This way, the client can manually follow the redirect it receives.
|
||||
* @see https://github.com/nodejs/undici/blob/a6dac3149c505b58d2e6d068b97f4dc993da55f0/lib/web/fetch/index.js#L1173
|
||||
*/
|
||||
if (FetchResponse.isRedirectResponse(response.status)) {
|
||||
// Reject the request promise if its `redirect` is set to `error`
|
||||
// and it receives a mocked redirect response.
|
||||
if (request.redirect === 'error') {
|
||||
responsePromise.reject(createNetworkError('unexpected redirect'))
|
||||
return
|
||||
}
|
||||
|
||||
if (request.redirect === 'follow') {
|
||||
followFetchRedirect(request, response).then(
|
||||
(response) => {
|
||||
responsePromise.resolve(response)
|
||||
},
|
||||
(reason) => {
|
||||
responsePromise.reject(reason)
|
||||
}
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (this.emitter.listenerCount('response') > 0) {
|
||||
this.logger.info('emitting the "response" event...')
|
||||
|
||||
// Await the response listeners to finish before resolving
|
||||
// the response promise. This ensures all your logic finishes
|
||||
// before the interceptor resolves the pending response.
|
||||
await emitAsync(this.emitter, 'response', {
|
||||
// Clone the mocked response for the "response" event listener.
|
||||
// This way, the listener can read the response and not lock its body
|
||||
// for the actual fetch consumer.
|
||||
response: response.clone(),
|
||||
isMockedResponse: true,
|
||||
request,
|
||||
requestId,
|
||||
})
|
||||
}
|
||||
|
||||
responsePromise.resolve(response)
|
||||
},
|
||||
errorWith: (reason) => {
|
||||
this.logger.info('request has been aborted!', { reason })
|
||||
responsePromise.reject(reason)
|
||||
},
|
||||
})
|
||||
|
||||
this.logger.info('[%s] %s', request.method, request.url)
|
||||
this.logger.info('awaiting for the mocked response...')
|
||||
|
||||
this.logger.info(
|
||||
'emitting the "request" event for %s listener(s)...',
|
||||
this.emitter.listenerCount('request')
|
||||
)
|
||||
|
||||
await handleRequest({
|
||||
request,
|
||||
requestId,
|
||||
emitter: this.emitter,
|
||||
controller,
|
||||
})
|
||||
|
||||
return responsePromise
|
||||
}
|
||||
|
||||
Object.defineProperty(globalThis.fetch, IS_PATCHED_MODULE, {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
value: true,
|
||||
})
|
||||
|
||||
this.subscriptions.push(() => {
|
||||
Object.defineProperty(globalThis.fetch, IS_PATCHED_MODULE, {
|
||||
value: undefined,
|
||||
})
|
||||
|
||||
globalThis.fetch = pureFetch
|
||||
|
||||
this.logger.info(
|
||||
'restored native "globalThis.fetch"!',
|
||||
globalThis.fetch.name
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
14
node_modules/@mswjs/interceptors/src/interceptors/fetch/utils/brotli-decompress.browser.ts
generated
vendored
Normal file
14
node_modules/@mswjs/interceptors/src/interceptors/fetch/utils/brotli-decompress.browser.ts
generated
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
export class BrotliDecompressionStream extends TransformStream {
|
||||
constructor() {
|
||||
console.warn(
|
||||
'[Interceptors]: Brotli decompression of response streams is not supported in the browser'
|
||||
)
|
||||
|
||||
super({
|
||||
transform(chunk, controller) {
|
||||
// Keep the stream as passthrough, it does nothing.
|
||||
controller.enqueue(chunk)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
31
node_modules/@mswjs/interceptors/src/interceptors/fetch/utils/brotli-decompress.ts
generated
vendored
Normal file
31
node_modules/@mswjs/interceptors/src/interceptors/fetch/utils/brotli-decompress.ts
generated
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
import zlib from 'node:zlib'
|
||||
|
||||
export class BrotliDecompressionStream extends TransformStream {
|
||||
constructor() {
|
||||
const decompress = zlib.createBrotliDecompress({
|
||||
flush: zlib.constants.BROTLI_OPERATION_FLUSH,
|
||||
finishFlush: zlib.constants.BROTLI_OPERATION_FLUSH,
|
||||
})
|
||||
|
||||
super({
|
||||
async transform(chunk, controller) {
|
||||
const buffer = Buffer.from(chunk)
|
||||
|
||||
const decompressed = await new Promise<Buffer>((resolve, reject) => {
|
||||
decompress.write(buffer, (error) => {
|
||||
if (error) reject(error)
|
||||
})
|
||||
|
||||
decompress.flush()
|
||||
decompress.once('data', (data) => resolve(data))
|
||||
decompress.once('error', (error) => reject(error))
|
||||
decompress.once('end', () => controller.terminate())
|
||||
}).catch((error) => {
|
||||
controller.error(error)
|
||||
})
|
||||
|
||||
controller.enqueue(decompressed)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
5
node_modules/@mswjs/interceptors/src/interceptors/fetch/utils/createNetworkError.ts
generated
vendored
Normal file
5
node_modules/@mswjs/interceptors/src/interceptors/fetch/utils/createNetworkError.ts
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
export function createNetworkError(cause?: unknown) {
|
||||
return Object.assign(new TypeError('Failed to fetch'), {
|
||||
cause,
|
||||
})
|
||||
}
|
||||
85
node_modules/@mswjs/interceptors/src/interceptors/fetch/utils/decompression.ts
generated
vendored
Normal file
85
node_modules/@mswjs/interceptors/src/interceptors/fetch/utils/decompression.ts
generated
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
// Import from an internal alias that resolves to different modules
|
||||
// depending on the environment. This way, we can keep the fetch interceptor
|
||||
// intact while using different strategies for Brotli decompression.
|
||||
import { BrotliDecompressionStream } from 'internal:brotli-decompress'
|
||||
|
||||
class PipelineStream extends TransformStream {
|
||||
constructor(
|
||||
transformStreams: Array<TransformStream>,
|
||||
...strategies: Array<QueuingStrategy>
|
||||
) {
|
||||
super({}, ...strategies)
|
||||
|
||||
const readable = [super.readable as any, ...transformStreams].reduce(
|
||||
(readable, transform) => readable.pipeThrough(transform)
|
||||
)
|
||||
|
||||
Object.defineProperty(this, 'readable', {
|
||||
get() {
|
||||
return readable
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function parseContentEncoding(contentEncoding: string): Array<string> {
|
||||
return contentEncoding
|
||||
.toLowerCase()
|
||||
.split(',')
|
||||
.map((coding) => coding.trim())
|
||||
}
|
||||
|
||||
function createDecompressionStream(
|
||||
contentEncoding: string
|
||||
): TransformStream | null {
|
||||
if (contentEncoding === '') {
|
||||
return null
|
||||
}
|
||||
|
||||
const codings = parseContentEncoding(contentEncoding)
|
||||
|
||||
if (codings.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const transformers = codings.reduceRight<Array<TransformStream>>(
|
||||
(transformers, coding) => {
|
||||
if (coding === 'gzip' || coding === 'x-gzip') {
|
||||
return transformers.concat(new DecompressionStream('gzip'))
|
||||
} else if (coding === 'deflate') {
|
||||
return transformers.concat(new DecompressionStream('deflate'))
|
||||
} else if (coding === 'br') {
|
||||
return transformers.concat(new BrotliDecompressionStream())
|
||||
} else {
|
||||
transformers.length = 0
|
||||
}
|
||||
|
||||
return transformers
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
return new PipelineStream(transformers)
|
||||
}
|
||||
|
||||
export function decompressResponse(
|
||||
response: Response
|
||||
): ReadableStream<any> | null {
|
||||
if (response.body === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
const decompressionStream = createDecompressionStream(
|
||||
response.headers.get('content-encoding') || ''
|
||||
)
|
||||
|
||||
if (!decompressionStream) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Use `pipeTo` and return the decompression stream's readable
|
||||
// instead of `pipeThrough` because that will lock the original
|
||||
// response stream, making it unusable as the input to Response.
|
||||
response.body.pipeTo(decompressionStream.writable)
|
||||
return decompressionStream.readable
|
||||
}
|
||||
114
node_modules/@mswjs/interceptors/src/interceptors/fetch/utils/followRedirect.ts
generated
vendored
Normal file
114
node_modules/@mswjs/interceptors/src/interceptors/fetch/utils/followRedirect.ts
generated
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
import { createNetworkError } from './createNetworkError'
|
||||
|
||||
const REQUEST_BODY_HEADERS = [
|
||||
'content-encoding',
|
||||
'content-language',
|
||||
'content-location',
|
||||
'content-type',
|
||||
'content-length',
|
||||
]
|
||||
|
||||
const kRedirectCount = Symbol('kRedirectCount')
|
||||
|
||||
/**
|
||||
* @see https://github.com/nodejs/undici/blob/a6dac3149c505b58d2e6d068b97f4dc993da55f0/lib/web/fetch/index.js#L1210
|
||||
*/
|
||||
export async function followFetchRedirect(
|
||||
request: Request,
|
||||
response: Response
|
||||
): Promise<Response> {
|
||||
if (response.status !== 303 && request.body != null) {
|
||||
return Promise.reject(createNetworkError())
|
||||
}
|
||||
|
||||
const requestUrl = new URL(request.url)
|
||||
|
||||
let locationUrl: URL
|
||||
try {
|
||||
// If the location is a relative URL, use the request URL as the base URL.
|
||||
locationUrl = new URL(response.headers.get('location')!, request.url)
|
||||
} catch (error) {
|
||||
return Promise.reject(createNetworkError(error))
|
||||
}
|
||||
|
||||
if (
|
||||
!(locationUrl.protocol === 'http:' || locationUrl.protocol === 'https:')
|
||||
) {
|
||||
return Promise.reject(
|
||||
createNetworkError('URL scheme must be a HTTP(S) scheme')
|
||||
)
|
||||
}
|
||||
|
||||
if (Reflect.get(request, kRedirectCount) > 20) {
|
||||
return Promise.reject(createNetworkError('redirect count exceeded'))
|
||||
}
|
||||
|
||||
Object.defineProperty(request, kRedirectCount, {
|
||||
value: (Reflect.get(request, kRedirectCount) || 0) + 1,
|
||||
})
|
||||
|
||||
if (
|
||||
request.mode === 'cors' &&
|
||||
(locationUrl.username || locationUrl.password) &&
|
||||
!sameOrigin(requestUrl, locationUrl)
|
||||
) {
|
||||
return Promise.reject(
|
||||
createNetworkError('cross origin not allowed for request mode "cors"')
|
||||
)
|
||||
}
|
||||
|
||||
const requestInit: RequestInit = {}
|
||||
|
||||
if (
|
||||
([301, 302].includes(response.status) && request.method === 'POST') ||
|
||||
(response.status === 303 && !['HEAD', 'GET'].includes(request.method))
|
||||
) {
|
||||
requestInit.method = 'GET'
|
||||
requestInit.body = null
|
||||
|
||||
REQUEST_BODY_HEADERS.forEach((headerName) => {
|
||||
request.headers.delete(headerName)
|
||||
})
|
||||
}
|
||||
|
||||
if (!sameOrigin(requestUrl, locationUrl)) {
|
||||
request.headers.delete('authorization')
|
||||
request.headers.delete('proxy-authorization')
|
||||
request.headers.delete('cookie')
|
||||
request.headers.delete('host')
|
||||
}
|
||||
|
||||
/**
|
||||
* @note Undici "safely" extracts the request body.
|
||||
* I suspect we cannot dispatch this request again
|
||||
* since its body has been read and the stream is locked.
|
||||
*/
|
||||
|
||||
requestInit.headers = request.headers
|
||||
const finalResponse = await fetch(new Request(locationUrl, requestInit))
|
||||
Object.defineProperty(finalResponse, 'redirected', {
|
||||
value: true,
|
||||
configurable: true,
|
||||
})
|
||||
|
||||
return finalResponse
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://github.com/nodejs/undici/blob/a6dac3149c505b58d2e6d068b97f4dc993da55f0/lib/web/fetch/util.js#L761
|
||||
*/
|
||||
function sameOrigin(left: URL, right: URL): boolean {
|
||||
if (left.origin === right.origin && left.origin === 'null') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (
|
||||
left.protocol === right.protocol &&
|
||||
left.hostname === right.hostname &&
|
||||
left.port === right.port
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
11
node_modules/@mswjs/interceptors/src/presets/browser.ts
generated
vendored
Normal file
11
node_modules/@mswjs/interceptors/src/presets/browser.ts
generated
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
import { FetchInterceptor } from '../interceptors/fetch'
|
||||
import { XMLHttpRequestInterceptor } from '../interceptors/XMLHttpRequest'
|
||||
|
||||
/**
|
||||
* The default preset provisions the interception of requests
|
||||
* regardless of their type (fetch/XMLHttpRequest).
|
||||
*/
|
||||
export default [
|
||||
new FetchInterceptor(),
|
||||
new XMLHttpRequestInterceptor(),
|
||||
] as const
|
||||
13
node_modules/@mswjs/interceptors/src/presets/node.ts
generated
vendored
Normal file
13
node_modules/@mswjs/interceptors/src/presets/node.ts
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ClientRequestInterceptor } from '../interceptors/ClientRequest'
|
||||
import { XMLHttpRequestInterceptor } from '../interceptors/XMLHttpRequest'
|
||||
import { FetchInterceptor } from '../interceptors/fetch'
|
||||
|
||||
/**
|
||||
* The default preset provisions the interception of requests
|
||||
* regardless of their type (http/https/XMLHttpRequest).
|
||||
*/
|
||||
export default [
|
||||
new ClientRequestInterceptor(),
|
||||
new XMLHttpRequestInterceptor(),
|
||||
new FetchInterceptor(),
|
||||
] as const
|
||||
21
node_modules/@mswjs/interceptors/src/utils/bufferUtils.test.ts
generated
vendored
Normal file
21
node_modules/@mswjs/interceptors/src/utils/bufferUtils.test.ts
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
import { it, expect } from 'vitest'
|
||||
import { decodeBuffer, encodeBuffer } from './bufferUtils'
|
||||
|
||||
it('encodes utf-8 string', () => {
|
||||
const encoded = encodeBuffer('😁')
|
||||
expect(new Uint8Array(encoded)).toEqual(new Uint8Array([240, 159, 152, 129]))
|
||||
})
|
||||
|
||||
it('decodes utf-8 string', () => {
|
||||
const array = new Uint8Array([240, 159, 152, 129])
|
||||
const decoded = decodeBuffer(array.buffer)
|
||||
expect(decoded).toEqual('😁')
|
||||
})
|
||||
|
||||
it('decodes string with custom encoding', () => {
|
||||
const array = new Uint8Array([
|
||||
207, 240, 232, 226, 229, 242, 44, 32, 236, 232, 240, 33,
|
||||
])
|
||||
const decoded = decodeBuffer(array.buffer, 'windows-1251')
|
||||
expect(decoded).toEqual('Привет, мир!')
|
||||
})
|
||||
22
node_modules/@mswjs/interceptors/src/utils/bufferUtils.ts
generated
vendored
Normal file
22
node_modules/@mswjs/interceptors/src/utils/bufferUtils.ts
generated
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
const encoder = new TextEncoder()
|
||||
|
||||
export function encodeBuffer(text: string): Uint8Array {
|
||||
return encoder.encode(text)
|
||||
}
|
||||
|
||||
export function decodeBuffer(buffer: ArrayBuffer, encoding?: string): string {
|
||||
const decoder = new TextDecoder(encoding)
|
||||
return decoder.decode(buffer)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an `ArrayBuffer` from the given `Uint8Array`.
|
||||
* Takes the byte offset into account to produce the right buffer
|
||||
* in the case when the buffer is bigger than the data view.
|
||||
*/
|
||||
export function toArrayBuffer(array: Uint8Array): ArrayBuffer {
|
||||
return array.buffer.slice(
|
||||
array.byteOffset,
|
||||
array.byteOffset + array.byteLength
|
||||
)
|
||||
}
|
||||
13
node_modules/@mswjs/interceptors/src/utils/canParseUrl.ts
generated
vendored
Normal file
13
node_modules/@mswjs/interceptors/src/utils/canParseUrl.ts
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Returns a boolean indicating whether the given URL string
|
||||
* can be parsed into a `URL` instance.
|
||||
* A substitute for `URL.canParse()` for Node.js 18.
|
||||
*/
|
||||
export function canParseUrl(url: string): boolean {
|
||||
try {
|
||||
new URL(url)
|
||||
return true
|
||||
} catch (_error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
94
node_modules/@mswjs/interceptors/src/utils/cloneObject.test.ts
generated
vendored
Normal file
94
node_modules/@mswjs/interceptors/src/utils/cloneObject.test.ts
generated
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
import { it, expect } from 'vitest'
|
||||
import { cloneObject } from './cloneObject'
|
||||
|
||||
it('clones a shallow object', () => {
|
||||
const original = { a: 1, b: 2, c: [1, 2, 3] }
|
||||
const clone = cloneObject(original)
|
||||
|
||||
expect(clone).toEqual(original)
|
||||
|
||||
clone.a = 5
|
||||
clone.b = 6
|
||||
clone.c = [5, 6, 7]
|
||||
|
||||
expect(clone).toHaveProperty('a', 5)
|
||||
expect(clone).toHaveProperty('b', 6)
|
||||
expect(clone).toHaveProperty('c', [5, 6, 7])
|
||||
expect(original).toHaveProperty('a', 1)
|
||||
expect(original).toHaveProperty('b', 2)
|
||||
expect(original).toHaveProperty('c', [1, 2, 3])
|
||||
})
|
||||
|
||||
it('clones a nested object', () => {
|
||||
const original = { a: { b: 1 }, c: { d: { e: 2 } } }
|
||||
const clone = cloneObject(original)
|
||||
|
||||
expect(clone).toEqual(original)
|
||||
|
||||
clone.a.b = 10
|
||||
clone.c.d.e = 20
|
||||
|
||||
expect(clone).toHaveProperty(['a', 'b'], 10)
|
||||
expect(clone).toHaveProperty(['c', 'd', 'e'], 20)
|
||||
expect(original).toHaveProperty(['a', 'b'], 1)
|
||||
expect(original).toHaveProperty(['c', 'd', 'e'], 2)
|
||||
})
|
||||
|
||||
it('clones a class instance', () => {
|
||||
class Car {
|
||||
public manufacturer: string
|
||||
constructor() {
|
||||
this.manufacturer = 'Audi'
|
||||
}
|
||||
getManufacturer() {
|
||||
return this.manufacturer
|
||||
}
|
||||
}
|
||||
|
||||
const car = new Car()
|
||||
const clone = cloneObject(car)
|
||||
|
||||
expect(clone).toHaveProperty('manufacturer', 'Audi')
|
||||
expect(clone).toHaveProperty('getManufacturer')
|
||||
expect(clone.getManufacturer).toBeInstanceOf(Function)
|
||||
expect(clone.getManufacturer()).toEqual('Audi')
|
||||
})
|
||||
|
||||
it('ignores nested class instances', () => {
|
||||
class Car {
|
||||
name: string
|
||||
constructor(name: string) {
|
||||
this.name = name
|
||||
}
|
||||
getName() {
|
||||
return this.name
|
||||
}
|
||||
}
|
||||
const original = {
|
||||
a: 1,
|
||||
car: new Car('Audi'),
|
||||
}
|
||||
const clone = cloneObject(original)
|
||||
|
||||
expect(clone).toEqual(original)
|
||||
expect(clone.car).toBeInstanceOf(Car)
|
||||
expect(clone.car.getName()).toEqual('Audi')
|
||||
|
||||
clone.car = new Car('BMW')
|
||||
|
||||
expect(clone.car).toBeInstanceOf(Car)
|
||||
expect(clone.car.getName()).toEqual('BMW')
|
||||
expect(original.car).toBeInstanceOf(Car)
|
||||
expect(original.car.getName()).toEqual('Audi')
|
||||
})
|
||||
|
||||
it('clones an object with null prototype', () => {
|
||||
const original = {
|
||||
key: Object.create(null),
|
||||
}
|
||||
const clone = cloneObject(original)
|
||||
|
||||
expect(clone).toEqual({
|
||||
key: {},
|
||||
})
|
||||
})
|
||||
36
node_modules/@mswjs/interceptors/src/utils/cloneObject.ts
generated
vendored
Normal file
36
node_modules/@mswjs/interceptors/src/utils/cloneObject.ts
generated
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Logger } from '@open-draft/logger'
|
||||
|
||||
const logger = new Logger('cloneObject')
|
||||
|
||||
function isPlainObject(obj?: Record<string, any>): boolean {
|
||||
logger.info('is plain object?', obj)
|
||||
|
||||
if (obj == null || !obj.constructor?.name) {
|
||||
logger.info('given object is undefined, not a plain object...')
|
||||
return false
|
||||
}
|
||||
|
||||
logger.info('checking the object constructor:', obj.constructor.name)
|
||||
return obj.constructor.name === 'Object'
|
||||
}
|
||||
|
||||
export function cloneObject<ObjectType extends Record<string, any>>(
|
||||
obj: ObjectType
|
||||
): ObjectType {
|
||||
logger.info('cloning object:', obj)
|
||||
|
||||
const enumerableProperties = Object.entries(obj).reduce<Record<string, any>>(
|
||||
(acc, [key, value]) => {
|
||||
logger.info('analyzing key-value pair:', key, value)
|
||||
|
||||
// Recursively clone only plain objects, omitting class instances.
|
||||
acc[key] = isPlainObject(value) ? cloneObject(value) : value
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
return isPlainObject(obj)
|
||||
? enumerableProperties
|
||||
: Object.assign(Object.getPrototypeOf(obj), enumerableProperties)
|
||||
}
|
||||
164
node_modules/@mswjs/interceptors/src/utils/createProxy.test.ts
generated
vendored
Normal file
164
node_modules/@mswjs/interceptors/src/utils/createProxy.test.ts
generated
vendored
Normal file
@@ -0,0 +1,164 @@
|
||||
import { vi, it, expect } from 'vitest'
|
||||
import { createProxy } from './createProxy'
|
||||
|
||||
it('does not interfere with default constructors', () => {
|
||||
const ProxyClass = createProxy(
|
||||
class {
|
||||
constructor(public name: string) {}
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
const instance = new ProxyClass('John')
|
||||
expect(instance.name).toBe('John')
|
||||
})
|
||||
|
||||
it('does not interfere with default getters', () => {
|
||||
const proxy = createProxy({ foo: 'initial' }, {})
|
||||
expect(proxy.foo).toBe('initial')
|
||||
})
|
||||
|
||||
it('does not interfere with default setters', () => {
|
||||
const proxy = createProxy({ foo: 'initial' }, {})
|
||||
proxy.foo = 'next'
|
||||
|
||||
expect(proxy.foo).toBe('next')
|
||||
})
|
||||
|
||||
it('does not interfere with default methods', () => {
|
||||
const proxy = createProxy({ getValue: () => 'initial' }, {})
|
||||
expect(proxy.getValue()).toBe('initial')
|
||||
})
|
||||
|
||||
it('does not interfere with existing descriptors', () => {
|
||||
const target = {} as { foo: string; bar: number }
|
||||
let internalBar = 0
|
||||
|
||||
Object.defineProperties(target, {
|
||||
foo: {
|
||||
get: () => 'initial',
|
||||
},
|
||||
bar: {
|
||||
set: (value) => {
|
||||
internalBar = value + 10
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const proxy = createProxy(target, {
|
||||
getProperty(data, next) {
|
||||
return next()
|
||||
},
|
||||
})
|
||||
expect(proxy.foo).toBe('initial')
|
||||
|
||||
proxy.bar = 5
|
||||
expect(proxy.bar).toBeUndefined()
|
||||
expect(internalBar).toBe(15)
|
||||
})
|
||||
|
||||
it('infer prototype descriptors', () => {
|
||||
class Child {
|
||||
ok: boolean
|
||||
|
||||
set status(nextStatus: number) {
|
||||
this.ok = nextStatus >= 200 && nextStatus < 300
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperties(Child.prototype, {
|
||||
status: { enumerable: true },
|
||||
})
|
||||
|
||||
const scope = {} as { child: typeof Child }
|
||||
|
||||
Object.defineProperty(scope, 'child', {
|
||||
enumerable: true,
|
||||
value: Child,
|
||||
})
|
||||
|
||||
const ProxyClass = createProxy(scope.child, {})
|
||||
const instance = new ProxyClass()
|
||||
|
||||
instance.status = 201
|
||||
expect(instance.ok).toBe(true)
|
||||
})
|
||||
|
||||
it('spies on the constructor', () => {
|
||||
const OriginalClass = class {
|
||||
constructor(public name: string, public age: number) {}
|
||||
}
|
||||
|
||||
const constructorCall = vi.fn<
|
||||
(
|
||||
args: ConstructorParameters<typeof OriginalClass>,
|
||||
next: () => typeof OriginalClass
|
||||
) => typeof OriginalClass
|
||||
>((args, next) => next())
|
||||
|
||||
const ProxyClass = createProxy(OriginalClass, {
|
||||
constructorCall,
|
||||
})
|
||||
|
||||
new ProxyClass('John', 32)
|
||||
|
||||
expect(constructorCall).toHaveBeenCalledTimes(1)
|
||||
expect(constructorCall).toHaveBeenCalledWith(
|
||||
['John', 32],
|
||||
expect.any(Function)
|
||||
)
|
||||
})
|
||||
|
||||
it('spies on property getters', () => {
|
||||
const getProperty = vi.fn((args, next) => next())
|
||||
const proxy = createProxy({ foo: 'initial' }, { getProperty })
|
||||
|
||||
proxy.foo
|
||||
|
||||
expect(getProperty).toHaveBeenCalledTimes(1)
|
||||
expect(getProperty).toHaveBeenCalledWith(['foo', proxy], expect.any(Function))
|
||||
})
|
||||
|
||||
it('spies on property setters', () => {
|
||||
const setProperty = vi.fn((args, next) => next())
|
||||
const proxy = createProxy({ foo: 'initial' }, { setProperty })
|
||||
|
||||
proxy.foo = 'next'
|
||||
|
||||
expect(setProperty).toHaveBeenCalledTimes(1)
|
||||
expect(setProperty).toHaveBeenCalledWith(
|
||||
['foo', 'next'],
|
||||
expect.any(Function)
|
||||
)
|
||||
})
|
||||
|
||||
it('spies on method calls', () => {
|
||||
const methodCall = vi.fn((args, next) => next())
|
||||
const proxy = createProxy(
|
||||
{
|
||||
greet: (name: string) => `hello ${name}`,
|
||||
},
|
||||
{ methodCall }
|
||||
)
|
||||
|
||||
proxy.greet('Clair')
|
||||
|
||||
expect(methodCall).toHaveBeenCalledTimes(1)
|
||||
expect(methodCall).toHaveBeenCalledWith(
|
||||
['greet', ['Clair']],
|
||||
expect.any(Function)
|
||||
)
|
||||
})
|
||||
|
||||
it('proxies properties on the prototype level', () => {
|
||||
const method = vi.fn()
|
||||
const prototype = { method }
|
||||
|
||||
const proxy = createProxy(Object.create(prototype), {})
|
||||
const proxyMethod = vi.fn()
|
||||
proxy.method = proxyMethod
|
||||
|
||||
prototype.method()
|
||||
expect(method).toHaveBeenCalledTimes(0)
|
||||
expect(proxyMethod).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
104
node_modules/@mswjs/interceptors/src/utils/createProxy.ts
generated
vendored
Normal file
104
node_modules/@mswjs/interceptors/src/utils/createProxy.ts
generated
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
import { findPropertySource } from './findPropertySource'
|
||||
|
||||
export interface ProxyOptions<Target extends Record<string, any>> {
|
||||
constructorCall?(args: Array<unknown>, next: NextFunction<Target>): Target
|
||||
|
||||
methodCall?<F extends keyof Target>(
|
||||
this: Target,
|
||||
data: [methodName: F, args: Array<unknown>],
|
||||
next: NextFunction<void>
|
||||
): void
|
||||
|
||||
setProperty?(
|
||||
data: [propertyName: string | symbol, nextValue: unknown],
|
||||
next: NextFunction<boolean>
|
||||
): boolean
|
||||
|
||||
getProperty?(
|
||||
data: [propertyName: string | symbol, receiver: Target],
|
||||
next: NextFunction<void>
|
||||
): void
|
||||
}
|
||||
|
||||
export type NextFunction<ReturnType> = () => ReturnType
|
||||
|
||||
export function createProxy<Target extends object>(
|
||||
target: Target,
|
||||
options: ProxyOptions<Target>
|
||||
): Target {
|
||||
const proxy = new Proxy(target, optionsToProxyHandler(options))
|
||||
|
||||
return proxy
|
||||
}
|
||||
|
||||
function optionsToProxyHandler<T extends Record<string, any>>(
|
||||
options: ProxyOptions<T>
|
||||
): ProxyHandler<T> {
|
||||
const { constructorCall, methodCall, getProperty, setProperty } = options
|
||||
const handler: ProxyHandler<T> = {}
|
||||
|
||||
if (typeof constructorCall !== 'undefined') {
|
||||
handler.construct = function (target, args, newTarget) {
|
||||
const next = Reflect.construct.bind(null, target as any, args, newTarget)
|
||||
return constructorCall.call(newTarget, args, next)
|
||||
}
|
||||
}
|
||||
|
||||
handler.set = function (target, propertyName, nextValue) {
|
||||
const next = () => {
|
||||
const propertySource = findPropertySource(target, propertyName) || target
|
||||
const ownDescriptors = Reflect.getOwnPropertyDescriptor(
|
||||
propertySource,
|
||||
propertyName
|
||||
)
|
||||
|
||||
// Respect any custom setters present for this property.
|
||||
if (typeof ownDescriptors?.set !== 'undefined') {
|
||||
ownDescriptors.set.apply(target, [nextValue])
|
||||
return true
|
||||
}
|
||||
|
||||
// Otherwise, set the property on the source.
|
||||
return Reflect.defineProperty(propertySource, propertyName, {
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
value: nextValue,
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof setProperty !== 'undefined') {
|
||||
return setProperty.call(target, [propertyName, nextValue], next)
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
handler.get = function (target, propertyName, receiver) {
|
||||
/**
|
||||
* @note Using `Reflect.get()` here causes "TypeError: Illegal invocation".
|
||||
*/
|
||||
const next = () => target[propertyName as any]
|
||||
|
||||
const value =
|
||||
typeof getProperty !== 'undefined'
|
||||
? getProperty.call(target, [propertyName, receiver], next)
|
||||
: next()
|
||||
|
||||
if (typeof value === 'function') {
|
||||
return (...args: Array<any>) => {
|
||||
const next = value.bind(target, ...args)
|
||||
|
||||
if (typeof methodCall !== 'undefined') {
|
||||
return methodCall.call(target, [propertyName as any, args], next)
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
return handler
|
||||
}
|
||||
25
node_modules/@mswjs/interceptors/src/utils/emitAsync.ts
generated
vendored
Normal file
25
node_modules/@mswjs/interceptors/src/utils/emitAsync.ts
generated
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Emitter, EventMap } from 'strict-event-emitter'
|
||||
|
||||
/**
|
||||
* Emits an event on the given emitter but executes
|
||||
* the listeners sequentially. This accounts for asynchronous
|
||||
* listeners (e.g. those having "sleep" and handling the request).
|
||||
*/
|
||||
export async function emitAsync<
|
||||
Events extends EventMap,
|
||||
EventName extends keyof Events
|
||||
>(
|
||||
emitter: Emitter<Events>,
|
||||
eventName: EventName,
|
||||
...data: Events[EventName]
|
||||
): Promise<void> {
|
||||
const listeners = emitter.listeners(eventName)
|
||||
|
||||
if (listeners.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const listener of listeners) {
|
||||
await listener.apply(emitter, data)
|
||||
}
|
||||
}
|
||||
119
node_modules/@mswjs/interceptors/src/utils/fetchUtils.ts
generated
vendored
Normal file
119
node_modules/@mswjs/interceptors/src/utils/fetchUtils.ts
generated
vendored
Normal file
@@ -0,0 +1,119 @@
|
||||
import { canParseUrl } from './canParseUrl'
|
||||
import { getValueBySymbol } from './getValueBySymbol'
|
||||
|
||||
export interface FetchResponseInit extends ResponseInit {
|
||||
url?: string
|
||||
}
|
||||
|
||||
interface UndiciFetchInternalState {
|
||||
aborted: boolean
|
||||
rangeRequested: boolean
|
||||
timingAllowPassed: boolean
|
||||
requestIncludesCredentials: boolean
|
||||
type: ResponseType
|
||||
status: number
|
||||
statusText: string
|
||||
timingInfo: unknown
|
||||
cacheState: unknown
|
||||
headersList: Record<symbol, Map<string, unknown>>
|
||||
urlList: Array<URL>
|
||||
body?: {
|
||||
stream: ReadableStream
|
||||
source: unknown
|
||||
length: number
|
||||
}
|
||||
}
|
||||
|
||||
export class FetchResponse extends Response {
|
||||
/**
|
||||
* Response status codes for responses that cannot have body.
|
||||
* @see https://fetch.spec.whatwg.org/#statuses
|
||||
*/
|
||||
static readonly STATUS_CODES_WITHOUT_BODY = [101, 103, 204, 205, 304]
|
||||
|
||||
static readonly STATUS_CODES_WITH_REDIRECT = [301, 302, 303, 307, 308]
|
||||
|
||||
static isConfigurableStatusCode(status: number): boolean {
|
||||
return status >= 200 && status <= 599
|
||||
}
|
||||
|
||||
static isRedirectResponse(status: number): boolean {
|
||||
return FetchResponse.STATUS_CODES_WITH_REDIRECT.includes(status)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean indicating whether the given response status
|
||||
* code represents a response that can have a body.
|
||||
*/
|
||||
static isResponseWithBody(status: number): boolean {
|
||||
return !FetchResponse.STATUS_CODES_WITHOUT_BODY.includes(status)
|
||||
}
|
||||
|
||||
static setUrl(url: string | undefined, response: Response): void {
|
||||
if (!url || url === 'about:' || !canParseUrl(url)) {
|
||||
return
|
||||
}
|
||||
|
||||
const state = getValueBySymbol<UndiciFetchInternalState>('state', response)
|
||||
|
||||
if (state) {
|
||||
// In Undici, push the URL to the internal list of URLs.
|
||||
// This will respect the `response.url` getter logic correctly.
|
||||
state.urlList.push(new URL(url))
|
||||
} else {
|
||||
// In other libraries, redefine the `url` property directly.
|
||||
Object.defineProperty(response, 'url', {
|
||||
value: url,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the given raw HTTP headers into a Fetch API `Headers` instance.
|
||||
*/
|
||||
static parseRawHeaders(rawHeaders: Array<string>): Headers {
|
||||
const headers = new Headers()
|
||||
for (let line = 0; line < rawHeaders.length; line += 2) {
|
||||
headers.append(rawHeaders[line], rawHeaders[line + 1])
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
constructor(body?: BodyInit | null, init: FetchResponseInit = {}) {
|
||||
const status = init.status ?? 200
|
||||
const safeStatus = FetchResponse.isConfigurableStatusCode(status)
|
||||
? status
|
||||
: 200
|
||||
const finalBody = FetchResponse.isResponseWithBody(status) ? body : null
|
||||
|
||||
super(finalBody, {
|
||||
status: safeStatus,
|
||||
statusText: init.statusText,
|
||||
headers: init.headers,
|
||||
})
|
||||
|
||||
if (status !== safeStatus) {
|
||||
/**
|
||||
* @note Undici keeps an internal "Symbol(state)" that holds
|
||||
* the actual value of response status. Update that in Node.js.
|
||||
*/
|
||||
const state = getValueBySymbol<UndiciFetchInternalState>('state', this)
|
||||
|
||||
if (state) {
|
||||
state.status = status
|
||||
} else {
|
||||
Object.defineProperty(this, 'status', {
|
||||
value: status,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
writable: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
FetchResponse.setUrl(init.url, this)
|
||||
}
|
||||
}
|
||||
27
node_modules/@mswjs/interceptors/src/utils/findPropertySource.test.ts
generated
vendored
Normal file
27
node_modules/@mswjs/interceptors/src/utils/findPropertySource.test.ts
generated
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
import { it, expect } from 'vitest'
|
||||
import { findPropertySource } from './findPropertySource'
|
||||
|
||||
it('returns the source for objects without prototypes', () => {
|
||||
const obj = Object.create(null)
|
||||
obj.test = undefined
|
||||
const source = findPropertySource(obj, 'test')
|
||||
expect(source).toBe(obj)
|
||||
})
|
||||
|
||||
it('returns the source for objects with prototypes', () => {
|
||||
const prototype = Object.create(null)
|
||||
prototype.test = undefined
|
||||
|
||||
const obj = Object.create(prototype)
|
||||
|
||||
const source = findPropertySource(obj, 'test')
|
||||
expect(source).toBe(prototype)
|
||||
})
|
||||
|
||||
it('returns null if the prototype chain does not contain the property', () => {
|
||||
const prototype = Object.create(null)
|
||||
const obj = Object.create(prototype)
|
||||
|
||||
const source = findPropertySource(obj, 'test')
|
||||
expect(source).toBeNull()
|
||||
})
|
||||
20
node_modules/@mswjs/interceptors/src/utils/findPropertySource.ts
generated
vendored
Normal file
20
node_modules/@mswjs/interceptors/src/utils/findPropertySource.ts
generated
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Returns the source object of the given property on the target object
|
||||
* (the target itself, any parent in its prototype, or null).
|
||||
*/
|
||||
export function findPropertySource(
|
||||
target: object,
|
||||
propertyName: string | symbol
|
||||
): object | null {
|
||||
if (!(propertyName in target)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const hasProperty = Object.prototype.hasOwnProperty.call(target, propertyName)
|
||||
if (hasProperty) {
|
||||
return target
|
||||
}
|
||||
|
||||
const prototype = Reflect.getPrototypeOf(target)
|
||||
return prototype ? findPropertySource(prototype, propertyName) : null
|
||||
}
|
||||
32
node_modules/@mswjs/interceptors/src/utils/getCleanUrl.test.ts
generated
vendored
Normal file
32
node_modules/@mswjs/interceptors/src/utils/getCleanUrl.test.ts
generated
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { getCleanUrl } from './getCleanUrl'
|
||||
|
||||
describe('getCleanUrl', () => {
|
||||
describe('given a URL without query parameters', () => {
|
||||
it('should return url href as-is', () => {
|
||||
const url = new URL('https://github.com')
|
||||
expect(getCleanUrl(url)).toEqual('https://github.com/')
|
||||
})
|
||||
})
|
||||
|
||||
describe('given a URL with query parameters', () => {
|
||||
it('should return url without parameters', () => {
|
||||
const url = new URL('https://github.com/mswjs/?userId=abc-123')
|
||||
expect(getCleanUrl(url)).toEqual('https://github.com/mswjs/')
|
||||
})
|
||||
})
|
||||
|
||||
describe('given a URL with a hash', () => {
|
||||
it('should return a url without hash', () => {
|
||||
const url = new URL('https://github.com/mswjs/#hello-world')
|
||||
expect(getCleanUrl(url)).toEqual('https://github.com/mswjs/')
|
||||
})
|
||||
})
|
||||
|
||||
describe('given an absolute URL ', () => {
|
||||
it('should return a clean relative URL', () => {
|
||||
const url = new URL('/login?query=value', 'https://github.com')
|
||||
expect(getCleanUrl(url, false)).toEqual('/login')
|
||||
})
|
||||
})
|
||||
})
|
||||
6
node_modules/@mswjs/interceptors/src/utils/getCleanUrl.ts
generated
vendored
Normal file
6
node_modules/@mswjs/interceptors/src/utils/getCleanUrl.ts
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Removes query parameters and hashes from a given URL.
|
||||
*/
|
||||
export function getCleanUrl(url: URL, isAbsolute: boolean = true): string {
|
||||
return [isAbsolute && url.origin, url.pathname].filter(Boolean).join('')
|
||||
}
|
||||
163
node_modules/@mswjs/interceptors/src/utils/getUrlByRequestOptions.test.ts
generated
vendored
Normal file
163
node_modules/@mswjs/interceptors/src/utils/getUrlByRequestOptions.test.ts
generated
vendored
Normal file
@@ -0,0 +1,163 @@
|
||||
import { it, expect } from 'vitest'
|
||||
import { Agent as HttpAgent } from 'http'
|
||||
import { RequestOptions, Agent as HttpsAgent } from 'https'
|
||||
import { getUrlByRequestOptions } from './getUrlByRequestOptions'
|
||||
|
||||
it('returns a URL based on the basic RequestOptions', () => {
|
||||
expect(
|
||||
getUrlByRequestOptions({
|
||||
protocol: 'https:',
|
||||
host: '127.0.0.1',
|
||||
path: '/resource',
|
||||
}).href
|
||||
).toBe('https://127.0.0.1/resource')
|
||||
})
|
||||
|
||||
it('inherits protocol and port from http.Agent, if set', () => {
|
||||
expect(
|
||||
getUrlByRequestOptions({
|
||||
host: '127.0.0.1',
|
||||
path: '/',
|
||||
agent: new HttpAgent(),
|
||||
}).href
|
||||
).toBe('http://127.0.0.1/')
|
||||
})
|
||||
|
||||
it('inherits protocol and port from https.Agent, if set', () => {
|
||||
expect(
|
||||
getUrlByRequestOptions({
|
||||
host: '127.0.0.1',
|
||||
path: '/',
|
||||
agent: new HttpsAgent({
|
||||
port: 3080,
|
||||
}),
|
||||
}).href
|
||||
).toBe('https://127.0.0.1:3080/')
|
||||
})
|
||||
|
||||
it('resolves protocol to "http" given no explicit protocol and no certificate', () => {
|
||||
expect(
|
||||
getUrlByRequestOptions({
|
||||
host: '127.0.0.1',
|
||||
path: '/',
|
||||
}).href
|
||||
).toBe('http://127.0.0.1/')
|
||||
})
|
||||
|
||||
it('resolves protocol to "https" given no explicit protocol, but certificate', () => {
|
||||
expect(
|
||||
getUrlByRequestOptions({
|
||||
host: '127.0.0.1',
|
||||
path: '/secure',
|
||||
cert: '<!-- SSL certificate -->',
|
||||
}).href
|
||||
).toBe('https://127.0.0.1/secure')
|
||||
})
|
||||
|
||||
it('resolves protocol to "https" given no explicit protocol, but port is 443', () => {
|
||||
expect(
|
||||
getUrlByRequestOptions({
|
||||
host: '127.0.0.1',
|
||||
port: 443,
|
||||
path: '/resource',
|
||||
}).href
|
||||
).toBe('https://127.0.0.1/resource')
|
||||
})
|
||||
|
||||
it('resolves protocol to "https" given no explicit protocol, but agent port is 443', () => {
|
||||
expect(
|
||||
getUrlByRequestOptions({
|
||||
host: '127.0.0.1',
|
||||
agent: new HttpsAgent({
|
||||
port: 443,
|
||||
}),
|
||||
path: '/resource',
|
||||
}).href
|
||||
).toBe('https://127.0.0.1/resource')
|
||||
})
|
||||
|
||||
it('respects explicitly provided port', () => {
|
||||
expect(
|
||||
getUrlByRequestOptions({
|
||||
protocol: 'http:',
|
||||
host: '127.0.0.1',
|
||||
port: 4002,
|
||||
path: '/',
|
||||
}).href
|
||||
).toBe('http://127.0.0.1:4002/')
|
||||
})
|
||||
|
||||
it('inherits "username" and "password"', () => {
|
||||
const url = getUrlByRequestOptions({
|
||||
protocol: 'https:',
|
||||
host: '127.0.0.1',
|
||||
path: '/user',
|
||||
auth: 'admin:abc-123',
|
||||
})
|
||||
|
||||
expect(url).toBeInstanceOf(URL)
|
||||
expect(url).toHaveProperty('username', 'admin')
|
||||
expect(url).toHaveProperty('password', 'abc-123')
|
||||
expect(url).toHaveProperty('href', 'https://admin:abc-123@127.0.0.1/user')
|
||||
})
|
||||
|
||||
it('resolves hostname to localhost if none provided', () => {
|
||||
expect(getUrlByRequestOptions({}).hostname).toBe('localhost')
|
||||
})
|
||||
|
||||
it('resolves host to localhost if none provided', () => {
|
||||
expect(getUrlByRequestOptions({}).host).toBe('localhost')
|
||||
})
|
||||
|
||||
it('supports "hostname" and "port"', () => {
|
||||
const options: RequestOptions = {
|
||||
protocol: 'https:',
|
||||
hostname: '127.0.0.1',
|
||||
port: 1234,
|
||||
path: '/resource',
|
||||
}
|
||||
|
||||
expect(getUrlByRequestOptions(options).href).toBe(
|
||||
'https://127.0.0.1:1234/resource'
|
||||
)
|
||||
})
|
||||
|
||||
it('use "hostname" if both "hostname" and "host" are specified', () => {
|
||||
const options: RequestOptions = {
|
||||
protocol: 'https:',
|
||||
host: 'host',
|
||||
hostname: 'hostname',
|
||||
path: '/resource',
|
||||
}
|
||||
|
||||
expect(getUrlByRequestOptions(options).href).toBe(
|
||||
'https://hostname/resource'
|
||||
)
|
||||
})
|
||||
|
||||
it('parses "host" in IPv6', () => {
|
||||
expect(
|
||||
getUrlByRequestOptions({
|
||||
host: '::1',
|
||||
path: '/resource',
|
||||
}).href
|
||||
).toBe('http://[::1]/resource')
|
||||
|
||||
expect(
|
||||
getUrlByRequestOptions({
|
||||
host: '[::1]',
|
||||
path: '/resource',
|
||||
}).href
|
||||
).toBe('http://[::1]/resource')
|
||||
|
||||
})
|
||||
|
||||
it('parses "host" and "port" in IPv6', () => {
|
||||
expect(
|
||||
getUrlByRequestOptions({
|
||||
host: '::1',
|
||||
port: 3001,
|
||||
path: '/resource',
|
||||
}).href
|
||||
).toBe('http://[::1]:3001/resource')
|
||||
})
|
||||
152
node_modules/@mswjs/interceptors/src/utils/getUrlByRequestOptions.ts
generated
vendored
Normal file
152
node_modules/@mswjs/interceptors/src/utils/getUrlByRequestOptions.ts
generated
vendored
Normal file
@@ -0,0 +1,152 @@
|
||||
import { Agent } from 'http'
|
||||
import { RequestOptions, Agent as HttpsAgent } from 'https'
|
||||
import { Logger } from '@open-draft/logger'
|
||||
|
||||
const logger = new Logger('utils getUrlByRequestOptions')
|
||||
|
||||
// Request instance constructed by the "request" library
|
||||
// has a "self" property that has a "uri" field. This is
|
||||
// reproducible by performing a "XMLHttpRequest" request in JSDOM.
|
||||
export interface RequestSelf {
|
||||
uri?: URL
|
||||
}
|
||||
|
||||
export type ResolvedRequestOptions = RequestOptions & RequestSelf
|
||||
|
||||
export const DEFAULT_PATH = '/'
|
||||
const DEFAULT_PROTOCOL = 'http:'
|
||||
const DEFAULT_HOSTNAME = 'localhost'
|
||||
const SSL_PORT = 443
|
||||
|
||||
function getAgent(
|
||||
options: ResolvedRequestOptions
|
||||
): Agent | HttpsAgent | undefined {
|
||||
return options.agent instanceof Agent ? options.agent : undefined
|
||||
}
|
||||
|
||||
function getProtocolByRequestOptions(options: ResolvedRequestOptions): string {
|
||||
if (options.protocol) {
|
||||
return options.protocol
|
||||
}
|
||||
|
||||
const agent = getAgent(options)
|
||||
const agentProtocol = (agent as RequestOptions)?.protocol
|
||||
|
||||
if (agentProtocol) {
|
||||
return agentProtocol
|
||||
}
|
||||
|
||||
const port = getPortByRequestOptions(options)
|
||||
const isSecureRequest = options.cert || port === SSL_PORT
|
||||
|
||||
return isSecureRequest ? 'https:' : options.uri?.protocol || DEFAULT_PROTOCOL
|
||||
}
|
||||
|
||||
function getPortByRequestOptions(
|
||||
options: ResolvedRequestOptions
|
||||
): number | undefined {
|
||||
// Use the explicitly provided port.
|
||||
if (options.port) {
|
||||
return Number(options.port)
|
||||
}
|
||||
|
||||
// Otherwise, try to resolve port from the agent.
|
||||
const agent = getAgent(options)
|
||||
|
||||
if ((agent as HttpsAgent)?.options.port) {
|
||||
return Number((agent as HttpsAgent).options.port)
|
||||
}
|
||||
|
||||
if ((agent as RequestOptions)?.defaultPort) {
|
||||
return Number((agent as RequestOptions).defaultPort)
|
||||
}
|
||||
|
||||
// Lastly, return undefined indicating that the port
|
||||
// must inferred from the protocol. Do not infer it here.
|
||||
return undefined
|
||||
}
|
||||
|
||||
interface RequestAuth {
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
function getAuthByRequestOptions(
|
||||
options: ResolvedRequestOptions
|
||||
): RequestAuth | undefined {
|
||||
if (options.auth) {
|
||||
const [username, password] = options.auth.split(':')
|
||||
return { username, password }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if host looks like an IPv6 address without surrounding brackets
|
||||
* It assumes any host containing `:` is definitely not IPv4 and probably IPv6,
|
||||
* but note that this could include invalid IPv6 addresses as well.
|
||||
*/
|
||||
function isRawIPv6Address(host: string): boolean {
|
||||
return host.includes(':') && !host.startsWith('[') && !host.endsWith(']')
|
||||
}
|
||||
|
||||
function getHostname(options: ResolvedRequestOptions): string | undefined {
|
||||
let host = options.hostname || options.host
|
||||
|
||||
if (host) {
|
||||
if (isRawIPv6Address(host)) {
|
||||
host = `[${host}]`
|
||||
}
|
||||
|
||||
// Check the presence of the port, and if it's present,
|
||||
// remove it from the host, returning a hostname.
|
||||
return new URL(`http://${host}`).hostname
|
||||
}
|
||||
|
||||
return DEFAULT_HOSTNAME
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a `URL` instance from a given `RequestOptions` object.
|
||||
*/
|
||||
export function getUrlByRequestOptions(options: ResolvedRequestOptions): URL {
|
||||
logger.info('request options', options)
|
||||
|
||||
if (options.uri) {
|
||||
logger.info(
|
||||
'constructing url from explicitly provided "options.uri": %s',
|
||||
options.uri
|
||||
)
|
||||
return new URL(options.uri.href)
|
||||
}
|
||||
|
||||
logger.info('figuring out url from request options...')
|
||||
|
||||
const protocol = getProtocolByRequestOptions(options)
|
||||
logger.info('protocol', protocol)
|
||||
|
||||
const port = getPortByRequestOptions(options)
|
||||
logger.info('port', port)
|
||||
|
||||
const hostname = getHostname(options)
|
||||
logger.info('hostname', hostname)
|
||||
|
||||
const path = options.path || DEFAULT_PATH
|
||||
logger.info('path', path)
|
||||
|
||||
const credentials = getAuthByRequestOptions(options)
|
||||
logger.info('credentials', credentials)
|
||||
|
||||
const authString = credentials
|
||||
? `${credentials.username}:${credentials.password}@`
|
||||
: ''
|
||||
logger.info('auth string:', authString)
|
||||
|
||||
const portString = typeof port !== 'undefined' ? `:${port}` : ''
|
||||
const url = new URL(`${protocol}//${hostname}${portString}${path}`)
|
||||
url.username = credentials?.username || ''
|
||||
url.password = credentials?.password || ''
|
||||
|
||||
logger.info('created url:', url)
|
||||
|
||||
return url
|
||||
}
|
||||
14
node_modules/@mswjs/interceptors/src/utils/getValueBySymbol.test.ts
generated
vendored
Normal file
14
node_modules/@mswjs/interceptors/src/utils/getValueBySymbol.test.ts
generated
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
import { it, expect } from 'vitest'
|
||||
import { getValueBySymbol } from './getValueBySymbol'
|
||||
|
||||
it('returns undefined given a non-existing symbol', () => {
|
||||
expect(getValueBySymbol('non-existing', {})).toBeUndefined()
|
||||
})
|
||||
|
||||
it('returns value behind the given symbol', () => {
|
||||
const symbol = Symbol('kInternal')
|
||||
|
||||
expect(getValueBySymbol('kInternal', { [symbol]: null })).toBe(null)
|
||||
expect(getValueBySymbol('kInternal', { [symbol]: true })).toBe(true)
|
||||
expect(getValueBySymbol('kInternal', { [symbol]: 'value' })).toBe('value')
|
||||
})
|
||||
19
node_modules/@mswjs/interceptors/src/utils/getValueBySymbol.ts
generated
vendored
Normal file
19
node_modules/@mswjs/interceptors/src/utils/getValueBySymbol.ts
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Returns the value behind the symbol with the given name.
|
||||
*/
|
||||
export function getValueBySymbol<T>(
|
||||
symbolName: string,
|
||||
source: object
|
||||
): T | undefined {
|
||||
const ownSymbols = Object.getOwnPropertySymbols(source)
|
||||
|
||||
const symbol = ownSymbols.find((symbol) => {
|
||||
return symbol.description === symbolName
|
||||
})
|
||||
|
||||
if (symbol) {
|
||||
return Reflect.get(source, symbol)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
205
node_modules/@mswjs/interceptors/src/utils/handleRequest.ts
generated
vendored
Normal file
205
node_modules/@mswjs/interceptors/src/utils/handleRequest.ts
generated
vendored
Normal file
@@ -0,0 +1,205 @@
|
||||
import type { Emitter } from 'strict-event-emitter'
|
||||
import { DeferredPromise } from '@open-draft/deferred-promise'
|
||||
import { until } from '@open-draft/until'
|
||||
import type { HttpRequestEventMap } from '../glossary'
|
||||
import { emitAsync } from './emitAsync'
|
||||
import { RequestController } from '../RequestController'
|
||||
import {
|
||||
createServerErrorResponse,
|
||||
isResponseError,
|
||||
isResponseLike,
|
||||
} from './responseUtils'
|
||||
import { InterceptorError } from '../InterceptorError'
|
||||
import { isNodeLikeError } from './isNodeLikeError'
|
||||
import { isObject } from './isObject'
|
||||
|
||||
interface HandleRequestOptions {
|
||||
requestId: string
|
||||
request: Request
|
||||
emitter: Emitter<HttpRequestEventMap>
|
||||
controller: RequestController
|
||||
}
|
||||
|
||||
export async function handleRequest(
|
||||
options: HandleRequestOptions
|
||||
): Promise<void> {
|
||||
const handleResponse = async (
|
||||
response: Response | Error | Record<string, any>
|
||||
) => {
|
||||
if (response instanceof Error) {
|
||||
await options.controller.errorWith(response)
|
||||
return true
|
||||
}
|
||||
|
||||
// Handle "Response.error()" instances.
|
||||
if (isResponseError(response)) {
|
||||
await options.controller.respondWith(response)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle normal responses or response-like objects.
|
||||
* @note This must come before the arbitrary object check
|
||||
* since Response instances are, in fact, objects.
|
||||
*/
|
||||
if (isResponseLike(response)) {
|
||||
await options.controller.respondWith(response)
|
||||
return true
|
||||
}
|
||||
|
||||
// Handle arbitrary objects provided to `.errorWith(reason)`.
|
||||
if (isObject(response)) {
|
||||
await options.controller.errorWith(response)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const handleResponseError = async (error: unknown): Promise<boolean> => {
|
||||
// Forward the special interceptor error instances
|
||||
// to the developer. These must not be handled in any way.
|
||||
if (error instanceof InterceptorError) {
|
||||
throw result.error
|
||||
}
|
||||
|
||||
// Support mocking Node.js-like errors.
|
||||
if (isNodeLikeError(error)) {
|
||||
await options.controller.errorWith(error)
|
||||
return true
|
||||
}
|
||||
|
||||
// Handle thrown responses.
|
||||
if (error instanceof Response) {
|
||||
return await handleResponse(error)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Add the last "request" listener to check if the request
|
||||
// has been handled in any way. If it hasn't, resolve the
|
||||
// response promise with undefined.
|
||||
// options.emitter.once('request', async ({ requestId: pendingRequestId }) => {
|
||||
// if (
|
||||
// pendingRequestId === options.requestId &&
|
||||
// options.controller.readyState === RequestController.PENDING
|
||||
// ) {
|
||||
// await options.controller.passthrough()
|
||||
// }
|
||||
// })
|
||||
|
||||
const requestAbortPromise = new DeferredPromise<void, unknown>()
|
||||
|
||||
/**
|
||||
* @note `signal` is not always defined in React Native.
|
||||
*/
|
||||
if (options.request.signal) {
|
||||
if (options.request.signal.aborted) {
|
||||
await options.controller.errorWith(options.request.signal.reason)
|
||||
return
|
||||
}
|
||||
|
||||
options.request.signal.addEventListener(
|
||||
'abort',
|
||||
() => {
|
||||
requestAbortPromise.reject(options.request.signal.reason)
|
||||
},
|
||||
{ once: true }
|
||||
)
|
||||
}
|
||||
|
||||
const result = await until(async () => {
|
||||
// Emit the "request" event and wait until all the listeners
|
||||
// for that event are finished (e.g. async listeners awaited).
|
||||
// By the end of this promise, the developer cannot affect the
|
||||
// request anymore.
|
||||
const requestListenersPromise = emitAsync(options.emitter, 'request', {
|
||||
requestId: options.requestId,
|
||||
request: options.request,
|
||||
controller: options.controller,
|
||||
})
|
||||
|
||||
await Promise.race([
|
||||
// Short-circuit the request handling promise if the request gets aborted.
|
||||
requestAbortPromise,
|
||||
requestListenersPromise,
|
||||
options.controller.handled,
|
||||
])
|
||||
})
|
||||
|
||||
// Handle the request being aborted while waiting for the request listeners.
|
||||
if (requestAbortPromise.state === 'rejected') {
|
||||
await options.controller.errorWith(requestAbortPromise.rejectionReason)
|
||||
return
|
||||
}
|
||||
|
||||
if (result.error) {
|
||||
// Handle the error during the request listener execution.
|
||||
// These can be thrown responses or request errors.
|
||||
if (await handleResponseError(result.error)) {
|
||||
return
|
||||
}
|
||||
|
||||
// If the developer has added "unhandledException" listeners,
|
||||
// allow them to handle the error. They can translate it to a
|
||||
// mocked response, network error, or forward it as-is.
|
||||
if (options.emitter.listenerCount('unhandledException') > 0) {
|
||||
// Create a new request controller just for the unhandled exception case.
|
||||
// This is needed because the original controller might have been already
|
||||
// interacted with (e.g. "respondWith" or "errorWith" called on it).
|
||||
const unhandledExceptionController = new RequestController(
|
||||
options.request,
|
||||
{
|
||||
/**
|
||||
* @note Intentionally empty passthrough handle.
|
||||
* This controller is created within another controller and we only need
|
||||
* to know if `unhandledException` listeners handled the request.
|
||||
*/
|
||||
passthrough() {},
|
||||
async respondWith(response) {
|
||||
await handleResponse(response)
|
||||
},
|
||||
async errorWith(reason) {
|
||||
/**
|
||||
* @note Handle the result of the unhandled controller
|
||||
* in the same way as the original request controller.
|
||||
* The exception here is that thrown errors within the
|
||||
* "unhandledException" event do NOT result in another
|
||||
* emit of the same event. They are forwarded as-is.
|
||||
*/
|
||||
await options.controller.errorWith(reason)
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
await emitAsync(options.emitter, 'unhandledException', {
|
||||
error: result.error,
|
||||
request: options.request,
|
||||
requestId: options.requestId,
|
||||
controller: unhandledExceptionController,
|
||||
})
|
||||
|
||||
// If all the "unhandledException" listeners have finished
|
||||
// but have not handled the request in any way, passthrough.
|
||||
if (
|
||||
unhandledExceptionController.readyState !== RequestController.PENDING
|
||||
) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, coerce unhandled exceptions to a 500 Internal Server Error response.
|
||||
await options.controller.respondWith(
|
||||
createServerErrorResponse(result.error)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// If the request hasn't been handled by this point, passthrough.
|
||||
if (options.controller.readyState === RequestController.PENDING) {
|
||||
return await options.controller.passthrough()
|
||||
}
|
||||
|
||||
return options.controller.handled
|
||||
}
|
||||
83
node_modules/@mswjs/interceptors/src/utils/hasConfigurableGlobal.test.ts
generated
vendored
Normal file
83
node_modules/@mswjs/interceptors/src/utils/hasConfigurableGlobal.test.ts
generated
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
import { vi, beforeAll, afterEach, afterAll, it, expect } from 'vitest'
|
||||
import { hasConfigurableGlobal } from './hasConfigurableGlobal'
|
||||
|
||||
beforeAll(() => {
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('returns true if the global property exists and is configurable', () => {
|
||||
Object.defineProperty(global, '_existsAndConfigurable', {
|
||||
value: 'something',
|
||||
configurable: true,
|
||||
})
|
||||
|
||||
expect(hasConfigurableGlobal('_existsAndConfigurable')).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false if the global property does not exist', () => {
|
||||
expect(hasConfigurableGlobal('_non-existing')).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false for existing global with undefined as a value', () => {
|
||||
Object.defineProperty(global, '_existsAndUndefined', {
|
||||
value: undefined,
|
||||
configurable: true,
|
||||
})
|
||||
expect(hasConfigurableGlobal('_existsAndUndefined')).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false for existing global with null as a value', () => {
|
||||
Object.defineProperty(global, '_existsAndNull', {
|
||||
value: null,
|
||||
configurable: true,
|
||||
})
|
||||
expect(hasConfigurableGlobal('_existsAndNull')).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false for existing global with a getter that returns undefined', () => {
|
||||
Object.defineProperty(global, '_existsGetterUndefined', {
|
||||
get: () => undefined,
|
||||
configurable: true,
|
||||
})
|
||||
expect(hasConfigurableGlobal('_existsGetterUndefined')).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false and prints an error for implicitly non-configurable global property', () => {
|
||||
Object.defineProperty(global, '_implicitlyNonConfigurable', {
|
||||
value: 'something',
|
||||
})
|
||||
|
||||
expect(hasConfigurableGlobal('_implicitlyNonConfigurable')).toBe(false)
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
'[MSW] Failed to apply interceptor: the global `_implicitlyNonConfigurable` property is non-configurable. This is likely an issue with your environment. If you are using a framework, please open an issue about this in their repository.'
|
||||
)
|
||||
})
|
||||
|
||||
it('returns false and prints an error for explicitly non-configurable global property', () => {
|
||||
Object.defineProperty(global, '_explicitlyNonConfigurable', {
|
||||
value: 'something',
|
||||
configurable: false,
|
||||
})
|
||||
|
||||
expect(hasConfigurableGlobal('_explicitlyNonConfigurable')).toBe(false)
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
'[MSW] Failed to apply interceptor: the global `_explicitlyNonConfigurable` property is non-configurable. This is likely an issue with your environment. If you are using a framework, please open an issue about this in their repository.'
|
||||
)
|
||||
})
|
||||
|
||||
it('returns false and prints an error for global property that only has a getter', () => {
|
||||
Object.defineProperty(global, '_onlyGetter', { get: () => 'something' })
|
||||
|
||||
expect(hasConfigurableGlobal('_onlyGetter')).toBe(false)
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
'[MSW] Failed to apply interceptor: the global `_onlyGetter` property is non-configurable. This is likely an issue with your environment. If you are using a framework, please open an issue about this in their repository.'
|
||||
)
|
||||
})
|
||||
34
node_modules/@mswjs/interceptors/src/utils/hasConfigurableGlobal.ts
generated
vendored
Normal file
34
node_modules/@mswjs/interceptors/src/utils/hasConfigurableGlobal.ts
generated
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Returns a boolean indicating whether the given global property
|
||||
* is defined and is configurable.
|
||||
*/
|
||||
export function hasConfigurableGlobal(propertyName: string): boolean {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(globalThis, propertyName)
|
||||
|
||||
// The property is not set at all.
|
||||
if (typeof descriptor === 'undefined') {
|
||||
return false
|
||||
}
|
||||
|
||||
// The property is set to a getter that returns undefined.
|
||||
if (
|
||||
typeof descriptor.get === 'function' &&
|
||||
typeof descriptor.get() === 'undefined'
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// The property is set to a value equal to undefined.
|
||||
if (typeof descriptor.get === 'undefined' && descriptor.value == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof descriptor.set === 'undefined' && !descriptor.configurable) {
|
||||
console.error(
|
||||
`[MSW] Failed to apply interceptor: the global \`${propertyName}\` property is non-configurable. This is likely an issue with your environment. If you are using a framework, please open an issue about this in their repository.`
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
13
node_modules/@mswjs/interceptors/src/utils/isNodeLikeError.ts
generated
vendored
Normal file
13
node_modules/@mswjs/interceptors/src/utils/isNodeLikeError.ts
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
export function isNodeLikeError(
|
||||
error: unknown
|
||||
): error is NodeJS.ErrnoException {
|
||||
if (error == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!(error instanceof Error)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return 'code' in error && 'errno' in error
|
||||
}
|
||||
21
node_modules/@mswjs/interceptors/src/utils/isObject.test.ts
generated
vendored
Normal file
21
node_modules/@mswjs/interceptors/src/utils/isObject.test.ts
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
import { it, expect } from 'vitest'
|
||||
import { isObject } from './isObject'
|
||||
|
||||
it('returns true given an object', () => {
|
||||
expect(isObject({})).toBe(true)
|
||||
expect(isObject({ a: 1 })).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false given an object-like instance', () => {
|
||||
expect(isObject([1])).toBe(false)
|
||||
expect(isObject(function () {})).toBe(false)
|
||||
expect(isObject(new Response())).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false given a non-object instance', () => {
|
||||
expect(isObject(null)).toBe(false)
|
||||
expect(isObject(undefined)).toBe(false)
|
||||
expect(isObject(false)).toBe(false)
|
||||
expect(isObject(123)).toBe(false)
|
||||
expect(isObject(Symbol('object Object'))).toBe(false)
|
||||
})
|
||||
8
node_modules/@mswjs/interceptors/src/utils/isObject.ts
generated
vendored
Normal file
8
node_modules/@mswjs/interceptors/src/utils/isObject.ts
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Determines if a given value is an instance of object.
|
||||
*/
|
||||
export function isObject<T>(value: any, loose = false): value is T {
|
||||
return loose
|
||||
? Object.prototype.toString.call(value).startsWith('[object ')
|
||||
: Object.prototype.toString.call(value) === '[object Object]'
|
||||
}
|
||||
19
node_modules/@mswjs/interceptors/src/utils/isPropertyAccessible.ts
generated
vendored
Normal file
19
node_modules/@mswjs/interceptors/src/utils/isPropertyAccessible.ts
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* A function that validates if property access is possible on an object
|
||||
* without throwing. It returns `true` if the property access is possible
|
||||
* and `false` otherwise.
|
||||
*
|
||||
* Environments like miniflare will throw on property access on certain objects
|
||||
* like Request and Response, for unimplemented properties.
|
||||
*/
|
||||
export function isPropertyAccessible<Obj extends Record<string, any>>(
|
||||
obj: Obj,
|
||||
key: keyof Obj
|
||||
) {
|
||||
try {
|
||||
obj[key]
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
11
node_modules/@mswjs/interceptors/src/utils/nextTick.ts
generated
vendored
Normal file
11
node_modules/@mswjs/interceptors/src/utils/nextTick.ts
generated
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
export function nextTick(callback: () => void) {
|
||||
setTimeout(callback, 0)
|
||||
}
|
||||
|
||||
export function nextTickAsync(callback: () => void) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(callback())
|
||||
}, 0)
|
||||
})
|
||||
}
|
||||
39
node_modules/@mswjs/interceptors/src/utils/node/index.ts
generated
vendored
Normal file
39
node_modules/@mswjs/interceptors/src/utils/node/index.ts
generated
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
import { ClientRequest } from 'node:http'
|
||||
import { Readable } from 'node:stream'
|
||||
import { invariant } from 'outvariant'
|
||||
import { getRawRequest } from '../../getRawRequest'
|
||||
|
||||
const kRawRequestBodyStream = Symbol('kRawRequestBodyStream')
|
||||
|
||||
/**
|
||||
* Returns the request body stream of the given request.
|
||||
* @note This is only relevant in the context of `http.ClientRequest`.
|
||||
* This function will throw if the given `request` wasn't created based on
|
||||
* the `http.ClientRequest` instance.
|
||||
* You must rely on the web stream consumers for other request clients.
|
||||
*/
|
||||
export function getClientRequestBodyStream(request: Request): Readable {
|
||||
const rawRequest = getRawRequest(request)
|
||||
|
||||
invariant(
|
||||
rawRequest instanceof ClientRequest,
|
||||
`Failed to retrieve raw request body stream: request is not an instance of "http.ClientRequest". Note that you can only use the "getClientRequestBodyStream" function with the requests issued by "http.clientRequest".`
|
||||
)
|
||||
|
||||
const requestBodyStream = Reflect.get(request, kRawRequestBodyStream)
|
||||
|
||||
invariant(
|
||||
requestBodyStream instanceof Readable,
|
||||
'Failed to retrieve raw request body stream: corrupted stream (%s)',
|
||||
typeof requestBodyStream
|
||||
)
|
||||
|
||||
return requestBodyStream
|
||||
}
|
||||
|
||||
export function setRawRequestBodyStream(
|
||||
request: Request,
|
||||
stream: Readable
|
||||
): void {
|
||||
Reflect.set(request, kRawRequestBodyStream, stream)
|
||||
}
|
||||
10
node_modules/@mswjs/interceptors/src/utils/parseJson.test.ts
generated
vendored
Normal file
10
node_modules/@mswjs/interceptors/src/utils/parseJson.test.ts
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
import { it, expect } from 'vitest'
|
||||
import { parseJson } from './parseJson'
|
||||
|
||||
it('parses a given string into JSON', () => {
|
||||
expect(parseJson('{"id":1}')).toEqual({ id: 1 })
|
||||
})
|
||||
|
||||
it('returns null given invalid JSON string', () => {
|
||||
expect(parseJson('{"o:2\'')).toBeNull()
|
||||
})
|
||||
12
node_modules/@mswjs/interceptors/src/utils/parseJson.ts
generated
vendored
Normal file
12
node_modules/@mswjs/interceptors/src/utils/parseJson.ts
generated
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Parses a given string into JSON.
|
||||
* Gracefully handles invalid JSON by returning `null`.
|
||||
*/
|
||||
export function parseJson(data: string): Record<string, unknown> | null {
|
||||
try {
|
||||
const json = JSON.parse(data)
|
||||
return json
|
||||
} catch (_) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
45
node_modules/@mswjs/interceptors/src/utils/resolveWebSocketUrl.ts
generated
vendored
Normal file
45
node_modules/@mswjs/interceptors/src/utils/resolveWebSocketUrl.ts
generated
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Resolve potentially relative WebSocket URLs the same way
|
||||
* the browser does (replace the protocol, use the origin, etc).
|
||||
*
|
||||
* @see https://websockets.spec.whatwg.org//#dom-websocket-websocket
|
||||
*/
|
||||
export function resolveWebSocketUrl(url: string | URL): string {
|
||||
if (typeof url === 'string') {
|
||||
/**
|
||||
* @note Cast the string to a URL first so the parsing errors
|
||||
* are thrown as a part of the WebSocket constructor, not consumers.
|
||||
*/
|
||||
const urlRecord = new URL(
|
||||
url,
|
||||
typeof location !== 'undefined' ? location.href : undefined
|
||||
)
|
||||
|
||||
return resolveWebSocketUrl(urlRecord)
|
||||
}
|
||||
|
||||
if (url.protocol === 'http:') {
|
||||
url.protocol = 'ws:'
|
||||
} else if (url.protocol === 'https:') {
|
||||
url.protocol = 'wss:'
|
||||
}
|
||||
|
||||
if (url.protocol !== 'ws:' && url.protocol !== 'wss:') {
|
||||
/**
|
||||
* @note These errors are modeled after the browser errors.
|
||||
* The exact error messages aren't provided in the specification.
|
||||
* Node.js uses more obscure error messages that I don't wish to replicate.
|
||||
*/
|
||||
throw new SyntaxError(
|
||||
`Failed to construct 'WebSocket': The URL's scheme must be either 'http', 'https', 'ws', or 'wss'. '${url.protocol}' is not allowed.`
|
||||
)
|
||||
}
|
||||
|
||||
if (url.hash !== '') {
|
||||
throw new SyntaxError(
|
||||
`Failed to construct 'WebSocket': The URL contains a fragment identifier ('${url.hash}'). Fragment identifiers are not allowed in WebSocket URLs.`
|
||||
)
|
||||
}
|
||||
|
||||
return url.href
|
||||
}
|
||||
59
node_modules/@mswjs/interceptors/src/utils/responseUtils.ts
generated
vendored
Normal file
59
node_modules/@mswjs/interceptors/src/utils/responseUtils.ts
generated
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
import { isObject } from './isObject'
|
||||
import { isPropertyAccessible } from './isPropertyAccessible'
|
||||
|
||||
/**
|
||||
* Creates a generic 500 Unhandled Exception response.
|
||||
*/
|
||||
export function createServerErrorResponse(body: unknown): Response {
|
||||
return new Response(
|
||||
JSON.stringify(
|
||||
body instanceof Error
|
||||
? {
|
||||
name: body.name,
|
||||
message: body.message,
|
||||
stack: body.stack,
|
||||
}
|
||||
: body
|
||||
),
|
||||
{
|
||||
status: 500,
|
||||
statusText: 'Unhandled Exception',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export type ResponseError = Response & { type: 'error' }
|
||||
|
||||
/**
|
||||
* Check if the given response is a `Response.error()`.
|
||||
*
|
||||
* @note Some environments, like Miniflare (Cloudflare) do not
|
||||
* implement the "Response.type" property and throw on its access.
|
||||
* Safely check if we can access "type" on "Response" before continuing.
|
||||
* @see https://github.com/mswjs/msw/issues/1834
|
||||
*/
|
||||
export function isResponseError(response: unknown): response is ResponseError {
|
||||
return (
|
||||
response != null &&
|
||||
response instanceof Response &&
|
||||
isPropertyAccessible(response, 'type') &&
|
||||
response.type === 'error'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given value is a `Response` or a Response-like object.
|
||||
* This is different from `value instanceof Response` because it supports
|
||||
* custom `Response` constructors, like the one when using Undici directly.
|
||||
*/
|
||||
export function isResponseLike(value: unknown): value is Response {
|
||||
return (
|
||||
isObject<Record<string, any>>(value, true) &&
|
||||
isPropertyAccessible(value, 'status') &&
|
||||
isPropertyAccessible(value, 'statusText') &&
|
||||
isPropertyAccessible(value, 'bodyUsed')
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user