Skip to content

Commit

Permalink
fix: use consistent localhost for remote server
Browse files Browse the repository at this point in the history
  • Loading branch information
kettanaito committed Dec 18, 2024
1 parent 9d04f0a commit cfc120c
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 128 deletions.
54 changes: 31 additions & 23 deletions src/node/SetupServerApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
6 changes: 5 additions & 1 deletion src/node/setupRemoteServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -276,7 +280,7 @@ async function createSyncServer(port: number): Promise<http.Server> {
const serverReadyPromise = new DeferredPromise<http.Server>()
const server = http.createServer()

server.listen(+port, '127.0.0.1', () => {
server.listen(port, 'localhost', async () => {
serverReadyPromise.resolve(server)
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
213 changes: 121 additions & 92 deletions test/node/msw-api/setup-remote-server/response.body.test.ts
Original file line number Diff line number Diff line change
@@ -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(() => {
Expand All @@ -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')
}),
)
3 changes: 0 additions & 3 deletions test/node/msw-api/setup-remote-server/use.app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
})

Expand Down
8 changes: 3 additions & 5 deletions test/node/msw-api/setup-remote-server/utils.ts
Original file line number Diff line number Diff line change
@@ -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<string>().then((resolvedUrl) => {
url = resolvedUrl
Expand All @@ -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(),
},
})

Expand Down

0 comments on commit cfc120c

Please sign in to comment.