FitnessApp/scripts/test-jitsi-integration.ts

363 lines
11 KiB
TypeScript

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<boolean>((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<boolean>((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();