/** * Tests for BLE Concurrent Connection Protection */ import { MockBLEManager } from '../MockBLEManager'; import { BLEConnectionState } from '../types'; describe('BLE Concurrent Connection Protection', () => { let manager: MockBLEManager; beforeEach(() => { manager = new MockBLEManager(); }); afterEach(async () => { await manager.cleanup(); }); describe('Concurrent Connection Prevention', () => { it('should prevent concurrent connection attempts to the same device', async () => { const deviceId = 'test-device'; // Start two connection attempts simultaneously const connection1Promise = manager.connectDevice(deviceId); const connection2Promise = manager.connectDevice(deviceId); // Wait for both to complete const [result1, result2] = await Promise.all([connection1Promise, connection2Promise]); // One should succeed, one should fail expect(result1 !== result2).toBe(true); // The device should end up connected (from the successful attempt) const finalState = manager.getConnectionState(deviceId); expect([BLEConnectionState.READY, BLEConnectionState.ERROR]).toContain(finalState); }); it('should allow reconnection after first connection completes', async () => { const deviceId = 'test-device'; // First connection const result1 = await manager.connectDevice(deviceId); expect(result1).toBe(true); expect(manager.getConnectionState(deviceId)).toBe(BLEConnectionState.READY); // Disconnect await manager.disconnectDevice(deviceId); expect(manager.getConnectionState(deviceId)).toBe(BLEConnectionState.DISCONNECTED); // Second connection should succeed const result2 = await manager.connectDevice(deviceId); expect(result2).toBe(true); expect(manager.getConnectionState(deviceId)).toBe(BLEConnectionState.READY); }); it('should handle multiple concurrent connections to different devices', async () => { const deviceIds = ['device-1', 'device-2', 'device-3']; // Connect to all devices simultaneously const connectionPromises = deviceIds.map(id => manager.connectDevice(id)); const results = await Promise.all(connectionPromises); // All connections should succeed expect(results).toEqual([true, true, true]); // All devices should be in READY state deviceIds.forEach(id => { expect(manager.getConnectionState(id)).toBe(BLEConnectionState.READY); }); }); it('should allow connection if device is already connected', async () => { const deviceId = 'test-device'; // First connection const result1 = await manager.connectDevice(deviceId); expect(result1).toBe(true); expect(manager.getConnectionState(deviceId)).toBe(BLEConnectionState.READY); // Second connection attempt while already connected should return true const result2 = await manager.connectDevice(deviceId); expect(result2).toBe(true); expect(manager.getConnectionState(deviceId)).toBe(BLEConnectionState.READY); }); it('should emit connection_failed event for concurrent connection attempt', async () => { const deviceId = 'test-device'; const events: Array<{ event: string; error?: string }> = []; const listener = (id: string, event: string, data?: any) => { if (id === deviceId) { events.push({ event, error: data?.error }); } }; manager.addEventListener(listener); // Start two connection attempts simultaneously await Promise.all([ manager.connectDevice(deviceId), manager.connectDevice(deviceId), ]); // Should have at least one connection_failed event const failedEvents = events.filter(e => e.event === 'connection_failed'); expect(failedEvents.length).toBeGreaterThanOrEqual(1); // The error should mention concurrent connection const concurrentError = failedEvents.find(e => e.error?.includes('Already trying to connect') || e.error?.includes('Connection in Progress') ); expect(concurrentError).toBeDefined(); }); }); describe('Connection State During Concurrent Attempts', () => { it('should maintain consistent state during concurrent attempts', async () => { const deviceId = 'test-device'; const states: BLEConnectionState[] = []; const listener = (id: string, event: string, data?: any) => { if (id === deviceId && event === 'state_changed') { states.push(data.state); } }; manager.addEventListener(listener); // Start concurrent connections await Promise.all([ manager.connectDevice(deviceId), manager.connectDevice(deviceId), ]); // States should follow valid transitions // Should not have duplicate CONNECTING states (only one should enter) const connectingCount = states.filter(s => s === BLEConnectionState.CONNECTING).length; expect(connectingCount).toBeLessThanOrEqual(2); // Max 2: one success, one immediate error // Final state should be either READY or ERROR const finalState = manager.getConnectionState(deviceId); expect([BLEConnectionState.READY, BLEConnectionState.ERROR]).toContain(finalState); }); it('should track connection attempts correctly with getAllConnections', async () => { const deviceId = 'test-device'; // Start concurrent connections await Promise.all([ manager.connectDevice(deviceId), manager.connectDevice(deviceId), ]); const connections = manager.getAllConnections(); const deviceConnection = connections.get(deviceId); expect(deviceConnection).toBeDefined(); expect(deviceConnection?.deviceId).toBe(deviceId); expect([BLEConnectionState.READY, BLEConnectionState.ERROR]).toContain(deviceConnection?.state); }); }); describe('Cleanup During Connection', () => { it('should properly cleanup concurrent connection attempts', async () => { const deviceIds = ['device-1', 'device-2', 'device-3']; // Start connections const connectionPromises = deviceIds.map(id => manager.connectDevice(id)); // Don't wait for connections, cleanup immediately await manager.cleanup(); // All connections should be cleaned up const connections = manager.getAllConnections(); expect(connections.size).toBe(0); // All devices should be disconnected deviceIds.forEach(id => { expect(manager.getConnectionState(id)).toBe(BLEConnectionState.DISCONNECTED); }); }); it('should clear connecting devices set on cleanup', async () => { const deviceId = 'test-device'; // Start connection (but don't await) const connectionPromise = manager.connectDevice(deviceId); // Cleanup before connection completes await manager.cleanup(); // Try to connect again after cleanup - should work const result = await manager.connectDevice(deviceId); expect(result).toBe(true); expect(manager.getConnectionState(deviceId)).toBe(BLEConnectionState.READY); }); }); describe('Edge Cases', () => { it('should handle rapid connect/disconnect/connect cycles', async () => { const deviceId = 'test-device'; // Connect await manager.connectDevice(deviceId); expect(manager.getConnectionState(deviceId)).toBe(BLEConnectionState.READY); // Disconnect await manager.disconnectDevice(deviceId); expect(manager.getConnectionState(deviceId)).toBe(BLEConnectionState.DISCONNECTED); // Reconnect immediately const result = await manager.connectDevice(deviceId); expect(result).toBe(true); expect(manager.getConnectionState(deviceId)).toBe(BLEConnectionState.READY); }); it('should handle connection attempt during disconnection', async () => { const deviceId = 'test-device'; // Connect first await manager.connectDevice(deviceId); expect(manager.getConnectionState(deviceId)).toBe(BLEConnectionState.READY); // Start disconnection (don't await) const disconnectPromise = manager.disconnectDevice(deviceId); // Try to connect while disconnecting const connectResult = await manager.connectDevice(deviceId); // Wait for disconnect to complete await disconnectPromise; // Connection attempt should have a defined result expect(typeof connectResult).toBe('boolean'); }); it('should handle multiple rapid concurrent attempts', async () => { const deviceId = 'test-device'; // Start 5 concurrent connection attempts const attempts = Array(5).fill(null).map(() => manager.connectDevice(deviceId)); const results = await Promise.all(attempts); // At least one should succeed or all should fail consistently const successCount = results.filter(r => r === true).length; const failureCount = results.filter(r => r === false).length; expect(successCount + failureCount).toBe(5); // If any succeeded, device should be connected if (successCount > 0) { expect(manager.getConnectionState(deviceId)).toBe(BLEConnectionState.READY); } }); }); });