from datetime import datetime from pve_backup_report.models import ( BackupCoverage, BackupJob, Guest, PbsAccessUser, PbsBackupSnapshotSummary, PbsDatastoreUsage, PbsGarbageCollectionStatus, PbsRetentionPolicy, PbsStorage, ReportData, ReportSummary, ) from pve_backup_report.report_weasy_pdf import build_template_context from pve_backup_report.report_weasy_pdf import render_html def test_build_template_context_contains_sections() -> None: report_data = ReportData( pbs_storages=[ PbsStorage( storage_id="BACKUP-PROD", username="backup@pbs", server="192.0.2.10", datastore="RAID5", namespace="serveurs", enabled=True, ), PbsStorage( storage_id="BACKUP-LAB", username="backup@pbs", server="192.0.2.10", datastore="RAID5", namespace="lab", enabled=True, ) ], pbs_datastore_usages=[ PbsDatastoreUsage( server_name="PBS01", datastore="RAID5", total_bytes=100, used_bytes=60, available_bytes=40, ) ], pbs_access_users=[ PbsAccessUser( server_name="PBS01", auth_id="backup@pbs", user_id="backup@pbs", storage_id="BACKUP-PROD", datastore="RAID5", namespace="serveurs", enabled=True, expire=0, email="admin@example.invalid", permissions={"Datastore.Backup": True, "Datastore.Modify": False}, comment="Compte PVE", ) ], pbs_gc_statuses=[ PbsGarbageCollectionStatus( server_name="PBS01", datastore="RAID5", status="en_cours", ) ], pbs_retention_policies=[ PbsRetentionPolicy( policy_id="prune-serveurs", server_name="PBS01", datastore="RAID5", namespace="serveurs", keep_daily=3, ), PbsRetentionPolicy( policy_id="prune-lab", server_name="PBS01", datastore="RAID5", namespace="lab", keep_daily=3, ), ], guests=[ Guest(vmid=100, name="srv", guest_type="qemu", notes="note production"), Guest(vmid=101, name="lab", guest_type="qemu"), ], coverage=[ BackupCoverage( guest=Guest(vmid=100, name="srv", guest_type="qemu", notes="note production"), status="sauvegarde_pbs_planifiee", jobs=[BackupJob(job_id="backup-prod", schedule="22:00")], storages=["BACKUP-PROD"], ), BackupCoverage( guest=Guest(vmid=101, name="lab", guest_type="qemu"), status="sauvegarde_pbs_planifiee", jobs=[BackupJob(job_id="backup-lab", schedule="21:00")], storages=["BACKUP-LAB"], ) ], pbs_snapshot_summaries={ ("PBS01", "RAID5", "serveurs", "qemu", 100): PbsBackupSnapshotSummary( server_name="PBS01", vmid=100, guest_type="qemu", datastore="RAID5", namespace="serveurs", snapshot_count=3, oldest_backup_at=datetime(2026, 5, 7, 22, 0), newest_backup_at=datetime(2026, 5, 9, 22, 0), ), ("PBS01", "RAID5", "lab", "qemu", 101): PbsBackupSnapshotSummary( server_name="PBS01", vmid=101, guest_type="qemu", datastore="RAID5", namespace="lab", snapshot_count=2, oldest_backup_at=datetime(2026, 5, 8, 21, 0), newest_backup_at=datetime(2026, 5, 9, 21, 0), ), }, summary=ReportSummary(generated_at=datetime(2026, 5, 9, 2, 0)), ) context = build_template_context( report_data, {"192.0.2.10": "backup-display"}, ) assert context["generated_at"] == "2026-05-09 02:00" assert [section.title for section in context["sections"]][:4] == [ "Resume", "Stockages PBS déclarés sur PVE", "Utilisateurs PBS - Audit des accès", "Espaces de stockage PBS", ] access_section = next( section for section in context["sections"] if section.title == "Utilisateurs PBS - Audit des accès" ) assert access_section.headers == [ "Serveur PBS", "Auth-id", "Storage PVE", "Datastore", "Namespace", "Actif", "Expiration", "Email", "Permissions", "Commentaire", ] assert access_section.rows[0] == [ "PBS01", "backup@pbs", "BACKUP-PROD", "RAID5", "serveurs", "oui", "aucune", "admin@example.invalid", "Datastore.Backup", "Compte PVE", ] missing_section = next( section for section in context["sections"] if section.title == "VM/CT non sauvegardees" ) assert missing_section.headers == ["VMID", "Nom", "Notes", "Type", "Noeud", "Etat", "Detail"] coverage_group = next( section for section in context["sections"] if section.title == "Sauvegarde des VM/CT" ) assert coverage_group.level == 1 coverage_sections = [ section for section in context["sections"] if section.title.startswith("Sauvegarde des VM/CT -") ] assert [section.title for section in coverage_sections] == [ "Sauvegarde des VM/CT - lab", "Sauvegarde des VM/CT - serveurs", ] coverage_section = next( section for section in coverage_sections if section.title == "Sauvegarde des VM/CT - serveurs" ) assert "Namespace" not in coverage_section.headers assert coverage_section.headers[2] == "Notes" assert coverage_section.rows[0][2] == "note production" assert coverage_section.rows[0][7] == "192.0.2.10 (backup-display)" assert coverage_section.rows[0][9] == "non renseigne" retention_group = next( section for section in context["sections"] if section.title == "Retention des sauvegardes VM/CT" ) assert retention_group.level == 1 assert retention_group.warning is None retention_sections = [ section for section in context["sections"] if section.title.startswith("Retention des sauvegardes VM/CT PBS01") ] assert [section.title for section in retention_sections] == [ "Retention des sauvegardes VM/CT PBS01 - lab", "Retention des sauvegardes VM/CT PBS01 - serveurs", ] assert not any( section.title.startswith("Retention des sauvegardes VM/CT PBS02") for section in context["sections"] ) assert not any( section.title.startswith("Retention des sauvegardes VM/CT PBS03") for section in context["sections"] ) assert all("Namespace" not in section.headers for section in retention_sections) assert all("Datastore" in section.headers for section in retention_sections) assert all("Nombre attendu de versions" in section.headers for section in retention_sections) assert all("Delta" in section.headers for section in retention_sections) assert all("garbage collector" in (section.warning or "") for section in retention_sections) assert retention_sections[0].rows[0][0] == 101 assert retention_sections[0].rows[0][2] == "RAID5" assert retention_sections[0].rows[0][3] == "Active sur PVE" assert retention_sections[0].rows[0][5] == "3" assert retention_sections[0].rows[0][6] == "-1" def test_render_html_keeps_css_unescaped() -> None: html = render_html(ReportData()) assert 'content: "Rapport des sauvegardes Proxmox VE"' in html assert """ not in html assert '

' in html assert "Sauvegarde des VM/CT" in html assert '

Retention des sauvegardes VM/CT

' not in html def test_render_html_supports_english_labels() -> None: report_data = ReportData( coverage=[ BackupCoverage( guest=Guest(vmid=100, name="srv", guest_type="qemu"), status="non_sauvegardee", ) ], summary=ReportSummary(generated_at=datetime(2026, 5, 9, 2, 0)), ) html = render_html(report_data, language="en") assert '' in html assert "Proxmox VE Backup Report" in html assert "Table of contents" in html assert "VM/CT without backup" in html assert "not specified" in html assert "Rapport des sauvegardes Proxmox VE" not in html def test_pdf_pbs_access_users_table_keeps_expected_fields_without_raw_secrets() -> None: report_data = ReportData( pbs_access_users=[ PbsAccessUser( server_name="PBS01", auth_id="backup@pbs", user_id="backup@pbs", storage_id="BACKUP-PROD", datastore="RAID5", namespace="serveurs", enabled=True, expire=0, email="admin@example.invalid", permissions={"Datastore.Backup": True}, comment="Compte PVE", raw={ "Authorization": "PBSAPIToken=abc:secret", "api_token_secret": "secret-value", "password": "secret-password", }, ) ], ) html = render_html(report_data) assert "Utilisateurs PBS - Audit des accès" in html assert "backup@pbs" in html assert "BACKUP-PROD" in html assert "RAID5" in html assert "serveurs" in html assert "Datastore.Backup" in html assert "Compte PVE" in html assert "PBSAPIToken=abc:secret" not in html assert "secret-value" not in html assert "secret-password" not in html assert "api_token_secret" not in html def test_build_template_context_omits_unconfigured_retention_servers() -> None: report_data = ReportData(pbs_server_names=["PBS01"]) context = build_template_context(report_data) titles = [section.title for section in context["sections"]] assert "Retention des sauvegardes VM/CT PBS01 - non renseigne" in titles assert "Retention des sauvegardes VM/CT PBS02 - non renseigne" not in titles assert "Retention des sauvegardes VM/CT PBS03 - non renseigne" not in titles