Initial commit

This commit is contained in:
2026-05-13 16:04:17 +02:00
commit b66612d672
43 changed files with 10515 additions and 0 deletions
+237
View File
@@ -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