From 2bea178e768a1dc292989aff497062184875a14c Mon Sep 17 00:00:00 2001 From: juancarmore Date: Fri, 5 Sep 2025 01:12:17 +0200 Subject: [PATCH] tests: update tests to reflect code changes and add test cases for update room status endpoint --- backend/tests/helpers/assertion-helpers.ts | 32 +- backend/tests/helpers/request-helpers.ts | 46 +- .../api/meetings/end-meeting.test.ts | 4 +- ...ipant.test.ts => kick-participant.test.ts} | 16 +- .../api/meetings/update-participant.test.ts | 4 +- .../api/participants/generate-token.test.ts | 15 +- .../recordings/bulk-delete-recording.test.ts | 47 +- .../api/recordings/delete-recording.test.ts | 13 +- .../api/recordings/race-conditions.test.ts | 5 +- .../api/rooms/bulk-delete-rooms.test.ts | 413 ++++++++-------- .../integration/api/rooms/create-room.test.ts | 82 +++- .../integration/api/rooms/delete-room.test.ts | 455 ++++++++++++------ .../api/rooms/garbage-collector.test.ts | 121 +++-- .../integration/api/rooms/get-rooms.test.ts | 4 +- ...est.ts => update-room-preferences.test.ts} | 4 +- .../api/rooms/update-room-status.test.ts | 103 ++++ .../integration/api/security/auth.test.ts | 4 +- .../api/security/meeting-security.test.ts | 2 +- .../api/security/recording-security.test.ts | 10 +- .../api/security/room-security.test.ts | 40 +- .../api/users/change-password.test.ts | 1 - .../integration/webhooks/webhook.test.ts | 2 +- 22 files changed, 921 insertions(+), 502 deletions(-) rename backend/tests/integration/api/meetings/{delete-participant.test.ts => kick-participant.test.ts} (79%) rename backend/tests/integration/api/rooms/{update-room.test.ts => update-room-preferences.test.ts} (98%) create mode 100644 backend/tests/integration/api/rooms/update-room-status.test.ts diff --git a/backend/tests/helpers/assertion-helpers.ts b/backend/tests/helpers/assertion-helpers.ts index 5d786e7..3db2147 100644 --- a/backend/tests/helpers/assertion-helpers.ts +++ b/backend/tests/helpers/assertion-helpers.ts @@ -3,11 +3,16 @@ import { container } from '../../src/config/dependency-injector.config'; import INTERNAL_CONFIG from '../../src/config/internal-config'; import { TokenService } from '../../src/services'; import { + MeetingEndAction, MeetRecordingAccess, MeetRecordingInfo, MeetRecordingStatus, MeetRoom, + MeetRoomAutoDeletionPolicy, + MeetRoomDeletionPolicyWithMeeting, + MeetRoomDeletionPolicyWithRecordings, MeetRoomPreferences, + MeetRoomStatus, ParticipantPermissions, ParticipantRole } from '../../src/typings/ce'; @@ -91,7 +96,7 @@ export const expectSuccessRoomResponse = ( preferences?: MeetRoomPreferences ) => { expect(response.status).toBe(200); - expectValidRoom(response.body, roomName, autoDeletionDate, preferences); + expectValidRoom(response.body, roomName, preferences, autoDeletionDate); }; export const expectSuccessRoomPreferencesResponse = (response: any, preferences: MeetRoomPreferences) => { @@ -103,9 +108,11 @@ export const expectSuccessRoomPreferencesResponse = (response: any, preferences: export const expectValidRoom = ( room: MeetRoom, name: string, - autoDeletionDate?: number, preferences?: MeetRoomPreferences, - markedForDeletion?: boolean + autoDeletionDate?: number, + autoDeletionPolicy?: MeetRoomAutoDeletionPolicy, + status?: MeetRoomStatus, + meetingEndAction?: MeetingEndAction ) => { expect(room).toBeDefined(); @@ -123,6 +130,16 @@ export const expectValidRoom = ( expect(room.autoDeletionDate).toBeUndefined(); } + if (autoDeletionPolicy !== undefined) { + expect(room.autoDeletionPolicy).toBeDefined(); + expect(room.autoDeletionPolicy).toEqual(autoDeletionPolicy); + } else { + expect(room.autoDeletionPolicy).toEqual({ + withMeeting: MeetRoomDeletionPolicyWithMeeting.WHEN_MEETING_ENDS, + withRecordings: MeetRoomDeletionPolicyWithRecordings.CLOSE + }); + } + expect(room.preferences).toBeDefined(); if (preferences !== undefined) { @@ -143,11 +160,10 @@ export const expectValidRoom = ( expect(room.moderatorUrl).toContain(room.roomId); expect(room.speakerUrl).toContain(room.roomId); - if (markedForDeletion !== undefined) { - expect(room.autoDeletionDate).toBeDefined(); - - expect(room.markedForDeletion).toBe(markedForDeletion ?? false); - } + expect(room.status).toBeDefined(); + expect(room.status).toEqual(status || MeetRoomStatus.OPEN); + expect(room.meetingEndAction).toBeDefined(); + expect(room.meetingEndAction).toEqual(meetingEndAction || MeetingEndAction.NONE); }; export const expectValidRecording = ( diff --git a/backend/tests/helpers/request-helpers.ts b/backend/tests/helpers/request-helpers.ts index f38c409..02cf347 100644 --- a/backend/tests/helpers/request-helpers.ts +++ b/backend/tests/helpers/request-helpers.ts @@ -22,6 +22,8 @@ import { MeetRecordingInfo, MeetRecordingStatus, MeetRoom, + MeetRoomDeletionPolicyWithMeeting, + MeetRoomDeletionPolicyWithRecordings, MeetRoomOptions, ParticipantRole, WebhookPreferences @@ -244,20 +246,18 @@ export const getRoom = async (roomId: string, fields?: string, cookie?: string, export const getRoomPreferences = async (roomId: string) => { checkAppIsRunning(); - const adminCookie = await loginUser(); return await request(app) .get(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms/${roomId}/preferences`) - .set('Cookie', adminCookie) + .set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_INITIAL_API_KEY) .send(); }; export const updateRoomPreferences = async (roomId: string, preferences: any) => { checkAppIsRunning(); - const adminCookie = await loginUser(); return await request(app) .put(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms/${roomId}/preferences`) - .set('Cookie', adminCookie) + .set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_INITIAL_API_KEY) .send({ preferences }); }; @@ -277,6 +277,15 @@ export const updateRecordingAccessPreferencesInRoom = async (roomId: string, rec 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 = {}) => { checkAppIsRunning(); @@ -288,13 +297,17 @@ export const deleteRoom = async (roomId: string, query: Record = {} return result; }; -export const bulkDeleteRooms = async (roomIds: any[], force?: any) => { +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(','), force }); + .query({ roomIds: roomIds.join(','), withMeeting, withRecordings }); await sleep('1s'); return result; }; @@ -305,11 +318,8 @@ export const deleteAllRooms = async () => { let nextPageToken: string | undefined; do { - const response: any = await request(app) - .get(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms`) - .query({ fields: 'roomId', maxItems: 100, nextPageToken }) - .set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_INITIAL_API_KEY) - .expect(200); + 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); @@ -318,13 +328,12 @@ export const deleteAllRooms = async () => { break; } - await request(app) - .delete(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms`) - .query({ roomIds: roomIds.join(','), force: true }) - .set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_INITIAL_API_KEY); + await bulkDeleteRooms( + roomIds, + MeetRoomDeletionPolicyWithMeeting.FORCE, + MeetRoomDeletionPolicyWithRecordings.FORCE + ); } while (nextPageToken); - - await sleep('1s'); }; /** @@ -339,6 +348,7 @@ export const runRoomGarbageCollector = async () => { const roomService = container.get(RoomService); await (roomService as any)['deleteExpiredRooms'](); + await sleep('1s'); }; export const runReleaseActiveRecordingLock = async (roomId: string) => { @@ -547,7 +557,7 @@ export const updateParticipant = async ( return response; }; -export const deleteParticipant = async (roomId: string, participantIdentity: string, moderatorCookie: string) => { +export const kickParticipant = async (roomId: string, participantIdentity: string, moderatorCookie: string) => { checkAppIsRunning(); const response = await request(app) diff --git a/backend/tests/integration/api/meetings/end-meeting.test.ts b/backend/tests/integration/api/meetings/end-meeting.test.ts index f1206cc..ce8c52f 100644 --- a/backend/tests/integration/api/meetings/end-meeting.test.ts +++ b/backend/tests/integration/api/meetings/end-meeting.test.ts @@ -71,8 +71,8 @@ describe('Meetings API Tests', () => { it('should fail with 404 if the room does not exist', async () => { // Delete the room to ensure it does not exist - let response = await deleteRoom(roomData.room.roomId, { force: true }); - expect(response.status).toBe(204); + let response = await deleteRoom(roomData.room.roomId, { withMeeting: 'force' }); + expect(response.status).toBe(200); response = await endMeeting(roomData.room.roomId, roomData.moderatorCookie); expect(response.status).toBe(404); diff --git a/backend/tests/integration/api/meetings/delete-participant.test.ts b/backend/tests/integration/api/meetings/kick-participant.test.ts similarity index 79% rename from backend/tests/integration/api/meetings/delete-participant.test.ts rename to backend/tests/integration/api/meetings/kick-participant.test.ts index 9b2b389..c0cedd5 100644 --- a/backend/tests/integration/api/meetings/delete-participant.test.ts +++ b/backend/tests/integration/api/meetings/kick-participant.test.ts @@ -4,9 +4,9 @@ import { OpenViduMeetError } from '../../../../src/models/error.model.js'; import { LiveKitService } from '../../../../src/services/index.js'; import { deleteAllRooms, - deleteParticipant, deleteRoom, disconnectFakeParticipants, + kickParticipant, startTestServer } from '../../../helpers/request-helpers.js'; import { RoomData, setupSingleRoom } from '../../../helpers/test-scenarios.js'; @@ -27,19 +27,19 @@ describe('Meetings API Tests', () => { await deleteAllRooms(); }); - describe('Delete Participant Tests', () => { + describe('Kick Participant Tests', () => { beforeEach(async () => { roomData = await setupSingleRoom(true); }); - it('should remove participant from LiveKit room', async () => { + it('should kick participant from LiveKit room', async () => { // Check if participant exists before deletion const participant = await livekitService.getParticipant(roomData.room.roomId, participantIdentity); expect(participant).toBeDefined(); expect(participant.identity).toBe(participantIdentity); // Delete the participant - const response = await deleteParticipant(roomData.room.roomId, participantIdentity, roomData.moderatorCookie); + const response = await kickParticipant(roomData.room.roomId, participantIdentity, roomData.moderatorCookie); expect(response.status).toBe(200); // Check if the participant has been removed from LiveKit @@ -51,7 +51,7 @@ describe('Meetings API Tests', () => { }); it('should fail with 404 if participant does not exist', async () => { - const response = await deleteParticipant( + const response = await kickParticipant( roomData.room.roomId, 'NON_EXISTENT_PARTICIPANT', roomData.moderatorCookie @@ -62,10 +62,10 @@ describe('Meetings API Tests', () => { it('should fail with 404 if room does not exist', async () => { // Delete the room to ensure it does not exist - let response = await deleteRoom(roomData.room.roomId, { force: true }); - expect(response.status).toBe(204); + let response = await deleteRoom(roomData.room.roomId, { withMeeting: 'force' }); + expect(response.status).toBe(200); - response = await deleteParticipant(roomData.room.roomId, participantIdentity, roomData.moderatorCookie); + response = await kickParticipant(roomData.room.roomId, participantIdentity, roomData.moderatorCookie); expect(response.status).toBe(404); expect(response.body.error).toBe('Room Error'); }); diff --git a/backend/tests/integration/api/meetings/update-participant.test.ts b/backend/tests/integration/api/meetings/update-participant.test.ts index 82ae2f1..b4211e6 100644 --- a/backend/tests/integration/api/meetings/update-participant.test.ts +++ b/backend/tests/integration/api/meetings/update-participant.test.ts @@ -141,8 +141,8 @@ describe('Meetings API Tests', () => { it('should fail with 404 if room does not exist', async () => { // Delete the room to ensure it does not exist - let response = await deleteRoom(roomData.room.roomId, { force: true }); - expect(response.status).toBe(204); + let response = await deleteRoom(roomData.room.roomId, { withMeeting: 'force' }); + expect(response.status).toBe(200); response = await updateParticipant( roomData.room.roomId, diff --git a/backend/tests/integration/api/participants/generate-token.test.ts b/backend/tests/integration/api/participants/generate-token.test.ts index e488245..e9f72ff 100644 --- a/backend/tests/integration/api/participants/generate-token.test.ts +++ b/backend/tests/integration/api/participants/generate-token.test.ts @@ -4,9 +4,11 @@ import { expectValidationError, expectValidParticipantTokenResponse } from '../. import { deleteAllRooms, disconnectFakeParticipants, + endMeeting, generateParticipantToken, generateParticipantTokenCookie, - startTestServer + startTestServer, + updateRoomStatus } from '../../../helpers/request-helpers.js'; import { RoomData, setupSingleRoom } from '../../../helpers/test-scenarios.js'; @@ -125,6 +127,17 @@ describe('Participant API Tests', () => { roomData = await setupSingleRoom(); }); + it('should fail with 409 when room is closed', async () => { + await endMeeting(roomData.room.roomId, roomData.moderatorCookie); + await updateRoomStatus(roomData.room.roomId, 'closed'); + const response = await generateParticipantToken({ + roomId: roomData.room.roomId, + secret: roomData.moderatorSecret, + participantName + }); + expect(response.status).toBe(409); + }); + it('should fail with 404 when room does not exist', async () => { const response = await generateParticipantToken({ roomId: 'non_existent_room', diff --git a/backend/tests/integration/api/recordings/bulk-delete-recording.test.ts b/backend/tests/integration/api/recordings/bulk-delete-recording.test.ts index d481d75..104f110 100644 --- a/backend/tests/integration/api/recordings/bulk-delete-recording.test.ts +++ b/backend/tests/integration/api/recordings/bulk-delete-recording.test.ts @@ -30,7 +30,7 @@ describe('Recording API Tests', () => { }); describe('Bulk Delete Recording Tests', () => { - it('should return 200 when mixed valid and non-existent IDs are provided', async () => { + it('should return 400 when mixed valid and non-existent IDs are provided', async () => { const testContext = await setupMultiRecordingsTestContext(3, 3, 3); const recordingIds = testContext.rooms.map((room) => room.recordingId); const nonExistentIds = ['nonExistent--EG_000--1234', 'nonExistent--EG_111--5678']; @@ -38,10 +38,11 @@ describe('Recording API Tests', () => { const deleteResponse = await bulkDeleteRecordings(mixedIds); - expect(deleteResponse.status).toBe(200); + expect(deleteResponse.status).toBe(400); expect(deleteResponse.body).toEqual({ + message: expect.stringContaining('could not be deleted'), deleted: expect.arrayContaining(recordingIds), - notDeleted: expect.arrayContaining( + failed: expect.arrayContaining( nonExistentIds.map((id) => ({ recordingId: id, error: expect.stringContaining(`Recording '${id}' not found`) @@ -50,7 +51,7 @@ describe('Recording API Tests', () => { }); }); - it('should return 200 with mixed results when some recordings are in active state', async () => { + it('should return 400 with mixed results when some recordings are in active state', async () => { const testContext = await setupMultiRecordingsTestContext(3, 3, 2); const activeRecordingRoom = testContext.getLastRoom(); const recordingIds = testContext.rooms @@ -60,10 +61,11 @@ describe('Recording API Tests', () => { const activeRecordingId = activeRecordingRoom?.recordingId; let deleteResponse = await bulkDeleteRecordings([...recordingIds, activeRecordingId]); - expect(deleteResponse.status).toBe(200); + expect(deleteResponse.status).toBe(400); expect(deleteResponse.body).toEqual({ + message: expect.stringContaining('could not be deleted'), deleted: expect.arrayContaining(recordingIds), - notDeleted: [ + failed: [ { recordingId: activeRecordingId, error: expect.stringContaining(`Recording '${activeRecordingId}' is not stopped yet`) @@ -75,18 +77,22 @@ describe('Recording API Tests', () => { deleteResponse = await bulkDeleteRecordings([activeRecordingId]); - expect(deleteResponse.status).toBe(204); - expect(deleteResponse.body).toStrictEqual({}); + expect(deleteResponse.status).toBe(200); + expect(deleteResponse.body).toEqual({ + message: expect.stringContaining('All recordings deleted successfully'), + deleted: expect.arrayContaining([activeRecordingId]) + }); }); - it('should not delete any recordings and return 200', async () => { + it('should not delete any recordings and return 400', async () => { const testContext = await setupMultiRecordingsTestContext(2, 2, 0); const recordingIds = testContext.rooms.map((room) => room.recordingId); const deleteResponse = await bulkDeleteRecordings(recordingIds); - expect(deleteResponse.status).toBe(200); + expect(deleteResponse.status).toBe(400); expect(deleteResponse.body).toEqual({ + message: expect.stringContaining('could not be deleted'), deleted: [], - notDeleted: expect.arrayContaining( + failed: expect.arrayContaining( recordingIds.map((id) => ({ recordingId: id, error: expect.stringContaining(`Recording '${id}' is not stopped yet`) @@ -101,12 +107,12 @@ describe('Recording API Tests', () => { ); }); - it('should delete all recordings and return 204 when all operations succeed', async () => { + it('should delete all recordings and return 200 when all operations succeed', async () => { const response = await setupMultiRecordingsTestContext(5, 5, 5); const recordingIds = response.rooms.map((room) => room.recordingId); const deleteResponse = await bulkDeleteRecordings(recordingIds); - expect(deleteResponse.status).toBe(204); + expect(deleteResponse.status).toBe(200); }); it('should only delete recordings belonging to the room when using a recording token', async () => { @@ -125,10 +131,11 @@ describe('Recording API Tests', () => { // Intenta eliminar ambas grabaciones usando el token de la primera sala const deleteResponse = await bulkDeleteRecordings([recordingId, otherRecordingId], recordingCookie); - expect(deleteResponse.status).toBe(200); + expect(deleteResponse.status).toBe(400); expect(deleteResponse.body).toEqual({ + message: expect.stringContaining('could not be deleted'), deleted: [recordingId], - notDeleted: [ + failed: [ { recordingId: otherRecordingId, error: expect.stringContaining( @@ -144,7 +151,7 @@ describe('Recording API Tests', () => { const recordingIds = response.rooms.map((room) => room.recordingId); const deleteResponse = await bulkDeleteRecordings(recordingIds); - expect(deleteResponse.status).toBe(204); + expect(deleteResponse.status).toBe(200); const storageService = container.get(MeetStorageService); @@ -159,8 +166,7 @@ describe('Recording API Tests', () => { const recordingId = testContext.rooms[0].recordingId; const deleteResponse = await bulkDeleteRecordings([recordingId]); - expect(deleteResponse.status).toBe(204); - expect(deleteResponse.body).toStrictEqual({}); + expect(deleteResponse.status).toBe(200); }); it('should handle duplicate recording IDs by treating them as a single delete', async () => { @@ -168,8 +174,7 @@ describe('Recording API Tests', () => { const recordingId = testContext.getRoomByIndex(0)!.recordingId; const deleteResponse = await bulkDeleteRecordings([recordingId, recordingId]); - expect(deleteResponse.status).toBe(204); - expect(deleteResponse.body).toStrictEqual({}); + expect(deleteResponse.status).toBe(200); }); it('should delete room metadata when deleting the last recording', async () => { @@ -197,7 +202,7 @@ describe('Recording API Tests', () => { await stopRecording(secondRecordingId, moderatorCookie); // Delete first recording - room metadata should remain const bulkResponse = await bulkDeleteRecordings([firstRecordingId, secondRecordingId]); - expect(bulkResponse.status).toBe(204); + expect(bulkResponse.status).toBe(200); // // Verify second recording still exists // const secondRecordingResponse = await getRecording(secondRecordingId); diff --git a/backend/tests/integration/api/recordings/delete-recording.test.ts b/backend/tests/integration/api/recordings/delete-recording.test.ts index 45c7168..6ac613a 100644 --- a/backend/tests/integration/api/recordings/delete-recording.test.ts +++ b/backend/tests/integration/api/recordings/delete-recording.test.ts @@ -44,7 +44,7 @@ describe('Recording API Tests', () => { it('should delete a recording successfully', async () => { // Delete the recording const deleteResponse = await deleteRecording(recordingId); - expect(deleteResponse.status).toBe(204); + expect(deleteResponse.status).toBe(200); // Verify that the recording is deleted const getResponse = await deleteRecording(recordingId); @@ -61,7 +61,7 @@ describe('Recording API Tests', () => { // Check that the room metadata still exists after deleteing the first recording const deleteResponse = await deleteRecording(recordingId!); - expect(deleteResponse.status).toBe(204); + expect(deleteResponse.status).toBe(200); recSecrets = await storageService.getAccessRecordingSecrets(recordingId); expect(recSecrets).toBe(null); @@ -85,7 +85,7 @@ describe('Recording API Tests', () => { // Check that the room metadata still exists after deleteing the first recording let deleteResponse = await deleteRecording(recordingId!); - expect(deleteResponse.status).toBe(204); + expect(deleteResponse.status).toBe(200); roomMetadata = await meetStorageService.getArchivedRoomMetadata(room.roomId); @@ -95,7 +95,7 @@ describe('Recording API Tests', () => { // Delete the second recording deleteResponse = await deleteRecording(secondRecordingId!); - expect(deleteResponse.status).toBe(204); + expect(deleteResponse.status).toBe(200); // Verify that the room metadata is deleted after deleting the last recording roomMetadata = await meetStorageService.getArchivedRoomMetadata(room.roomId); @@ -149,8 +149,7 @@ describe('Recording API Tests', () => { it('should sanitize recordingId before validation', async () => { const response = await deleteRecording(` ${recordingId} `); - expect(response.status).toBe(204); - expect(response.body).toStrictEqual({}); + expect(response.status).toBe(200); }); it('should return 409 when attempting to delete an active recording', async () => { @@ -164,7 +163,7 @@ describe('Recording API Tests', () => { await stopRecording(activeRecordingId, moderatorCookie); // Attempt to delete the recording again deleteResponse = await deleteRecording(activeRecordingId); - expect(deleteResponse.status).toBe(204); + expect(deleteResponse.status).toBe(200); }); }); }); diff --git a/backend/tests/integration/api/recordings/race-conditions.test.ts b/backend/tests/integration/api/recordings/race-conditions.test.ts index dfbbbee..817cd4c 100644 --- a/backend/tests/integration/api/recordings/race-conditions.test.ts +++ b/backend/tests/integration/api/recordings/race-conditions.test.ts @@ -434,7 +434,7 @@ describe('Recording API Race Conditions Tests', () => { streamResponse.status === 'fulfilled' && (streamResponse.value.status === 200 || streamResponse.value.status === 206); - const deleteSuccessful = deleteResponse.status === 'fulfilled' && deleteResponse.value.status === 204; + const deleteSuccessful = deleteResponse.status === 'fulfilled' && deleteResponse.value.status === 200; console.log(`Stream successful: ${streamSuccessful}, Delete successful: ${deleteSuccessful}`); @@ -485,8 +485,7 @@ describe('Recording API Race Conditions Tests', () => { // Both operations should complete successfully const [bulkDeleteResult, newRecordingResult] = await Promise.all([bulkDeletePromise, startNewRecordingPromise]); - expect(bulkDeleteResult.status).toBe(204); - expect(bulkDeleteResult.body).toEqual({}); + expect(bulkDeleteResult.status).toBe(200); // Check that the new recording started successfully expectValidStartRecordingResponse(newRecordingResult, room3.room.roomId, room3.room.roomName); diff --git a/backend/tests/integration/api/rooms/bulk-delete-rooms.test.ts b/backend/tests/integration/api/rooms/bulk-delete-rooms.test.ts index dba7aa8..b5ff799 100644 --- a/backend/tests/integration/api/rooms/bulk-delete-rooms.test.ts +++ b/backend/tests/integration/api/rooms/bulk-delete-rooms.test.ts @@ -1,224 +1,150 @@ -import { afterEach, beforeAll, describe, expect, it } from '@jest/globals'; +import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; +import { + MeetingEndAction, + MeetRoom, + MeetRoomDeletionErrorCode, + MeetRoomDeletionPolicyWithMeeting, + MeetRoomDeletionPolicyWithRecordings, + MeetRoomDeletionSuccessCode, + MeetRoomStatus +} from '../../../../src/typings/ce/room.js'; +import { expectValidRoom } from '../../../helpers/assertion-helpers.js'; import { bulkDeleteRooms, createRoom, + deleteAllRecordings, deleteAllRooms, disconnectFakeParticipants, + endMeeting, getRoom, - joinFakeParticipant, - sleep, startTestServer } from '../../../helpers/request-helpers.js'; +import { setupSingleRoom, setupSingleRoomWithRecording } from '../../../helpers/test-scenarios.js'; describe('Room API Tests', () => { beforeAll(() => { startTestServer(); }); - afterEach(async () => { - // Remove all rooms created + afterAll(async () => { await disconnectFakeParticipants(); await deleteAllRooms(); + await deleteAllRecordings(); }); 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({ roomName: 'test-invalid-force' }); + it('should return 200 when all rooms are processed for deletion successfully', async () => { + const { roomId } = await createRoom(); 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({}); + expect(response.status).toBe(200); + expect(response.body).toEqual({ + message: 'All rooms successfully processed for deletion', + successful: expect.arrayContaining([ + { + roomId, + successCode: MeetRoomDeletionSuccessCode.ROOM_DELETED, + message: expect.any(String) + } + ]) + }); }); - it('should mark room for deletion (202) with invalid force parameter when participants exist', async () => { - const { roomId } = await createRoom({ roomName: 'test-invalid-force' }); + it('should return 400 when some rooms fail to process for deletion', async () => { + const room1 = await createRoom(); + const { room: room2 } = await setupSingleRoom(true); - await joinFakeParticipant(roomId, 'test-participant-1'); - - 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 for deletion'); + const response = await bulkDeleteRooms([room1.roomId, room2.roomId]); + expect(response.status).toBe(400); + expect(response.body).toEqual({ + message: '1 room(s) failed to process while deleting', + successful: expect.arrayContaining([ + { + roomId: room1.roomId, + successCode: MeetRoomDeletionSuccessCode.ROOM_DELETED, + message: expect.any(String) + } + ]), + failed: expect.arrayContaining([ + { + roomId: room2.roomId, + error: MeetRoomDeletionErrorCode.ROOM_HAS_ACTIVE_MEETING, + message: expect.any(String) + } + ]) + }); }); - it('should delete room (204) with force=true parameter when no participants exist', async () => { - const { roomId } = await createRoom({ roomName: 'test-force' }); + it('should return 400 when all rooms fail to process for deletion', async () => { + const { room } = await setupSingleRoom(true); - 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({ roomName: 'test-force' }); - - await joinFakeParticipant(roomId, 'test-participant-1'); - - 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({}); + const response = await bulkDeleteRooms([room.roomId]); + expect(response.status).toBe(400); + expect(response.body).toEqual({ + message: '1 room(s) failed to process while deleting', + successful: [], + failed: expect.arrayContaining([ + { + roomId: room.roomId, + error: MeetRoomDeletionErrorCode.ROOM_HAS_ACTIVE_MEETING, + message: expect.any(String) + } + ]) + }); }); it('should successfully delete the room requesting the same roomId multiple times', async () => { - const { roomId } = await createRoom({ roomName: 'test-duplicate' }); + const { roomId } = await createRoom(); - const response = await bulkDeleteRooms([roomId, roomId, roomId], true); - - expect(response.status).toBe(204); - expect(response.body).toStrictEqual({}); + const response = await bulkDeleteRooms([roomId, roomId, roomId]); + expect(response.status).toBe(200); + expect(response.body).toEqual({ + message: 'All rooms successfully processed for deletion', + successful: expect.arrayContaining([ + { + roomId, + successCode: MeetRoomDeletionSuccessCode.ROOM_DELETED, + message: expect.any(String) + } + ]) + }); }); it('should successfully delete valid roomIds while ignoring invalid ones', async () => { - const { roomId } = await createRoom({ roomName: 'test-invalid-force' }); + const { roomId } = await createRoom(); 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({ roomName: 'test-bulk-1' }), - createRoom({ roomName: '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({ roomName: 'test-bulk-1' }), - createRoom({ roomName: 'test-bulk-2' }) - ]); - - await Promise.all([ - joinFakeParticipant(room1.roomId, 'test-participant-1'), - joinFakeParticipant(room2.roomId, 'test-participant-2') - ]); - - // 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.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({ roomName: '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 test rooms - const [room1, room2] = await Promise.all([ - createRoom({ roomName: 'test-bulk-1' }), - createRoom({ roomName: 'test-bulk-2' }) - ]); - - // Join a participant to the rooms - await Promise.all([ - joinFakeParticipant(room1.roomId, 'test-participant-1'), - joinFakeParticipant(room2.roomId, 'test-participant-2') - ]); - - // Attempt to delete the rooms with force=false - const response = await bulkDeleteRooms([room1.roomId, room2.roomId], true); - - expect(response.status).toBe(204); - expect(response.body).toStrictEqual({}); - - // Verify that the rooms are deleted - await sleep('1s'); // Wait a bit for the meetings to be closed and the rooms 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({ roomName: 'empty-room' }); - const room2 = await createRoom({ roomName: 'occupied-room' }); - - // Add participant to only one room - await joinFakeParticipant(room2.roomId, 'test-participant'); - - // 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); + expect(response.body).toEqual({ + message: 'All rooms successfully processed for deletion', + successful: expect.arrayContaining([ + { + roomId, + successCode: MeetRoomDeletionSuccessCode.ROOM_DELETED, + message: expect.any(String) + } + ]) + }); }); it('should handle a large number of room IDs', async () => { - // Create 20+ rooms and test deletion + // Create 20 rooms const rooms = await Promise.all( Array.from({ length: 20 }, (_, i) => createRoom({ roomName: `bulk-${i}` })) ); const response = await bulkDeleteRooms(rooms.map((r) => r.roomId)); - expect(response.status).toBe(204); + expect(response.status).toBe(200); + expect(response.body).toEqual({ + message: 'All rooms successfully processed for deletion', + successful: expect.arrayContaining( + rooms.map((room) => ({ + roomId: room.roomId, + successCode: MeetRoomDeletionSuccessCode.ROOM_DELETED, + message: expect.any(String) + })) + ) + }); // Verify all rooms are deleted for (const room of rooms) { @@ -227,39 +153,98 @@ describe('Room API Tests', () => { } }); - 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({ roomName: `bulk-${i}` })) - ); - - await joinFakeParticipant(rooms[0].roomId, 'test-participant-1'); - - const response = await bulkDeleteRooms([ - ...rooms.map((r) => r.roomId), - '!!@##$', - ',,,,', - 'room-1', - 'room-2' + it('should handle deletion when specifying withMeeting and withRecordings parameters', async () => { + const [room1, { room: room2 }, { room: room3 }, { room: room4, moderatorCookie }] = await Promise.all([ + createRoom(), // Room without active meeting or recordings + setupSingleRoom(true), // Room with active meeting + setupSingleRoomWithRecording(true), // Room with active meeting and recordings + setupSingleRoomWithRecording(true) // Room with recordings ]); + await endMeeting(room4.roomId, moderatorCookie); + const fakeRoomId = 'fakeRoomId'; // Non-existing room - expect(response.status).toBe(200); + const response = await bulkDeleteRooms( + [room1.roomId, room2.roomId, room3.roomId, room4.roomId, fakeRoomId], + MeetRoomDeletionPolicyWithMeeting.WHEN_MEETING_ENDS, + MeetRoomDeletionPolicyWithRecordings.CLOSE + ); + expect(response.status).toBe(400); + expect(response.body).toEqual({ + message: '1 room(s) failed to process while deleting', + successful: expect.arrayContaining([ + { + roomId: room1.roomId, + successCode: MeetRoomDeletionSuccessCode.ROOM_DELETED, + message: expect.any(String) + }, + { + roomId: room2.roomId, + successCode: MeetRoomDeletionSuccessCode.ROOM_WITH_ACTIVE_MEETING_SCHEDULED_TO_BE_DELETED, + message: expect.any(String), + room: expect.any(Object) + }, + { + roomId: room3.roomId, + successCode: MeetRoomDeletionSuccessCode.ROOM_WITH_ACTIVE_MEETING_SCHEDULED_TO_BE_CLOSED, + message: expect.any(String), + room: expect.any(Object) + }, + { + roomId: room4.roomId, + successCode: MeetRoomDeletionSuccessCode.ROOM_CLOSED, + message: expect.any(String), + room: expect.any(Object) + } + ]), + failed: expect.arrayContaining([ + { + roomId: fakeRoomId, + error: 'Room Error', + message: expect.stringContaining('does not exist') + } + ]) + }); - // 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); + // Check successful rooms properties + const successfulRoom2 = response.body.successful.find( + (s: { roomId: string; successCode: MeetRoomDeletionSuccessCode; message: string; room?: MeetRoom }) => + s.room?.roomId === room2.roomId + ); + expectValidRoom( + successfulRoom2.room, + successfulRoom2.room.roomName, + undefined, + undefined, + undefined, + MeetRoomStatus.ACTIVE_MEETING, + MeetingEndAction.DELETE + ); + const successfulRoom3 = response.body.successful.find( + (r: { roomId: string; successCode: MeetRoomDeletionSuccessCode; message: string; room?: MeetRoom }) => + r.room?.roomId === room3.roomId + ); + expectValidRoom( + successfulRoom3.room, + successfulRoom3.room.roomName, + undefined, + undefined, + undefined, + MeetRoomStatus.ACTIVE_MEETING, + MeetingEndAction.CLOSE + ); + const successfulRoom4 = response.body.successful.find( + (r: { roomId: string; successCode: MeetRoomDeletionSuccessCode; message: string; room?: MeetRoom }) => + r.room?.roomId === room4.roomId + ); + expectValidRoom( + successfulRoom4.room, + successfulRoom4.room.roomName, + undefined, + undefined, + undefined, + MeetRoomStatus.CLOSED, + MeetingEndAction.NONE + ); }); }); @@ -293,5 +278,21 @@ describe('Room API Tests', () => { 'At least one valid roomId is required after sanitization' ); }); + + it('should fail when withMeeting parameter is invalid', async () => { + const response = await bulkDeleteRooms(['testRoom'], 'invalid_value'); + + expect(response.status).toBe(422); + expect(response.body.error).toContain('Unprocessable Entity'); + expect(JSON.stringify(response.body.details)).toContain('Invalid enum value'); + }); + + it('should fail when withRecordings parameter is invalid', async () => { + const response = await bulkDeleteRooms(['testRoom'], 'force', 'invalid_value'); + + expect(response.status).toBe(422); + expect(response.body.error).toContain('Unprocessable Entity'); + expect(JSON.stringify(response.body.details)).toContain('Invalid enum value'); + }); }); }); diff --git a/backend/tests/integration/api/rooms/create-room.test.ts b/backend/tests/integration/api/rooms/create-room.test.ts index f905410..9fe29be 100644 --- a/backend/tests/integration/api/rooms/create-room.test.ts +++ b/backend/tests/integration/api/rooms/create-room.test.ts @@ -3,7 +3,11 @@ import { Express } from 'express'; import ms from 'ms'; import request from 'supertest'; import INTERNAL_CONFIG from '../../../../src/config/internal-config.js'; -import { MeetRecordingAccess } from '../../../../src/typings/ce/index.js'; +import { + MeetRecordingAccess, + MeetRoomDeletionPolicyWithMeeting, + MeetRoomDeletionPolicyWithRecordings +} from '../../../../src/typings/ce/index.js'; import { expectValidRoom } from '../../../helpers/assertion-helpers.js'; import { createRoom, deleteAllRooms, loginUser, startTestServer } from '../../../helpers/request-helpers.js'; @@ -25,6 +29,11 @@ describe('Room API Tests', () => { }); describe('Room Creation Tests', () => { + it('Should create a room with default name when roomName is omitted', async () => { + const room = await createRoom(); + expectValidRoom(room, 'Room'); + }); + it('Should create a room without autoDeletionDate (default behavior)', async () => { const room = await createRoom({ roomName: ' Test Room ' @@ -38,13 +47,17 @@ describe('Room API Tests', () => { roomName: ' .,-------}{ยก$#<+My Room *123 ' }); - expectValidRoom(room, 'My Room 123', validAutoDeletionDate); + expectValidRoom(room, 'My Room 123', undefined, validAutoDeletionDate); }); it('Should create a room when sending full valid payload', async () => { const payload = { roomName: ' =Example Room&/ ', autoDeletionDate: validAutoDeletionDate, + autoDeletionPolicy: { + withMeeting: MeetRoomDeletionPolicyWithMeeting.FORCE, + withRecordings: MeetRoomDeletionPolicyWithRecordings.FORCE + }, preferences: { recordingPreferences: { enabled: false, @@ -57,7 +70,13 @@ describe('Room API Tests', () => { const room = await createRoom(payload); - expectValidRoom(room, 'Example Room', validAutoDeletionDate, payload.preferences); + expectValidRoom( + room, + 'Example Room', + payload.preferences, + validAutoDeletionDate, + payload.autoDeletionPolicy + ); }); }); @@ -122,6 +141,63 @@ describe('Room API Tests', () => { expect(JSON.stringify(response.body.details)).toContain('Expected number'); }); + it('should fail when autoDeletionPolicy is not an object (string provided)', async () => { + const payload = { + roomName: 'TestRoom', + autoDeletionDate: validAutoDeletionDate, + autoDeletionPolicy: 'invalid-policy' + }; + + const response = await request(app).post(ROOMS_PATH).set('Cookie', adminCookie).send(payload).expect(422); + + expect(JSON.stringify(response.body.details)).toContain('Expected object'); + }); + + it('should fail when autoDeletionPolicy has invalid structure', async () => { + const payload = { + roomName: 'TestRoom', + autoDeletionDate: validAutoDeletionDate, + autoDeletionPolicy: { + withMeeting: 'invalid-value', + withRecordings: MeetRoomDeletionPolicyWithRecordings.CLOSE + } + }; + + const response = await request(app).post(ROOMS_PATH).set('Cookie', adminCookie).send(payload).expect(422); + + expect(JSON.stringify(response.body.details)).toContain('Invalid enum value'); + }); + + it('should fail when autoDeletionPolicy.withMeeting has FAIL policy', async () => { + const payload = { + roomName: 'TestRoom', + autoDeletionDate: validAutoDeletionDate, + autoDeletionPolicy: { + withMeeting: MeetRoomDeletionPolicyWithMeeting.FAIL, + withRecordings: MeetRoomDeletionPolicyWithRecordings.CLOSE + } + }; + + const response = await request(app).post(ROOMS_PATH).set('Cookie', adminCookie).send(payload).expect(422); + + expect(JSON.stringify(response.body.details)).toContain('FAIL policy is not allowed for withMeeting auto-deletion policy'); + }); + + it('should fail when autoDeletionPolicy.withRecordings has FAIL policy', async () => { + const payload = { + roomName: 'TestRoom', + autoDeletionDate: validAutoDeletionDate, + autoDeletionPolicy: { + withMeeting: MeetRoomDeletionPolicyWithMeeting.WHEN_MEETING_ENDS, + withRecordings: MeetRoomDeletionPolicyWithRecordings.FAIL + } + }; + + const response = await request(app).post(ROOMS_PATH).set('Cookie', adminCookie).send(payload).expect(422); + + expect(JSON.stringify(response.body.details)).toContain('FAIL policy is not allowed for withRecordings auto-deletion policy'); + }); + it('should fail when roomName is not a string (number provided)', async () => { const payload = { roomName: 12345, diff --git a/backend/tests/integration/api/rooms/delete-room.test.ts b/backend/tests/integration/api/rooms/delete-room.test.ts index a981d11..4412251 100644 --- a/backend/tests/integration/api/rooms/delete-room.test.ts +++ b/backend/tests/integration/api/rooms/delete-room.test.ts @@ -1,209 +1,342 @@ -import { afterEach, beforeAll, describe, expect, it } from '@jest/globals'; -import ms from 'ms'; -import { expectValidRoom } from '../../../helpers/assertion-helpers.js'; +import { afterAll, beforeAll, beforeEach, describe, expect, it } from '@jest/globals'; +import { + MeetingEndAction, + MeetRoomDeletionErrorCode, + MeetRoomDeletionPolicyWithMeeting, + MeetRoomDeletionPolicyWithRecordings, + MeetRoomDeletionSuccessCode, + MeetRoomStatus +} from '../../../../src/typings/ce/room.js'; +import { expectSuccessListRecordingResponse, expectValidRoom } from '../../../helpers/assertion-helpers.js'; import { createRoom, + deleteAllRecordings, deleteAllRooms, deleteRoom, disconnectFakeParticipants, + endMeeting, + getAllRecordings, 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'; +import { setupSingleRoom, setupSingleRoomWithRecording } from '../../../helpers/test-scenarios.js'; describe('Room API Tests', () => { beforeAll(() => { startTestServer(); }); - afterEach(async () => { + afterAll(async () => { // Remove all rooms created await disconnectFakeParticipants(); await deleteAllRooms(); + await deleteAllRecordings(); }); describe('Delete Room Tests', () => { - it('should return 204 when room does not exist (idempotent deletion)', async () => { - const response = await deleteRoom('non-existent-room-id'); + describe('without active meeting or recordings', () => { + it('should return 200 with successCode=room_deleted', async () => { + const { roomId } = await createRoom(); - expect(response.status).toBe(204); - }); + const response = await deleteRoom(roomId); + expect(response.status).toBe(200); + expect(response.body).toHaveProperty('successCode', MeetRoomDeletionSuccessCode.ROOM_DELETED); - it('should default to force=false when force parameter is invalid', async () => { - // Create a room first - const { roomId } = await createRoom({ - roomName: 'test-room' + // Check room is deleted + const getResponse = await getRoom(roomId); + expect(getResponse.status).toBe(404); }); - 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' + describe('with active meeting but no recordings', () => { + let roomId: string; + let roomName: string; + let moderatorCookie: string; + + beforeEach(async () => { + // Create a room with an active meeting + const { room, moderatorCookie: cookie } = await setupSingleRoom(true); + roomId = room.roomId; + roomName = room.roomName; + moderatorCookie = cookie; }); - await joinFakeParticipant(roomId, 'test-participant'); + it('should return 200 with successCode=room_with_active_meeting_deleted when withMeeting=force', async () => { + const response = await deleteRoom(roomId, { + withMeeting: MeetRoomDeletionPolicyWithMeeting.FORCE + }); + expect(response.status).toBe(200); + expect(response.body).toHaveProperty( + 'successCode', + MeetRoomDeletionSuccessCode.ROOM_WITH_ACTIVE_MEETING_DELETED + ); - // 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' + // Check room is deleted + const getResponse = await getRoom(roomId); + expect(getResponse.status).toBe(404); }); - // Add some spaces and special chars to the valid roomId - const modifiedId = ` ${createdRoom.roomId}!@# `; - const response = await deleteRoom(modifiedId); + it('should return 202 with successCode=room_with_active_meeting_scheduled_to_be_deleted when withMeeting=when_meeting_ends', async () => { + const response = await deleteRoom(roomId, { + withMeeting: MeetRoomDeletionPolicyWithMeeting.WHEN_MEETING_ENDS + }); + expect(response.status).toBe(202); + expect(response.body).toHaveProperty( + 'successCode', + MeetRoomDeletionSuccessCode.ROOM_WITH_ACTIVE_MEETING_SCHEDULED_TO_BE_DELETED + ); + expectValidRoom( + response.body.room, + roomName, + undefined, + undefined, + undefined, + MeetRoomStatus.ACTIVE_MEETING, + MeetingEndAction.DELETE + ); - // 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' + // End meeting and check the room is deleted + await endMeeting(roomId, moderatorCookie); + const getResponse = await getRoom(roomId); + expect(getResponse.status).toBe(404); }); - 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 return 409 with error=room_has_active_meeting when withMeeting=fail', async () => { + const response = await deleteRoom(roomId, { withMeeting: MeetRoomDeletionPolicyWithMeeting.FAIL }); + expect(response.status).toBe(409); + expect(response.body).toHaveProperty('error', MeetRoomDeletionErrorCode.ROOM_HAS_ACTIVE_MEETING); + }); }); - 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 + describe('with recordings but no active meeting', () => { + let roomId: string; + let roomName: string; + + beforeEach(async () => { + // Create a room with recordings and end the meeting + const { room, moderatorCookie } = await setupSingleRoomWithRecording(true); + roomId = room.roomId; + roomName = room.roomName; + await endMeeting(roomId, moderatorCookie); }); - await joinFakeParticipant(roomId, 'test-participant'); + it('should return 200 with successCode=room_and_recordings_deleted when withRecording=force', async () => { + const response = await deleteRoom(roomId, { + withRecordings: MeetRoomDeletionPolicyWithRecordings.FORCE + }); + expect(response.status).toBe(200); + expect(response.body).toHaveProperty( + 'successCode', + MeetRoomDeletionSuccessCode.ROOM_AND_RECORDINGS_DELETED + ); - 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 + // Check the room and recordings are deleted + const roomResponse = await getRoom(roomId); + expect(roomResponse.status).toBe(404); + const recordingsResponse = await getAllRecordings({ roomId, maxItems: 1 }); + expectSuccessListRecordingResponse(recordingsResponse, 0, false, false, 1); }); - 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); + it('should return 200 with successCode=room_closed when withRecording=close', async () => { + const response = await deleteRoom(roomId, { + withRecordings: MeetRoomDeletionPolicyWithRecordings.CLOSE + }); + expect(response.status).toBe(200); + expect(response.body).toHaveProperty('successCode', MeetRoomDeletionSuccessCode.ROOM_CLOSED); - // Join a participant to the room - await joinFakeParticipant(roomId, 'test-participant'); + // Check that the room is closed and recordings are not deleted + expectValidRoom( + response.body.room, + roomName, + undefined, + undefined, + undefined, + MeetRoomStatus.CLOSED, + MeetingEndAction.NONE + ); - 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' + const recordingsResponse = await getAllRecordings({ roomId, maxItems: 1 }); + expectSuccessListRecordingResponse(recordingsResponse, 1, false, false, 1); }); - 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 return 409 with error=room_has_recordings when withRecording=fail', async () => { + const response = await deleteRoom(roomId, { + withRecordings: MeetRoomDeletionPolicyWithRecordings.FAIL + }); + expect(response.status).toBe(409); + expect(response.body).toHaveProperty('error', MeetRoomDeletionErrorCode.ROOM_HAS_RECORDINGS); + }); }); - it('should successfully delete a room already marked for deletion', async () => { - const { roomId } = await createRoom({ roomName: 'test-marked' }); + describe('with active meeting and recordings', () => { + let roomId: string; + let roomName: string; + let moderatorCookie: string; - // First mark it for deletion - await joinFakeParticipant(roomId, 'test-participant'); + beforeEach(async () => { + // Create a room with recordings, keep the meeting active + const { room, moderatorCookie: cookie } = await setupSingleRoomWithRecording(true); + roomId = room.roomId; + roomName = room.roomName; + moderatorCookie = cookie; + }); - await deleteRoom(roomId, { force: false }); + it('should return 200 with successCode=room_with_active_meeting_and_recordings_deleted when withMeeting=force and withRecording=force', async () => { + const response = await deleteRoom(roomId, { + withMeeting: MeetRoomDeletionPolicyWithMeeting.FORCE, + withRecordings: MeetRoomDeletionPolicyWithRecordings.FORCE + }); + expect(response.status).toBe(200); + expect(response.body).toHaveProperty( + 'successCode', + MeetRoomDeletionSuccessCode.ROOM_WITH_ACTIVE_MEETING_AND_RECORDINGS_DELETED + ); - // Then try to delete it again - const response = await deleteRoom(roomId, { force: true }); - expect(response.status).toBe(204); - }); + // Check the room and recordings are deleted + const roomResponse = await getRoom(roomId); + expect(roomResponse.status).toBe(404); + const recordingsResponse = await getAllRecordings({ roomId, maxItems: 1 }); + expectSuccessListRecordingResponse(recordingsResponse, 0, false, false, 1); + }); - it('should handle repeated deletion of the same room gracefully', async () => { - const { roomId } = await createRoom({ roomName: 'test-idempotent' }); + it('should return 200 with successCode=room_with_active_meeting_closed when withMeeting=force and withRecording=close', async () => { + const response = await deleteRoom(roomId, { + withMeeting: MeetRoomDeletionPolicyWithMeeting.FORCE, + withRecordings: MeetRoomDeletionPolicyWithRecordings.CLOSE + }); + expect(response.status).toBe(200); + expect(response.body).toHaveProperty( + 'successCode', + MeetRoomDeletionSuccessCode.ROOM_WITH_ACTIVE_MEETING_CLOSED + ); + expectValidRoom( + response.body.room, + roomName, + undefined, + undefined, + undefined, + MeetRoomStatus.ACTIVE_MEETING, + MeetingEndAction.CLOSE + ); - // Delete first time - const response1 = await deleteRoom(roomId); - expect(response1.status).toBe(204); + // Check that the room is closed and recordings are not deleted + const roomResponse = await getRoom(roomId); + expect(roomResponse.status).toBe(200); + expectValidRoom( + roomResponse.body, + roomName, + undefined, + undefined, + undefined, + MeetRoomStatus.CLOSED, + MeetingEndAction.NONE + ); - // Delete second time - should still return 204 (no error) - const response2 = await deleteRoom(roomId); - expect(response2.status).toBe(204); + const recordingsResponse = await getAllRecordings({ roomId, maxItems: 1 }); + expectSuccessListRecordingResponse(recordingsResponse, 1, false, false, 1); + }); + + it('should return 409 with error=room_with_active_meeting_has_recordings when withMeeting=force and withRecording=fail', async () => { + const response = await deleteRoom(roomId, { + withMeeting: MeetRoomDeletionPolicyWithMeeting.FORCE, + withRecordings: MeetRoomDeletionPolicyWithRecordings.FAIL + }); + expect(response.status).toBe(409); + expect(response.body).toHaveProperty( + 'error', + MeetRoomDeletionErrorCode.ROOM_WITH_ACTIVE_MEETING_HAS_RECORDINGS + ); + }); + + it('should return 202 with successCode=room_with_active_meeting_and_recordings_scheduled_to_be_deleted when withMeeting=when_meeting_ends and withRecording=force', async () => { + const response = await deleteRoom(roomId, { + withMeeting: MeetRoomDeletionPolicyWithMeeting.WHEN_MEETING_ENDS, + withRecordings: MeetRoomDeletionPolicyWithRecordings.FORCE + }); + expect(response.status).toBe(202); + expect(response.body).toHaveProperty( + 'successCode', + MeetRoomDeletionSuccessCode.ROOM_WITH_ACTIVE_MEETING_AND_RECORDINGS_SCHEDULED_TO_BE_DELETED + ); + expectValidRoom( + response.body.room, + roomName, + undefined, + undefined, + undefined, + MeetRoomStatus.ACTIVE_MEETING, + MeetingEndAction.DELETE + ); + + // End meeting and check the room and recordings are deleted + await endMeeting(roomId, moderatorCookie); + const roomResponse = await getRoom(roomId); + expect(roomResponse.status).toBe(404); + const recordingsResponse = await getAllRecordings({ roomId, maxItems: 1 }); + expectSuccessListRecordingResponse(recordingsResponse, 0, false, false, 1); + }); + + it('should return 202 with successCode=room_with_active_meeting_scheduled_to_be_closed when withMeeting=when_meeting_ends and withRecording=close', async () => { + const response = await deleteRoom(roomId, { + withMeeting: MeetRoomDeletionPolicyWithMeeting.WHEN_MEETING_ENDS, + withRecordings: MeetRoomDeletionPolicyWithRecordings.CLOSE + }); + expect(response.status).toBe(202); + expect(response.body).toHaveProperty( + 'successCode', + MeetRoomDeletionSuccessCode.ROOM_WITH_ACTIVE_MEETING_SCHEDULED_TO_BE_CLOSED + ); + expectValidRoom( + response.body.room, + roomName, + undefined, + undefined, + undefined, + MeetRoomStatus.ACTIVE_MEETING, + MeetingEndAction.CLOSE + ); + + // End meeting and check that the room is closed and recordings are not deleted + await endMeeting(roomId, moderatorCookie); + const roomResponse = await getRoom(roomId); + expect(roomResponse.status).toBe(200); + expectValidRoom( + roomResponse.body, + roomName, + undefined, + undefined, + undefined, + MeetRoomStatus.CLOSED, + MeetingEndAction.NONE + ); + + const recordingsResponse = await getAllRecordings({ roomId, maxItems: 1 }); + expectSuccessListRecordingResponse(recordingsResponse, 1, false, false, 1); + }); + + it('should return 409 with error=room_with_active_meeting_has_recordings_cannot_schedule_deletion when withMeeting=when_meeting_ends and withRecording=fail', async () => { + const response = await deleteRoom(roomId, { + withMeeting: MeetRoomDeletionPolicyWithMeeting.WHEN_MEETING_ENDS, + withRecordings: MeetRoomDeletionPolicyWithRecordings.FAIL + }); + expect(response.status).toBe(409); + expect(response.body).toHaveProperty( + 'error', + MeetRoomDeletionErrorCode.ROOM_WITH_ACTIVE_MEETING_HAS_RECORDINGS_CANNOT_SCHEDULE_DELETION + ); + }); + + it('should return 409 with error=room_with_recordings_has_active_meeting when withMeeting=fail', async () => { + const response = await deleteRoom(roomId, { + withMeeting: MeetRoomDeletionPolicyWithMeeting.FAIL, + withRecordings: MeetRoomDeletionPolicyWithRecordings.FORCE + }); + expect(response.status).toBe(409); + expect(response.body).toHaveProperty( + 'error', + MeetRoomDeletionErrorCode.ROOM_WITH_RECORDINGS_HAS_ACTIVE_MEETING + ); + }); }); }); @@ -217,12 +350,20 @@ describe('Room API Tests', () => { 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 } }); + it('should fail when withMeeting parameter is invalid', async () => { + const response = await deleteRoom('testRoom', { withMeeting: 'invalid_value' }); expect(response.status).toBe(422); expect(response.body.error).toContain('Unprocessable Entity'); - expect(JSON.stringify(response.body.details)).toContain('Expected boolean, received object'); + expect(JSON.stringify(response.body.details)).toContain('Invalid enum value'); + }); + + it('should fail when withRecordings parameter is invalid', async () => { + const response = await deleteRoom('testRoom', { withRecordings: 'invalid_value' }); + + expect(response.status).toBe(422); + expect(response.body.error).toContain('Unprocessable Entity'); + expect(JSON.stringify(response.body.details)).toContain('Invalid enum value'); }); }); }); diff --git a/backend/tests/integration/api/rooms/garbage-collector.test.ts b/backend/tests/integration/api/rooms/garbage-collector.test.ts index 2ea981c..734edba 100644 --- a/backend/tests/integration/api/rooms/garbage-collector.test.ts +++ b/backend/tests/integration/api/rooms/garbage-collector.test.ts @@ -1,19 +1,25 @@ -import { afterEach, beforeAll, describe, expect, it } from '@jest/globals'; +import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'; import ms from 'ms'; import { setInternalConfig } from '../../../../src/config/internal-config.js'; +import { MeetRoomHelper } from '../../../../src/helpers/room.helper.js'; +import { + MeetRoomDeletionPolicyWithMeeting, + MeetRoomDeletionPolicyWithRecordings +} from '../../../../src/typings/ce/room.js'; import { createRoom, + deleteAllRecordings, deleteAllRooms, disconnectFakeParticipants, + endMeeting, + generateParticipantTokenCookie, getRoom, - getRooms, joinFakeParticipant, runRoomGarbageCollector, sleep, + startRecording, startTestServer } from '../../../helpers/request-helpers.js'; -import { container } from '../../../../src/config/dependency-injector.config.js'; -import { RoomService } from '../../../../src/services/room.service.js'; describe('Room Garbage Collector Tests', () => { beforeAll(() => { @@ -23,13 +29,14 @@ describe('Room Garbage Collector Tests', () => { startTestServer(); }); - afterEach(async () => { + afterAll(async () => { // Remove all rooms created await disconnectFakeParticipants(); await deleteAllRooms(); + await deleteAllRecordings(); }); - it('should delete a room with a past auto-deletion date if no participant is present', async () => { + it('should delete a room with a past auto-deletion date if no active meeting', async () => { const createdRoom = await createRoom({ roomName: 'test-gc', autoDeletionDate: Date.now() + ms('1s') @@ -48,20 +55,20 @@ describe('Room Garbage Collector Tests', () => { expect(response.status).toBe(404); }); - it('should mark room for deletion but not delete when expiration date has passed and participants exist', async () => { + it('should schedule room to be deleted when expiration date has passed and there is a active meeting', async () => { const createdRoom = await createRoom({ roomName: 'test-gc-participants', autoDeletionDate: Date.now() + ms('1s') }); - await joinFakeParticipant(createdRoom.roomId, 'test-participant'); await runRoomGarbageCollector(); - // The room should not be deleted but marked for deletion + // The room should not be deleted but scheduled for deletion const response = await getRoom(createdRoom.roomId); expect(response.status).toBe(200); - expect(response.body.markedForDeletion).toBe(true); + expect(response.body).toHaveProperty('status', 'active_meeting'); + expect(response.body).toHaveProperty('meetingEndAction', 'delete'); }); it('should not touch a room with a future auto-deletion date', async () => { @@ -74,45 +81,32 @@ describe('Room Garbage Collector Tests', () => { const response = await getRoom(createdRoom.roomId); expect(response.status).toBe(200); - expect(response.body.markedForDeletion).toBeFalsy(); + expect(response.body).toHaveProperty('status', 'open'); + expect(response.body).toHaveProperty('meetingEndAction', 'none'); }); - it('should delete a room after the last participant leaves when it was marked for deletion', async () => { - const { roomId } = await createRoom({ + it('should delete a room scheduled for deletion when the the meeting ends', async () => { + const room = await createRoom({ roomName: 'test-gc-lifecycle', autoDeletionDate: Date.now() + ms('1s') }); + await joinFakeParticipant(room.roomId, 'test-participant'); - 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); - - await joinFakeParticipant(roomId, 'test-participant'); - - // Wait for the auto-deletion date to pass - await sleep('1s'); - - // Should mark the room for deletion but not delete it yet await runRoomGarbageCollector(); - let response = await getRoom(roomId); + // The room should not be deleted but scheduled for deletion + let response = await getRoom(room.roomId); expect(response.status).toBe(200); - expect(response.body.markedForDeletion).toBe(true); - expect(response.body.autoDeletionDate).toBeTruthy(); - expect(response.body.autoDeletionDate).toBeLessThan(Date.now()); + expect(response.body).toHaveProperty('status', 'active_meeting'); + expect(response.body).toHaveProperty('meetingEndAction', 'delete'); - await disconnectFakeParticipants(); - - // Wait to receive webhook room_finished - await sleep('1s'); + // End the meeting + const { moderatorSecret } = MeetRoomHelper.extractSecretsFromRoom(room); + const moderatorCookie = await generateParticipantTokenCookie(room.roomId, moderatorSecret, 'moderator'); + await endMeeting(room.roomId, moderatorCookie); // Verify that the room is deleted - response = await getRoom(roomId); + response = await getRoom(room.roomId); expect(response.status).toBe(404); }); @@ -123,14 +117,10 @@ describe('Room Garbage Collector Tests', () => { await runRoomGarbageCollector(); - let response = await getRoom(createdRoom.roomId); + const response = await getRoom(createdRoom.roomId); expect(response.status).toBe(200); - - await runRoomGarbageCollector(); - response = await getRoom(createdRoom.roomId); - expect(response.status).toBe(200); - expect(response.body.markedForDeletion).toBeFalsy(); - expect(response.body.autoDeletionDate).toBeFalsy(); + expect(response.body).toHaveProperty('status', 'open'); + expect(response.body).toHaveProperty('meetingEndAction', 'none'); }); it('should handle multiple expired rooms in one batch', async () => { @@ -161,11 +151,48 @@ describe('Room Garbage Collector Tests', () => { expect(response.status).toBe(200); // Should still exist } } + }); - const response = await getRooms(); - const { body } = response; + it('should handle expired rooms correctly when specifying autoDeletionPolicy', async () => { + // Create both rooms in parallel + const [room1, room2] = await Promise.all([ + createRoom({ + roomName: 'test-gc-policy-1', + autoDeletionDate: Date.now() + ms('1s'), + autoDeletionPolicy: { + withMeeting: MeetRoomDeletionPolicyWithMeeting.FORCE, + withRecordings: MeetRoomDeletionPolicyWithRecordings.CLOSE + } + }), + createRoom({ + roomName: 'test-gc-policy-2', + autoDeletionDate: Date.now() + ms('1s'), + autoDeletionPolicy: { + withMeeting: MeetRoomDeletionPolicyWithMeeting.WHEN_MEETING_ENDS, + withRecordings: MeetRoomDeletionPolicyWithRecordings.FORCE + } + }) + ]); + // Join participants + await joinFakeParticipant(room1.roomId, 'participant1'); + await joinFakeParticipant(room2.roomId, 'participant2'); + + // Start recording + const { moderatorSecret } = MeetRoomHelper.extractSecretsFromRoom(room1); + const moderatorCookie = await generateParticipantTokenCookie(room1.roomId, moderatorSecret, 'moderator'); + await startRecording(room1.roomId, moderatorCookie); + + await runRoomGarbageCollector(); + + const response = await getRoom(room1.roomId); expect(response.status).toBe(200); - expect(body.rooms.length).toBe(2); // Only 2 rooms should remain + expect(response.body).toHaveProperty('status', 'closed'); + expect(response.body).toHaveProperty('meetingEndAction', 'none'); + + const response2 = await getRoom(room2.roomId); + expect(response2.status).toBe(200); + expect(response2.body).toHaveProperty('status', 'active_meeting'); + expect(response2.body).toHaveProperty('meetingEndAction', 'delete'); }); }); diff --git a/backend/tests/integration/api/rooms/get-rooms.test.ts b/backend/tests/integration/api/rooms/get-rooms.test.ts index a7da28f..e898bd0 100644 --- a/backend/tests/integration/api/rooms/get-rooms.test.ts +++ b/backend/tests/integration/api/rooms/get-rooms.test.ts @@ -82,7 +82,7 @@ describe('Room API Tests', () => { expectSuccessRoomsResponse(response, 3, 3, true, true); rooms.forEach((room: MeetRoom, i: number) => { - expectValidRoom(room, `test-room-${i}`, validAutoDeletionDate); + expectValidRoom(room, `test-room-${i}`, undefined, validAutoDeletionDate); }); const nextPageToken = pagination.nextPageToken; @@ -90,7 +90,7 @@ describe('Room API Tests', () => { ({ pagination, rooms } = response.body); expectSuccessRoomsResponse(response, 3, 3, false, false); rooms.forEach((room: MeetRoom, i: number) => { - expectValidRoom(room, `test-room-${i + 3}`, validAutoDeletionDate); + expectValidRoom(room, `test-room-${i + 3}`, undefined, validAutoDeletionDate); }); }); diff --git a/backend/tests/integration/api/rooms/update-room.test.ts b/backend/tests/integration/api/rooms/update-room-preferences.test.ts similarity index 98% rename from backend/tests/integration/api/rooms/update-room.test.ts rename to backend/tests/integration/api/rooms/update-room-preferences.test.ts index cb0ef7d..1af5840 100644 --- a/backend/tests/integration/api/rooms/update-room.test.ts +++ b/backend/tests/integration/api/rooms/update-room-preferences.test.ts @@ -21,7 +21,7 @@ describe('Room API Tests', () => { await deleteAllRooms(); }); - describe('Update Room Tests', () => { + describe('Update Room Preferences Tests', () => { let frontendEventService: FrontendEventService; beforeAll(() => { @@ -113,7 +113,7 @@ describe('Room API Tests', () => { }); }); - describe('Update Room Validation failures', () => { + describe('Update Room Preferences Validation failures', () => { it('should fail when preferences have incorrect structure', async () => { const { roomId } = await createRoom({ roomName: 'validation-test' diff --git a/backend/tests/integration/api/rooms/update-room-status.test.ts b/backend/tests/integration/api/rooms/update-room-status.test.ts new file mode 100644 index 0000000..8f09e12 --- /dev/null +++ b/backend/tests/integration/api/rooms/update-room-status.test.ts @@ -0,0 +1,103 @@ +import { afterEach, beforeAll, describe, expect, it } from '@jest/globals'; +import { + createRoom, + deleteAllRooms, + disconnectFakeParticipants, + endMeeting, + getRoom, + startTestServer, + updateRoomStatus +} from '../../../helpers/request-helpers.js'; +import { setupSingleRoom } from '../../../helpers/test-scenarios.js'; + +describe('Room API Tests', () => { + beforeAll(() => { + startTestServer(); + }); + + afterEach(async () => { + await disconnectFakeParticipants(); + await deleteAllRooms(); + }); + + describe('Update Room Status Tests', () => { + it('should successfully update room status to open', async () => { + const createdRoom = await createRoom({ + roomName: 'update-test' + }); + + // Update the room status + const response = await updateRoomStatus(createdRoom.roomId, 'open'); + expect(response.status).toBe(200); + expect(response.body).toHaveProperty('message'); + + // Verify with a get request + const getResponse = await getRoom(createdRoom.roomId); + expect(getResponse.status).toBe(200); + expect(getResponse.body.status).toEqual('open'); + }); + + it('should successfully update room status to closed', async () => { + const createdRoom = await createRoom({ + roomName: 'update-test' + }); + + // Update the room status + const response = await updateRoomStatus(createdRoom.roomId, 'closed'); + expect(response.status).toBe(200); + expect(response.body).toHaveProperty('message'); + + // Verify with a get request + const getResponse = await getRoom(createdRoom.roomId); + expect(getResponse.status).toBe(200); + expect(getResponse.body.status).toEqual('closed'); + }); + + it('should schedule room to be closed when meeting ends if there is an active meeting', async () => { + const roomData = await setupSingleRoom(true); + + // Update the room status + const response = await updateRoomStatus(roomData.room.roomId, 'closed'); + expect(response.status).toBe(202); + expect(response.body).toHaveProperty('message'); + + // Verify with a get request + let getResponse = await getRoom(roomData.room.roomId); + expect(getResponse.status).toBe(200); + expect(getResponse.body.status).toEqual('active_meeting'); + expect(getResponse.body.meetingEndAction).toEqual('close'); + + // End meeting and verify closed status + await endMeeting(roomData.room.roomId, roomData.moderatorCookie); + + getResponse = await getRoom(roomData.room.roomId); + expect(getResponse.status).toBe(200); + expect(getResponse.body.status).toEqual('closed'); + expect(getResponse.body.meetingEndAction).toEqual('none'); + }); + + it('should fail with 404 when updating non-existent room', async () => { + const nonExistentRoomId = 'non-existent-room'; + + const response = await updateRoomStatus(nonExistentRoomId, 'closed'); + + expect(response.status).toBe(404); + expect(response.body.message).toContain(`'${nonExistentRoomId}' does not exist`); + }); + }); + + describe('Update Room Status Validation failures', () => { + it('should fail when status is invalid', async () => { + const { roomId } = await createRoom({ + roomName: 'validation-test' + }); + + // Invalid status + const response = await updateRoomStatus(roomId, 'invalid_status'); + + expect(response.status).toBe(422); + expect(response.body.error).toContain('Unprocessable Entity'); + expect(JSON.stringify(response.body.details)).toContain('Invalid enum value'); + }); + }); +}); diff --git a/backend/tests/integration/api/security/auth.test.ts b/backend/tests/integration/api/security/auth.test.ts index e1e180a..b9d0fae 100644 --- a/backend/tests/integration/api/security/auth.test.ts +++ b/backend/tests/integration/api/security/auth.test.ts @@ -27,7 +27,6 @@ describe('Authentication API Tests', () => { .expect(200); expect(response.body).toHaveProperty('message'); - expect(response.body.message).toBe('Login succeeded'); // Check for access token and refresh token cookies expect(response.headers['set-cookie']).toBeDefined(); @@ -144,7 +143,6 @@ describe('Authentication API Tests', () => { .expect(200); expect(response.body).toHaveProperty('message'); - expect(response.body.message).toBe('Token refreshed'); // Check for new access token cookie const newCookies = response.headers['set-cookie'] as unknown as string[]; @@ -235,7 +233,7 @@ describe('Authentication API Tests', () => { it('should delete all API keys', async () => { const apiKey = await generateApiKey(); - await request(app).delete(`${AUTH_PATH}/api-keys`).set('Cookie', adminCookie).expect(204); + await request(app).delete(`${AUTH_PATH}/api-keys`).set('Cookie', adminCookie).expect(200); // Confirm deletion const getResponse = await getApiKeys(); diff --git a/backend/tests/integration/api/security/meeting-security.test.ts b/backend/tests/integration/api/security/meeting-security.test.ts index 918ba64..4f3d8d0 100644 --- a/backend/tests/integration/api/security/meeting-security.test.ts +++ b/backend/tests/integration/api/security/meeting-security.test.ts @@ -141,7 +141,7 @@ describe('Meeting API Security Tests', () => { }); }); - describe('Delete Participant from Meeting Tests', () => { + describe('Kick Participant from Meeting Tests', () => { const PARTICIPANT_IDENTITY = 'TEST_PARTICIPANT'; it('should fail when request includes API key', async () => { diff --git a/backend/tests/integration/api/security/recording-security.test.ts b/backend/tests/integration/api/security/recording-security.test.ts index 5599438..3f8427a 100644 --- a/backend/tests/integration/api/security/recording-security.test.ts +++ b/backend/tests/integration/api/security/recording-security.test.ts @@ -428,7 +428,7 @@ describe('Recording API Security Tests', () => { /* Use a simulated recording ID matching the API's expected format. This allows testing the delete endpoint logic without deleting a real recording. - As a result, all successful delete tests will expect a 404 Not Found response. + As a result, all successful delete tests will expect a 400 response with failed recordings. */ fakeRecordingId = `${roomData.room.roomId}--EG_xxx--uid`; }); @@ -438,7 +438,7 @@ describe('Recording API Security Tests', () => { .delete(RECORDINGS_PATH) .query({ recordingIds: fakeRecordingId }) .set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_INITIAL_API_KEY); - expect(response.status).toBe(200); + expect(response.status).toBe(400); }); it('should succeed when user is authenticated as admin', async () => { @@ -446,7 +446,7 @@ describe('Recording API Security Tests', () => { .delete(RECORDINGS_PATH) .query({ recordingIds: fakeRecordingId }) .set('Cookie', adminCookie); - expect(response.status).toBe(200); + expect(response.status).toBe(400); }); it('should fail when recording access is admin-moderator-speaker and participant is speaker', async () => { @@ -480,7 +480,7 @@ describe('Recording API Security Tests', () => { .delete(RECORDINGS_PATH) .query({ recordingIds: fakeRecordingId }) .set('Cookie', recordingCookie); - expect(response.status).toBe(200); + expect(response.status).toBe(400); }); it('should fail when recording access is admin-moderator and participant is speaker', async () => { @@ -508,7 +508,7 @@ describe('Recording API Security Tests', () => { .delete(RECORDINGS_PATH) .query({ recordingIds: fakeRecordingId }) .set('Cookie', recordingCookie); - expect(response.status).toBe(200); + expect(response.status).toBe(400); }); }); diff --git a/backend/tests/integration/api/security/room-security.test.ts b/backend/tests/integration/api/security/room-security.test.ts index 2db6bdd..e4cb62f 100644 --- a/backend/tests/integration/api/security/room-security.test.ts +++ b/backend/tests/integration/api/security/room-security.test.ts @@ -85,7 +85,7 @@ describe('Room API Security Tests', () => { .delete(ROOMS_PATH) .query({ roomIds: roomId }) .set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_INITIAL_API_KEY); - expect(response.status).toBe(204); + expect(response.status).toBe(200); }); it('should succeed when user is authenticated as admin', async () => { @@ -93,7 +93,7 @@ describe('Room API Security Tests', () => { .delete(ROOMS_PATH) .query({ roomIds: roomId }) .set('Cookie', adminCookie); - expect(response.status).toBe(204); + expect(response.status).toBe(200); }); it('should fail when user is not authenticated', async () => { @@ -165,12 +165,12 @@ describe('Room API Security Tests', () => { const response = await request(app) .delete(`${ROOMS_PATH}/${roomId}`) .set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_INITIAL_API_KEY); - expect(response.status).toBe(204); + expect(response.status).toBe(200); }); it('should succeed when user is authenticated as admin', async () => { const response = await request(app).delete(`${ROOMS_PATH}/${roomId}`).set('Cookie', adminCookie); - expect(response.status).toBe(204); + expect(response.status).toBe(200); }); it('should fail when user is not authenticated', async () => { @@ -283,6 +283,38 @@ describe('Room API Security Tests', () => { }); }); + describe('Update Room Status Tests', () => { + let roomId: string; + + beforeAll(async () => { + const room = await createRoom(); + roomId = room.roomId; + }); + + it('should succeed when request includes API key', async () => { + const response = await request(app) + .put(`${ROOMS_PATH}/${roomId}/status`) + .set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_INITIAL_API_KEY) + .send({ status: 'open' }); + expect(response.status).toBe(200); + }); + + it('should succeed when user is authenticated as admin', async () => { + const response = await request(app) + .put(`${ROOMS_PATH}/${roomId}/status`) + .set('Cookie', adminCookie) + .send({ status: 'open' }); + expect(response.status).toBe(200); + }); + + it('should fail when user is not authenticated', async () => { + const response = await request(app) + .put(`${ROOMS_PATH}/${roomId}/status`) + .send({ status: 'open' }); + expect(response.status).toBe(401); + }); + }); + describe('Generate Recording Token Tests', () => { let roomData: RoomData; diff --git a/backend/tests/integration/api/users/change-password.test.ts b/backend/tests/integration/api/users/change-password.test.ts index b1e079f..13dc8a2 100644 --- a/backend/tests/integration/api/users/change-password.test.ts +++ b/backend/tests/integration/api/users/change-password.test.ts @@ -16,7 +16,6 @@ describe('Users API Tests', () => { const newPassword = 'newpassword123'; const response = await changePassword(MEET_INITIAL_ADMIN_PASSWORD, newPassword, adminCookie); expect(response.status).toBe(200); - expect(response.body).toHaveProperty('message', 'Password changed successfully'); // Reset password await changePassword(newPassword, MEET_INITIAL_ADMIN_PASSWORD, adminCookie); diff --git a/backend/tests/integration/webhooks/webhook.test.ts b/backend/tests/integration/webhooks/webhook.test.ts index 8145c68..d1b5d48 100644 --- a/backend/tests/integration/webhooks/webhook.test.ts +++ b/backend/tests/integration/webhooks/webhook.test.ts @@ -114,7 +114,7 @@ describe('Webhook Integration Tests', () => { const context = await setupSingleRoom(true); const roomData = context.room; // Forcefully delete the room - await deleteRoom(roomData.roomId, { force: true }); + await deleteRoom(roomData.roomId, { withMeeting: 'force' }); // Verify 'meetingEnded' webhook is sent expect(receivedWebhooks.length).toBeGreaterThanOrEqual(1);