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/unit/__init__.py
Normal file
0
tests/unit/__init__.py
Normal file
0
tests/unit/ble/__init__.py
Normal file
0
tests/unit/ble/__init__.py
Normal file
419
tests/unit/ble/test_gatt_server.py
Normal file
419
tests/unit/ble/test_gatt_server.py
Normal file
@@ -0,0 +1,419 @@
|
||||
"""Unit tests for BLE GATT Server.
|
||||
|
||||
Tests the GATT server implementation to ensure:
|
||||
- Proper delegation to service layer
|
||||
- Correct data format conversion (BLE ↔ Service)
|
||||
- Error handling and response formatting
|
||||
- Notification sending
|
||||
- ZERO business logic in GATT handlers (all in services!)
|
||||
"""
|
||||
import pytest
|
||||
import json
|
||||
from unittest.mock import Mock, AsyncMock, patch
|
||||
|
||||
from app.backend.ble.gatt_server import DangerousPiGATTServer
|
||||
from app.backend.ble.characteristics import PM3CharacteristicUUIDs, WiFiCharacteristicUUIDs
|
||||
from app.backend.services.pm3_service import PM3ServiceResult, PM3ServiceError
|
||||
|
||||
|
||||
class TestGATTServerInitialization:
|
||||
"""Test GATT server initialization."""
|
||||
|
||||
def test_initialization(self):
|
||||
"""Test GATT server initializes with all characteristics."""
|
||||
# Act
|
||||
server = DangerousPiGATTServer()
|
||||
|
||||
# Assert
|
||||
assert server.is_running is False
|
||||
characteristics = server.get_all_characteristics()
|
||||
assert len(characteristics) > 0
|
||||
|
||||
# Verify PM3 characteristics registered
|
||||
assert PM3CharacteristicUUIDs.COMMAND_WRITE in characteristics
|
||||
assert PM3CharacteristicUUIDs.STATUS in characteristics
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_stop(self):
|
||||
"""Test GATT server start and stop."""
|
||||
# Arrange
|
||||
server = DangerousPiGATTServer()
|
||||
|
||||
# Act
|
||||
await server.start()
|
||||
assert server.is_running is True
|
||||
|
||||
await server.stop()
|
||||
assert server.is_running is False
|
||||
|
||||
|
||||
class TestPM3GATTCharacteristics:
|
||||
"""Test PM3 GATT characteristic handlers.
|
||||
|
||||
These tests verify that handlers are THIN ADAPTERS that delegate
|
||||
to PM3Service with zero business logic.
|
||||
"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_command_write_success(self):
|
||||
"""Test PM3 command execution via BLE delegates to PM3Service."""
|
||||
# Arrange
|
||||
server = DangerousPiGATTServer()
|
||||
|
||||
# Mock PM3Service response
|
||||
mock_service_result = PM3ServiceResult(
|
||||
success=True,
|
||||
data={
|
||||
"output": "Proxmark3 RFID instrument",
|
||||
"command": "hw version",
|
||||
"session_id": "test-session"
|
||||
}
|
||||
)
|
||||
|
||||
# Mock notification callback
|
||||
notification_called = False
|
||||
notification_value = None
|
||||
|
||||
async def mock_notify(value):
|
||||
nonlocal notification_called, notification_value
|
||||
notification_called = True
|
||||
notification_value = value
|
||||
|
||||
server.register_notification_callback(
|
||||
PM3CharacteristicUUIDs.COMMAND_RESULT,
|
||||
mock_notify
|
||||
)
|
||||
|
||||
# Prepare BLE command
|
||||
command_data = {"command": "hw version", "session_id": "test-session"}
|
||||
command_bytes = json.dumps(command_data).encode('utf-8')
|
||||
|
||||
# Act - patch container and call handler
|
||||
with patch('app.backend.ble.gatt_server.container') as mock_container:
|
||||
mock_container.pm3_service.execute_command = AsyncMock(
|
||||
return_value=mock_service_result
|
||||
)
|
||||
|
||||
result = await server._handle_pm3_command_write(command_bytes)
|
||||
|
||||
# Verify service was called with correct params
|
||||
mock_container.pm3_service.execute_command.assert_called_once_with(
|
||||
command="hw version",
|
||||
session_id="test-session"
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result["success"] is True
|
||||
assert "Proxmark3" in result["output"]
|
||||
# Notification should have been sent
|
||||
assert notification_called is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_command_write_session_locked(self):
|
||||
"""Test PM3 command with session locked error from service."""
|
||||
# Arrange
|
||||
server = DangerousPiGATTServer()
|
||||
|
||||
mock_service_result = PM3ServiceResult(
|
||||
success=False,
|
||||
error=PM3ServiceError(
|
||||
code="session_locked",
|
||||
message="Another session is active"
|
||||
)
|
||||
)
|
||||
|
||||
command_data = {"command": "hw version", "session_id": "test-session"}
|
||||
command_bytes = json.dumps(command_data).encode('utf-8')
|
||||
|
||||
# Act
|
||||
with patch('app.backend.ble.gatt_server.container') as mock_container:
|
||||
mock_container.pm3_service.execute_command = AsyncMock(
|
||||
return_value=mock_service_result
|
||||
)
|
||||
|
||||
result = await server._handle_pm3_command_write(command_bytes)
|
||||
|
||||
# Assert
|
||||
assert result["success"] is False
|
||||
assert result["error"]["code"] == "session_locked"
|
||||
assert "Another session is active" in result["error"]["message"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_status_read_delegates_to_service(self):
|
||||
"""Test PM3 status read delegates to PM3Service."""
|
||||
# Arrange
|
||||
server = DangerousPiGATTServer()
|
||||
|
||||
mock_service_result = PM3ServiceResult(
|
||||
success=True,
|
||||
data={
|
||||
"connected": True,
|
||||
"device_path": "/dev/ttyACM0",
|
||||
"version": "Proxmark3 v4.0",
|
||||
"session_active": False
|
||||
}
|
||||
)
|
||||
|
||||
# Act
|
||||
with patch('app.backend.ble.gatt_server.container') as mock_container:
|
||||
mock_container.pm3_service.get_status = AsyncMock(
|
||||
return_value=mock_service_result
|
||||
)
|
||||
|
||||
result_bytes = await server._handle_pm3_status_read()
|
||||
result = json.loads(result_bytes.decode('utf-8'))
|
||||
|
||||
# Assert
|
||||
assert result["success"] is True
|
||||
assert result["connected"] is True
|
||||
assert result["device_path"] == "/dev/ttyACM0"
|
||||
# Verify service was called
|
||||
mock_container.pm3_service.get_status.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_create_delegates_to_service(self):
|
||||
"""Test session creation delegates to PM3Service."""
|
||||
# Arrange
|
||||
server = DangerousPiGATTServer()
|
||||
|
||||
mock_service_result = PM3ServiceResult(
|
||||
success=True,
|
||||
data={"session_id": "new-session-123"}
|
||||
)
|
||||
|
||||
request_data = {"force_takeover": False}
|
||||
request_bytes = json.dumps(request_data).encode('utf-8')
|
||||
|
||||
# Act
|
||||
with patch('app.backend.ble.gatt_server.container') as mock_container:
|
||||
mock_container.pm3_service.create_session = Mock(
|
||||
return_value=mock_service_result
|
||||
)
|
||||
|
||||
result = await server._handle_session_create_write(request_bytes)
|
||||
|
||||
# Assert
|
||||
assert result["success"] is True
|
||||
assert result["session_id"] == "new-session-123"
|
||||
mock_container.pm3_service.create_session.assert_called_once_with(
|
||||
force_takeover=False
|
||||
)
|
||||
|
||||
|
||||
class TestWiFiGATTCharacteristics:
|
||||
"""Test WiFi GATT characteristic handlers."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wifi_status_read_delegates_to_service(self):
|
||||
"""Test WiFi status read delegates to WiFiService."""
|
||||
# Arrange
|
||||
server = DangerousPiGATTServer()
|
||||
|
||||
from app.backend.services.pm3_service import PM3ServiceResult
|
||||
|
||||
mock_service_result = PM3ServiceResult(
|
||||
success=True,
|
||||
data={
|
||||
"mode": "ap",
|
||||
"interfaces": [],
|
||||
"client": None,
|
||||
"access_point": {"ssid": "dangerous-pi", "ip": "10.3.141.1"},
|
||||
"supports_dual": False
|
||||
}
|
||||
)
|
||||
|
||||
# Act
|
||||
with patch('app.backend.ble.gatt_server.container') as mock_container:
|
||||
mock_container.wifi_service.get_status = AsyncMock(
|
||||
return_value=mock_service_result
|
||||
)
|
||||
|
||||
result_bytes = await server._handle_wifi_status_read()
|
||||
result = json.loads(result_bytes.decode('utf-8'))
|
||||
|
||||
# Assert
|
||||
assert result["mode"] == "ap"
|
||||
assert result["access_point"]["ssid"] == "dangerous-pi"
|
||||
mock_container.wifi_service.get_status.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wifi_connect_delegates_to_service(self):
|
||||
"""Test WiFi connect delegates to WiFiService."""
|
||||
# Arrange
|
||||
server = DangerousPiGATTServer()
|
||||
|
||||
mock_service_result = PM3ServiceResult(
|
||||
success=True,
|
||||
data={
|
||||
"message": "Connected to TestNetwork",
|
||||
"ssid": "TestNetwork",
|
||||
"ip": "192.168.1.100"
|
||||
}
|
||||
)
|
||||
|
||||
request_data = {
|
||||
"ssid": "TestNetwork",
|
||||
"password": "testpass123",
|
||||
"hidden": False
|
||||
}
|
||||
request_bytes = json.dumps(request_data).encode('utf-8')
|
||||
|
||||
# Act
|
||||
with patch('app.backend.ble.gatt_server.container') as mock_container:
|
||||
mock_container.wifi_service.connect = AsyncMock(
|
||||
return_value=mock_service_result
|
||||
)
|
||||
|
||||
result = await server._handle_wifi_connect_write(request_bytes)
|
||||
|
||||
# Assert
|
||||
assert result["success"] is True
|
||||
assert result["ssid"] == "TestNetwork"
|
||||
assert result["ip"] == "192.168.1.100"
|
||||
|
||||
# Verify service was called with correct params
|
||||
mock_container.wifi_service.connect.assert_called_once_with(
|
||||
ssid="TestNetwork",
|
||||
password="testpass123",
|
||||
hidden=False
|
||||
)
|
||||
|
||||
|
||||
class TestSystemGATTCharacteristics:
|
||||
"""Test System GATT characteristic handlers."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_system_info_read_delegates_to_service(self):
|
||||
"""Test system info read delegates to SystemService."""
|
||||
# Arrange
|
||||
server = DangerousPiGATTServer()
|
||||
|
||||
mock_service_result = PM3ServiceResult(
|
||||
success=True,
|
||||
data={
|
||||
"hostname": "dangerous-pi",
|
||||
"platform": "Linux",
|
||||
"cpu": {"count": 4, "percent": 25.0, "temperature": 45.5},
|
||||
"memory": {"total": 4294967296, "used": 2147483648, "percent": 50.0},
|
||||
"disk": {"total": 32212254720, "used": 10737418240, "percent": 33.3},
|
||||
"uptime": 86400.0
|
||||
}
|
||||
)
|
||||
|
||||
# Act
|
||||
with patch('app.backend.ble.gatt_server.container') as mock_container:
|
||||
mock_container.system_service.get_info = AsyncMock(
|
||||
return_value=mock_service_result
|
||||
)
|
||||
|
||||
result_bytes = await server._handle_system_info_read()
|
||||
result = json.loads(result_bytes.decode('utf-8'))
|
||||
|
||||
# Assert
|
||||
assert result["hostname"] == "dangerous-pi"
|
||||
assert result["cpu"]["count"] == 4
|
||||
mock_container.system_service.get_info.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_shutdown_write_delegates_to_service(self):
|
||||
"""Test shutdown request delegates to SystemService."""
|
||||
# Arrange
|
||||
server = DangerousPiGATTServer()
|
||||
|
||||
mock_service_result = PM3ServiceResult(
|
||||
success=True,
|
||||
data={"message": "Shutdown initiated"}
|
||||
)
|
||||
|
||||
request_data = {"delay": 30}
|
||||
request_bytes = json.dumps(request_data).encode('utf-8')
|
||||
|
||||
# Act
|
||||
with patch('app.backend.ble.gatt_server.container') as mock_container:
|
||||
mock_container.system_service.shutdown = AsyncMock(
|
||||
return_value=mock_service_result
|
||||
)
|
||||
|
||||
result = await server._handle_system_shutdown_write(request_bytes)
|
||||
|
||||
# Assert
|
||||
assert result["success"] is True
|
||||
assert "Shutdown initiated" in result["message"]
|
||||
mock_container.system_service.shutdown.assert_called_once_with(delay=30)
|
||||
|
||||
|
||||
class TestGATTServerNotifications:
|
||||
"""Test notification system."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_notification_callback_registration(self):
|
||||
"""Test registering notification callbacks."""
|
||||
# Arrange
|
||||
server = DangerousPiGATTServer()
|
||||
callback_called = False
|
||||
callback_value = None
|
||||
|
||||
async def test_callback(value):
|
||||
nonlocal callback_called, callback_value
|
||||
callback_called = True
|
||||
callback_value = value
|
||||
|
||||
# Act
|
||||
server.register_notification_callback(
|
||||
PM3CharacteristicUUIDs.COMMAND_RESULT,
|
||||
test_callback
|
||||
)
|
||||
|
||||
await server._notify_characteristic(
|
||||
PM3CharacteristicUUIDs.COMMAND_RESULT,
|
||||
b"test notification"
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert callback_called is True
|
||||
assert callback_value == b"test notification"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_notification_without_callback(self):
|
||||
"""Test notification when no callback registered (should not error)."""
|
||||
# Arrange
|
||||
server = DangerousPiGATTServer()
|
||||
|
||||
# Act & Assert (should not raise)
|
||||
await server._notify_characteristic(
|
||||
"nonexistent-uuid",
|
||||
b"test"
|
||||
)
|
||||
|
||||
|
||||
class TestErrorHandling:
|
||||
"""Test error handling in GATT handlers."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invalid_json_in_command(self):
|
||||
"""Test handling of invalid JSON in command."""
|
||||
# Arrange
|
||||
server = DangerousPiGATTServer()
|
||||
invalid_json = b"not valid json{"
|
||||
|
||||
# Act
|
||||
result = await server._handle_pm3_command_write(invalid_json)
|
||||
|
||||
# Assert
|
||||
assert result["success"] is False
|
||||
assert result["error"]["code"] == "invalid_json"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_missing_required_field(self):
|
||||
"""Test handling of missing required field."""
|
||||
# Arrange
|
||||
server = DangerousPiGATTServer()
|
||||
request_data = {} # Missing "command" field
|
||||
request_bytes = json.dumps(request_data).encode('utf-8')
|
||||
|
||||
# Act
|
||||
result = await server._handle_pm3_command_write(request_bytes)
|
||||
|
||||
# Assert
|
||||
assert result["success"] is False
|
||||
assert result["error"]["code"] == "invalid_request"
|
||||
339
tests/unit/managers/test_pm3_device_manager.py
Normal file
339
tests/unit/managers/test_pm3_device_manager.py
Normal file
@@ -0,0 +1,339 @@
|
||||
"""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"])
|
||||
0
tests/unit/services/__init__.py
Normal file
0
tests/unit/services/__init__.py
Normal file
305
tests/unit/services/test_pm3_service.py
Normal file
305
tests/unit/services/test_pm3_service.py
Normal file
@@ -0,0 +1,305 @@
|
||||
"""Unit tests for PM3Service.
|
||||
|
||||
Tests the PM3Service business logic layer, ensuring:
|
||||
- Proper session validation
|
||||
- Command execution flow
|
||||
- Error handling and error codes
|
||||
- Response format consistency
|
||||
"""
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
from dataclasses import dataclass
|
||||
|
||||
from app.backend.services.pm3_service import PM3Service, PM3ServiceResult, PM3ServiceError
|
||||
|
||||
|
||||
@dataclass
|
||||
class MockPM3Result:
|
||||
"""Mock PM3Worker command result."""
|
||||
success: bool
|
||||
output: str = ""
|
||||
error: str = None
|
||||
|
||||
|
||||
class TestPM3ServiceCommandExecution:
|
||||
"""Test command execution with session validation."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_command_success(self, mock_pm3_worker, mock_session_manager):
|
||||
"""Test successful command execution."""
|
||||
# Arrange
|
||||
mock_pm3_worker.execute_command.return_value = MockPM3Result(
|
||||
success=True,
|
||||
output="Proxmark3 RFID instrument"
|
||||
)
|
||||
service = PM3Service(mock_pm3_worker, mock_session_manager)
|
||||
|
||||
# Act
|
||||
result = await service.execute_command(
|
||||
command="hw version",
|
||||
session_id="test-session"
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert result.data is not None
|
||||
assert result.data["output"] == "Proxmark3 RFID instrument"
|
||||
assert result.data["command"] == "hw version"
|
||||
assert result.data["session_id"] == "test-session"
|
||||
assert result.error is None
|
||||
|
||||
# Verify session was updated
|
||||
mock_session_manager.update_activity.assert_called_once_with("test-session")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_command_session_locked(self, mock_pm3_worker, mock_session_manager):
|
||||
"""Test command execution when another session is active."""
|
||||
# Arrange
|
||||
mock_session_manager.can_execute.return_value = False
|
||||
service = PM3Service(mock_pm3_worker, mock_session_manager)
|
||||
|
||||
# Act
|
||||
result = await service.execute_command(
|
||||
command="hw version",
|
||||
session_id="test-session"
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result.success is False
|
||||
assert result.error is not None
|
||||
assert result.error.code == "session_locked"
|
||||
assert "Another session is active" in result.error.message
|
||||
assert result.data is None
|
||||
|
||||
# Verify command was not executed
|
||||
mock_pm3_worker.execute_command.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_command_pm3_not_connected(self, mock_pm3_worker, mock_session_manager):
|
||||
"""Test command execution when PM3 is not connected."""
|
||||
# Arrange
|
||||
mock_pm3_worker.is_connected.return_value = False
|
||||
service = PM3Service(mock_pm3_worker, mock_session_manager)
|
||||
|
||||
# Act
|
||||
result = await service.execute_command(
|
||||
command="hw version",
|
||||
session_id="test-session"
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result.success is False
|
||||
assert result.error is not None
|
||||
assert result.error.code == "pm3_not_connected"
|
||||
assert "not connected" in result.error.message.lower()
|
||||
assert result.data is None
|
||||
|
||||
# Verify command was not executed
|
||||
mock_pm3_worker.execute_command.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_command_failure(self, mock_pm3_worker, mock_session_manager):
|
||||
"""Test command execution when PM3 command fails."""
|
||||
# Arrange
|
||||
mock_pm3_worker.execute_command.return_value = MockPM3Result(
|
||||
success=False,
|
||||
error="Invalid command"
|
||||
)
|
||||
service = PM3Service(mock_pm3_worker, mock_session_manager)
|
||||
|
||||
# Act
|
||||
result = await service.execute_command(
|
||||
command="invalid",
|
||||
session_id="test-session"
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result.success is False
|
||||
assert result.error is not None
|
||||
assert result.error.code == "command_failed"
|
||||
assert "Invalid command" in result.error.details
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_command_exception_handling(self, mock_pm3_worker, mock_session_manager):
|
||||
"""Test command execution when an exception occurs."""
|
||||
# Arrange
|
||||
mock_pm3_worker.execute_command.side_effect = Exception("Connection lost")
|
||||
service = PM3Service(mock_pm3_worker, mock_session_manager)
|
||||
|
||||
# Act
|
||||
result = await service.execute_command(
|
||||
command="hw version",
|
||||
session_id="test-session"
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result.success is False
|
||||
assert result.error is not None
|
||||
assert result.error.code == "execution_error"
|
||||
assert "Connection lost" in result.error.details
|
||||
|
||||
|
||||
class TestPM3ServiceStatus:
|
||||
"""Test status query operations."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_status_connected(self, mock_pm3_worker, mock_session_manager):
|
||||
"""Test status query when PM3 is connected."""
|
||||
# Arrange
|
||||
mock_pm3_worker.execute_command.return_value = MockPM3Result(
|
||||
success=True,
|
||||
output="Proxmark3 RFID instrument\nFirmware version: v4.0"
|
||||
)
|
||||
mock_session_manager.has_active_session.return_value = True
|
||||
service = PM3Service(mock_pm3_worker, mock_session_manager)
|
||||
|
||||
# Act
|
||||
result = await service.get_status()
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert result.data["connected"] is True
|
||||
assert result.data["device_path"] == "/dev/ttyACM0"
|
||||
assert "Proxmark3" in result.data["version"]
|
||||
assert result.data["session_active"] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_status_not_connected(self, mock_pm3_worker, mock_session_manager):
|
||||
"""Test status query when PM3 is not connected."""
|
||||
# Arrange
|
||||
mock_pm3_worker.is_connected.return_value = False
|
||||
service = PM3Service(mock_pm3_worker, mock_session_manager)
|
||||
|
||||
# Act
|
||||
result = await service.get_status()
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert result.data["connected"] is False
|
||||
assert result.data["version"] is None
|
||||
|
||||
|
||||
class TestPM3ServiceConnection:
|
||||
"""Test connection management."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_connect_success(self, mock_pm3_worker, mock_session_manager):
|
||||
"""Test successful PM3 connection."""
|
||||
# Arrange
|
||||
service = PM3Service(mock_pm3_worker, mock_session_manager)
|
||||
|
||||
# Act
|
||||
result = await service.connect()
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert "Connected" in result.data["message"]
|
||||
mock_pm3_worker.connect.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_connect_failure(self, mock_pm3_worker, mock_session_manager):
|
||||
"""Test PM3 connection failure."""
|
||||
# Arrange
|
||||
mock_pm3_worker.connect.side_effect = Exception("Device not found")
|
||||
service = PM3Service(mock_pm3_worker, mock_session_manager)
|
||||
|
||||
# Act
|
||||
result = await service.connect()
|
||||
|
||||
# Assert
|
||||
assert result.success is False
|
||||
assert result.error.code == "connection_error"
|
||||
assert "Device not found" in result.error.details
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_disconnect_success(self, mock_pm3_worker, mock_session_manager):
|
||||
"""Test successful PM3 disconnection."""
|
||||
# Arrange
|
||||
service = PM3Service(mock_pm3_worker, mock_session_manager)
|
||||
|
||||
# Act
|
||||
result = await service.disconnect()
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert "Disconnected" in result.data["message"]
|
||||
mock_pm3_worker.disconnect.assert_called_once()
|
||||
|
||||
|
||||
class TestPM3ServiceSessionManagement:
|
||||
"""Test session management operations."""
|
||||
|
||||
def test_create_session_success(self, mock_pm3_worker, mock_session_manager):
|
||||
"""Test successful session creation."""
|
||||
# Arrange
|
||||
service = PM3Service(mock_pm3_worker, mock_session_manager)
|
||||
|
||||
# Act
|
||||
result = service.create_session(force_takeover=False)
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert result.data["session_id"] == "test-session-id"
|
||||
mock_session_manager.create_session.assert_called_once_with(force_takeover=False)
|
||||
|
||||
def test_create_session_exists(self, mock_pm3_worker, mock_session_manager):
|
||||
"""Test session creation when session already exists."""
|
||||
# Arrange
|
||||
mock_session_manager.create_session.return_value = None
|
||||
service = PM3Service(mock_pm3_worker, mock_session_manager)
|
||||
|
||||
# Act
|
||||
result = service.create_session(force_takeover=False)
|
||||
|
||||
# Assert
|
||||
assert result.success is False
|
||||
assert result.error.code == "session_exists"
|
||||
assert "force_takeover" in result.error.details.lower()
|
||||
|
||||
def test_release_session(self, mock_pm3_worker, mock_session_manager):
|
||||
"""Test session release."""
|
||||
# Arrange
|
||||
service = PM3Service(mock_pm3_worker, mock_session_manager)
|
||||
|
||||
# Act
|
||||
result = service.release_session("test-session")
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert "released" in result.data["message"].lower()
|
||||
mock_session_manager.release_session.assert_called_once_with("test-session")
|
||||
|
||||
def test_get_session_info_found(self, mock_pm3_worker, mock_session_manager):
|
||||
"""Test getting session info for existing session."""
|
||||
# Arrange
|
||||
from datetime import datetime
|
||||
mock_session = Mock()
|
||||
mock_session.session_id = "test-session"
|
||||
mock_session.created_at = datetime(2025, 1, 1, 12, 0, 0)
|
||||
mock_session.last_activity = datetime(2025, 1, 1, 12, 5, 0)
|
||||
|
||||
mock_session_manager.get_session.return_value = mock_session
|
||||
mock_session_manager.is_session_active.return_value = True
|
||||
|
||||
service = PM3Service(mock_pm3_worker, mock_session_manager)
|
||||
|
||||
# Act
|
||||
result = service.get_session_info("test-session")
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert result.data["session_id"] == "test-session"
|
||||
assert result.data["is_active"] is True
|
||||
|
||||
def test_get_session_info_not_found(self, mock_pm3_worker, mock_session_manager):
|
||||
"""Test getting session info for non-existent session."""
|
||||
# Arrange
|
||||
mock_session_manager.get_session.return_value = None
|
||||
service = PM3Service(mock_pm3_worker, mock_session_manager)
|
||||
|
||||
# Act
|
||||
result = service.get_session_info("nonexistent")
|
||||
|
||||
# Assert
|
||||
assert result.success is False
|
||||
assert result.error.code == "session_not_found"
|
||||
300
tests/unit/services/test_system_service.py
Normal file
300
tests/unit/services/test_system_service.py
Normal file
@@ -0,0 +1,300 @@
|
||||
"""Unit tests for SystemService.
|
||||
|
||||
Tests the SystemService business logic layer, ensuring:
|
||||
- System information queries
|
||||
- Shutdown/restart operations
|
||||
- Error handling and error codes
|
||||
- Command execution and output parsing
|
||||
"""
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from app.backend.services.system_service import SystemService
|
||||
|
||||
|
||||
class TestSystemServiceInfo:
|
||||
"""Test system information queries."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_info_success(self):
|
||||
"""Test successful system info retrieval."""
|
||||
# Arrange
|
||||
service = SystemService()
|
||||
|
||||
# Act
|
||||
with patch('psutil.cpu_percent', return_value=25.5), \
|
||||
patch('psutil.virtual_memory') as mock_memory, \
|
||||
patch('psutil.disk_usage') as mock_disk, \
|
||||
patch('psutil.cpu_count', return_value=4), \
|
||||
patch('psutil.boot_time', return_value=1000000000.0), \
|
||||
patch('psutil.time.time', return_value=1000086400.0), \
|
||||
patch('platform.node', return_value='dangerous-pi'), \
|
||||
patch('platform.system', return_value='Linux'), \
|
||||
patch('platform.release', return_value='6.1.21'):
|
||||
|
||||
# Mock memory stats
|
||||
mock_memory.return_value = Mock(
|
||||
total=4294967296, # 4GB
|
||||
used=2147483648, # 2GB
|
||||
percent=50.0
|
||||
)
|
||||
|
||||
# Mock disk stats
|
||||
mock_disk.return_value = Mock(
|
||||
total=32212254720, # 30GB
|
||||
used=10737418240, # 10GB
|
||||
percent=33.3
|
||||
)
|
||||
|
||||
result = await service.get_info()
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert result.data["hostname"] == "dangerous-pi"
|
||||
assert result.data["platform"] == "Linux"
|
||||
assert result.data["cpu"]["count"] == 4
|
||||
assert result.data["cpu"]["percent"] == 25.5
|
||||
assert result.data["memory"]["total"] == 4294967296
|
||||
assert result.data["memory"]["used"] == 2147483648
|
||||
assert result.data["memory"]["percent"] == 50.0
|
||||
assert result.data["disk"]["total"] == 32212254720
|
||||
assert result.data["uptime"] == 86400.0 # 1 day
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_info_with_temperature(self):
|
||||
"""Test system info with CPU temperature."""
|
||||
# Arrange
|
||||
service = SystemService()
|
||||
|
||||
# Act
|
||||
with patch('psutil.cpu_percent', return_value=50.0), \
|
||||
patch('psutil.virtual_memory'), \
|
||||
patch('psutil.disk_usage'), \
|
||||
patch('psutil.cpu_count', return_value=4), \
|
||||
patch('psutil.boot_time', return_value=1000000000.0), \
|
||||
patch('psutil.time.time', return_value=1000000100.0), \
|
||||
patch('platform.node', return_value='test'), \
|
||||
patch('platform.system', return_value='Linux'), \
|
||||
patch('platform.release', return_value='6.1'), \
|
||||
patch('pathlib.Path.exists', return_value=True), \
|
||||
patch('pathlib.Path.read_text', return_value='45678'):
|
||||
|
||||
# Mock memory and disk
|
||||
patch('psutil.virtual_memory', return_value=Mock(total=1, used=1, percent=1))
|
||||
patch('psutil.disk_usage', return_value=Mock(total=1, used=1, percent=1))
|
||||
|
||||
result = await service.get_info()
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert result.data["cpu"]["temperature"] == 45.678
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_info_exception_handling(self):
|
||||
"""Test system info retrieval with exception."""
|
||||
# Arrange
|
||||
service = SystemService()
|
||||
|
||||
# Act
|
||||
with patch('psutil.cpu_percent', side_effect=Exception("psutil error")):
|
||||
result = await service.get_info()
|
||||
|
||||
# Assert
|
||||
assert result.success is False
|
||||
assert result.error.code == "system_info_error"
|
||||
assert "psutil error" in result.error.details
|
||||
|
||||
|
||||
class TestSystemServiceShutdown:
|
||||
"""Test shutdown operations."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_shutdown_immediate(self):
|
||||
"""Test immediate shutdown."""
|
||||
# Arrange
|
||||
service = SystemService()
|
||||
|
||||
# Act
|
||||
with patch.object(service, '_run_command', new_callable=AsyncMock) as mock_cmd:
|
||||
mock_cmd.return_value = ""
|
||||
result = await service.shutdown(delay=0)
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert "initiated" in result.data["message"].lower()
|
||||
mock_cmd.assert_called_once_with("sudo shutdown -h now")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_shutdown_with_delay(self):
|
||||
"""Test scheduled shutdown."""
|
||||
# Arrange
|
||||
service = SystemService()
|
||||
|
||||
# Act
|
||||
with patch.object(service, '_run_command', new_callable=AsyncMock) as mock_cmd:
|
||||
mock_cmd.return_value = ""
|
||||
result = await service.shutdown(delay=300) # 5 minutes
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert "scheduled" in result.data["message"].lower()
|
||||
assert result.data["delay"] == 300
|
||||
# 300 seconds = 5 minutes
|
||||
mock_cmd.assert_called_once_with("sudo shutdown -h +5")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_shutdown_failure(self):
|
||||
"""Test shutdown command failure."""
|
||||
# Arrange
|
||||
service = SystemService()
|
||||
|
||||
# Act
|
||||
with patch.object(service, '_run_command', new_callable=AsyncMock) as mock_cmd:
|
||||
mock_cmd.side_effect = RuntimeError("Permission denied")
|
||||
result = await service.shutdown(delay=0)
|
||||
|
||||
# Assert
|
||||
assert result.success is False
|
||||
assert result.error.code == "shutdown_error"
|
||||
assert "Permission denied" in result.error.details
|
||||
|
||||
|
||||
class TestSystemServiceRestart:
|
||||
"""Test restart operations."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_restart_immediate(self):
|
||||
"""Test immediate restart."""
|
||||
# Arrange
|
||||
service = SystemService()
|
||||
|
||||
# Act
|
||||
with patch.object(service, '_run_command', new_callable=AsyncMock) as mock_cmd:
|
||||
mock_cmd.return_value = ""
|
||||
result = await service.restart(delay=0)
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert "initiated" in result.data["message"].lower()
|
||||
mock_cmd.assert_called_once_with("sudo shutdown -r now")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_restart_with_delay(self):
|
||||
"""Test scheduled restart."""
|
||||
# Arrange
|
||||
service = SystemService()
|
||||
|
||||
# Act
|
||||
with patch.object(service, '_run_command', new_callable=AsyncMock) as mock_cmd:
|
||||
mock_cmd.return_value = ""
|
||||
result = await service.restart(delay=600) # 10 minutes
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert "scheduled" in result.data["message"].lower()
|
||||
assert result.data["delay"] == 600
|
||||
mock_cmd.assert_called_once_with("sudo shutdown -r +10")
|
||||
|
||||
|
||||
class TestSystemServiceCancelShutdown:
|
||||
"""Test shutdown cancellation."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cancel_shutdown_success(self):
|
||||
"""Test successful shutdown cancellation."""
|
||||
# Arrange
|
||||
service = SystemService()
|
||||
|
||||
# Act
|
||||
with patch.object(service, '_run_command', new_callable=AsyncMock) as mock_cmd:
|
||||
mock_cmd.return_value = ""
|
||||
result = await service.cancel_shutdown()
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert "cancelled" in result.data["message"].lower()
|
||||
mock_cmd.assert_called_once_with("sudo shutdown -c")
|
||||
|
||||
|
||||
class TestSystemServiceLogs:
|
||||
"""Test log retrieval."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_logs_success(self):
|
||||
"""Test successful log retrieval."""
|
||||
# Arrange
|
||||
service = SystemService()
|
||||
log_content = "Jan 01 12:00:00 test systemd[1]: Started dangerous-pi"
|
||||
|
||||
# Act
|
||||
with patch.object(service, '_run_command', new_callable=AsyncMock) as mock_cmd:
|
||||
mock_cmd.return_value = log_content
|
||||
result = await service.get_logs(service="dangerous-pi", lines=100)
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert result.data["service"] == "dangerous-pi"
|
||||
assert result.data["lines"] == 100
|
||||
assert result.data["logs"] == log_content
|
||||
mock_cmd.assert_called_once_with(
|
||||
"sudo journalctl -u dangerous-pi -n 100 --no-pager"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_logs_failure(self):
|
||||
"""Test log retrieval failure."""
|
||||
# Arrange
|
||||
service = SystemService()
|
||||
|
||||
# Act
|
||||
with patch.object(service, '_run_command', new_callable=AsyncMock) as mock_cmd:
|
||||
mock_cmd.side_effect = RuntimeError("Service not found")
|
||||
result = await service.get_logs(service="nonexistent")
|
||||
|
||||
# Assert
|
||||
assert result.success is False
|
||||
assert result.error.code == "logs_error"
|
||||
|
||||
|
||||
class TestSystemServiceStatus:
|
||||
"""Test service status queries."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_service_status_active(self):
|
||||
"""Test getting status of active service."""
|
||||
# Arrange
|
||||
service = SystemService()
|
||||
status_output = "● dangerous-pi.service - Dangerous Pi Backend\n Loaded: loaded\n Active: active (running)"
|
||||
|
||||
# Act
|
||||
with patch.object(service, '_run_command', new_callable=AsyncMock) as mock_cmd, \
|
||||
patch.object(service, '_is_service_enabled', new_callable=AsyncMock) as mock_enabled:
|
||||
mock_cmd.return_value = status_output
|
||||
mock_enabled.return_value = True
|
||||
result = await service.get_service_status("dangerous-pi")
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert result.data["service"] == "dangerous-pi"
|
||||
assert result.data["active"] is True
|
||||
assert result.data["enabled"] is True
|
||||
assert "active (running)" in result.data["status"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_service_status_inactive(self):
|
||||
"""Test getting status of inactive service."""
|
||||
# Arrange
|
||||
service = SystemService()
|
||||
status_output = "● test.service\n Loaded: loaded\n Active: inactive (dead)"
|
||||
|
||||
# Act
|
||||
with patch.object(service, '_run_command', new_callable=AsyncMock) as mock_cmd, \
|
||||
patch.object(service, '_is_service_enabled', new_callable=AsyncMock) as mock_enabled:
|
||||
mock_cmd.return_value = status_output
|
||||
mock_enabled.return_value = False
|
||||
result = await service.get_service_status("test")
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert result.data["active"] is False
|
||||
assert result.data["enabled"] is False
|
||||
369
tests/unit/services/test_update_service.py
Normal file
369
tests/unit/services/test_update_service.py
Normal file
@@ -0,0 +1,369 @@
|
||||
"""Unit tests for UpdateService.
|
||||
|
||||
Tests the UpdateService business logic layer, ensuring:
|
||||
- Update checking
|
||||
- Update downloading
|
||||
- Update installation
|
||||
- Progress monitoring
|
||||
- Release notes retrieval
|
||||
- Error handling and error codes
|
||||
"""
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
from app.backend.services.update_service import UpdateService
|
||||
from app.backend.managers.update_manager import UpdateProgress, UpdateStatus
|
||||
|
||||
|
||||
class TestUpdateServiceCheckForUpdates:
|
||||
"""Test update checking operations."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_for_updates_available(self, mock_update_manager):
|
||||
"""Test checking for updates when update is available."""
|
||||
# Arrange
|
||||
mock_update_manager.check_for_updates.return_value = {
|
||||
"update_available": True,
|
||||
"current_version": "1.0.0",
|
||||
"latest_version": "1.1.0",
|
||||
"release_date": "2025-01-15",
|
||||
"changelog": "## New Features\n- Feature 1\n- Feature 2",
|
||||
"is_prerelease": False,
|
||||
"download_size": 10485760 # 10MB
|
||||
}
|
||||
service = UpdateService(mock_update_manager)
|
||||
|
||||
# Act
|
||||
result = await service.check_for_updates()
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert result.data["update_available"] is True
|
||||
assert result.data["latest_version"] == "1.1.0"
|
||||
assert result.data["download_size"] == 10485760
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_for_updates_none_available(self, mock_update_manager):
|
||||
"""Test checking for updates when system is up to date."""
|
||||
# Arrange
|
||||
mock_update_manager.check_for_updates.return_value = {
|
||||
"update_available": False,
|
||||
"current_version": "1.1.0",
|
||||
"latest_version": "1.1.0",
|
||||
"message": "System is up to date"
|
||||
}
|
||||
service = UpdateService(mock_update_manager)
|
||||
|
||||
# Act
|
||||
result = await service.check_for_updates()
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert result.data["update_available"] is False
|
||||
assert result.data["message"] == "System is up to date"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_for_updates_exception(self, mock_update_manager):
|
||||
"""Test update check with exception."""
|
||||
# Arrange
|
||||
mock_update_manager.check_for_updates.side_effect = Exception("Network error")
|
||||
service = UpdateService(mock_update_manager)
|
||||
|
||||
# Act
|
||||
result = await service.check_for_updates()
|
||||
|
||||
# Assert
|
||||
assert result.success is False
|
||||
assert result.error.code == "update_check_error"
|
||||
assert "Network error" in result.error.details
|
||||
|
||||
|
||||
class TestUpdateServiceDownload:
|
||||
"""Test update download operations."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_download_update_success(self, mock_update_manager):
|
||||
"""Test successful update download."""
|
||||
# Arrange
|
||||
mock_update_manager.download_update.return_value = True
|
||||
service = UpdateService(mock_update_manager)
|
||||
|
||||
# Act
|
||||
result = await service.download_update()
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert "successfully" in result.data["message"].lower()
|
||||
assert result.data["ready_to_install"] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_download_update_no_update_available(self, mock_update_manager):
|
||||
"""Test download when no update is available."""
|
||||
# Arrange
|
||||
mock_update_manager.download_update.side_effect = ValueError("No update available to download")
|
||||
service = UpdateService(mock_update_manager)
|
||||
|
||||
# Act
|
||||
result = await service.download_update()
|
||||
|
||||
# Assert
|
||||
assert result.success is False
|
||||
assert result.error.code == "no_update_available"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_download_update_failure(self, mock_update_manager):
|
||||
"""Test download failure."""
|
||||
# Arrange
|
||||
mock_update_manager.download_update.side_effect = Exception("Download failed")
|
||||
service = UpdateService(mock_update_manager)
|
||||
|
||||
# Act
|
||||
result = await service.download_update()
|
||||
|
||||
# Assert
|
||||
assert result.success is False
|
||||
assert result.error.code == "update_download_error"
|
||||
|
||||
|
||||
class TestUpdateServiceInstall:
|
||||
"""Test update installation operations."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_install_update_success(self, mock_update_manager):
|
||||
"""Test successful update installation."""
|
||||
# Arrange
|
||||
mock_update_manager.install_update.return_value = True
|
||||
service = UpdateService(mock_update_manager)
|
||||
|
||||
# Act
|
||||
result = await service.install_update()
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert "successfully" in result.data["message"].lower()
|
||||
assert result.data["requires_restart"] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_install_update_no_download(self, mock_update_manager):
|
||||
"""Test install when no update has been downloaded."""
|
||||
# Arrange
|
||||
mock_update_manager.install_update.side_effect = ValueError("No update downloaded")
|
||||
service = UpdateService(mock_update_manager)
|
||||
|
||||
# Act
|
||||
result = await service.install_update()
|
||||
|
||||
# Assert
|
||||
assert result.success is False
|
||||
assert result.error.code == "no_update_downloaded"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_install_update_failure(self, mock_update_manager):
|
||||
"""Test installation failure."""
|
||||
# Arrange
|
||||
mock_update_manager.install_update.side_effect = Exception("Installation failed")
|
||||
service = UpdateService(mock_update_manager)
|
||||
|
||||
# Act
|
||||
result = await service.install_update()
|
||||
|
||||
# Assert
|
||||
assert result.success is False
|
||||
assert result.error.code == "update_install_error"
|
||||
|
||||
|
||||
class TestUpdateServiceProgress:
|
||||
"""Test progress monitoring."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_progress_idle(self, mock_update_manager):
|
||||
"""Test getting progress when idle."""
|
||||
# Arrange
|
||||
mock_update_manager.get_progress.return_value = UpdateProgress(
|
||||
status=UpdateStatus.IDLE,
|
||||
current_version="1.0.0",
|
||||
available_version=None,
|
||||
download_progress=0.0,
|
||||
error_message=None,
|
||||
last_check="2025-01-15T10:00:00"
|
||||
)
|
||||
service = UpdateService(mock_update_manager)
|
||||
|
||||
# Act
|
||||
result = await service.get_progress()
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert result.data["status"] == "idle"
|
||||
assert result.data["current_version"] == "1.0.0"
|
||||
assert result.data["download_progress"] == 0.0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_progress_downloading(self, mock_update_manager):
|
||||
"""Test getting progress during download."""
|
||||
# Arrange
|
||||
mock_update_manager.get_progress.return_value = UpdateProgress(
|
||||
status=UpdateStatus.DOWNLOADING,
|
||||
current_version="1.0.0",
|
||||
available_version="1.1.0",
|
||||
download_progress=45.5,
|
||||
error_message=None,
|
||||
last_check="2025-01-15T10:00:00"
|
||||
)
|
||||
service = UpdateService(mock_update_manager)
|
||||
|
||||
# Act
|
||||
result = await service.get_progress()
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert result.data["status"] == "downloading"
|
||||
assert result.data["available_version"] == "1.1.0"
|
||||
assert result.data["download_progress"] == 45.5
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_progress_failed(self, mock_update_manager):
|
||||
"""Test getting progress after failure."""
|
||||
# Arrange
|
||||
mock_update_manager.get_progress.return_value = UpdateProgress(
|
||||
status=UpdateStatus.FAILED,
|
||||
current_version="1.0.0",
|
||||
available_version="1.1.0",
|
||||
download_progress=0.0,
|
||||
error_message="Network timeout",
|
||||
last_check="2025-01-15T10:00:00"
|
||||
)
|
||||
service = UpdateService(mock_update_manager)
|
||||
|
||||
# Act
|
||||
result = await service.get_progress()
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert result.data["status"] == "failed"
|
||||
assert result.data["error_message"] == "Network timeout"
|
||||
|
||||
|
||||
class TestUpdateServiceReleaseNotes:
|
||||
"""Test release notes retrieval."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_release_notes_latest(self, mock_update_manager):
|
||||
"""Test getting latest release notes."""
|
||||
# Arrange
|
||||
mock_update_manager.get_release_notes.return_value = "## Version 1.1.0\n- New feature"
|
||||
service = UpdateService(mock_update_manager)
|
||||
|
||||
# Act
|
||||
result = await service.get_release_notes(version=None)
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert result.data["version"] == "latest"
|
||||
assert "New feature" in result.data["release_notes"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_release_notes_specific_version(self, mock_update_manager):
|
||||
"""Test getting notes for specific version."""
|
||||
# Arrange
|
||||
mock_update_manager.get_release_notes.return_value = "## Version 1.0.0\n- Initial release"
|
||||
service = UpdateService(mock_update_manager)
|
||||
|
||||
# Act
|
||||
result = await service.get_release_notes(version="1.0.0")
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert result.data["version"] == "1.0.0"
|
||||
assert "Initial release" in result.data["release_notes"]
|
||||
mock_update_manager.get_release_notes.assert_called_once_with("1.0.0")
|
||||
|
||||
|
||||
class TestUpdateServiceCombinedOperations:
|
||||
"""Test combined update operations."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_and_download_update_available(self, mock_update_manager):
|
||||
"""Test combined check and download when update is available."""
|
||||
# Arrange
|
||||
mock_update_manager.check_for_updates.return_value = {
|
||||
"update_available": True,
|
||||
"current_version": "1.0.0",
|
||||
"latest_version": "1.1.0"
|
||||
}
|
||||
mock_update_manager.download_update.return_value = True
|
||||
service = UpdateService(mock_update_manager)
|
||||
|
||||
# Act
|
||||
result = await service.check_and_download_update()
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert result.data["ready_to_install"] is True
|
||||
assert "update_info" in result.data
|
||||
mock_update_manager.download_update.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_and_download_update_not_available(self, mock_update_manager):
|
||||
"""Test combined check and download when no update available."""
|
||||
# Arrange
|
||||
mock_update_manager.check_for_updates.return_value = {
|
||||
"update_available": False,
|
||||
"current_version": "1.1.0"
|
||||
}
|
||||
service = UpdateService(mock_update_manager)
|
||||
|
||||
# Act
|
||||
result = await service.check_and_download_update()
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert result.data["update_available"] is False
|
||||
mock_update_manager.download_update.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_full_update_success(self, mock_update_manager):
|
||||
"""Test full update workflow."""
|
||||
# Arrange
|
||||
mock_update_manager.check_for_updates.return_value = {
|
||||
"update_available": True,
|
||||
"current_version": "1.0.0",
|
||||
"latest_version": "1.1.0"
|
||||
}
|
||||
mock_update_manager.download_update.return_value = True
|
||||
mock_update_manager.install_update.return_value = True
|
||||
service = UpdateService(mock_update_manager)
|
||||
|
||||
# Act
|
||||
result = await service.full_update()
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert result.data["update_performed"] is True
|
||||
assert result.data["previous_version"] == "1.0.0"
|
||||
assert result.data["new_version"] == "1.1.0"
|
||||
assert result.data["requires_restart"] is True
|
||||
|
||||
# Verify all steps were called
|
||||
mock_update_manager.check_for_updates.assert_called_once()
|
||||
mock_update_manager.download_update.assert_called_once()
|
||||
mock_update_manager.install_update.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_full_update_already_up_to_date(self, mock_update_manager):
|
||||
"""Test full update when already up to date."""
|
||||
# Arrange
|
||||
mock_update_manager.check_for_updates.return_value = {
|
||||
"update_available": False,
|
||||
"current_version": "1.1.0"
|
||||
}
|
||||
service = UpdateService(mock_update_manager)
|
||||
|
||||
# Act
|
||||
result = await service.full_update()
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert result.data["update_performed"] is False
|
||||
mock_update_manager.download_update.assert_not_called()
|
||||
mock_update_manager.install_update.assert_not_called()
|
||||
391
tests/unit/services/test_wifi_service.py
Normal file
391
tests/unit/services/test_wifi_service.py
Normal file
@@ -0,0 +1,391 @@
|
||||
"""Unit tests for WiFiService.
|
||||
|
||||
Tests the WiFiService business logic layer, ensuring:
|
||||
- WiFi status queries
|
||||
- Network scanning and connection
|
||||
- Mode switching
|
||||
- Saved network management
|
||||
- Error handling and error codes
|
||||
"""
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
from app.backend.services.wifi_service import WiFiService
|
||||
from app.backend.managers.wifi_manager import WiFiMode, WiFiStatus, WiFiNetwork, WiFiInterface
|
||||
|
||||
|
||||
class TestWiFiServiceStatus:
|
||||
"""Test WiFi status queries."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_status_success(self, mock_wifi_manager):
|
||||
"""Test successful WiFi status retrieval."""
|
||||
# Arrange
|
||||
test_interface = WiFiInterface(
|
||||
name="wlan0",
|
||||
mac="00:11:22:33:44:55",
|
||||
is_usb=False,
|
||||
is_up=True,
|
||||
connected=False,
|
||||
ssid=None,
|
||||
ip_address="10.3.141.1"
|
||||
)
|
||||
mock_wifi_manager.get_status.return_value = WiFiStatus(
|
||||
mode=WiFiMode.AP,
|
||||
interfaces=[test_interface],
|
||||
current_ssid=None,
|
||||
current_ip=None,
|
||||
ap_ssid="dangerous-pi",
|
||||
ap_ip="10.3.141.1",
|
||||
supports_dual=False
|
||||
)
|
||||
|
||||
service = WiFiService(mock_wifi_manager)
|
||||
|
||||
# Act
|
||||
result = await service.get_status()
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert result.data["mode"] == "ap"
|
||||
assert len(result.data["interfaces"]) == 1
|
||||
assert result.data["interfaces"][0]["name"] == "wlan0"
|
||||
assert result.data["access_point"]["ssid"] == "dangerous-pi"
|
||||
assert result.data["supports_dual"] is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_status_with_client_connection(self, mock_wifi_manager):
|
||||
"""Test WiFi status with client connection."""
|
||||
# Arrange
|
||||
test_interface = WiFiInterface(
|
||||
name="wlan0",
|
||||
mac="00:11:22:33:44:55",
|
||||
is_usb=False,
|
||||
is_up=True,
|
||||
connected=True,
|
||||
ssid="MyNetwork",
|
||||
ip_address="192.168.1.100"
|
||||
)
|
||||
mock_wifi_manager.get_status.return_value = WiFiStatus(
|
||||
mode=WiFiMode.CLIENT,
|
||||
interfaces=[test_interface],
|
||||
current_ssid="MyNetwork",
|
||||
current_ip="192.168.1.100",
|
||||
ap_ssid="dangerous-pi",
|
||||
ap_ip="10.3.141.1",
|
||||
supports_dual=False
|
||||
)
|
||||
|
||||
service = WiFiService(mock_wifi_manager)
|
||||
|
||||
# Act
|
||||
result = await service.get_status()
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert result.data["mode"] == "client"
|
||||
assert result.data["client"]["ssid"] == "MyNetwork"
|
||||
assert result.data["client"]["ip"] == "192.168.1.100"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_status_exception(self, mock_wifi_manager):
|
||||
"""Test WiFi status query with exception."""
|
||||
# Arrange
|
||||
mock_wifi_manager.get_status.side_effect = Exception("WiFi error")
|
||||
service = WiFiService(mock_wifi_manager)
|
||||
|
||||
# Act
|
||||
result = await service.get_status()
|
||||
|
||||
# Assert
|
||||
assert result.success is False
|
||||
assert result.error.code == "wifi_status_error"
|
||||
assert "WiFi error" in result.error.details
|
||||
|
||||
|
||||
class TestWiFiServiceNetworkScanning:
|
||||
"""Test network scanning."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_scan_networks_success(self, mock_wifi_manager):
|
||||
"""Test successful network scan."""
|
||||
# Arrange
|
||||
test_networks = [
|
||||
WiFiNetwork(
|
||||
ssid="TestNetwork",
|
||||
bssid="AA:BB:CC:DD:EE:FF",
|
||||
signal_strength=-45,
|
||||
frequency=2437,
|
||||
encrypted=True,
|
||||
in_use=False
|
||||
),
|
||||
WiFiNetwork(
|
||||
ssid="OpenNetwork",
|
||||
bssid="11:22:33:44:55:66",
|
||||
signal_strength=-65,
|
||||
frequency=2462,
|
||||
encrypted=False,
|
||||
in_use=False
|
||||
)
|
||||
]
|
||||
mock_wifi_manager.scan_networks.return_value = test_networks
|
||||
service = WiFiService(mock_wifi_manager)
|
||||
|
||||
# Act
|
||||
result = await service.scan_networks()
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert result.data["count"] == 2
|
||||
assert len(result.data["networks"]) == 2
|
||||
assert result.data["networks"][0]["ssid"] == "TestNetwork"
|
||||
assert result.data["networks"][0]["encrypted"] is True
|
||||
assert result.data["networks"][1]["ssid"] == "OpenNetwork"
|
||||
assert result.data["networks"][1]["encrypted"] is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_scan_networks_with_interface(self, mock_wifi_manager):
|
||||
"""Test network scan with specific interface."""
|
||||
# Arrange
|
||||
mock_wifi_manager.scan_networks.return_value = []
|
||||
service = WiFiService(mock_wifi_manager)
|
||||
|
||||
# Act
|
||||
result = await service.scan_networks(interface="wlan1")
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
mock_wifi_manager.scan_networks.assert_called_once_with("wlan1")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_scan_networks_exception(self, mock_wifi_manager):
|
||||
"""Test network scan with exception."""
|
||||
# Arrange
|
||||
mock_wifi_manager.scan_networks.side_effect = Exception("Scan failed")
|
||||
service = WiFiService(mock_wifi_manager)
|
||||
|
||||
# Act
|
||||
result = await service.scan_networks()
|
||||
|
||||
# Assert
|
||||
assert result.success is False
|
||||
assert result.error.code == "wifi_scan_error"
|
||||
|
||||
|
||||
class TestWiFiServiceConnection:
|
||||
"""Test network connection management."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_connect_success(self, mock_wifi_manager):
|
||||
"""Test successful network connection."""
|
||||
# Arrange
|
||||
mock_wifi_manager.connect_to_network.return_value = True
|
||||
mock_wifi_manager.get_status.return_value = WiFiStatus(
|
||||
mode=WiFiMode.CLIENT,
|
||||
interfaces=[],
|
||||
current_ssid="TestNetwork",
|
||||
current_ip="192.168.1.100",
|
||||
ap_ssid=None,
|
||||
ap_ip=None,
|
||||
supports_dual=False
|
||||
)
|
||||
service = WiFiService(mock_wifi_manager)
|
||||
|
||||
# Act
|
||||
result = await service.connect(
|
||||
ssid="TestNetwork",
|
||||
password="testpass123"
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert "TestNetwork" in result.data["message"]
|
||||
assert result.data["ssid"] == "TestNetwork"
|
||||
assert result.data["ip"] == "192.168.1.100"
|
||||
mock_wifi_manager.connect_to_network.assert_called_once_with(
|
||||
ssid="TestNetwork",
|
||||
password="testpass123",
|
||||
interface=None,
|
||||
hidden=False
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_connect_open_network(self, mock_wifi_manager):
|
||||
"""Test connection to open network."""
|
||||
# Arrange
|
||||
mock_wifi_manager.connect_to_network.return_value = True
|
||||
mock_wifi_manager.get_status.return_value = WiFiStatus(
|
||||
mode=WiFiMode.CLIENT,
|
||||
interfaces=[],
|
||||
current_ssid="OpenNet",
|
||||
current_ip="192.168.1.50",
|
||||
ap_ssid=None,
|
||||
ap_ip=None,
|
||||
supports_dual=False
|
||||
)
|
||||
service = WiFiService(mock_wifi_manager)
|
||||
|
||||
# Act
|
||||
result = await service.connect(ssid="OpenNet", password=None)
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
mock_wifi_manager.connect_to_network.assert_called_once_with(
|
||||
ssid="OpenNet",
|
||||
password=None,
|
||||
interface=None,
|
||||
hidden=False
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_connect_failure(self, mock_wifi_manager):
|
||||
"""Test failed network connection."""
|
||||
# Arrange
|
||||
mock_wifi_manager.connect_to_network.return_value = False
|
||||
service = WiFiService(mock_wifi_manager)
|
||||
|
||||
# Act
|
||||
result = await service.connect(ssid="BadNetwork", password="wrong")
|
||||
|
||||
# Assert
|
||||
assert result.success is False
|
||||
assert result.error.code == "connection_failed"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_disconnect_success(self, mock_wifi_manager):
|
||||
"""Test successful disconnect."""
|
||||
# Arrange
|
||||
mock_wifi_manager.disconnect_from_network.return_value = True
|
||||
service = WiFiService(mock_wifi_manager)
|
||||
|
||||
# Act
|
||||
result = await service.disconnect()
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert "Disconnected" in result.data["message"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_disconnect_failure(self, mock_wifi_manager):
|
||||
"""Test failed disconnect."""
|
||||
# Arrange
|
||||
mock_wifi_manager.disconnect_from_network.return_value = False
|
||||
service = WiFiService(mock_wifi_manager)
|
||||
|
||||
# Act
|
||||
result = await service.disconnect()
|
||||
|
||||
# Assert
|
||||
assert result.success is False
|
||||
assert result.error.code == "disconnect_failed"
|
||||
|
||||
|
||||
class TestWiFiServiceModeSwitch:
|
||||
"""Test WiFi mode switching."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_mode_ap(self, mock_wifi_manager):
|
||||
"""Test switching to AP mode."""
|
||||
# Arrange
|
||||
mock_wifi_manager.set_mode.return_value = True
|
||||
service = WiFiService(mock_wifi_manager)
|
||||
|
||||
# Act
|
||||
result = await service.set_mode("ap")
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert result.data["mode"] == "ap"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_mode_client(self, mock_wifi_manager):
|
||||
"""Test switching to client mode."""
|
||||
# Arrange
|
||||
mock_wifi_manager.set_mode.return_value = True
|
||||
service = WiFiService(mock_wifi_manager)
|
||||
|
||||
# Act
|
||||
result = await service.set_mode("client")
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert result.data["mode"] == "client"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_mode_invalid(self, mock_wifi_manager):
|
||||
"""Test invalid mode."""
|
||||
# Arrange
|
||||
service = WiFiService(mock_wifi_manager)
|
||||
|
||||
# Act
|
||||
result = await service.set_mode("invalid_mode")
|
||||
|
||||
# Assert
|
||||
assert result.success is False
|
||||
assert result.error.code == "invalid_mode"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_mode_dual_not_supported(self, mock_wifi_manager):
|
||||
"""Test dual mode without USB adapter."""
|
||||
# Arrange
|
||||
mock_wifi_manager.set_mode.side_effect = ValueError("Dual mode requires USB WiFi adapter")
|
||||
service = WiFiService(mock_wifi_manager)
|
||||
|
||||
# Act
|
||||
result = await service.set_mode("dual")
|
||||
|
||||
# Assert
|
||||
assert result.success is False
|
||||
assert result.error.code == "mode_not_supported"
|
||||
|
||||
|
||||
class TestWiFiServiceSavedNetworks:
|
||||
"""Test saved network management."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_saved_networks_success(self, mock_wifi_manager):
|
||||
"""Test retrieving saved networks."""
|
||||
# Arrange
|
||||
saved = [
|
||||
{"id": "0", "ssid": "HomeNet", "enabled": True},
|
||||
{"id": "1", "ssid": "WorkNet", "enabled": False}
|
||||
]
|
||||
mock_wifi_manager.get_saved_networks.return_value = saved
|
||||
service = WiFiService(mock_wifi_manager)
|
||||
|
||||
# Act
|
||||
result = await service.get_saved_networks()
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert result.data["count"] == 2
|
||||
assert result.data["networks"][0]["ssid"] == "HomeNet"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forget_network_success(self, mock_wifi_manager):
|
||||
"""Test forgetting a saved network."""
|
||||
# Arrange
|
||||
mock_wifi_manager.forget_network.return_value = True
|
||||
service = WiFiService(mock_wifi_manager)
|
||||
|
||||
# Act
|
||||
result = await service.forget_network("OldNetwork")
|
||||
|
||||
# Assert
|
||||
assert result.success is True
|
||||
assert "OldNetwork" in result.data["message"]
|
||||
mock_wifi_manager.forget_network.assert_called_once_with("OldNetwork")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forget_network_not_found(self, mock_wifi_manager):
|
||||
"""Test forgetting non-existent network."""
|
||||
# Arrange
|
||||
mock_wifi_manager.forget_network.return_value = False
|
||||
service = WiFiService(mock_wifi_manager)
|
||||
|
||||
# Act
|
||||
result = await service.forget_network("NonExistent")
|
||||
|
||||
# Assert
|
||||
assert result.success is False
|
||||
assert result.error.code == "network_not_found"
|
||||
Reference in New Issue
Block a user