Add test coverage for WebRTC signaling and detection: Backend tests (socket-webrtc.test.js): - WebRTC offer/answer relay via Socket.IO - ICE candidate exchange - Authorization checks for match access - Full WebRTC signaling flow - All 7 tests passing Frontend tests (ready for test runner): - webrtcDetection.test.js: Browser WebRTC capability detection - WebRTCWarning.test.jsx: Warning component rendering and interaction Note: Frontend tests require test runner setup (e.g., Vitest)
385 lines
9.9 KiB
JavaScript
385 lines
9.9 KiB
JavaScript
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);
|
|
});
|
|
});
|
|
});
|