Skip to content

Commit

Permalink
feat: add "req.passthrough" (#1204)
Browse files Browse the repository at this point in the history
  • Loading branch information
kettanaito committed May 17, 2022
1 parent 3f96505 commit 4e1b1ad
Show file tree
Hide file tree
Showing 15 changed files with 352 additions and 61 deletions.
28 changes: 10 additions & 18 deletions src/handlers/GraphQLHandler.ts
@@ -1,15 +1,13 @@
import { DocumentNode, OperationTypeNode } from 'graphql'
import { SerializedResponse } from '../setupWorker/glossary'
import { set } from '../context/set'
import { status } from '../context/status'
import { delay } from '../context/delay'
import { fetch } from '../context/fetch'
import { data } from '../context/data'
import { extensions } from '../context/extensions'
import { errors } from '../context/errors'
import { GraphQLPayloadContext } from '../typeUtils'
import { cookie } from '../context/cookie'
import {
defaultContext,
DefaultContext,
MockedRequest,
RequestHandler,
RequestHandlerDefaultInfo,
Expand All @@ -36,22 +34,16 @@ export type GraphQLHandlerNameSelector = DocumentNode | RegExp | string
// GraphQL related context should contain utility functions
// useful for GraphQL. Functions like `xml()` bear no value
// in the GraphQL universe.
export type GraphQLContext<QueryType extends Record<string, unknown>> = {
set: typeof set
status: typeof status
delay: typeof delay
fetch: typeof fetch
data: GraphQLPayloadContext<QueryType>
extensions: GraphQLPayloadContext<QueryType>
errors: typeof errors
cookie: typeof cookie
}
export type GraphQLContext<QueryType extends Record<string, unknown>> =
DefaultContext & {
data: GraphQLPayloadContext<QueryType>
extensions: GraphQLPayloadContext<QueryType>
errors: typeof errors
cookie: typeof cookie
}

export const graphqlContext: GraphQLContext<any> = {
set,
status,
delay,
fetch,
...defaultContext,
data,
extensions,
errors,
Expand Down
42 changes: 37 additions & 5 deletions src/handlers/RequestHandler.ts
@@ -1,5 +1,10 @@
import { Headers } from 'headers-polyfill'
import { MockedResponse, response, ResponseComposition } from '../response'
import {
MaybePromise,
MockedResponse,
response,
ResponseComposition,
} from '../response'
import { getCallFrame } from '../utils/internal/getCallFrame'
import { isIterable } from '../utils/internal/isIterable'
import { status } from '../context/status'
Expand All @@ -9,7 +14,14 @@ import { fetch } from '../context/fetch'
import { ResponseResolutionContext } from '../utils/getResponse'
import { SerializedResponse } from '../setupWorker/glossary'

export const defaultContext = {
export type DefaultContext = {
status: typeof status
set: typeof set
delay: typeof delay
fetch: typeof fetch
}

export const defaultContext: DefaultContext = {
status,
set,
delay,
Expand Down Expand Up @@ -47,6 +59,7 @@ export interface MockedRequest<Body = DefaultRequestBody> {
referrerPolicy: Request['referrerPolicy']
body: Body
bodyUsed: Request['bodyUsed']
passthrough: typeof passthrough
}

export interface RequestHandlerDefaultInfo {
Expand All @@ -64,9 +77,9 @@ export type ResponseResolverReturnType<ReturnType> =
| undefined
| void

export type MaybeAsyncResponseResolverReturnType<ReturnType> =
| ResponseResolverReturnType<ReturnType>
| Promise<ResponseResolverReturnType<ReturnType>>
export type MaybeAsyncResponseResolverReturnType<ReturnType> = MaybePromise<
ResponseResolverReturnType<ReturnType>
>

export type AsyncResponseResolverReturnType<ReturnType> =
| MaybeAsyncResponseResolverReturnType<ReturnType>
Expand Down Expand Up @@ -272,3 +285,22 @@ export abstract class RequestHandler<
}
}
}

/**
* Bypass this intercepted request.
* This will make a call to the actual endpoint requested.
*/
export function passthrough(): MockedResponse<null> {
// Constructing a dummy "101 Continue" mocked response
// to keep the return type of the resolver consistent.
return {
status: 101,
statusText: 'Continue',
headers: new Headers(),
body: null,
// Setting "passthrough" to true will signal the response pipeline
// to perform this intercepted request as-is.
passthrough: true,
once: false,
}
}
25 changes: 5 additions & 20 deletions src/handlers/RestHandler.ts
@@ -1,14 +1,4 @@
import {
body,
cookie,
delay,
fetch,
json,
set,
status,
text,
xml,
} from '../context'
import { body, cookie, json, text, xml } from '../context'
import { SerializedResponse } from '../setupWorker/glossary'
import { ResponseResolutionContext } from '../utils/getResponse'
import { devUtils } from '../utils/internal/devUtils'
Expand All @@ -27,6 +17,8 @@ import { getPublicUrlFromRequest } from '../utils/request/getPublicUrlFromReques
import { cleanUrl, getSearchParams } from '../utils/url/cleanUrl'
import {
DefaultRequestBody,
defaultContext,
DefaultContext,
MockedRequest,
RequestHandler,
RequestHandlerDefaultInfo,
Expand All @@ -52,28 +44,21 @@ export enum RESTMethods {

// Declaring a context interface infers
// JSDoc description of the referenced utils.
export type RestContext = {
set: typeof set
status: typeof status
export type RestContext = DefaultContext & {
cookie: typeof cookie
text: typeof text
body: typeof body
json: typeof json
xml: typeof xml
delay: typeof delay
fetch: typeof fetch
}

export const restContext: RestContext = {
set,
status,
...defaultContext,
cookie,
body,
text,
json,
xml,
delay,
fetch,
}

export type RequestQuery = {
Expand Down
8 changes: 6 additions & 2 deletions src/response.ts
Expand Up @@ -2,6 +2,8 @@ import { Headers } from 'headers-polyfill'
import { compose } from './utils/internal/compose'
import { NetworkError } from './utils/NetworkError'

export type MaybePromise<ValueType = any> = ValueType | Promise<ValueType>

/**
* Internal representation of a mocked response instance.
*/
Expand All @@ -11,6 +13,7 @@ export interface MockedResponse<BodyType = any> {
statusText: string
headers: Headers
once: boolean
passthrough: boolean
delay?: number
}

Expand All @@ -19,11 +22,11 @@ export type ResponseTransformer<
TransformerBodyType = any,
> = (
res: MockedResponse<TransformerBodyType>,
) => MockedResponse<BodyType> | Promise<MockedResponse<BodyType>>
) => MaybePromise<MockedResponse<BodyType>>

export type ResponseFunction<BodyType = any> = (
...transformers: ResponseTransformer<BodyType>[]
) => MockedResponse<BodyType> | Promise<MockedResponse<BodyType>>
) => MaybePromise<MockedResponse<BodyType>>

export type ResponseComposition<BodyType = any> = ResponseFunction<BodyType> & {
/**
Expand All @@ -40,6 +43,7 @@ export const defaultResponse: Omit<MockedResponse, 'headers'> = {
body: null,
delay: 0,
once: false,
passthrough: false,
}

export type ResponseCompositionOptions<BodyType> = {
Expand Down
45 changes: 37 additions & 8 deletions src/utils/handleRequest.test.ts
Expand Up @@ -5,7 +5,7 @@ import { createMockedRequest } from '../../test/support/utils'
import { SharedOptions } from '../sharedOptions'
import { RequestHandler } from '../handlers/RequestHandler'
import { rest } from '../rest'
import { handleRequest } from './handleRequest'
import { handleRequest, HandleRequestOptions } from './handleRequest'
import { response } from '../response'
import { context } from '..'
import { RequiredDeep } from '../typeUtils'
Expand All @@ -24,8 +24,8 @@ function getEmittedEvents() {
const options: RequiredDeep<SharedOptions> = {
onUnhandledRequest: jest.fn(),
}
const callbacks = {
onBypassResponse: jest.fn(),
const callbacks: Partial<Record<keyof HandleRequestOptions<any>, any>> = {
onPassthroughResponse: jest.fn(),
onMockedResponse: jest.fn(),
onMockedResponseSent: jest.fn(),
}
Expand Down Expand Up @@ -67,7 +67,7 @@ test('returns undefined for a request with the "x-msw-bypass" header equal to "t
['request:end', request],
])
expect(options.onUnhandledRequest).not.toHaveBeenCalled()
expect(callbacks.onBypassResponse).toHaveBeenNthCalledWith(1, request)
expect(callbacks.onPassthroughResponse).toHaveBeenNthCalledWith(1, request)
expect(callbacks.onMockedResponse).not.toHaveBeenCalled()
expect(callbacks.onMockedResponseSent).not.toHaveBeenCalled()
})
Expand Down Expand Up @@ -123,7 +123,7 @@ test('reports request as unhandled when it has no matching request handlers', as
warning: expect.any(Function),
error: expect.any(Function),
})
expect(callbacks.onBypassResponse).toHaveBeenNthCalledWith(1, request)
expect(callbacks.onPassthroughResponse).toHaveBeenNthCalledWith(1, request)
expect(callbacks.onMockedResponse).not.toHaveBeenCalled()
expect(callbacks.onMockedResponseSent).not.toHaveBeenCalled()
})
Expand Down Expand Up @@ -153,7 +153,7 @@ test('returns undefined and warns on a request handler that returns no response'
['request:end', request],
])
expect(options.onUnhandledRequest).not.toHaveBeenCalled()
expect(callbacks.onBypassResponse).toHaveBeenNthCalledWith(1, request)
expect(callbacks.onPassthroughResponse).toHaveBeenNthCalledWith(1, request)
expect(callbacks.onMockedResponse).not.toHaveBeenCalled()
expect(callbacks.onMockedResponseSent).not.toHaveBeenCalled()

Expand Down Expand Up @@ -198,7 +198,7 @@ test('returns the mocked response for a request with a matching request handler'
['request:match', request],
['request:end', request],
])
expect(callbacks.onBypassResponse).not.toHaveBeenCalled()
expect(callbacks.onPassthroughResponse).not.toHaveBeenCalled()
expect(callbacks.onMockedResponse).toHaveBeenNthCalledWith(
1,
mockedResponse,
Expand Down Expand Up @@ -243,7 +243,7 @@ test('returns a transformed response if the "transformResponse" option is provid
['request:match', request],
['request:end', request],
])
expect(callbacks.onBypassResponse).not.toHaveBeenCalled()
expect(callbacks.onPassthroughResponse).not.toHaveBeenCalled()
expect(transformResponse).toHaveBeenNthCalledWith(1, mockedResponse)
expect(callbacks.onMockedResponse).toHaveBeenNthCalledWith(
1,
Expand All @@ -256,3 +256,32 @@ test('returns a transformed response if the "transformResponse" option is provid
lookupResult,
)
})

it('returns undefined without warning on a passthrough request', async () => {
const request = createMockedRequest({
url: new URL('http://localhost/user'),
})
const handlers: RequestHandler[] = [
rest.get('/user', (req) => {
return req.passthrough()
}),
]

const result = await handleRequest(
request,
handlers,
options,
emitter,
callbacks,
)

expect(result).toBeUndefined()
expect(getEmittedEvents()).toEqual([
['request:start', request],
['request:end', request],
])
expect(options.onUnhandledRequest).not.toHaveBeenCalled()
expect(callbacks.onPassthroughResponse).toHaveBeenNthCalledWith(1, request)
expect(callbacks.onMockedResponse).not.toHaveBeenCalled()
expect(callbacks.onMockedResponseSent).not.toHaveBeenCalled()
})
18 changes: 13 additions & 5 deletions src/utils/handleRequest.ts
Expand Up @@ -24,9 +24,9 @@ export interface HandleRequestOptions<ResponseType> {
transformResponse?(response: MockedResponse<string>): ResponseType

/**
* Invoked whenever returning a bypassed (as-is) response.
* Invoked whenever a request is performed as-is.
*/
onBypassResponse?(request: MockedRequest): void
onPassthroughResponse?(request: MockedRequest): void

/**
* Invoked when the mocked response is ready to be sent.
Expand Down Expand Up @@ -60,7 +60,7 @@ export async function handleRequest<
// Perform bypassed requests (i.e. issued via "ctx.fetch") as-is.
if (request.headers.get('x-msw-bypass') === 'true') {
emitter.emit('request:end', request)
handleRequestOptions?.onBypassResponse?.(request)
handleRequestOptions?.onPassthroughResponse?.(request)
return
}

Expand All @@ -78,7 +78,7 @@ export async function handleRequest<
onUnhandledRequest(request, handlers, options.onUnhandledRequest)
emitter.emit('request:unhandled', request)
emitter.emit('request:end', request)
handleRequestOptions?.onBypassResponse?.(request)
handleRequestOptions?.onPassthroughResponse?.(request)
return
}

Expand All @@ -98,7 +98,15 @@ Expected response resolver to return a mocked response Object, but got %s. The o
)

emitter.emit('request:end', request)
handleRequestOptions?.onBypassResponse?.(request)
handleRequestOptions?.onPassthroughResponse?.(request)
return
}

// When the developer explicitly returned "req.passthrough()" do not warn them.
// Perform the request as-is.
if (response.passthrough) {
emitter.emit('request:end', request)
handleRequestOptions?.onPassthroughResponse?.(request)
return
}

Expand Down
2 changes: 2 additions & 0 deletions src/utils/logging/prepareRequest.test.ts
@@ -1,4 +1,5 @@
import { Headers } from 'headers-polyfill'
import { passthrough } from '../../handlers/RequestHandler'
import { prepareRequest } from './prepareRequest'

test('converts request headers into an object', () => {
Expand All @@ -22,6 +23,7 @@ test('converts request headers into an object', () => {
body: 'text-body',
bodyUsed: false,
cookies: {},
passthrough,
})

// Converts `Headers` instance into inspectable object
Expand Down
3 changes: 2 additions & 1 deletion src/utils/request/parseIsomorphicRequest.test.ts
Expand Up @@ -4,7 +4,7 @@
import { Headers } from 'headers-polyfill/lib'
import { parseIsomorphicRequest } from './parseIsomorphicRequest'
import { createIsomorphicRequest } from '../../../test/support/utils'
import { MockedRequest } from '../../handlers/RequestHandler'
import { MockedRequest, passthrough } from '../../handlers/RequestHandler'

test('parses an isomorphic request', () => {
const request = parseIsomorphicRequest(
Expand Down Expand Up @@ -46,6 +46,7 @@ test('parses an isomorphic request', () => {
body: {
id: 'user-1',
},
passthrough,
})
})

Expand Down

0 comments on commit 4e1b1ad

Please sign in to comment.