odoo_mqtt/iot_bridge/tests/conftest.py
matthias.lotz ff4ef2f563 Phase 3: Complete type safety & logging unification (3.1-3.2)
Phase 3.1: Type Safety
- Add bridge_types.py for shared type aliases (EventDict, PowerWatts, Timestamp, DeviceID)
- Define protocols for callbacks and message parsers
- Strict type annotations on all core modules (session_detector, event_queue, device_manager)
- Fix Optional handling and type guards throughout codebase
- Achieve full mypy compliance: 0 errors across 47 source files

Phase 3.2: Logging Unification
- Migrate from stdlib logging to pure structlog across all runtime modules
- Convert all logs to structured event+fields format (snake_case event names)
- Remove f-string and printf-style logger calls
- Add contextvars support for per-request correlation
- Implement FastAPI middleware to bind request_id, http_method, http_path
- Propagate X-Request-ID header in responses
- Remove stdlib logging imports except setup layer (utils/logging.py)
- Ensure log-level consistency across all modules

Files Modified:
- iot_bridge/bridge_types.py (new) - Central type definitions
- iot_bridge/core/* - Type safety and logging unification
- iot_bridge/clients/* - Structured logging with request context
- iot_bridge/parsers/* - Type-safe parsing with structured logs
- iot_bridge/utils/logging.py - Pure structlog setup with contextvars
- iot_bridge/api/server.py - Added request correlation middleware
- iot_bridge/tests/* - Test fixtures updated for type safety
- iot_bridge/OPTIMIZATION_PLAN.md - Phase 3 status updated

Validation:
- mypy . → 0 errors (47 files)
- All unit tests pass
- Runtime behavior unchanged
- API response headers include X-Request-ID
2026-02-18 23:54:27 +01:00

331 lines
9.2 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."""
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(session_config):
"""Provide a sample device configuration."""
return DeviceConfig(
device_id="test_device",
mqtt_topic="test/device/#",
parser_type="shelly_pm",
machine_name="Test Device",
session_config=session_config,
)
@pytest.fixture
def device_configs(session_config):
"""Provide a list of device configurations."""
return [
DeviceConfig(
device_id="test_device_1",
mqtt_topic="test/device1/#",
parser_type="shelly_pm",
machine_name="Test Device 1",
session_config=session_config,
),
DeviceConfig(
device_id="test_device_2",
mqtt_topic="test/device2/#",
parser_type="shelly_pm",
machine_name="Test Device 2",
session_config=session_config,
),
]
@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")