import { jest } from '@jest/globals'; import fs from 'fs'; import * as Path from 'path'; import process from 'process'; import { setup, teardown } from 'jest-dev-server'; import { Signaling, WebSocketSignaling } from "../../src/core/signaling.js"; import { MockSignaling, reset } from "../mocks/mocksignaling.js"; import { waitFor, sleep, serverExeName } from "../helpers/testutils.js"; const portNumber = 8081; const runSignalingIntegration = process.env.RUN_SIGNALING_INTEGRATION === '1'; const signalingModes = runSignalingIntegration ? [{ mode: "mock" }, { mode: "http" }, { mode: "websocket" }] : [{ mode: "mock" }]; jest.setTimeout(10000); function buildServerCommand(args = '') { const binaryPath = Path.resolve(`../bin~/${serverExeName()}`); const buildEntryPath = Path.resolve('../build/index.js'); const serverCommand = fs.existsSync(binaryPath) ? `"${binaryPath}"` : `"${process.execPath}" "${buildEntryPath}"`; return `${serverCommand} ${args}`.trim(); } function getPortForMode(mode, isPrivate) { if (mode === 'mock') { return portNumber; } const publicPorts = { http: portNumber + 1, websocket: portNumber + 2 }; const privatePorts = { http: portNumber + 3, websocket: portNumber + 4 }; return (isPrivate ? privatePorts : publicPorts)[mode]; } function createHttpSignaling(port) { return new Signaling(1, `http://localhost:${port}`); } function createWebSocketSignaling(port) { return new WebSocketSignaling(1, `ws://localhost:${port}`); } describe('recording signaling message envelope', () => { const OriginalWebSocket = window.WebSocket; let sentMessages; beforeEach(() => { sentMessages = []; window.WebSocket = class { constructor() { this.readyState = 1; } send(message) { sentMessages.push(message); } close() { if (this.onclose) { this.onclose(); } } }; }); afterEach(() => { window.WebSocket = OriginalWebSocket; }); test('sends recording offer through on-message', () => { const signaling = new WebSocketSignaling(1, 'ws://localhost:1234'); signaling.sendRecordingOffer({ recordingId: 'recording-1', connectionId: 'room-1', participantId: 'participant-1', sdp: 'offer-sdp' }); expect(sentMessages).toHaveLength(1); const outer = JSON.parse(sentMessages[0]); expect(outer.type).toBe('on-message'); expect(outer.data.connectionId).toBe('room-1'); expect(outer.data.message).toEqual({ type: 'recording-offer', data: { recordingId: 'recording-1', connectionId: 'room-1', participantId: 'participant-1', sdp: 'offer-sdp' } }); }); test('dispatches wrapped recording messages as recording events', () => { const signaling = new WebSocketSignaling(1, 'ws://localhost:1234'); let recordingAnswer; signaling.addEventListener('recording-answer', (event) => { recordingAnswer = event.detail; }); signaling.websocket.onmessage({ data: JSON.stringify({ type: 'on-message', from: 'room-1', data: JSON.stringify({ type: 'recording-answer', data: { recordingId: 'recording-1', connectionId: 'room-1', sdp: 'answer-sdp' } }) }) }); expect(recordingAnswer).toEqual({ type: 'recording-answer', recordingId: 'recording-1', connectionId: 'room-1', sdp: 'answer-sdp' }); }); }); describe.each(signalingModes)('signaling test in public mode', ({ mode }) => { let signaling1; let signaling2; const connectionId1 = "12345"; const connectionId2 = "67890"; const testsdp = "test sdp"; const testcandidate = "test candidate"; beforeAll(async () => { if (mode == "mock") { reset(false); signaling1 = new MockSignaling(1); signaling2 = new MockSignaling(1); } else { const serverPort = getPortForMode(mode, false); let cmd = buildServerCommand(`-p ${serverPort}`); if (mode == "http") { cmd += " -t http"; } await setup({ command: cmd, port: serverPort, usedPortAction: 'error' }); if (mode == "http") { signaling1 = createHttpSignaling(serverPort); signaling2 = createHttpSignaling(serverPort); } if (mode == "websocket") { signaling1 = createWebSocketSignaling(serverPort); signaling2 = createWebSocketSignaling(serverPort); } } await signaling1.start(); await signaling2.start(); }); afterAll(async () => { await signaling1?.stop(); await signaling2?.stop(); signaling1 = null; signaling2 = null; if (mode == "mock") { return; } await teardown(); // work around for linux, waitng kill server process await sleep(1000); }); test(`onConnect using ${mode}`, async () => { const signaling1Spy = jest.spyOn(signaling1, 'dispatchEvent'); let connectRes; let disconnectRes; signaling1.addEventListener('connect', (e) => connectRes = e.detail); signaling1.addEventListener('disconnect', (e) => disconnectRes = e.detail); await signaling1.createConnection(connectionId1); await waitFor(() => connectRes != null); expect(connectRes.connectionId).toBe(connectionId1); expect(connectRes.polite).toBe(true); await signaling1.deleteConnection(connectionId1); await waitFor(() => disconnectRes != null); expect(disconnectRes.connectionId).toBe(connectionId1); const disconnectCalledCount = signaling1Spy.mock.calls.map(x => x[0].type).filter(x => x == "disconnect").length; expect(disconnectCalledCount).toBe(1); signaling1Spy.mockRestore(); }); test(`onOffer using ${mode}`, async () => { let connectRes1; let disconnectRes1; signaling1.addEventListener('connect', (e) => connectRes1 = e.detail); signaling1.addEventListener('disconnect', (e) => disconnectRes1 = e.detail); let connectRes2; let disconnectRes2; let offerRes2; signaling2.addEventListener('connect', (e) => connectRes2 = e.detail); signaling2.addEventListener('disconnect', (e) => disconnectRes2 = e.detail); signaling2.addEventListener('offer', (e) => offerRes2 = e.detail); await signaling1.createConnection(connectionId1); await signaling2.createConnection(connectionId2); await waitFor(() => connectRes1 != null && connectRes2 != null); expect(connectRes1.connectionId).toBe(connectionId1); expect(connectRes2.connectionId).toBe(connectionId2); await signaling1.sendOffer(connectionId1, testsdp); await waitFor(() => offerRes2 != null); expect(offerRes2.connectionId).toBe(connectionId1); expect(offerRes2.polite).toBe(false); await signaling1.deleteConnection(connectionId1); await waitFor(() => disconnectRes1 != null); expect(disconnectRes1.connectionId).toBe(connectionId1); await signaling2.deleteConnection(connectionId2); await waitFor(() => disconnectRes2 != null); expect(disconnectRes2.connectionId).toBe(connectionId2); }); test(`onAnswer using ${mode}`, async () => { let connectRes1; let disconnectRes1; let answerRes1; signaling1.addEventListener('connect', (e) => connectRes1 = e.detail); signaling1.addEventListener('disconnect', (e) => disconnectRes1 = e.detail); signaling1.addEventListener('answer', (e) => answerRes1 = e.detail); let connectRes2; let disconnectRes2; let offerRes2; signaling2.addEventListener('connect', (e) => connectRes2 = e.detail); signaling2.addEventListener('disconnect', (e) => disconnectRes2 = e.detail); signaling2.addEventListener('offer', (e) => offerRes2 = e.detail); await signaling1.createConnection(connectionId1); await signaling2.createConnection(connectionId2); await waitFor(() => connectRes1 != null && connectRes2 != null); await signaling1.sendOffer(connectionId1, testsdp); await waitFor(() => offerRes2 != null); expect(offerRes2.connectionId).toBe(connectionId1); expect(offerRes2.sdp).toBe(testsdp); signaling2.sendAnswer(connectionId1, testsdp); await waitFor(() => answerRes1 != null); expect(answerRes1.connectionId).toBe(connectionId1); expect(answerRes1.sdp).toBe(testsdp); await signaling1.deleteConnection(connectionId1); await waitFor(() => disconnectRes1 != null); await signaling2.deleteConnection(connectionId2); await waitFor(() => disconnectRes2 != null); }); test(`onCandidate using ${mode}`, async () => { let connectRes1; let disconnectRes1; let answerRes1; let candidateRes1; signaling1.addEventListener('connect', (e) => connectRes1 = e.detail); signaling1.addEventListener('disconnect', (e) => disconnectRes1 = e.detail); signaling1.addEventListener('answer', (e) => answerRes1 = e.detail); signaling1.addEventListener('candidate', (e) => candidateRes1 = e.detail); let connectRes2; let disconnectRes2; let offerRes2; let candidateRes2; signaling2.addEventListener('connect', (e) => connectRes2 = e.detail); signaling2.addEventListener('disconnect', (e) => disconnectRes2 = e.detail); signaling2.addEventListener('offer', (e) => offerRes2 = e.detail); signaling2.addEventListener('candidate', (e) => candidateRes2 = e.detail); await signaling1.createConnection(connectionId1); await signaling2.createConnection(connectionId2); await waitFor(() => connectRes1 != null && connectRes2 != null); await signaling1.sendOffer(connectionId1, testsdp); await waitFor(() => offerRes2 != null); expect(offerRes2.connectionId).toBe(connectionId1); expect(offerRes2.sdp).toBe(testsdp); signaling2.sendAnswer(connectionId1, testsdp); await waitFor(() => answerRes1 != null); expect(answerRes1.connectionId).toBe(connectionId1); expect(answerRes1.sdp).toBe(testsdp); await signaling2.sendCandidate(connectionId1, testcandidate, 1, 1); await waitFor(() => candidateRes1 != null); expect(candidateRes1.connectionId).toBe(connectionId1); expect(candidateRes1.candidate).toBe(testcandidate); expect(candidateRes1.sdpMid).toBe(1); expect(candidateRes1.sdpMLineIndex).toBe(1); await signaling1.sendCandidate(connectionId1, testcandidate, 1, 1); await waitFor(() => candidateRes2 != null); expect(candidateRes2.connectionId).toBe(connectionId1); expect(candidateRes2.candidate).toBe(testcandidate); expect(candidateRes2.sdpMid).toBe(1); expect(candidateRes2.sdpMLineIndex).toBe(1); await signaling1.deleteConnection(connectionId1); await waitFor(() => disconnectRes1 != null); await signaling2.deleteConnection(connectionId2); await waitFor(() => disconnectRes2 != null); }); }); describe.each(signalingModes)('signaling test in private mode', ({ mode }) => { let signaling1; let signaling2; const connectionId = "12345"; const testsdp = "test sdp"; const testcandidate = "test candidate"; beforeAll(async () => { if (mode == "mock") { reset(true); signaling1 = new MockSignaling(1); signaling2 = new MockSignaling(1); return; } const serverPort = getPortForMode(mode, true); let cmd = buildServerCommand(`-p ${serverPort} -m private`); if (mode == "http") { cmd += " -t http"; } await setup({ command: cmd, port: serverPort, usedPortAction: 'error' }); if (mode == "http") { signaling1 = createHttpSignaling(serverPort); signaling2 = createHttpSignaling(serverPort); } if (mode == "websocket") { signaling1 = createWebSocketSignaling(serverPort); signaling2 = createWebSocketSignaling(serverPort); } await signaling1.start(); await signaling2.start(); }); afterAll(async () => { await signaling1?.stop(); await signaling2?.stop(); signaling1 = null; signaling2 = null; if (mode == "mock") { return; } await teardown(); // work around for linux, waitng kill server process await sleep(1000); }); test(`onConnect using ${mode}`, async () => { let connectRes1; let disconnectRes1; signaling1.addEventListener('connect', (e) => connectRes1 = e.detail); signaling1.addEventListener('disconnect', (e) => disconnectRes1 = e.detail); let connectRes2; let disconnectRes2; signaling2.addEventListener('connect', (e) => connectRes2 = e.detail); signaling2.addEventListener('disconnect', (e) => disconnectRes2 = e.detail); await signaling1.createConnection(connectionId); await waitFor(() => connectRes1 != null); expect(connectRes1.connectionId).toBe(connectionId); expect(connectRes1.polite).toBe(false); await signaling2.createConnection(connectionId); await waitFor(() => connectRes2 != null); expect(connectRes2.connectionId).toBe(connectionId); expect(connectRes2.polite).toBe(true); await sleep(signaling1.interval * 2); await signaling1.deleteConnection(connectionId); await waitFor(() => disconnectRes1 != null && disconnectRes2 != null); expect(disconnectRes1.connectionId).toBe(connectionId); expect(disconnectRes2.connectionId).toBe(connectionId); disconnectRes2 = null; await signaling2.deleteConnection(connectionId); await waitFor(() => disconnectRes2 != null); }); test(`onOffer using ${mode}`, async () => { let connectRes1; let disconnectRes1; signaling1.addEventListener('connect', (e) => connectRes1 = e.detail); signaling1.addEventListener('disconnect', (e) => disconnectRes1 = e.detail); let connectRes2; let disconnectRes2; let offerRes2; signaling2.addEventListener('connect', (e) => connectRes2 = e.detail); signaling2.addEventListener('disconnect', (e) => disconnectRes2 = e.detail); signaling2.addEventListener('offer', (e) => offerRes2 = e.detail); await signaling1.createConnection(connectionId); await waitFor(() => connectRes1 != null); expect(connectRes1.connectionId).toBe(connectionId); signaling1.sendOffer(connectionId, testsdp); await sleep(signaling1.interval * 2); // Do not receive offer other signaling if not connected same sendoffer connectionId in private mode expect(offerRes2).toBeUndefined(); await signaling2.createConnection(connectionId); await waitFor(() => connectRes2 != null); expect(connectRes2.connectionId).toBe(connectionId); await signaling1.sendOffer(connectionId, testsdp); await waitFor(() => offerRes2 != null); expect(offerRes2.connectionId).toBe(connectionId); expect(offerRes2.polite).toBe(true); await signaling1.deleteConnection(connectionId); await waitFor(() => disconnectRes1 != null && disconnectRes2 != null); expect(disconnectRes1.connectionId).toBe(connectionId); expect(disconnectRes2.connectionId).toBe(connectionId); disconnectRes2 = null; await signaling2.deleteConnection(connectionId); await waitFor(() => disconnectRes2 != null); }); test(`onAnswer using ${mode}`, async () => { let connectRes1; let disconnectRes1; let answerRes1; signaling1.addEventListener('connect', (e) => connectRes1 = e.detail); signaling1.addEventListener('disconnect', (e) => disconnectRes1 = e.detail); signaling1.addEventListener('answer', (e) => answerRes1 = e.detail); let connectRes2; let disconnectRes2; let offerRes2; signaling2.addEventListener('connect', (e) => connectRes2 = e.detail); signaling2.addEventListener('disconnect', (e) => disconnectRes2 = e.detail); signaling2.addEventListener('offer', (e) => offerRes2 = e.detail); await signaling1.createConnection(connectionId); await signaling2.createConnection(connectionId); await waitFor(() => connectRes1 != null && connectRes2 != null); await signaling1.sendOffer(connectionId, testsdp); await waitFor(() => offerRes2 != null); expect(offerRes2.connectionId).toBe(connectionId); expect(offerRes2.sdp).toBe(testsdp); await signaling2.sendAnswer(connectionId, testsdp); await waitFor(() => answerRes1 != null); expect(answerRes1.connectionId).toBe(connectionId); expect(answerRes1.sdp).toBe(testsdp); await signaling1.deleteConnection(connectionId); await waitFor(() => disconnectRes1 != null && disconnectRes2 != null); expect(disconnectRes1.connectionId).toBe(connectionId); expect(disconnectRes2.connectionId).toBe(connectionId); disconnectRes2 = null; await signaling2.deleteConnection(connectionId); await waitFor(() => disconnectRes2 != null); }); test(`onCandidate using ${mode}`, async () => { let connectRes1; let disconnectRes1; let answerRes1; let candidateRes1; signaling1.addEventListener('connect', (e) => connectRes1 = e.detail); signaling1.addEventListener('disconnect', (e) => disconnectRes1 = e.detail); signaling1.addEventListener('answer', (e) => answerRes1 = e.detail); signaling1.addEventListener('candidate', (e) => candidateRes1 = e.detail); let connectRes2; let disconnectRes2; let offerRes2; let candidateRes2; signaling2.addEventListener('connect', (e) => connectRes2 = e.detail); signaling2.addEventListener('disconnect', (e) => disconnectRes2 = e.detail); signaling2.addEventListener('offer', (e) => offerRes2 = e.detail); signaling2.addEventListener('candidate', (e) => candidateRes2 = e.detail); await signaling1.createConnection(connectionId); await signaling2.createConnection(connectionId); await waitFor(() => connectRes1 != null && connectRes2 != null); await signaling1.sendOffer(connectionId, testsdp); await waitFor(() => offerRes2 != null); expect(offerRes2.connectionId).toBe(connectionId); expect(offerRes2.sdp).toBe(testsdp); await signaling2.sendAnswer(connectionId, testsdp); await waitFor(() => answerRes1 != null); expect(answerRes1.connectionId).toBe(connectionId); expect(answerRes1.sdp).toBe(testsdp); await signaling2.sendCandidate(connectionId, testcandidate, 1, 1); await waitFor(() => candidateRes1 != null); expect(candidateRes1.connectionId).toBe(connectionId); expect(candidateRes1.candidate).toBe(testcandidate); expect(candidateRes1.sdpMLineIndex).toBe(1); expect(candidateRes1.sdpMid).toBe(1); await signaling1.sendCandidate(connectionId, testcandidate, 1, 1); await waitFor(() => candidateRes2 != null); expect(candidateRes2.connectionId).toBe(connectionId); expect(candidateRes2.candidate).toBe(testcandidate); expect(candidateRes2.sdpMLineIndex).toBe(1); expect(candidateRes2.sdpMid).toBe(1); await signaling1.deleteConnection(connectionId); await waitFor(() => disconnectRes1 != null && disconnectRes2 != null); expect(disconnectRes1.connectionId).toBe(connectionId); expect(disconnectRes2.connectionId).toBe(connectionId); disconnectRes2 = null; await signaling2.deleteConnection(connectionId); await waitFor(() => disconnectRes2 != null); }); test(`notReceiveOwnOfferAnswer using ${mode}`, async () => { let connectRes1; let disconnectRes1; let offerRes1; let answerRes1; signaling1.addEventListener('connect', (e) => connectRes1 = e.detail); signaling1.addEventListener('disconnect', (e) => disconnectRes1 = e.detail); let connectRes2; let disconnectRes2; let offerRes2; let answerRes2; signaling2.addEventListener('connect', (e) => connectRes2 = e.detail); signaling2.addEventListener('disconnect', (e) => disconnectRes2 = e.detail); await signaling1.createConnection(connectionId); await signaling2.createConnection(connectionId); await waitFor(() => connectRes1 != null && connectRes2 != null); signaling1.addEventListener('offer', (e) => offerRes1 = e.detail); signaling2.addEventListener('offer', (e) => offerRes2 = e.detail); await signaling1.sendOffer(connectionId, testsdp); await waitFor(() => offerRes2 != null); await sleep(signaling1.interval * 2); expect(offerRes1).toBeUndefined(); expect(offerRes2).not.toBeUndefined(); expect(offerRes2.connectionId).toBe(connectionId); expect(offerRes2.sdp).toBe(testsdp); signaling1.addEventListener('answer', (e) => answerRes1 = e.detail); signaling2.addEventListener('answer', (e) => answerRes2 = e.detail); await signaling2.sendAnswer(connectionId, testsdp); await waitFor(() => answerRes1 != null); await sleep(signaling2.interval * 2); expect(answerRes1).not.toBeUndefined(); expect(answerRes1.connectionId).toBe(connectionId); expect(answerRes1.sdp).toBe(testsdp); expect(answerRes2).toBeUndefined(); await signaling1.deleteConnection(connectionId); await waitFor(() => disconnectRes1 != null && disconnectRes2 != null); expect(disconnectRes1.connectionId).toBe(connectionId); expect(disconnectRes2.connectionId).toBe(connectionId); disconnectRes2 = null; await signaling2.deleteConnection(connectionId); await waitFor(() => disconnectRes2 != null); }); });