"""Integration tests for multi-device discovery.""" import pytest import asyncio from unittest.mock import Mock, patch from app.backend.managers.pm3_device_manager import ( PM3DeviceManager, DeviceStatus ) from app.backend.managers.session_manager import SessionManager @pytest.fixture async def device_manager(): """Create and start device manager.""" manager = PM3DeviceManager() # Don't start monitoring for tests yield manager await manager.stop() @pytest.fixture def session_manager(): """Create session manager.""" return SessionManager() class TestMultiDeviceDiscovery: """Integration tests for device discovery.""" @pytest.mark.asyncio async def test_discover_no_devices(self, device_manager): """Test discovery when no devices are connected.""" with patch('app.backend.managers.pm3_device_manager.SERIAL_AVAILABLE', False): with patch('pathlib.Path.glob', return_value=[]): devices = await device_manager.discover_devices() assert len(devices) == 0 @pytest.mark.asyncio async def test_discover_single_device(self, device_manager): """Test discovering a single device.""" with patch('pathlib.Path.glob') as mock_glob: # Mock single device mock_device = Mock() mock_device.is_char_device.return_value = True mock_device.__str__ = lambda x: "/dev/ttyACM0" mock_glob.return_value = [mock_device] devices = await device_manager.discover_devices() assert len(devices) == 1 assert devices[0].device_path == "/dev/ttyACM0" assert devices[0].status == DeviceStatus.CONNECTED @pytest.mark.asyncio async def test_discover_multiple_devices(self, device_manager): """Test discovering multiple devices.""" with patch('pathlib.Path.glob') as mock_glob: # Mock multiple devices mock_devices = [] for i in range(3): mock_device = Mock() mock_device.is_char_device.return_value = True mock_device.__str__ = lambda x, i=i: f"/dev/ttyACM{i}" mock_devices.append(mock_device) mock_glob.return_value = mock_devices devices = await device_manager.discover_devices() assert len(devices) == 3 paths = [d.device_path for d in devices] assert "/dev/ttyACM0" in paths assert "/dev/ttyACM1" in paths assert "/dev/ttyACM2" in paths @pytest.mark.asyncio async def test_device_hotplug_simulation(self, device_manager): """Test device hotplug by simulating device connect/disconnect.""" with patch('pathlib.Path.glob') as mock_glob: # First discovery: 1 device mock_device1 = Mock() mock_device1.is_char_device.return_value = True mock_device1.__str__ = lambda x: "/dev/ttyACM0" mock_glob.return_value = [mock_device1] devices = await device_manager.discover_devices() assert len(devices) == 1 device1_id = devices[0].device_id # Second discovery: 2 devices (device plugged in) mock_device2 = Mock() mock_device2.is_char_device.return_value = True mock_device2.__str__ = lambda x: "/dev/ttyACM1" mock_glob.return_value = [mock_device1, mock_device2] devices = await device_manager.discover_devices() assert len(devices) == 2 # Third discovery: 1 device (device unplugged) mock_glob.return_value = [mock_device1] devices = await device_manager.discover_devices() # Still 2 devices tracked, but one is DISCONNECTED assert len(devices) == 2 connected = [d for d in devices if d.status == DeviceStatus.CONNECTED] disconnected = [d for d in devices if d.status == DeviceStatus.DISCONNECTED] assert len(connected) == 1 assert len(disconnected) == 1 @pytest.mark.asyncio async def test_device_persistence_across_discovery(self, device_manager): """Test that device objects persist across discovery runs.""" with patch('pathlib.Path.glob') as mock_glob: mock_device = Mock() mock_device.is_char_device.return_value = True mock_device.__str__ = lambda x: "/dev/ttyACM0" mock_glob.return_value = [mock_device] # First discovery devices1 = await device_manager.discover_devices() device_id = devices1[0].device_id # Second discovery devices2 = await device_manager.discover_devices() # Device ID should be the same assert len(devices2) == 1 assert devices2[0].device_id == device_id class TestMultiDeviceSessionIsolation: """Integration tests for session isolation across devices.""" @pytest.mark.asyncio async def test_single_session_per_device(self, device_manager, session_manager): """Test that each device can have its own session.""" # Note: This will be fully implemented in Sprint 2 # For now, just verify session manager exists assert session_manager is not None # TODO (Sprint 2): Test multiple devices with independent sessions class TestDeviceFiltering: """Integration tests for device filtering.""" @pytest.mark.asyncio async def test_filter_by_status(self, device_manager): """Test filtering devices by status.""" with patch('pathlib.Path.glob') as mock_glob: mock_devices = [] for i in range(3): mock_device = Mock() mock_device.is_char_device.return_value = True mock_device.__str__ = lambda x, i=i: f"/dev/ttyACM{i}" mock_devices.append(mock_device) mock_glob.return_value = mock_devices # Discover devices await device_manager.discover_devices() # Set different statuses devices = await device_manager.get_all_devices() await device_manager.set_device_status(devices[0].device_id, DeviceStatus.CONNECTED) await device_manager.set_device_status(devices[1].device_id, DeviceStatus.IN_USE) await device_manager.set_device_status(devices[2].device_id, DeviceStatus.DISCONNECTED) # Get available devices (only CONNECTED) available = await device_manager.get_available_devices() assert len(available) == 1 assert available[0].status == DeviceStatus.CONNECTED # Get connected devices (CONNECTED + IN_USE) connected = await device_manager.get_connected_devices() assert len(connected) == 2 @pytest.mark.hardware @pytest.mark.asyncio async def test_real_hardware_discovery(): """Test discovery with real hardware. This test requires actual Proxmark3 hardware connected. Run with: pytest -m hardware """ manager = PM3DeviceManager() try: devices = await manager.discover_devices() # If hardware is connected, verify it was found if devices: print(f"\nFound {len(devices)} PM3 device(s):") for device in devices: print(f" - {device.device_path} (ID: {device.device_id})") print(f" Status: {device.status.value}") print(f" VID:PID: {device.usb_vid}:{device.usb_pid}") else: print("\nNo PM3 devices found (this is OK for CI/CD)") finally: await manager.stop() if __name__ == "__main__": pytest.main([__file__, "-v"])