import importlib.util from pathlib import Path import unittest spec = importlib.util.spec_from_file_location( "backup_restore_acceptance", Path(__file__).resolve().parents[1] / "deploy" / "backup_restore_acceptance.py") acceptance = importlib.util.module_from_spec(spec) spec.loader.exec_module(acceptance) COMMIT = "a" * 40 RELEASE_SHA = "b" * 64 def valid_record(): return { "schemaVersion": 1, "system": "guestops-backup-recovery", "evidenceId": "backup-restore", "dataClassification": "synthetic-only", "releaseVersion": "0.2.1", "releaseCommit": COMMIT, "releaseRecordSha256": RELEASE_SHA, "archiveSha256": "c" * 64, "environment": "https://sandbox-guestops.futuresens.co.uk", "hostIdentifier": "guestops-sandbox-01", "images": { "api": {"reference": f"guestops-api:{COMMIT}", "id": "sha256:" + "d" * 64}, "worker": {"reference": f"guestops-worker:{COMMIT}", "id": "sha256:" + "e" * 64}, "mongo": {"reference": "mongo:8.0", "id": "sha256:" + "f" * 64}, }, "owners": { "backup": "Backup owner", "monitoring": "Monitoring owner", "recoveryOperator": "Recovery operator", "technicalEscalation": "Technical owner", "retention": "Retention owner", "independentReviewer": "Independent reviewer", }, "startedAt": "2026-10-01T09:00:00Z", "endedAt": "2026-10-01T12:00:00Z", "reviewedAt": "2026-10-01T13:00:00Z", "recoveryObjectives": { "targetRpoHours": 24, "observedRpoHours": 1, "targetRtoMinutes": 240, "observedRtoMinutes": 180, }, "retention": { "localVerifiedDays": 7, "offHostDaily": 35, "offHostMonthly": 12, "legalHoldOverrideTested": True, }, "backup": { "createdAt": "2026-10-01T08:00:00Z", "sha256": "1" * 64, "transferredSha256": "1" * 64, "privateKeyPresentOnHost": False, }, "rollback": { "previousReleaseCommit": "2" * 40, "previousArchiveSha256": "3" * 64, "previousImages": { "api": {"reference": "guestops-api:" + "2" * 40, "id": "sha256:" + "4" * 64}, "worker": {"reference": "guestops-worker:" + "2" * 40, "id": "sha256:" + "5" * 64}, }, "persistentVolumesReplaced": False, "restoredReleaseCommit": COMMIT, "unresolvedOperations": 0, "finalControls": { "googleSending": "disabled", "faqLiveMode": "disabled", "pmsWrites": "disabled", "paymentCreation": "disabled", }, }, "monitoringState": "healthy", "unresolvedCriticalFindings": 0, "scenarios": [ {"id": scenario, "status": "pass", "evidence": [f"restricted-{index}"]} for index, scenario in enumerate(sorted(acceptance.SCENARIOS), 1) ], } class BackupRestoreAcceptanceTests(unittest.TestCase): def test_complete_record_passes(self): acceptance.validate(valid_record(), COMMIT, RELEASE_SHA) def test_release_identity_and_images_are_bound(self): record = valid_record(); record["releaseCommit"] = "9" * 40 with self.assertRaisesRegex(ValueError, "approved candidate"): acceptance.validate(record, COMMIT, RELEASE_SHA) record = valid_record(); record["images"]["api"]["reference"] = "guestops-api:latest" with self.assertRaisesRegex(ValueError, "full approved commit"): acceptance.validate(record, COMMIT, RELEASE_SHA) record = valid_record(); record["rollback"]["previousImages"]["worker"]["id"] = "mutable" with self.assertRaisesRegex(ValueError, "retained previous release identity"): acceptance.validate(record, COMMIT, RELEASE_SHA) def test_recovery_objectives_and_checksums_must_pass(self): record = valid_record(); record["recoveryObjectives"]["observedRtoMinutes"] = 241 with self.assertRaisesRegex(ValueError, "four-hour"): acceptance.validate(record, COMMIT, RELEASE_SHA) record = valid_record(); record["backup"]["transferredSha256"] = "9" * 64 with self.assertRaisesRegex(ValueError, "checksums must match"): acceptance.validate(record, COMMIT, RELEASE_SHA) record = valid_record(); record["recoveryObjectives"]["observedRpoHours"] = 2 with self.assertRaisesRegex(ValueError, "match the backup"): acceptance.validate(record, COMMIT, RELEASE_SHA) def test_independent_review_retention_and_final_state_are_required(self): record = valid_record(); record["owners"]["independentReviewer"] = record["owners"]["backup"] with self.assertRaisesRegex(ValueError, "different"): acceptance.validate(record, COMMIT, RELEASE_SHA) record = valid_record(); record["retention"]["legalHoldOverrideTested"] = False with self.assertRaisesRegex(ValueError, "Retention"): acceptance.validate(record, COMMIT, RELEASE_SHA) record = valid_record(); record["rollback"]["persistentVolumesReplaced"] = True with self.assertRaisesRegex(ValueError, "must not replace"): acceptance.validate(record, COMMIT, RELEASE_SHA) def test_exact_passed_scenarios_and_monitoring_are_required(self): record = valid_record(); record["scenarios"].pop() with self.assertRaisesRegex(ValueError, "exact backup/recovery scenario"): acceptance.validate(record, COMMIT, RELEASE_SHA) record = valid_record(); record["monitoringState"] = "degraded" with self.assertRaisesRegex(ValueError, "Monitoring must be healthy"): acceptance.validate(record, COMMIT, RELEASE_SHA) if __name__ == "__main__": unittest.main()