openvidu/meet-ce/backend/tests/helpers/request-helpers.ts
Carlos Santos 3d4f04feb1 Migrate to pnpm
chore: migrate project to pnpm and update workspace configuration

- Added pnpm workspace configuration in pnpm-workspace.yaml to manage packages.
- Updated prepare.sh script to use pnpm for installing and building packages.
- Modified testapp/package.json scripts to use pnpm instead of npm.
- Enhanced tsconfig.client.json and tsconfig.json with additional options and exclusions.
- Updated typings README.md to reflect the use of pnpm for installation and building.

streamline build process in prepare script and update dependency installation

Refactor code structure for improved readability and maintainability

refactor: optimize Dockerfile for better layer caching and dependency installation

refactor: migrate typings from '@lib/typings/ce' to '@openvidu-meet/typings'

- Updated imports across multiple components and services to use the new '@openvidu-meet/typings' package.
- Removed legacy typings references and adjusted paths in the frontend and webcomponent projects.
- Cleaned up the typings package structure and added build scripts for TypeScript.
- Removed the sync-types.sh script as it is no longer needed with the new structure.
- Updated README and package.json files to reflect the new package name and structure.
feat: add nodemon configuration for API documentation updates and enhance development scripts

feat: implement type checking in watch mode and update development scripts

feat: enhance development scripts with wait-for-typings and watch-typings utilities

fix: remove obsolete sync:backend script and enhance dev script with preserveWatchOutput option

feat: enhance development scripts with typings guard and improve watch behavior

Refactors build and dev scripts

Simplifies and consolidates build and development-related scripts for improved maintainability.

- Updates the build process to be more streamlined.
- Improves the development workflow by consolidating common tasks.
- Removes redundant scripts.

Replaces prepare script with meet script

Replaces the old `prepare.sh` script with a new `meet.sh` script to provide a more user-friendly and comprehensive interface for building and managing the project.

- Integrates command-line arguments for different build targets.
- Includes documentation generation for web components and REST APIs.
- Provides improved error handling and user feedback.
- Simplifies the build process with `pnpm`.

chore: update typescript version to 5.9.2 across multiple package.json files
refactor: replace constructor injection with inject function for AppDataService

feat: add commands to build webcomponent and run unit tests

meet.sh: add end-to-end testing support for webcomponent with optional Playwright browser installation

chore: update pnpm version to 10 and streamline test commands in workflows

meet.sh: rename build_webcomponent_only to build_webcomponent and streamline dependency installation

gitignore: add test-results directory to ignore list

meet.sh: rename build_webcomponent_only to build_webcomponent for consistency

Updated pnpm-lock.yml

refactor: streamline build scripts and enhance service start options in meet.sh

ci: update OpenVidu Meet actions to use meet-pnpm-migration version

refactor: update import paths for WebComponentCommand and WebComponentEvent to use shared typings

fix: add moduleNameMapper for typings path in jest configuration

fix: correct action version syntax for OpenVidu Meet setup in workflow

fix: update typings imports to use shared @openvidu-meet/typings package

fix: add skip-install and skip-typings options to meet.sh and update workflows

meet.sh: add development mode command and update start services options

fix: format code in meeting.component.ts and remove unused export in public-api.ts

added openvidu-components-angular to the local workspace and watch for changes in dev mode

fix: update Node.js action to v5 and streamline build steps in wc-unit-test.yaml

fix: remove pnpm install from build scripts in package.json

fix: update backend unit test workflow and add test unit command in meet.sh

fix: update unit test command in package.json to use pnpm exec

Updates import path for LiveKit permissions

Updates the import path for LiveKit permissions to align with the new typings package location, ensuring the test suite remains functional after the project's dependencies are migrated.

fix: remove redundant dependency installation and build steps in start_services function

fix: update Node.js setup action version and adjust OpenVidu actions for pnpm migration

fix: update tsconfig.json to exclude specific type declaration paths

fix: remove deprecated dependencies and update openapi-generate-html version

fix: update build messages and streamline start commands for production and CI modes

