backend: Add bulk delete recordings tests and enhance test utilities
This commit is contained in:
parent
32c0c9d242
commit
f8a176b4fa
@ -0,0 +1,187 @@
|
||||
import { afterAll, beforeAll, describe, expect, it } from '@jest/globals';
|
||||
import {
|
||||
bulkDeleteRecordings,
|
||||
deleteAllRecordings,
|
||||
deleteAllRooms,
|
||||
startTestServer,
|
||||
stopRecording,
|
||||
stopTestServer
|
||||
} from '../../../utils/helpers';
|
||||
import { setupMultiRecordingsTestContext } from '../../../utils/test-scenarios';
|
||||
|
||||
describe('Recording API Tests', () => {
|
||||
beforeAll(async () => {
|
||||
await startTestServer();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await deleteAllRooms();
|
||||
await deleteAllRecordings();
|
||||
await stopTestServer();
|
||||
});
|
||||
|
||||
describe('Bulk Delete Recording Tests', () => {
|
||||
it('"should return 200 when mixed valid and non-existent IDs are provided', async () => {
|
||||
const testContext = await setupMultiRecordingsTestContext(3, 3, 3, '0s');
|
||||
const recordingIds = testContext.rooms.map((room) => room.recordingId);
|
||||
const nonExistentIds = ['nonExistent--EG_000--1234', 'nonExistent--EG_111--5678'];
|
||||
const mixedIds = [...recordingIds, ...nonExistentIds];
|
||||
|
||||
const deleteResponse = await bulkDeleteRecordings(mixedIds);
|
||||
|
||||
expect(deleteResponse.status).toBe(200);
|
||||
expect(deleteResponse.body).toEqual({
|
||||
deleted: expect.arrayContaining(recordingIds),
|
||||
notDeleted: expect.arrayContaining(
|
||||
nonExistentIds.map((id) => ({
|
||||
recordingId: id,
|
||||
error: expect.stringContaining(`Recording '${id}' not found`)
|
||||
}))
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 200 with mixed results when some recordings are in active state', async () => {
|
||||
const testContext = await setupMultiRecordingsTestContext(3, 3, 2, '0s');
|
||||
const activeRecordingRoom = testContext.getLastRoom();
|
||||
const recordingIds = testContext.rooms
|
||||
.map((room) => room.recordingId)
|
||||
.filter((id) => id !== activeRecordingRoom!.recordingId);
|
||||
|
||||
const activeRecordingId = activeRecordingRoom?.recordingId;
|
||||
let deleteResponse = await bulkDeleteRecordings([...recordingIds, activeRecordingId]);
|
||||
|
||||
expect(deleteResponse.status).toBe(200);
|
||||
expect(deleteResponse.body).toEqual({
|
||||
deleted: expect.arrayContaining(recordingIds),
|
||||
notDeleted: [
|
||||
{
|
||||
recordingId: activeRecordingId,
|
||||
error: expect.stringContaining(`Recording '${activeRecordingId}' is not stopped yet`)
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await stopRecording(activeRecordingId!, activeRecordingRoom!.moderatorCookie);
|
||||
|
||||
deleteResponse = await bulkDeleteRecordings([activeRecordingId]);
|
||||
|
||||
expect(deleteResponse.status).toBe(204);
|
||||
expect(deleteResponse.body).toStrictEqual({});
|
||||
});
|
||||
|
||||
it('should not delete any recordings and return 200', async () => {
|
||||
const testContext = await setupMultiRecordingsTestContext(2, 2, 0, '0s');
|
||||
const recordingIds = testContext.rooms.map((room) => room.recordingId);
|
||||
const deleteResponse = await bulkDeleteRecordings(recordingIds);
|
||||
expect(deleteResponse.status).toBe(200);
|
||||
expect(deleteResponse.body).toEqual({
|
||||
deleted: [],
|
||||
notDeleted: expect.arrayContaining(
|
||||
recordingIds.map((id) => ({
|
||||
recordingId: id,
|
||||
error: expect.stringContaining(`Recording '${id}' is not stopped yet`)
|
||||
}))
|
||||
)
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
recordingIds.map((id, index) => {
|
||||
return stopRecording(id!, testContext.getRoomByIndex(index)!.moderatorCookie);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should delete all recordings and return 204 when all operations succeed', async () => {
|
||||
const response = await setupMultiRecordingsTestContext(5, 5, 5, '0s');
|
||||
const recordingIds = response.rooms.map((room) => room.recordingId);
|
||||
const deleteResponse = await bulkDeleteRecordings(recordingIds);
|
||||
|
||||
expect(deleteResponse.status).toBe(204);
|
||||
});
|
||||
|
||||
it('should handle single recording deletion correctly', async () => {
|
||||
const testContext = await setupMultiRecordingsTestContext(1, 1, 1, '0s');
|
||||
const recordingId = testContext.rooms[0].recordingId;
|
||||
const deleteResponse = await bulkDeleteRecordings([recordingId]);
|
||||
|
||||
expect(deleteResponse.status).toBe(204);
|
||||
expect(deleteResponse.body).toStrictEqual({});
|
||||
});
|
||||
|
||||
it('should handle duplicate recording IDs by treating them as a single delete', async () => {
|
||||
const testContext = await setupMultiRecordingsTestContext(1, 1, 1, '0s');
|
||||
const recordingId = testContext.getRoomByIndex(0)!.recordingId;
|
||||
const deleteResponse = await bulkDeleteRecordings([recordingId, recordingId]);
|
||||
|
||||
expect(deleteResponse.status).toBe(204);
|
||||
expect(deleteResponse.body).toStrictEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Bulk Delete Recording Validation', () => {
|
||||
it('should handle empty recordingIds array gracefully', async () => {
|
||||
const deleteResponse = await bulkDeleteRecordings([]);
|
||||
|
||||
expect(deleteResponse.status).toBe(422);
|
||||
expect(deleteResponse.body).toEqual({
|
||||
details: [
|
||||
{
|
||||
field: 'recordingIds',
|
||||
message: 'recordingIds must contain at least one item'
|
||||
}
|
||||
],
|
||||
error: 'Unprocessable Entity',
|
||||
message: 'Invalid request'
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject a CSV string with invalid format', async () => {
|
||||
const invalidRecordingIds = 'invalid--recording.id,invalid--EG_111--5678';
|
||||
const deleteResponse = await bulkDeleteRecordings([invalidRecordingIds]);
|
||||
|
||||
expect(deleteResponse.status).toBe(422);
|
||||
expect(deleteResponse.body).toMatchObject({
|
||||
details: [
|
||||
{
|
||||
message: 'recordingId does not follow the expected format'
|
||||
}
|
||||
],
|
||||
error: 'Unprocessable Entity',
|
||||
message: 'Invalid request'
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject an array containing empty strings after sanitization', async () => {
|
||||
const invalidRecordingIds = ['', ' '];
|
||||
const deleteResponse = await bulkDeleteRecordings(invalidRecordingIds);
|
||||
|
||||
expect(deleteResponse.status).toBe(422);
|
||||
expect(deleteResponse.body).toMatchObject({
|
||||
details: [
|
||||
{
|
||||
message: 'recordingIds must contain at least one item'
|
||||
}
|
||||
],
|
||||
error: 'Unprocessable Entity',
|
||||
message: 'Invalid request'
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject an array with mixed valid and totally invalid IDs', async () => {
|
||||
const invalidRecordingIds = ['valid--EG_111--5678', 'invalid--recording.id'];
|
||||
const deleteResponse = await bulkDeleteRecordings(invalidRecordingIds);
|
||||
|
||||
expect(deleteResponse.status).toBe(422);
|
||||
expect(deleteResponse.body).toMatchObject({
|
||||
details: [
|
||||
{
|
||||
message: 'recordingId does not follow the expected format'
|
||||
}
|
||||
],
|
||||
error: 'Unprocessable Entity',
|
||||
message: 'Invalid request'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,5 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import request from 'supertest';
|
||||
import request, { Response } from 'supertest';
|
||||
import { Express } from 'express';
|
||||
import { Server } from 'http';
|
||||
import { createApp, registerDependencies } from '../../src/server.js';
|
||||
@ -20,8 +20,8 @@ import INTERNAL_CONFIG from '../../src/config/internal-config.js';
|
||||
import { ChildProcess, execSync, spawn } from 'child_process';
|
||||
import { container } from '../../src/config/dependency-injector.config.js';
|
||||
import { RoomService } from '../../src/services/room.service.js';
|
||||
import { MeetRoomHelper } from '../../src/helpers/room.helper.js';
|
||||
import { RecordingService } from '../../src/services/recording.service.js';
|
||||
import ms, { StringValue } from 'ms';
|
||||
|
||||
const CREDENTIALS = {
|
||||
user: {
|
||||
@ -39,8 +39,8 @@ let server: Server;
|
||||
|
||||
const fakeParticipantsProcesses = new Map<string, ChildProcess>();
|
||||
|
||||
export const sleep = (ms: number) => {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
export const sleep = (time: StringValue) => {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms(time)));
|
||||
};
|
||||
|
||||
/**
|
||||
@ -187,40 +187,6 @@ export const updateRoomPreferences = async (roomId: string, preferences: any) =>
|
||||
.send(preferences);
|
||||
};
|
||||
|
||||
/**
|
||||
* Asserts that a rooms response matches the expected values for testing purposes.
|
||||
* Validates the room array length and pagination properties.
|
||||
*
|
||||
* @param body - The API response body to validate
|
||||
* @param expectedRoomLength - The expected number of rooms in the response
|
||||
* @param expectedMaxItems - The expected maximum number of items in pagination
|
||||
* @param expectedTruncated - The expected value for pagination.isTruncated flag
|
||||
* @param expectedNextPageToken - The expected presence of pagination.nextPageToken
|
||||
* (if true, expects nextPageToken to be defined;
|
||||
* if false, expects nextPageToken to be undefined)
|
||||
*/
|
||||
export const assertSuccessRoomsResponse = (
|
||||
response: any,
|
||||
expectedRoomLength: number,
|
||||
expectedMaxItems: number,
|
||||
expectedTruncated: boolean,
|
||||
expectedNextPageToken: boolean
|
||||
) => {
|
||||
const { body } = response;
|
||||
expect(response.status).toBe(200);
|
||||
expect(body).toBeDefined();
|
||||
expect(body.rooms).toBeDefined();
|
||||
expect(Array.isArray(body.rooms)).toBe(true);
|
||||
expect(body.rooms.length).toBe(expectedRoomLength);
|
||||
expect(body.pagination).toBeDefined();
|
||||
expect(body.pagination.isTruncated).toBe(expectedTruncated);
|
||||
|
||||
expectedNextPageToken
|
||||
? expect(body.pagination.nextPageToken).toBeDefined()
|
||||
: expect(body.pagination.nextPageToken).toBeUndefined();
|
||||
expect(body.pagination.maxItems).toBe(expectedMaxItems);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves information about a specific room from the API.
|
||||
*
|
||||
@ -262,16 +228,6 @@ export const bulkDeleteRooms = async (roomIds: any[], force?: any) => {
|
||||
.query({ roomIds: roomIds.join(','), force });
|
||||
};
|
||||
|
||||
export const assertEmptyRooms = async () => {
|
||||
if (!app) {
|
||||
throw new Error('App instance is not defined');
|
||||
}
|
||||
|
||||
const response = await getRooms();
|
||||
|
||||
assertSuccessRoomsResponse(response, 0, 10, false, false);
|
||||
};
|
||||
|
||||
/**
|
||||
* Runs the room garbage collector to delete expired rooms.
|
||||
*
|
||||
@ -286,8 +242,6 @@ export const runRoomGarbageCollector = async () => {
|
||||
|
||||
const roomService = container.get(RoomService);
|
||||
await (roomService as any)['deleteExpiredRooms']();
|
||||
|
||||
await sleep(1000);
|
||||
};
|
||||
|
||||
export const runReleaseActiveRecordingLock = async (roomId: string) => {
|
||||
@ -327,8 +281,6 @@ export const deleteAllRooms = async () => {
|
||||
.delete(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms`)
|
||||
.query({ roomIds: roomIds.join(','), force: true })
|
||||
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_API_KEY);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for 1 second
|
||||
} while (nextPageToken);
|
||||
};
|
||||
|
||||
@ -407,7 +359,7 @@ export const joinFakeParticipant = async (roomId: string, participantName: strin
|
||||
|
||||
// Store the process to be able to terminate it later
|
||||
fakeParticipantsProcesses.set(participantName, process);
|
||||
await sleep(1000);
|
||||
await sleep('1s');
|
||||
};
|
||||
|
||||
export const disconnectFakeParticipants = async () => {
|
||||
@ -417,7 +369,7 @@ export const disconnectFakeParticipants = async () => {
|
||||
});
|
||||
|
||||
fakeParticipantsProcesses.clear();
|
||||
await sleep(1000);
|
||||
await sleep('1s');
|
||||
};
|
||||
|
||||
export const startRecording = async (roomId: string, moderatorCookie = '') => {
|
||||
@ -442,7 +394,7 @@ export const stopRecording = async (recordingId: string, moderatorCookie = '') =
|
||||
.post(`${INTERNAL_CONFIG.INTERNAL_API_BASE_PATH_V1}/recordings/${recordingId}/stop`)
|
||||
.set('Cookie', moderatorCookie)
|
||||
.send();
|
||||
await sleep(2500);
|
||||
await sleep('2.5s');
|
||||
|
||||
return response;
|
||||
};
|
||||
@ -469,7 +421,7 @@ export const stopAllRecordings = async (moderatorCookie: string) => {
|
||||
.send()
|
||||
);
|
||||
await Promise.all(tasks);
|
||||
await sleep(1000); // Wait for 1 second
|
||||
await sleep('1s');
|
||||
};
|
||||
|
||||
export const getAllRecordings = async (query: Record<string, any> = {}) => {
|
||||
@ -483,6 +435,18 @@ export const getAllRecordings = async (query: Record<string, any> = {}) => {
|
||||
.query(query);
|
||||
};
|
||||
|
||||
export const bulkDeleteRecordings = async (recordingIds: any[]): Promise<Response> => {
|
||||
if (!app) {
|
||||
throw new Error('App instance is not defined');
|
||||
}
|
||||
|
||||
const response = await request(app)
|
||||
.delete(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/recordings`)
|
||||
.query({ recordingIds: recordingIds.join(',') })
|
||||
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_API_KEY);
|
||||
return response;
|
||||
};
|
||||
|
||||
export const deleteAllRecordings = async () => {
|
||||
if (!app) {
|
||||
throw new Error('App instance is not defined');
|
||||
@ -507,14 +471,7 @@ export const deleteAllRecordings = async () => {
|
||||
break;
|
||||
}
|
||||
|
||||
console.log(`Deleting ${recordingIds.length} recordings...`);
|
||||
console.log('Recording IDs:', recordingIds);
|
||||
await request(app)
|
||||
.delete(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/recordings`)
|
||||
.query({ recordingIds: recordingIds.join(',')})
|
||||
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_API_KEY);
|
||||
|
||||
await sleep(1000); // Wait for 1 second
|
||||
await bulkDeleteRecordings(recordingIds);
|
||||
} while (nextPageToken);
|
||||
};
|
||||
|
||||
|
||||
@ -1,10 +1,20 @@
|
||||
import { MeetRoomHelper } from '../../src/helpers';
|
||||
import { createRoom, loginUserAsRole, generateParticipantToken, joinFakeParticipant } from './helpers';
|
||||
import {
|
||||
createRoom,
|
||||
loginUserAsRole,
|
||||
generateParticipantToken,
|
||||
joinFakeParticipant,
|
||||
startRecording,
|
||||
stopRecording,
|
||||
sleep
|
||||
} from './helpers';
|
||||
|
||||
import { UserRole } from '../../src/typings/ce';
|
||||
import { MeetRoom, UserRole } from '../../src/typings/ce';
|
||||
import ms, { StringValue } from 'ms';
|
||||
import { expectValidStartRecordingResponse } from './assertion-helpers';
|
||||
|
||||
export interface RoomData {
|
||||
room: any;
|
||||
room: MeetRoom;
|
||||
moderatorCookie: string;
|
||||
moderatorSecret: string;
|
||||
recordingId?: string;
|
||||
@ -13,6 +23,7 @@ export interface RoomData {
|
||||
export interface TestContext {
|
||||
rooms: RoomData[];
|
||||
getRoomByIndex(index: number): RoomData | undefined;
|
||||
getLastRoom(): RoomData | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -28,18 +39,11 @@ export async function setupMultiRoomTestContext(numRooms: number, withParticipan
|
||||
roomIdPrefix: `test-recording-room-${i + 1}`
|
||||
});
|
||||
const { moderatorSecret } = MeetRoomHelper.extractSecretsFromRoom(room);
|
||||
const moderatorCookie = await generateParticipantToken(
|
||||
adminCookie,
|
||||
room.roomId,
|
||||
`Moderator-${i + 1}`,
|
||||
moderatorSecret
|
||||
);
|
||||
|
||||
if (withParticipants) {
|
||||
const participantId = `TEST_P-${i + 1}`;
|
||||
|
||||
await joinFakeParticipant(room.roomId, participantId);
|
||||
}
|
||||
const [moderatorCookie, _] = await Promise.all([
|
||||
generateParticipantToken(adminCookie, room.roomId, `Moderator-${i + 1}`, moderatorSecret),
|
||||
// Join participant (if needed) concurrently with token generation
|
||||
withParticipants ? joinFakeParticipant(room.roomId, `TEST_P-${i + 1}`) : Promise.resolve()
|
||||
]);
|
||||
|
||||
rooms.push({
|
||||
room,
|
||||
@ -56,6 +60,73 @@ export async function setupMultiRoomTestContext(numRooms: number, withParticipan
|
||||
}
|
||||
|
||||
return rooms[index];
|
||||
},
|
||||
|
||||
getLastRoom: () => {
|
||||
if (rooms.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return rooms[rooms.length - 1];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Quickly creates multiple recordings for bulk delete testing.
|
||||
* Allows customizing how many recordings to start and how many to stop after a delay.
|
||||
*
|
||||
* @param numRooms Number of rooms to use.
|
||||
* @param numStarts Number of recordings to start.
|
||||
* @param numStops Number of recordings to stop after the delay.
|
||||
* @param stopDelayMs Delay in milliseconds before stopping recordings.
|
||||
* @returns Test context with created recordings (some stopped, some still running).
|
||||
*/
|
||||
export async function setupMultiRecordingsTestContext(
|
||||
numRooms: number,
|
||||
numStarts: number,
|
||||
numStops: number,
|
||||
stopDelay: StringValue
|
||||
): Promise<TestContext> {
|
||||
// Setup rooms with participants
|
||||
const testContext = await setupMultiRoomTestContext(numRooms, true);
|
||||
|
||||
// Start the specified number of recordings in parallel
|
||||
const startPromises = Array.from({ length: numStarts }).map(async (_, i) => {
|
||||
const roomIndex = i % numRooms;
|
||||
const roomData = testContext.getRoomByIndex(roomIndex);
|
||||
|
||||
if (!roomData) {
|
||||
throw new Error(`Room at index ${roomIndex} not found`);
|
||||
}
|
||||
|
||||
// Send start recording request
|
||||
const response = await startRecording(roomData.room.roomId, roomData.moderatorCookie);
|
||||
expectValidStartRecordingResponse(response, roomData.room.roomId);
|
||||
|
||||
// Store the recordingId in context
|
||||
roomData.recordingId = response.body.recordingId;
|
||||
return roomData;
|
||||
});
|
||||
const startedRooms = await Promise.all(startPromises);
|
||||
|
||||
// Wait for the configured delay before stopping recordings
|
||||
if (ms(stopDelay) > 0) {
|
||||
await sleep(stopDelay);
|
||||
}
|
||||
|
||||
// Stop recordings for the first numStops rooms
|
||||
const stopPromises = startedRooms.slice(0, numStops).map(async (roomData) => {
|
||||
if (roomData.recordingId) {
|
||||
await stopRecording(roomData.recordingId, roomData.moderatorCookie);
|
||||
console.log(`Recording stopped for room ${roomData.room.roomId}`);
|
||||
return roomData.recordingId;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
const stoppedIds = (await Promise.all(stopPromises)).filter((id): id is string => Boolean(id));
|
||||
console.log(`Stopped ${stoppedIds.length} recordings after ${stopDelay}ms:`, stoppedIds);
|
||||
|
||||
return testContext;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user