import { test } from 'node:test'; import assert from 'node:assert/strict'; import { randomBytes, randomUUID, createHmac } from 'node:crypto'; import { createServer } from 'node:http'; import { Server } from 'socket.io'; import { io as connect, type Socket } from 'socket.io-client'; import { MongoClient } from 'mongodb'; import { createGameSessionStore } from '../src/server/game-session-store.js'; import { createRoomStore } from '../src/server/rooms.js'; import { registerSignaling } from '../src/server/signaling.js'; test('signaling authenticates membership, relays only within room and releases sockets without forfeiting', { skip: !process.env.TEST_MONGODB_URI }, async () => { const client = await new MongoClient(process.env.TEST_MONGODB_URI!, { serverSelectionTimeoutMS: 5000 }).connect(); const db = client.db(`dragon_ball_test_signal_${randomUUID().replaceAll('-', '')}`); const http = createServer(), io = new Server(http); const sockets: Socket[] = []; try { const sessions = await createGameSessionStore(db), rooms = await createRoomStore(db); const a = await sessions.create(randomBytes(32).toString('base64url')), b = await sessions.create(randomBytes(32).toString('base64url')), outsider = await sessions.create(randomBytes(32).toString('base64url')); const room = await rooms.create(a.gameSessionId, randomUUID()); await rooms.join(b.gameSessionId, room.code); const signaling = registerSignaling(io, sessions, rooms, { turnUrls: ['turn:localhost:3478'], turnSecret: 'test-secret' }); await new Promise(r => http.listen(0, '127.0.0.1', r)); const url = `http://127.0.0.1:${(http.address() as any).port}/signal`; function socket(auth: typeof a) { const s = connect(url, { transports: ['websocket'], reconnection: false, autoConnect: false, auth: { ...auth, roomId: room.id } }); sockets.push(s); return s; } const sa = socket(a), sb = socket(b), stranger = socket(outsider); const ready = (s: Socket) => new Promise((r, reject) => { s.once('connect', () => r()); s.once('connect_error', reject); s.connect(); }); await Promise.all([ready(sa), ready(sb)]); await assert.rejects(ready(stranger)); const received = new Promise(r => sb.once('signal', r)); const packet = { type: 'offer', matchId: room.currentMatchId, epoch: 0, payload: { type: 'offer', sdp: 'test-sdp' } }; assert.equal((await sa.timeout(2000).emitWithAck('signal', packet)).ok, true); assert.equal((await received).payload.sdp, 'test-sdp'); assert.equal((await sa.timeout(2000).emitWithAck('signal', { ...packet, matchId: randomUUID() })).ok, false); assert.equal((await sb.timeout(2000).emitWithAck('signal', packet)).ok, false); const resumed = socket(a); await ready(resumed); await new Promise(r => setTimeout(r, 20)); assert.equal(sa.connected, false); assert.equal(resumed.connected, true); assert.equal(signaling.namespace.sockets.size, 2); const resumedMessage = new Promise(r => sb.once('signal', r)); assert.equal((await resumed.timeout(2000).emitWithAck('signal', { ...packet, epoch: 1 })).ok, true); assert.equal((await resumedMessage).epoch, 1); const active = await sessions.register(a, { requestId: randomUUID(), matchId: room.currentMatchId, roomId: room.id, mode: 'friend', expectedPreviousSeq: 0 }); resumed.disconnect(); sb.disconnect(); await new Promise(r => setTimeout(r, 20)); assert.equal(signaling.namespace.sockets.size, 0); assert.equal((await sessions.get(a)).activeMatch?.matchId, active.matchId); const rtc = await signaling.rtcConfig(a, room.id); const turn = rtc.iceServers[0]; assert.ok(turn.username); assert.equal(turn.credential, createHmac('sha1', 'test-secret').update(turn.username!).digest('base64')); assert.ok(rtc.expiresAt > Date.now()); } finally { sockets.forEach(s => s.disconnect()); await new Promise(r => io.close(() => r())); try { await db.dropDatabase(); } finally { await client.close(); } } });