fix: update OpenVidu Meet and Testapp actions to use main branch and streamline pre-startup commands

Refactors type import for auth mode

Updates the import path for the authentication transport mode type.

This change ensures consistency across the application by using a centralized type definition.

Refactors backend integration tests

Streamlines the backend integration test workflow.

Consolidates test jobs for better organization and efficiency.
Leverages matrix testing for recordings API with different storage providers.
Improves AWS runner management for recording tests.
Adds artifact cleanup to prevent storage bloat.

Sets up Node.js and pnpm

Adds Node.js and pnpm setup steps to the integration test workflow.

This enables the use of pnpm for managing dependencies during integration tests.

Refactors test commands to use pnpm exec

Updates the test commands in package.json to use `pnpm exec`
for running Jest.

This ensures that the Jest CLI is executed within the pnpm
managed environment, resolving potential path and dependency
issues.

Refactors imports to use the new typings package

Updates imports to use the new `@openvidu-meet/typings` package.
Removes now-unnecessary module name mappings.

This change is part of the pnpm migration, ensuring correct
resolution of shared types.

Enhances backend integration tests and updates Node.js setup

Simplifies integration tests execution

Updates integration test scripts to streamline execution.

- Uses a single, parameterized script to run all backend integration tests.
- Removes redundant prefixes from test script names.

Refactors jest configuration to include moduleNameMapper for improved module resolution

Updates Jest integration test commands to use experimental VM modules and adjusts TypeScript root directory settings for better output structure

Ensures OpenVidu Meet logs are uploaded

Guarantees OpenVidu Meet logs are uploaded as artifacts, regardless of test outcome.

Moves log upload to ensure consistent capture, and does so for all test scenarios.

Commented backend integration tests

Fix build script to specify TypeScript configuration file

Refactor integration test command to use pnpm bin for jest execution

Update integration test commands to use relative paths for Jest execution

Revert "Commented backend integration tests"

This reverts commit 1da8cddb55e29036c2a816244f4bc8b665ede581.

Change log upload condition to trigger on failure for OpenVidu Meet logs

Add caching step for OpenVidu local deployment images in backend integration tests

Revert "Add caching step for OpenVidu local deployment images in backend integration tests"

This reverts commit bf4692d168c671100a88c09853a460ec5417979d.

Enhance AWS runner setup with storage provider matrix and update job names for clarity

Refactor AWS runner setup to separate jobs for S3, ABS, and GCS, enhancing clarity and maintainability

Update README.md to enhance structure and clarity, including detailed sections on prerequisites, getting started, development, and documentation.

Refactor Dockerfile and entrypoint script, remove deprecated image creation scripts, and enhance meet.sh with Docker build functionality and base href support

Update README.md to reflect changes in Docker image build commands using meet.sh

Update package.json to correct versioning and remove redundant entries

Added browser sync for live reloading

chore: update @typescript-eslint packages to version 8.46.1 in frontend and pnpm-lock.yaml

fix: correct argument skipping logic and ensure typings are built in install_dependencies function

Adapt project structure

backend: add TypeScript type annotations for Router instances in route files

fix: update path for nodemon configuration in dev:rest-api-docs script

fix: update paths in webcomponent documentation generation scripts

fix: update Dockerfile and entrypoint script for correct directory structure and improve error handling

fix: update .dockerignore and Dockerfile for improved directory handling and permissions; add backend type checker script

Added all tests files

Updates OpenVidu Meet action refs to main

Updates the OpenVidu Meet GitHub Action references
in the CI workflows to point to the `main` branch.

This ensures that the workflows use the latest version
of the action.
2025-10-15 17:42:04 +02:00

