diff --git a/iot_bridge/OPTIMIZATION_PLAN.md b/iot_bridge/OPTIMIZATION_PLAN.md index b4e58b5..efaaca7 100644 --- a/iot_bridge/OPTIMIZATION_PLAN.md +++ b/iot_bridge/OPTIMIZATION_PLAN.md @@ -603,7 +603,7 @@ iot_bridge/tests/ ## 📦 Phase 2: Code Organization -**Status:** 🟡 Teilweise abgeschlossen (2.1-2.3 umgesetzt, 2.4 offen) +**Status:** 🟡 Teilweise abgeschlossen (2.1-2.4 umgesetzt, testlastige Restaufgaben offen) **Aufwand:** ~6-8 Stunden **Priorität:** 🟡 Mittel **Abhängigkeiten:** Phase 0-1 abgeschlossen @@ -727,10 +727,10 @@ class MQTTConfig(BaseSettings): **Dateien:** `dependencies.py` (NEU), `main.py` **Aufgaben:** -- [ ] `dependencies.py` mit DI-Container erstellen -- [ ] Factory-Methoden für Service-Initialisierung -- [ ] Constructor-Injection statt globale Variablen -- [ ] Tests mit Mock-Dependencies schreiben +- [x] `dependencies.py` mit DI-Container erstellen +- [x] Factory-Methoden für Service-Initialisierung +- [x] Constructor-Injection statt globale Variablen +- [x] Tests mit Mock-Dependencies schreiben **Erfolgskriterien:** - ✅ Services werden als Dependencies übergeben diff --git a/iot_bridge/dependencies.py b/iot_bridge/dependencies.py new file mode 100644 index 0000000..8f95b6d --- /dev/null +++ b/iot_bridge/dependencies.py @@ -0,0 +1,56 @@ +"""Dependency injection helpers for IoT Bridge runtime wiring.""" + +from dataclasses import dataclass +from typing import Callable + +from core.bootstrap import BootstrapConfig, bootstrap, get_mqtt_config +from core.service_manager import ServiceManager + + +@dataclass(frozen=True) +class RuntimeContainer: + """Container for runtime dependencies and factory callables.""" + + bootstrap_factory: Callable[[], BootstrapConfig] = bootstrap + mqtt_config_factory: Callable[[object], object] = get_mqtt_config + service_manager_factory: Callable[..., ServiceManager] = ServiceManager + + +@dataclass(frozen=True) +class RuntimeContext: + """Resolved runtime objects used by main orchestration.""" + + boot_config: BootstrapConfig + service_manager: ServiceManager + mqtt_config: object + + +def create_service_manager( + boot_config: BootstrapConfig, + service_manager_factory: Callable[..., ServiceManager] = ServiceManager, +) -> ServiceManager: + """Create service manager via injectable factory.""" + return service_manager_factory( + config=boot_config.config, + logger=boot_config.logger, + bridge_port=boot_config.bridge_port, + bridge_token=boot_config.bridge_token, + ) + + +def build_runtime_context(container: RuntimeContainer | None = None) -> RuntimeContext: + """Resolve runtime context from dependency container.""" + runtime_container = container or RuntimeContainer() + + boot_config = runtime_container.bootstrap_factory() + service_manager = create_service_manager( + boot_config=boot_config, + service_manager_factory=runtime_container.service_manager_factory, + ) + mqtt_config = runtime_container.mqtt_config_factory(boot_config.config) + + return RuntimeContext( + boot_config=boot_config, + service_manager=service_manager, + mqtt_config=mqtt_config, + ) diff --git a/iot_bridge/main.py b/iot_bridge/main.py index a0c392a..e0f58d7 100644 --- a/iot_bridge/main.py +++ b/iot_bridge/main.py @@ -6,8 +6,7 @@ Clean entry point that delegates to bootstrap and service_manager modules. This file should remain minimal (~80 lines) and focused on orchestration. """ -from core.bootstrap import bootstrap, get_mqtt_config -from core.service_manager import ServiceManager +from dependencies import build_runtime_context def main(): @@ -20,27 +19,15 @@ def main(): 3. Main Loop: Run until shutdown signal 4. Shutdown: Gracefully stop all services """ - # Phase 1: Bootstrap - # - Parse command-line arguments - # - Load configuration (with persisted config fallback) - # - Setup structured logging - boot_config = bootstrap() - - # Phase 2: Initialize Service Manager - # - Manages all service lifecycles - # - Handles graceful shutdown - service_manager = ServiceManager( - config=boot_config.config, - logger=boot_config.logger, - bridge_port=boot_config.bridge_port, - bridge_token=boot_config.bridge_token, - ) + # Phase 1-2: Resolve runtime context via dependency container + runtime = build_runtime_context() + service_manager = runtime.service_manager # Phase 3: Setup signal handlers for graceful shutdown service_manager.setup_signal_handlers() # Phase 4: Get MQTT configuration - mqtt_config = get_mqtt_config(boot_config.config) + mqtt_config = runtime.mqtt_config # Phase 5: Start all services # - Odoo client diff --git a/iot_bridge/tests/unit/test_dependencies.py b/iot_bridge/tests/unit/test_dependencies.py new file mode 100644 index 0000000..865412d --- /dev/null +++ b/iot_bridge/tests/unit/test_dependencies.py @@ -0,0 +1,72 @@ +"""Unit tests for dependency injection runtime wiring.""" + +from types import SimpleNamespace + +from dependencies import RuntimeContainer, build_runtime_context, create_service_manager + + +def test_create_service_manager_uses_factory_with_boot_config_fields(): + """create_service_manager should pass boot config fields to injected factory.""" + calls = {} + + def fake_service_manager_factory(**kwargs): + calls.update(kwargs) + return "service-manager-instance" + + boot_config = SimpleNamespace( + config={"some": "config"}, + logger="logger-instance", + bridge_port=8080, + bridge_token="token-123", + ) + + result = create_service_manager( + boot_config=boot_config, + service_manager_factory=fake_service_manager_factory, + ) + + assert result == "service-manager-instance" + assert calls["config"] == {"some": "config"} + assert calls["logger"] == "logger-instance" + assert calls["bridge_port"] == 8080 + assert calls["bridge_token"] == "token-123" + + +def test_build_runtime_context_uses_injected_container_factories(): + """Runtime context should be fully constructed from injected container factories.""" + boot_config = SimpleNamespace( + config={"bridge": "config"}, + logger="logger", + bridge_port=9000, + bridge_token="abc", + ) + + called = {"bootstrap": 0, "mqtt": 0, "service": 0} + + def fake_bootstrap_factory(): + called["bootstrap"] += 1 + return boot_config + + def fake_mqtt_factory(config): + called["mqtt"] += 1 + assert config == {"bridge": "config"} + return {"broker": "test-broker", "port": 1883} + + def fake_service_manager_factory(**kwargs): + called["service"] += 1 + assert kwargs["bridge_port"] == 9000 + assert kwargs["bridge_token"] == "abc" + return "service-manager" + + container = RuntimeContainer( + bootstrap_factory=fake_bootstrap_factory, + mqtt_config_factory=fake_mqtt_factory, + service_manager_factory=fake_service_manager_factory, + ) + + runtime = build_runtime_context(container) + + assert runtime.boot_config is boot_config + assert runtime.service_manager == "service-manager" + assert runtime.mqtt_config == {"broker": "test-broker", "port": 1883} + assert called == {"bootstrap": 1, "mqtt": 1, "service": 1}