UNPKG

msw

Version:

Seamless REST/GraphQL API mocking library for browser and Node.js.

490 lines (421 loc) 13.5 kB
// @vitest-environment jsdom import { Emitter } from 'strict-event-emitter' import { createRequestId } from '@mswjs/interceptors' import { LifeCycleEventsMap, SharedOptions } from '../sharedOptions' import { RequestHandler } from '../handlers/RequestHandler' import { http } from '../http' import { handleRequest, HandleRequestOptions } from './handleRequest' import { RequiredDeep } from '../typeUtils' import { HttpResponse } from '../HttpResponse' import { passthrough } from '../passthrough' const options: RequiredDeep<SharedOptions> = { onUnhandledRequest: vi.fn(), } const handleRequestOptions: Partial<Record<keyof HandleRequestOptions, any>> = { onPassthroughResponse: vi.fn(), onMockedResponse: vi.fn(), } function setup() { const emitter = new Emitter<LifeCycleEventsMap>() const listener = vi.fn() const createMockListener = (name: string) => { return (...args: any) => { listener(name, ...args) } } emitter.on('request:start', createMockListener('request:start')) emitter.on('request:match', createMockListener('request:match')) emitter.on('request:unhandled', createMockListener('request:unhandled')) emitter.on('request:end', createMockListener('request:end')) emitter.on('response:mocked', createMockListener('response:mocked')) emitter.on('response:bypass', createMockListener('response:bypass')) const events = listener.mock.calls return { emitter, events } } beforeEach(() => { vi.spyOn(global.console, 'warn').mockImplementation(() => void 0) }) afterEach(() => { vi.resetAllMocks() }) test('returns undefined for a request with the "accept: msw/passthrough" header equal to "bypass"', async () => { const { emitter, events } = setup() const requestId = createRequestId() const request = new Request(new URL('http://localhost/user'), { headers: new Headers({ accept: 'msw/passthrough', }), }) const handlers: Array<RequestHandler> = [] const result = await handleRequest( request, requestId, handlers, options, emitter, handleRequestOptions, ) expect(result).toBeUndefined() expect(events).toEqual([ ['request:start', { request, requestId }], ['request:end', { request, requestId }], ]) expect(options.onUnhandledRequest).not.toHaveBeenCalled() expect(handleRequestOptions.onPassthroughResponse).toHaveBeenNthCalledWith( 1, request, ) expect(handleRequestOptions.onMockedResponse).not.toHaveBeenCalled() }) test('does not bypass a request with "accept: msw/*" header set to arbitrary value', async () => { const { emitter } = setup() const request = new Request(new URL('http://localhost/user'), { headers: new Headers({ accept: 'msw/invalid', }), }) const handlers: Array<RequestHandler> = [ http.get('/user', () => { return HttpResponse.text('hello world') }), ] const result = await handleRequest( request, createRequestId(), handlers, options, emitter, handleRequestOptions, ) expect(result).not.toBeUndefined() expect(options.onUnhandledRequest).not.toHaveBeenCalled() expect(handleRequestOptions.onMockedResponse).toHaveBeenCalledTimes(1) }) test('reports request as unhandled when it has no matching request handlers', async () => { const { emitter, events } = setup() const requestId = createRequestId() const request = new Request(new URL('http://localhost/user')) const handlers: Array<RequestHandler> = [] const result = await handleRequest( request, requestId, handlers, options, emitter, handleRequestOptions, ) expect(result).toBeUndefined() expect(events).toEqual([ ['request:start', { request, requestId }], ['request:unhandled', { request, requestId }], ['request:end', { request, requestId }], ]) expect(options.onUnhandledRequest).toHaveBeenNthCalledWith(1, request, { warning: expect.any(Function), error: expect.any(Function), }) expect(handleRequestOptions.onPassthroughResponse).toHaveBeenNthCalledWith( 1, request, ) expect(handleRequestOptions.onMockedResponse).not.toHaveBeenCalled() }) test('returns undefined on a request handler that returns no response', async () => { const { emitter, events } = setup() const requestId = createRequestId() const request = new Request(new URL('http://localhost/user')) const handlers: Array<RequestHandler> = [ http.get('/user', () => { // Intentionally blank response resolver. return }), ] const result = await handleRequest( request, requestId, handlers, options, emitter, handleRequestOptions, ) expect(result).toBeUndefined() expect(events).toEqual([ ['request:start', { request, requestId }], ['request:end', { request, requestId }], ]) expect(options.onUnhandledRequest).not.toHaveBeenCalled() expect(handleRequestOptions.onPassthroughResponse).toHaveBeenNthCalledWith( 1, request, ) expect(handleRequestOptions.onMockedResponse).not.toHaveBeenCalled() /** * @note Returning undefined from a resolver no longer prints a warning. */ expect(console.warn).toHaveBeenCalledTimes(0) }) test('returns the mocked response for a request with a matching request handler', async () => { const { emitter, events } = setup() const requestId = createRequestId() const request = new Request(new URL('http://localhost/user')) const mockedResponse = HttpResponse.json({ firstName: 'John' }) const handlers: Array<RequestHandler> = [ http.get('/user', () => { return mockedResponse }), ] const lookupResult = { handler: handlers[0], response: mockedResponse, request, parsedResult: { match: { matches: true, params: {} }, cookies: {}, }, } const result = await handleRequest( request, requestId, handlers, options, emitter, handleRequestOptions, ) expect(result).toEqual(mockedResponse) expect(events).toEqual([ ['request:start', { request, requestId }], ['request:match', { request, requestId }], ['request:end', { request, requestId }], ]) expect(handleRequestOptions.onPassthroughResponse).not.toHaveBeenCalled() expect(handleRequestOptions.onMockedResponse).toHaveBeenCalledTimes(1) const [mockedResponseParam, lookupResultParam] = handleRequestOptions.onMockedResponse.mock.calls[0] expect(mockedResponseParam.status).toBe(mockedResponse.status) expect(mockedResponseParam.statusText).toBe(mockedResponse.statusText) expect(Object.fromEntries(mockedResponseParam.headers.entries())).toEqual( Object.fromEntries(mockedResponse.headers.entries()), ) expect(lookupResultParam).toEqual({ handler: lookupResult.handler, parsedResult: lookupResult.parsedResult, response: expect.objectContaining({ status: lookupResult.response.status, statusText: lookupResult.response.statusText, }), }) }) it('returns undefined without warning on a passthrough request', async () => { const { emitter, events } = setup() const requestId = createRequestId() const request = new Request(new URL('http://localhost/user')) const handlers: Array<RequestHandler> = [ http.get('/user', () => { return passthrough() }), ] const result = await handleRequest( request, requestId, handlers, options, emitter, handleRequestOptions, ) expect(result).toBeUndefined() expect(events).toEqual([ ['request:start', { request, requestId }], ['request:end', { request, requestId }], ]) expect(options.onUnhandledRequest).not.toHaveBeenCalled() expect(handleRequestOptions.onPassthroughResponse).toHaveBeenNthCalledWith( 1, request, ) expect(handleRequestOptions.onMockedResponse).not.toHaveBeenCalled() }) it('calls the handler with the requestId', async () => { const { emitter } = setup() const requestId = createRequestId() const request = new Request(new URL('http://localhost/user')) const handlerFn = vi.fn() const handlers: Array<RequestHandler> = [http.get('/user', handlerFn)] await handleRequest( request, requestId, handlers, options, emitter, handleRequestOptions, ) expect(handlerFn).toHaveBeenCalledWith(expect.objectContaining({ requestId })) }) it('marks the first matching one-time handler as used', async () => { const { emitter } = setup() const oneTimeHandler = http.get( '/resource', () => { return HttpResponse.text('One-time') }, { once: true }, ) const anotherHandler = http.get('/resource', () => { return HttpResponse.text('Another') }) const handlers: Array<RequestHandler> = [oneTimeHandler, anotherHandler] const requestId = createRequestId() const request = new Request('http://localhost/resource') const firstResult = await handleRequest( request, requestId, handlers, options, emitter, handleRequestOptions, ) expect(await firstResult?.text()).toBe('One-time') expect(oneTimeHandler.isUsed).toBe(true) expect(anotherHandler.isUsed).toBe(false) const secondResult = await handleRequest( request, requestId, handlers, options, emitter, handleRequestOptions, ) expect(await secondResult?.text()).toBe('Another') expect(anotherHandler.isUsed).toBe(true) expect(oneTimeHandler.isUsed).toBe(true) }) it('does not mark non-matching one-time handlers as used', async () => { const { emitter } = setup() const oneTimeHandler = http.get( '/resource', () => { return HttpResponse.text('One-time') }, { once: true }, ) const anotherHandler = http.get( '/another', () => { return HttpResponse.text('Another') }, { once: true }, ) const handlers: Array<RequestHandler> = [oneTimeHandler, anotherHandler] const requestId = createRequestId() const firstResult = await handleRequest( new Request('http://localhost/another'), requestId, handlers, options, emitter, handleRequestOptions, ) expect(await firstResult?.text()).toBe('Another') expect(oneTimeHandler.isUsed).toBe(false) expect(anotherHandler.isUsed).toBe(true) const secondResult = await handleRequest( new Request('http://localhost/resource'), requestId, handlers, options, emitter, handleRequestOptions, ) expect(await secondResult?.text()).toBe('One-time') expect(anotherHandler.isUsed).toBe(true) expect(oneTimeHandler.isUsed).toBe(true) }) it('handles parallel requests with one-time handlers', async () => { const { emitter } = setup() const oneTimeHandler = http.get( '/resource', () => { return HttpResponse.text('One-time') }, { once: true }, ) const anotherHandler = http.get('/resource', () => { return HttpResponse.text('Another') }) const handlers: Array<RequestHandler> = [oneTimeHandler, anotherHandler] const requestId = createRequestId() const request = new Request('http://localhost/resource') const firstResultPromise = handleRequest( request, requestId, handlers, options, emitter, handleRequestOptions, ) const secondResultPromise = handleRequest( request, requestId, handlers, options, emitter, handleRequestOptions, ) const firstResult = await firstResultPromise const secondResult = await secondResultPromise expect(await firstResult?.text()).toBe('One-time') expect(await secondResult?.text()).toBe('Another') expect(oneTimeHandler.isUsed).toBe(true) expect(anotherHandler.isUsed).toBe(true) }) describe('[Private] - resolutionContext - used for extensions', () => { describe('#baseUrl', () => { test('when defined, handle requests to that base url only defining pathnames in the handler', async () => { const { emitter } = setup() const baseUrl = 'http://this-base-url-works.com' const handleRequestOptionsWithBaseUrl: HandleRequestOptions = { ...handleRequestOptions, resolutionContext: { baseUrl }, } const handler = http.get('/resource', () => { return HttpResponse.text('Mocked response') }) const handlers: Array<RequestHandler> = [handler] const requestId = createRequestId() const request = new Request(new URL('/resource', baseUrl)) const response = await handleRequest( request, requestId, handlers, options, emitter, handleRequestOptionsWithBaseUrl, ) expect(await response?.text()).toBe('Mocked response') }) test('when defined, do not handle requests to different base urls when defining pathnames in the handler', async () => { const { emitter } = setup() const baseUrl = 'http://this-base-url-works.com' const handleRequestOptionsWithBaseUrl: HandleRequestOptions = { ...handleRequestOptions, resolutionContext: { baseUrl }, } const handler = http.get('/resource', () => { return HttpResponse.text('Mocked response') }) const handlers: Array<RequestHandler> = [handler] const requestId = createRequestId() const request = new Request( new URL('/resource', `http://not-the-base-url.com`), ) const response = await handleRequest( request, requestId, handlers, options, emitter, handleRequestOptionsWithBaseUrl, ) expect(response).toBeUndefined() }) }) })