843 lines
25 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-explicit-any */
import { expect } from '@jest/globals';
import { ChildProcess, spawn } from 'child_process';
import { Express } from 'express';
import ms, { StringValue } from 'ms';
import request, { Response } from 'supertest';
import { container } from '../../src/config/index.js';
import INTERNAL_CONFIG from '../../src/config/internal-config.js';
import {
LIVEKIT_API_KEY,
LIVEKIT_API_SECRET,
MEET_INITIAL_ADMIN_PASSWORD,
MEET_INITIAL_ADMIN_USER,
MEET_INITIAL_API_KEY
} from '../../src/environment.js';
import { createApp, registerDependencies } from '../../src/server.js';
import { RecordingService, RoomService } from '../../src/services/index.js';
import {
AuthMode,
AuthTransportMode,
MeetRecordingAccess,
MeetRecordingInfo,
MeetRecordingStatus,
MeetRoom,
MeetRoomDeletionPolicyWithMeeting,
MeetRoomDeletionPolicyWithRecordings,
MeetRoomOptions,
ParticipantRole,
WebhookConfig
} from '@openvidu-meet/typings';
const CREDENTIALS = {
admin: {
username: MEET_INITIAL_ADMIN_USER,
password: MEET_INITIAL_ADMIN_PASSWORD
}
};
let app: Express;
const fakeParticipantsProcesses = new Map<string, ChildProcess>();
export const sleep = (time: StringValue) => {
return new Promise((resolve) => setTimeout(resolve, ms(time)));
};
export const startTestServer = (): Express => {
if (app) {
return app;
}
registerDependencies();
app = createApp();
return app;
};
export const generateApiKey = async (): Promise<string> => {
checkAppIsRunning();
const accessToken = await loginUser();
const response = await request(app)
.post(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/auth/api-keys`)
.set(selectHeaderBasedOnToken(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken), accessToken)
.send();
expect(response.status).toBe(201);
expect(response.body).toHaveProperty('key');
return response.body.key;
};
export const getApiKeys = async () => {
checkAppIsRunning();
const accessToken = await loginUser();
const response = await request(app)
.get(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/auth/api-keys`)
.set(selectHeaderBasedOnToken(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken), accessToken)
.send();
return response;
};
export const getRoomsAppearanceConfig = async () => {
checkAppIsRunning();
const response = await request(app)
.get(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/config/rooms/appearance`)
.send();
return response;
};
export const updateRoomsAppearanceConfig = async (config: any) => {
checkAppIsRunning();
const accessToken = await loginUser();
const response = await request(app)
.put(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/config/rooms/appearance`)
.set(selectHeaderBasedOnToken(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken), accessToken)
.send(config);
return response;
};
export const getWebbhookConfig = async () => {
checkAppIsRunning();
const accessToken = await loginUser();
const response = await request(app)
.get(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/config/webhooks`)
.set(selectHeaderBasedOnToken(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken), accessToken)
.send();
return response;
};
export const updateWebbhookConfig = async (config: WebhookConfig) => {
checkAppIsRunning();
const accessToken = await loginUser();
const response = await request(app)
.put(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/config/webhooks`)
.set(selectHeaderBasedOnToken(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken), accessToken)
.send(config);
return response;
};
export const testWebhookUrl = async (url: string) => {
checkAppIsRunning();
const response = await request(app)
.post(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/config/webhooks/test`)
.send({ url });
return response;
};
export const getSecurityConfig = async () => {
checkAppIsRunning();
const response = await request(app).get(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/config/security`).send();
return response;
};
export const updateSecurityConfig = async (config: any) => {
checkAppIsRunning();
const accessToken = await loginUser();
const response = await request(app)
.put(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/config/security`)
.set(selectHeaderBasedOnToken(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken), accessToken)
.send(config);
return response;
};
export const changeSecurityConfig = async (authMode: AuthMode) => {
// Get current config to avoid overwriting other properties
let response = await getSecurityConfig();
expect(response.status).toBe(200);
const currentConfig = response.body;
currentConfig.authentication.authModeToAccessRoom = authMode;
response = await updateSecurityConfig(currentConfig);
expect(response.status).toBe(200);
};
export const changeAuthTransportMode = async (authTransportMode: AuthTransportMode) => {
// Get current config to avoid overwriting other properties
let response = await getSecurityConfig();
expect(response.status).toBe(200);
const currentConfig = response.body;
currentConfig.authentication.authTransportMode = authTransportMode;
response = await updateSecurityConfig(currentConfig);
expect(response.status).toBe(200);
};
const getAuthTransportMode = async (): Promise<AuthTransportMode> => {
const response = await getSecurityConfig();
return response.body.authentication.authTransportMode;
};
/**
* Logs in a user and returns the access token in the format
* "Bearer <token>" or the cookie string if in cookie mode
*/
export const loginUser = async (): Promise<string> => {
checkAppIsRunning();
const response = await request(app)
.post(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/auth/login`)
.send(CREDENTIALS.admin)
.expect(200);
const authTransportMode = await getAuthTransportMode();
// Return token in header or cookie based on transport mode
if (authTransportMode === AuthTransportMode.COOKIE) {
const cookie = extractCookieFromHeaders(response, INTERNAL_CONFIG.ACCESS_TOKEN_COOKIE_NAME);
return cookie!;
}
expect(response.body).toHaveProperty('accessToken');
return `Bearer ${response.body.accessToken}`;
};
/**
* Extracts cookie from response headers
*
* @param response - The supertest response
* @param cookieName - Name of the cookie to extract
* @returns The cookie string
*/
export const extractCookieFromHeaders = (response: Response, cookieName: string): string | undefined => {
expect(response.headers['set-cookie']).toBeDefined();
const cookies = response.headers['set-cookie'] as unknown as string[];
return cookies?.find((cookie) => cookie.startsWith(`${cookieName}=`));
};
/**
* Selects the appropriate HTTP header name based on the format of the provided access token.
*
* If the access token starts with 'Bearer ', the specified header name is returned (typically 'Authorization').
* Otherwise, 'Cookie' is returned, indicating that the token should be sent as a cookie.
*/
const selectHeaderBasedOnToken = (headerName: string, accessToken: string): string => {
return accessToken.startsWith('Bearer ') ? headerName : 'Cookie';
};
export const getProfile = async (accessToken: string) => {
checkAppIsRunning();
return await request(app)
.get(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/users/profile`)
.set(selectHeaderBasedOnToken(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken), accessToken)
.send();
};
export const changePassword = async (currentPassword: string, newPassword: string, accessToken: string) => {
checkAppIsRunning();
return await request(app)
.post(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/users/change-password`)
.set(selectHeaderBasedOnToken(INTERNAL_CONFIG.ACCESS_TOKEN_HEADER, accessToken), accessToken)
.send({ currentPassword, newPassword });
};
export const createRoom = async (options: MeetRoomOptions = {}): Promise<MeetRoom> => {
checkAppIsRunning();
const response = await request(app)
.post(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms`)
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_INITIAL_API_KEY)
.send(options)
.expect(201);
return response.body;
};
export const getRooms = async (query: Record<string, any> = {}) => {
checkAppIsRunning();
return await request(app)
.get(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms`)
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_INITIAL_API_KEY)
.query(query);
};
/**
* Retrieves information about a specific room from the API.
*
* @param roomId - The unique identifier of the room to retrieve
* @param fields - Optional fields to filter in the response
* @returns A Promise that resolves to the room data
* @throws Error if the app instance is not defined
*/
export const getRoom = async (roomId: string, fields?: string, participantToken?: string, role?: ParticipantRole) => {
checkAppIsRunning();
const req = request(app).get(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms/${roomId}`).query({ fields });
if (participantToken && role) {
req.set(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, participantToken).set(
INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER,
role
);
} else {
req.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_INITIAL_API_KEY);
}
return await req;
};
export const getRoomConfig = async (roomId: string) => {
checkAppIsRunning();
return await request(app)
.get(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms/${roomId}/config`)
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_INITIAL_API_KEY)
.send();
};
export const updateRoomConfig = async (roomId: string, config: any) => {
checkAppIsRunning();
return await request(app)
.put(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms/${roomId}/config`)
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_INITIAL_API_KEY)
.send({ config });
};
export const updateRecordingAccessConfigInRoom = async (roomId: string, recordingAccess: MeetRecordingAccess) => {
const response = await updateRoomConfig(roomId, {
recording: {
enabled: true,
allowAccessTo: recordingAccess
},
chat: {
enabled: true
},
virtualBackground: {
enabled: true
}
});
expect(response.status).toBe(200);
};
export const updateRoomStatus = async (roomId: string, status: string) => {
checkAppIsRunning();
return await request(app)
.put(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms/${roomId}/status`)
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_INITIAL_API_KEY)
.send({ status });
};
export const deleteRoom = async (roomId: string, query: Record<string, any> = {}) => {
checkAppIsRunning();
const result = await request(app)
.delete(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms/${roomId}`)
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_INITIAL_API_KEY)
.query(query);
await sleep('1s');
return result;
};
export const bulkDeleteRooms = async (roomIds: any[], withMeeting?: string, withRecordings?: string) => {
checkAppIsRunning();
const result = await request(app)
.delete(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms`)
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_INITIAL_API_KEY)
.query({ roomIds: roomIds.join(','), withMeeting, withRecordings });
await sleep('1s');
return result;
};
export const deleteAllRooms = async () => {
checkAppIsRunning();
let nextPageToken: string | undefined;
do {
const response = await getRooms({ fields: 'roomId', maxItems: 100, nextPageToken });
expect(response.status).toBe(200);
nextPageToken = response.body.pagination?.nextPageToken ?? undefined;
const roomIds = response.body.rooms.map((room: { roomId: string }) => room.roomId);
if (roomIds.length === 0) {
break;
}
await bulkDeleteRooms(
roomIds,
MeetRoomDeletionPolicyWithMeeting.FORCE,
MeetRoomDeletionPolicyWithRecordings.FORCE
);
} while (nextPageToken);
};
/**
* Runs the room garbage collector to delete expired rooms.
*
* This function retrieves the RoomService from the dependency injection container
* and calls its deleteExpiredRooms method to clean up expired rooms.
* It then waits for 1 second before completing.
*/
export const runRoomGarbageCollector = async () => {
checkAppIsRunning();
const roomService = container.get(RoomService);
await (roomService as any)['deleteExpiredRooms']();
await sleep('1s');
};
export const runReleaseActiveRecordingLock = async (roomId: string) => {
checkAppIsRunning();
const recordingService = container.get(RecordingService);
await recordingService.releaseRecordingLockIfNoEgress(roomId);
};
export const getRoomRoles = async (roomId: string) => {
checkAppIsRunning();
const response = await request(app)
.get(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/rooms/${roomId}/roles`)
.send();
return response;
};
export const getRoomRoleBySecret = async (roomId: string, secret: string) => {
checkAppIsRunning();
const response = await request(app)
.get(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/rooms/${roomId}/roles/${secret}`)
.send();
return response;
};
export const generateParticipantTokenRequest = async (participantOptions: any, previousToken?: string) => {
checkAppIsRunning();
// Disable authentication to generate the token
await changeSecurityConfig(AuthMode.NONE);
// Generate the participant token
const req = request(app).post(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/participants/token`);
if (previousToken) {
req.set(selectHeaderBasedOnToken(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, previousToken), previousToken);
}
req.send(participantOptions);
return await req;
};
/**
* Generates a participant token for a room and returns the JWT token in the format "Bearer <token>"
*/
export const generateParticipantToken = async (
roomId: string,
secret: string,
participantName: string
): Promise<string> => {
const response = await generateParticipantTokenRequest({
roomId,
secret,
participantName
});
expect(response.status).toBe(200);
const authTransportMode = await getAuthTransportMode();
// Return token in header or cookie based on transport mode
if (authTransportMode === AuthTransportMode.COOKIE) {
const cookie = extractCookieFromHeaders(response, INTERNAL_CONFIG.PARTICIPANT_TOKEN_COOKIE_NAME);
return cookie!;
}
expect(response.body).toHaveProperty('token');
return `Bearer ${response.body.token}`;
};
export const refreshParticipantToken = async (participantOptions: any, previousToken: string) => {
checkAppIsRunning();
// Disable authentication to generate the token
await changeSecurityConfig(AuthMode.NONE);
const response = await request(app)
.post(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/participants/token/refresh`)
.set(selectHeaderBasedOnToken(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, previousToken), previousToken)
.send(participantOptions);
return response;
};
/**
* 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) => {
const process = spawn('lk', [
'room',
'join',
'--identity',
participantIdentity,
'--publish-demo',
roomId,
'--api-key',
LIVEKIT_API_KEY,
'--api-secret',
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: any) => {
await ensureLivekitCliInstalled();
spawn('lk', [
'room',
'participants',
'update',
'--room',
roomId,
'--identity',
participantIdentity,
'--metadata',
JSON.stringify(metadata),
'--api-key',
LIVEKIT_API_KEY,
'--api-secret',
LIVEKIT_API_SECRET
]);
await sleep('1s');
};
/**
* 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);
});
};
export const disconnectFakeParticipants = async () => {
fakeParticipantsProcesses.forEach((process, participant) => {
process.kill();
console.log(`Stopped process for participant '${participant}'`);
});
fakeParticipantsProcesses.clear();
await sleep('1s');
};
export const updateParticipant = async (
roomId: string,
participantIdentity: string,
newRole: ParticipantRole,
moderatorToken: string
) => {
checkAppIsRunning();
const response = await request(app)
.put(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/meetings/${roomId}/participants/${participantIdentity}/role`)
.set(selectHeaderBasedOnToken(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, moderatorToken), moderatorToken)
.set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.MODERATOR)
.send({ role: newRole });
return response;
};
export const kickParticipant = async (roomId: string, participantIdentity: string, moderatorToken: string) => {
checkAppIsRunning();
const response = await request(app)
.delete(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/meetings/${roomId}/participants/${participantIdentity}`)
.set(selectHeaderBasedOnToken(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, moderatorToken), moderatorToken)
.set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.MODERATOR)
.send();
return response;
};
export const endMeeting = async (roomId: string, moderatorToken: string) => {
checkAppIsRunning();
const response = await request(app)
.delete(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/meetings/${roomId}`)
.set(selectHeaderBasedOnToken(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, moderatorToken), moderatorToken)
.set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.MODERATOR)
.send();
await sleep('1s');
return response;
};
export const generateRecordingTokenRequest = async (roomId: string, secret: string) => {
checkAppIsRunning();
// Disable authentication to generate the token
await changeSecurityConfig(AuthMode.NONE);
const response = await request(app)
.post(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/rooms/${roomId}/recording-token`)
.send({
secret
});
return response;
};
/**
* Generates a token for retrieving/deleting recordings from a room and returns the JWT token in the format "Bearer <token>"
*/
export const generateRecordingToken = async (roomId: string, secret: string) => {
const response = await generateRecordingTokenRequest(roomId, secret);
expect(response.status).toBe(200);
const authTransportMode = await getAuthTransportMode();
// Return token in header or cookie based on transport mode
if (authTransportMode === AuthTransportMode.COOKIE) {
const cookie = extractCookieFromHeaders(response, INTERNAL_CONFIG.RECORDING_TOKEN_COOKIE_NAME);
return cookie!;
}
expect(response.body).toHaveProperty('token');
return `Bearer ${response.body.token}`;
};
export const startRecording = async (roomId: string, moderatorToken: string) => {
checkAppIsRunning();
return await request(app)
.post(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/recordings`)
.set(selectHeaderBasedOnToken(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, moderatorToken), moderatorToken)
.set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.MODERATOR)
.send({
roomId
});
};
export const stopRecording = async (recordingId: string, moderatorToken: string) => {
checkAppIsRunning();
const response = await request(app)
.post(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/recordings/${recordingId}/stop`)
.set(selectHeaderBasedOnToken(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, moderatorToken), moderatorToken)
.set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.MODERATOR)
.send();
await sleep('2.5s');
return response;
};
export const getRecording = async (recordingId: string) => {
checkAppIsRunning();
return await request(app)
.get(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/recordings/${recordingId}`)
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_INITIAL_API_KEY);
};
export const getRecordingMedia = async (recordingId: string, range?: string) => {
checkAppIsRunning();
const req = request(app)
.get(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/recordings/${recordingId}/media`)
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_INITIAL_API_KEY);
if (range) {
req.set('range', range);
}
return await req;
};
export const getRecordingUrl = async (recordingId: string, privateAccess = false) => {
checkAppIsRunning();
return await request(app)
.get(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/recordings/${recordingId}/url`)
.query({ privateAccess })
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_INITIAL_API_KEY);
};
export const deleteRecording = async (recordingId: string) => {
checkAppIsRunning();
return await request(app)
.delete(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/recordings/${recordingId}`)
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_INITIAL_API_KEY);
};
export const bulkDeleteRecordings = async (recordingIds: any[], recordingToken?: string): Promise<Response> => {
checkAppIsRunning();
const req = request(app)
.delete(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/recordings`)
.query({ recordingIds: recordingIds.join(',') });
if (recordingToken) {
req.set(selectHeaderBasedOnToken(INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, recordingToken), recordingToken);
} else {
req.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_INITIAL_API_KEY);
}
return await req;
};
export const downloadRecordings = async (
recordingIds: string[],
asBuffer = true,
recordingToken?: string
): Promise<Response> => {
checkAppIsRunning();
const req = request(app)
.get(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/recordings/download`)
.query({ recordingIds: recordingIds.join(',') });
if (recordingToken) {
req.set(selectHeaderBasedOnToken(INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, recordingToken), recordingToken);
} else {
req.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_INITIAL_API_KEY);
}
if (asBuffer) {
return await req.buffer().parse((res, cb) => {
const data: Buffer[] = [];
res.on('data', (chunk) => data.push(chunk));
res.on('end', () => cb(null, Buffer.concat(data)));
});
}
return await req;
};
export const stopAllRecordings = async (moderatorToken: string) => {
checkAppIsRunning();
const response = await getAllRecordings();
const recordingIds: string[] = response.body.recordings
.filter((rec: MeetRecordingInfo) => rec.status === MeetRecordingStatus.ACTIVE)
.map((recording: { recordingId: string }) => recording.recordingId);
if (recordingIds.length === 0) {
return;
}
console.log(`Stopping ${recordingIds.length} recordings...`, recordingIds);
const tasks = recordingIds.map((recordingId: string) =>
request(app)
.post(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/recordings/${recordingId}/stop`)
.set(selectHeaderBasedOnToken(INTERNAL_CONFIG.PARTICIPANT_TOKEN_HEADER, moderatorToken), moderatorToken)
.set(INTERNAL_CONFIG.PARTICIPANT_ROLE_HEADER, ParticipantRole.MODERATOR)
.send()
);
const results = await Promise.all(tasks);
// Check responses
results.forEach((response) => {
expect(response.status).toBe(202);
});
await sleep('1s');
};
export const getAllRecordings = async (query: Record<string, any> = {}) => {
checkAppIsRunning();
return await request(app)
.get(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/recordings`)
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_INITIAL_API_KEY)
.query(query);
};
export const getAllRecordingsFromRoom = async (recordingToken: string) => {
checkAppIsRunning();
return await request(app)
.get(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/recordings`)
.set(selectHeaderBasedOnToken(INTERNAL_CONFIG.RECORDING_TOKEN_HEADER, recordingToken), recordingToken);
};
export const deleteAllRecordings = async () => {
checkAppIsRunning();
let nextPageToken: string | undefined;
do {
const response: any = await getAllRecordings({
fields: 'recordingId',
maxItems: 100,
nextPageToken
});
expect(response.status).toBe(200);
nextPageToken = response.body.pagination?.nextPageToken ?? undefined;
const recordingIds = response.body.recordings.map(
(recording: { recordingId: string }) => recording.recordingId
);
if (recordingIds.length === 0) {
break;
}
await bulkDeleteRecordings(recordingIds);
} while (nextPageToken);
};
// PRIVATE METHODS
const checkAppIsRunning = () => {
if (!app) {
throw new Error('App instance is not defined');
}
};