import { describe, expect, it, beforeEach, afterEach, beforeAll } from '@jest/globals'; import { container, registerDependencies } from '../../../src/config/index.js'; import { ParticipantNameService } from '../../../src/services/participant-name.service.js'; import { RedisService } from '../../../src/services/redis.service.js'; import ms from 'ms'; describe('ParticipantNameService', () => { let participantNameService: ParticipantNameService; let redisService: RedisService; const testRoomId = 'test-room-unique-names'; beforeAll(async () => { registerDependencies(); participantNameService = container.get(ParticipantNameService); redisService = container.get(RedisService); }); beforeEach(async () => { // Clean up any existing test data await cleanupTestData(); }); afterEach(async () => { // Clean up test data after each test await cleanupTestData(); }); async function cleanupTestData() { try { const pattern = `ov_meet:room_participants:${testRoomId}:*`; const keys = await redisService.getKeys(pattern); if (keys.length > 0) { await redisService.delete(keys); } const counterPattern = `ov_meet:participant_counter:${testRoomId}:*`; const counterKeys = await redisService.getKeys(counterPattern); if (counterKeys.length > 0) { await redisService.delete(counterKeys); } } catch (error) { // Ignore cleanup errors } } describe('Reserve unique participant name', () => { it('should reserve the original name when available', async () => { const requestedName = 'Participant'; const reservedName = await participantNameService.reserveUniqueName(testRoomId, requestedName); expect(reservedName).toBe(requestedName); // Verify the name is actually reserved const isReserved = await participantNameService.isNameReserved(testRoomId, requestedName); expect(isReserved).toBe(true); }); it('should treat names as case-insensitive if required', async () => { await participantNameService.reserveUniqueName(testRoomId, 'Participant'); const reserved2 = await participantNameService.reserveUniqueName(testRoomId, 'participant'); expect(reserved2).toBe('participant_1'); }); it('should generate alternative names when original is taken', async () => { const requestedName = 'Participant'; // Reserve the original name const firstReservation = await participantNameService.reserveUniqueName(testRoomId, requestedName); expect(firstReservation).toBe(requestedName); // Try to reserve the same name again - should get alternative const secondReservation = await participantNameService.reserveUniqueName(testRoomId, requestedName); expect(secondReservation).toBe(`${requestedName}_1`); // Try again - should get next alternative const thirdReservation = await participantNameService.reserveUniqueName(testRoomId, requestedName); expect(thirdReservation).toBe(`${requestedName}_2`); }); it('should handle concurrent reservations properly', async () => { const requestedName = 'Participant'; const concurrentRequests = 5; // Simulate concurrent requests for the same name const reservationPromises = Array.from({ length: concurrentRequests }, () => participantNameService.reserveUniqueName(testRoomId, requestedName) ); const reservedNames = await Promise.all(reservationPromises); // All names should be unique const uniqueNames = new Set(reservedNames); expect(uniqueNames.size).toBe(concurrentRequests); // First name should be the original expect(reservedNames).toContain(requestedName); // Others should be alternatives for (let i = 1; i < concurrentRequests; i++) { expect(reservedNames).toContain(`${requestedName}_${i}`); } }); it('should reuse numbers when participants disconnect', async () => { const requestedName = 'Participant'; // Reserve multiple names const name1 = await participantNameService.reserveUniqueName(testRoomId, requestedName); const name2 = await participantNameService.reserveUniqueName(testRoomId, requestedName); const name3 = await participantNameService.reserveUniqueName(testRoomId, requestedName); expect(name1).toBe('Participant'); expect(name2).toBe('Participant_1'); expect(name3).toBe('Participant_2'); // Release the middle one await participantNameService.releaseName(testRoomId, name2); // Next reservation should reuse the released number const name4 = await participantNameService.reserveUniqueName(testRoomId, requestedName); expect(name4).toBe('Participant_1'); // Should reuse the released number }); it('should maintain optimal numbering after multiple releases', async () => { const requestedName = 'Optimized'; // Create several names const names: string[] = []; for (let i = 0; i < 5; i++) { names.push(await participantNameService.reserveUniqueName(testRoomId, requestedName)); } expect(names).toEqual(['Optimized', 'Optimized_1', 'Optimized_2', 'Optimized_3', 'Optimized_4']); // Release some names (simulate participants leaving) await participantNameService.releaseName(testRoomId, 'Optimized_1'); await participantNameService.releaseName(testRoomId, 'Optimized_3'); // New participants should get the lowest available numbers const newName1 = await participantNameService.reserveUniqueName(testRoomId, requestedName); const newName2 = await participantNameService.reserveUniqueName(testRoomId, requestedName); expect(newName1).toBe('Optimized_1'); // Lowest available expect(newName2).toBe('Optimized_3'); // Next lowest available }); }); describe('releaseName', () => { it('should release a reserved name', async () => { const participantName = 'Participant'; // Reserve a name await participantNameService.reserveUniqueName(testRoomId, participantName); expect(await participantNameService.isNameReserved(testRoomId, participantName)).toBe(true); // Release the name await participantNameService.releaseName(testRoomId, participantName); expect(await participantNameService.isNameReserved(testRoomId, participantName)).toBe(false); }); it('should allow reusing a released name', async () => { const participantName = 'Frank'; // Reserve, release, and reserve again await participantNameService.reserveUniqueName(testRoomId, participantName); await participantNameService.releaseName(testRoomId, participantName); const newReservation = await participantNameService.reserveUniqueName(testRoomId, participantName); expect(newReservation).toBe(participantName); }); }); describe('getReservedNames', () => { it('should return all reserved names in a room in lowercase', async () => { const names = ['Grace', 'Henry', 'Iris']; // Reserve multiple names for (const name of names) { await participantNameService.reserveUniqueName(testRoomId, name); } const reservedNames = await participantNameService.getReservedNames(testRoomId); for (const name of names) { expect(reservedNames).toContain(name.toLowerCase()); } }); it('should return empty array when no names are reserved', async () => { const reservedNames = await participantNameService.getReservedNames(testRoomId); expect(reservedNames).toEqual([]); }); }); describe('Reserve unique participant name - edge cases', () => { it('should be able to reserve same 20 names', async () => { const requestedName = 'LimitTest'; const promises: Promise[] = []; const twentyNames = participantNameService['MAX_CONCURRENT_NAME_REQUESTS']; for (let i = 0; i <= twentyNames; i++) { promises.push(participantNameService.reserveUniqueName(testRoomId, requestedName)); } // Los primeros MAX_RETRIES deben resolverse bien const results = await Promise.allSettled(promises); const fulfilled = results.filter((r) => r.status === 'fulfilled') as PromiseFulfilledResult[]; const rejected = results.filter((r) => r.status === 'rejected') as PromiseRejectedResult[]; console.log(fulfilled); expect(fulfilled.length).toBe(twentyNames + 1); // +1 for the original name expect(rejected.length).toBe(0); }); it('should handle race condition when reusing released numbers', async () => { const requestedName = 'RaceTest'; // Try to reserve two names const n1 = await participantNameService.reserveUniqueName(testRoomId, requestedName); const n2 = await participantNameService.reserveUniqueName(testRoomId, requestedName); expect([n1, n2]).toEqual(['RaceTest', 'RaceTest_1']); // Release _1 await participantNameService.releaseName(testRoomId, n2); // Try to reserve again concurrently const [c1, c2] = await Promise.all([ participantNameService.reserveUniqueName(testRoomId, requestedName), participantNameService.reserveUniqueName(testRoomId, requestedName) ]); // One of them should be _1 and the other should be _2 expect([c1, c2].sort()).toEqual(['RaceTest_1', 'RaceTest_2']); }); it('should reuse expired names after TTL', async () => { (participantNameService as any)['PARTICIPANT_NAME_TTL'] = ms('1ms'); const requestedName = 'TTLTest'; // Reserva con TTL muy corto (simulado) const name = await participantNameService.reserveUniqueName(testRoomId, requestedName); expect(name).toBe('TTLTest'); // Wait for TTL to expire await new Promise((resolve) => setTimeout(resolve, (participantNameService['PARTICIPANT_NAME_TTL'] + 1) * 1000) ); // Try to reserve again const newName = await participantNameService.reserveUniqueName(testRoomId, requestedName); expect(newName).toBe('TTLTest'); // Reuse original name }); it('should keep names isolated per room', async () => { const requestedName = 'Isolated'; // Reserve in two different rooms const room1Name = await participantNameService.reserveUniqueName('room1', requestedName); const room2Name = await participantNameService.reserveUniqueName('room2', requestedName); // Both names should be isolated expect(room1Name).toBe(requestedName); expect(room2Name).toBe(requestedName); }); it('should treat names case-insensitively if normalization is enabled', async () => { const requestedName = 'CaseTest'; const n1 = await participantNameService.reserveUniqueName(testRoomId, requestedName); expect(n1).toBe('CaseTest'); // Try to reserve with different casing const n2 = await participantNameService.reserveUniqueName(testRoomId, 'casetest'); expect(n2).toBe('casetest_1'); // Should return alternative name with original case }); }); });