🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
211 lines
7.6 KiB
Python
211 lines
7.6 KiB
Python
"""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"])
|