/** * BLE Manager Bulk Operations Unit Tests * Tests for bulkDisconnect, bulkReboot, and bulkSetWiFi */ import { MockBLEManager } from '../MockBLEManager'; import { BLEConnectionState } from '../types'; describe('BLEManager Bulk Operations', () => { let manager: MockBLEManager; beforeEach(() => { manager = new MockBLEManager(); }); afterEach(async () => { await manager.cleanup(); }); describe('bulkDisconnect', () => { it('should disconnect multiple devices', async () => { // Connect devices first await manager.connectDevice('mock-743'); await manager.connectDevice('mock-769'); expect(manager.isDeviceConnected('mock-743')).toBe(true); expect(manager.isDeviceConnected('mock-769')).toBe(true); // Bulk disconnect const results = await manager.bulkDisconnect(['mock-743', 'mock-769']); expect(results).toHaveLength(2); expect(results[0].success).toBe(true); expect(results[1].success).toBe(true); expect(manager.isDeviceConnected('mock-743')).toBe(false); expect(manager.isDeviceConnected('mock-769')).toBe(false); }); it('should return results with device info', async () => { await manager.connectDevice('mock-743'); const results = await manager.bulkDisconnect(['mock-743']); expect(results[0]).toEqual({ deviceId: 'mock-743', deviceName: 'WP_497_81a14c', success: true, }); }); it('should handle empty device list', async () => { const results = await manager.bulkDisconnect([]); expect(results).toEqual([]); }); it('should handle non-connected devices gracefully', async () => { const results = await manager.bulkDisconnect(['non-existent-device']); expect(results).toHaveLength(1); expect(results[0].success).toBe(true); // Mock doesn't throw for non-connected expect(results[0].deviceId).toBe('non-existent-device'); }); it('should continue processing after individual failure', async () => { await manager.connectDevice('mock-743'); await manager.connectDevice('mock-769'); // Disconnect all const results = await manager.bulkDisconnect(['mock-743', 'mock-769']); // All should have been processed expect(results).toHaveLength(2); }); }); describe('bulkReboot', () => { it('should reboot multiple devices', async () => { await manager.connectDevice('mock-743'); await manager.connectDevice('mock-769'); const results = await manager.bulkReboot(['mock-743', 'mock-769']); expect(results).toHaveLength(2); expect(results[0].success).toBe(true); expect(results[1].success).toBe(true); }); it('should return results with device names', async () => { await manager.connectDevice('mock-743'); const results = await manager.bulkReboot(['mock-743']); expect(results[0].deviceId).toBe('mock-743'); expect(results[0].deviceName).toBe('WP_497_81a14c'); expect(results[0].success).toBe(true); }); it('should disconnect devices after reboot', async () => { await manager.connectDevice('mock-743'); expect(manager.isDeviceConnected('mock-743')).toBe(true); await manager.bulkReboot(['mock-743']); expect(manager.isDeviceConnected('mock-743')).toBe(false); }); it('should handle empty device list', async () => { const results = await manager.bulkReboot([]); expect(results).toEqual([]); }); }); describe('bulkSetWiFi', () => { const devices = [ { id: 'mock-743', name: 'WP_497_81a14c' }, { id: 'mock-769', name: 'WP_523_81aad4' }, ]; const ssid = 'TestNetwork'; const password = 'testpass123'; // BulkSetWiFi has delays: connect (800ms) + setWiFi (1200ms) + reboot (600ms) = ~2.6s per device // For 2 devices: ~5.2s, so we need 15s timeout to be safe jest.setTimeout(15000); it('should configure WiFi on multiple devices', async () => { const results = await manager.bulkSetWiFi(devices, ssid, password); expect(results).toHaveLength(2); expect(results[0].success).toBe(true); expect(results[1].success).toBe(true); }, 15000); it('should return detailed results', async () => { const results = await manager.bulkSetWiFi(devices.slice(0, 1), ssid, password); expect(results[0]).toEqual({ deviceId: 'mock-743', deviceName: 'WP_497_81a14c', success: true, }); }, 10000); it('should call progress callback for each stage', async () => { const progressCalls: Array<{ deviceId: string; status: string; error?: string; }> = []; const onProgress = ( deviceId: string, status: 'connecting' | 'configuring' | 'rebooting' | 'success' | 'error', error?: string ) => { progressCalls.push({ deviceId, status, error }); }; await manager.bulkSetWiFi(devices.slice(0, 1), ssid, password, onProgress); // Should have: connecting, configuring, rebooting, success expect(progressCalls).toHaveLength(4); expect(progressCalls[0].status).toBe('connecting'); expect(progressCalls[1].status).toBe('configuring'); expect(progressCalls[2].status).toBe('rebooting'); expect(progressCalls[3].status).toBe('success'); }, 10000); it('should handle empty device list', async () => { const results = await manager.bulkSetWiFi([], ssid, password); expect(results).toEqual([]); }); it('should handle wrong password', async () => { const results = await manager.bulkSetWiFi( [{ id: 'mock-743', name: 'WP_497' }], ssid, 'wrongpass' // Mock treats 'wrongpass' as incorrect ); expect(results[0].success).toBe(false); expect(results[0].error).toBeDefined(); }, 10000); it('should call error progress callback on failure', async () => { const progressCalls: string[] = []; const onProgress = ( _deviceId: string, status: string, _error?: string ) => { progressCalls.push(status); }; await manager.bulkSetWiFi( [{ id: 'mock-743', name: 'WP_497' }], ssid, 'wrongpass', onProgress ); expect(progressCalls).toContain('error'); }, 10000); it('should continue after one device fails', async () => { // First device will fail (wrong password), second should still process const mixedDevices = [ { id: 'fail-device', name: 'FailDevice' }, { id: 'mock-769', name: 'WP_523_81aad4' }, ]; const results = await manager.bulkSetWiFi( mixedDevices, 'OfflineNetwork', // Mock treats 'offline' SSIDs as failures password ); expect(results).toHaveLength(2); // Both should have been processed (not short-circuited) }, 15000); it('should validate WiFi credentials', async () => { // Invalid SSID with pipe character const results = await manager.bulkSetWiFi( [{ id: 'mock-743', name: 'WP_497' }], 'Invalid|SSID', password ); expect(results[0].success).toBe(false); expect(results[0].error).toContain('invalid character'); }, 10000); it('should validate password length', async () => { // Password too short (< 8 chars) const results = await manager.bulkSetWiFi( [{ id: 'mock-743', name: 'WP_497' }], ssid, 'short' ); expect(results[0].success).toBe(false); // Mock returns generic invalid credentials error for short password expect(results[0].error).toContain('invalid'); }, 10000); it('should reboot devices after successful WiFi config', async () => { await manager.connectDevice('mock-743'); expect(manager.isDeviceConnected('mock-743')).toBe(true); await manager.bulkSetWiFi(devices.slice(0, 1), ssid, password); // Device should be disconnected after reboot expect(manager.isDeviceConnected('mock-743')).toBe(false); }, 10000); }); describe('Bulk Operations - Concurrency', () => { it('should handle parallel bulk operations', async () => { // Connect devices await Promise.all([ manager.connectDevice('mock-743'), manager.connectDevice('mock-769'), ]); // Run bulk disconnect const disconnectResults = await manager.bulkDisconnect(['mock-743', 'mock-769']); expect(disconnectResults.every(r => r.success)).toBe(true); }, 10000); it('should track all results independently', async () => { const devices = [ { id: 'mock-743', name: 'WP_497' }, { id: 'mock-769', name: 'WP_523' }, ]; const results = await manager.bulkSetWiFi(devices, 'Network', 'password123'); // Each device should have its own result const device1Result = results.find(r => r.deviceId === 'mock-743'); const device2Result = results.find(r => r.deviceId === 'mock-769'); expect(device1Result).toBeDefined(); expect(device2Result).toBeDefined(); expect(device1Result?.deviceId).not.toBe(device2Result?.deviceId); }, 15000); }); describe('Bulk Operations - Edge Cases', () => { it('should handle duplicate device IDs', async () => { await manager.connectDevice('mock-743'); const results = await manager.bulkDisconnect(['mock-743', 'mock-743']); // Both should be processed (first disconnects, second is already disconnected) expect(results).toHaveLength(2); }); it('should handle mixed valid and invalid devices', async () => { const devices = [ { id: 'mock-743', name: 'WP_497' }, { id: 'invalid-device', name: 'Invalid' }, ]; const results = await manager.bulkSetWiFi( devices, 'TestNetwork', 'password123' ); expect(results).toHaveLength(2); }, 15000); it('should handle multiple device lists', async () => { // Reduced from 10 to 3 devices to avoid timeout const manyDevices = Array.from({ length: 3 }, (_, i) => ({ id: `device-${i}`, name: `WP_${i}`, })); const results = await manager.bulkSetWiFi( manyDevices, 'Network', 'password123' ); expect(results).toHaveLength(3); }, 30000); }); });