"""Tests para refresh_local_hub. No recarga Caddy ni reinicia Glance de verdad: mockea `subprocess.run`. Escribe las configs generadas en un `tmp_path` parcheando las rutas de salida. Mockea `discover_local_services` para devolver 2 servicios fijos (no depende de puertos reales). """ from __future__ import annotations import os import yaml import pytest from pipelines import refresh_local_hub as rlh_module from pipelines.refresh_local_hub import refresh_local_hub FAKE_SERVICES = [ { "name": "metabase", "subdomain": "metabase", "port": 3030, "health_path": "/api/health", "title": "Metabase", "icon": "si:metabase", "category": "Datos", "up": True, }, { "name": "portainer", "subdomain": "portainer", "port": 9000, "health_path": "/", "title": "Portainer", "icon": "si:portainer", "category": "Infra", "up": False, }, ] def _write_manifest(tmp_path) -> str: """Crea un manifiesto YAML mínimo y devuelve su ruta.""" manifest = { "dashboard_subdomain": "home", "glance_port": 8585, "services": [], } path = os.path.join(str(tmp_path), "local_services.yaml") with open(path, "w", encoding="utf-8") as fh: yaml.safe_dump(manifest, fh) return path @pytest.fixture def patched_env(tmp_path, monkeypatch): """Parchea discover_local_services, las rutas de salida y subprocess.run.""" # discover_local_services devuelve 2 servicios fijos. monkeypatch.setattr(rlh_module, "discover_local_services", lambda *a, **k: list(FAKE_SERVICES)) # Rutas de salida hacia tmp_path. caddy_path = os.path.join(str(tmp_path), "local_hub.caddy") monkeypatch.setattr(rlh_module, "CADDY_FRAGMENT_PATH", caddy_path) monkeypatch.setattr(rlh_module, "_registry_root", lambda: str(tmp_path)) # subprocess.run mockeado: registra las llamadas y devuelve un objeto con returncode 0. calls: list[list[str]] = [] class _FakeProc: def __init__(self, rc: int = 0) -> None: self.returncode = rc self.stdout = "" self.stderr = "" def _fake_run(cmd, *args, **kwargs): calls.append(list(cmd)) return _FakeProc(0) monkeypatch.setattr(rlh_module.subprocess, "run", _fake_run) manifest_path = _write_manifest(tmp_path) return { "manifest_path": manifest_path, "caddy_path": caddy_path, "glance_path": os.path.join(str(tmp_path), "apps", "local_hub", "glance", "glance.yml"), "calls": calls, "tmp_path": str(tmp_path), } def test_compone_las_tres_funciones_y_escribe_configs(patched_env): """compone las tres funciones del registry y escribe ambas configs""" result = refresh_local_hub(manifest_path=patched_env["manifest_path"], reload=False) # Caddyfile escrito con un bloque por servicio + dashboard. assert os.path.exists(patched_env["caddy_path"]) caddy_text = open(patched_env["caddy_path"], encoding="utf-8").read() assert "metabase.localhost" in caddy_text assert "portainer.localhost" in caddy_text assert "home.localhost" in caddy_text # bloque del dashboard assert "reverse_proxy 127.0.0.1:3030" in caddy_text # Glance escrito con el bloque servidor fijo + la salida de render_glance_config. assert os.path.exists(result["glance_path"]) glance_text = open(result["glance_path"], encoding="utf-8").read() assert "host: 127.0.0.1" in glance_text assert "port: 8585" in glance_text assert "primary-color: 210 90 70" in glance_text assert "type: monitor" in glance_text def test_glance_full_es_yaml_parseable(patched_env): """la config completa de Glance generada es YAML parseable""" result = refresh_local_hub(manifest_path=patched_env["manifest_path"], reload=False) glance_text = open(result["glance_path"], encoding="utf-8").read() parsed = yaml.safe_load(glance_text) assert isinstance(parsed, dict) assert parsed["server"]["host"] == "127.0.0.1" assert parsed["server"]["port"] == 8585 assert "theme" in parsed assert "pages" in parsed # la salida de render_glance_config def test_reload_false_no_llama_subprocess(patched_env): """con reload=False no se llama a subprocess.run""" result = refresh_local_hub(manifest_path=patched_env["manifest_path"], reload=False) assert patched_env["calls"] == [] assert result["reloaded"] is False assert result["caddy_reload_rc"] is None assert result["glance_restart_rc"] is None def test_reload_true_recarga_caddy_y_reinicia_glance(patched_env): """con reload=True se invoca caddy reload y systemctl --user restart glance""" result = refresh_local_hub(manifest_path=patched_env["manifest_path"], reload=True) cmds = patched_env["calls"] assert len(cmds) == 2 assert cmds[0][0] == "caddy" and "reload" in cmds[0] assert cmds[1] == ["systemctl", "--user", "restart", "glance"] assert result["reloaded"] is True assert result["caddy_reload_rc"] == 0 assert result["glance_restart_rc"] == 0 def test_dict_retorno_tiene_claves_esperadas(patched_env): """el dict de retorno tiene todas las claves del contrato""" result = refresh_local_hub(manifest_path=patched_env["manifest_path"], reload=False) expected_keys = { "total", "up", "down", "caddy_path", "glance_path", "reloaded", "caddy_reload_rc", "glance_restart_rc", "services", } assert expected_keys <= set(result.keys()) assert result["total"] == 2 assert result["up"] == 1 assert result["down"] == 1 assert len(result["services"]) == 2 assert result["services"][0]["subdomain"] == "metabase" assert {"name", "subdomain", "port", "up"} <= set(result["services"][0].keys()) if __name__ == "__main__": pytest.main([__file__, "-v"])