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:
michael
2026-01-06 13:45:29 -08:00
parent 1da6730735
commit 4f35df1781
323 changed files with 98287 additions and 1195 deletions

230
tests/README.md Normal file
View File

@@ -0,0 +1,230 @@
# Dangerous Pi Test Suite
Comprehensive test suite for the Dangerous Pi backend refactoring.
## Test Structure
```
tests/
├── conftest.py # Shared fixtures and configuration
├── unit/ # Unit tests (isolated, fast)
│ └── services/ # Service layer tests
│ ├── test_pm3_service.py
│ ├── test_system_service.py
│ ├── test_wifi_service.py
│ └── test_update_service.py
└── integration/ # Integration tests (API + services)
└── api/ # API endpoint tests
├── test_pm3_api.py
└── ...
```
## Running Tests
### Run all tests
```bash
pytest
```
### Run specific test types
```bash
# Unit tests only
pytest tests/unit/
# Integration tests only
pytest tests/integration/
# Specific service tests
pytest tests/unit/services/test_pm3_service.py
```
### Run with coverage
```bash
# Generate coverage report
pytest --cov=app/backend/services --cov=app/backend/api --cov-report=html
# View coverage report
open htmlcov/index.html
```
### Run specific test classes or methods
```bash
# Run specific test class
pytest tests/unit/services/test_pm3_service.py::TestPM3ServiceCommandExecution
# Run specific test method
pytest tests/unit/services/test_pm3_service.py::TestPM3ServiceCommandExecution::test_execute_command_success
```
### Run with markers
```bash
# Run only async tests
pytest -m asyncio
# Run only unit tests
pytest -m unit
# Skip slow tests
pytest -m "not slow"
```
## Test Coverage
The test suite covers:
### Unit Tests (tests/unit/services/)
**PM3Service** (test_pm3_service.py):
- ✅ Command execution with session validation
- ✅ Session locked error handling
- ✅ PM3 not connected error handling
- ✅ Command failure handling
- ✅ Exception handling
- ✅ Status queries
- ✅ Connection management
- ✅ Session management (create, release, get info)
**SystemService** (test_system_service.py):
- ✅ System information queries (CPU, memory, disk)
- ✅ CPU temperature reading
- ✅ Shutdown operations (immediate and delayed)
- ✅ Restart operations (immediate and delayed)
- ✅ Shutdown cancellation
- ✅ Log retrieval
- ✅ Service status queries
- ✅ Error handling for all operations
**WiFiService** (test_wifi_service.py):
- ✅ WiFi status queries (AP and client modes)
- ✅ Network scanning
- ✅ Network connection (encrypted and open)
- ✅ Network disconnection
- ✅ Mode switching (AP, client, dual, auto, off)
- ✅ Invalid mode handling
- ✅ Dual mode without USB adapter error
- ✅ Saved network management
- ✅ Forget network operation
**UpdateService** (test_update_service.py):
- ✅ Update checking (available and up-to-date)
- ✅ Update downloading
- ✅ Update installation
- ✅ Progress monitoring (all states)
- ✅ Release notes retrieval
- ✅ Combined operations (check+download, full update)
- ✅ Error handling for no update available/downloaded
### Integration Tests (tests/integration/api/)
**PM3 API** (test_pm3_api.py):
- ✅ Status endpoint integration with PM3Service
- ✅ Command endpoint with session management
- ✅ HTTP status code mapping (423 for session locked, 503 for not connected)
- ✅ Connection/disconnection endpoints
- ✅ Request/response format consistency
## Best Practices Demonstrated
### 1. Proper Test Isolation
- Each test is independent and can run in any order
- Mocks are used to isolate unit tests from external dependencies
- Fixtures provide clean test setup
### 2. Clear Test Structure
- Tests follow Arrange-Act-Assert (AAA) pattern
- Descriptive test names explain what is being tested
- Test classes group related tests logically
### 3. Comprehensive Coverage
- Both success and error paths tested
- Edge cases covered (session locked, not connected, etc.)
- Exception handling verified
### 4. Async Test Support
- `pytest-asyncio` used for testing async code
- Proper mocking of async functions with `AsyncMock`
### 5. Integration Testing
- API endpoints tested with FastAPI TestClient
- Service integration verified
- HTTP status codes validated
### 6. Maintainability
- Shared fixtures in conftest.py reduce duplication
- Clear documentation in test docstrings
- Consistent naming conventions
## Writing New Tests
### Unit Test Template
```python
@pytest.mark.asyncio
async def test_feature_success(self, mock_dependency):
"""Test description."""
# Arrange
mock_dependency.method.return_value = expected_value
service = YourService(mock_dependency)
# Act
result = await service.your_method()
# Assert
assert result.success is True
assert result.data["key"] == expected_value
mock_dependency.method.assert_called_once()
```
### Integration Test Template
```python
def test_api_endpoint_success(self, test_client, mock_service):
"""Test API endpoint description."""
# Arrange
mock_service.method.return_value = ServiceResult(...)
# Act
with patch('app.backend.api.module.container.service', mock_service):
response = test_client.post("/api/endpoint", json={...})
# Assert
assert response.status_code == 200
assert response.json()["key"] == expected_value
```
## CI/CD Integration
Tests can be run in CI/CD pipelines:
```yaml
# Example GitHub Actions
- name: Run tests
run: |
pytest --cov --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
```
## Troubleshooting
### Tests fail with "RuntimeError: Event loop is closed"
- Ensure `pytest-asyncio` is installed and configured
- Check that `asyncio_mode = auto` is set in pytest.ini
### Import errors
- Verify that PYTHONPATH includes project root
- Check that all __init__.py files are present
### Coverage not generated
- Ensure `pytest-cov` is installed
- Check that source paths in pytest.ini are correct
## Contributing
When adding new features:
1. Write unit tests for the service layer
2. Write integration tests for API endpoints
3. Ensure tests pass: `pytest`
4. Verify coverage: `pytest --cov`
5. Follow existing test patterns and naming conventions

