Files
CloudRecoveryAS/tests/test_retention.py

158 lines
5.8 KiB
Python

"""
Pruebas de la retención por nodo (Procesados/) y por antigüedad (Fallados/).
Se mockea el acceso a la BD (JobRepository/EventRepository) y se opera sobre carpetas reales
en tmp_path. Las fechas-carpeta de Procesados/ se derivan con el mismo helper que usa el
limpiador para que la prueba sea independiente de la zona horaria del runner.
"""
from datetime import datetime
import pytest
from app.engine.retention import RetentionCleaner
class FakeJob:
def __init__(self, job_id, node_name, source_name, finished_at):
self.job_id = job_id
self.node_name = node_name
self.source_name = source_name
self.finished_at = finished_at
def _config(tmp_path, *, days=2, failed_days=7, dry_run=False):
return {
"paths": {
"processed_folder": str(tmp_path / "processed"),
"failed_folder": str(tmp_path / "failed"),
},
"retention": {"days": days, "failed_days": failed_days, "dry_run": dry_run},
}
def _date_folder(base, iso):
"""Crea (si falta) la carpeta-fecha local correspondiente a un finished_at ISO UTC."""
local_date = RetentionCleaner._local_date_from_iso(iso)
folder = base / local_date.isoformat()
folder.mkdir(parents=True, exist_ok=True)
return folder
def test_procesados_borra_obsoletos_conserva_reciente_y_nodo_unico(monkeypatch, tmp_path):
processed = tmp_path / "processed"
ref_iso = "2026-07-24T10:00:00"
obsolete_iso = "2026-07-20T10:00:00"
node_b_iso = "2026-07-22T10:00:00"
# NODO_A: copia obsoleta (a borrar) + copia más reciente (a conservar).
obsolete_file = _date_folder(processed, obsolete_iso) / "NODO_A.zip"
obsolete_file.write_bytes(b"viejo")
recent_file = _date_folder(processed, ref_iso) / "NODO_A.zip"
recent_file.write_bytes(b"nuevo")
# NODO_B: una sola restauración (nunca se toca).
node_b_file = _date_folder(processed, node_b_iso) / "NODO_B.zip"
node_b_file.write_bytes(b"unico")
obsolete_job = FakeJob("job-a-old", "NODO_A", "NODO_A.zip", obsolete_iso)
purged: list[str] = []
monkeypatch.setattr(
"app.engine.retention.JobRepository.get_obsolete_completed_by_node",
lambda days: [obsolete_job],
)
monkeypatch.setattr(
"app.engine.retention.JobRepository.get_latest_completed_per_node",
lambda: {"NODO_A": ref_iso, "NODO_B": node_b_iso},
)
monkeypatch.setattr(
"app.engine.retention.JobRepository.mark_purged", lambda job_id: purged.append(job_id)
)
monkeypatch.setattr("app.engine.retention.EventRepository.create", lambda *a, **k: None)
result = RetentionCleaner(_config(tmp_path)).run()
assert not obsolete_file.exists() # obsoleto borrado
assert recent_file.exists() # más reciente intacto
assert node_b_file.exists() # nodo de una sola copia intacto
assert purged == ["job-a-old"]
assert result.deleted_files == 1
def test_procesados_dry_run_no_borra(monkeypatch, tmp_path):
processed = tmp_path / "processed"
obsolete_iso = "2026-07-20T10:00:00"
obsolete_file = _date_folder(processed, obsolete_iso) / "NODO_A.zip"
obsolete_file.write_bytes(b"viejo")
monkeypatch.setattr(
"app.engine.retention.JobRepository.get_obsolete_completed_by_node",
lambda days: [FakeJob("job-a-old", "NODO_A", "NODO_A.zip", obsolete_iso)],
)
monkeypatch.setattr(
"app.engine.retention.JobRepository.get_latest_completed_per_node",
lambda: {"NODO_A": "2026-07-24T10:00:00"},
)
purged: list[str] = []
monkeypatch.setattr(
"app.engine.retention.JobRepository.mark_purged", lambda job_id: purged.append(job_id)
)
monkeypatch.setattr("app.engine.retention.EventRepository.create", lambda *a, **k: None)
result = RetentionCleaner(_config(tmp_path, dry_run=True)).run()
assert obsolete_file.exists() # dry-run no borra
assert purged == [] # ni marca purgado
assert result.dry_run is True
assert result.deleted_files == 1 # sí lo contabiliza como "se borraría"
def test_fallados_borra_por_antiguedad_absoluta(monkeypatch, tmp_path):
failed = tmp_path / "failed"
old_folder = failed / "2026-07-10" # < (hoy - 7)
recent_folder = failed / "2026-07-20" # >= (hoy - 7)
old_folder.mkdir(parents=True)
recent_folder.mkdir(parents=True)
old_file = old_folder / "VIEJO.zip"
old_file.write_bytes(b"x")
recent_file = recent_folder / "RECIENTE.zip"
recent_file.write_bytes(b"y")
# Sin obsoletos en Procesados; solo probamos Fallados.
monkeypatch.setattr(
"app.engine.retention.JobRepository.get_obsolete_completed_by_node", lambda days: []
)
monkeypatch.setattr(
"app.engine.retention.JobRepository.get_latest_completed_per_node", lambda: {}
)
monkeypatch.setattr("app.engine.retention.EventRepository.create", lambda *a, **k: None)
cleaner = RetentionCleaner(
_config(tmp_path, failed_days=7), clock=lambda: datetime(2026, 7, 24, 12, 0, 0)
)
cleaner.run()
assert not old_file.exists() # carpeta-fecha vieja borrada
assert recent_file.exists() # dentro de la ventana, se conserva
def test_fallados_ignora_carpetas_no_fecha(monkeypatch, tmp_path):
failed = tmp_path / "failed"
weird = failed / "no-es-fecha"
weird.mkdir(parents=True)
keep = weird / "algo.zip"
keep.write_bytes(b"z")
monkeypatch.setattr(
"app.engine.retention.JobRepository.get_obsolete_completed_by_node", lambda days: []
)
monkeypatch.setattr(
"app.engine.retention.JobRepository.get_latest_completed_per_node", lambda: {}
)
monkeypatch.setattr("app.engine.retention.EventRepository.create", lambda *a, **k: None)
RetentionCleaner(
_config(tmp_path), clock=lambda: datetime(2026, 7, 24, 12, 0, 0)
).run()
assert keep.exists() # nombre que no es fecha: no se toca