Files
CloudRecoveryAS/tests/test_forward_false_failure.py

171 lines
5.4 KiB
Python

"""
Regresión del "falso fallo" en el reenvío por SFTP.
Escenario reportado: el SFTP SÍ entrega el archivo, pero un error POSTERIOR a la entrega
(p.ej. al mover el ZIP a Procesados) degradaba el job a FAILED y lo reportaba como 'failed'
al panel, aunque el respaldo ya había llegado al destino. La corrección hace que la entrega
exitosa sea el punto de no retorno: el job queda COMPLETED/forwarded y el error posterior
solo se registra, sin caer a Fallados ni reportar 'failed'.
"""
import pytest
from app.constants import JobStatus
from app.engine.restore_worker import RestoreWorker
FORWARD_ROUTE = {
"action": "forward",
"db_name": "DB1",
"node_key": "NODO",
"target": {
"id": 7,
"name": "Omega",
"ssh_host": "h",
"ssh_username": "u",
"ssh_password": "p",
"input_folder": "D:\\In",
},
}
@pytest.fixture
def base_config(tmp_path):
return {
"paths": {
"input_folder": str(tmp_path / "in"),
"processed_folder": str(tmp_path / "processed"),
"failed_folder": str(tmp_path / "failed"),
"extract_folder": str(tmp_path / "extract"),
"data_sql_folder": str(tmp_path / "data"),
"seven_zip_exe": "C:\\Program Files\\7-Zip\\7z.exe",
},
"sql": {"server": "localhost", "use_windows_auth": True},
"timeouts": {"extract_minutes": 30, "restore_minutes": 60},
"panel": {
"api_url": "http://panel:3000",
"api_token": "tok",
"instance_key": "Alfa",
},
}
class FakeJob:
source_name = "NODO.ZIP"
source_path = "C:\\in\\NODO.ZIP"
db_name = "DB1"
node_name = "NODO"
def _wire_common(monkeypatch, statuses, reported):
"""Mockea las dependencias del worker comunes a los dos escenarios."""
monkeypatch.setattr(
"app.engine.restore_worker.JobRepository.get", lambda job_id: FakeJob()
)
monkeypatch.setattr(
"app.engine.restore_worker.JobRepository.update_node_and_db",
lambda *a, **k: None,
)
monkeypatch.setattr(
"app.engine.restore_worker.JobRepository.update_timing", lambda *a, **k: None
)
monkeypatch.setattr(
"app.engine.restore_worker.JobRepository.update_status",
lambda job_id, status, **k: statuses.append(status),
)
monkeypatch.setattr(
"app.engine.restore_worker.JobRepository.delete", lambda *a, **k: None
)
monkeypatch.setattr(
"app.engine.restore_worker.JobStepRepository.create", lambda *a, **k: 1
)
monkeypatch.setattr(
"app.engine.restore_worker.JobStepRepository.complete", lambda *a, **k: None
)
monkeypatch.setattr(
"app.engine.restore_worker.EventRepository.create", lambda *a, **k: None
)
monkeypatch.setattr(
"app.engine.restore_worker.panel_client.resolve_route",
lambda *a, **k: FORWARD_ROUTE,
)
monkeypatch.setattr(
"app.engine.restore_worker.panel_client.report_job_result",
lambda **k: reported.append(k.get("status")),
)
def test_error_post_entrega_no_degrada_a_fallido(monkeypatch, base_config):
"""Entrega OK + error al mover a Procesados => job forwarded, nunca failed."""
statuses: list[str] = []
reported: list[str] = []
_wire_common(monkeypatch, statuses, reported)
# El SFTP entrega con éxito.
monkeypatch.setattr(
"app.engine.restore_worker.sftp_copy.upload_zip_parts",
lambda *a, **k: ["D:/In/NODO.ZIP"],
)
moved_to_failed = {"called": False}
monkeypatch.setattr(
RestoreWorker,
"_move_zip_to_failed",
lambda self, job: moved_to_failed.__setitem__("called", True),
)
# Error POSTERIOR a la entrega: mover a Procesados falla.
def boom(self, job):
raise OSError("disco lleno al mover a Procesados")
monkeypatch.setattr(RestoreWorker, "_move_zip_to_processed", boom)
worker = RestoreWorker("job-1", base_config)
worker.run()
assert JobStatus.COMPLETED in statuses
assert JobStatus.FAILED not in statuses
assert reported == ["forwarded"]
assert moved_to_failed["called"] is False
def test_fallo_real_de_sftp_va_a_fallados_y_limpia_parcial(monkeypatch, base_config):
"""Fallo genuino de entrega => FAILED, ZIP a Fallados, y limpieza de partes subidas."""
from app.transfer.sftp_copy import SFTPCopyError
statuses: list[str] = []
reported: list[str] = []
_wire_common(monkeypatch, statuses, reported)
# El SFTP falla en la parte 2, adjuntando lo ya subido (envío parcial).
err = SFTPCopyError("timeout en la parte 2")
err.uploaded = ["D:/In/NODO.ZIP.001"]
def failing_upload(*a, **k):
raise err
monkeypatch.setattr(
"app.engine.restore_worker.sftp_copy.upload_zip_parts", failing_upload
)
cleaned: list[str] = []
monkeypatch.setattr(
"app.engine.restore_worker.sftp_copy.cleanup_remote",
lambda target, path: cleaned.append(path),
)
moved_to_failed = {"called": False}
monkeypatch.setattr(
RestoreWorker,
"_move_zip_to_failed",
lambda self, job: moved_to_failed.__setitem__("called", True),
)
worker = RestoreWorker("job-1", base_config)
worker.run()
assert JobStatus.FAILED in statuses
assert JobStatus.COMPLETED not in statuses
assert reported == ["failed"]
assert moved_to_failed["called"] is True
assert cleaned == ["D:/In/NODO.ZIP.001"]