From 451a3b74e89ec2a6423310faab0fc2774e2903f0 Mon Sep 17 00:00:00 2001 From: Carlos Santos <4a.santos@gmail.com> Date: Mon, 14 Apr 2025 17:58:50 +0200 Subject: [PATCH] backend: Add tests for bulk room deletion --- .../api/rooms/bulk-delete-rooms.test.ts | 307 ++++++++++++++++++ backend/tests/utils/helpers.ts | 11 + 2 files changed, 318 insertions(+) create mode 100644 backend/tests/integration/api/rooms/bulk-delete-rooms.test.ts diff --git a/backend/tests/integration/api/rooms/bulk-delete-rooms.test.ts b/backend/tests/integration/api/rooms/bulk-delete-rooms.test.ts new file mode 100644 index 0000000..b7eb7e4 --- /dev/null +++ b/backend/tests/integration/api/rooms/bulk-delete-rooms.test.ts @@ -0,0 +1,307 @@ +import { describe, it, expect, beforeAll, afterAll, afterEach } from '@jest/globals'; +import { + createRoom, + deleteAllRooms, + startTestServer, + stopTestServer, + getRoom, + joinFakeParticipant, + sleep, + disconnectFakeParticipants, + bulkDeleteRooms +} from '../../../utils/helpers.js'; + +describe('OpenVidu Meet Room API Tests', () => { + beforeAll(async () => { + await startTestServer(); + }); + + afterAll(async () => { + await stopTestServer(); + }); + + afterEach(async () => { + // Remove all rooms created + disconnectFakeParticipants(); + await deleteAllRooms(); + }); + + describe('Bulk Delete Room Tests', () => { + it('should return 204 when room does not exist (idempotent deletion)', async () => { + // The roomId will be transformed to string + const response = await bulkDeleteRooms([{ invalid: 'format' }], true); + + expect(response.status).toBe(204); + expect(response.body).toStrictEqual({}); + }); + + it('should delete room (204) with invalid force parameter when no participants exist', async () => { + const { roomId } = await createRoom({ roomIdPrefix: 'test-invalid-force' }); + + const response = await bulkDeleteRooms([roomId]); + + //The bulk operation for only one room should be the same as deleting the room + expect(response.status).toBe(204); + expect(response.body).toStrictEqual({}); + }); + + it('should mark room for deletion (202) with invalid force parameter when participants exist', async () => { + const { roomId } = await createRoom({ roomIdPrefix: 'test-invalid-force' }); + + joinFakeParticipant(roomId, 'test-participant-1'); + + await sleep(1000); + const response = await bulkDeleteRooms([roomId]); + + //The bulk operation for only one room should be the same as deleting the room + expect(response.status).toBe(202); + expect(response.body.message).toContain('marked as deleted'); + }); + + it('should delete room (204) with force=true parameter when no participants exist', async () => { + const { roomId } = await createRoom({ roomIdPrefix: 'test-force' }); + + const response = await bulkDeleteRooms([roomId], true); + + //The bulk operation for only one room should be the same as deleting the room + expect(response.status).toBe(204); + expect(response.body).toStrictEqual({}); + }); + + it('should delete room (204) with force=true parameter when participants exist', async () => { + const { roomId } = await createRoom({ roomIdPrefix: 'test-force' }); + + joinFakeParticipant(roomId, 'test-participant-1'); + + await sleep(1000); + const response = await bulkDeleteRooms([roomId], true); + + //The bulk operation for only one room should be the same as deleting the room + expect(response.status).toBe(204); + expect(response.body).toStrictEqual({}); + }); + + it('should successfully delete the room requesting the same roomId multiple times', async () => { + const { roomId } = await createRoom({ roomIdPrefix: 'test-duplicate' }); + + const response = await bulkDeleteRooms([roomId, roomId, roomId], true); + + expect(response.status).toBe(204); + expect(response.body).toStrictEqual({}); + }); + + it('should successfully delete valid roomIds while ignoring invalid ones', async () => { + const { roomId } = await createRoom({ roomIdPrefix: 'test-invalid-force' }); + + const response = await bulkDeleteRooms([roomId, '!!@##$']); + + expect(response.status).toBe(204); + expect(response.body).toStrictEqual({}); + }); + + it('should successfully delete multiple rooms with valid roomIds', async () => { + // Create test rooms + const [room1, room2] = await Promise.all([ + createRoom({ roomIdPrefix: 'test-bulk-1' }), + createRoom({ roomIdPrefix: 'test-bulk-2' }) + ]); + + // Delete both rooms + const response = await bulkDeleteRooms([room1.roomId, room2.roomId]); + + expect(response.status).toBe(204); + expect(response.body).toStrictEqual({}); + + // Verify that the rooms are deleted + const getRoom1 = await getRoom(room1.roomId); + const getRoom2 = await getRoom(room2.roomId); + expect(getRoom1.status).toBe(404); + expect(getRoom2.status).toBe(404); + expect(getRoom1.body.message).toContain(`'${room1.roomId}' does not exist`); + expect(getRoom2.body.message).toContain(`'${room2.roomId}' does not exist`); + }); + + it('should successfully marked for deletion multiple rooms with valid roomIds', async () => { + // Create test rooms + const [room1, room2] = await Promise.all([ + createRoom({ roomIdPrefix: 'test-bulk-1' }), + createRoom({ roomIdPrefix: 'test-bulk-2' }) + ]); + + joinFakeParticipant(room1.roomId, 'test-participant-1'); + joinFakeParticipant(room2.roomId, 'test-participant-2'); + await sleep(1000); + + // Delete both rooms + const response = await bulkDeleteRooms([room1.roomId, room2.roomId]); + + expect(response.status).toBe(202); + + expect(response.body.message).toContain(`Rooms ${room1.roomId}, ${room2.roomId} marked for deletion`); + expect(response.body.markedForDeletion).toBeDefined(); + expect(response.body.markedForDeletion).toHaveLength(2); + expect(response.body.markedForDeletion).toStrictEqual([room1.roomId, room2.roomId]); + expect(response.body.deleted).toBeUndefined(); + + // Verify that the rooms are marked for deletion + const getRoom1 = await getRoom(room1.roomId); + const getRoom2 = await getRoom(room2.roomId); + expect(getRoom1.status).toBe(200); + expect(getRoom2.status).toBe(200); + expect(getRoom1.body.roomId).toBe(room1.roomId); + expect(getRoom2.body.roomId).toBe(room2.roomId); + expect(getRoom1.body.markedForDeletion).toBe(true); + expect(getRoom2.body.markedForDeletion).toBe(true); + }); + + it('should sanitize roomIds before deleting', async () => { + // Create a test room + const { roomId } = await createRoom({ roomIdPrefix: 'test-sanitize' }); + + const response = await bulkDeleteRooms([roomId + '!!@##$']); + expect(response.status).toBe(204); + expect(response.body).toStrictEqual({}); + const deletedRoom = await getRoom(roomId); + expect(deletedRoom.status).toBe(404); + expect(deletedRoom.body.message).toContain(`'${roomId}' does not exist`); + }); + + it('should delete rooms when force=true and participants exist', async () => { + // Create a test room + const [room1, room2] = await Promise.all([ + createRoom({ roomIdPrefix: 'test-bulk-1' }), + createRoom({ roomIdPrefix: 'test-bulk-2' }) + ]); + + // Join a participant to the room + joinFakeParticipant(room1.roomId, 'test-participant-1'); + joinFakeParticipant(room2.roomId, 'test-participant-2'); + + await sleep(1000); + + // Attempt to delete the room with force=false + const response = await bulkDeleteRooms([room1.roomId, room2.roomId], true); + + expect(response.status).toBe(204); + expect(response.body).toStrictEqual({}); + + // Verify that the room is deleted + const deletedRoom1 = await getRoom(room1.roomId); + const deletedRoom2 = await getRoom(room2.roomId); + expect(deletedRoom1.status).toBe(404); + expect(deletedRoom1.body.message).toContain(`'${room1.roomId}' does not exist`); + expect(deletedRoom2.status).toBe(404); + expect(deletedRoom2.body.message).toContain(`'${room2.roomId}' does not exist`); + }); + + it('should return mixed results (200) when some rooms are deleted and others marked for deletion', async () => { + // Create rooms + const room1 = await createRoom({ roomIdPrefix: 'empty-room' }); + const room2 = await createRoom({ roomIdPrefix: 'occupied-room' }); + + // Add participant to only one room + joinFakeParticipant(room2.roomId, 'test-participant'); + await sleep(1000); + + // Delete both rooms (without force) + const response = await bulkDeleteRooms([room1.roomId, room2.roomId], false); + + // Should return 200 with mixed results + expect(response.status).toBe(200); + expect(response.body.deleted).toHaveLength(1); + expect(response.body.deleted).toContain(room1.roomId); + expect(response.body.markedForDeletion).toHaveLength(1); + expect(response.body.markedForDeletion).toContain(room2.roomId); + + // Verify deletion state + const getRoom1 = await getRoom(room1.roomId); + const getRoom2 = await getRoom(room2.roomId); + expect(getRoom1.status).toBe(404); + expect(getRoom2.status).toBe(200); + }); + + it('should handle a large number of room IDs', async () => { + // Create 20+ rooms and test deletion + const rooms = await Promise.all( + Array.from({ length: 20 }, (_, i) => createRoom({ roomIdPrefix: `bulk-${i}` })) + ); + + const response = await bulkDeleteRooms(rooms.map((r) => r.roomId)); + expect(response.status).toBe(204); + + // Verify all rooms are deleted + for (const room of rooms) { + const getResponse = await getRoom(room.roomId); + expect(getResponse.status).toBe(404); + } + }); + + it('should handle a large number of room IDs with mixed valid and invalid IDs', async () => { + // Create 20+ rooms and test deletion + const rooms = await Promise.all( + Array.from({ length: 20 }, (_, i) => createRoom({ roomIdPrefix: `bulk-${i}` })) + ); + + joinFakeParticipant(rooms[0].roomId, 'test-participant-1'); + await sleep(1000); + + const response = await bulkDeleteRooms([ + ...rooms.map((r) => r.roomId), + '!!@##$', + ',,,,', + 'room-1', + 'room-2' + ]); + + expect(response.status).toBe(200); + + // Verify all valid rooms are deleted + for (const room of rooms) { + if (room.roomId === rooms[0].roomId) { + continue; // Skip the room with a participant + } + + const getResponse = await getRoom(room.roomId); + expect(getResponse.status).toBe(404); + } + + // Verify the room with a participant is marked for deletion + const getResponse = await getRoom(rooms[0].roomId); + expect(getResponse.status).toBe(200); + expect(getResponse.body.markedForDeletion).toBe(true); + expect(getResponse.body.roomId).toBe(rooms[0].roomId); + }); + }); + + describe('Bulk delete Room Validation failures', () => { + it('should handle empty roomIds array (no rooms deleted)', async () => { + const response = await bulkDeleteRooms([]); + + expect(response.status).toBe(422); + expect(response.body.error).toContain('Unprocessable Entity'); + expect(JSON.stringify(response.body.details)).toContain( + 'At least one valid roomId is required after sanitization' + ); + }); + it('should fail when roomIds contains an ID that becomes empty after sanitization', async () => { + const response = await bulkDeleteRooms([',,,,']); + + expect(response.status).toBe(422); + + expect(response.body.error).toContain('Unprocessable Entity'); + expect(JSON.stringify(response.body.details)).toContain( + 'At least one valid roomId is required after sanitization' + ); + }); + + it('should validate roomIds and return 422 when all are invalid', async () => { + const response = await bulkDeleteRooms(['!!@##$', '!!@##$', ',', '.,-------}{ยก$#<+']); + + expect(response.status).toBe(422); + expect(response.body.error).toContain('Unprocessable Entity'); + expect(JSON.stringify(response.body.details)).toContain( + 'At least one valid roomId is required after sanitization' + ); + }); + }); +}); diff --git a/backend/tests/utils/helpers.ts b/backend/tests/utils/helpers.ts index 6dbb4c0..6d876c2 100644 --- a/backend/tests/utils/helpers.ts +++ b/backend/tests/utils/helpers.ts @@ -231,6 +231,17 @@ export const deleteRoom = async (roomId: string, query: Record = {} .query(query); }; +export const bulkDeleteRooms = async (roomIds: any[], force?: any) => { + if (!app) { + throw new Error('App instance is not defined'); + } + + return await request(app) + .delete(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms`) + .set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_API_KEY) + .query({ roomIds: roomIds.join(','), force }); +}; + export const assertEmptyRooms = async () => { if (!app) { throw new Error('App instance is not defined');