Files
CloudRecoveryAS/tests/test_bootstrap.py

188 lines
8.1 KiB
Python

"""Pruebas de bootstrap y carga de .env."""
import os
from pathlib import Path
import pytest
from app.config.bootstrap import ensure_runtime_layout
from app.config.env_loader import (
apply_env_overrides,
get_launch_options,
is_panel_configured,
render_env_template,
)
from app.constants import CONFIG_DIR, DEFAULT_CONFIG, DIR_ENTRADA, ENV_PATH
def test_render_env_template_contains_app_dir(tmp_path: Path):
text = render_env_template(tmp_path)
assert str(tmp_path) in text
assert "CLOUDRESTORE_PANEL_API_URL" in text
def test_ensure_runtime_layout_creates_structure(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr("app.config.bootstrap.APP_DIR", tmp_path)
monkeypatch.setattr("app.config.bootstrap.CONFIG_DIR", tmp_path / "config")
monkeypatch.setattr("app.config.bootstrap.DATA_DIR", tmp_path / "config" / "data")
monkeypatch.setattr("app.config.bootstrap.LOGS_DIR", tmp_path / "config" / "logs")
monkeypatch.setattr("app.config.bootstrap.ODBC_DIR", tmp_path / "config" / "odbc")
monkeypatch.setattr("app.config.bootstrap.SEVEN_ZIP_DIR", tmp_path / "config" / "7zip")
monkeypatch.setattr("app.config.bootstrap.ENV_PATH", tmp_path / "config" / ".env")
monkeypatch.setattr("app.config.bootstrap.DIR_ENTRADA", tmp_path / "Entrada")
monkeypatch.setattr("app.config.bootstrap.DIR_PROCESADOS", tmp_path / "Procesados")
monkeypatch.setattr("app.config.bootstrap.DIR_FALLADOS", tmp_path / "Fallados")
monkeypatch.setattr("app.config.bootstrap.DIR_TEMP", tmp_path / "Temp")
monkeypatch.setattr("app.constants.DB_PATH", tmp_path / "config" / "data" / "app.db")
ensure_runtime_layout()
assert (tmp_path / "config" / ".env").is_file()
assert (tmp_path / "Entrada").is_dir()
assert (tmp_path / "config" / "data" / "app.db").is_file()
def test_is_panel_configured(monkeypatch: pytest.MonkeyPatch):
monkeypatch.delenv("CLOUDRESTORE_PANEL_API_URL", raising=False)
monkeypatch.delenv("CLOUDRESTORE_PANEL_API_TOKEN", raising=False)
monkeypatch.delenv("CLOUDRESTORE_PANEL_INSTANCE_KEY", raising=False)
assert not is_panel_configured()
monkeypatch.setenv("CLOUDRESTORE_PANEL_API_URL", "https://panel:3000")
monkeypatch.setenv("CLOUDRESTORE_PANEL_API_TOKEN", "secret")
monkeypatch.setenv("CLOUDRESTORE_PANEL_INSTANCE_KEY", "srv1")
assert is_panel_configured()
def test_get_launch_options_auto_start_does_not_minimize(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("CLOUDRESTORE_AUTO_START", "true")
monkeypatch.delenv("CLOUDRESTORE_START_MINIMIZED", raising=False)
opts = get_launch_options()
assert opts.start_engine is True
assert opts.minimized is False
def test_get_launch_options_start_minimized(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("CLOUDRESTORE_START_MINIMIZED", "true")
opts = get_launch_options()
assert opts.minimized is True
def test_render_env_template_documents_start_minimized(tmp_path: Path):
text = render_env_template(tmp_path)
assert "CLOUDRESTORE_START_MINIMIZED=false" in text
def test_apply_env_overrides_paths(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("CLOUDRESTORE_INPUT_FOLDER", "/tmp/in")
cfg = apply_env_overrides(DEFAULT_CONFIG.copy())
assert cfg["paths"]["input_folder"] == "/tmp/in"
def _redirect_bootstrap(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
"""Reapunta los globales de bootstrap a un árbol temporal. Devuelve config/."""
for name, rel in (
("APP_DIR", ""),
("CONFIG_DIR", "config"),
("DATA_DIR", "config/data"),
("LOGS_DIR", "config/logs"),
("ODBC_DIR", "config/odbc"),
("SEVEN_ZIP_DIR", "config/7zip"),
("ENV_PATH", "config/.env"),
("DIR_ENTRADA", "Entrada"),
("DIR_PROCESADOS", "Procesados"),
("DIR_FALLADOS", "Fallados"),
("DIR_TEMP", "Temp"),
):
target = tmp_path / rel if rel else tmp_path
monkeypatch.setattr(f"app.config.bootstrap.{name}", target)
monkeypatch.setattr("app.constants.DB_PATH", tmp_path / "config" / "data" / "app.db")
return tmp_path / "config"
def test_bootstrap_escribe_sello_de_version(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
"""
El instalador remoto del PANEL lee config/.version por SFTP para verificar el
despliegue: en Windows el .exe se compila con console=False y no hay stdout confiable.
"""
from app import __version__
config_dir = _redirect_bootstrap(tmp_path, monkeypatch)
ensure_runtime_layout()
assert (config_dir / ".version").read_text(encoding="utf-8").strip() == __version__
def test_bootstrap_redespliega_deps_cuando_cambia_el_manifiesto(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
"""
config/7zip y config/odbc se re-copian cuando el build trae otras versiones embebidas.
Antes solo se copiaban si la carpeta estaba vacía, así que una actualización con driver
ODBC nuevo conservaba el viejo indefinidamente.
"""
config_dir = _redirect_bootstrap(tmp_path, monkeypatch)
src_7zip = tmp_path / "bundle" / "7zip"
src_odbc = tmp_path / "bundle" / "odbc"
src_7zip.mkdir(parents=True)
src_odbc.mkdir(parents=True)
(src_7zip / "7zz").write_text("7zz v26", encoding="utf-8")
(src_odbc / "odbcinst.ini").write_text("odbc 18.5", encoding="utf-8")
monkeypatch.setattr("app.config.bootstrap.BUNDLED_SOURCE_7ZIP", src_7zip)
monkeypatch.setattr("app.config.bootstrap.BUNDLED_SOURCE_ODBC", src_odbc)
# bundled-versions.json se busca bajo BUNDLE_DIR/packaging (va embebido en el onefile).
bundle_root = tmp_path / "bundle_root"
(bundle_root / "packaging").mkdir(parents=True)
manifest = bundle_root / "packaging" / "bundled-versions.json"
manifest.write_text('{"seven_zip": "26.01"}', encoding="utf-8")
monkeypatch.setattr("app.config.bootstrap.BUNDLE_DIR", bundle_root)
ensure_runtime_layout()
assert (config_dir / "7zip" / "7zz").read_text(encoding="utf-8") == "7zz v26"
stamp_before = (config_dir / ".bundled_deps").read_text(encoding="utf-8").strip()
assert stamp_before.startswith("sha256:")
# Mismo manifiesto: no debe re-copiar (no pisa ajustes locales en cada arranque).
(config_dir / "7zip" / "7zz").write_text("editado a mano", encoding="utf-8")
ensure_runtime_layout()
assert (config_dir / "7zip" / "7zz").read_text(encoding="utf-8") == "editado a mano"
# Manifiesto distinto (build con deps nuevas): debe re-copiar.
(src_7zip / "7zz").write_text("7zz v27", encoding="utf-8")
(src_odbc / "odbcinst.ini").write_text("odbc 19.0", encoding="utf-8")
manifest.write_text('{"seven_zip": "27.00"}', encoding="utf-8")
ensure_runtime_layout()
assert (config_dir / "7zip" / "7zz").read_text(encoding="utf-8") == "7zz v27"
assert (config_dir / "odbc" / "odbcinst.ini").read_text(encoding="utf-8") == "odbc 19.0"
assert (config_dir / ".bundled_deps").read_text(encoding="utf-8").strip() != stamp_before
# El re-despliegue SOBRESCRIBE, así que lo que el operador hubiera editado a mano debe
# quedar respaldado: no es reconstruible.
assert (config_dir / "7zip.bak" / "7zz").read_text(encoding="utf-8") == "editado a mano"
assert (config_dir / "odbc.bak" / "odbcinst.ini").read_text(encoding="utf-8") == "odbc 18.5"
def test_bootstrap_no_respalda_cuando_no_hay_refresco(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
"""Sin cambio de manifiesto no se re-copia, así que tampoco debe crearse el .bak."""
config_dir = _redirect_bootstrap(tmp_path, monkeypatch)
src_7zip = tmp_path / "bundle" / "7zip"
src_7zip.mkdir(parents=True)
(src_7zip / "7zz").write_text("7zz v26", encoding="utf-8")
monkeypatch.setattr("app.config.bootstrap.BUNDLED_SOURCE_7ZIP", src_7zip)
bundle_root = tmp_path / "bundle_root"
(bundle_root / "packaging").mkdir(parents=True)
(bundle_root / "packaging" / "bundled-versions.json").write_text(
'{"seven_zip": "26.01"}', encoding="utf-8"
)
monkeypatch.setattr("app.config.bootstrap.BUNDLE_DIR", bundle_root)
ensure_runtime_layout()
ensure_runtime_layout() # segunda vez: mismo manifiesto, no debe refrescar
assert not (config_dir / "7zip.bak").exists()