From cfc120c477394d67a06644aac6affcea0d132262 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Wed, 18 Dec 2024 13:31:08 +0100 Subject: [PATCH] fix: use consistent `localhost` for remote server --- src/node/SetupServerApi.ts | 54 +++-- src/node/setupRemoteServer.ts | 6 +- .../remote-boundary.test.ts | 5 +- .../setup-remote-server/response.body.test.ts | 213 ++++++++++-------- .../msw-api/setup-remote-server/use.app.js | 3 - .../node/msw-api/setup-remote-server/utils.ts | 8 +- 6 files changed, 161 insertions(+), 128 deletions(-) diff --git a/src/node/SetupServerApi.ts b/src/node/SetupServerApi.ts index 4f93feec2..da47621ab 100644 --- a/src/node/SetupServerApi.ts +++ b/src/node/SetupServerApi.ts @@ -125,6 +125,36 @@ export class SetupServerApi port: remotePort, }) + // Kick off connection to the server early. + const remoteConnectionPromise = remoteClient.connect().then( + () => { + this.handlersController.currentHandlers = new Proxy( + this.handlersController.currentHandlers, + { + apply: (target, thisArg, args) => { + return Array.prototype.concat( + new RemoteRequestHandler({ + remoteClient, + // Get the remote boundary context ID from the environment. + // This way, the user doesn't have to explicitly drill it here. + contextId: process.env[remoteContext.variableName], + }), + Reflect.apply(target, thisArg, args), + ) + }, + }, + ) + }, + // Ignore connection errors. Continue operation as normal. + // The remote server is not required for `setupServer` to work. + () => { + // eslint-disable-next-line no-console + console.error( + `Failed to connect to a remote server at port "${remotePort}"`, + ) + }, + ) + this.beforeRequest = async ({ request }) => { if (shouldBypassRequest(request)) { return @@ -133,29 +163,7 @@ export class SetupServerApi // Once the sync server connection is established, prepend the // remote request handler to be the first for this process. // This way, the remote process' handlers take priority. - await remoteClient.connect().then( - () => { - this.handlersController.currentHandlers = new Proxy( - this.handlersController.currentHandlers, - { - apply: (target, thisArg, args) => { - return Array.prototype.concat( - new RemoteRequestHandler({ - remoteClient, - // Get the remote boundary context ID from the environment. - // This way, the user doesn't have to explicitly drill it here. - contextId: process.env[remoteContext.variableName], - }), - Reflect.apply(target, thisArg, args), - ) - }, - }, - ) - }, - // Ignore connection errors. Continue operation as normal. - // The remote server is not required for `setupServer` to work. - () => {}, - ) + await remoteConnectionPromise } // Forward all life-cycle events from this process to the remote. diff --git a/src/node/setupRemoteServer.ts b/src/node/setupRemoteServer.ts index 4d9deba29..c5b5dbf8b 100644 --- a/src/node/setupRemoteServer.ts +++ b/src/node/setupRemoteServer.ts @@ -113,6 +113,10 @@ export class SetupRemoteServerApi .once('SIGINT', () => closeSyncServer(server)) server.on('request', async (incoming, outgoing) => { + if (!incoming.method) { + return + } + // Handle the handshake request from the client. if (incoming.method === 'HEAD') { outgoing.writeHead(200).end() @@ -276,7 +280,7 @@ async function createSyncServer(port: number): Promise { const serverReadyPromise = new DeferredPromise() const server = http.createServer() - server.listen(+port, '127.0.0.1', () => { + server.listen(port, 'localhost', async () => { serverReadyPromise.resolve(server) }) diff --git a/test/node/msw-api/setup-remote-server/remote-boundary.test.ts b/test/node/msw-api/setup-remote-server/remote-boundary.test.ts index 5ce50440b..618ef05fb 100644 --- a/test/node/msw-api/setup-remote-server/remote-boundary.test.ts +++ b/test/node/msw-api/setup-remote-server/remote-boundary.test.ts @@ -40,10 +40,7 @@ it.concurrent( }), ) - await using testApp = await spawnTestApp(require.resolve('./use.app.js'), { - // Bind the application to this test's context. - contextId: remote.contextId, - }) + await using testApp = await spawnTestApp(require.resolve('./use.app.js')) const response = await fetch(new URL('/resource', testApp.url)) expect(response.status).toBe(200) diff --git a/test/node/msw-api/setup-remote-server/response.body.test.ts b/test/node/msw-api/setup-remote-server/response.body.test.ts index 302a8a8de..36d21b4dc 100644 --- a/test/node/msw-api/setup-remote-server/response.body.test.ts +++ b/test/node/msw-api/setup-remote-server/response.body.test.ts @@ -1,14 +1,12 @@ // @vitest-environment node import { http, HttpResponse } from 'msw' import { setupRemoteServer } from 'msw/node' -import { TestNodeApp } from './utils' +import { spawnTestApp } from './utils' const remote = setupRemoteServer() -const testApp = new TestNodeApp(require.resolve('./use.app.js')) beforeAll(async () => { await remote.listen() - await testApp.start() }) afterEach(() => { @@ -17,94 +15,125 @@ afterEach(() => { afterAll(async () => { await remote.close() - await testApp.close() }) -it('supports responding to a remote request with text', async () => { - remote.use( - http.get('https://example.com/resource', () => { - return HttpResponse.text('hello world') - }), - ) - - const response = await fetch(new URL('/resource', testApp.url)) - expect(response.status).toBe(200) - expect(response.statusText).toBe('OK') - await expect(response.text()).resolves.toBe('hello world') -}) - -it('supports responding to a remote request with JSON', async () => { - remote.use( - http.get('https://example.com/resource', () => { - return HttpResponse.json({ hello: 'world' }) - }), - ) - - const response = await fetch(new URL('/resource', testApp.url)) - expect(response.status).toBe(200) - await expect(response.json()).resolves.toEqual({ hello: 'world' }) -}) - -it('supports responding to a remote request with ArrayBuffer', async () => { - remote.use( - http.get('https://example.com/resource', () => { - return HttpResponse.arrayBuffer(new TextEncoder().encode('hello world')) - }), - ) - - const response = await fetch(new URL('/resource', testApp.url)) - const buffer = await response.arrayBuffer() - - expect(response.status).toBe(200) - expect(new TextDecoder().decode(buffer)).toBe('hello world') -}) - -it('supports responding to a remote request with Blob', async () => { - remote.use( - http.get('https://example.com/resource', () => { - return new Response(new Blob(['hello world'])) - }), - ) - - const response = await fetch(new URL('/resource', testApp.url)) - expect(response.status).toBe(200) - await expect(response.blob()).resolves.toEqual(new Blob(['hello world'])) -}) - -it('supports responding to a remote request with FormData', async () => { - remote.use( - http.get('https://example.com/resource', () => { - const formData = new FormData() - formData.append('hello', 'world') - return HttpResponse.formData(formData) - }), - ) - - const response = await fetch(new URL('/resource', testApp.url)) - expect(response.status).toBe(200) - - await expect(response.text()).resolves.toMatch( - /^------formdata-undici-\d{12}\r\nContent-Disposition: form-data; name="hello"\r\n\r\nworld\r\n------formdata-undici-\d{12}--$/, - ) -}) - -it('supports responding to a remote request with ReadableStream', async () => { - const encoder = new TextEncoder() - remote.use( - http.get('https://example.com/resource', () => { - const stream = new ReadableStream({ - start(controller) { - controller.enqueue(encoder.encode('hello')) - controller.enqueue(encoder.encode(' ')) - controller.enqueue(encoder.encode('world')) - controller.close() - }, - }) - return new Response(stream, { headers: { 'Content-Type': 'text/plain' } }) - }), - ) - - const response = await fetch(new URL('/resource', testApp.url)) - expect(response.status).toBe(200) - await expect(response.text()).resolves.toBe('hello world') -}) +it( + 'supports responding to a remote request with text', + remote.boundary(async () => { + remote.use( + http.get('https://example.com/resource', () => { + return HttpResponse.text('hello world') + }), + ) + + await using testApp = await spawnTestApp(require.resolve('./use.app.js')) + + const response = await fetch(new URL('/resource', testApp.url)) + expect(response.status).toBe(200) + expect(response.statusText).toBe('OK') + await expect(response.text()).resolves.toBe('hello world') + }), +) + +it( + 'supports responding to a remote request with JSON', + remote.boundary(async () => { + remote.use( + http.get('https://example.com/resource', () => { + return HttpResponse.json({ hello: 'world' }) + }), + ) + + await using testApp = await spawnTestApp(require.resolve('./use.app.js')) + + const response = await fetch(new URL('/resource', testApp.url)) + expect(response.status).toBe(200) + await expect(response.json()).resolves.toEqual({ hello: 'world' }) + }), +) + +it( + 'supports responding to a remote request with ArrayBuffer', + remote.boundary(async () => { + remote.use( + http.get('https://example.com/resource', () => { + return HttpResponse.arrayBuffer(new TextEncoder().encode('hello world')) + }), + ) + + await using testApp = await spawnTestApp(require.resolve('./use.app.js')) + + const response = await fetch(new URL('/resource', testApp.url)) + const buffer = await response.arrayBuffer() + + expect(response.status).toBe(200) + expect(new TextDecoder().decode(buffer)).toBe('hello world') + }), +) + +it( + 'supports responding to a remote request with Blob', + remote.boundary(async () => { + remote.use( + http.get('https://example.com/resource', () => { + return new Response(new Blob(['hello world'])) + }), + ) + + await using testApp = await spawnTestApp(require.resolve('./use.app.js')) + + const response = await fetch(new URL('/resource', testApp.url)) + expect(response.status).toBe(200) + await expect(response.blob()).resolves.toEqual(new Blob(['hello world'])) + }), +) + +it( + 'supports responding to a remote request with FormData', + remote.boundary(async () => { + remote.use( + http.get('https://example.com/resource', () => { + const formData = new FormData() + formData.append('hello', 'world') + return HttpResponse.formData(formData) + }), + ) + + await using testApp = await spawnTestApp(require.resolve('./use.app.js')) + + const response = await fetch(new URL('/resource', testApp.url)) + expect(response.status).toBe(200) + + await expect(response.text()).resolves.toMatch( + /^------formdata-undici-\d{12}\r\nContent-Disposition: form-data; name="hello"\r\n\r\nworld\r\n------formdata-undici-\d{12}--$/, + ) + }), +) + +it( + 'supports responding to a remote request with ReadableStream', + remote.boundary(async () => { + const encoder = new TextEncoder() + remote.use( + http.get('https://example.com/resource', () => { + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(encoder.encode('hello')) + controller.enqueue(encoder.encode(' ')) + controller.enqueue(encoder.encode('world')) + controller.close() + }, + }) + return new Response(stream, { + headers: { 'Content-Type': 'text/plain' }, + }) + }), + ) + + await using testApp = await spawnTestApp(require.resolve('./use.app.js')) + + const response = await fetch(new URL('/resource', testApp.url)) + expect(response.status).toBe(200) + await expect(response.text()).resolves.toBe('hello world') + }), +) diff --git a/test/node/msw-api/setup-remote-server/use.app.js b/test/node/msw-api/setup-remote-server/use.app.js index 995dd8eca..d74266fca 100644 --- a/test/node/msw-api/setup-remote-server/use.app.js +++ b/test/node/msw-api/setup-remote-server/use.app.js @@ -13,9 +13,6 @@ const server = setupServer( server.listen({ remote: { enabled: true, - // If provided, use explicit context id to bound this - // runtime to a particular `remote.boundary()` in tests. - contextId: process.env.MSW_REMOTE_CONTEXT_ID, }, }) diff --git a/test/node/msw-api/setup-remote-server/utils.ts b/test/node/msw-api/setup-remote-server/utils.ts index 93fe015a7..99035e399 100644 --- a/test/node/msw-api/setup-remote-server/utils.ts +++ b/test/node/msw-api/setup-remote-server/utils.ts @@ -1,11 +1,9 @@ import { invariant } from 'outvariant' import { ChildProcess, spawn } from 'child_process' import { DeferredPromise } from '@open-draft/deferred-promise' +import { remoteContext } from 'msw/node' -export async function spawnTestApp( - appSourcePath: string, - options?: { contextId: string }, -) { +export async function spawnTestApp(appSourcePath: string) { let url: string | undefined const spawnPromise = new DeferredPromise().then((resolvedUrl) => { url = resolvedUrl @@ -19,7 +17,7 @@ export async function spawnTestApp( stdio: ['pipe', 'pipe', 'pipe', 'ipc'], env: { ...process.env, - MSW_REMOTE_CONTEXT_ID: options?.contextId, + [remoteContext.variableName]: remoteContext.getContextId(), }, })