Files
compose-farm/tests/test_state.py
2025-12-20 16:00:41 -08:00

242 lines
8.0 KiB
Python

"""Tests for state module."""
from pathlib import Path
import pytest
from compose_farm.config import Config, Host
from compose_farm.state import (
get_orphaned_stacks,
get_stack_host,
get_stacks_not_in_state,
load_state,
remove_stack,
save_state,
set_stack_host,
)
@pytest.fixture
def config(tmp_path: Path) -> Config:
"""Create a config with a temporary config path for state storage."""
config_path = tmp_path / "compose-farm.yaml"
config_path.write_text("") # Create empty file
return Config(
compose_dir=tmp_path / "compose",
hosts={"nas01": Host(address="192.168.1.10")},
stacks={"plex": "nas01"},
config_path=config_path,
)
class TestLoadState:
"""Tests for load_state function."""
def test_load_state_empty(self, config: Config) -> None:
"""Returns empty dict when state file doesn't exist."""
result = load_state(config)
assert result == {}
def test_load_state_with_data(self, config: Config) -> None:
"""Loads existing state from file."""
state_file = config.get_state_path()
state_file.write_text("deployed:\n plex: nas01\n jellyfin: nas02\n")
result = load_state(config)
assert result == {"plex": "nas01", "jellyfin": "nas02"}
def test_load_state_empty_file(self, config: Config) -> None:
"""Returns empty dict for empty file."""
state_file = config.get_state_path()
state_file.write_text("")
result = load_state(config)
assert result == {}
class TestSaveState:
"""Tests for save_state function."""
def test_save_state(self, config: Config) -> None:
"""Saves state to file."""
save_state(config, {"plex": "nas01", "jellyfin": "nas02"})
state_file = config.get_state_path()
assert state_file.exists()
content = state_file.read_text()
assert "plex: nas01" in content
assert "jellyfin: nas02" in content
class TestGetStackHost:
"""Tests for get_stack_host function."""
def test_get_existing_stack(self, config: Config) -> None:
"""Returns host for existing stack."""
state_file = config.get_state_path()
state_file.write_text("deployed:\n plex: nas01\n")
host = get_stack_host(config, "plex")
assert host == "nas01"
def test_get_nonexistent_stack(self, config: Config) -> None:
"""Returns None for stack not in state."""
state_file = config.get_state_path()
state_file.write_text("deployed:\n plex: nas01\n")
host = get_stack_host(config, "unknown")
assert host is None
class TestSetStackHost:
"""Tests for set_stack_host function."""
def test_set_new_stack(self, config: Config) -> None:
"""Adds new stack to state."""
set_stack_host(config, "plex", "nas01")
result = load_state(config)
assert result["plex"] == "nas01"
def test_update_existing_stack(self, config: Config) -> None:
"""Updates host for existing stack."""
state_file = config.get_state_path()
state_file.write_text("deployed:\n plex: nas01\n")
set_stack_host(config, "plex", "nas02")
result = load_state(config)
assert result["plex"] == "nas02"
class TestRemoveStack:
"""Tests for remove_stack function."""
def test_remove_existing_stack(self, config: Config) -> None:
"""Removes stack from state."""
state_file = config.get_state_path()
state_file.write_text("deployed:\n plex: nas01\n jellyfin: nas02\n")
remove_stack(config, "plex")
result = load_state(config)
assert "plex" not in result
assert result["jellyfin"] == "nas02"
def test_remove_nonexistent_stack(self, config: Config) -> None:
"""Removing nonexistent stack doesn't error."""
state_file = config.get_state_path()
state_file.write_text("deployed:\n plex: nas01\n")
remove_stack(config, "unknown") # Should not raise
result = load_state(config)
assert result["plex"] == "nas01"
class TestGetOrphanedStacks:
"""Tests for get_orphaned_stacks function."""
def test_no_orphans(self, config: Config) -> None:
"""Returns empty dict when all stacks in state are in config."""
state_file = config.get_state_path()
state_file.write_text("deployed:\n plex: nas01\n")
result = get_orphaned_stacks(config)
assert result == {}
def test_finds_orphaned_stack(self, config: Config) -> None:
"""Returns stacks in state but not in config."""
state_file = config.get_state_path()
state_file.write_text("deployed:\n plex: nas01\n jellyfin: nas02\n")
result = get_orphaned_stacks(config)
# plex is in config, jellyfin is not
assert result == {"jellyfin": "nas02"}
def test_finds_orphaned_multi_host_stack(self, config: Config) -> None:
"""Returns multi-host orphaned stacks with host list."""
state_file = config.get_state_path()
state_file.write_text("deployed:\n plex: nas01\n dozzle:\n - nas01\n - nas02\n")
result = get_orphaned_stacks(config)
assert result == {"dozzle": ["nas01", "nas02"]}
def test_empty_state(self, config: Config) -> None:
"""Returns empty dict when state is empty."""
result = get_orphaned_stacks(config)
assert result == {}
def test_all_orphaned(self, tmp_path: Path) -> None:
"""Returns all stacks when none are in config."""
config_path = tmp_path / "compose-farm.yaml"
config_path.write_text("")
cfg = Config(
compose_dir=tmp_path / "compose",
hosts={"nas01": Host(address="192.168.1.10")},
stacks={}, # No stacks in config
config_path=config_path,
)
state_file = cfg.get_state_path()
state_file.write_text("deployed:\n plex: nas01\n jellyfin: nas02\n")
result = get_orphaned_stacks(cfg)
assert result == {"plex": "nas01", "jellyfin": "nas02"}
class TestGetStacksNotInState:
"""Tests for get_stacks_not_in_state function."""
def test_all_in_state(self, config: Config) -> None:
"""Returns empty list when all stacks are in state."""
state_file = config.get_state_path()
state_file.write_text("deployed:\n plex: nas01\n")
result = get_stacks_not_in_state(config)
assert result == []
def test_finds_missing_service(self, tmp_path: Path) -> None:
"""Returns stacks in config but not in state."""
config_path = tmp_path / "compose-farm.yaml"
config_path.write_text("")
cfg = Config(
compose_dir=tmp_path / "compose",
hosts={"nas01": Host(address="192.168.1.10")},
stacks={"plex": "nas01", "jellyfin": "nas01"},
config_path=config_path,
)
state_file = cfg.get_state_path()
state_file.write_text("deployed:\n plex: nas01\n")
result = get_stacks_not_in_state(cfg)
assert result == ["jellyfin"]
def test_empty_state(self, tmp_path: Path) -> None:
"""Returns all stacks when state is empty."""
config_path = tmp_path / "compose-farm.yaml"
config_path.write_text("")
cfg = Config(
compose_dir=tmp_path / "compose",
hosts={"nas01": Host(address="192.168.1.10")},
stacks={"plex": "nas01", "jellyfin": "nas01"},
config_path=config_path,
)
result = get_stacks_not_in_state(cfg)
assert set(result) == {"plex", "jellyfin"}
def test_empty_config(self, config: Config) -> None:
"""Returns empty list when config has no stacks."""
# config fixture has plex: nas01, but we need empty config
config_path = config.config_path
config_path.write_text("")
cfg = Config(
compose_dir=config.compose_dir,
hosts={"nas01": Host(address="192.168.1.10")},
stacks={},
config_path=config_path,
)
result = get_stacks_not_in_state(cfg)
assert result == []