452 lines
15 KiB
Python
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
|