import sys import tkinter as tk from pathlib import Path from unittest.mock import MagicMock import pytest # Ensure `installer` is importable when tests are invoked directly via `pytest` PROJECT_ROOT = Path(__file__).resolve().parents[2] if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) import knoe.ui.screens.cluster as cluster_mod from knoe.ui.screens.base import ScreenBaseMixin from knoe.ui.screens.cluster import ClusterScreenMixin class _DummyClusterSaveApp(ScreenBaseMixin, ClusterScreenMixin): """Minimal surface to exercise the Cluster Environment screen Save behavior.""" def __init__(self, root: tk.Tk): self.root = root # Canvas is not required by `_validate_and_save_cluster_config`, but other mixins # assume it exists. self.bg_canvas = tk.Canvas(root, width=1, height=1) self._canvas_items = [] self._overlay_widgets = [] self.cluster_env = tk.StringVar(master=root, value="service") self.selected_k3d_cluster = tk.StringVar(master=root, value="") self.selected_kubectx = tk.StringVar(master=root, value="") self.service_namespace = tk.StringVar(master=root, value="knoe-system") self.prod_artifacts_path = tk.StringVar(master=root, value=str(Path.home())) self.k3s_server_url = tk.StringVar(master=root, value="") self.k3s_token = tk.StringVar(master=root, value="") self.supabase_enabled = tk.BooleanVar(master=root, value=False) self.gitops_enabled = tk.BooleanVar(master=root, value=False) self.kerberos_enabled = tk.BooleanVar(master=root, value=False) self.at_rest_encryption_enabled = tk.BooleanVar(master=root, value=False) # Minimal config structure required by `_validate_and_save_cluster_config`. self.knoe_cfg_data = { "Initialize Cluster": { "ENVIRONMENT": "service", "K3S_SERVER_URL": "", "K3S_TOKEN": "", }, "Global": { # Keep old/new equal so we don't trigger the namespace cleanup prompt. "SERVICE_NAMESPACE": "knoe-system", }, "Optional Features": {}, "Prod Cluster (k8s)": {}, } self.saved_calls = 0 self.status_check_calls = 0 def _save_knoe_cfg(self): self.saved_calls += 1 def _verify_k3s_services(self): self.status_check_calls += 1 def test_cluster_env_save_triggers_status_check(monkeypatch): # Some test modules replace tkinter with MagicMocks at import-time. if isinstance(sys.modules.get("tkinter"), MagicMock): pytest.skip("tkinter is mocked in this test run") # Avoid depending on a real kubeconfig file. monkeypatch.setattr(cluster_mod, "_find_kubeconfig_file", lambda: "/tmp/kubeconfig") # Avoid any real secret handling. monkeypatch.setattr(cluster_mod, "_encrypt_cfg_secret", lambda s: s) root = tk.Tk() root.withdraw() try: app = _DummyClusterSaveApp(root) assert app._validate_and_save_cluster_config() is True assert app.saved_calls == 1 # Requirement: clicking Save should immediately run a status check. assert app.status_check_calls == 1 finally: try: root.destroy() except Exception: pass def test_cluster_env_prod_artifacts_dir_expands_knoe_home_from_cfg(monkeypatch, tmp_path): # Some test modules replace tkinter with MagicMocks at import-time. if isinstance(sys.modules.get("tkinter"), MagicMock): pytest.skip("tkinter is mocked in this test run") # Ensure `$KNOE_HOME` is NOT available as an OS env var; it must come from config. monkeypatch.delenv("KNOE_HOME", raising=False) monkeypatch.chdir(tmp_path) root = tk.Tk() root.withdraw() try: app = _DummyClusterSaveApp(root) # Switch to prod so the artifacts directory is validated/created. app.cluster_env.set("prod") # Provide KNOE_HOME via config data only. knoe_home = tmp_path / "knoe-home" knoe_home.mkdir(parents=True, exist_ok=True) app.knoe_cfg_data.setdefault("System Environment", {})["KNOE_HOME"] = str( knoe_home ) # Use a path expression that previously would create a literal `$KNOE_HOME` dir. app.prod_artifacts_path.set("$KNOE_HOME/staging") assert app._validate_and_save_cluster_config() is True assert (knoe_home / "staging").is_dir() assert not (tmp_path / "$KNOE_HOME" / "staging").exists() finally: try: root.destroy() except Exception: pass