158 lines
5.8 KiB
Python
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
|