363 lines
11 KiB
TypeScript
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();
|