diff --git a/meet-ce/backend/tests/helpers/livekit-cli-helpers.ts b/meet-ce/backend/tests/helpers/livekit-cli-helpers.ts new file mode 100644 index 00000000..031a7f68 --- /dev/null +++ b/meet-ce/backend/tests/helpers/livekit-cli-helpers.ts @@ -0,0 +1,219 @@ +import { MeetRoomMemberTokenMetadata } from '@openvidu-meet/typings'; +import { ChildProcess, spawn } from 'child_process'; +import { MEET_ENV } from '../../src/environment.js'; +import { + waitForParticipantsToDisconnect, + waitForParticipantToConnect, + waitForParticipantToUpdateMetadata +} from './wait-helpers.js'; + +const fakeParticipantsProcesses = new Map(); +/** Tracks all room IDs that currently have at least one fake participant joined via joinFakeParticipant. */ +const fakeParticipantRooms = new Set(); + +/** + * Adds a fake participant to a LiveKit room for testing purposes. + * + * @param roomId The ID of the room to join + * @param participantIdentity The identity for the fake participant + */ +export const joinFakeParticipant = async (roomId: string, participantIdentity: string) => { + await ensureLivekitCliInstalled(); + const process = spawnLivekitCliProcess([ + 'room', + 'join', + '--identity', + participantIdentity, + '--publish-demo', + roomId + ]); + + // Store the process to be able to terminate it later + fakeParticipantsProcesses.set(`${roomId}-${participantIdentity}`, process); + fakeParticipantRooms.add(roomId); + await waitForParticipantToConnect(roomId, participantIdentity); +}; + +/** + * Updates the metadata for a participant in a LiveKit room. + * + * @param roomId The ID of the room + * @param participantIdentity The identity of the participant + * @param metadata The metadata to update + */ +export const updateParticipantMetadata = async ( + roomId: string, + participantIdentity: string, + metadata: MeetRoomMemberTokenMetadata +) => { + await ensureLivekitCliInstalled(); + spawnLivekitCliProcess([ + 'room', + 'participants', + 'update', + '--room', + roomId, + '--identity', + participantIdentity, + '--metadata', + JSON.stringify(metadata) + ]); + await waitForParticipantToUpdateMetadata(roomId, participantIdentity, metadata); +}; + +export const disconnectFakeParticipants = async () => { + // Capture the rooms that had fake participants before clearing the set + const roomIds = [...fakeParticipantRooms]; + await ensureLivekitCliInstalled(); + + fakeParticipantsProcesses.forEach((process, participant) => { + process.kill(); + console.log(`Stopped process for participant '${participant}'`); + }); + + fakeParticipantsProcesses.clear(); + + for (const roomId of roomIds) { + const identities = await listRoomParticipantIdentities(roomId); + + for (const identity of identities) { + await executeLivekitCliCommand([ + 'room', + 'participants', + 'remove', + '--room', + roomId, + '--identity', + identity + ]); + } + } + + fakeParticipantRooms.clear(); + + // Wait until LiveKit confirms no participants remain in any of the affected rooms + await waitForParticipantsToDisconnect(roomIds); +}; + +const withLivekitCredentials = (args: string[]): string[] => { + return [...args, '--api-key', MEET_ENV.LIVEKIT_API_KEY, '--api-secret', MEET_ENV.LIVEKIT_API_SECRET]; +}; + +const spawnLivekitCliProcess = (args: string[], stdio: 'pipe' | 'inherit' = 'pipe'): ChildProcess => { + return spawn('lk', withLivekitCredentials(args), { stdio }); +}; + +const executeLivekitCliCommand = async (args: string[], timeoutMs = 10000): Promise => { + return new Promise((resolve, reject) => { + const process = spawnLivekitCliProcess(args, 'pipe'); + + let stdout = ''; + let stderr = ''; + let hasResolved = false; + + const resolveOnce = (success: boolean, payload?: string) => { + if (hasResolved) return; + + hasResolved = true; + + if (success) { + resolve(payload ?? ''); + } else { + reject(new Error(payload ?? 'LiveKit CLI command failed')); + } + }; + + process.stdout?.on('data', (chunk) => { + stdout += chunk.toString(); + }); + + process.stderr?.on('data', (chunk) => { + stderr += chunk.toString(); + }); + + process.on('error', (error) => { + resolveOnce(false, `Failed to execute LiveKit CLI: ${error.message}`); + }); + + process.on('exit', (code) => { + if (code === 0) { + resolveOnce(true, stdout); + } else { + resolveOnce( + false, + `LiveKit CLI exited with code ${code}. stderr: ${stderr.trim() || 'N/A'}. stdout: ${stdout.trim() || 'N/A'}` + ); + } + }); + + setTimeout(() => { + process.kill(); + resolveOnce(false, `LiveKit CLI command timed out after ${timeoutMs}ms`); + }, timeoutMs); + }); +}; + +const parseParticipantIdentities = (participantsListOutput: string): string[] => { + const headerTokens = new Set(['identity', 'id', 'name']); + + const identities = participantsListOutput + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => line.length > 0) + .filter((line) => !/^[-=+|]+$/.test(line)) + .map((line) => line.split(/\s+/)[0]) + .map((token) => token.replace(/^\|+|\|+$/g, '')) + .filter((token) => token.length > 0) + .filter((token) => !headerTokens.has(token.toLowerCase())); + + return [...new Set(identities)]; +}; + +const listRoomParticipantIdentities = async (roomId: string): Promise => { + const output = await executeLivekitCliCommand(['room', 'participants', 'list', roomId]); + + return parseParticipantIdentities(output); +}; + +const ensureLivekitCliInstalled = async (): Promise => { + return new Promise((resolve, reject) => { + const checkProcess = spawn('lk', ['--version'], { + stdio: 'pipe' + }); + + let hasResolved = false; + + const resolveOnce = (success: boolean, message?: string) => { + if (hasResolved) return; + + hasResolved = true; + + if (success) { + resolve(); + } else { + reject(new Error(message || 'LiveKit CLI check failed')); + } + }; + + checkProcess.on('error', (error) => { + if (error.message.includes('ENOENT')) { + resolveOnce(false, '❌ LiveKit CLI tool "lk" is not installed or not in PATH.'); + } else { + resolveOnce(false, `Failed to check LiveKit CLI: ${error.message}`); + } + }); + + checkProcess.on('exit', (code) => { + if (code === 0) { + resolveOnce(true); + } else { + resolveOnce(false, `LiveKit CLI exited with code ${code}`); + } + }); + + setTimeout(() => { + checkProcess.kill(); + resolveOnce(false, 'LiveKit CLI check timed out'); + }, 5000); + }); +}; diff --git a/meet-ce/backend/tests/helpers/request-helpers.ts b/meet-ce/backend/tests/helpers/request-helpers.ts index 31f2c575..6856c6f8 100644 --- a/meet-ce/backend/tests/helpers/request-helpers.ts +++ b/meet-ce/backend/tests/helpers/request-helpers.ts @@ -14,7 +14,6 @@ import { MeetRoomField, MeetRoomMemberOptions, MeetRoomMemberRole, - MeetRoomMemberTokenMetadata, MeetRoomMemberTokenOptions, MeetRoomOptions, MeetRoomRolesConfig, @@ -23,7 +22,6 @@ import { SecurityConfig, WebhookConfig } from '@openvidu-meet/typings'; -import { ChildProcess, spawn } from 'child_process'; import { Express } from 'express'; import ms, { StringValue } from 'ms'; import request, { Response } from 'supertest'; @@ -31,12 +29,19 @@ import { container, initializeEagerServices } from '../../src/config/dependency- import { INTERNAL_CONFIG } from '../../src/config/internal-config.js'; import { MEET_ENV } from '../../src/environment.js'; import { GlobalConfigRepository } from '../../src/repositories/global-config.repository.js'; +import { RoomRepository } from '../../src/repositories/room.repository.js'; import { createApp, registerDependencies } from '../../src/server.js'; import { ApiKeyService } from '../../src/services/api-key.service.js'; import { GlobalConfigService } from '../../src/services/global-config.service.js'; import { RecordingService } from '../../src/services/recording.service.js'; import { RoomScheduledTasksService } from '../../src/services/room-scheduled-tasks.service.js'; import { getBasePath } from '../../src/utils/html-dynamic-base-path.utils.js'; +import { + waitForAllRecordingsToStop, + waitForAllRoomsToDelete, + waitForMeetingToEnd, + waitForRecordingToStop +} from './wait-helpers.js'; /** * Constructs the full API path by prepending the base path. @@ -54,7 +59,6 @@ export const getFullPath = (apiPath: string): string => { }; let app: Express; -const fakeParticipantsProcesses = new Map(); export const sleep = (time: StringValue) => { return new Promise((resolve) => setTimeout(resolve, ms(time))); @@ -576,9 +580,7 @@ export const deleteRoom = async ( req.set('x-extrafields', headers.xExtraFields); } - const result = await req; - await sleep('5s'); // TODO - replace with a more robust solution to ensure webhook is processed before proceeding with the tests - return result; + return await req; }; export const bulkDeleteRooms = async ( @@ -618,9 +620,7 @@ export const bulkDeleteRooms = async ( req.set('x-extrafields', headers.xExtraFields); } - const result = await req; - await sleep('5s'); // TODO - replace with a more robust solution to ensure webhook is processed before proceeding with the tests - return result; + return await req; }; export const deleteAllRooms = async () => { @@ -657,9 +657,19 @@ export const deleteAllRooms = async () => { export const runExpiredRoomsGC = async () => { checkAppIsRunning(); + // Capture expired rooms without active meetings — these are synchronously deleted by the GC, + // which in turn causes LiveKit to emit room_finished webhooks that the backend must process. + const roomRepository = container.get(RoomRepository); + const expiredRooms = await roomRepository.findExpiredRooms(); + const expiredRoomIdsToWait = expiredRooms + .filter((r) => r.status !== MeetRoomStatus.ACTIVE_MEETING) + .map((r) => r.roomId); + const roomTaskScheduler = container.get(RoomScheduledTasksService); await roomTaskScheduler['deleteExpiredRooms'](); - await sleep('5s'); // TODO - replace with a more robust solution to ensure webhook is processed before proceeding with the tests + + // Wait until the deleted rooms are confirmed gone (404) or no longer active in the Meet API. + await waitForAllRoomsToDelete(expiredRoomIdsToWait); }; /** @@ -767,73 +777,6 @@ export const generateRoomMemberToken = async ( // MEETING HELPERS -/** - * Adds a fake participant to a LiveKit room for testing purposes. - * - * @param roomId The ID of the room to join - * @param participantIdentity The identity for the fake participant - */ -export const joinFakeParticipant = async (roomId: string, participantIdentity: string) => { - await ensureLivekitCliInstalled(); - const process = spawn('lk', [ - 'room', - 'join', - '--identity', - participantIdentity, - '--publish-demo', - roomId, - '--api-key', - MEET_ENV.LIVEKIT_API_KEY, - '--api-secret', - MEET_ENV.LIVEKIT_API_SECRET - ]); - - // Store the process to be able to terminate it later - fakeParticipantsProcesses.set(`${roomId}-${participantIdentity}`, process); - await sleep('1s'); -}; - -/** - * Updates the metadata for a participant in a LiveKit room. - * - * @param roomId The ID of the room - * @param participantIdentity The identity of the participant - * @param metadata The metadata to update - */ -export const updateParticipantMetadata = async ( - roomId: string, - participantIdentity: string, - metadata: MeetRoomMemberTokenMetadata -) => { - await ensureLivekitCliInstalled(); - spawn('lk', [ - 'room', - 'participants', - 'update', - '--room', - roomId, - '--identity', - participantIdentity, - '--metadata', - JSON.stringify(metadata), - '--api-key', - MEET_ENV.LIVEKIT_API_KEY, - '--api-secret', - MEET_ENV.LIVEKIT_API_SECRET - ]); - await sleep('1s'); -}; - -export const disconnectFakeParticipants = async () => { - fakeParticipantsProcesses.forEach((process, participant) => { - process.kill(); - console.log(`Stopped process for participant '${participant}'`); - }); - - fakeParticipantsProcesses.clear(); - await sleep('1s'); // TODO - replace with a more robust solution to ensure webhook is processed before proceeding with the tests -}; - export const updateParticipant = async ( roomId: string, participantIdentity: string, @@ -874,7 +817,8 @@ export const endMeeting = async (roomId: string, moderatorToken: string) => { .delete(getFullPath(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/meetings/${roomId}`)) .set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, moderatorToken) .send(); - await sleep('5s'); // TODO - replace with a more robust solution to ensure webhook is processed before proceeding with the tests + + await waitForMeetingToEnd(roomId); return response; }; @@ -934,7 +878,7 @@ export const stopRecording = async ( } const response = await req; - await sleep('2.5s'); // TODO - replace with a more robust solution to ensure webhook is processed before proceeding with the tests + await waitForRecordingToStop(recordingId); return response; }; @@ -1107,7 +1051,7 @@ export const stopAllRecordings = async () => { results.forEach((response) => { expect(response.status).toBe(202); }); - await sleep('1s'); + await waitForAllRecordingsToStop(recordingIds); }; export const deleteAllRecordings = async () => { @@ -1164,51 +1108,3 @@ const checkAppIsRunning = () => { throw new Error('App instance is not defined'); } }; - -/** - * Verifies that the LiveKit CLI tool 'lk' is installed and accessible - * @throws Error if 'lk' command is not found - */ -const ensureLivekitCliInstalled = async (): Promise => { - return new Promise((resolve, reject) => { - const checkProcess = spawn('lk', ['--version'], { - stdio: 'pipe' - }); - - let hasResolved = false; - - const resolveOnce = (success: boolean, message?: string) => { - if (hasResolved) return; - - hasResolved = true; - - if (success) { - resolve(); - } else { - reject(new Error(message || 'LiveKit CLI check failed')); - } - }; - - checkProcess.on('error', (error) => { - if (error.message.includes('ENOENT')) { - resolveOnce(false, '❌ LiveKit CLI tool "lk" is not installed or not in PATH.'); - } else { - resolveOnce(false, `Failed to check LiveKit CLI: ${error.message}`); - } - }); - - checkProcess.on('exit', (code) => { - if (code === 0) { - resolveOnce(true); - } else { - resolveOnce(false, `LiveKit CLI exited with code ${code}`); - } - }); - - // Timeout after 5 seconds - setTimeout(() => { - checkProcess.kill(); - resolveOnce(false, 'LiveKit CLI check timed out'); - }, 5000); - }); -}; diff --git a/meet-ce/backend/tests/helpers/test-scenarios.ts b/meet-ce/backend/tests/helpers/test-scenarios.ts index 33f6ce8d..3e42e072 100644 --- a/meet-ce/backend/tests/helpers/test-scenarios.ts +++ b/meet-ce/backend/tests/helpers/test-scenarios.ts @@ -16,13 +16,13 @@ import { MeetRoomHelper } from '../../src/helpers/room.helper'; import { RoomRepository } from '../../src/repositories/room.repository'; import { RoomData, RoomMemberData, RoomTestUsers, TestContext, TestUsers, UserData } from '../interfaces/scenarios'; import { expectValidStartRecordingResponse } from './assertion-helpers'; +import { joinFakeParticipant } from './livekit-cli-helpers.js'; import { changePassword, createRoom, createRoomMember, createUser, generateRoomMemberToken, - joinFakeParticipant, loginUser, sleep, startRecording, diff --git a/meet-ce/backend/tests/helpers/wait-helpers.ts b/meet-ce/backend/tests/helpers/wait-helpers.ts new file mode 100644 index 00000000..ccad813e --- /dev/null +++ b/meet-ce/backend/tests/helpers/wait-helpers.ts @@ -0,0 +1,303 @@ +import { MeetRecordingStatus, MeetRoomStatus } from '@openvidu-meet/typings'; +import { container } from '../../src/config/dependency-injector.config.js'; +import { RecordingRepository } from '../../src/repositories/recording.repository.js'; +import { RoomRepository } from '../../src/repositories/room.repository.js'; +import { LiveKitService } from '../../src/services/livekit.service.js'; + +// ─── CONFIGURATION ─────────────────────────────────────────────────────────── + +const DEFAULT_POLL_INTERVAL_MS = 250; +const DEFAULT_ROOM_TIMEOUT_MS = 15_000; +const DEFAULT_RECORDING_TIMEOUT_MS = 30_000; +const DEFAULT_PARTICIPANT_TIMEOUT_MS = 15_000; + +// ─── GENERIC POLLING ───────────────────────────────────────────────────────── + +/** + * Generic active-wait utility. + * + * Repeatedly invokes `condition` every `intervalMs` milliseconds until it + * returns `true` or `timeoutMs` milliseconds have elapsed. + * Throws a descriptive error if the condition is never satisfied. + */ +const pollUntil = async ( + condition: () => Promise, + options: { + intervalMs?: number; + timeoutMs?: number; + errorMessage?: string; + } = {} +): Promise => { + const intervalMs = options.intervalMs ?? DEFAULT_POLL_INTERVAL_MS; + const timeoutMs = options.timeoutMs ?? DEFAULT_ROOM_TIMEOUT_MS; + const errorMessage = options.errorMessage ?? 'pollUntil: condition was not met within the timeout'; + + const deadline = Date.now() + timeoutMs; + + while (Date.now() < deadline) { + if (await condition()) return; + + await new Promise((resolve) => setTimeout(resolve, intervalMs)); + } + + throw new Error(`${errorMessage} (timeout: ${timeoutMs}ms)`); +}; + +// ─── ROOM WAIT HELPERS ──────────────────────────────────────────────────────── + +/** + * Waits until a room no longer exists in the repository. + * + * This helper is intended for flows where the expected backend side effect is + * deletion of the room document. + * + * Polls `RoomRepository` directly to observe the persisted backend state with + * no HTTP indirection. + * + * @param roomId - Room identifier to poll. + * @param timeoutMs - Maximum wait time in milliseconds (default: 15 000). + */ +export const waitForRoomToDelete = async (roomId: string, timeoutMs = DEFAULT_ROOM_TIMEOUT_MS): Promise => { + const roomRepository = container.get(RoomRepository); + + await pollUntil( + async () => { + const room = await roomRepository.findByRoomId(roomId); + return !room; + }, + { timeoutMs, errorMessage: `Room '${roomId}': meeting did not end` } + ); +}; + +/** + * Waits until every room in `roomIds` has been deleted. + * + * All rooms are polled concurrently. + * + * @param roomIds - Array of room identifiers to poll. + * @param timeoutMs - Maximum wait time per room in milliseconds (default: 15 000). + */ +export const waitForAllRoomsToDelete = async ( + roomIds: string[], + timeoutMs = DEFAULT_ROOM_TIMEOUT_MS +): Promise => { + await Promise.all(roomIds.map((id) => waitForRoomToDelete(id, timeoutMs))); +}; + +/** + * Waits until a room has been created and is available in the repository. + * + * Polls `RoomRepository` directly until `findByRoomId(roomId)` returns a + * document. + * + * @param roomId - Room identifier to poll. + * @param timeoutMs - Maximum wait time in milliseconds (default: 15 000). + */ +export const waitForRoomToCreate = async (roomId: string, timeoutMs = DEFAULT_ROOM_TIMEOUT_MS): Promise => { + const roomRepository = container.get(RoomRepository); + + await pollUntil( + async () => { + const room = await roomRepository.findByRoomId(roomId); + return !!room; + }, + { timeoutMs, errorMessage: `Room '${roomId}' was not created` } + ); +}; + +// ─── PARTICIPANT WAIT HELPERS ───────────────────────────────────────────────── + +/** + * Waits until all participants have disconnected from the given LiveKit rooms. + * + * Queries `LiveKitService.roomHasParticipants()` directly — the authoritative + * source of truth for participant presence — so no HTTP auth overhead is + * incurred. + * + * Resolves immediately if `roomIds` is empty. + * + * @param roomIds - Room identifiers to check for remaining participants. + * @param timeoutMs - Maximum wait time in milliseconds (default: 15 000). + */ +export const waitForParticipantsToDisconnect = async ( + roomIds: string[], + timeoutMs = DEFAULT_PARTICIPANT_TIMEOUT_MS +): Promise => { + if (roomIds.length === 0) return; + + const livekitService = container.get(LiveKitService); + + await pollUntil( + async () => { + const checks = await Promise.all(roomIds.map((id) => livekitService.roomHasParticipants(id))); + console.log(`Checked participant presence in rooms [${roomIds.join(', ')}]:`, checks); + return checks.every((hasParticipants) => !hasParticipants); + }, + { + timeoutMs, + errorMessage: `Participants in rooms [${roomIds.join(', ')}] did not disconnect` + } + ); +}; + +/** + * Waits until a room reaches the {@link MeetRoomStatus.CLOSED} state. + * + * The room must still exist in the repository; deletion does not satisfy this + * condition. + * + * @param roomId - Room identifier to poll. + * @param timeoutMs - Maximum wait time in milliseconds (default: 15 000). + */ +export const waitForRoomToClose = async (roomId: string, timeoutMs = DEFAULT_ROOM_TIMEOUT_MS): Promise => { + const roomRepository = container.get(RoomRepository); + + await pollUntil( + async () => { + const room = await roomRepository.findByRoomId(roomId); + return !!room && room.status === MeetRoomStatus.CLOSED; + }, + { timeoutMs, errorMessage: `Room '${roomId}' was not closed` } + ); +}; + +/** + * Waits until a participant is present in the given LiveKit room. + * + * Uses `LiveKitService.participantExists()` as the source of truth for + * participant presence. + * + * @param roomId - Room identifier to query. + * @param participantIdentity - Participant identity expected to appear. + * @param timeoutMs - Maximum wait time in milliseconds (default: 15 000). + */ +export const waitForParticipantToConnect = async ( + roomId: string, + participantIdentity: string, + timeoutMs = DEFAULT_PARTICIPANT_TIMEOUT_MS +): Promise => { + const livekitService = container.get(LiveKitService); + + await pollUntil( + async () => { + return await livekitService.participantExists(roomId, participantIdentity); + }, + { + timeoutMs, + errorMessage: `No participants connected to room '${roomId}'` + } + ); +}; + +/** + * Waits until a participant's metadata matches the expected serialized value. + * + * The helper fetches the participant directly from LiveKit and compares its + * `metadata` field with `JSON.stringify(metadata)`. + * + * @param roomId - Room identifier to query. + * @param participantIdentity - Participant identity expected to update. + * @param metadata - Metadata object expected to be stored. + * @param timeoutMs - Maximum wait time in milliseconds (default: 15 000). + */ +export const waitForParticipantToUpdateMetadata = async ( + roomId: string, + participantIdentity: string, + metadata: Record, + timeoutMs = DEFAULT_PARTICIPANT_TIMEOUT_MS +): Promise => { + const livekitService = container.get(LiveKitService); + + await pollUntil( + async () => { + const participant = await livekitService.getParticipant(roomId, participantIdentity); + + if (!participant) return false; + + return participant.metadata === JSON.stringify(metadata); + }, + { + timeoutMs, + errorMessage: `Participant '${participantIdentity}' in room '${roomId}' did not update metadata to ${JSON.stringify( + metadata + )}` + } + ); +}; + +// ─── RECORDING WAIT HELPERS ─────────────────────────────────────────────────── + +/** + * Waits until a recording's `egress_ended` LiveKit webhook has been fully + * processed by the backend handler. + * + * The condition is satisfied when the recording no longer exists in the database + * or its status is no longer {@link MeetRecordingStatus.ACTIVE}. + * + * Polls `RecordingRepository` directly to observe the exact side-effect written + * by the webhook handler. + * + * @param recordingId - Recording identifier to poll. + * @param timeoutMs - Maximum wait time in milliseconds (default: 30 000). + */ +export const waitForRecordingToStop = async ( + recordingId: string, + timeoutMs = DEFAULT_RECORDING_TIMEOUT_MS +): Promise => { + const recordingRepository = container.get(RecordingRepository); + + await pollUntil( + async () => { + const recording = await recordingRepository.findByRecordingId(recordingId); + + // Recording deleted → handler finished. + if (!recording) return true; + + return [MeetRecordingStatus.COMPLETE, MeetRecordingStatus.FAILED, MeetRecordingStatus.ABORTED].includes( + recording.status + ); + }, + { timeoutMs, errorMessage: `Recording '${recordingId}' did not stop` } + ); +}; + +/** + * Waits until a meeting has ended from both the backend and LiveKit point of view. + * + * The condition is satisfied when either the room no longer exists in the + * repository, or LiveKit no longer reports the room while the stored room state + * is no longer {@link MeetRoomStatus.ACTIVE_MEETING}. + * + * @param roomId - Room identifier to poll. + * @param timeoutMs - Maximum wait time in milliseconds (default: 15 000). + */ +export const waitForMeetingToEnd = async (roomId: string, timeoutMs = DEFAULT_ROOM_TIMEOUT_MS): Promise => { + const roomRepository = container.get(RoomRepository); + const livekitService = container.get(LiveKitService); + + await pollUntil( + async () => { + const lkRoomExists = await livekitService.roomExists(roomId); // Ensure we have the latest room state from LiveKit + const room = await roomRepository.findByRoomId(roomId); + + if (!room) return true; + + return !lkRoomExists && room.status !== MeetRoomStatus.ACTIVE_MEETING; + }, + { timeoutMs, errorMessage: `Meeting in room '${roomId}' did not end` } + ); +}; + +/** + * Waits until all recordings in `recordingIds` have stopped. + * All recordings are polled concurrently. + * + * @param recordingIds - Array of recording identifiers to poll. + * @param timeoutMs - Maximum wait time per recording in milliseconds (default: 30 000). + */ +export const waitForAllRecordingsToStop = async ( + recordingIds: string[], + timeoutMs = DEFAULT_RECORDING_TIMEOUT_MS +): Promise => { + await Promise.all(recordingIds.map((id) => waitForRecordingToStop(id, timeoutMs))); +}; diff --git a/meet-ce/backend/tests/integration/api/meetings/end-meeting.test.ts b/meet-ce/backend/tests/integration/api/meetings/end-meeting.test.ts index 0d9e9ed8..ee20dcde 100644 --- a/meet-ce/backend/tests/integration/api/meetings/end-meeting.test.ts +++ b/meet-ce/backend/tests/integration/api/meetings/end-meeting.test.ts @@ -2,13 +2,9 @@ import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; import { container } from '../../../../src/config/dependency-injector.config.js'; import { OpenViduMeetError } from '../../../../src/models/error.model.js'; import { LiveKitService } from '../../../../src/services/livekit.service.js'; -import { - deleteAllRooms, - disconnectFakeParticipants, - endMeeting, - getRoom, - startTestServer -} from '../../../helpers/request-helpers.js'; +import { disconnectFakeParticipants } from '../../../helpers/livekit-cli-helpers.js'; +import { deleteAllRooms, endMeeting, getRoom, startTestServer } from '../../../helpers/request-helpers.js'; + import { setupSingleRoom } from '../../../helpers/test-scenarios.js'; import { RoomData } from '../../../interfaces/scenarios.js'; diff --git a/meet-ce/backend/tests/integration/api/meetings/kick-participant.test.ts b/meet-ce/backend/tests/integration/api/meetings/kick-participant.test.ts index 7e2c054d..6b216946 100644 --- a/meet-ce/backend/tests/integration/api/meetings/kick-participant.test.ts +++ b/meet-ce/backend/tests/integration/api/meetings/kick-participant.test.ts @@ -3,11 +3,10 @@ import { container } from '../../../../src/config/dependency-injector.config.js' import { OpenViduMeetError } from '../../../../src/models/error.model.js'; import { LiveKitService } from '../../../../src/services/livekit.service.js'; import { - deleteAllRooms, - disconnectFakeParticipants, - kickParticipant, - startTestServer -} from '../../../helpers/request-helpers.js'; + disconnectFakeParticipants +} from '../../../helpers/livekit-cli-helpers.js'; +import { deleteAllRooms, kickParticipant, startTestServer } from '../../../helpers/request-helpers.js'; + import { setupSingleRoom } from '../../../helpers/test-scenarios.js'; import { RoomData } from '../../../interfaces/scenarios.js'; diff --git a/meet-ce/backend/tests/integration/api/meetings/update-participant.test.ts b/meet-ce/backend/tests/integration/api/meetings/update-participant.test.ts index 7e884ff0..0b387fb0 100644 --- a/meet-ce/backend/tests/integration/api/meetings/update-participant.test.ts +++ b/meet-ce/backend/tests/integration/api/meetings/update-participant.test.ts @@ -4,13 +4,8 @@ import { container } from '../../../../src/config/dependency-injector.config.js' import { MEET_ENV } from '../../../../src/environment.js'; import { FrontendEventService } from '../../../../src/services/frontend-event.service.js'; import { LiveKitService } from '../../../../src/services/livekit.service.js'; -import { - deleteAllRooms, - disconnectFakeParticipants, - startTestServer, - updateParticipant, - updateParticipantMetadata -} from '../../../helpers/request-helpers.js'; +import { disconnectFakeParticipants, updateParticipantMetadata } from '../../../helpers/livekit-cli-helpers.js'; +import { deleteAllRooms, startTestServer, updateParticipant } from '../../../helpers/request-helpers.js'; import { setupSingleRoom } from '../../../helpers/test-scenarios.js'; import { RoomData } from '../../../interfaces/scenarios.js'; diff --git a/meet-ce/backend/tests/integration/api/recordings/bulk-delete-recording.test.ts b/meet-ce/backend/tests/integration/api/recordings/bulk-delete-recording.test.ts index cbff64cb..f7e005c5 100644 --- a/meet-ce/backend/tests/integration/api/recordings/bulk-delete-recording.test.ts +++ b/meet-ce/backend/tests/integration/api/recordings/bulk-delete-recording.test.ts @@ -1,15 +1,16 @@ import { afterEach, beforeAll, describe, expect, it } from '@jest/globals'; import { expectValidationError } from '../../../helpers/assertion-helpers'; +import { disconnectFakeParticipants } from '../../../helpers/livekit-cli-helpers.js'; import { bulkDeleteRecordings, deleteAllRecordings, deleteAllRooms, - disconnectFakeParticipants, generateRoomMemberToken, getAllRecordings, startTestServer, stopRecording } from '../../../helpers/request-helpers'; + import { setupMultiRecordingsTestContext, setupSingleRoomWithRecording } from '../../../helpers/test-scenarios'; describe('Recording API Tests', () => { diff --git a/meet-ce/backend/tests/integration/api/recordings/delete-recording.test.ts b/meet-ce/backend/tests/integration/api/recordings/delete-recording.test.ts index 88bb3fba..90d9ff81 100644 --- a/meet-ce/backend/tests/integration/api/recordings/delete-recording.test.ts +++ b/meet-ce/backend/tests/integration/api/recordings/delete-recording.test.ts @@ -1,15 +1,16 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from '@jest/globals'; import { MeetRoom } from '@openvidu-meet/typings'; import { expectValidationError } from '../../../helpers/assertion-helpers'; +import { disconnectFakeParticipants } from '../../../helpers/livekit-cli-helpers.js'; import { deleteAllRecordings, deleteAllRooms, deleteRecording, - disconnectFakeParticipants, startTestServer, stopAllRecordings, stopRecording } from '../../../helpers/request-helpers'; + import { setupMultiRecordingsTestContext } from '../../../helpers/test-scenarios'; describe('Recording API Tests', () => { diff --git a/meet-ce/backend/tests/integration/api/recordings/download-recordings.test.ts b/meet-ce/backend/tests/integration/api/recordings/download-recordings.test.ts index d95920b3..3602851f 100644 --- a/meet-ce/backend/tests/integration/api/recordings/download-recordings.test.ts +++ b/meet-ce/backend/tests/integration/api/recordings/download-recordings.test.ts @@ -2,14 +2,15 @@ import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; import stream from 'stream'; import unzipper from 'unzipper'; import { expectValidationError } from '../../../helpers/assertion-helpers.js'; +import { disconnectFakeParticipants } from '../../../helpers/livekit-cli-helpers.js'; import { deleteAllRecordings, deleteAllRooms, - disconnectFakeParticipants, downloadRecordings, generateRoomMemberToken, startTestServer } from '../../../helpers/request-helpers'; + import { setupMultiRecordingsTestContext, setupSingleRoomWithRecording } from '../../../helpers/test-scenarios'; describe('Recording API Tests', () => { diff --git a/meet-ce/backend/tests/integration/api/recordings/get-media-recording.test.ts b/meet-ce/backend/tests/integration/api/recordings/get-media-recording.test.ts index b4435367..c19f787a 100644 --- a/meet-ce/backend/tests/integration/api/recordings/get-media-recording.test.ts +++ b/meet-ce/backend/tests/integration/api/recordings/get-media-recording.test.ts @@ -1,14 +1,15 @@ import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; import { MeetRoom } from '@openvidu-meet/typings'; import { expectSuccessRecordingMediaResponse, expectValidationError } from '../../../helpers/assertion-helpers'; +import { disconnectFakeParticipants } from '../../../helpers/livekit-cli-helpers.js'; import { deleteAllRecordings, deleteAllRooms, - disconnectFakeParticipants, getRecordingMedia, startTestServer, stopRecording } from '../../../helpers/request-helpers'; + import { setupMultiRecordingsTestContext } from '../../../helpers/test-scenarios'; describe('Recording API Tests', () => { diff --git a/meet-ce/backend/tests/integration/api/recordings/get-recording-url.test.ts b/meet-ce/backend/tests/integration/api/recordings/get-recording-url.test.ts index 57bd7f61..0f18c281 100644 --- a/meet-ce/backend/tests/integration/api/recordings/get-recording-url.test.ts +++ b/meet-ce/backend/tests/integration/api/recordings/get-recording-url.test.ts @@ -4,14 +4,17 @@ import request from 'supertest'; import { INTERNAL_CONFIG } from '../../../../src/config/internal-config.js'; import { errorRecordingNotFound } from '../../../../src/models/error.model.js'; import { expectValidGetRecordingUrlResponse } from '../../../helpers/assertion-helpers.js'; +import { + disconnectFakeParticipants +} from '../../../helpers/livekit-cli-helpers.js'; import { deleteAllRecordings, deleteAllRooms, - disconnectFakeParticipants, getFullPath, getRecordingUrl, startTestServer } from '../../../helpers/request-helpers.js'; + import { setupSingleRoomWithRecording } from '../../../helpers/test-scenarios.js'; describe('Recording API Tests', () => { diff --git a/meet-ce/backend/tests/integration/api/recordings/get-recording.test.ts b/meet-ce/backend/tests/integration/api/recordings/get-recording.test.ts index 2fe2b1c7..2ffd4a0a 100644 --- a/meet-ce/backend/tests/integration/api/recordings/get-recording.test.ts +++ b/meet-ce/backend/tests/integration/api/recordings/get-recording.test.ts @@ -6,14 +6,15 @@ import { expectValidGetRecordingResponse, expectValidRecordingWithFields } from '../../../helpers/assertion-helpers.js'; +import { disconnectFakeParticipants } from '../../../helpers/livekit-cli-helpers.js'; import { deleteAllRecordings, deleteAllRooms, - disconnectFakeParticipants, getRecording, startTestServer, stopAllRecordings } from '../../../helpers/request-helpers.js'; + import { setupMultiRecordingsTestContext } from '../../../helpers/test-scenarios.js'; import { TestContext } from '../../../interfaces/scenarios.js'; diff --git a/meet-ce/backend/tests/integration/api/recordings/get-recordings.test.ts b/meet-ce/backend/tests/integration/api/recordings/get-recordings.test.ts index bd695ae1..c5be6372 100644 --- a/meet-ce/backend/tests/integration/api/recordings/get-recordings.test.ts +++ b/meet-ce/backend/tests/integration/api/recordings/get-recordings.test.ts @@ -6,10 +6,10 @@ import { expectValidRecording, expectValidRecordingWithFields } from '../../../helpers/assertion-helpers.js'; +import { disconnectFakeParticipants } from '../../../helpers/livekit-cli-helpers.js'; import { deleteAllRecordings, deleteAllRooms, - disconnectFakeParticipants, generateRoomMemberToken, getAllRecordings, getAllRecordingsFromRoom, @@ -17,6 +17,7 @@ import { startTestServer, stopRecording } from '../../../helpers/request-helpers.js'; + import { setupMultiRecordingsTestContext, setupSingleRoomWithRecording } from '../../../helpers/test-scenarios.js'; import { RoomData, TestContext } from '../../../interfaces/scenarios.js'; diff --git a/meet-ce/backend/tests/integration/api/recordings/race-conditions.test.ts b/meet-ce/backend/tests/integration/api/recordings/race-conditions.test.ts index b54e7d24..759a0adf 100644 --- a/meet-ce/backend/tests/integration/api/recordings/race-conditions.test.ts +++ b/meet-ce/backend/tests/integration/api/recordings/race-conditions.test.ts @@ -9,12 +9,12 @@ import { expectValidStopRecordingResponse } from '../../../helpers/assertion-helpers'; import { eventController } from '../../../helpers/event-controller'; +import { disconnectFakeParticipants } from '../../../helpers/livekit-cli-helpers.js'; import { bulkDeleteRecordings, deleteAllRecordings, deleteAllRooms, deleteRecording, - disconnectFakeParticipants, getRecording, getRecordingMedia, sleep, @@ -23,6 +23,7 @@ import { stopAllRecordings, stopRecording } from '../../../helpers/request-helpers'; + import { setupMultiRecordingsTestContext, setupMultiRoomTestContext } from '../../../helpers/test-scenarios'; import { TestContext } from '../../../interfaces/scenarios.js'; @@ -39,12 +40,12 @@ describe('Recording API Race Conditions Tests', () => { }); afterEach(async () => { + await disconnectFakeParticipants(); await stopAllRecordings(); - eventController.reset(); - await disconnectFakeParticipants(); await deleteAllRooms(); await deleteAllRecordings(); + eventController.reset(); jest.clearAllMocks(); }); diff --git a/meet-ce/backend/tests/integration/api/recordings/recording-header-fields.test.ts b/meet-ce/backend/tests/integration/api/recordings/recording-header-fields.test.ts index 8766ba29..03318369 100644 --- a/meet-ce/backend/tests/integration/api/recordings/recording-header-fields.test.ts +++ b/meet-ce/backend/tests/integration/api/recordings/recording-header-fields.test.ts @@ -3,13 +3,14 @@ import { expectValidRecordingLocationHeader, expectValidRecordingWithFields } from '../../../helpers/assertion-helpers.js'; +import { disconnectFakeParticipants } from '../../../helpers/livekit-cli-helpers.js'; import { deleteAllRecordings, deleteAllRooms, - disconnectFakeParticipants, startTestServer, stopRecording } from '../../../helpers/request-helpers.js'; + import { setupSingleRoomWithRecording } from '../../../helpers/test-scenarios.js'; /** diff --git a/meet-ce/backend/tests/integration/api/recordings/start-recording.test.ts b/meet-ce/backend/tests/integration/api/recordings/start-recording.test.ts index a7c55d4b..0a0916b9 100644 --- a/meet-ce/backend/tests/integration/api/recordings/start-recording.test.ts +++ b/meet-ce/backend/tests/integration/api/recordings/start-recording.test.ts @@ -18,16 +18,16 @@ import { expectValidStartRecordingResponse, expectValidStopRecordingResponse } from '../../../helpers/assertion-helpers.js'; +import { disconnectFakeParticipants, joinFakeParticipant } from '../../../helpers/livekit-cli-helpers.js'; import { deleteAllRecordings, deleteAllRooms, - disconnectFakeParticipants, - joinFakeParticipant, startRecording, startTestServer, stopAllRecordings, stopRecording } from '../../../helpers/request-helpers.js'; + import { setupMultiRoomTestContext } from '../../../helpers/test-scenarios.js'; import { TestContext } from '../../../interfaces/scenarios.js'; diff --git a/meet-ce/backend/tests/integration/api/recordings/stop-recording.test.ts b/meet-ce/backend/tests/integration/api/recordings/stop-recording.test.ts index 9c745dc1..b5f41222 100644 --- a/meet-ce/backend/tests/integration/api/recordings/stop-recording.test.ts +++ b/meet-ce/backend/tests/integration/api/recordings/stop-recording.test.ts @@ -6,15 +6,16 @@ import { expectValidRecordingWithFields, expectValidStopRecordingResponse } from '../../../helpers/assertion-helpers'; +import { disconnectFakeParticipants } from '../../../helpers/livekit-cli-helpers.js'; import { deleteAllRecordings, deleteAllRooms, - disconnectFakeParticipants, startRecording, startTestServer, stopAllRecordings, stopRecording } from '../../../helpers/request-helpers'; + import { setupMultiRoomTestContext } from '../../../helpers/test-scenarios'; import { TestContext } from '../../../interfaces/scenarios'; diff --git a/meet-ce/backend/tests/integration/api/room-members/bulk-delete-room-members.test.ts b/meet-ce/backend/tests/integration/api/room-members/bulk-delete-room-members.test.ts index 059c96d3..d2a71146 100644 --- a/meet-ce/backend/tests/integration/api/room-members/bulk-delete-room-members.test.ts +++ b/meet-ce/backend/tests/integration/api/room-members/bulk-delete-room-members.test.ts @@ -4,6 +4,7 @@ import { container } from '../../../../src/config/dependency-injector.config.js' import { OpenViduMeetError } from '../../../../src/models/error.model.js'; import { LiveKitService } from '../../../../src/services/livekit.service.js'; import { expectValidationError } from '../../../helpers/assertion-helpers.js'; +import { disconnectFakeParticipants, joinFakeParticipant } from '../../../helpers/livekit-cli-helpers.js'; import { bulkDeleteRoomMembers, createRoom, @@ -11,10 +12,9 @@ import { createUser, deleteAllRooms, deleteAllUsers, - disconnectFakeParticipants, + getRoomMember, getUser, - joinFakeParticipant, startTestServer } from '../../../helpers/request-helpers.js'; diff --git a/meet-ce/backend/tests/integration/api/room-members/delete-room-member.test.ts b/meet-ce/backend/tests/integration/api/room-members/delete-room-member.test.ts index 5492b487..94ba9838 100644 --- a/meet-ce/backend/tests/integration/api/room-members/delete-room-member.test.ts +++ b/meet-ce/backend/tests/integration/api/room-members/delete-room-member.test.ts @@ -11,14 +11,17 @@ import { deleteAllRooms, deleteAllUsers, deleteRoomMember, - disconnectFakeParticipants, getRoomMember, getUser, - joinFakeParticipant, - startTestServer, - updateParticipantMetadata + startTestServer } from '../../../helpers/request-helpers.js'; +import { + disconnectFakeParticipants, + joinFakeParticipant, + updateParticipantMetadata +} from '../../../helpers/livekit-cli-helpers.js'; + describe('Room Members API Tests', () => { let roomId: string; diff --git a/meet-ce/backend/tests/integration/api/room-members/generate-room-member-token.test.ts b/meet-ce/backend/tests/integration/api/room-members/generate-room-member-token.test.ts index e3116b1c..446bbff1 100644 --- a/meet-ce/backend/tests/integration/api/room-members/generate-room-member-token.test.ts +++ b/meet-ce/backend/tests/integration/api/room-members/generate-room-member-token.test.ts @@ -8,12 +8,12 @@ import { MeetUserRole } from '@openvidu-meet/typings'; import { expectValidationError, expectValidRoomMemberTokenResponse } from '../../../helpers/assertion-helpers.js'; +import { disconnectFakeParticipants } from '../../../helpers/livekit-cli-helpers.js'; import { createRoom, createRoomMember, deleteAllRooms, deleteAllUsers, - disconnectFakeParticipants, endMeeting, generateRoomMemberToken, generateRoomMemberTokenRequest, diff --git a/meet-ce/backend/tests/integration/api/room-members/update-room-member.test.ts b/meet-ce/backend/tests/integration/api/room-members/update-room-member.test.ts index 28888be3..663032ce 100644 --- a/meet-ce/backend/tests/integration/api/room-members/update-room-member.test.ts +++ b/meet-ce/backend/tests/integration/api/room-members/update-room-member.test.ts @@ -4,15 +4,15 @@ import { container } from '../../../../src/config/dependency-injector.config.js' import { OpenViduMeetError } from '../../../../src/models/error.model.js'; import { LiveKitService } from '../../../../src/services/livekit.service.js'; import { expectValidationError } from '../../../helpers/assertion-helpers.js'; +import { disconnectFakeParticipants, joinFakeParticipant } from '../../../helpers/livekit-cli-helpers.js'; import { createRoom, createRoomMember, createUser, deleteAllRooms, deleteAllUsers, - disconnectFakeParticipants, + getRoomMember, - joinFakeParticipant, sleep, startTestServer, updateRoomMember diff --git a/meet-ce/backend/tests/integration/api/rooms/active-status-rooms-gc.test.ts b/meet-ce/backend/tests/integration/api/rooms/active-status-rooms-gc.test.ts index e2a6b2ee..e2964e51 100644 --- a/meet-ce/backend/tests/integration/api/rooms/active-status-rooms-gc.test.ts +++ b/meet-ce/backend/tests/integration/api/rooms/active-status-rooms-gc.test.ts @@ -3,11 +3,11 @@ import { MeetRoomStatus } from '@openvidu-meet/typings'; import { container } from '../../../../src/config/dependency-injector.config.js'; import { RoomRepository } from '../../../../src/repositories/room.repository.js'; import { LiveKitService } from '../../../../src/services/livekit.service.js'; +import { disconnectFakeParticipants } from '../../../helpers/livekit-cli-helpers.js'; import { createRoom, deleteAllRecordings, deleteAllRooms, - disconnectFakeParticipants, executeRoomStatusValidationGC, getRoom, startTestServer diff --git a/meet-ce/backend/tests/integration/api/rooms/bulk-delete-rooms.test.ts b/meet-ce/backend/tests/integration/api/rooms/bulk-delete-rooms.test.ts index 0f708b57..d2e11d7b 100644 --- a/meet-ce/backend/tests/integration/api/rooms/bulk-delete-rooms.test.ts +++ b/meet-ce/backend/tests/integration/api/rooms/bulk-delete-rooms.test.ts @@ -9,17 +9,17 @@ import { MeetRoomStatus } from '@openvidu-meet/typings'; import { expectExtraFieldsInResponse, expectValidRoom } from '../../../helpers/assertion-helpers.js'; +import { disconnectFakeParticipants } from '../../../helpers/livekit-cli-helpers.js'; import { bulkDeleteRooms, createRoom, deleteAllRecordings, deleteAllRooms, - disconnectFakeParticipants, endMeeting, - getRoom, startTestServer } from '../../../helpers/request-helpers.js'; import { setupSingleRoom, setupSingleRoomWithRecording } from '../../../helpers/test-scenarios.js'; +import { waitForAllRoomsToDelete, waitForRoomToClose } from '../../../helpers/wait-helpers.js'; describe('Room API Tests', () => { beforeAll(async () => { @@ -37,6 +37,7 @@ describe('Room API Tests', () => { const { roomId } = await createRoom(); const response = await bulkDeleteRooms([roomId]); + await waitForAllRoomsToDelete([roomId]); expect(response.status).toBe(200); expect(response.body).toEqual({ message: 'All rooms successfully processed for deletion', @@ -55,6 +56,7 @@ describe('Room API Tests', () => { const { room: room2 } = await setupSingleRoom(true); const response = await bulkDeleteRooms([room1.roomId, room2.roomId]); + await waitForAllRoomsToDelete([room1.roomId]); expect(response.status).toBe(400); expect(response.body).toEqual({ message: '1 room(s) failed to process while deleting', @@ -97,6 +99,7 @@ describe('Room API Tests', () => { const { roomId } = await createRoom(); const response = await bulkDeleteRooms([roomId, roomId, roomId]); + await waitForAllRoomsToDelete([roomId]); expect(response.status).toBe(200); expect(response.body).toEqual({ message: 'All rooms successfully processed for deletion', @@ -114,6 +117,7 @@ describe('Room API Tests', () => { const { roomId } = await createRoom(); const response = await bulkDeleteRooms([roomId, '!!@##$']); + await waitForAllRoomsToDelete([roomId]); expect(response.status).toBe(200); expect(response.body).toEqual({ message: 'All rooms successfully processed for deletion', @@ -147,27 +151,32 @@ describe('Room API Tests', () => { }); // Verify all rooms are deleted - for (const room of rooms) { - const getResponse = await getRoom(room.roomId); - expect(getResponse.status).toBe(404); - } + await waitForAllRoomsToDelete(rooms.map((r) => r.roomId)); }); it('should handle deletion when specifying withMeeting and withRecordings parameters', async () => { - const [room1, { room: room2 }, { room: room3 }, { room: room4, moderatorToken }] = await Promise.all([ + const [ + room1, + { room: room2, moderatorToken: modToken2 }, + { room: room3, moderatorToken: modToken3 }, + { room: room4, moderatorToken: modToken4 } + ] = await Promise.all([ createRoom(), // Room without active meeting or recordings setupSingleRoom(true), // Room with active meeting setupSingleRoomWithRecording(true), // Room with active meeting and recordings setupSingleRoomWithRecording(true) // Room with recordings ]); - await endMeeting(room4.roomId, moderatorToken); + await endMeeting(room4.roomId, modToken4); // End meeting for room4 so it has recordings but no active meeting const fakeRoomId = 'fake_room-123'; // Non-existing room - const response = await bulkDeleteRooms( [room1.roomId, room2.roomId, room3.roomId, room4.roomId, fakeRoomId], MeetRoomDeletionPolicyWithMeeting.WHEN_MEETING_ENDS, MeetRoomDeletionPolicyWithRecordings.CLOSE ); + + // Room 3 and Room 2 are scheduled to be closed, so we need to wait for the meeting to end before asserting the response + await waitForAllRoomsToDelete([room1.roomId]); + await waitForRoomToClose(room4.roomId); // Room 4 should be CLOSED expect(response.status).toBe(400); expect(response.body).toEqual({ message: '1 room(s) failed to process while deleting', @@ -252,6 +261,12 @@ describe('Room API Tests', () => { MeetingEndAction.NONE ); expectExtraFieldsInResponse(successfulRoom4.room); + + await endMeeting(room2.roomId, modToken2); + await endMeeting(room3.roomId, modToken3); + + await waitForAllRoomsToDelete([room2.roomId]); + await waitForRoomToClose(room3.roomId); // Room 3 should be CLOSED }); it('should return partial room properties based on fields parameter when some rooms fail due to active meetings', async () => { diff --git a/meet-ce/backend/tests/integration/api/rooms/delete-room.test.ts b/meet-ce/backend/tests/integration/api/rooms/delete-room.test.ts index cafb1f42..5178c662 100644 --- a/meet-ce/backend/tests/integration/api/rooms/delete-room.test.ts +++ b/meet-ce/backend/tests/integration/api/rooms/delete-room.test.ts @@ -12,18 +12,19 @@ import { expectSuccessListRecordingResponse, expectValidRoom } from '../../../helpers/assertion-helpers.js'; +import { disconnectFakeParticipants } from "../../../helpers/livekit-cli-helpers.js"; import { createRoom, deleteAllRecordings, deleteAllRooms, deleteRoom, - disconnectFakeParticipants, endMeeting, getAllRecordings, getRoom, startTestServer } from '../../../helpers/request-helpers.js'; import { setupSingleRoom, setupSingleRoomWithRecording } from '../../../helpers/test-scenarios.js'; +import { waitForParticipantsToDisconnect, waitForRoomToClose, waitForRoomToDelete } from '../../../helpers/wait-helpers.js'; describe('Room API Tests', () => { beforeAll(async () => { @@ -76,7 +77,7 @@ describe('Room API Tests', () => { MeetRoomDeletionSuccessCode.ROOM_WITH_ACTIVE_MEETING_DELETED ); expect(response.body).not.toHaveProperty('room'); - + await waitForRoomToDelete(roomId); // Wait for the meeting to end and webhook to be processed // Check room is deleted const getResponse = await getRoom(roomId); expect(getResponse.status).toBe(404); @@ -105,6 +106,7 @@ describe('Room API Tests', () => { // End meeting and check the room is deleted await endMeeting(roomId, moderatorToken); + await waitForRoomToDelete(roomId); // Wait for the meeting to end and webhook to be processed const getResponse = await getRoom(roomId); expect(getResponse.status).toBe(404); }); @@ -279,6 +281,7 @@ describe('Room API Tests', () => { const response = await deleteRoom(roomId, { withRecordings: MeetRoomDeletionPolicyWithRecordings.FORCE }); + await waitForRoomToDelete(roomId); // Wait for the webhook to process the deletion expect(response.status).toBe(200); expect(response.body).toHaveProperty( 'successCode', @@ -350,6 +353,7 @@ describe('Room API Tests', () => { MeetRoomDeletionSuccessCode.ROOM_WITH_ACTIVE_MEETING_AND_RECORDINGS_DELETED ); expect(response.body).not.toHaveProperty('room'); + await waitForRoomToDelete(roomId); // Wait for the meeting to end and webhook to be processed // Check the room and recordings are deleted const roomResponse = await getRoom(roomId); @@ -363,6 +367,7 @@ describe('Room API Tests', () => { withMeeting: MeetRoomDeletionPolicyWithMeeting.FORCE, withRecordings: MeetRoomDeletionPolicyWithRecordings.CLOSE }); + expect(response.status).toBe(200); expect(response.body).toHaveProperty( 'successCode', @@ -379,6 +384,8 @@ describe('Room API Tests', () => { MeetingEndAction.CLOSE ); expectExtraFieldsInResponse(response.body.room); + await waitForParticipantsToDisconnect([roomId]); // Wait for participants to be disconnected after meeting is closed + await waitForRoomToClose(roomId); // Wait for the room status to be updated to closed // Check that the room is closed and recordings are not deleted const roomResponse = await getRoom(roomId); @@ -434,6 +441,7 @@ describe('Room API Tests', () => { // End meeting and check the room and recordings are deleted await endMeeting(roomId, moderatorToken); + await waitForRoomToDelete(roomId); const roomResponse = await getRoom(roomId); expect(roomResponse.status).toBe(404); const recordingsResponse = await getAllRecordings({ roomId, maxItems: 1 }); diff --git a/meet-ce/backend/tests/integration/api/rooms/expired-rooms-gc.test.ts b/meet-ce/backend/tests/integration/api/rooms/expired-rooms-gc.test.ts index 86c98e18..b027ca4b 100644 --- a/meet-ce/backend/tests/integration/api/rooms/expired-rooms-gc.test.ts +++ b/meet-ce/backend/tests/integration/api/rooms/expired-rooms-gc.test.ts @@ -3,21 +3,22 @@ import { MeetRoomDeletionPolicyWithMeeting, MeetRoomDeletionPolicyWithRecordings import ms from 'ms'; import { setInternalConfig } from '../../../../src/config/internal-config.js'; import { MeetRoomHelper } from '../../../../src/helpers/room.helper.js'; +import { disconnectFakeParticipants, joinFakeParticipant } from '../../../helpers/livekit-cli-helpers.js'; import { createRoom, deleteAllRecordings, deleteAllRooms, - disconnectFakeParticipants, endMeeting, generateRoomMemberToken, getRoom, - joinFakeParticipant, runExpiredRoomsGC, sleep, startRecording, startTestServer } from '../../../helpers/request-helpers.js'; +import { waitForRoomToClose, waitForRoomToDelete } from '../../../helpers/wait-helpers.js'; + describe('Expired Rooms GC Tests', () => { beforeAll(async () => { setInternalConfig({ @@ -58,7 +59,7 @@ describe('Expired Rooms GC Tests', () => { autoDeletionDate: Date.now() + ms('1s') }); await joinFakeParticipant(createdRoom.roomId, 'test-participant'); - + await sleep('2s'); // Make sure the auto-deletion date has passed await runExpiredRoomsGC(); // The room should not be deleted but scheduled for deletion @@ -88,7 +89,7 @@ describe('Expired Rooms GC Tests', () => { autoDeletionDate: Date.now() + ms('1s') }); await joinFakeParticipant(room.roomId, 'test-participant'); - + await sleep('2s'); // Make sure the auto-deletion date has passed await runExpiredRoomsGC(); // The room should not be deleted but scheduled for deletion @@ -102,6 +103,7 @@ describe('Expired Rooms GC Tests', () => { const moderatorToken = await generateRoomMemberToken(room.roomId, { secret: moderatorSecret }); await endMeeting(room.roomId, moderatorToken); + await waitForRoomToDelete(room.roomId); // Verify that the room is deleted response = await getRoom(room.roomId); expect(response.status).toBe(404); @@ -179,6 +181,7 @@ describe('Expired Rooms GC Tests', () => { await startRecording(room1.roomId); await runExpiredRoomsGC(); + await waitForRoomToClose(room1.roomId); const response = await getRoom(room1.roomId); expect(response.status).toBe(200); diff --git a/meet-ce/backend/tests/integration/api/rooms/room-header-fields.test.ts b/meet-ce/backend/tests/integration/api/rooms/room-header-fields.test.ts index 40c8f0c6..e8121731 100644 --- a/meet-ce/backend/tests/integration/api/rooms/room-header-fields.test.ts +++ b/meet-ce/backend/tests/integration/api/rooms/room-header-fields.test.ts @@ -11,13 +11,13 @@ import { expectValidRoom, expectValidRoomWithFields } from '../../../helpers/assertion-helpers.js'; +import { disconnectFakeParticipants } from '../../../helpers/livekit-cli-helpers.js'; import { bulkDeleteRooms, createRoom, deleteAllRecordings, deleteAllRooms, deleteRoom, - disconnectFakeParticipants, getRoom, getRooms, startTestServer diff --git a/meet-ce/backend/tests/integration/api/rooms/update-room-status.test.ts b/meet-ce/backend/tests/integration/api/rooms/update-room-status.test.ts index 35ad4431..eaf86da1 100644 --- a/meet-ce/backend/tests/integration/api/rooms/update-room-status.test.ts +++ b/meet-ce/backend/tests/integration/api/rooms/update-room-status.test.ts @@ -1,15 +1,16 @@ import { afterEach, beforeAll, describe, expect, it } from '@jest/globals'; +import { MeetRoomStatus } from '@openvidu-meet/typings'; +import { disconnectFakeParticipants } from '../../../helpers/livekit-cli-helpers.js'; import { createRoom, deleteAllRooms, - disconnectFakeParticipants, endMeeting, getRoom, startTestServer, updateRoomStatus } from '../../../helpers/request-helpers.js'; import { setupSingleRoom } from '../../../helpers/test-scenarios.js'; -import { MeetRoomStatus } from '@openvidu-meet/typings'; +import { waitForRoomToClose } from '../../../helpers/wait-helpers.js'; describe('Room API Tests', () => { beforeAll(async () => { @@ -70,7 +71,7 @@ describe('Room API Tests', () => { // End meeting and verify closed status await endMeeting(roomData.room.roomId, roomData.moderatorToken); - + await waitForRoomToClose(roomData.room.roomId); getResponse = await getRoom(roomData.room.roomId); expect(getResponse.status).toBe(200); expect(getResponse.body.status).toEqual('closed'); diff --git a/meet-ce/backend/tests/integration/api/security/meeting-security.test.ts b/meet-ce/backend/tests/integration/api/security/meeting-security.test.ts index bdc80471..8b18503c 100644 --- a/meet-ce/backend/tests/integration/api/security/meeting-security.test.ts +++ b/meet-ce/backend/tests/integration/api/security/meeting-security.test.ts @@ -5,14 +5,12 @@ import request from 'supertest'; import { INTERNAL_CONFIG } from '../../../../src/config/internal-config.js'; import { MEET_ENV } from '../../../../src/environment.js'; import { - deleteAllRooms, disconnectFakeParticipants, - getFullPath, joinFakeParticipant, - loginRootAdmin, - startTestServer, updateParticipantMetadata -} from '../../../helpers/request-helpers.js'; +} from '../../../helpers/livekit-cli-helpers.js'; +import { deleteAllRooms, getFullPath, loginRootAdmin, startTestServer } from '../../../helpers/request-helpers.js'; + import { setupRoomMember, setupSingleRoom, updateRoomMemberPermissions } from '../../../helpers/test-scenarios.js'; import { RoomData, RoomMemberData } from '../../../interfaces/scenarios.js'; diff --git a/meet-ce/backend/tests/integration/api/security/recording-security.test.ts b/meet-ce/backend/tests/integration/api/security/recording-security.test.ts index 991b3961..ec71655d 100644 --- a/meet-ce/backend/tests/integration/api/security/recording-security.test.ts +++ b/meet-ce/backend/tests/integration/api/security/recording-security.test.ts @@ -5,11 +5,11 @@ import request from 'supertest'; import { INTERNAL_CONFIG } from '../../../../src/config/internal-config.js'; import { MEET_ENV } from '../../../../src/environment.js'; import { expectValidStartRecordingResponse } from '../../../helpers/assertion-helpers.js'; +import { disconnectFakeParticipants } from '../../../helpers/livekit-cli-helpers.js'; import { deleteAllRecordings, deleteAllRooms, deleteAllUsers, - disconnectFakeParticipants, getFullPath, getRecordingAccessSecret, sleep, @@ -17,6 +17,7 @@ import { startTestServer, stopAllRecordings } from '../../../helpers/request-helpers.js'; + import { setupCompletedRecording, setupRoomMember, diff --git a/meet-ce/backend/tests/integration/webhooks/webhook.test.ts b/meet-ce/backend/tests/integration/webhooks/webhook.test.ts index 4cf4f0ec..2ec60bfb 100644 --- a/meet-ce/backend/tests/integration/webhooks/webhook.test.ts +++ b/meet-ce/backend/tests/integration/webhooks/webhook.test.ts @@ -6,6 +6,7 @@ import { MeetRecordingStatus, MeetRoom, MeetRoomConfig, + MeetRoomDeletionPolicyWithMeeting, MeetWebhookEvent, MeetWebhookEventType } from '@openvidu-meet/typings'; @@ -18,7 +19,6 @@ import { disconnectFakeParticipants, endMeeting, restoreDefaultGlobalConfig, - sleep, startTestServer, updateWebhookConfig } from '../../helpers/request-helpers.js'; @@ -28,6 +28,7 @@ import { startWebhookServer, stopWebhookServer } from '../../helpers/test-scenarios.js'; +import { waitForRecordingToStop, waitForRoomToDelete } from '../../helpers/wait-helpers.js'; describe('Webhook Integration Tests', () => { let receivedWebhooks: { headers: http.IncomingHttpHeaders; body: MeetWebhookEvent }[] = []; @@ -86,8 +87,6 @@ describe('Webhook Integration Tests', () => { await setupSingleRoom(true); - // Wait for the room to be created - await sleep('3s'); expect(receivedWebhooks.length).toBe(0); }); @@ -95,9 +94,6 @@ describe('Webhook Integration Tests', () => { const context = await setupSingleRoom(true); const roomData = context.room; - // Wait for the room to be created - await sleep('1s'); - // Verify 'meetingStarted' webhook is sent expect(receivedWebhooks.length).toBeGreaterThanOrEqual(1); const meetingStartedWebhook = receivedWebhooks.find( @@ -123,9 +119,6 @@ describe('Webhook Integration Tests', () => { // Close the room await endMeeting(roomData.roomId, moderatorToken); - // Wait for the room to be closed - await sleep('1s'); - // Verify 'meetingEnded' webhook is sent expect(receivedWebhooks.length).toBeGreaterThanOrEqual(1); const meetingEndedWebhook = receivedWebhooks.find((w) => w.body.event === MeetWebhookEventType.MEETING_ENDED); @@ -144,8 +137,8 @@ describe('Webhook Integration Tests', () => { const context = await setupSingleRoom(true); const roomData = context.room; // Forcefully delete the room - await deleteRoom(roomData.roomId, { withMeeting: 'force' }); - + await deleteRoom(roomData.roomId, { withMeeting: MeetRoomDeletionPolicyWithMeeting.FORCE }); + await waitForRoomToDelete(roomData.roomId); // Wait for the webhook to process the deletion // Verify 'meetingEnded' webhook is sent expect(receivedWebhooks.length).toBeGreaterThanOrEqual(1); const meetingEndedWebhook = receivedWebhooks.find((w) => w.body.event === MeetWebhookEventType.MEETING_ENDED); @@ -167,6 +160,8 @@ describe('Webhook Integration Tests', () => { const roomData = context.room; const recordingId = context.recordingId; + await waitForRecordingToStop(recordingId!); // Wait for the recording to stop and webhook to be processed + const recordingWebhooks = receivedWebhooks.filter((w) => w.body.event.startsWith('recording')); // STARTED, ACTIVE, ENDING, COMPLETE expect(recordingWebhooks.length).toBe(4);