""" Tests for decnet.config — Pydantic models, save/load/clear state. Covers the uncovered lines: validators, save_state, load_state, clear_state. """ import json import pytest from pathlib import Path from unittest.mock import patch import decnet.config as config_module from decnet.config import ( DeckyConfig, DecnetConfig, save_state, load_state, clear_state, ) # --------------------------------------------------------------------------- # DeckyConfig validator # --------------------------------------------------------------------------- class TestDeckyConfig: def _base(self, **kwargs): defaults = dict( name="decky-01", ip="192.168.1.10", services=["ssh"], distro="debian", base_image="debian", hostname="host-01", ) defaults.update(kwargs) return defaults def test_valid_decky(self): d = DeckyConfig(**self._base()) assert d.name == "decky-01" def test_empty_services_raises(self): with pytest.raises(Exception, match="at least one service"): DeckyConfig(**self._base(services=[])) def test_multiple_services_ok(self): d = DeckyConfig(**self._base(services=["ssh", "smb", "rdp"])) assert len(d.services) == 3 # --------------------------------------------------------------------------- # DecnetConfig validator # --------------------------------------------------------------------------- class TestDecnetConfig: def _base_decky(self): return DeckyConfig( name="d", ip="10.0.0.2", services=["ssh"], distro="debian", base_image="debian", hostname="h", ) def test_valid_config(self): cfg = DecnetConfig( mode="unihost", interface="eth0", subnet="10.0.0.0/24", gateway="10.0.0.1", deckies=[self._base_decky()], ) assert cfg.mode == "unihost" def test_valid_log_target(self): cfg = DecnetConfig( mode="unihost", interface="eth0", subnet="10.0.0.0/24", gateway="10.0.0.1", deckies=[self._base_decky()], log_target="192.168.1.5:5140", ) assert cfg.log_target == "192.168.1.5:5140" def test_none_log_target_ok(self): cfg = DecnetConfig( mode="unihost", interface="eth0", subnet="10.0.0.0/24", gateway="10.0.0.1", deckies=[self._base_decky()], log_target=None, ) assert cfg.log_target is None def test_invalid_log_target_no_port(self): with pytest.raises(Exception): DecnetConfig( mode="unihost", interface="eth0", subnet="10.0.0.0/24", gateway="10.0.0.1", deckies=[self._base_decky()], log_target="192.168.1.5", ) def test_invalid_log_target_non_digit_port(self): with pytest.raises(Exception): DecnetConfig( mode="unihost", interface="eth0", subnet="10.0.0.0/24", gateway="10.0.0.1", deckies=[self._base_decky()], log_target="192.168.1.5:abc", ) # --------------------------------------------------------------------------- # save_state / load_state / clear_state # --------------------------------------------------------------------------- @pytest.fixture(autouse=True) def patch_state_file(tmp_path, monkeypatch): monkeypatch.setattr(config_module, "STATE_FILE", tmp_path / "decnet-state.json") def _sample_config(): return DecnetConfig( mode="unihost", interface="eth0", subnet="192.168.1.0/24", gateway="192.168.1.1", deckies=[ DeckyConfig( name="decky-01", ip="192.168.1.10", services=["ssh"], distro="debian", base_image="debian", hostname="host-01", ) ], log_target="10.0.0.1:5140", ) def test_save_and_load_state(tmp_path): cfg = _sample_config() compose = tmp_path / "docker-compose.yml" save_state(cfg, compose) result = load_state() assert result is not None loaded_cfg, loaded_compose = result assert loaded_cfg.mode == "unihost" assert loaded_cfg.deckies[0].name == "decky-01" assert loaded_cfg.log_target == "10.0.0.1:5140" assert loaded_compose == compose def test_load_state_returns_none_when_missing(tmp_path, monkeypatch): monkeypatch.setattr(config_module, "STATE_FILE", tmp_path / "nonexistent.json") assert load_state() is None def test_clear_state(tmp_path): cfg = _sample_config() save_state(cfg, tmp_path / "compose.yml") assert config_module.STATE_FILE.exists() clear_state() assert not config_module.STATE_FILE.exists() def test_clear_state_noop_when_missing(tmp_path, monkeypatch): monkeypatch.setattr(config_module, "STATE_FILE", tmp_path / "nonexistent.json") clear_state() # should not raise def test_state_roundtrip_preserves_all_fields(tmp_path): cfg = _sample_config() cfg.deckies[0].archetype = "workstation" cfg.deckies[0].mutate_interval = 45 compose = tmp_path / "compose.yml" save_state(cfg, compose) loaded_cfg, _ = load_state() assert loaded_cfg.deckies[0].archetype == "workstation" assert loaded_cfg.deckies[0].mutate_interval == 45