backend: Enhance deleteRoom functionality and add delete room integration tests
This commit is contained in:
parent
7bcb3be1dd
commit
33a970d1ef
@ -6,6 +6,7 @@ const jestConfig = {
|
||||
...createDefaultEsmPreset({
|
||||
tsconfig: 'tsconfig.json'
|
||||
}),
|
||||
testTimeout: 30000,
|
||||
resolver: 'ts-jest-resolver',
|
||||
testMatch: ['**/?(*.)+(spec|test).[tj]s?(x)'],
|
||||
moduleFileExtensions: ['js', 'ts', 'json', 'node'],
|
||||
|
||||
@ -72,9 +72,11 @@ export const deleteRoom = async (req: Request, res: Response) => {
|
||||
const { deleted } = await roomService.bulkDeleteRooms([roomId], forceDelete);
|
||||
|
||||
if (deleted.length > 0) {
|
||||
// Room was deleted
|
||||
return res.status(204).send();
|
||||
}
|
||||
|
||||
// Room was marked as deleted
|
||||
return res.status(202).json({ message: `Room ${roomId} marked as deleted` });
|
||||
} catch (error) {
|
||||
logger.error(`Error deleting room: ${roomId}`);
|
||||
|
||||
208
backend/tests/integration/api/rooms/delete-room.test.ts
Normal file
208
backend/tests/integration/api/rooms/delete-room.test.ts
Normal file
@ -0,0 +1,208 @@
|
||||
import { describe, it, expect, beforeAll, afterAll, afterEach } from '@jest/globals';
|
||||
import {
|
||||
createRoom,
|
||||
deleteAllRooms,
|
||||
startTestServer,
|
||||
stopTestServer,
|
||||
getRoom,
|
||||
deleteRoom,
|
||||
joinFakeParticipant,
|
||||
sleep,
|
||||
disconnectFakeParticipants
|
||||
} from '../../../utils/helpers.js';
|
||||
import ms from 'ms';
|
||||
|
||||
describe('OpenVidu Meet Room API Tests', () => {
|
||||
beforeAll(async () => {
|
||||
await startTestServer();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await stopTestServer();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Remove all rooms created
|
||||
disconnectFakeParticipants();
|
||||
await deleteAllRooms();
|
||||
});
|
||||
|
||||
describe('Delete Room Tests', () => {
|
||||
it('should return 204 when room does not exist (idempotent deletion)', async () => {
|
||||
const response = await deleteRoom('non-existent-room-id');
|
||||
|
||||
expect(response.status).toBe(204);
|
||||
});
|
||||
|
||||
it('should default to force=false when force parameter is invalid', async () => {
|
||||
// Create a room first
|
||||
const { roomId } = await createRoom({
|
||||
roomIdPrefix: 'test-room'
|
||||
});
|
||||
const response = await deleteRoom(roomId, { force: 'not-a-boolean' });
|
||||
|
||||
expect(response.status).toBe(204);
|
||||
// Verify it's deleted
|
||||
const getResponse = await getRoom(roomId);
|
||||
expect(getResponse.status).toBe(404);
|
||||
});
|
||||
|
||||
it('should mark room for deletion when participants exist and force parameter is invalid', async () => {
|
||||
// Create a room first
|
||||
const { roomId } = await createRoom({
|
||||
roomIdPrefix: 'test-room'
|
||||
});
|
||||
|
||||
joinFakeParticipant(roomId, 'test-participant');
|
||||
|
||||
await sleep(500);
|
||||
|
||||
// The force parameter is not a boolean so it should be defined as false
|
||||
// and the room should be marked for deletion
|
||||
const response = await deleteRoom(roomId, { force: 'not-a-boolean' });
|
||||
|
||||
// Check operation accepted
|
||||
expect(response.status).toBe(202);
|
||||
|
||||
// The room should be marked for deletion
|
||||
const roomResponse = await getRoom(roomId);
|
||||
expect(roomResponse.body).toBeDefined();
|
||||
expect(roomResponse.body.roomId).toBe(roomId);
|
||||
expect(roomResponse.body.markedForDeletion).toBeDefined();
|
||||
expect(roomResponse.body.markedForDeletion).toBe(true);
|
||||
});
|
||||
|
||||
it('should delete an empty room completely (204)', async () => {
|
||||
const { roomId } = await createRoom({ roomIdPrefix: 'test-room' });
|
||||
|
||||
const response = await deleteRoom(roomId);
|
||||
|
||||
expect(response.status).toBe(204);
|
||||
|
||||
// Try to retrieve the room again
|
||||
const responseAfterDelete = await getRoom(roomId);
|
||||
expect(responseAfterDelete.status).toBe(404);
|
||||
});
|
||||
|
||||
it('should sanitize roomId with spaces and special characters before deletion', async () => {
|
||||
// Create a room first
|
||||
const createdRoom = await createRoom({
|
||||
roomIdPrefix: 'test-mixed'
|
||||
});
|
||||
|
||||
// Add some spaces and special chars to the valid roomId
|
||||
const modifiedId = ` ${createdRoom.roomId}!@# `;
|
||||
const response = await deleteRoom(modifiedId);
|
||||
|
||||
// The validation should sanitize the ID and successfully delete
|
||||
expect(response.status).toBe(204);
|
||||
|
||||
// Verify it's deleted
|
||||
const getResponse = await getRoom(createdRoom.roomId);
|
||||
expect(getResponse.status).toBe(404);
|
||||
});
|
||||
|
||||
it('should handle explicit force=true for room with no participants', async () => {
|
||||
const createdRoom = await createRoom({
|
||||
roomIdPrefix: 'test-room'
|
||||
});
|
||||
|
||||
const response = await deleteRoom(createdRoom.roomId, { force: true });
|
||||
|
||||
expect(response.status).toBe(204);
|
||||
|
||||
// Try to retrieve the room again
|
||||
const responseAfterDelete = await getRoom(createdRoom.roomId);
|
||||
expect(responseAfterDelete.status).toBe(404);
|
||||
});
|
||||
|
||||
it('should mark room for deletion (202) when participants exist and force=false', async () => {
|
||||
const { roomId } = await createRoom({
|
||||
roomIdPrefix: 'test-room',
|
||||
autoDeletionDate: Date.now() + ms('5h')
|
||||
});
|
||||
|
||||
joinFakeParticipant(roomId, 'test-participant');
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const response = await deleteRoom(roomId, { force: false });
|
||||
|
||||
expect(response.status).toBe(202);
|
||||
|
||||
const roomResponse = await getRoom(roomId);
|
||||
expect(roomResponse.body).toBeDefined();
|
||||
expect(roomResponse.body.roomId).toBe(roomId);
|
||||
expect(roomResponse.body.markedForDeletion).toBeDefined();
|
||||
expect(roomResponse.body.markedForDeletion).toBe(true);
|
||||
|
||||
disconnectFakeParticipants();
|
||||
|
||||
await sleep(2000);
|
||||
const responseAfterDelete = await getRoom(roomId);
|
||||
expect(responseAfterDelete.status).toBe(404);
|
||||
});
|
||||
|
||||
it('should force delete (204) room with active participants when force=true', async () => {
|
||||
const { roomId } = await createRoom({
|
||||
roomIdPrefix: 'test-room'
|
||||
});
|
||||
|
||||
joinFakeParticipant(roomId, 'test-participant');
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const response = await deleteRoom(roomId, { force: true });
|
||||
|
||||
expect(response.status).toBe(204);
|
||||
|
||||
// Try to retrieve the room again
|
||||
const responseAfterDelete = await getRoom(roomId);
|
||||
expect(responseAfterDelete.status).toBe(404);
|
||||
});
|
||||
|
||||
it('should successfully delete a room already marked for deletion', async () => {
|
||||
const { roomId } = await createRoom({ roomIdPrefix: 'test-marked' });
|
||||
|
||||
// First mark it for deletion
|
||||
joinFakeParticipant(roomId, 'test-participant');
|
||||
await sleep(500);
|
||||
await deleteRoom(roomId, { force: false });
|
||||
|
||||
// Then try to delete it again
|
||||
const response = await deleteRoom(roomId, { force: true });
|
||||
expect(response.status).toBe(204);
|
||||
});
|
||||
|
||||
it('should handle repeated deletion of the same room gracefully', async () => {
|
||||
const { roomId } = await createRoom({ roomIdPrefix: 'test-idempotent' });
|
||||
|
||||
// Delete first time
|
||||
const response1 = await deleteRoom(roomId);
|
||||
expect(response1.status).toBe(204);
|
||||
|
||||
// Delete second time - should still return 204 (no error)
|
||||
const response2 = await deleteRoom(roomId);
|
||||
expect(response2.status).toBe(204);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Delete Room Validation failures', () => {
|
||||
it('should fail when roomId becomes empty after sanitization', async () => {
|
||||
const response = await deleteRoom('!!-*!@#$%^&*()_+{}|:"<>?');
|
||||
|
||||
expect(response.status).toBe(422);
|
||||
// Expect an error message indicating the resulting roomId is empty.
|
||||
expect(response.body.error).toContain('Unprocessable Entity');
|
||||
expect(JSON.stringify(response.body.details)).toContain('roomId cannot be empty after sanitization');
|
||||
});
|
||||
|
||||
it('should fail when force parameter is a number instead of boolean', async () => {
|
||||
const response = await deleteRoom('testRoom', { force: { value: 123 } });
|
||||
|
||||
expect(response.status).toBe(422);
|
||||
expect(response.body.error).toContain('Unprocessable Entity');
|
||||
expect(JSON.stringify(response.body.details)).toContain('Expected boolean, received object');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -9,11 +9,15 @@ import {
|
||||
MEET_USER,
|
||||
MEET_SECRET,
|
||||
MEET_ADMIN_USER,
|
||||
MEET_ADMIN_SECRET
|
||||
MEET_ADMIN_SECRET,
|
||||
LIVEKIT_API_SECRET,
|
||||
LIVEKIT_API_KEY,
|
||||
MEET_NAME_ID
|
||||
} from '../../src/environment.js';
|
||||
import { AuthMode, AuthType, MeetRoom, UserRole, MeetRoomOptions } from '../../src/typings/ce/index.js';
|
||||
import { expect } from '@jest/globals';
|
||||
import INTERNAL_CONFIG from '../../src/config/internal-config.js';
|
||||
import { ChildProcess, execSync, spawn } from 'child_process';
|
||||
|
||||
const CREDENTIALS = {
|
||||
user: {
|
||||
@ -29,6 +33,8 @@ const CREDENTIALS = {
|
||||
let app: Express;
|
||||
let server: Server;
|
||||
|
||||
const fakeParticipantsProcesses = new Map<string, ChildProcess>();
|
||||
|
||||
/**
|
||||
* Starts the test server
|
||||
*/
|
||||
@ -214,6 +220,17 @@ export const getRoom = async (roomId: string, fields?: string) => {
|
||||
.query({ fields });
|
||||
};
|
||||
|
||||
export const deleteRoom = async (roomId: string, query: Record<string, any> = {}) => {
|
||||
if (!app) {
|
||||
throw new Error('App instance is not defined');
|
||||
}
|
||||
|
||||
return await request(app)
|
||||
.delete(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms/${roomId}`)
|
||||
.set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_API_KEY)
|
||||
.query(query);
|
||||
};
|
||||
|
||||
export const assertEmptyRooms = async () => {
|
||||
if (!app) {
|
||||
throw new Error('App instance is not defined');
|
||||
@ -287,6 +304,74 @@ export const generateParticipantToken = async (
|
||||
return participantTokenCookie;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a fake participant to a LiveKit room for testing purposes.
|
||||
*
|
||||
* This workflow involves three key steps:
|
||||
* 1. Create the LiveKit room manually
|
||||
* 2. Set room metadata to mark it as managed by OpenVidu Meet
|
||||
* 3. Connect a fake participant to the room
|
||||
*
|
||||
* @param roomId The ID of the room to join
|
||||
* @param participantName The name for the fake participant
|
||||
*/
|
||||
export const joinFakeParticipant = (roomId: string, participantName: string) => {
|
||||
// Step 1: Manually create the LiveKit room
|
||||
// In normal operation, the room is created when a real participant requests a token,
|
||||
// but for testing we need to create it ourselves since we're bypassing the token flow.
|
||||
// We set a short departureTimeout (1s) to ensure the room is quickly cleaned up
|
||||
// when our tests disconnect participants, preventing lingering test resources.
|
||||
const createRoomCommand = `lk room create --api-key ${LIVEKIT_API_KEY} --api-secret ${LIVEKIT_API_SECRET} --departure-timeout 1 ${roomId}`;
|
||||
runCommandSync(createRoomCommand);
|
||||
|
||||
// Step 2: Set required room metadata
|
||||
// The room must have the createdBy field set to MEET_NAME_ID so that:
|
||||
// 1. OpenVidu Meet recognizes it as a managed room
|
||||
// 2. The room can be properly deleted through our API later
|
||||
// 3. Other OpenVidu Meet features know this room belongs to our system
|
||||
const metadata = JSON.stringify({ createdBy: MEET_NAME_ID });
|
||||
const updateMetadataCommand = `lk room update --metadata '${metadata}' --api-key ${LIVEKIT_API_KEY} --api-secret ${LIVEKIT_API_SECRET} ${roomId}`;
|
||||
runCommandSync(updateMetadataCommand);
|
||||
|
||||
// Step 3: Join a fake participant with demo audio/video
|
||||
const process = spawn('lk', [
|
||||
'room',
|
||||
'join',
|
||||
'--identity',
|
||||
participantName,
|
||||
'--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(participantName, process);
|
||||
};
|
||||
|
||||
export const disconnectFakeParticipants = () => {
|
||||
fakeParticipantsProcesses.forEach((process, participantName) => {
|
||||
process.kill();
|
||||
console.log(`Stopped process for participant ${participantName}`);
|
||||
});
|
||||
|
||||
fakeParticipantsProcesses.clear();
|
||||
};
|
||||
|
||||
export const sleep = (ms: number) => {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
};
|
||||
|
||||
// PRIVATE METHODS
|
||||
|
||||
const runCommandSync = (command: string): string => {
|
||||
try {
|
||||
const stdout = execSync(command, { encoding: 'utf-8' });
|
||||
return stdout;
|
||||
} catch (error) {
|
||||
console.error(`Error running command: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user