Initial commit - Phase 3/4
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
210
tests/integration/test_multi_device_discovery.py
Normal file
210
tests/integration/test_multi_device_discovery.py
Normal file
@@ -0,0 +1,210 @@
|
||||
"""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"])
|
||||
Reference in New Issue
Block a user