Skip to content

Commit

Permalink
Merge branch 'main' into mikicho-patch-1
Browse files Browse the repository at this point in the history
  • Loading branch information
mikicho authored Oct 7, 2024
2 parents 53797c9 + 343cc5a commit c24b931
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 25 deletions.
40 changes: 30 additions & 10 deletions src/interceptors/WebSocket/WebSocketServerConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,24 +138,43 @@ export class WebSocketServerConnection {

// Close the original connection when the mock client closes.
// E.g. "client.close()" was called. This is never forwarded anywhere.
this.client.addEventListener('close', this.handleMockClose.bind(this), {
signal: this.mockCloseController.signal,
})
this.client.addEventListener(
'close',
(event) => {
this.handleMockClose(event)
},
{
signal: this.mockCloseController.signal,
}
)

// Forward the "close" event to let the interceptor handle
// closures initiated by the original server.
realWebSocket.addEventListener('close', this.handleRealClose.bind(this), {
signal: this.realCloseController.signal,
})
realWebSocket.addEventListener(
'close',
(event) => {
this.handleRealClose(event)
},
{
signal: this.realCloseController.signal,
}
)

realWebSocket.addEventListener('error', () => {
const errorEvent = bindEvent(
realWebSocket,
new Event('error', { cancelable: true })
)

// Emit the "error" event on the `server` connection
// to let the interceptor react to original server errors.
this[kEmitter].dispatchEvent(bindEvent(realWebSocket, new Event('error')))
this[kEmitter].dispatchEvent(errorEvent)

// Forward original server errors to the WebSocket client.
// This ensures the client is closed if the original server errors.
this.client.dispatchEvent(bindEvent(this.client, new Event('error')))
// If the error event from the original server hasn't been prevented,
// forward it to the underlying client.
if (!errorEvent.defaultPrevented) {
this.client.dispatchEvent(bindEvent(this.client, new Event('error')))
}
})

this.realWebSocket = realWebSocket
Expand Down Expand Up @@ -273,6 +292,7 @@ export class WebSocketServerConnection {
return
}

// Close the actual client connection.
realWebSocket.close()

// Dispatch the "close" event on the `server` connection.
Expand Down
81 changes: 66 additions & 15 deletions test/modules/WebSocket/compliance/websocket.server.events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { WebSocketServer } from 'ws'
import { WebSocketInterceptor } from '../../../../src/interceptors/WebSocket/index'
import { getWsUrl } from '../utils/getWsUrl'
import { waitForWebSocketEvent } from '../utils/waitForWebSocketEvent'
import { waitForNextTick } from '../utils/waitForNextTick'

const interceptor = new WebSocketInterceptor()

Expand Down Expand Up @@ -46,25 +47,32 @@ it('emits "open" event when the server connection is open', async () => {
server.addEventListener('open', serverOpenListener)
})

new WebSocket(getWsUrl(wsServer))
const client = new WebSocket(getWsUrl(wsServer))
expect(client.readyState).toBe(WebSocket.CONNECTING)

await vi.waitFor(() => {
expect(serverOpenListener).toHaveBeenCalledTimes(1)
})

expect(client.readyState).toBe(WebSocket.OPEN)
})

it('emits "open" event if the listener was added before calling "connect()"', async () => {
const serverOpenListener = vi.fn()
interceptor.once('connection', ({ server }) => {
server.connect()
server.addEventListener('open', serverOpenListener)

server.connect()
})

new WebSocket(getWsUrl(wsServer))
const client = new WebSocket(getWsUrl(wsServer))
expect(client.readyState).toBe(WebSocket.CONNECTING)

await vi.waitFor(() => {
expect(serverOpenListener).toHaveBeenCalledTimes(1)
})

expect(client.readyState).toBe(WebSocket.OPEN)
})

it('emits "close" event when the server connection is closed', async () => {
Expand All @@ -81,12 +89,14 @@ it('emits "close" event when the server connection is closed', async () => {
server.addEventListener('close', serverCloseListener)
})

new WebSocket(getWsUrl(wsServer))
const client = new WebSocket(getWsUrl(wsServer))
expect(client.readyState).toBe(WebSocket.CONNECTING)

await vi.waitFor(() => {
expect(serverCloseListener).toHaveBeenCalledTimes(1)
})

expect(client.readyState).toBe(WebSocket.CLOSED)
expect(serverErrorListener).not.toHaveBeenCalled()
})

