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:
0
tests/integration/__init__.py
Normal file
0
tests/integration/__init__.py
Normal file
0
tests/integration/api/__init__.py
Normal file
0
tests/integration/api/__init__.py
Normal file
199
tests/integration/api/test_pm3_api.py
Normal file
199
tests/integration/api/test_pm3_api.py
Normal file
@@ -0,0 +1,199 @@
|
||||
"""Integration tests for PM3 API endpoints.
|
||||
|
||||
Tests the refactored PM3 API endpoints to ensure:
|
||||
- Proper integration with PM3Service
|
||||
- Correct HTTP status codes for different error types
|
||||
- Request/response format consistency
|
||||
- Session management integration
|
||||
"""
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from unittest.mock import patch, AsyncMock, Mock
|
||||
|
||||
from app.backend.services.pm3_service import PM3ServiceResult, PM3ServiceError
|
||||
|
||||
|
||||
class TestPM3APIStatus:
|
||||
"""Test /api/pm3/status endpoint."""
|
||||
|
||||
def test_get_status_success(self, test_client, mock_pm3_service):
|
||||
"""Test successful status retrieval."""
|
||||
# Arrange
|
||||
mock_pm3_service.get_status.return_value = PM3ServiceResult(
|
||||
success=True,
|
||||
data={
|
||||
"connected": True,
|
||||
"device_path": "/dev/ttyACM0",
|
||||
"version": "Proxmark3 v4.0",
|
||||
"session_active": False
|
||||
}
|
||||
)
|
||||
|
||||
# Act
|
||||
with patch('app.backend.api.pm3.container') as mock_container:
|
||||
mock_container.pm3_service = mock_pm3_service
|
||||
response = test_client.get("/api/pm3/status")
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["connected"] is True
|
||||
assert data["device"] == "/dev/ttyACM0"
|
||||
assert "Proxmark3" in data["version"]
|
||||
|
||||
def test_get_status_error(self, test_client, mock_pm3_service):
|
||||
"""Test status retrieval with error."""
|
||||
# Arrange
|
||||
mock_pm3_service.get_status.return_value = PM3ServiceResult(
|
||||
success=False,
|
||||
error=PM3ServiceError(
|
||||
code="status_error",
|
||||
message="Failed to query status"
|
||||
)
|
||||
)
|
||||
|
||||
# Act
|
||||
with patch('app.backend.api.pm3.container') as mock_container:
|
||||
mock_container.pm3_service = mock_pm3_service
|
||||
response = test_client.get("/api/pm3/status")
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 500
|
||||
assert "Failed to query status" in response.json()["detail"]
|
||||
|
||||
|
||||
class TestPM3APICommand:
|
||||
"""Test /api/pm3/command endpoint."""
|
||||
|
||||
def test_execute_command_success(self, test_client, mock_pm3_service):
|
||||
"""Test successful command execution."""
|
||||
# Arrange
|
||||
mock_pm3_service.execute_command.return_value = PM3ServiceResult(
|
||||
success=True,
|
||||
data={
|
||||
"output": "Proxmark3 RFID instrument",
|
||||
"command": "hw version",
|
||||
"session_id": "test-session"
|
||||
}
|
||||
)
|
||||
|
||||
# Act
|
||||
with patch('app.backend.api.pm3.container') as mock_container:
|
||||
mock_container.pm3_service = mock_pm3_service
|
||||
response = test_client.post(
|
||||
"/api/pm3/command",
|
||||
json={"command": "hw version", "session_id": "test-session"}
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert "Proxmark3" in data["output"]
|
||||
assert data["error"] is None
|
||||
|
||||
def test_execute_command_session_locked(self, test_client, mock_pm3_service):
|
||||
"""Test command execution with session locked."""
|
||||
# Arrange
|
||||
mock_pm3_service.execute_command.return_value = PM3ServiceResult(
|
||||
success=False,
|
||||
error=PM3ServiceError(
|
||||
code="session_locked",
|
||||
message="Another session is active"
|
||||
)
|
||||
)
|
||||
|
||||
# Act
|
||||
with patch('app.backend.api.pm3.container') as mock_container:
|
||||
mock_container.pm3_service = mock_pm3_service
|
||||
response = test_client.post(
|
||||
"/api/pm3/command",
|
||||
json={"command": "hw version", "session_id": "test-session"}
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 423 # Locked
|
||||
assert "Another session is active" in response.json()["detail"]
|
||||
|
||||
def test_execute_command_pm3_not_connected(self, test_client, mock_pm3_service):
|
||||
"""Test command execution when PM3 not connected."""
|
||||
# Arrange
|
||||
mock_pm3_service.execute_command.return_value = PM3ServiceResult(
|
||||
success=False,
|
||||
error=PM3ServiceError(
|
||||
code="pm3_not_connected",
|
||||
message="Proxmark3 not connected"
|
||||
)
|
||||
)
|
||||
|
||||
# Act
|
||||
with patch('app.backend.api.pm3.container') as mock_container:
|
||||
mock_container.pm3_service = mock_pm3_service
|
||||
response = test_client.post(
|
||||
"/api/pm3/command",
|
||||
json={"command": "hw version"}
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 200 # Returns in body, not HTTP error
|
||||
data = response.json()
|
||||
assert data["success"] is False
|
||||
assert "not connected" in data["error"].lower()
|
||||
|
||||
|
||||
class TestPM3APIConnection:
|
||||
"""Test PM3 connection endpoints."""
|
||||
|
||||
def test_connect_success(self, test_client, mock_pm3_service):
|
||||
"""Test successful PM3 connection."""
|
||||
# Arrange
|
||||
mock_pm3_service.connect.return_value = PM3ServiceResult(
|
||||
success=True,
|
||||
data={"message": "Connected to Proxmark3"}
|
||||
)
|
||||
|
||||
# Act
|
||||
with patch('app.backend.api.pm3.container') as mock_container:
|
||||
mock_container.pm3_service = mock_pm3_service
|
||||
response = test_client.post("/api/pm3/connect")
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 200
|
||||
assert response.json()["success"] is True
|
||||
|
||||
def test_connect_failure(self, test_client, mock_pm3_service):
|
||||
"""Test PM3 connection failure."""
|
||||
# Arrange
|
||||
mock_pm3_service.connect.return_value = PM3ServiceResult(
|
||||
success=False,
|
||||
error=PM3ServiceError(
|
||||
code="connection_error",
|
||||
message="Device not found"
|
||||
)
|
||||
)
|
||||
|
||||
# Act
|
||||
with patch('app.backend.api.pm3.container') as mock_container:
|
||||
mock_container.pm3_service = mock_pm3_service
|
||||
response = test_client.post("/api/pm3/connect")
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 503
|
||||
assert "Device not found" in response.json()["detail"]
|
||||
|
||||
def test_disconnect_success(self, test_client, mock_pm3_service):
|
||||
"""Test successful PM3 disconnection."""
|
||||
# Arrange
|
||||
mock_pm3_service.disconnect.return_value = PM3ServiceResult(
|
||||
success=True,
|
||||
data={"message": "Disconnected from Proxmark3"}
|
||||
)
|
||||
|
||||
# Act
|
||||
with patch('app.backend.api.pm3.container') as mock_container:
|
||||
mock_container.pm3_service = mock_pm3_service
|
||||
response = test_client.post("/api/pm3/disconnect")
|
||||
|
||||
# Assert
|
||||
assert response.status_code == 200
|
||||
assert response.json()["success"] is True
|
||||
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