import { afterEach, beforeAll, describe, expect, it } from '@jest/globals'; import ms from 'ms'; import { expectValidRoom } from '../../../helpers/assertion-helpers.js'; import { createRoom, deleteAllRooms, deleteRoom, disconnectFakeParticipants, getRoom, joinFakeParticipant, sleep, startTestServer } from '../../../helpers/request-helpers.js'; import { RoomService } from '../../../../src/services/room.service.js'; import { container } from '../../../../src/config/dependency-injector.config.js'; import { setInternalConfig } from '../../../../src/config/internal-config.js'; describe('Room API Tests', () => { beforeAll(() => { startTestServer(); }); afterEach(async () => { // Remove all rooms created await 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({ roomName: '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({ roomName: 'test-room' }); await joinFakeParticipant(roomId, 'test-participant'); // 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({ roomName: '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({ roomName: '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({ roomName: '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 autoDeletionDate = Date.now() + ms('5h'); const { roomId } = await createRoom({ roomName: 'test-room', autoDeletionDate }); await joinFakeParticipant(roomId, 'test-participant'); const response = await deleteRoom(roomId, { force: false }); expect(response.status).toBe(202); const roomResponse = await getRoom(roomId); expectValidRoom(roomResponse.body, 'test-room', autoDeletionDate, undefined, true); }); it('should delete a room marked for deletion when the webhook room_finished is received', async () => { const autoDeletionDate = Date.now() + ms('5h'); const { roomId } = await createRoom({ roomName: 'test-room', autoDeletionDate }); const roomService = container.get(RoomService); // Set MEETING_DEPARTURE_TIMEOUT to 1s to force the room to be closed immediately setInternalConfig({ MEETING_DEPARTURE_TIMEOUT: '1s' }); // Create livekit room with custom departure timeout // This is needed to trigger the room_finished event await roomService.createLivekitRoom(roomId); // Join a participant to the room await joinFakeParticipant(roomId, 'test-participant'); const response = await deleteRoom(roomId, { force: false }); expect(response.status).toBe(202); const roomResponse = await getRoom(roomId); expectValidRoom(roomResponse.body, 'test-room', autoDeletionDate, undefined, true); await disconnectFakeParticipants(); // Wait for room deletion await sleep('2s'); 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({ roomName: 'test-room' }); await joinFakeParticipant(roomId, 'test-participant'); const response = await deleteRoom(roomId, { force: true }); expect(response.status).toBe(204); // Try to retrieve the room again await sleep('1s'); // Wait a bit for the meeting to be closed and the room deleted 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({ roomName: 'test-marked' }); // First mark it for deletion await joinFakeParticipant(roomId, 'test-participant'); 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({ roomName: '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'); }); }); });