""" 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"]