from __future__ import annotations from pathlib import Path import importlib import os import sys import tempfile import pytest from fastapi.testclient import TestClient PROJECT_ROOT = Path(__file__).resolve().parents[1] if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) from app.config import AppConfig, ConfigStore, DEVICE_ROLE_ENV_VAR def _require_module(module_name: str): try: return importlib.import_module(module_name) except ModuleNotFoundError as exc: pytest.skip(f"Module '{module_name}' is not available yet: {exc}") def test_config_store_missing_file_and_roundtrip_save(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv(DEVICE_ROLE_ENV_VAR, "tower") config_path = Path(tempfile.mkdtemp()) / "config.json" store = ConfigStore(config_path) assert store.get() == AppConfig(device_role="tower", device_port="DP1") assert not config_path.exists() saved = store.save(AppConfig(device_role="tower", device_port="HDMI")) assert saved.device_role == "tower" assert saved.device_port == "HDMI" assert config_path.exists() reloaded = ConfigStore(config_path).get() assert reloaded.device_role == "tower" assert reloaded.device_port == "HDMI" def test_api_status_and_settings_endpoints_with_fake_service() -> None: app_main = _require_module("app.main") create_app = getattr(app_main, "create_app", None) if create_app is None: pytest.skip("app.main.create_app is not available yet.") class FakeService: def __init__(self) -> None: self.payload = { "config": {"device_role": "tower", "device_port": "DP1"}, "samsung_present": False, "samsung_connected_session_active": False, "samsung_session_attempted": False, "samsung_session_successful": False, "samsung_session_attempt_count": 0, "waiting_for_samsung_disconnect": False, "trigger_input_code": None, "alienware_detected": False, "alienware_input_code": None, "resolved_target": None, "ddm_slot": None, "ddm_ready": False, "last_switch_result": "idle", "last_switch_at": None, "errors": [], } def start(self) -> None: return None def stop(self) -> None: return None def get_status(self) -> dict[str, object]: return self.payload def save_settings(self, device_role: str, device_port: str) -> dict[str, object]: self.payload["config"] = { "device_role": device_role, "device_port": device_port, } self.payload["last_switch_result"] = "updated" return self.payload app = create_app(service=FakeService(), manage_lifecycle=False) with TestClient(app) as client: status_response = client.get("/api/status") assert status_response.status_code == 200 assert status_response.json()["config"]["device_role"] == "tower" save_response = client.post( "/api/settings", json={"device_role": "laptop", "device_port": "HDMI"}, ) assert save_response.status_code == 200 assert save_response.json()["config"]["device_role"] == "laptop" assert save_response.json()["config"]["device_port"] == "HDMI" def test_polling_switches_when_trigger_matches_device_role(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv(DEVICE_ROLE_ENV_VAR, "laptop") service_module = _require_module("app.service") hardware_module = _require_module("app.hardware") ddm_module = _require_module("app.ddm") KvmSwitcherService = getattr(service_module, "KvmSwitcherService", None) HardwareScan = getattr(hardware_module, "HardwareScan", None) DDMCommandResult = getattr(ddm_module, "DDMCommandResult", None) if KvmSwitcherService is None or HardwareScan is None or DDMCommandResult is None: pytest.skip("Service backend interfaces are not available yet.") class FakeMonitorBackend: def __init__(self) -> None: self.call_count = 0 def scan(self): self.call_count += 1 if self.call_count == 1: return HardwareScan( samsung_present=True, trigger_input_code=19, alienware_detected=True, alienware_input_code=15, errors=[], ) return HardwareScan( samsung_present=True, trigger_input_code=19, alienware_detected=True, alienware_input_code=17, errors=[], ) class FakeDDMBackend: def __init__(self) -> None: self.calls: list[tuple[int, str]] = [] self.slot = 1 def is_available(self) -> bool: return True def resolve_alienware_slot(self, force: bool = False) -> int | None: return self.slot def invalidate_slot(self) -> None: self.slot = None def switch_to_port(self, slot: int, port_name: str): self.calls.append((slot, port_name)) return DDMCommandResult(True, "ok") config_path = Path(tempfile.mkdtemp()) / "config.json" service = KvmSwitcherService( config_store=ConfigStore(config_path), monitor_backend=FakeMonitorBackend(), ddm_backend=FakeDDMBackend(), poll_interval_seconds=0.01, retry_wait_seconds=0.0, ) status = service.save_settings(device_role="laptop", device_port="HDMI") assert status["resolved_target"] == "laptop" assert status["config"]["device_role"] == "laptop" assert status["config"]["device_port"] == "HDMI" assert status["samsung_session_attempted"] is True assert status["samsung_session_successful"] is True assert status["last_switch_result"] == "switched"