const { initializeSocket } = require('../socket'); const { createServer } = require('http'); const { Server } = require('socket.io'); const Client = require('socket.io-client'); const { generateToken } = require('../utils/auth'); const { prisma } = require('../utils/db'); describe('Socket.IO WebRTC Signaling', () => { let httpServer; let io; let clientSocket1; let clientSocket2; let token1; let token2; let testUser1; let testUser2; let testEvent; let testMatch; beforeAll(async () => { // Create test users testUser1 = await prisma.user.create({ data: { username: 'webrtc_user1', email: 'webrtc1@test.com', passwordHash: 'hash', emailVerified: true, }, }); testUser2 = await prisma.user.create({ data: { username: 'webrtc_user2', email: 'webrtc2@test.com', passwordHash: 'hash', emailVerified: true, }, }); // Create test event testEvent = await prisma.event.create({ data: { name: 'WebRTC Test Event', location: 'Test Location', startDate: new Date('2025-01-01'), endDate: new Date('2025-01-02'), }, }); // Create chat room for the match const chatRoom = await prisma.chatRoom.create({ data: { type: 'private', }, }); // Create test match testMatch = await prisma.match.create({ data: { user1Id: testUser1.id, user2Id: testUser2.id, eventId: testEvent.id, roomId: chatRoom.id, status: 'accepted', }, }); // Generate tokens token1 = generateToken({ userId: testUser1.id }); token2 = generateToken({ userId: testUser2.id }); // Create HTTP server and Socket.IO httpServer = createServer(); io = initializeSocket(httpServer); await new Promise((resolve) => { httpServer.listen(() => resolve()); }); }); afterAll(async () => { await prisma.match.deleteMany({ where: { OR: [ { user1Id: testUser1.id }, { user2Id: testUser2.id }, ], }, }); await prisma.user.deleteMany({ where: { id: { in: [testUser1.id, testUser2.id], }, }, }); await prisma.event.deleteMany({ where: { id: testEvent.id, }, }); io.close(); httpServer.close(); }); beforeEach((done) => { const port = httpServer.address().port; // Connect first client clientSocket1 = Client(`http://localhost:${port}`, { auth: { token: token1 }, transports: ['websocket'], }); clientSocket1.on('connect', () => { // Connect second client clientSocket2 = Client(`http://localhost:${port}`, { auth: { token: token2 }, transports: ['websocket'], }); clientSocket2.on('connect', () => { done(); }); }); }); afterEach(() => { if (clientSocket1?.connected) clientSocket1.disconnect(); if (clientSocket2?.connected) clientSocket2.disconnect(); }); describe('WebRTC Offer', () => { it('should relay WebRTC offer to the other user in match room', (done) => { const offerData = { type: 'offer', sdp: 'mock-sdp-offer-data', }; // User 1 and User 2 join match room clientSocket1.emit('join_match_room', { matchId: testMatch.id }); clientSocket2.emit('join_match_room', { matchId: testMatch.id }); // User 2 listens for offer clientSocket2.on('webrtc_offer', (data) => { expect(data.from).toBe(testUser1.id); expect(data.offer).toEqual(offerData); done(); }); // User 1 sends offer setTimeout(() => { clientSocket1.emit('webrtc_offer', { matchId: testMatch.id, offer: offerData, }); }, 100); }); it('should not relay offer to self', (done) => { const offerData = { type: 'offer', sdp: 'mock-sdp-offer-data', }; clientSocket1.emit('join_match_room', { matchId: testMatch.id }); let receivedOffer = false; clientSocket1.on('webrtc_offer', () => { receivedOffer = true; }); clientSocket1.emit('webrtc_offer', { matchId: testMatch.id, offer: offerData, }); setTimeout(() => { expect(receivedOffer).toBe(false); done(); }, 200); }); it('should require authorization for the match', async () => { // Create a chat room for the unauthorized match const unauthorizedRoom = await prisma.chatRoom.create({ data: { type: 'private', }, }); // Create a match that user1 is NOT part of const unauthorizedMatch = await prisma.match.create({ data: { user1Id: testUser2.id, user2Id: testUser2.id, // Both same user eventId: testEvent.id, roomId: unauthorizedRoom.id, status: 'accepted', }, }); // Wait for error event const errorPromise = new Promise((resolve) => { clientSocket1.on('error', (error) => { resolve(error); }); }); clientSocket1.emit('webrtc_offer', { matchId: unauthorizedMatch.id, offer: { type: 'offer', sdp: 'test' }, }); const error = await errorPromise; expect(error.message).toContain('authorized'); // Cleanup await prisma.match.delete({ where: { id: unauthorizedMatch.id } }); }); }); describe('WebRTC Answer', () => { it('should relay WebRTC answer to the other user in match room', (done) => { const answerData = { type: 'answer', sdp: 'mock-sdp-answer-data', }; clientSocket1.emit('join_match_room', { matchId: testMatch.id }); clientSocket2.emit('join_match_room', { matchId: testMatch.id }); // User 1 listens for answer clientSocket1.on('webrtc_answer', (data) => { expect(data.from).toBe(testUser2.id); expect(data.answer).toEqual(answerData); done(); }); // User 2 sends answer setTimeout(() => { clientSocket2.emit('webrtc_answer', { matchId: testMatch.id, answer: answerData, }); }, 100); }); }); describe('WebRTC ICE Candidate', () => { it('should relay ICE candidate to the other user in match room', (done) => { const candidateData = { candidate: 'mock-ice-candidate', sdpMid: 'data', sdpMLineIndex: 0, }; clientSocket1.emit('join_match_room', { matchId: testMatch.id }); clientSocket2.emit('join_match_room', { matchId: testMatch.id }); // User 2 listens for ICE candidate clientSocket2.on('webrtc_ice_candidate', (data) => { expect(data.from).toBe(testUser1.id); expect(data.candidate).toEqual(candidateData); done(); }); // User 1 sends ICE candidate setTimeout(() => { clientSocket1.emit('webrtc_ice_candidate', { matchId: testMatch.id, candidate: candidateData, }); }, 100); }); it('should not relay ICE candidate to self', (done) => { const candidateData = { candidate: 'mock-ice-candidate', sdpMid: 'data', sdpMLineIndex: 0, }; clientSocket1.emit('join_match_room', { matchId: testMatch.id }); let receivedCandidate = false; clientSocket1.on('webrtc_ice_candidate', () => { receivedCandidate = true; }); clientSocket1.emit('webrtc_ice_candidate', { matchId: testMatch.id, candidate: candidateData, }); setTimeout(() => { expect(receivedCandidate).toBe(false); done(); }, 200); }); }); describe('WebRTC Full Flow', () => { it('should successfully exchange offer, answer, and ICE candidates', (done) => { const offerData = { type: 'offer', sdp: 'offer-sdp' }; const answerData = { type: 'answer', sdp: 'answer-sdp' }; const candidateData = { candidate: 'ice-candidate' }; let user1ReceivedAnswer = false; let user1ReceivedCandidate = false; let user2ReceivedOffer = false; let user2ReceivedCandidate = false; clientSocket1.emit('join_match_room', { matchId: testMatch.id }); clientSocket2.emit('join_match_room', { matchId: testMatch.id }); // User 1 listeners clientSocket1.on('webrtc_answer', (data) => { expect(data.from).toBe(testUser2.id); user1ReceivedAnswer = true; checkCompletion(); }); clientSocket1.on('webrtc_ice_candidate', (data) => { expect(data.from).toBe(testUser2.id); user1ReceivedCandidate = true; checkCompletion(); }); // User 2 listeners clientSocket2.on('webrtc_offer', (data) => { expect(data.from).toBe(testUser1.id); user2ReceivedOffer = true; // User 2 sends answer clientSocket2.emit('webrtc_answer', { matchId: testMatch.id, answer: answerData, }); // User 2 sends ICE candidate clientSocket2.emit('webrtc_ice_candidate', { matchId: testMatch.id, candidate: candidateData, }); checkCompletion(); }); clientSocket2.on('webrtc_ice_candidate', (data) => { expect(data.from).toBe(testUser1.id); user2ReceivedCandidate = true; checkCompletion(); }); function checkCompletion() { if ( user1ReceivedAnswer && user1ReceivedCandidate && user2ReceivedOffer && user2ReceivedCandidate ) { done(); } } // User 1 starts: send offer and ICE candidate setTimeout(() => { clientSocket1.emit('webrtc_offer', { matchId: testMatch.id, offer: offerData, }); clientSocket1.emit('webrtc_ice_candidate', { matchId: testMatch.id, candidate: candidateData, }); }, 100); }); }); });