Expand All @@ -106,20 +116,33 @@ it('emits "close" event when the server connection is closed by the interceptor'
server.addEventListener('open', () => server.close())
})

new WebSocket(getWsUrl(wsServer))
const client = new WebSocket(getWsUrl(wsServer))
expect(client.readyState).toBe(WebSocket.CONNECTING)

await vi.waitFor(() => {
expect(serverCloseListener).toHaveBeenCalledTimes(1)
})

/**
* @note Unlike receiving the "close" event from the original server,
* closing the real server connection via `server.close()` has NO effect
* on the client. It will remain open.
*/
expect(client.readyState).toBe(WebSocket.OPEN)

const [closeEvent] = serverCloseListener.mock.calls[0]
expect(closeEvent).toHaveProperty('code', 1000)
expect(closeEvent).toHaveProperty('reason', '')

expect(serverErrorListener).not.toHaveBeenCalled()
})

it('emits both "error" and "close" events when the server connection errors', async () => {
/**
* There's a bug in Undici that doesn't dispatch the "close" event upon "error" event.
* Unskip this test once that bug is resolved.
* @see https://github.com/nodejs/undici/issues/3697
*/
it.skip('emits both "error" and "close" events when the server connection errors', async () => {
const clientCloseListener = vi.fn()
const serverErrorListener = vi.fn()
const serverCloseListener = vi.fn()
Expand All @@ -136,25 +159,53 @@ it('emits both "error" and "close" events when the server connection errors', as
* @note `server.connect()` will attempt to establish connection
* to a valid, non-existing URL. That will trigger an error.
*/
const client = new WebSocket('https://example.com/non-existing-url')
const client = new WebSocket('wss://example.com/non-existing-url')
expect(client.readyState).toBe(WebSocket.CONNECTING)

const instanceErrorListener = vi.fn()
const instanceCloseListener = vi.fn()
client.addEventListener('error', instanceErrorListener)
client.addEventListener('close', instanceCloseListener)

await vi.waitFor(() => {
expect(serverErrorListener).toHaveBeenCalledTimes(1)
expect(serverCloseListener).toHaveBeenCalledTimes(1)
})

expect(client.readyState).toBe(WebSocket.CLOSING)

await waitForNextTick()
expect(client.readyState).toBe(WebSocket.CLOSED)

// Must emit the "error" event.
expect(instanceErrorListener).toHaveBeenCalledOnce()

// Must emit the "close" event because:
// - The connection closed due to an error (non-existing host).
// - The "close" event wasn't prevented.
expect(serverCloseListener).toHaveBeenCalledOnce()
expect(clientCloseListener).toHaveBeenCalledOnce()
expect(instanceCloseListener).toHaveBeenCalledOnce()
})

it('prevents "error" event forwarding by calling "event.preventDefault()', async () => {
interceptor.once('connection', ({ server, client }) => {
server.connect()
server.addEventListener('error', (event) => {
expect(event.defaultPrevented).toBe(false)
event.preventDefault()
expect(event.defaultPrevented).toBe(true)

process.nextTick(() => client.close())
})
})

// Must not emit the "close" event because the connection
// was never established (it errored).
expect(serverCloseListener).not.toHaveBeenCalled()
expect(clientCloseListener).not.toHaveBeenCalled()
expect(instanceCloseListener).not.toHaveBeenCalled()
const client = new WebSocket('wss://non-existing-host.com/intentional')
const instanceErrorListener = vi.fn()
client.addEventListener('error', instanceErrorListener)

await waitForWebSocketEvent('close', client)

// Must emit the correct events on the WebSocket client.
expect(instanceErrorListener).toHaveBeenCalledTimes(1)
expect(instanceErrorListener).not.toHaveBeenCalled()
})

it('prevents "close" event forwarding by calling "event.preventDefault()"', async () => {
Expand Down

0 comments on commit c24b931

Please sign in to comment.