import dotenv from 'dotenv'; dotenv.config({ path: '.env.local' }); dotenv.config({ path: '.env' }); import jwt from 'jsonwebtoken'; import https from 'https'; import http from 'http'; import net from 'net'; import { generateJitsiRoomName, generateJitsiToken, generateJitsiMeetingUrl, JitsiUser, } from '../lib/jitsi'; import { prisma, getTenantPrisma } from '../lib/prisma'; // ANSI color codes const GREEN = '\x1b[32m'; const RED = '\x1b[31m'; const YELLOW = '\x1b[33m'; const CYAN = '\x1b[36m'; const BOLD = '\x1b[1m'; const RESET = '\x1b[0m'; async function testRoomIdGeneration() { console.log(`\n${CYAN}${BOLD}[1/4] Testing Deterministic Jitsi Room ID Generation...${RESET}`); const tenantSlug = 'peak-fitness'; const sessionId = 'session_abc123'; const fixedTimestamp = 1775740000000; const roomName = generateJitsiRoomName(tenantSlug, sessionId, fixedTimestamp); const expectedFormat = `${tenantSlug}_${sessionId}_${fixedTimestamp}`; if (roomName !== expectedFormat) { throw new Error(`Expected room name '${expectedFormat}', got '${roomName}'`); } // Test special character sanitization const messySlug = 'Gym @ Downtown #1!'; const sanitizedRoom = generateJitsiRoomName(messySlug, sessionId, fixedTimestamp); if (/[@#! ]/.test(sanitizedRoom)) { throw new Error(`Room name was not properly sanitized: '${sanitizedRoom}'`); } console.log(` ${GREEN}✔ Deterministic room format verified: ${roomName}${RESET}`); console.log(` ${GREEN}✔ Slug sanitization verified: '${messySlug}' -> '${sanitizedRoom}'${RESET}`); console.log(` ${GREEN}${BOLD}✔ Room ID generation check: PASSED${RESET}`); return true; } async function testJwtSigning() { console.log(`\n${CYAN}${BOLD}[2/4] Testing Jitsi JWT Room Token Signing & Claims...${RESET}`); const roomName = 'iron-gym_session_999_1775740000000'; const tenantSlug = 'iron-gym'; const secret = process.env.JITSI_APP_SECRET; if (!secret) { throw new Error('JITSI_APP_SECRET is required for JWT verification'); } // 1. Host (Instructor) Token const hostUser: JitsiUser = { id: 'user_instructor_01', name: 'Marcus Miller', email: 'marcus@iron-gym.local', role: 'INSTRUCTOR', }; const hostToken = generateJitsiToken({ roomName, tenantSlug, user: hostUser, expiresInSeconds: 3600, }); const decodedHost = jwt.verify(hostToken, secret) as any; if (decodedHost.room !== roomName) { throw new Error(`Token room mismatch: expected '${roomName}', got '${decodedHost.room}'`); } if (decodedHost.context?.user?.moderator !== true) { throw new Error('Host token expected moderator: true'); } if (decodedHost.context?.features?.recording !== true) { throw new Error('Host token expected recording: true'); } console.log(` ${GREEN}✔ Host JWT verified: moderator=true, recording=true, room=${decodedHost.room}${RESET}`); // 2. Participant (Client) Token const clientUser: JitsiUser = { id: 'user_client_02', name: 'Sarah Connor', email: 'sarah@iron-gym.local', role: 'CLIENT', }; const clientToken = generateJitsiToken({ roomName, tenantSlug, user: clientUser, expiresInSeconds: 3600, }); const decodedClient = jwt.verify(clientToken, secret) as any; if (decodedClient.context?.user?.moderator !== false) { throw new Error('Client token expected moderator: false'); } if (decodedClient.context?.features?.recording !== false) { throw new Error('Client token expected recording: false'); } console.log(` ${GREEN}✔ Client JWT verified: moderator=false, recording=false, room=${decodedClient.room}${RESET}`); // 3. Meeting URL Construction const meetingUrl = generateJitsiMeetingUrl(roomName, hostToken); if (!meetingUrl.includes('https://') || !meetingUrl.includes(roomName) || !meetingUrl.includes('jwt=')) { throw new Error(`Meeting URL construction failed: ${meetingUrl}`); } console.log(` ${GREEN}✔ Meeting URL constructed successfully: ${meetingUrl.slice(0, 75)}...${RESET}`); console.log(` ${GREEN}${BOLD}✔ JWT signing & claims check: PASSED${RESET}`); return true; } async function testJitsiConnectivity() { console.log(`\n${CYAN}${BOLD}[3/4] Testing Connectivity to Jitsi Web (192.168.16.50)...${RESET}`); const host = '192.168.16.50'; // 1. TCP socket test to Port 443 const port443Ok = await new Promise((resolve) => { const socket = new net.Socket(); socket.setTimeout(3000); socket.connect(443, host, () => { socket.destroy(); resolve(true); }); socket.on('error', () => resolve(false)); socket.on('timeout', () => { socket.destroy(); resolve(false); }); }); if (!port443Ok) { throw new Error(`TCP connection to ${host}:443 failed`); } console.log(` ${GREEN}✔ TCP connection to ${host}:443 successful${RESET}`); // 2. HTTPS request to Jitsi Web endpoint const httpsOk = await new Promise((resolve, reject) => { const req = https.request( { host, port: 443, path: '/', method: 'GET', rejectUnauthorized: false, timeout: 4000, }, (res) => { console.log(` ${GREEN}✔ Jitsi Web HTTPS response received: HTTP ${res.statusCode} ${res.statusMessage}${RESET}`); resolve(true); } ); req.on('error', (err) => { console.log(` ${YELLOW}⚠ HTTPS request error (${err.message}), checking HTTP port 80 fallback...${RESET}`); http.get(`http://${host}:80/`, (res80) => { console.log(` ${GREEN}✔ Jitsi Web HTTP:80 response received: HTTP ${res80.statusCode}${RESET}`); resolve(true); }).on('error', reject); }); req.on('timeout', () => { req.destroy(); resolve(false); }); req.end(); }); if (!httpsOk) { throw new Error(`Failed to receive HTTP/HTTPS response from Jitsi Web at ${host}`); } console.log(` ${GREEN}${BOLD}✔ Jitsi Web endpoint check: PASSED${RESET}`); return true; } async function testTenantClassroomIntegration() { console.log(`\n${CYAN}${BOLD}[4/4] Testing Multi-Tenant Classroom Database Flow...${RESET}`); const testSlug = 'jitsi-test-tenant'; let tenant = await prisma.tenant.findUnique({ where: { slug: testSlug } }); if (!tenant) { tenant = await prisma.tenant.create({ data: { name: 'Jitsi Test Gym', slug: testSlug, domain: 'jitsi-test.fitnessapp.local', }, }); } const tenantPrisma = getTenantPrisma(tenant.id); // Create class session with Jitsi room ID const timestamp = Date.now(); const jitsiRoomId = generateJitsiRoomName(tenant.slug, 'class-poc', timestamp); const session = await tenantPrisma.classSession.create({ data: { tenantId: tenant.id, title: 'Milestone 2 Live HIIT Test', description: 'End-to-end WebRTC classroom verification session', startTime: new Date(), endTime: new Date(Date.now() + 3600 * 1000), capacity: 10, jitsiRoomId, }, }); console.log(` ${GREEN}✔ ClassSession created with Jitsi Room ID: ${session.jitsiRoomId}${RESET}`); // Create Instructor and Client const instructor = await tenantPrisma.user.upsert({ where: { tenantId_email: { tenantId: tenant.id, email: `instructor@${testSlug}.com`, }, }, update: {}, create: { tenantId: tenant.id, name: 'Lead Trainer', email: `instructor@${testSlug}.com`, role: 'INSTRUCTOR', }, }); const client = await tenantPrisma.user.upsert({ where: { tenantId_email: { tenantId: tenant.id, email: `client@${testSlug}.com`, }, }, update: {}, create: { tenantId: tenant.id, name: 'Test Athlete', email: `client@${testSlug}.com`, role: 'CLIENT', }, }); // Verify Booking Creation const booking = await tenantPrisma.booking.upsert({ where: { userId_classSessionId: { userId: client.id, classSessionId: session.id, }, }, update: {}, create: { tenantId: tenant.id, userId: client.id, classSessionId: session.id, status: 'CONFIRMED', signedWaiverAt: new Date(), }, }); console.log(` ${GREEN}✔ Verified Client Booking: user ${client.id} -> session ${session.id} (Status: ${booking.status})${RESET}`); // Generate tokens for both const hostToken = generateJitsiToken({ roomName: session.jitsiRoomId, tenantSlug: tenant.slug, user: { id: instructor.id, name: instructor.name || 'Lead Trainer', email: instructor.email, role: instructor.role, }, }); const clientToken = generateJitsiToken({ roomName: session.jitsiRoomId, tenantSlug: tenant.slug, user: { id: client.id, name: client.name || 'Test Athlete', email: client.email, role: client.role, }, }); if (!hostToken || !clientToken) { throw new Error('Token generation failed for database entities'); } console.log(` ${GREEN}✔ Tenant isolation verified: both host & client tokens signed for ${session.jitsiRoomId}${RESET}`); console.log(` ${GREEN}${BOLD}✔ Multi-Tenant Classroom Flow check: PASSED${RESET}`); return true; } async function main() { console.log(`${BOLD}====================================================${RESET}`); console.log(`${BOLD} Milestone 2 Verification: Jitsi WebRTC Integration${RESET}`); console.log(`${BOLD}====================================================${RESET}`); let allPassed = true; try { await testRoomIdGeneration(); } catch (err: any) { console.error(` ${RED}✘ Room ID check failed: ${err.message}${RESET}`); allPassed = false; } try { await testJwtSigning(); } catch (err: any) { console.error(` ${RED}✘ JWT signing check failed: ${err.message}${RESET}`); allPassed = false; } try { await testJitsiConnectivity(); } catch (err: any) { console.error(` ${RED}✘ Jitsi connectivity check failed: ${err.message}${RESET}`); allPassed = false; } try { await testTenantClassroomIntegration(); } catch (err: any) { console.error(` ${RED}✘ Multi-tenant flow check failed: ${err.message}${RESET}`); allPassed = false; } console.log(`\n${BOLD}====================================================${RESET}`); if (allPassed) { console.log(`${GREEN}${BOLD}🎉 ALL MILESTONE 2 JITSI VERIFICATION CHECKS PASSED!${RESET}`); console.log(`${BOLD}====================================================${RESET}\n`); process.exit(0); } else { console.log(`${RED}${BOLD}❌ SOME JITSI VERIFICATION CHECKS FAILED.${RESET}`); console.log(`${BOLD}====================================================${RESET}\n`); process.exit(1); } } main();