session_config wird nicht mehr über die API/YAML gesendet. parser_config ist jetzt das einzige Config-Format zwischen Odoo und Bridge. Änderungen: - api/models.py: DeviceConfig.session_config → parser_config: dict SessionConfig bleibt als internes Modell für SessionDetector DeviceConfig.to_session_config() extrahiert Werte mit Defaults - config/schema.py: DeviceConfig gleich umgestellt + to_session_config() - config/loader.py: liest parser_config aus YAML, Fallback für legacy session_config (Rückwärtskompatibilität für bestehende config-active.yaml) - core/device_manager.py: device.session_config → device.to_session_config() - core/service_manager.py: session_config Referenzen entfernt - Odoo _build_bridge_config: sendet parser_config direkt (+ heartbeat) - Odoo iot_api.py: gleich umgestellt - Tests: alle SessionConfig-Fixtures → parser_config dicts 63/63 passing
346 lines
9.6 KiB
Python
346 lines
9.6 KiB
Python
"""
|
||
Shared pytest fixtures for IoT Bridge tests.
|
||
|
||
This module provides reusable test fixtures for:
|
||
- Configuration objects
|
||
- MQTT client mocks
|
||
- Odoo client mocks
|
||
- Device fixtures
|
||
- Session detector instances
|
||
- Event queue instances
|
||
"""
|
||
|
||
import tempfile
|
||
from pathlib import Path
|
||
from unittest.mock import MagicMock, Mock
|
||
|
||
import pytest
|
||
import yaml
|
||
|
||
from config.schema import (
|
||
BridgeConfig,
|
||
DeviceConfig,
|
||
DeviceStatusConfig,
|
||
EventQueueConfig,
|
||
LoggingConfig,
|
||
MQTTConfig,
|
||
OdooConfig,
|
||
SessionConfig,
|
||
)
|
||
|
||
# ============================================================================
|
||
# Configuration Fixtures
|
||
# ============================================================================
|
||
|
||
|
||
@pytest.fixture
|
||
def temp_config_file():
|
||
"""Create a temporary YAML config file for testing."""
|
||
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
|
||
config_data = {
|
||
"mqtt": {
|
||
"broker": "localhost",
|
||
"port": 1883,
|
||
"client_id": "test_bridge",
|
||
"username": "",
|
||
"password": "",
|
||
"use_tls": False,
|
||
},
|
||
"odoo": {
|
||
"base_url": "http://localhost:8069",
|
||
"database": "test_db",
|
||
"username": "test@example.com",
|
||
"password": "test_password",
|
||
},
|
||
"logging": {"level": "DEBUG", "format": "json"},
|
||
"session": {
|
||
"heartbeat_interval": 300,
|
||
"grace_period": 60,
|
||
"min_session_duration": 10,
|
||
},
|
||
"devices": [
|
||
{
|
||
"device_id": "test_device_1",
|
||
"topic": "test/device1/#",
|
||
"parser": "shelly",
|
||
},
|
||
{
|
||
"device_id": "test_device_2",
|
||
"topic": "test/device2/#",
|
||
"parser": "shelly",
|
||
},
|
||
],
|
||
}
|
||
yaml.dump(config_data, f)
|
||
temp_path = Path(f.name)
|
||
|
||
yield temp_path
|
||
|
||
# Cleanup
|
||
temp_path.unlink(missing_ok=True)
|
||
|
||
|
||
@pytest.fixture
|
||
def mqtt_config():
|
||
"""Provide a sample MQTT configuration."""
|
||
return MQTTConfig(
|
||
broker="localhost",
|
||
port=1883,
|
||
client_id="test_bridge",
|
||
username="",
|
||
password="",
|
||
use_tls=False,
|
||
)
|
||
|
||
|
||
@pytest.fixture
|
||
def odoo_config():
|
||
"""Provide a sample Odoo configuration."""
|
||
return OdooConfig(
|
||
base_url="http://localhost:8069",
|
||
database="test_db",
|
||
username="test@example.com",
|
||
api_key="test_password",
|
||
)
|
||
|
||
|
||
@pytest.fixture
|
||
def logging_config():
|
||
"""Provide a sample logging configuration."""
|
||
return LoggingConfig(level="DEBUG", format="json", log_file=None)
|
||
|
||
|
||
@pytest.fixture
|
||
def session_config():
|
||
"""Provide a sample session configuration (legacy – derived from parser_config)."""
|
||
return SessionConfig(
|
||
strategy="power_threshold",
|
||
standby_threshold_w=5.0,
|
||
working_threshold_w=50.0,
|
||
start_debounce_s=3.0,
|
||
stop_debounce_s=15.0,
|
||
message_timeout_s=20.0,
|
||
heartbeat_interval_s=300.0,
|
||
)
|
||
|
||
|
||
@pytest.fixture
|
||
def device_config():
|
||
"""Provide a sample device configuration."""
|
||
return DeviceConfig(
|
||
device_id="test_device",
|
||
mqtt_topic="test/device/#",
|
||
parser_type="shelly_pm",
|
||
machine_name="Test Device",
|
||
parser_config={
|
||
"standby_threshold_w": 5.0,
|
||
"working_threshold_w": 50.0,
|
||
"start_debounce_s": 3.0,
|
||
"stop_debounce_s": 15.0,
|
||
"message_timeout_s": 20.0,
|
||
"heartbeat_interval_s": 300.0,
|
||
},
|
||
)
|
||
|
||
|
||
@pytest.fixture
|
||
def device_configs():
|
||
"""Provide a list of device configurations."""
|
||
_cfg = {
|
||
"standby_threshold_w": 5.0,
|
||
"working_threshold_w": 50.0,
|
||
"start_debounce_s": 3.0,
|
||
"stop_debounce_s": 15.0,
|
||
"message_timeout_s": 20.0,
|
||
"heartbeat_interval_s": 300.0,
|
||
}
|
||
return [
|
||
DeviceConfig(
|
||
device_id="test_device_1",
|
||
mqtt_topic="test/device1/#",
|
||
parser_type="shelly_pm",
|
||
machine_name="Test Device 1",
|
||
parser_config=_cfg,
|
||
),
|
||
DeviceConfig(
|
||
device_id="test_device_2",
|
||
mqtt_topic="test/device2/#",
|
||
parser_type="shelly_pm",
|
||
machine_name="Test Device 2",
|
||
parser_config=_cfg,
|
||
),
|
||
]
|
||
|
||
|
||
@pytest.fixture
|
||
def bridge_config(mqtt_config, odoo_config, logging_config, device_configs):
|
||
"""Provide a complete bridge configuration."""
|
||
return BridgeConfig(
|
||
mqtt=mqtt_config,
|
||
odoo=odoo_config,
|
||
logging=logging_config,
|
||
event_queue=EventQueueConfig(),
|
||
device_status=DeviceStatusConfig(),
|
||
devices=device_configs,
|
||
)
|
||
|
||
|
||
# ============================================================================
|
||
# MQTT Client Fixtures
|
||
# ============================================================================
|
||
|
||
|
||
@pytest.fixture
|
||
def mock_mqtt_client():
|
||
"""Provide a mock MQTT client."""
|
||
client = MagicMock()
|
||
client.connect = Mock(return_value=True)
|
||
client.disconnect = Mock()
|
||
client.subscribe = Mock()
|
||
client.unsubscribe = Mock()
|
||
client.publish = Mock()
|
||
client.loop_start = Mock()
|
||
client.loop_stop = Mock()
|
||
client.is_connected = Mock(return_value=True)
|
||
return client
|
||
|
||
|
||
@pytest.fixture
|
||
def mock_paho_mqtt_client(monkeypatch):
|
||
"""Mock paho.mqtt.client.Client for tests that instantiate it."""
|
||
mock_client = MagicMock()
|
||
mock_client.connect = Mock(return_value=0) # MQTT success code
|
||
mock_client.disconnect = Mock(return_value=0)
|
||
mock_client.subscribe = Mock(return_value=(0, 1)) # (result, mid)
|
||
mock_client.unsubscribe = Mock(return_value=(0, 1))
|
||
mock_client.publish = Mock(return_value=(0, 1))
|
||
mock_client.loop_start = Mock(return_value=0)
|
||
mock_client.loop_stop = Mock(return_value=0)
|
||
|
||
def mock_mqtt_client_init(*args, **kwargs):
|
||
return mock_client
|
||
|
||
monkeypatch.setattr("paho.mqtt.client.Client", mock_mqtt_client_init)
|
||
return mock_client
|
||
|
||
|
||
# ============================================================================
|
||
# Odoo Client Fixtures
|
||
# ============================================================================
|
||
|
||
|
||
@pytest.fixture
|
||
def mock_odoo_client():
|
||
"""Provide a mock Odoo client."""
|
||
client = MagicMock()
|
||
client.send_event = Mock(return_value=True)
|
||
client.authenticate = Mock(return_value=True)
|
||
client.base_url = "http://localhost:8069"
|
||
client.database = "test_db"
|
||
return client
|
||
|
||
|
||
@pytest.fixture
|
||
def mock_requests(monkeypatch):
|
||
"""Mock requests library for HTTP calls."""
|
||
mock_response = MagicMock()
|
||
mock_response.status_code = 200
|
||
mock_response.json = Mock(return_value={"result": "success"})
|
||
mock_response.raise_for_status = Mock()
|
||
|
||
mock_post = Mock(return_value=mock_response)
|
||
mock_get = Mock(return_value=mock_response)
|
||
|
||
monkeypatch.setattr("requests.post", mock_post)
|
||
monkeypatch.setattr("requests.get", mock_get)
|
||
|
||
return {"post": mock_post, "get": mock_get, "response": mock_response}
|
||
|
||
|
||
# ============================================================================
|
||
# Device & Parser Fixtures
|
||
# ============================================================================
|
||
|
||
|
||
@pytest.fixture
|
||
def shelly_pm_message():
|
||
"""Provide a sample Shelly PM MQTT message."""
|
||
return {
|
||
"topic": "shellypmminig3-test123/status/pm1:0",
|
||
"payload": {
|
||
"id": 0,
|
||
"voltage": 230.5,
|
||
"current": 2.1,
|
||
"apower": 484.0,
|
||
"freq": 50.0,
|
||
"aenergy": {"total": 12345.678, "by_minute": [120.5, 115.3, 118.7]},
|
||
},
|
||
}
|
||
|
||
|
||
@pytest.fixture
|
||
def mock_parser():
|
||
"""Provide a mock message parser."""
|
||
parser = MagicMock()
|
||
parser.parse_message = Mock(
|
||
return_value={
|
||
"device_id": "test_device",
|
||
"event_type": "power",
|
||
"power": 484.0,
|
||
"voltage": 230.5,
|
||
"current": 2.1,
|
||
"energy": 12345.678,
|
||
"timestamp": "2026-02-18T20:00:00Z",
|
||
}
|
||
)
|
||
return parser
|
||
|
||
|
||
# ============================================================================
|
||
# Session Detector Fixtures
|
||
# ============================================================================
|
||
|
||
|
||
@pytest.fixture
|
||
def session_detector_config():
|
||
"""Provide configuration for session detector."""
|
||
return {
|
||
"device_id": "test_device",
|
||
"heartbeat_interval": 300,
|
||
"grace_period": 60,
|
||
"min_session_duration": 10,
|
||
"power_threshold": 50.0,
|
||
}
|
||
|
||
|
||
# ============================================================================
|
||
# Event Queue Fixtures
|
||
# ============================================================================
|
||
|
||
|
||
@pytest.fixture
|
||
def sample_event():
|
||
"""Provide a sample IoT event."""
|
||
return {
|
||
"device_id": "test_device",
|
||
"event_type": "session_start",
|
||
"timestamp": "2026-02-18T20:00:00Z",
|
||
"power": 484.0,
|
||
"energy": 12345.678,
|
||
}
|
||
|
||
|
||
# ============================================================================
|
||
# Pytest Configuration
|
||
# ============================================================================
|
||
|
||
|
||
def pytest_configure(config):
|
||
"""Configure pytest with custom markers."""
|
||
config.addinivalue_line("markers", "unit: Unit tests (fast, no external dependencies)")
|
||
config.addinivalue_line(
|
||
"markers", "integration: Integration tests (may require MQTT broker, Odoo)"
|
||
)
|
||
config.addinivalue_line("markers", "e2e: End-to-end tests (full system test)")
|
||
config.addinivalue_line("markers", "slow: Slow running tests")
|