import { expect } from 'chai'; import { Server, WebSocket } from 'mock-socket'; import type {Server as HttpServer} from 'node:http'; import { Realm } from '../../../src/models/realm'; import { WebSocketServer } from '../../../src/services/webSocketServer'; import { Errors, MessageType } from '../../../src/enums'; import { wait } from '../../utils'; type Destroyable = T & { destroy?: () => Promise; }; const checkOpen = async (c: WebSocket): Promise => { return new Promise(resolve => { c.onmessage = (event: object & { data?: string; }): void => { const message = JSON.parse(event.data as string); resolve(message.type === MessageType.OPEN); }; }); }; const checkSequence = async (c: WebSocket, msgs: { type: MessageType; error?: Errors; }[]): Promise => { return new Promise(resolve => { const restMessages = [...msgs]; const finish = (success = false): void => { resolve(success); }; c.onmessage = (event: object & { data?: string; }): void => { const [mes] = restMessages; if (!mes) { return finish(); } restMessages.shift(); const message = JSON.parse(event.data as string); if (message.type !== mes.type) { return finish(); } const isOk = !mes.error || message.payload?.msg === mes.error; if (!isOk) { return finish(); } if (restMessages.length === 0) { finish(true); } }; }); }; const createTestServer = ({ realm, config, url }: { realm: Realm; config: { path: string; key: string; concurrent_limit: number; }; url: string; }): Destroyable => { const server = new Server(url) as Server & HttpServer; const webSocketServer: Destroyable = new WebSocketServer({ server, realm, config }); server.on('connection', (socket: WebSocket & { on?: (eventName: string, callback: () => void) => void; }) => { const s = webSocketServer.socketServer; s.emit('connection', socket, { url: socket.url }); socket.onclose = (): void => { const userId = socket.url.split('?')[1]?.split('&').find(p => p.startsWith('id'))?.split('=')[1]; if (!userId) return; const client = realm.getClientById(userId); const clientSocket = client?.getSocket(); if (!clientSocket) return; (clientSocket as unknown as WebSocket).listeners['server::close']?.forEach((s: () => void) => s()); }; socket.onmessage = (event: object & { data?: string; }): void => { const userId = socket.url.split('?')[1]?.split('&').find(p => p.startsWith('id'))?.split('=')[1]; if (!userId) return; const client = realm.getClientById(userId); const clientSocket = client?.getSocket(); if (!clientSocket) return; (clientSocket as unknown as WebSocket).listeners['server::message']?.forEach((s: (data: object) => void) => s(event)); }; }); webSocketServer.destroy = async (): Promise => { server.close(); }; return webSocketServer; }; describe('WebSocketServer', () => { it('should return valid path', () => { const realm = new Realm(); const config = { path: '/', key: 'testKey', concurrent_limit: 1 }; const config2 = { ...config, path: 'path' }; const server = new Server('path1') as Server & HttpServer; const server2 = new Server('path2') as Server & HttpServer; const webSocketServer = new WebSocketServer({ server, realm, config }); expect(webSocketServer.path).to.be.eq('/peerjs'); const webSocketServer2 = new WebSocketServer({ server: server2, realm, config: config2 }); expect(webSocketServer2.path).to.be.eq('path/peerjs'); server.stop(); server2.stop(); }); it(`should check client's params`, async () => { const realm = new Realm(); const config = { path: '/', key: 'testKey', concurrent_limit: 1 }; const fakeURL = 'ws://localhost:8080/peerjs'; const getError = async (url: string, validError: Errors = Errors.INVALID_WS_PARAMETERS): Promise => { const webSocketServer = createTestServer({ url, realm, config }); const ws = new WebSocket(url); const errorSent = await checkSequence(ws, [{ type: MessageType.ERROR, error: validError }]); ws.close(); await webSocketServer.destroy?.(); return errorSent; }; expect(await getError(fakeURL)).to.be.true; expect(await getError(`${fakeURL}?key=${config.key}`)).to.be.true; expect(await getError(`${fakeURL}?key=${config.key}&id=1`)).to.be.true; expect(await getError(`${fakeURL}?key=notValidKey&id=userId&token=userToken`, Errors.INVALID_KEY)).to.be.true; }); it(`should check concurrent limit`, async () => { const realm = new Realm(); const config = { path: '/', key: 'testKey', concurrent_limit: 1 }; const fakeURL = 'ws://localhost:8080/peerjs'; const createClient = (id: string): Destroyable => { // id in the path ensures that all mock servers listen on different urls const url = `${fakeURL}${id}?key=${config.key}&id=${id}&token=${id}`; const webSocketServer = createTestServer({ url, realm, config }); const ws: Destroyable = new WebSocket(url); ws.destroy = async (): Promise => { ws.close(); wait(10); webSocketServer.destroy?.(); wait(10); ws.destroy = undefined; }; return ws; }; const c1 = createClient('1'); expect(await checkOpen(c1)).to.be.true; const c2 = createClient('2'); expect(await checkSequence(c2, [ { type: MessageType.ERROR, error: Errors.CONNECTION_LIMIT_EXCEED } ])).to.be.true; await c1.destroy?.(); await c2.destroy?.(); await wait(10); expect(realm.getClientsIds().length).to.be.eq(0); const c3 = createClient('3'); expect(await checkOpen(c3)).to.be.true; await c3.destroy?.(); }); });