Initial commit
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from pve_backup_report.config import AppConfig
|
||||
from pve_backup_report.pve_client import PveClient, PveConnectionError, PveHttpError
|
||||
|
||||
|
||||
class FakeResponse:
|
||||
def __init__(self, status_code: int, payload: dict, reason: str = "OK") -> None:
|
||||
self.status_code = status_code
|
||||
self._payload = payload
|
||||
self.reason = reason
|
||||
|
||||
def json(self) -> dict:
|
||||
return self._payload
|
||||
|
||||
|
||||
class FakeSession:
|
||||
def __init__(self, response: FakeResponse) -> None:
|
||||
self.headers: dict[str, str] = {}
|
||||
self.response = response
|
||||
self.calls: list[tuple[str, dict[str, object] | None, int, bool | str]] = []
|
||||
|
||||
def get(
|
||||
self,
|
||||
url: str,
|
||||
params: dict[str, object] | None,
|
||||
timeout: int,
|
||||
verify: bool | str,
|
||||
) -> FakeResponse:
|
||||
self.calls.append((url, params, timeout, verify))
|
||||
return self.response
|
||||
|
||||
def close(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def make_config() -> AppConfig:
|
||||
return AppConfig(
|
||||
pve_api_url="https://pve.example.invalid:8006",
|
||||
pve_api_token_id="backup-report@pve!report",
|
||||
pve_api_token_secret="secret",
|
||||
report_output_dir=Path("reports"),
|
||||
report_timezone="Europe/Paris",
|
||||
pve_verify_tls=True,
|
||||
pve_ca_bundle=None,
|
||||
pve_timeout_seconds=30,
|
||||
pve_backup_jobs_endpoint="/cluster/backup",
|
||||
pve_task_history_limit=500,
|
||||
pve_task_log_limit=5000,
|
||||
pbs_hostnames={},
|
||||
pbs_servers=(),
|
||||
log_level="INFO",
|
||||
report_filename_prefix="rapport-sauvegardes-pve",
|
||||
)
|
||||
|
||||
|
||||
def test_get_uses_token_auth_and_returns_data() -> None:
|
||||
session = FakeSession(FakeResponse(200, {"data": [{"node": "pve1"}]}))
|
||||
client = PveClient(make_config(), session=session) # type: ignore[arg-type]
|
||||
|
||||
data = client.get_nodes()
|
||||
|
||||
assert data == [{"node": "pve1"}]
|
||||
assert session.calls == [
|
||||
("https://pve.example.invalid:8006/api2/json/nodes", None, 30, True)
|
||||
]
|
||||
assert session.headers["Authorization"].startswith(
|
||||
"PVEAPIToken=backup-report@pve!report="
|
||||
)
|
||||
assert "secret" in session.headers["Authorization"]
|
||||
|
||||
|
||||
def test_http_error_keeps_endpoint_and_status() -> None:
|
||||
session = FakeSession(FakeResponse(401, {"message": "permission denied"}, "Unauthorized"))
|
||||
client = PveClient(make_config(), session=session) # type: ignore[arg-type]
|
||||
|
||||
with pytest.raises(PveHttpError) as exc_info:
|
||||
client.get_storages()
|
||||
|
||||
assert exc_info.value.endpoint == "/storage"
|
||||
assert exc_info.value.status_code == 401
|
||||
|
||||
|
||||
def test_http_error_message_is_sanitized() -> None:
|
||||
session = FakeSession(
|
||||
FakeResponse(
|
||||
500,
|
||||
{"message": "PVEAPIToken=backup@pve!report=secret secret=secret2"},
|
||||
"Server Error",
|
||||
)
|
||||
)
|
||||
client = PveClient(make_config(), session=session) # type: ignore[arg-type]
|
||||
|
||||
with pytest.raises(PveHttpError) as exc_info:
|
||||
client.get_nodes()
|
||||
|
||||
assert "report=secret" not in exc_info.value.message
|
||||
assert "secret2" not in exc_info.value.message
|
||||
assert exc_info.value.message == "PVEAPIToken=*** secret=***"
|
||||
|
||||
|
||||
def test_ca_bundle_overrides_boolean_verify_tls(tmp_path: Path) -> None:
|
||||
ca_bundle = tmp_path / "internal-ca.pem"
|
||||
ca_bundle.write_text("test ca", encoding="utf-8")
|
||||
config = make_config()
|
||||
config = AppConfig(
|
||||
**{
|
||||
**config.__dict__,
|
||||
"pve_ca_bundle": ca_bundle,
|
||||
}
|
||||
)
|
||||
session = FakeSession(FakeResponse(200, {"data": []}))
|
||||
client = PveClient(config, session=session) # type: ignore[arg-type]
|
||||
|
||||
client.get_backup_jobs()
|
||||
|
||||
assert session.calls[0][3] == str(ca_bundle)
|
||||
|
||||
|
||||
def test_check_required_endpoints_keeps_testing_after_error() -> None:
|
||||
class MultiResponseSession(FakeSession):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(FakeResponse(200, {"data": []}))
|
||||
self.responses = [
|
||||
FakeResponse(200, {"data": [{"node": "pve1"}]}),
|
||||
FakeResponse(200, {"data": [{"storage": "pbs"}]}),
|
||||
FakeResponse(200, {"data": [{"subdir": "jobs"}]}),
|
||||
FakeResponse(404, {"message": "No such endpoint"}),
|
||||
]
|
||||
|
||||
def get(
|
||||
self,
|
||||
url: str,
|
||||
params: dict[str, object] | None,
|
||||
timeout: int,
|
||||
verify: bool | str,
|
||||
) -> FakeResponse:
|
||||
self.calls.append((url, params, timeout, verify))
|
||||
return self.responses.pop(0)
|
||||
|
||||
client = PveClient(make_config(), session=MultiResponseSession()) # type: ignore[arg-type]
|
||||
|
||||
results = client.check_required_endpoints()
|
||||
|
||||
assert [result.endpoint for result in results] == [
|
||||
"/nodes",
|
||||
"/storage",
|
||||
"/cluster",
|
||||
"/cluster/backup",
|
||||
]
|
||||
assert [result.ok for result in results] == [True, True, True, False]
|
||||
assert results[2].detail == "sous-endpoints: jobs"
|
||||
|
||||
|
||||
def test_get_cluster_tasks_without_params() -> None:
|
||||
session = FakeSession(FakeResponse(200, {"data": []}))
|
||||
client = PveClient(make_config(), session=session) # type: ignore[arg-type]
|
||||
|
||||
assert client.get_cluster_tasks() == []
|
||||
assert session.calls == [
|
||||
(
|
||||
"https://pve.example.invalid:8006/api2/json/cluster/tasks",
|
||||
None,
|
||||
30,
|
||||
True,
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def test_get_node_tasks_without_params() -> None:
|
||||
session = FakeSession(FakeResponse(200, {"data": []}))
|
||||
client = PveClient(make_config(), session=session) # type: ignore[arg-type]
|
||||
|
||||
assert client.get_node_tasks("pve01") == []
|
||||
assert session.calls == [
|
||||
(
|
||||
"https://pve.example.invalid:8006/api2/json/nodes/pve01/tasks",
|
||||
None,
|
||||
30,
|
||||
True,
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def test_get_task_log_encodes_upid() -> None:
|
||||
session = FakeSession(FakeResponse(200, {"data": []}))
|
||||
client = PveClient(make_config(), session=session) # type: ignore[arg-type]
|
||||
|
||||
upid = "UPID:pve01:123:456:789:vzdump:100:root@pam:"
|
||||
|
||||
assert client.get_task_log("pve01", upid) == []
|
||||
assert session.calls == [
|
||||
(
|
||||
"https://pve.example.invalid:8006/api2/json/nodes/pve01/tasks/UPID%3Apve01%3A123%3A456%3A789%3Avzdump%3A100%3Aroot%40pam%3A/log",
|
||||
{"start": 0, "limit": 5000},
|
||||
30,
|
||||
True,
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def test_get_guest_config_endpoints() -> None:
|
||||
session = FakeSession(FakeResponse(200, {"data": {"description": "note"}}))
|
||||
client = PveClient(make_config(), session=session) # type: ignore[arg-type]
|
||||
|
||||
assert client.get_qemu_config("pve01", 100) == {"description": "note"}
|
||||
assert client.get_lxc_config("pve02", 200) == {"description": "note"}
|
||||
assert session.calls == [
|
||||
(
|
||||
"https://pve.example.invalid:8006/api2/json/nodes/pve01/qemu/100/config",
|
||||
None,
|
||||
30,
|
||||
True,
|
||||
),
|
||||
(
|
||||
"https://pve.example.invalid:8006/api2/json/nodes/pve02/lxc/200/config",
|
||||
None,
|
||||
30,
|
||||
True,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def test_pve_sanitize_exception_masks_sensitive_values() -> None:
|
||||
message = PveClient._sanitize_exception(
|
||||
PveConnectionError(
|
||||
"PVEAPIToken=backup@pve!report=secret PVE_API_TOKEN_SECRET=secret2"
|
||||
)
|
||||
)
|
||||
|
||||
assert "report=secret" not in message
|
||||
assert "secret2" not in message
|
||||
assert "PVEAPIToken=***" in message
|
||||
assert "PVE_API_TOKEN_SECRET=***" in message
|
||||
Reference in New Issue
Block a user