backend: Enhances test reliability with active waiting

Replaces arbitrary `sleep()` calls in integration tests with explicit `wait-helpers`. These new helpers actively poll for specific conditions (e.g., room deletion, participant connection, recording status) directly from the database or LiveKit, rather than relying on fixed delays. This significantly reduces test flakiness and improves the accuracy of assertions.

Extracts LiveKit CLI interaction helpers (`joinFakeParticipant`, `disconnectFakeParticipants`, `updateParticipantMetadata`) into a dedicated `livekit-cli-helpers.ts` file for better organization and separation of concerns. Updates numerous integration tests to utilize the new waiting and LiveKit CLI helpers.
This commit is contained in:
CSantosM 2026-03-06 16:38:12 +01:00
parent 467ebf6d49
commit c5bca6e133
31 changed files with 652 additions and 208 deletions

View File

@ -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<string, ChildProcess>();
/** Tracks all room IDs that currently have at least one fake participant joined via joinFakeParticipant. */
const fakeParticipantRooms = new Set<string>();
/**
* 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<string> => {
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<string[]> => {
const output = await executeLivekitCliCommand(['room', 'participants', 'list', roomId]);
return parseParticipantIdentities(output);
};
const ensureLivekitCliInstalled = async (): Promise<void> => {
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);
});
};

View File

@ -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<string, ChildProcess>();
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<void> => {
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);
});
};

View File

@ -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,

View File

@ -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<boolean>,
options: {
intervalMs?: number;
timeoutMs?: number;
errorMessage?: string;
} = {}
): Promise<void> => {
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<void>((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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<string, unknown>,
timeoutMs = DEFAULT_PARTICIPANT_TIMEOUT_MS
): Promise<void> => {
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<void> => {
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<void> => {
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<void> => {
await Promise.all(recordingIds.map((id) => waitForRecordingToStop(id, timeoutMs)));
};

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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', () => {

View File

@ -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', () => {

View File

@ -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', () => {

View File

@ -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', () => {

View File

@ -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', () => {

View File

@ -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';

View File

@ -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';

View File

@ -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();
});

View File

@ -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';
/**

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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;

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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 () => {

View File

@ -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 });

View File

@ -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);

View File

@ -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

View File

@ -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');

View File

@ -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';

View File

@ -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,

View File

@ -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);