1
tests/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Test suite for Dangerous Pi backend."""

109
tests/conftest.py Normal file
View File

@@ -0,0 +1,109 @@
"""Pytest configuration and shared fixtures."""
import pytest
import asyncio
from typing import Generator
from unittest.mock import AsyncMock, Mock
# Fixtures available to all tests
@pytest.fixture(scope="session")
def event_loop():
"""Create an event loop for the test session."""
loop = asyncio.get_event_loop_policy().new_event_loop()
yield loop
loop.close()
@pytest.fixture
def mock_pm3_worker():
"""Create a mock PM3Worker."""
worker = Mock()
worker.is_connected = AsyncMock(return_value=True)
worker.execute_command = AsyncMock()
worker.connect = AsyncMock()
worker.disconnect = AsyncMock()
worker.device_path = "/dev/ttyACM0"
return worker
@pytest.fixture
def mock_session_manager():
"""Create a mock SessionManager."""
manager = Mock()
manager.can_execute = Mock(return_value=True)
manager.update_activity = Mock()
manager.has_active_session = Mock(return_value=False)
manager.create_session = Mock(return_value="test-session-id")
manager.release_session = Mock()
manager.get_session = Mock(return_value=None)
manager.get_active_session = Mock(return_value=None)
return manager
@pytest.fixture
def mock_wifi_manager():
"""Create a mock WiFiManager."""
from app.backend.managers.wifi_manager import WiFiMode, WiFiStatus, WiFiInterface
manager = Mock()
manager.get_status = AsyncMock(return_value=WiFiStatus(
mode=WiFiMode.AP,
interfaces=[],
current_ssid=None,
current_ip=None,
ap_ssid="raspi-webgui",
ap_ip="10.3.141.1",
supports_dual=False
))
manager.scan_networks = AsyncMock(return_value=[])
manager.connect_to_network = AsyncMock(return_value=True)
manager.disconnect_from_network = AsyncMock(return_value=True)
manager.set_mode = AsyncMock(return_value=True)
manager.get_saved_networks = AsyncMock(return_value=[])
manager.forget_network = AsyncMock(return_value=True)
return manager
@pytest.fixture
def mock_update_manager():
"""Create a mock UpdateManager."""
from app.backend.managers.update_manager import UpdateProgress, UpdateStatus
manager = Mock()
manager.check_for_updates = AsyncMock(return_value={
"update_available": False,
"current_version": "1.0.0",
"message": "System is up to date"
})
manager.download_update = AsyncMock(return_value=True)
manager.install_update = AsyncMock(return_value=True)
manager.get_progress = AsyncMock(return_value=UpdateProgress(
status=UpdateStatus.IDLE,
current_version="1.0.0"
))
manager.get_release_notes = AsyncMock(return_value="Release notes")
return manager
@pytest.fixture
def mock_pm3_service():
"""Create a mock PM3Service for integration tests."""
service = Mock()
service.execute_command = AsyncMock()
service.get_status = AsyncMock()
service.connect = AsyncMock()
service.disconnect = AsyncMock()
service.create_session = Mock()
service.release_session = Mock()
service.get_session_info = Mock()
return service
@pytest.fixture
def test_client():
"""Create a FastAPI TestClient for integration tests."""
from fastapi.testclient import TestClient
from app.backend.main import app
return TestClient(app)

View File

View File

View 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

View 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"])

0
tests/unit/__init__.py Normal file
View File

View File

View 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"

View 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"])

View File

View 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"

View 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

View 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()

View 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"