UNPKG

bunshine

Version:

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

171 lines (168 loc) 5.08 kB
import type { Server } from 'bun'; import { afterEach, beforeEach, describe, expect, it, spyOn } from 'bun:test'; import { EventSource } from 'eventsource'; import HttpRouter from '../../HttpRouter/HttpRouter'; type SseTestEvent = { type: string; data: string; lastEventId?: string; origin?: string; }; describe('sse', () => { let app: HttpRouter; let server: Server; beforeEach(() => { app = new HttpRouter(); server = app.listen({ port: 0 }); }); afterEach(() => { server.stop(true); }); function sseTest({ payloads, event, headers = {}, }: { port: number; payloads: | Array<[string, any, string, number]> | Array<[string, any, string]> | Array<[string, any]> | Array<[string]>; event: string; headers?: Record<string, string>; }): Promise<SseTestEvent[]> { return new Promise((outerResolve, outerReject) => { const events: SseTestEvent[] = []; const readyToSend = new Promise((resolve, reject) => { app.get('/sse', c => { return c.sse( send => { resolve(() => { for (const payload of payloads) { // @ts-expect-error send(...payload); } }); }, { headers } ); }); app.onError(c => reject(c.error)); }) as Promise<() => void>; const readyToListen = new Promise((resolve, reject) => { const stream = new EventSource(`${server.url}sse`); stream.addEventListener('error', evt => { reject(evt); stream.close(); }); stream.addEventListener(event, evt => { events.push(evt); if (events.length === payloads.length) { outerResolve(events); stream.close(); } }); resolve(server.port); }) as Promise<number>; Promise.all([readyToSend, readyToListen]) .then(([doSend]) => doSend()) .catch(outerReject); }); } it('should handle unnamed data', async () => { const events = await sseTest({ event: 'message', port: 0, payloads: [['Hello'], ['World']], }); expect(events.length).toBe(2); expect(events[0].data).toBe('Hello'); expect(events[1].data).toBe('World'); }); it('should send last event id and origin', async () => { const events = await sseTest({ event: 'myEvent', port: 0, payloads: [ ['myEvent', 'hi1', 'id1'], ['myEvent', 'hi2', 'id2'], ], }); expect(events.length).toBe(2); expect(events[0].data).toBe('hi1'); expect(events[1].data).toBe('hi2'); expect(events[0].lastEventId).toBe('id1'); expect(events[1].lastEventId).toBe('id2'); expect(events[0].origin).toStartWith(`http://localhost:`); expect(events[0].origin).toBe(String(events[1].origin)); }); it('should accept retry milliseconds', async () => { const events = await sseTest({ event: 'myEvent', port: 0, payloads: [ ['myEvent', 'hi1', 'id1', 10000], ['myEvent', 'hi2', 'id2', 10000], ], }); expect(events.length).toBe(2); expect(events[0].data).toBe('hi1'); expect(events[1].data).toBe('hi2'); expect(events[0].lastEventId).toBe('id1'); expect(events[1].lastEventId).toBe('id2'); expect(events[0].origin).toStartWith(`http://localhost:`); expect(events[0].origin).toBe(String(events[1].origin)); }); it('should JSON encode data if needed', async () => { const events = await sseTest({ event: 'myEvent', port: 0, payloads: [['myEvent', { name: 'Bob' }]], }); expect(events.length).toBe(1); expect(events[0].data).toBe('{"name":"Bob"}'); }); it('should allow newlines', async () => { const events = await sseTest({ event: 'message', port: 0, payloads: [['Hello\n\n\n3'], ['World\n\n2']], }); expect(events.length).toBe(2); expect(events[0].data).toBe('Hello\n\n\n3'); expect(events[1].data).toBe('World\n\n2'); }); it('should warn when overriding some headers', async () => { spyOn(console, 'warn').mockImplementation(() => {}); await sseTest({ event: 'myEvent', port: 0, payloads: [['myEvent', { name: 'Bob' }]], headers: { 'Content-Type': 'text/plain', 'Cache-Control': 'foo', Connection: 'whatever', }, }); expect(console.warn).toHaveBeenCalledTimes(3); // @ts-expect-error console.warn.mockRestore(); }); it('should not warn if those headers are correct', async () => { spyOn(console, 'warn').mockImplementation(() => {}); await sseTest({ event: 'myEvent', port: 0, payloads: [['myEvent', { name: 'Bob' }]], headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive', }, }); expect(console.warn).toHaveBeenCalledTimes(0); // @ts-expect-error console.warn.mockRestore(); }); });