"""Tests for config command module.""" from pathlib import Path from typing import Any import pytest import yaml from typer.testing import CliRunner from compose_farm.cli import app from compose_farm.cli.config import ( _detect_domain, _generate_template, _get_config_file, _get_editor, ) from compose_farm.config import Config, Host @pytest.fixture def runner() -> CliRunner: return CliRunner() @pytest.fixture def valid_config_data() -> dict[str, Any]: return { "compose_dir": "/opt/compose", "hosts": {"server1": "192.168.1.10"}, "stacks": {"nginx": "server1"}, } class TestGetEditor: """Tests for _get_editor function.""" def test_uses_editor_env(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("EDITOR", "code") monkeypatch.delenv("VISUAL", raising=False) assert _get_editor() == "code" def test_uses_visual_env(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("EDITOR", raising=False) monkeypatch.setenv("VISUAL", "subl") assert _get_editor() == "subl" def test_editor_takes_precedence(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setenv("EDITOR", "vim") monkeypatch.setenv("VISUAL", "code") assert _get_editor() == "vim" class TestGetConfigFile: """Tests for _get_config_file function.""" def test_explicit_path(self, tmp_path: Path) -> None: config_file = tmp_path / "my-config.yaml" config_file.touch() result = _get_config_file(config_file) assert result == config_file.resolve() def test_cf_config_env(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: config_file = tmp_path / "env-config.yaml" config_file.touch() monkeypatch.setenv("CF_CONFIG", str(config_file)) result = _get_config_file(None) assert result == config_file.resolve() def test_returns_none_when_not_found( self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.chdir(tmp_path) monkeypatch.delenv("CF_CONFIG", raising=False) # Set XDG_CONFIG_HOME to a nonexistent path - config_search_paths() will # now return paths that don't exist monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "nonexistent")) result = _get_config_file(None) assert result is None class TestGenerateTemplate: """Tests for _generate_template function.""" def test_generates_valid_yaml(self) -> None: template = _generate_template() # Should be valid YAML data = yaml.safe_load(template) assert "compose_dir" in data assert "hosts" in data assert "stacks" in data def test_has_documentation_comments(self) -> None: template = _generate_template() assert "# Compose Farm configuration" in template assert "hosts:" in template assert "stacks:" in template class TestConfigInit: """Tests for cf config init command.""" def test_init_creates_file( self, runner: CliRunner, tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.delenv("CF_CONFIG", raising=False) config_file = tmp_path / "new-config.yaml" result = runner.invoke(app, ["config", "init", "-p", str(config_file)]) assert result.exit_code == 0 assert config_file.exists() assert "Config file created" in result.stdout def test_init_force_overwrites( self, runner: CliRunner, tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.delenv("CF_CONFIG", raising=False) config_file = tmp_path / "existing.yaml" config_file.write_text("old content") result = runner.invoke(app, ["config", "init", "-p", str(config_file), "-f"]) assert result.exit_code == 0 content = config_file.read_text() assert "old content" not in content assert "compose_dir" in content def test_init_prompts_on_existing( self, runner: CliRunner, tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.delenv("CF_CONFIG", raising=False) config_file = tmp_path / "existing.yaml" config_file.write_text("old content") result = runner.invoke(app, ["config", "init", "-p", str(config_file)], input="n\n") assert result.exit_code == 0 assert "Aborted" in result.stdout assert config_file.read_text() == "old content" class TestConfigPath: """Tests for cf config path command.""" def test_path_shows_config( self, runner: CliRunner, tmp_path: Path, valid_config_data: dict[str, Any], monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.chdir(tmp_path) monkeypatch.delenv("CF_CONFIG", raising=False) config_file = tmp_path / "compose-farm.yaml" config_file.write_text(yaml.dump(valid_config_data)) result = runner.invoke(app, ["config", "path"]) assert result.exit_code == 0 assert str(config_file) in result.stdout def test_path_with_explicit_path(self, runner: CliRunner, tmp_path: Path) -> None: # When explicitly provided, path is returned even if file doesn't exist nonexistent = tmp_path / "nonexistent.yaml" result = runner.invoke(app, ["config", "path", "-p", str(nonexistent)]) assert result.exit_code == 0 assert str(nonexistent) in result.stdout class TestConfigShow: """Tests for cf config show command.""" def test_show_displays_content( self, runner: CliRunner, tmp_path: Path, valid_config_data: dict[str, Any], monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.chdir(tmp_path) monkeypatch.delenv("CF_CONFIG", raising=False) config_file = tmp_path / "compose-farm.yaml" config_file.write_text(yaml.dump(valid_config_data)) result = runner.invoke(app, ["config", "show"]) assert result.exit_code == 0 assert "Config file:" in result.stdout def test_show_raw_output( self, runner: CliRunner, tmp_path: Path, valid_config_data: dict[str, Any], monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.chdir(tmp_path) monkeypatch.delenv("CF_CONFIG", raising=False) config_file = tmp_path / "compose-farm.yaml" content = yaml.dump(valid_config_data) config_file.write_text(content) result = runner.invoke(app, ["config", "show", "-r"]) assert result.exit_code == 0 assert content in result.stdout class TestConfigValidate: """Tests for cf config validate command.""" def test_validate_valid_config( self, runner: CliRunner, tmp_path: Path, valid_config_data: dict[str, Any], monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.chdir(tmp_path) monkeypatch.delenv("CF_CONFIG", raising=False) config_file = tmp_path / "compose-farm.yaml" config_file.write_text(yaml.dump(valid_config_data)) result = runner.invoke(app, ["config", "validate"]) assert result.exit_code == 0 assert "Valid config" in result.stdout assert "Hosts: 1" in result.stdout assert "Stacks: 1" in result.stdout def test_validate_invalid_config(self, runner: CliRunner, tmp_path: Path) -> None: config_file = tmp_path / "invalid.yaml" config_file.write_text("invalid: [yaml: content") result = runner.invoke(app, ["config", "validate", "-p", str(config_file)]) assert result.exit_code == 1 # Error goes to stderr (captured in output when using CliRunner) output = result.stdout + (result.stderr or "") assert "Invalid config" in output or "✗" in output def test_validate_missing_config(self, runner: CliRunner, tmp_path: Path) -> None: nonexistent = tmp_path / "nonexistent.yaml" result = runner.invoke(app, ["config", "validate", "-p", str(nonexistent)]) assert result.exit_code == 1 # Error goes to stderr output = result.stdout + (result.stderr or "") assert "Config file not found" in output or "not found" in output.lower() class TestDetectDomain: """Tests for _detect_domain function.""" def test_returns_none_for_empty_stacks(self) -> None: cfg = Config( compose_dir=Path("/opt/compose"), hosts={"nas": Host(address="192.168.1.6")}, stacks={}, ) result = _detect_domain(cfg) assert result is None def test_skips_local_domains(self, tmp_path: Path) -> None: # Create a minimal compose file with .local domain stack_dir = tmp_path / "test" stack_dir.mkdir() compose = stack_dir / "compose.yaml" compose.write_text( """ name: test services: web: image: nginx labels: - "traefik.http.routers.test-local.rule=Host(`test.local`)" """ ) cfg = Config( compose_dir=tmp_path, hosts={"nas": Host(address="192.168.1.6")}, stacks={"test": "nas"}, ) result = _detect_domain(cfg) # .local should be skipped assert result is None class TestConfigInitEnv: """Tests for cf config init-env command.""" def test_init_env_creates_file( self, runner: CliRunner, tmp_path: Path, valid_config_data: dict[str, Any], monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.delenv("CF_CONFIG", raising=False) config_file = tmp_path / "compose-farm.yaml" config_file.write_text(yaml.dump(valid_config_data)) env_file = tmp_path / ".env" result = runner.invoke( app, ["config", "init-env", "-p", str(config_file), "-o", str(env_file)] ) assert result.exit_code == 0 assert env_file.exists() content = env_file.read_text() assert "CF_COMPOSE_DIR=/opt/compose" in content assert "CF_UID=" in content assert "CF_GID=" in content def test_init_env_force_overwrites( self, runner: CliRunner, tmp_path: Path, valid_config_data: dict[str, Any], monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.delenv("CF_CONFIG", raising=False) config_file = tmp_path / "compose-farm.yaml" config_file.write_text(yaml.dump(valid_config_data)) env_file = tmp_path / ".env" env_file.write_text("OLD_CONTENT=true") result = runner.invoke( app, ["config", "init-env", "-p", str(config_file), "-o", str(env_file), "-f"] ) assert result.exit_code == 0 content = env_file.read_text() assert "OLD_CONTENT" not in content assert "CF_COMPOSE_DIR" in content def test_init_env_prompts_on_existing( self, runner: CliRunner, tmp_path: Path, valid_config_data: dict[str, Any], monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.delenv("CF_CONFIG", raising=False) config_file = tmp_path / "compose-farm.yaml" config_file.write_text(yaml.dump(valid_config_data)) env_file = tmp_path / ".env" env_file.write_text("KEEP_THIS=true") result = runner.invoke( app, ["config", "init-env", "-p", str(config_file), "-o", str(env_file)], input="n\n", ) assert result.exit_code == 0 assert "Aborted" in result.stdout assert env_file.read_text() == "KEEP_THIS=true" def test_init_env_defaults_to_current_dir( self, runner: CliRunner, tmp_path: Path, valid_config_data: dict[str, Any], monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.delenv("CF_CONFIG", raising=False) config_dir = tmp_path / "config" config_dir.mkdir() config_file = config_dir / "compose-farm.yaml" config_file.write_text(yaml.dump(valid_config_data)) # Create a separate working directory work_dir = tmp_path / "workdir" work_dir.mkdir() monkeypatch.chdir(work_dir) result = runner.invoke(app, ["config", "init-env", "-p", str(config_file)]) assert result.exit_code == 0 # Should create .env in current directory, not config directory env_file = work_dir / ".env" assert env_file.exists() assert not (config_dir / ".env").exists()