GuestOps/tests/test_backup_restore_acceptance.py
wolf-demon c41b937acb
Some checks failed
Build and verify web migration / verify (push) Has been cancelled
Gate B release candidate
2026-09-30 15:17:34 +01:00

130 lines
5.8 KiB
Python

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.0",
"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()