From f948d13d2df85efa8d276396e1e60da235523a0e Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 17 Apr 2024 13:11:26 +0200 Subject: [PATCH 1/6] fix: give `node` export condition higher priority (#2134) --- config/scripts/validate-esm.js | 13 +++++++++++-- package.json | 18 +++++++++++++++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/config/scripts/validate-esm.js b/config/scripts/validate-esm.js index cc473820b..a1da8c7ae 100644 --- a/config/scripts/validate-esm.js +++ b/config/scripts/validate-esm.js @@ -90,7 +90,7 @@ function validatePackageExports() { console.log('✅ Validated package.json exports') } -function validateExportConditions(pointer, conditions) { +function validateExportConditions(pointer, conditions, level = 0) { if (typeof conditions === 'string') { invariant( fs.existsSync(conditions), @@ -103,7 +103,7 @@ function validateExportConditions(pointer, conditions) { const keys = Object.keys(conditions) - if (conditions[keys[0]] !== null) { + if (level == 0 && conditions[keys[0]] !== null) { invariant(keys[0] === 'types', 'FS') } @@ -115,6 +115,15 @@ function validateExportConditions(pointer, conditions) { return } + if (typeof relativeExportPath === 'object') { + validateExportConditions( + `${pointer}.${key}`, + relativeExportPath, + level + 1, + ) + return + } + const exportPath = fromRoot(relativeExportPath) invariant( fs.existsSync(exportPath), diff --git a/package.json b/package.json index 2b2c7ca41..66849b212 100644 --- a/package.json +++ b/package.json @@ -14,22 +14,34 @@ "default": "./lib/core/index.js" }, "./browser": { - "node": null, "types": "./lib/browser/index.d.ts", + "browser": { + "require": "./lib/browser/index.js", + "import": "./lib/browser/index.mjs" + }, + "node": null, "require": "./lib/browser/index.js", "import": "./lib/browser/index.mjs", "default": "./lib/browser/index.js" }, "./node": { - "browser": null, "types": "./lib/node/index.d.ts", + "node": { + "require": "./lib/node/index.js", + "import": "./lib/node/index.mjs" + }, + "browser": null, "require": "./lib/node/index.js", "import": "./lib/node/index.mjs", "default": "./lib/node/index.mjs" }, "./native": { - "browser": null, "types": "./lib/native/index.d.ts", + "react-native": { + "require": "./lib/native/index.js", + "import": "./lib/native/index.mjs" + }, + "browser": null, "require": "./lib/native/index.js", "import": "./lib/native/index.mjs", "default": "./lib/native/index.js" From 64bcae7c27dfd36a35723a323c29cf0f5138a81c Mon Sep 17 00:00:00 2001 From: nagadevkrishna <35282691+nagadevkrishna@users.noreply.github.com> Date: Wed, 17 Apr 2024 07:33:11 -0400 Subject: [PATCH 2/6] fix: preserve search params in "onUnhandledRequest" messages (#2128) Co-authored-by: Nagadev Co-authored-by: Artem Zakharchenko --- .../utils/request/onUnhandledRequest.test.ts | 34 ++++++++++++++++--- src/core/utils/request/onUnhandledRequest.ts | 2 +- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/core/utils/request/onUnhandledRequest.test.ts b/src/core/utils/request/onUnhandledRequest.test.ts index fc717c952..7cb9826fc 100644 --- a/src/core/utils/request/onUnhandledRequest.test.ts +++ b/src/core/utils/request/onUnhandledRequest.test.ts @@ -7,10 +7,10 @@ import { } from './onUnhandledRequest' const fixtures = { - warningWithoutSuggestions: `\ + warningWithoutSuggestions: (url = `/api`) => `\ [MSW] Warning: intercepted a request without a matching request handler: - • GET /api + • GET ${url} If you still wish to intercept this unhandled request, please create a request handler for it. Read more: https://mswjs.io/docs/getting-started/mocks`, @@ -46,7 +46,9 @@ test('supports the "bypass" request strategy', async () => { test('supports the "warn" request strategy', async () => { await onUnhandledRequest(new Request(new URL('http://localhost/api')), 'warn') - expect(console.warn).toHaveBeenCalledWith(fixtures.warningWithoutSuggestions) + expect(console.warn).toHaveBeenCalledWith( + fixtures.warningWithoutSuggestions(), + ) }) test('supports the "error" request strategy', async () => { @@ -103,7 +105,9 @@ test('supports calling default strategies from the custom callback function', as test('does not print any suggestions given no handlers to suggest', async () => { await onUnhandledRequest(new Request(new URL('http://localhost/api')), 'warn') - expect(console.warn).toHaveBeenCalledWith(fixtures.warningWithoutSuggestions) + expect(console.warn).toHaveBeenCalledWith( + fixtures.warningWithoutSuggestions(), + ) }) test('throws an exception given unknown request strategy', async () => { @@ -117,3 +121,25 @@ test('throws an exception given unknown request strategy', async () => { '[MSW] Failed to react to an unhandled request: unknown strategy "invalid-strategy". Please provide one of the supported strategies ("bypass", "warn", "error") or a custom callback function as the value of the "onUnhandledRequest" option.', ) }) + +test('prints with a relative URL and search params', async () => { + await onUnhandledRequest( + new Request(new URL('http://localhost/api?foo=boo')), + 'warn', + ) + + expect(console.warn).toHaveBeenCalledWith( + fixtures.warningWithoutSuggestions(`/api?foo=boo`), + ) +}) + +test('prints with an absolute URL and search params', async () => { + await onUnhandledRequest( + new Request(new URL('https://mswjs.io/api?foo=boo')), + 'warn', + ) + + expect(console.warn).toHaveBeenCalledWith( + fixtures.warningWithoutSuggestions(`https://mswjs.io/api?foo=boo`), + ) +}) diff --git a/src/core/utils/request/onUnhandledRequest.ts b/src/core/utils/request/onUnhandledRequest.ts index 55e7dcef7..fcf4d5055 100644 --- a/src/core/utils/request/onUnhandledRequest.ts +++ b/src/core/utils/request/onUnhandledRequest.ts @@ -22,7 +22,7 @@ export async function onUnhandledRequest( strategy: UnhandledRequestStrategy = 'warn', ): Promise { const url = new URL(request.url) - const publicUrl = toPublicUrl(url) + const publicUrl = toPublicUrl(url) + url.search const unhandledRequestMessage = `intercepted a request without a matching request handler:\n\n \u2022 ${request.method} ${publicUrl}\n\nIf you still wish to intercept this unhandled request, please create a request handler for it.\nRead more: https://mswjs.io/docs/getting-started/mocks` From 43a163b2aab87d124aecde05826729b896ad3484 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Wed, 17 Apr 2024 11:45:33 +0000 Subject: [PATCH 3/6] chore(release): v2.2.14 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 66849b212..8372f2b01 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "msw", - "version": "2.2.13", + "version": "2.2.14", "description": "Seamless REST/GraphQL API mocking library for browser and Node.js.", "main": "./lib/core/index.js", "module": "./lib/core/index.mjs", From 519139924639a0135169210a0d9c5cd0a0d3e994 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Wed, 8 May 2024 15:34:54 +0200 Subject: [PATCH 4/6] feat: treat unhandled exceptions in handlers as 500 error responses (#2135) --- package.json | 2 +- pnpm-lock.yaml | 8 +- src/core/utils/internal/devUtils.test.ts | 21 ++ src/core/utils/internal/devUtils.ts | 13 + src/core/utils/request/onUnhandledRequest.ts | 6 +- src/node/SetupServerCommonApi.ts | 8 +- .../life-cycle-events/on.node.test.ts | 267 +++++++++++------- .../callback-throws.node.test.ts | 24 +- .../on-unhandled-request/error.node.test.ts | 12 +- ...test.ts => response-patching.node.test.ts} | 0 .../response/response-cookies.test.ts | 50 ++++ .../response/throw-response.node.test.ts | 19 +- 12 files changed, 288 insertions(+), 142 deletions(-) create mode 100644 src/core/utils/internal/devUtils.test.ts rename test/node/msw-api/setup-server/scenarios/{response-patching..node.test.ts => response-patching.node.test.ts} (100%) create mode 100644 test/node/rest-api/response/response-cookies.test.ts diff --git a/package.json b/package.json index 8372f2b01..2a2cd1556 100644 --- a/package.json +++ b/package.json @@ -137,7 +137,7 @@ "@bundled-es-modules/statuses": "^1.0.1", "@inquirer/confirm": "^3.0.0", "@mswjs/cookies": "^1.1.0", - "@mswjs/interceptors": "^0.26.14", + "@mswjs/interceptors": "^0.29.0", "@open-draft/until": "^2.1.0", "@types/cookie": "^0.6.0", "@types/statuses": "^2.0.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 42d53c560..2aa6f5fa4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ dependencies: specifier: ^1.1.0 version: 1.1.0 '@mswjs/interceptors': - specifier: ^0.26.14 - version: 0.26.15 + specifier: ^0.29.0 + version: 0.29.0 '@open-draft/until': specifier: ^2.1.0 version: 2.1.0 @@ -1410,8 +1410,8 @@ packages: engines: {node: '>=18'} dev: false - /@mswjs/interceptors@0.26.15: - resolution: {integrity: sha512-HM47Lu1YFmnYHKMBynFfjCp0U/yRskHj/8QEJW0CBEPOlw8Gkmjfll+S9b8M7V5CNDw2/ciRxjjnWeaCiblSIQ==} + /@mswjs/interceptors@0.29.0: + resolution: {integrity: sha512-eppU9TxaRS2t5IcR00nuh+36zMHcK09pyhUvWJLO1ae5+U8KL7iatUGKlLUlbxXaq3BvDjlcF0Q8Xhzyosk/xA==} engines: {node: '>=18'} dependencies: '@open-draft/deferred-promise': 2.2.0 diff --git a/src/core/utils/internal/devUtils.test.ts b/src/core/utils/internal/devUtils.test.ts new file mode 100644 index 000000000..76dd20192 --- /dev/null +++ b/src/core/utils/internal/devUtils.test.ts @@ -0,0 +1,21 @@ +import { InternalError } from './devUtils' + +describe(InternalError, () => { + it('creates an InternalError instance', () => { + const error = new InternalError('Message') + + expect(error.name).toBe('InternalError') + expect(error.message).toBe('Message') + expect(error.toString()).toBe('InternalError: Message') + expect(error.stack).toMatch(/\w+/) + }) + + it('passes the identity check', () => { + const error = new InternalError('Message') + expect(error instanceof InternalError).toBe(true) + expect(error instanceof Error).toBe(true) + + const extraneousError = new Error('Message') + expect(extraneousError).not.toBeInstanceOf(InternalError) + }) +}) diff --git a/src/core/utils/internal/devUtils.ts b/src/core/utils/internal/devUtils.ts index 47abfb754..08e5005e3 100644 --- a/src/core/utils/internal/devUtils.ts +++ b/src/core/utils/internal/devUtils.ts @@ -29,3 +29,16 @@ export const devUtils = { warn, error, } + +/** + * Internal error instance. + * Used to differentiate the library errors that must be forwarded + * to the user from the unhandled exceptions. Use this if you don't + * wish for the error to be coerced to a 500 fallback response. + */ +export class InternalError extends Error { + constructor(message: string) { + super(message) + this.name = 'InternalError' + } +} diff --git a/src/core/utils/request/onUnhandledRequest.ts b/src/core/utils/request/onUnhandledRequest.ts index fcf4d5055..02708b139 100644 --- a/src/core/utils/request/onUnhandledRequest.ts +++ b/src/core/utils/request/onUnhandledRequest.ts @@ -1,5 +1,5 @@ import { toPublicUrl } from './toPublicUrl' -import { devUtils } from '../internal/devUtils' +import { InternalError, devUtils } from '../internal/devUtils' export interface UnhandledRequestPrint { warning(): void @@ -33,7 +33,7 @@ export async function onUnhandledRequest( devUtils.error('Error: %s', unhandledRequestMessage) // Throw an exception to halt request processing and not perform the original request. - throw new Error( + throw new InternalError( devUtils.formatMessage( 'Cannot bypass a request when using the "error" strategy for the "onUnhandledRequest" option.', ), @@ -49,7 +49,7 @@ export async function onUnhandledRequest( break default: - throw new Error( + throw new InternalError( devUtils.formatMessage( 'Failed to react to an unhandled request: unknown strategy "%s". Please provide one of the supported strategies ("bypass", "warn", "error") or a custom callback function as the value of the "onUnhandledRequest" option.', strategy, diff --git a/src/node/SetupServerCommonApi.ts b/src/node/SetupServerCommonApi.ts index fc742429f..85c534d31 100644 --- a/src/node/SetupServerCommonApi.ts +++ b/src/node/SetupServerCommonApi.ts @@ -15,7 +15,7 @@ import { SetupApi } from '~/core/SetupApi' import { handleRequest } from '~/core/utils/handleRequest' import type { RequestHandler } from '~/core/handlers/RequestHandler' import { mergeRight } from '~/core/utils/internal/mergeRight' -import { devUtils } from '~/core/utils/internal/devUtils' +import { InternalError, devUtils } from '~/core/utils/internal/devUtils' import type { SetupServerCommon } from './glossary' export const DEFAULT_LISTEN_OPTIONS: RequiredDeep = { @@ -68,6 +68,12 @@ export class SetupServerCommonApi return }) + this.interceptor.on('unhandledException', ({ error }) => { + if (error instanceof InternalError) { + throw error + } + }) + this.interceptor.on( 'response', ({ response, isMockedResponse, request, requestId }) => { diff --git a/test/node/msw-api/setup-server/life-cycle-events/on.node.test.ts b/test/node/msw-api/setup-server/life-cycle-events/on.node.test.ts index 8219ade2e..be58a7427 100644 --- a/test/node/msw-api/setup-server/life-cycle-events/on.node.test.ts +++ b/test/node/msw-api/setup-server/life-cycle-events/on.node.test.ts @@ -2,9 +2,8 @@ * @vitest-environment node */ import { HttpResponse, http } from 'msw' -import { setupServer } from 'msw/node' +import { SetupServerApi, setupServer } from 'msw/node' import { HttpServer } from '@open-draft/test-server/http' -import { waitFor } from '../../../../support/waitFor' const httpServer = new HttpServer((app) => { app.get('/user', (req, res) => res.status(500).end()) @@ -14,18 +13,34 @@ const httpServer = new HttpServer((app) => { const server = setupServer() -const listener = vi.fn() +function spyOnEvents(server: SetupServerApi) { + const listener = vi.fn() + const wrapListener = (eventName: string, listener: any) => { + return (...args) => listener(eventName, ...args) + } -function getRequestId(requestStartListener: vi.Mock) { - const { calls } = requestStartListener.mock - const requestStartCall = calls.find((call) => { - return call[0].startsWith('[request:start]') - }) + server.events.on('request:start', wrapListener('request:start', listener)) + server.events.on('request:match', wrapListener('request:match', listener)) + server.events.on( + 'request:unhandled', + wrapListener('request:unhandled', listener), + ) + server.events.on('request:end', wrapListener('request:end', listener)) + server.events.on('response:mocked', wrapListener('response:mocked', listener)) + server.events.on('response:bypass', wrapListener('response:bypass', listener)) + server.events.on( + 'unhandledException', + wrapListener('unhandledException', listener), + ) - return requestStartCall[0].split(' ')[3] + return listener } beforeAll(async () => { + // Supress "Expected a mocking resolver function to return a mocked response" + // warnings when hitting intentionally empty resolver. + vi.spyOn(global.console, 'warn').mockImplementation(() => void 0) + await httpServer.listen() server.use( @@ -40,62 +55,6 @@ beforeAll(async () => { }), ) server.listen() - - server.events.on('request:start', ({ request, requestId }) => { - listener(`[request:start] ${request.method} ${request.url} ${requestId}`) - }) - - server.events.on('request:match', ({ request, requestId }) => { - listener(`[request:match] ${request.method} ${request.url} ${requestId}`) - }) - - server.events.on('request:unhandled', ({ request, requestId }) => { - listener( - `[request:unhandled] ${request.method} ${request.url} ${requestId}`, - ) - }) - - server.events.on('request:end', ({ request, requestId }) => { - listener(`[request:end] ${request.method} ${request.url} ${requestId}`) - }) - - server.events.on( - 'response:mocked', - async ({ response, request, requestId }) => { - listener( - `[response:mocked] ${await response.text()} ${request.method} ${ - request.url - } ${requestId}`, - ) - }, - ) - - server.events.on( - 'response:bypass', - async ({ response, request, requestId }) => { - listener( - `[response:bypass] ${await response.text()} ${request.method} ${ - request.url - } ${requestId}`, - ) - }, - ) - - server.events.on('unhandledException', ({ error, request, requestId }) => { - listener( - `[unhandledException] ${request.method} ${request.url} ${requestId} ${error.message}`, - ) - }) -}) - -beforeEach(() => { - // Supress "Expected a mocking resolver function to return a mocked response" - // warnings. Using intentional explicit empty resolver. - vi.spyOn(global.console, 'warn').mockImplementation(() => void 0) -}) - -afterEach(() => { - vi.restoreAllMocks() }) afterAll(async () => { @@ -103,103 +62,209 @@ afterAll(async () => { await httpServer.close() }) -test('emits events for a handler request and mocked response', async () => { +test('emits events for a handled request and mocked response', async () => { + const listener = spyOnEvents(server) const url = httpServer.http.url('/user') await fetch(url) - const requestId = getRequestId(listener) - - await waitFor(() => { - expect(listener).toHaveBeenCalledWith( - expect.stringContaining('[response:mocked]'), - ) - }) expect(listener).toHaveBeenNthCalledWith( 1, - `[request:start] GET ${url} ${requestId}`, + 'request:start', + expect.objectContaining({ + request: expect.any(Request), + requestId: expect.any(String), + }), ) + + const { request, requestId } = listener.mock.calls[0][1] + expect(request.method).toBe('GET') + expect(request.url).toBe(url) + expect(listener).toHaveBeenNthCalledWith( 2, - `[request:match] GET ${url} ${requestId}`, + 'request:match', + expect.objectContaining({ + request, + requestId, + }), ) expect(listener).toHaveBeenNthCalledWith( 3, - `[request:end] GET ${url} ${requestId}`, + 'request:end', + expect.objectContaining({ + request, + requestId, + }), ) expect(listener).toHaveBeenNthCalledWith( 4, - `[response:mocked] response-body GET ${url} ${requestId}`, + 'response:mocked', + expect.objectContaining({ + request, + requestId, + response: expect.any(Response), + }), ) + + const { response } = listener.mock.calls[3][1] + expect(response.status).toBe(200) + expect(response.statusText).toBe('OK') + expect(await response.text()).toBe('response-body') + expect(listener).toHaveBeenCalledTimes(4) }) test('emits events for a handled request with no response', async () => { + const listener = spyOnEvents(server) const url = httpServer.http.url('/no-response') await fetch(url, { method: 'POST' }) - const requestId = getRequestId(listener) - - await waitFor(() => { - expect(listener).toHaveBeenCalledWith( - expect.stringContaining('[response:bypass]'), - ) - }) expect(listener).toHaveBeenNthCalledWith( 1, - `[request:start] POST ${url} ${requestId}`, + 'request:start', + expect.objectContaining({ + request: expect.any(Request), + requestId: expect.any(String), + }), ) + + const { request, requestId } = listener.mock.calls[0][1] + expect(request.method).toBe('POST') + expect(request.url).toBe(url) + expect(listener).toHaveBeenNthCalledWith( 2, - `[request:end] POST ${url} ${requestId}`, + 'request:end', + expect.objectContaining({ + request, + requestId, + }), ) expect(listener).toHaveBeenNthCalledWith( 3, - `[response:bypass] original-response POST ${url} ${requestId}`, + 'response:bypass', + expect.objectContaining({ + request, + requestId, + response: expect.any(Response), + }), ) + + const { response } = listener.mock.calls[2][1] + expect(response.status).toBe(200) + expect(response.statusText).toBe('OK') + expect(await response.text()).toBe('original-response') + expect(listener).toHaveBeenCalledTimes(3) }) test('emits events for an unhandled request', async () => { + const listener = spyOnEvents(server) const url = httpServer.http.url('/unknown-route') await fetch(url) - const requestId = getRequestId(listener) - - await waitFor(() => { - expect(listener).toHaveBeenCalledWith( - expect.stringContaining('[response:bypass]'), - ) - }) expect(listener).toHaveBeenNthCalledWith( 1, - `[request:start] GET ${url} ${requestId}`, + 'request:start', + expect.objectContaining({ + request: expect.any(Request), + requestId: expect.any(String), + }), ) + + const { request, requestId } = listener.mock.calls[0][1] + expect(request.method).toBe('GET') + expect(request.url).toBe(url) + expect(listener).toHaveBeenNthCalledWith( 2, - `[request:unhandled] GET ${url} ${requestId}`, + 'request:unhandled', + expect.objectContaining({ + request, + requestId, + }), ) expect(listener).toHaveBeenNthCalledWith( 3, - `[request:end] GET ${url} ${requestId}`, + 'request:end', + expect.objectContaining({ + request, + requestId, + }), ) + expect(listener).toHaveBeenNthCalledWith( 4, - `[response:bypass] majestic-unknown GET ${url} ${requestId}`, + 'response:bypass', + expect.objectContaining({ + request, + requestId, + response: expect.any(Response), + }), ) + + const { response } = listener.mock.calls[3][1] + expect(response.status).toBe(200) + expect(response.statusText).toBe('OK') + expect(await response.text()).toBe('majestic-unknown') + + expect(listener).toHaveBeenCalledTimes(4) }) test('emits unhandled exceptions in the request handler', async () => { + const listener = spyOnEvents(server) const url = httpServer.http.url('/unhandled-exception') await fetch(url).catch(() => undefined) - const requestId = getRequestId(listener) - expect(listener).toHaveBeenCalledWith( - `[unhandledException] GET ${url} ${requestId} Unhandled resolver error`, + expect(listener).toHaveBeenNthCalledWith( + 1, + 'request:start', + expect.objectContaining({ + request: expect.any(Request), + requestId: expect.any(String), + }), + ) + + const { request, requestId } = listener.mock.calls[0][1] + expect(request.method).toBe('GET') + expect(request.url).toBe(url) + + expect(listener).toHaveBeenNthCalledWith( + 2, + 'unhandledException', + expect.objectContaining({ + request, + requestId, + error: new Error('Unhandled resolver error'), + }), + ) + + /** + * @note The fallback 500 response still counts as a mocked response. + * I'm torn on this but I believe we should indicate that the request + * (a) received a response; (b) that response was mocked. + */ + expect(listener).toHaveBeenNthCalledWith( + 3, + 'response:mocked', + expect.objectContaining({ + request, + requestId, + response: expect.any(Response), + }), ) + + const { response } = listener.mock.calls[2][1] + expect(response.status).toBe(500) + expect(response.statusText).toBe('Unhandled Exception') + + expect(listener).toHaveBeenCalledTimes(3) }) test('stops emitting events once the server is stopped', async () => { + const listener = spyOnEvents(server) server.close() + await fetch(httpServer.http.url('/user')) expect(listener).not.toHaveBeenCalled() diff --git a/test/node/msw-api/setup-server/scenarios/on-unhandled-request/callback-throws.node.test.ts b/test/node/msw-api/setup-server/scenarios/on-unhandled-request/callback-throws.node.test.ts index aff450f17..507587b04 100644 --- a/test/node/msw-api/setup-server/scenarios/on-unhandled-request/callback-throws.node.test.ts +++ b/test/node/msw-api/setup-server/scenarios/on-unhandled-request/callback-throws.node.test.ts @@ -27,20 +27,14 @@ afterAll(() => { server.close() }) -test('prevents a request when a custom callback throws an exception', async () => { - const makeRequest = () => { - return fetch('https://example.com') - .then(() => { - throw new Error('Must not resolve') - }) - .catch((error) => error) - } +test('handles exceptions in "onUnhandledRequest" callback as 500 responses', async () => { + const response = await fetch('https://example.com') - const requestError = await makeRequest() - - // Request should be cancelled with a fetch error, since the callback threw. - expect(requestError.message).toBe('Failed to fetch') - expect(requestError.cause).toEqual( - new Error('Custom error for GET https://example.com/'), - ) + expect(response.status).toBe(500) + expect(response.statusText).toBe('Unhandled Exception') + expect(await response.json()).toEqual({ + name: 'Error', + message: 'Custom error for GET https://example.com/', + stack: expect.any(String), + }) }) diff --git a/test/node/msw-api/setup-server/scenarios/on-unhandled-request/error.node.test.ts b/test/node/msw-api/setup-server/scenarios/on-unhandled-request/error.node.test.ts index 36f022abd..307a22c36 100644 --- a/test/node/msw-api/setup-server/scenarios/on-unhandled-request/error.node.test.ts +++ b/test/node/msw-api/setup-server/scenarios/on-unhandled-request/error.node.test.ts @@ -31,8 +31,9 @@ beforeAll(async () => { return }), http.post(httpServer.http.url('/implicit-return'), () => { - // The handler that has no return also performs the request as-is, - // still treating this request as handled. + // The handler that has no return value so it falls through any + // other matching handlers (whicbh are none). In the end, + // the request is performed as-is and is still considered handled. }), ) server.listen({ onUnhandledRequest: 'error' }) @@ -67,12 +68,7 @@ test('errors on unhandled request when using the "error" value', async () => { const requestError = await makeRequest() - expect(requestError.message).toBe('Failed to fetch') - /** - * @note Undici wraps fetch rejections in a generic "Failed to fetch" error, - * forwarding the actual rejection in the "error.cause" property. - */ - expect(requestError.cause).toEqual( + expect(requestError).toEqual( new Error( '[MSW] Cannot bypass a request when using the "error" strategy for the "onUnhandledRequest" option.', ), diff --git a/test/node/msw-api/setup-server/scenarios/response-patching..node.test.ts b/test/node/msw-api/setup-server/scenarios/response-patching.node.test.ts similarity index 100% rename from test/node/msw-api/setup-server/scenarios/response-patching..node.test.ts rename to test/node/msw-api/setup-server/scenarios/response-patching.node.test.ts diff --git a/test/node/rest-api/response/response-cookies.test.ts b/test/node/rest-api/response/response-cookies.test.ts new file mode 100644 index 000000000..bfdcaefd7 --- /dev/null +++ b/test/node/rest-api/response/response-cookies.test.ts @@ -0,0 +1,50 @@ +/** + * @vitest-environment node + */ +import { http, HttpResponse } from 'msw' +import { setupServer } from 'msw/node' + +const server = setupServer() + +beforeAll(() => { + server.listen() +}) + +afterEach(() => { + server.resetHandlers() +}) + +afterAll(() => { + server.close() +}) + +it('supports mocking a response cookie', async () => { + server.use( + http.get('*/resource', () => { + return new HttpResponse(null, { + headers: { + 'Set-Cookie': 'a=1', + }, + }) + }), + ) + + const response = await fetch('http://localhost/resource') + expect(response.headers.get('Set-Cookie')).toBe('a=1') +}) + +it('supports mocking multiple response cookies', async () => { + server.use( + http.get('*/resource', () => { + return new HttpResponse(null, { + headers: [ + ['Set-Cookie', 'a=1'], + ['Set-Cookie', 'b=2'], + ], + }) + }), + ) + + const response = await fetch('http://localhost/resource') + expect(response.headers.get('Set-Cookie')).toBe('a=1, b=2') +}) diff --git a/test/node/rest-api/response/throw-response.node.test.ts b/test/node/rest-api/response/throw-response.node.test.ts index a4625647b..b2ac3197f 100644 --- a/test/node/rest-api/response/throw-response.node.test.ts +++ b/test/node/rest-api/response/throw-response.node.test.ts @@ -95,19 +95,20 @@ it('supports middleware-style responses', async () => { expect(await errorResponse.text()).toBe('must have id') }) -it('throws non-Response errors as-is', async () => { +it('handles non-response errors as 500 error responses', async () => { server.use( http.get('https://example.com/', () => { - throw new Error('Oops!') + throw new Error('Custom error') }), ) - const requestError = await fetch('https://example.com/') - .then(() => null) - .catch((error) => error) + const response = await fetch('https://example.com') - expect(requestError.name).toBe('TypeError') - expect(requestError.message).toBe('Failed to fetch') - // Undici forwards the original error in the "cause" property. - expect(requestError.cause).toEqual(new Error('Oops!')) + expect(response.status).toBe(500) + expect(response.statusText).toBe('Unhandled Exception') + expect(await response.json()).toEqual({ + name: 'Error', + message: 'Custom error', + stack: expect.any(String), + }) }) From 686d3d668f7b76cbf26f75c990fded5e4fd298da Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Wed, 8 May 2024 15:49:37 +0200 Subject: [PATCH 5/6] fix(Disposable): make the ".dispose()" method synchronous (#2144) --- src/core/utils/internal/Disposable.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/core/utils/internal/Disposable.ts b/src/core/utils/internal/Disposable.ts index ca61652ab..42a680880 100644 --- a/src/core/utils/internal/Disposable.ts +++ b/src/core/utils/internal/Disposable.ts @@ -1,9 +1,12 @@ -export type DisposableSubscription = () => Promise | void +export type DisposableSubscription = () => void export class Disposable { protected subscriptions: Array = [] - public async dispose() { - await Promise.all(this.subscriptions.map((subscription) => subscription())) + public dispose() { + let subscription: DisposableSubscription | undefined + while ((subscription = this.subscriptions.shift())) { + subscription() + } } } From 7cf34c15ddd2edc688dc848eaf6eb89883ac6df6 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Wed, 8 May 2024 14:11:29 +0000 Subject: [PATCH 6/6] chore(release): v2.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2a2cd1556..90bb1f861 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "msw", - "version": "2.2.14", + "version": "2.3.0", "description": "Seamless REST/GraphQL API mocking library for browser and Node.js.", "main": "./lib/core/index.js", "module": "./lib/core/index.mjs",