From 9bdacf7d0f2a25b1f2f2410172468bdf360bcfbe Mon Sep 17 00:00:00 2001 From: Carlos Santos <4a.santos@gmail.com> Date: Tue, 15 Apr 2025 11:18:54 +0200 Subject: [PATCH] backend: Add integration tests for room garbage collector functionality --- .../api/rooms/garbage-collector.test.ts | 165 ++++++++++++++++++ backend/tests/utils/helpers.ts | 28 ++- 2 files changed, 189 insertions(+), 4 deletions(-) create mode 100644 backend/tests/integration/api/rooms/garbage-collector.test.ts diff --git a/backend/tests/integration/api/rooms/garbage-collector.test.ts b/backend/tests/integration/api/rooms/garbage-collector.test.ts new file mode 100644 index 0000000..33bee53 --- /dev/null +++ b/backend/tests/integration/api/rooms/garbage-collector.test.ts @@ -0,0 +1,165 @@ +import { describe, it, expect, beforeAll, afterAll, afterEach } from '@jest/globals'; +import { + createRoom, + deleteAllRooms, + startTestServer, + stopTestServer, + getRoom, + sleep, + joinFakeParticipant, + runRoomGarbageCollector, + disconnectFakeParticipants, + getRooms +} from '../../../utils/helpers.js'; +import ms from 'ms'; +import { setPrivateConfig } from '../../../../src/config/internal-config.js'; + +describe('OpenVidu Meet Room Garbage Collector Tests', () => { + beforeAll(async () => { + setPrivateConfig({ + MIN_FUTURE_TIME_FOR_ROOM_AUTODELETION_DATE: '0s' + }); + await startTestServer(); + }); + + afterAll(async () => { + await stopTestServer(); + }); + + afterEach(async () => { + // Remove all rooms created + await deleteAllRooms(); + }); + + it('should delete a room with a past auto-deletion date if no participant is present', async () => { + const createdRoom = await createRoom({ + roomIdPrefix: 'test-gc', + autoDeletionDate: Date.now() + ms('1s') + }); + + let response = await getRoom(createdRoom.roomId); + expect(response.status).toBe(200); + + // Wait for auto-deletion date to pass + await sleep(2000); + + // Run garbage collector + await runRoomGarbageCollector(); + + response = await getRoom(createdRoom.roomId); + expect(response.status).toBe(404); + }); + + it('should mark room for deletion but not delete when expiration date has passed and participants exist', async () => { + const createdRoom = await createRoom({ + roomIdPrefix: 'test-gc-participants', + autoDeletionDate: Date.now() + ms('1s') + }); + + joinFakeParticipant(createdRoom.roomId, 'test-participant'); + await sleep(2000); + + await runRoomGarbageCollector(); + + // The room should not be deleted but marked for deletion + const response = await getRoom(createdRoom.roomId); + expect(response.status).toBe(200); + expect(response.body.markedForDeletion).toBe(true); + }); + + it('should not touch a room with a future auto-deletion date', async () => { + const createdRoom = await createRoom({ + roomIdPrefix: 'test-gc-future', + autoDeletionDate: Date.now() + ms('1h') + }); + + await runRoomGarbageCollector(); + + const response = await getRoom(createdRoom.roomId); + expect(response.status).toBe(200); + expect(response.body.markedForDeletion).toBeFalsy(); + }); + + it('should delete a room after the last participant leaves when it was marked for deletion', async () => { + const createdRoom = await createRoom({ + roomIdPrefix: 'test-gc-lifecycle', + autoDeletionDate: Date.now() + ms('1s') + }); + + joinFakeParticipant(createdRoom.roomId, 'test-participant'); + + // Wait for the auto-deletion date to pass + await sleep(1000); + + // Should mark the room for deletion but not delete it yet + await runRoomGarbageCollector(); + + let response = await getRoom(createdRoom.roomId); + expect(response.status).toBe(200); + expect(response.body.markedForDeletion).toBe(true); + expect(response.body.autoDeletionDate).toBeTruthy(); + expect(response.body.autoDeletionDate).toBeLessThan(Date.now()); + + disconnectFakeParticipants(); + + // Wait to receive webhook room_finished + await sleep(3000); + + // Verify that the room is deleted + response = await getRoom(createdRoom.roomId); + expect(response.status).toBe(404); + }); + + it('should never delete a room without an auto-deletion date', async () => { + const createdRoom = await createRoom({ + roomIdPrefix: 'test-gc-no-date' + }); + + await runRoomGarbageCollector(); + + let 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(); + }); + + it('should handle multiple expired rooms in one batch', async () => { + const rooms = await Promise.all([ + createRoom({ roomIdPrefix: 'test-gc-multi-1', autoDeletionDate: Date.now() + ms('1s') }), + createRoom({ roomIdPrefix: 'test-gc-multi-2', autoDeletionDate: Date.now() + ms('1s') }), + createRoom({ roomIdPrefix: 'test-gc-multi-3', autoDeletionDate: Date.now() + ms('1s') }), + createRoom({ roomIdPrefix: 'test-gc-multi-4', autoDeletionDate: Date.now() + ms('1h') }), + createRoom({ roomIdPrefix: 'test-gc-multi-5', autoDeletionDate: Date.now() + ms('1h') }), + createRoom({ roomIdPrefix: 'test-gc-multi-6', autoDeletionDate: Date.now() + ms('1s') }), + createRoom({ roomIdPrefix: 'test-gc-multi-7', autoDeletionDate: Date.now() + ms('1s') }), + createRoom({ roomIdPrefix: 'test-gc-multi-8', autoDeletionDate: Date.now() + ms('1s') }), + createRoom({ roomIdPrefix: 'test-gc-multi-9', autoDeletionDate: Date.now() + ms('1s') }), + createRoom({ roomIdPrefix: 'test-gc-multi-10', autoDeletionDate: Date.now() + ms('1s') }) + ]); + + // Make sure all rooms are expired + await sleep(2000); + + await runRoomGarbageCollector(); + + for (const room of rooms) { + const response = await getRoom(room.roomId); + + if (room.autoDeletionDate! < Date.now()) { + expect(response.status).toBe(404); // Should be deleted + } else { + expect(response.status).toBe(200); // Should still exist + } + } + + const response = await getRooms(); + const { body } = response; + + expect(response.status).toBe(200); + expect(body.rooms.length).toBe(2); // Only 2 rooms should remain + }); +}); diff --git a/backend/tests/utils/helpers.ts b/backend/tests/utils/helpers.ts index a5c810e..f01f693 100644 --- a/backend/tests/utils/helpers.ts +++ b/backend/tests/utils/helpers.ts @@ -18,6 +18,8 @@ import { AuthMode, AuthType, MeetRoom, UserRole, MeetRoomOptions } from '../../s import { expect } from '@jest/globals'; import INTERNAL_CONFIG from '../../src/config/internal-config.js'; import { ChildProcess, execSync, spawn } from 'child_process'; +import { container } from '../../src/config/dependency-injector.config.js'; +import { RoomService } from '../../src/services/room.service.js'; const CREDENTIALS = { user: { @@ -35,6 +37,10 @@ let server: Server; const fakeParticipantsProcesses = new Map(); +export const sleep = (ms: number) => { + return new Promise((resolve) => setTimeout(resolve, ms)); +}; + /** * Starts the test server */ @@ -264,6 +270,24 @@ export const assertEmptyRooms = async () => { assertSuccessRoomsResponse(response, 0, 10, false, false); }; +/** + * Runs the room garbage collector to delete expired rooms. + * + * This function retrieves the RoomService from the dependency injection container + * and calls its deleteExpiredRooms method to clean up expired rooms. + * It then waits for 1 second before completing. + */ +export const runRoomGarbageCollector = async () => { + if (!app) { + throw new Error('App instance is not defined'); + } + + const roomService = container.get(RoomService); + await (roomService as any)['deleteExpiredRooms'](); + + await sleep(1000); +}; + /** * Deletes all rooms */ @@ -383,10 +407,6 @@ export const disconnectFakeParticipants = () => { fakeParticipantsProcesses.clear(); }; -export const sleep = (ms: number) => { - return new Promise((resolve) => setTimeout(resolve, ms)); -}; - // PRIVATE METHODS const runCommandSync = (command: string): string => {