"""Unit tests for PM3DeviceManager.""" import pytest import asyncio from unittest.mock import Mock, patch, AsyncMock, MagicMock from datetime import datetime from app.backend.managers.pm3_device_manager import ( PM3DeviceManager, PM3Device, DeviceStatus, PM3FirmwareInfo ) @pytest.fixture def device_manager(): """Create a PM3DeviceManager instance.""" return PM3DeviceManager() @pytest.fixture def mock_device(): """Create a mock PM3Device.""" return PM3Device( device_id="pm3_test123", device_path="/dev/ttyACM0", serial_number="ABC123", friendly_name="Test PM3", usb_vid="9ac4", usb_pid="4b8f", status=DeviceStatus.CONNECTED ) class TestPM3DeviceManager: """Tests for PM3DeviceManager class.""" def test_initialization(self, device_manager): """Test device manager initialization.""" assert device_manager is not None assert device_manager.auto_discover is True assert device_manager.discovery_interval == 30 assert len(device_manager._devices) == 0 def test_generate_device_id(self, device_manager): """Test device ID generation.""" device_id = device_manager._generate_device_id("/dev/ttyACM0") assert device_id.startswith("pm3_") assert len(device_id) == 12 # pm3_ + 8 char hash # Same path should generate same ID device_id2 = device_manager._generate_device_id("/dev/ttyACM0") assert device_id == device_id2 # Different path should generate different ID device_id3 = device_manager._generate_device_id("/dev/ttyACM1") assert device_id != device_id3 def test_generate_device_id_with_serial(self, device_manager): """Test device ID generation with serial number.""" device_id = device_manager._generate_device_id("/dev/ttyACM0", serial="ABC123") assert device_id.startswith("pm3_") # Same serial should generate same ID even with different path device_id2 = device_manager._generate_device_id("/dev/ttyACM1", serial="ABC123") assert device_id == device_id2 @pytest.mark.asyncio async def test_get_device(self, device_manager, mock_device): """Test getting device by ID.""" # Add device to manager device_manager._devices[mock_device.device_id] = mock_device # Get existing device device = await device_manager.get_device(mock_device.device_id) assert device is not None assert device.device_id == mock_device.device_id # Get non-existent device device = await device_manager.get_device("nonexistent") assert device is None @pytest.mark.asyncio async def test_get_all_devices(self, device_manager, mock_device): """Test getting all devices.""" # Initially empty devices = await device_manager.get_all_devices() assert len(devices) == 0 # Add device device_manager._devices[mock_device.device_id] = mock_device # Get all devices devices = await device_manager.get_all_devices() assert len(devices) == 1 assert devices[0].device_id == mock_device.device_id @pytest.mark.asyncio async def test_get_available_devices(self, device_manager): """Test getting available (not in use) devices.""" # Create devices with different statuses device1 = PM3Device( device_id="pm3_1", device_path="/dev/ttyACM0", status=DeviceStatus.CONNECTED ) device2 = PM3Device( device_id="pm3_2", device_path="/dev/ttyACM1", status=DeviceStatus.IN_USE ) device3 = PM3Device( device_id="pm3_3", device_path="/dev/ttyACM2", status=DeviceStatus.DISCONNECTED ) device_manager._devices = { device1.device_id: device1, device2.device_id: device2, device3.device_id: device3, } # Get available devices (only CONNECTED) available = await device_manager.get_available_devices() assert len(available) == 1 assert available[0].device_id == device1.device_id @pytest.mark.asyncio async def test_get_connected_devices(self, device_manager): """Test getting connected devices.""" # Create devices with different statuses device1 = PM3Device( device_id="pm3_1", device_path="/dev/ttyACM0", status=DeviceStatus.CONNECTED ) device2 = PM3Device( device_id="pm3_2", device_path="/dev/ttyACM1", status=DeviceStatus.IN_USE ) device3 = PM3Device( device_id="pm3_3", device_path="/dev/ttyACM2", status=DeviceStatus.DISCONNECTED ) device_manager._devices = { device1.device_id: device1, device2.device_id: device2, device3.device_id: device3, } # Get connected devices (CONNECTED + IN_USE, not DISCONNECTED) connected = await device_manager.get_connected_devices() assert len(connected) == 2 assert device1 in connected assert device2 in connected assert device3 not in connected @pytest.mark.asyncio async def test_set_device_status(self, device_manager, mock_device): """Test setting device status.""" device_manager._devices[mock_device.device_id] = mock_device # Set status result = await device_manager.set_device_status( mock_device.device_id, DeviceStatus.IN_USE ) assert result is True assert mock_device.status == DeviceStatus.IN_USE # Set status for non-existent device result = await device_manager.set_device_status( "nonexistent", DeviceStatus.IN_USE ) assert result is False def test_parse_firmware_version(self, device_manager): """Test parsing firmware version from hw version output.""" output = """ Proxmark3 RFID instrument bootrom: RRG/Iceman/master/v4.14831 os: RRG/Iceman/master/v4.14831 client: RRG/Iceman/master/v4.14831 """ info = device_manager._parse_firmware_version(output) assert info.bootrom_version == "v4.14831" assert info.os_version == "v4.14831" assert info.client_version == "v4.14831" assert info.compatible is True assert info.needs_upgrade is False assert info.needs_downgrade is False def test_parse_firmware_version_mismatch(self, device_manager): """Test parsing mismatched firmware versions.""" output = """ Proxmark3 RFID instrument bootrom: RRG/Iceman/master/v4.14000 os: RRG/Iceman/master/v4.14000 client: RRG/Iceman/master/v4.14831 """ info = device_manager._parse_firmware_version(output) assert info.bootrom_version == "v4.14000" assert info.os_version == "v4.14000" assert info.client_version == "v4.14831" assert info.compatible is False assert info.needs_upgrade is True @pytest.mark.asyncio async def test_discover_via_dev(self, device_manager): """Test device discovery via /dev scanning.""" with patch('pathlib.Path.glob') as mock_glob: # Mock /dev/ttyACM* files mock_device1 = Mock() mock_device1.is_char_device.return_value = True mock_device1.__str__ = lambda x: "/dev/ttyACM0" 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_via_dev() assert len(devices) == 2 assert "/dev/ttyACM0" in devices assert "/dev/ttyACM1" in devices @pytest.mark.asyncio async def test_identify_device_not_found(self, device_manager): """Test identifying a device that doesn't exist.""" with pytest.raises(ValueError, match="Device .* not found"): await device_manager.identify_device("nonexistent") @pytest.mark.asyncio async def test_identify_device_no_worker(self, device_manager): """Test identifying a device without a worker.""" device = PM3Device( device_id="pm3_test", device_path="/dev/ttyACM0", status=DeviceStatus.CONNECTED, worker=None ) device_manager._devices[device.device_id] = device with pytest.raises(RuntimeError, match="has no worker"): await device_manager.identify_device(device.device_id) def test_device_to_dict(self, mock_device): """Test converting PM3Device to dictionary.""" data = mock_device.to_dict() assert data["device_id"] == mock_device.device_id assert data["device_path"] == mock_device.device_path assert data["serial_number"] == mock_device.serial_number assert data["friendly_name"] == mock_device.friendly_name assert data["usb_vid"] == mock_device.usb_vid assert data["usb_pid"] == mock_device.usb_pid assert data["status"] == mock_device.status.value assert data["connected"] is True assert "firmware_info" in data def test_device_status_enum(self): """Test DeviceStatus enum values.""" assert DeviceStatus.CONNECTED.value == "connected" assert DeviceStatus.DISCONNECTED.value == "disconnected" assert DeviceStatus.IN_USE.value == "in_use" assert DeviceStatus.ERROR.value == "error" assert DeviceStatus.VERSION_MISMATCH.value == "version_mismatch" assert DeviceStatus.FLASHING.value == "flashing" assert DeviceStatus.BOOTLOADER_MODE.value == "bootloader_mode" assert DeviceStatus.DISABLED.value == "disabled" class TestPM3FirmwareInfo: """Tests for PM3FirmwareInfo dataclass.""" def test_default_values(self): """Test default firmware info values.""" info = PM3FirmwareInfo() assert info.bootrom_version == "unknown" assert info.os_version == "unknown" assert info.client_version == "unknown" assert info.compatible is False assert info.needs_upgrade is False assert info.needs_downgrade is False assert info.bootloader_outdated is False def test_compatibility_check(self): """Test firmware compatibility logic.""" # Compatible versions info1 = PM3FirmwareInfo( bootrom_version="v4.14831", os_version="v4.14831", client_version="v4.14831" ) info1.compatible = ( info1.os_version == info1.client_version and info1.bootrom_version == info1.client_version ) assert info1.compatible is True # Incompatible versions info2 = PM3FirmwareInfo( bootrom_version="v4.14000", os_version="v4.14000", client_version="v4.14831" ) info2.compatible = ( info2.os_version == info2.client_version and info2.bootrom_version == info2.client_version ) assert info2.compatible is False @pytest.mark.asyncio async def test_device_manager_lifecycle(device_manager): """Test device manager start/stop lifecycle.""" # Start manager await device_manager.start() assert device_manager._monitor_task is not None # Stop manager await device_manager.stop() # Monitor task should be cancelled if __name__ == "__main__": pytest.main([__file__, "-v"])