Files
CloudRecoveryAS/tests/test_panel_client.py

452 lines
15 KiB
Python

"""
Pruebas del cliente del PANEL (servidor de restauración asignado por base de datos).
Se mockea `requests` para no depender de un PANEL real.
"""
import pytest
from app.panel import panel_client
class FakeResponse:
def __init__(self, status_code, json_data=None, raise_json=False):
self.status_code = status_code
self._json = json_data
self._raise_json = raise_json
def json(self):
if self._raise_json:
raise ValueError("no es JSON")
return self._json
VALID_TARGET = {
"id": 2,
"name": "Omega",
"server": "192.168.1.100,1433",
"username": "sa",
"password": "secreto",
"data_folder": "C:\\SQLData",
"ssh_host": "192.168.1.100",
"ssh_port": 22,
"ssh_username": "Administrator",
"ssh_password": "ssh-secreto",
"remote_inbox_path": "C:\\RestoreInbox",
}
URL = "http://panel:3000"
TOKEN = "tok"
DB = "EMPRESA_DB"
def test_target_url_vacia_devuelve_none():
assert panel_client.get_target_for_database("", TOKEN, DB) is None
def test_target_url_invalida_devuelve_none():
assert panel_client.get_target_for_database("ftp://x", TOKEN, DB) is None
def test_target_token_vacio_devuelve_none():
assert panel_client.get_target_for_database(URL, "", DB) is None
def test_target_db_vacio_devuelve_none():
assert panel_client.get_target_for_database(URL, TOKEN, " ") is None
def test_target_ok(monkeypatch):
captured = {}
# **kwargs porque panel_client también pasa verify=; una firma rígida rompe la prueba
# cada vez que se agrega un kwarg al cliente.
def fake_get(url, headers=None, timeout=None, **kwargs):
captured["url"] = url
return FakeResponse(200, VALID_TARGET)
monkeypatch.setattr(panel_client.requests, "get", fake_get)
target = panel_client.get_target_for_database(URL, TOKEN, DB)
assert target is not None
assert target["name"] == "Omega"
# el db_name viaja como query param url-encoded
assert "database=EMPRESA_DB" in captured["url"]
def test_target_db_con_espacios_se_encodea(monkeypatch):
captured = {}
monkeypatch.setattr(
panel_client.requests, "get",
lambda url, **k: (captured.__setitem__("url", url), FakeResponse(200, VALID_TARGET))[1]
)
panel_client.get_target_for_database(URL, TOKEN, "MI BASE")
assert "MI%20BASE" in captured["url"]
def test_target_falta_campo_requerido(monkeypatch):
incompleto = dict(VALID_TARGET)
del incompleto["password"]
monkeypatch.setattr(panel_client.requests, "get", lambda *a, **k: FakeResponse(200, incompleto))
assert panel_client.get_target_for_database(URL, TOKEN, DB) is None
def test_target_campo_vacio_es_invalido(monkeypatch):
vacio = dict(VALID_TARGET, password=" ")
monkeypatch.setattr(panel_client.requests, "get", lambda *a, **k: FakeResponse(200, vacio))
assert panel_client.get_target_for_database(URL, TOKEN, DB) is None
@pytest.mark.parametrize("code", [404, 401, 500, 503])
def test_target_codigos_no_200_devuelven_none(monkeypatch, code):
monkeypatch.setattr(panel_client.requests, "get", lambda *a, **k: FakeResponse(code))
assert panel_client.get_target_for_database(URL, TOKEN, DB) is None
def test_target_error_red_devuelve_none(monkeypatch):
def boom(*a, **k):
raise panel_client.requests.exceptions.ConnectionError("caído")
monkeypatch.setattr(panel_client.requests, "get", boom)
assert panel_client.get_target_for_database(URL, TOKEN, DB) is None
def test_target_json_invalido_devuelve_none(monkeypatch):
monkeypatch.setattr(
panel_client.requests, "get", lambda *a, **k: FakeResponse(200, raise_json=True)
)
assert panel_client.get_target_for_database(URL, TOKEN, DB) is None
def test_report_job_result_201_true(monkeypatch):
captured = {}
def fake_post(url, json=None, headers=None, timeout=None, **kwargs):
captured["json"] = json
return FakeResponse(201)
monkeypatch.setattr(panel_client.requests, "post", fake_post)
ok = panel_client.report_job_result(
URL, TOKEN, filename="empresa.bak", status="completed",
restore_target_id=2, db_name="EMP", duration_ms=1000
)
assert ok is True
assert captured["json"]["filename"] == "empresa.bak"
assert captured["json"]["status"] == "completed"
def test_report_job_result_otro_codigo_false(monkeypatch):
monkeypatch.setattr(panel_client.requests, "post", lambda *a, **k: FakeResponse(500))
assert panel_client.report_job_result(URL, TOKEN, "x.bak", "failed") is False
def test_report_job_result_error_red_false(monkeypatch):
def boom(*a, **k):
raise panel_client.requests.exceptions.Timeout("timeout")
monkeypatch.setattr(panel_client.requests, "post", boom)
assert panel_client.report_job_result(URL, TOKEN, "x.bak", "failed") is False
@pytest.mark.parametrize("code", [200, 404])
def test_test_connection_ok(monkeypatch, code):
# 200 o 404 → conexión y token correctos
monkeypatch.setattr(panel_client.requests, "get", lambda *a, **k: FakeResponse(code))
ok, info = panel_client.test_connection(URL, TOKEN)
assert ok is True
def test_test_connection_token_rechazado(monkeypatch):
monkeypatch.setattr(panel_client.requests, "get", lambda *a, **k: FakeResponse(401))
ok, info = panel_client.test_connection(URL, TOKEN)
assert ok is False
assert "401" in info
def test_test_connection_url_invalida():
ok, info = panel_client.test_connection("noesurl", TOKEN)
assert ok is False
def test_report_instance_config_200_true(monkeypatch):
captured = {}
def fake_post(url, json=None, headers=None, timeout=None, **kwargs):
captured["url"] = url
captured["json"] = json
return FakeResponse(200)
monkeypatch.setattr(panel_client.requests, "post", fake_post)
ok = panel_client.report_instance_config(
URL, TOKEN, r"D:\Backups\Entrada", host_name="WIN-01", app_version="1.0.0"
)
assert ok is True
assert captured["url"].endswith("/api/restore/instance-config")
assert captured["json"]["input_folder"] == r"D:\Backups\Entrada"
assert captured["json"]["host_name"] == "WIN-01"
def test_report_instance_config_input_vacio_false():
assert panel_client.report_instance_config(URL, TOKEN, " ") is False
def test_report_instance_config_sin_token_false():
assert panel_client.report_instance_config(URL, "", r"D:\In") is False
def test_report_instance_config_otro_codigo_false(monkeypatch):
monkeypatch.setattr(panel_client.requests, "post", lambda *a, **k: FakeResponse(500))
assert panel_client.report_instance_config(URL, TOKEN, r"D:\In") is False
def test_report_instance_config_error_red_false(monkeypatch):
def boom(*a, **k):
raise panel_client.requests.exceptions.ConnectionError("caído")
monkeypatch.setattr(panel_client.requests, "post", boom)
assert panel_client.report_instance_config(URL, TOKEN, r"D:\In") is False
def test_target_con_instance_envia_param(monkeypatch):
captured = {}
monkeypatch.setattr(
panel_client.requests, "get",
lambda url, **k: (captured.__setitem__("url", url), FakeResponse(200, VALID_TARGET))[1]
)
panel_client.get_target_for_database(URL, TOKEN, DB, instance_key="Alfa")
assert "instance=Alfa" in captured["url"]
COLOCATED_TARGET = {
"id": 1,
"name": "Alfa",
"server": "localhost",
"username": "sa",
"password": "secreto",
"data_folder": "C:\\SQLData",
}
def test_target_colocado_no_exige_ssh(monkeypatch):
monkeypatch.setattr(
panel_client.requests, "get", lambda *a, **k: FakeResponse(200, COLOCATED_TARGET)
)
target = panel_client.get_target_for_database(URL, TOKEN, DB, instance_key="Alfa")
assert target is not None
assert target["name"] == "Alfa"
def test_report_instance_config_con_instance_key(monkeypatch):
captured = {}
monkeypatch.setattr(
panel_client.requests, "post",
lambda url, json=None, **k: (captured.update({"json": json}), FakeResponse(200))[1]
)
ok = panel_client.report_instance_config(
URL, TOKEN, r"D:\Alfa\In", instance_key="Alfa"
)
assert ok is True
assert captured["json"]["instance_key"] == "Alfa"
CATALOG_RESPONSE = {
"targets": [
{"id": 1, "name": "Alfa"},
{"id": 2, "name": "Omega"},
{"id": 4, "name": "Delta"},
]
}
def test_list_restore_target_names_ok(monkeypatch):
captured = {}
def fake_get(url, headers=None, timeout=None, **kwargs):
captured["url"] = url
return FakeResponse(200, CATALOG_RESPONSE)
monkeypatch.setattr(panel_client.requests, "get", fake_get)
names = panel_client.list_restore_target_names(URL, TOKEN)
assert names == ["Alfa", "Omega", "Delta"]
assert captured["url"].endswith("/api/restore/target-catalog")
def test_list_restore_target_names_url_vacia():
assert panel_client.list_restore_target_names("", TOKEN) == []
def test_list_restore_target_names_token_vacio():
assert panel_client.list_restore_target_names(URL, "") == []
@pytest.mark.parametrize("code", [401, 500, 503])
def test_list_restore_target_names_codigos_no_200(monkeypatch, code):
monkeypatch.setattr(panel_client.requests, "get", lambda *a, **k: FakeResponse(code))
assert panel_client.list_restore_target_names(URL, TOKEN) == []
def test_list_restore_target_names_error_red(monkeypatch):
def boom(*a, **k):
raise panel_client.requests.exceptions.ConnectionError("caído")
monkeypatch.setattr(panel_client.requests, "get", boom)
assert panel_client.list_restore_target_names(URL, TOKEN) == []
def test_list_restore_target_names_json_invalido(monkeypatch):
monkeypatch.setattr(
panel_client.requests, "get", lambda *a, **k: FakeResponse(200, raise_json=True)
)
assert panel_client.list_restore_target_names(URL, TOKEN) == []
def test_list_restore_target_names_sin_lista_targets(monkeypatch):
monkeypatch.setattr(
panel_client.requests, "get", lambda *a, **k: FakeResponse(200, {"foo": []})
)
assert panel_client.list_restore_target_names(URL, TOKEN) == []
ROUTE_FORWARD = {
"action": "forward",
"db_name": "GENERICA-TEST",
"node_key": "GENERICA-TEST",
"target": {
"id": 2,
"name": "Alfa",
"server": "192.168.1.10,1433",
"username": "sa",
"password": "secreto",
"data_folder": "C:\\SQLData",
"ssh_host": "192.168.1.10",
"ssh_port": 22,
"ssh_username": "Administrator",
"ssh_password": "ssh-secreto",
"remote_inbox_path": "C:\\RestoreInbox",
"input_folder": "D:\\Restore\\Alfa\\Entrada",
},
}
ROUTE_RESTORE_LOCAL = {
"action": "restore_local",
"db_name": "GENERICA-TEST",
"node_key": "GENERICA-TEST",
"target": {
"id": 2,
"name": "Alfa",
"server": "192.168.1.10,1433",
"username": "sa",
"password": "secreto",
"data_folder": "C:\\SQLData",
"ssh_host": "192.168.1.10",
"ssh_port": 22,
"ssh_username": "Administrator",
"ssh_password": "ssh-secreto",
"remote_inbox_path": "C:\\RestoreInbox",
"input_folder": "D:\\Restore\\Alfa\\Entrada",
},
}
def test_resolve_route_forward_ok(monkeypatch):
captured = {}
def fake_get(url, headers=None, timeout=None, **kwargs):
captured["url"] = url
return FakeResponse(200, ROUTE_FORWARD)
monkeypatch.setattr(panel_client.requests, "get", fake_get)
route = panel_client.resolve_route(URL, TOKEN, "GENERICA-TEST.ZIP", instance_key="Omega")
assert route is not None
assert route["action"] == "forward"
assert "resolve-route" in captured["url"]
assert "instance=Omega" in captured["url"]
def test_resolve_route_restore_local_ok(monkeypatch):
monkeypatch.setattr(
panel_client.requests, "get", lambda *a, **k: FakeResponse(200, ROUTE_RESTORE_LOCAL)
)
route = panel_client.resolve_route(URL, TOKEN, "GENERICA-TEST.ZIP", instance_key="Alfa")
assert route is not None
assert route["action"] == "restore_local"
def test_resolve_route_503_devuelve_none(monkeypatch):
monkeypatch.setattr(panel_client.requests, "get", lambda *a, **k: FakeResponse(503))
assert panel_client.resolve_route(URL, TOKEN, "X.ZIP") is None
def test_resolve_route_forward_sin_input_folder_invalido(monkeypatch):
bad = dict(ROUTE_FORWARD)
bad["target"] = dict(bad["target"], input_folder=" ")
monkeypatch.setattr(
panel_client.requests, "get", lambda *a, **k: FakeResponse(200, bad)
)
assert panel_client.resolve_route(URL, TOKEN, "X.ZIP") is None
# ============================================================================
# platform / arch: el PANEL los usa para elegir qué artefacto le toca a este
# servidor al instalar o actualizar (a24c.cras_releases se llavea por
# version + platform + arch).
# ============================================================================
def _capture_post(monkeypatch) -> dict:
captured = {}
def fake_post(url, json=None, headers=None, timeout=None, **kwargs):
captured["url"] = url
captured["json"] = json
captured["headers"] = headers
return FakeResponse(200)
monkeypatch.setattr(panel_client.requests, "post", fake_post)
return captured
def test_report_instance_config_envia_platform_y_arch(monkeypatch):
captured = _capture_post(monkeypatch)
ok = panel_client.report_instance_config(
URL,
TOKEN,
r"D:\Backups\Entrada",
processed_folder=r"D:\Backups\Procesados",
host_name="WIN-01",
app_version="1.1.0",
instance_key="Alfa",
platform_name="windows",
arch="x86_64",
)
assert ok is True
assert captured["json"]["platform"] == "windows"
assert captured["json"]["arch"] == "x86_64"
assert captured["json"]["processed_folder"] == r"D:\Backups\Procesados"
def test_report_instance_config_sin_platform_manda_none(monkeypatch):
# Compatibilidad hacia atrás: un agente viejo no manda estas claves y el PANEL
# debe poder caer al texto libre de restore_targets.os.
captured = _capture_post(monkeypatch)
panel_client.report_instance_config(URL, TOKEN, r"D:\In")
assert captured["json"]["platform"] is None
assert captured["json"]["arch"] is None
@pytest.mark.parametrize("blank", ["", " ", None])
def test_report_instance_config_platform_en_blanco_es_none(monkeypatch, blank):
captured = _capture_post(monkeypatch)
panel_client.report_instance_config(
URL, TOKEN, r"D:\In", platform_name=blank, arch=blank
)
assert captured["json"]["platform"] is None
assert captured["json"]["arch"] is None
def test_constantes_platform_arch_son_del_vocabulario_del_panel():
# El PANEL valida platform contra ('windows','linux'); si esto cambia hay que
# actualizar el CHECK de a24c.cras_releases y la validación del endpoint.
from app.constants import APP_ARCH, APP_PLATFORM
assert APP_PLATFORM in ("windows", "linux")
assert APP_ARCH and APP_ARCH == APP_ARCH.strip()
assert " " not in APP_ARCH