UNPKG

bunshine

Version:

A Bun HTTP & WebSocket server that is a little ray of sunshine.

340 lines (338 loc) 10.9 kB
import type { Server } from 'bun'; import { afterEach, beforeEach, describe, expect, it, spyOn } from 'bun:test'; import HttpRouter from '../HttpRouter/HttpRouter'; describe('server', () => { let app: HttpRouter; let server: Server; beforeEach(() => { app = new HttpRouter(); }); afterEach(() => { server.stop(true); }); it('should connect ok', async () => { const result: { binaryType: string | undefined; address: string; fruit: string; room: string; type: string; open: boolean; message: string; } = await new Promise((resolve, reject) => { app.socket.at<{ room: string }>('/chat/:room', { upgrade() { return { fruit: 'apple' }; }, open(sc) { sc.data.open = true; }, message(sc, request) { resolve({ binaryType: sc.binaryType, address: sc.remoteAddress, fruit: sc.data.fruit, room: sc.params.room, type: sc.type, open: sc.data.open, message: request.text(), }); }, }); server = app.listen({ port: 0 }); const chat = new WebSocket(`${server.url}chat/123`); chat.addEventListener('open', evt => { chat.send('hello'); }); chat.addEventListener('error', reject); }); expect(result.binaryType).toBe('nodebuffer'); expect(typeof result.address).toBe('string'); expect(result.fruit).toBe('apple'); expect(result.room).toBe('123'); expect(result.type).toBe('message'); expect(result.open).toBe(true); expect(result.message).toBe('hello'); }); it('should send buffers ok', async () => { const result: Buffer = await new Promise((resolve, reject) => { app.socket.at<{ room: string }>('/chat/:room', { open(sc) { sc.send(Buffer.from('hello')); }, }); server = app.listen({ port: 0 }); const chat = new WebSocket(`${server.url}chat/123`); chat.addEventListener('message', evt => { resolve(evt.data); }); }); expect(result).toBeInstanceOf(Buffer); expect(result.toString()).toEqual('hello'); }); it('should set binaryType ok', async () => { const result: Buffer = await new Promise((resolve, reject) => { app.socket.at<{ room: string }>('/chat/:room', { open(sc) { sc.binaryType = 'arraybuffer'; sc.send(Buffer.from('hello')); }, }); server = app.listen({ port: 0 }); const chat = new WebSocket(`${server.url}chat/123`); chat.addEventListener('message', evt => { resolve(evt.data); }); }); expect(result).toBeInstanceOf(Buffer); expect(result.toString()).toEqual('hello'); }); it('should send array buffers ok', async () => { const result: ArrayBuffer = await new Promise((resolve, reject) => { app.socket.at<{ room: string }>('/chat/:room', { open(sc) { sc.send(Buffer.from('hello').buffer); }, }); server = app.listen({ port: 0 }); const chat = new WebSocket(`${server.url}chat/123`); chat.addEventListener('message', evt => { resolve(evt.data); }); }); expect(result).toBeInstanceOf(Buffer); expect(result.toString()).toEqual('hello'); }); it('should send JSON ok', async () => { const result = await new Promise((resolve, reject) => { app.socket.at<{ room: string }>('/chat/:room', { open(sc) { sc.send({ hello: 'world' }); }, }); server = app.listen({ port: 0 }); const chat = new WebSocket(`${server.url}chat/123`); chat.addEventListener('message', evt => { resolve(JSON.parse(evt.data)); }); }); expect(result).toEqual({ hello: 'world' }); }); it('should upgrade http requests and pass messages', async () => { const result = await new Promise((resolve, reject) => { let s: Server; const result: any = []; app.socket.at<{ id: string }>('/chat/:id', { upgrade(c) { s = c.server; return { fruit: 'apple' }; }, open(sc) { result.push({ type: sc.type, fruit: sc.data.fruit, id: sc.params.id, pathname: sc.url.pathname, isServer: sc.server === s, }); }, message(sc, request) { result.push({ type: sc.type, fruit: sc.data.fruit, id: sc.params.id, pathname: sc.url.pathname, isServer: sc.server === s, message: request.text(), }); sc.send('world'); }, }); server = app.listen({ port: 0 }); const chat = new WebSocket(`${server.url}chat/123`); chat.addEventListener('open', evt => { chat.send('hello'); }); chat.addEventListener('message', evt => { result.push({ message: evt.data }); resolve(result); }); chat.addEventListener('error', evt => { console.log('client error!', evt); reject(); }); }); expect(result).toEqual([ { type: 'open', fruit: 'apple', id: '123', pathname: '/chat/123', isServer: true, }, { type: 'message', fruit: 'apple', id: '123', pathname: '/chat/123', isServer: true, message: 'hello', }, { message: 'world', }, ]); }); it('should handle errors', async () => { const errorData = await new Promise((resolve, reject) => { let data: Array<{ type: string; message: string }> = []; app.socket.at('/chat/:id', { error(sc, error) { data.push({ type: sc.type, message: error.message }); if (data.length === 2) { resolve(data); } }, open() { throw new Error('open!'); }, message() { throw new Error('message!'); }, }); server = app.listen({ port: 0 }); const chat = new WebSocket(`${server.url}chat/123`); chat.addEventListener('open', evt => { chat.send('hello'); }); }); expect(errorData).toEqual([ { type: 'open', message: 'open!' }, { type: 'message', message: 'message!' }, ]); }); it('should log unhandled errors', async () => { let message: string = ''; const spy = spyOn(console, 'error').mockImplementation(m => { message = m; }); app.socket.at('/chat/:id', { open() { throw new Error('open!'); }, }); server = app.listen({ port: 0 }); new WebSocket(`${server.url}chat/123`); await new Promise(r => setTimeout(r, 10)); expect(spy).toHaveBeenCalled(); expect(message).toContain('Unhandled WebSocket handler error'); spy.mockRestore(); }); it('should allow pub-sub', async () => { const [messages, events] = await new Promise<[string[], string[]]>( async resolve => { let messages: string[] = []; let events: string[] = []; app.socket.at<{ id: string; user: string }>('/chat/:id', { upgrade({ url, params }) { return { id: params.id, user: url.searchParams.get('user'), }; }, open(sc) { sc.subscribe(`room-${sc.data.id}`); sc.publish( `room-${sc.data.id}`, `${sc.data.user} entered the chat` ); sc.send(`${sc.data.user} entered the chat`); events.push(`${sc.data.user} entered the chat`); }, message(sc, message) { messages.push(message.text()); sc.publish(`room-${sc.data.id}`, message.toString()); sc.send(message.text()); }, close(sc) { sc.unsubscribe(`room-${sc.data.id}`); sc.publish(`room-${sc.data.id}`, `${sc.data.user} left the chat`); events.push(`${sc.data.user} left the chat`); if (events.length === 3 && messages.length === 2) { resolve([messages, events]); } }, error(sc, error) { console.log('error', error); }, }); server = app.listen({ port: 0 }); const user1 = new WebSocket(`${server.url}chat/123?user=a`); const user2 = new WebSocket(`${server.url}chat/123?user=b`); await Promise.all([ new Promise(r => user1.addEventListener('open', r)), new Promise(r => user2.addEventListener('open', r)), ]); user1.send('1.1'); await new Promise(r => setTimeout(r, 10)); server.publish(`room-123`, 'hello'); await new Promise(r => setTimeout(r, 10)); user2.send('2.1'); await new Promise(r => setTimeout(r, 10)); user2.close(); await new Promise(r => setTimeout(r, 10)); user1.send('1.2'); await new Promise(r => setTimeout(r, 10)); user2.close(); } ); // Note: we don't hear 1.2 because user2 is closed expect(messages).toEqual(['1.1', '2.1']); expect(events.includes('a entered the chat')).toBe(true); expect(events.includes('b entered the chat')).toBe(true); expect(events.includes('b left the chat')).toBe(true); // Note: we don't hear that "a left the chat" // because no one is subscribed to the room to hear it }); it('should close with reason', async () => { const result: { status: number; reason: string } = await new Promise( (resolve, reject) => { app.socket.at<{ room: string }>('/chat/:room', { message(sc, request) { sc.close(1000, 'l8r'); }, }); server = app.listen({ port: 0 }); const chat = new WebSocket(`${server.url}chat/123`); chat.addEventListener('open', evt => { chat.send('hello'); }); chat.addEventListener('close', evt => { resolve({ status: evt.code, reason: evt.reason }); }); } ); expect(result.status).toBe(1000); expect(result.reason).toBe('l8r'); }); it('should terminate', async () => { const result: { status: number; reason: string } = await new Promise( (resolve, reject) => { app.socket.at<{ room: string }>('/chat/:room', { message(sc, request) { sc.terminate(); }, }); server = app.listen({ port: 0 }); const chat = new WebSocket(`${server.url}chat/456`); chat.addEventListener('open', evt => { chat.send('hello'); }); chat.addEventListener('close', evt => { resolve({ status: evt.code, reason: evt.reason }); }); } ); expect(result.status).toBe(1006); // abnormal closure }); });