316 lines
7.8 KiB
TypeScript
316 lines
7.8 KiB
TypeScript
import { serve } from '@hono/node-server'
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
// @ts-ignore
|
|
import type { ServerType } from '@hono/node-server/dist/types'
|
|
import { Hono } from 'hono'
|
|
import { cors } from 'hono/cors'
|
|
import type { WSMessageReceive } from 'hono/ws'
|
|
import { WebSocket } from 'ws'
|
|
import { createNodeWebSocket } from '.'
|
|
|
|
describe('WebSocket helper', () => {
|
|
let app: Hono
|
|
let server: ServerType
|
|
let injectWebSocket: ReturnType<typeof createNodeWebSocket>['injectWebSocket']
|
|
let upgradeWebSocket: ReturnType<typeof createNodeWebSocket>['upgradeWebSocket']
|
|
let wss: ReturnType<typeof createNodeWebSocket>['wss']
|
|
|
|
beforeEach(async () => {
|
|
app = new Hono()
|
|
;({ injectWebSocket, upgradeWebSocket, wss } = createNodeWebSocket({ app }))
|
|
|
|
server = await new Promise<ServerType>((resolve) => {
|
|
const server = serve({ fetch: app.fetch, port: 3030 }, () => {
|
|
resolve(server)
|
|
})
|
|
})
|
|
injectWebSocket(server)
|
|
})
|
|
|
|
afterEach(() => {
|
|
server.close()
|
|
})
|
|
|
|
it('Should be inited WebSocket Context even if upgrading process is asynchronous', async () => {
|
|
const mainPromise = new Promise<boolean>((resolve) =>
|
|
app.get(
|
|
'/',
|
|
upgradeWebSocket(
|
|
() =>
|
|
new Promise((resolveWS) =>
|
|
setTimeout(() => {
|
|
resolveWS({
|
|
onOpen() {
|
|
resolve(true)
|
|
},
|
|
})
|
|
}, 100)
|
|
)
|
|
)
|
|
)
|
|
)
|
|
|
|
new WebSocket('ws://localhost:3030/')
|
|
|
|
expect(await mainPromise).toBe(true)
|
|
})
|
|
|
|
it('Should be rejected if upgradeWebSocket is not used', async () => {
|
|
app.get('/', (c) => c.body(''))
|
|
|
|
{
|
|
const ws = new WebSocket('ws://localhost:3030/')
|
|
const mainPromise = new Promise<boolean>((resolve) => {
|
|
ws.onerror = () => {
|
|
resolve(true)
|
|
}
|
|
ws.onopen = () => {
|
|
resolve(false)
|
|
}
|
|
})
|
|
|
|
expect(await mainPromise).toBe(true)
|
|
}
|
|
|
|
{
|
|
//also should rejected on fallback
|
|
const ws = new WebSocket('ws://localhost:3030/notFound')
|
|
const mainPromise = new Promise<boolean>((resolve) => {
|
|
ws.onerror = () => {
|
|
resolve(true)
|
|
}
|
|
ws.onopen = () => {
|
|
resolve(false)
|
|
}
|
|
})
|
|
|
|
expect(await mainPromise).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('Should be able to connect', async () => {
|
|
const mainPromise = new Promise<boolean>((resolve) =>
|
|
app.get(
|
|
'/',
|
|
upgradeWebSocket(() => ({
|
|
onOpen() {
|
|
resolve(true)
|
|
},
|
|
}))
|
|
)
|
|
)
|
|
|
|
new WebSocket('ws://localhost:3030/')
|
|
|
|
expect(await mainPromise).toBe(true)
|
|
})
|
|
|
|
it('Should be able to send and receive messages', async () => {
|
|
const mainPromise = new Promise((resolve) =>
|
|
app.get(
|
|
'/',
|
|
upgradeWebSocket(() => ({
|
|
onMessage(data) {
|
|
resolve(data.data)
|
|
},
|
|
}))
|
|
)
|
|
)
|
|
|
|
const ws = new WebSocket('ws://localhost:3030/')
|
|
await new Promise<void>((resolve) => ws.on('open', resolve))
|
|
ws.send('Hello')
|
|
|
|
expect(await mainPromise).toBe('Hello')
|
|
})
|
|
|
|
it('Should handle multiple concurrent connections', async () => {
|
|
const connectionCount = 5
|
|
let openConnections = 0
|
|
const messages: string[] = []
|
|
|
|
app.get(
|
|
'/',
|
|
upgradeWebSocket(() => ({
|
|
onOpen() {
|
|
openConnections++
|
|
},
|
|
onMessage(data, ws) {
|
|
messages.push(data.data as string)
|
|
ws.send(data.data as string)
|
|
},
|
|
}))
|
|
)
|
|
|
|
const connections = await Promise.all(
|
|
Array(connectionCount)
|
|
.fill(null)
|
|
.map(async () => {
|
|
const ws = new WebSocket('ws://localhost:3030/')
|
|
await new Promise<void>((resolve) => ws.on('open', resolve))
|
|
return ws
|
|
})
|
|
)
|
|
|
|
expect(openConnections).toBe(connectionCount)
|
|
|
|
await Promise.all(
|
|
connections.map((ws, index) => {
|
|
return new Promise<void>((resolve) => {
|
|
ws.send(`Hello from connection ${index + 1}`)
|
|
ws.on('message', () => {
|
|
resolve()
|
|
})
|
|
})
|
|
})
|
|
)
|
|
|
|
expect(messages.length).toBe(connectionCount)
|
|
messages.forEach((msg, index) => {
|
|
expect(msg).toBe(`Hello from connection ${index + 1}`)
|
|
})
|
|
|
|
connections.forEach((ws) => {
|
|
ws.close()
|
|
})
|
|
})
|
|
|
|
it('CloseEvent should be executed without crash', async () => {
|
|
const testCode = 3001
|
|
const testReason = 'Test!'
|
|
app.get(
|
|
'/',
|
|
upgradeWebSocket(() => ({
|
|
onClose(event) {
|
|
expect(event.code).toBe(testCode)
|
|
expect(event.reason).toBe(testReason)
|
|
},
|
|
}))
|
|
)
|
|
|
|
const ws = new WebSocket('ws://localhost:3030/')
|
|
await new Promise<void>((resolve) => ws.on('open', resolve))
|
|
ws.close(testCode, testReason)
|
|
})
|
|
|
|
it('Should be able to send and receive binary content with good length', async () => {
|
|
const mainPromise = new Promise<WSMessageReceive>((resolve) =>
|
|
app.get(
|
|
'/',
|
|
upgradeWebSocket(() => ({
|
|
onMessage(data) {
|
|
resolve(data.data)
|
|
},
|
|
}))
|
|
)
|
|
)
|
|
|
|
const binaryData = new Uint8Array(Array.from({ length: 16 }).map((_, i) => i ** 2))
|
|
|
|
const ws = new WebSocket('ws://localhost:3030/')
|
|
await new Promise<void>((resolve) => ws.on('open', resolve))
|
|
ws.send(binaryData)
|
|
|
|
const receivedMessage = await mainPromise
|
|
expect(receivedMessage).toBeInstanceOf(ArrayBuffer)
|
|
expect((receivedMessage as ArrayBuffer).byteLength).toBe(binaryData.length)
|
|
|
|
binaryData.forEach((val, idx) => {
|
|
expect(new Uint8Array(receivedMessage as ArrayBuffer)[idx]).toBe(val)
|
|
})
|
|
})
|
|
|
|
describe('Types', () => {
|
|
it('Should not throw a type error with an app with Variables generics', () => {
|
|
const app = new Hono<{
|
|
Variables: {
|
|
foo: string
|
|
}
|
|
}>()
|
|
createNodeWebSocket({ app })
|
|
})
|
|
})
|
|
|
|
it('Should client can connect when use cors()', async () => {
|
|
app.use(cors())
|
|
const mainPromise = new Promise<boolean>((resolve) =>
|
|
app.get(
|
|
'/',
|
|
upgradeWebSocket(() => ({
|
|
onOpen() {
|
|
resolve(true)
|
|
},
|
|
}))
|
|
)
|
|
)
|
|
|
|
new WebSocket('ws://localhost:3030/')
|
|
|
|
expect(await mainPromise).toBe(true)
|
|
})
|
|
it('Should client can connect even if a response has difference', async () => {
|
|
app.use(async (c, next) => {
|
|
c.res = new Response(null, c.res)
|
|
await next()
|
|
})
|
|
const mainPromise = new Promise<boolean>((resolve) =>
|
|
app.get(
|
|
'/',
|
|
upgradeWebSocket(() => ({
|
|
onOpen() {
|
|
resolve(true)
|
|
},
|
|
}))
|
|
)
|
|
)
|
|
|
|
new WebSocket('ws://localhost:3030/')
|
|
|
|
expect(await mainPromise).toBe(true)
|
|
})
|
|
|
|
it('Should not async processes to create events affect message handling', async () => {
|
|
const mainPromise = new Promise<boolean>((resolve) =>
|
|
app.get(
|
|
'/',
|
|
upgradeWebSocket(async () => {
|
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
return {
|
|
onMessage() {
|
|
resolve(true)
|
|
},
|
|
}
|
|
})
|
|
)
|
|
)
|
|
|
|
const ws = new WebSocket('ws://localhost:3030/')
|
|
ws.onopen = () => {
|
|
ws.send('Hello')
|
|
}
|
|
|
|
expect(await mainPromise).toBe(true)
|
|
})
|
|
|
|
it('Should return the wss used for the websocket helper', async () => {
|
|
let clientWs: WebSocket | null = null
|
|
const mainPromise = new Promise<void>((resolve) =>
|
|
wss.on('connection', (ws) => {
|
|
clientWs = ws
|
|
resolve()
|
|
})
|
|
)
|
|
|
|
app.get(
|
|
'/',
|
|
upgradeWebSocket(() => ({}))
|
|
)
|
|
new WebSocket('ws://localhost:3030/')
|
|
|
|
await mainPromise
|
|
|
|
expect(clientWs).toBeTruthy()
|
|
expect(wss.clients.size).toBe(1)
|
|
})
|
|
})
|