import contextlib import hashlib import importlib.util import json from pathlib import Path import tempfile import unittest from unittest.mock import patch ROOT = Path(__file__).resolve().parents[1] spec = importlib.util.spec_from_file_location("gate_b_bundle", ROOT / "deploy" / "gate_b_bundle.py") bundle = importlib.util.module_from_spec(spec) spec.loader.exec_module(bundle) COMMIT = "a" * 40 API = {"reference": f"guestops-api:{COMMIT}", "id": "sha256:" + "b" * 64} WORKER = {"reference": f"guestops-worker:{COMMIT}", "id": "sha256:" + "c" * 64} ORIGIN = "https://sandbox-guestops.futuresens.co.uk" class GateBBundleTests(unittest.TestCase): def fixture(self, directory: str): root = Path(directory) archive = root / "GuestOps-0.2.1-aaaaaaaaaaaa.tar.gz" source = root / "GuestOps-0.2.1-aaaaaaaaaaaa.source.json" release = root / "release-record.json" metrics = root / "host-metrics.json" archive.write_bytes(b"approved source") artifact = { "name": archive.name, "size": archive.stat().st_size, "sha256": hashlib.sha256(archive.read_bytes()).hexdigest(), } source.write_text(json.dumps({"schemaVersion": 1, "version": "0.2.1", "commit": COMMIT, "artifact": artifact}), encoding="utf-8") release.write_text(json.dumps({ "schemaVersion": 1, "version": "0.2.1", "commit": COMMIT, "artifact": artifact, "images": {"api": API, "worker": WORKER}, }), encoding="utf-8") release_sha = hashlib.sha256(release.read_bytes()).hexdigest() metrics.write_bytes(b'{"cpu":40,"memory":35}') values = {} paths = {} for name in bundle.RECORD_KEYS: value = {"releaseCommit": COMMIT, "releaseRecordSha256": release_sha} if name in { "debian-host", "persistence", "backup-restore", "google-mailbox", "automation", "identity-privacy", "inbox-usability", "incident-support", "pilot-findings", }: value["environment"] = ORIGIN if name in {"debian-host", "persistence", "backup-restore"}: value["archiveSha256"] = artifact["sha256"] value["images"] = {"api": API, "worker": WORKER} values[name] = value values["capacity"].update({ "schemaVersion": 1, "kind": "guestops-read-only-capacity", "originHost": "sandbox-guestops.futuresens.co.uk", "paths": ["/health/ready", "/api/hotel", "/api/conversations/page"], "concurrency": 10, "requests": 100, "successes": 100, "failures": 0, "errorRate": 0.0, "latencyMs": {"median": 100, "p95": 250, "maximum": 300}, }) pilot_path = root / "pilot-findings.json" pilot_path.write_text(json.dumps(values["pilot-findings"]), encoding="utf-8") paths["pilot-findings"] = pilot_path capacity_path = root / "capacity.json" capacity_path.write_text(json.dumps(values["capacity"]), encoding="utf-8") paths["capacity"] = capacity_path values["pilot-approval"].update({ "capacity": { "reportSha256": hashlib.sha256(capacity_path.read_bytes()).hexdigest(), "hostMetricsSha256": hashlib.sha256(metrics.read_bytes()).hexdigest(), "observedConcurrency": 10, "observedP95Ms": 250, "observedErrorRate": 0.0, }, "pilot": {"recordSha256": hashlib.sha256(pilot_path.read_bytes()).hexdigest()}, }) for name in bundle.RECORD_KEYS - paths.keys(): path = root / f"{name}.json" path.write_text(json.dumps(values[name]), encoding="utf-8") paths[name] = path records = {name: (paths[name], values[name]) for name in bundle.RECORD_KEYS} return archive, source, release, metrics, records @contextlib.contextmanager def mocked_individual_validators(self): patches = [ patch.object(bundle.debian_acceptance, "validate_pair"), patch.object(bundle.backup_restore_acceptance, "validate"), patch.object(bundle.google_acceptance, "validate"), patch.object(bundle.automation_acceptance, "validate"), patch.object(bundle.identity_privacy_acceptance, "validate"), patch.object(bundle.desktop_acceptance, "validate"), patch.object(bundle.incident_exercise, "validate"), patch.object(bundle.pilot_run, "validate"), patch.object(bundle.pilot_approval, "validate"), ] with contextlib.ExitStack() as stack: for item in patches: stack.enter_context(item) yield def test_complete_bundle_is_bound_to_one_release(self): with tempfile.TemporaryDirectory() as directory, self.mocked_individual_validators(): archive, source, release, metrics, records = self.fixture(directory) result = bundle.validate_bundle(archive, source, release, records, metrics) self.assertTrue(result["validated"]) self.assertEqual(result["releaseCommit"], COMMIT) self.assertEqual(set(result["records"]), bundle.RECORD_KEYS) def test_different_environment_or_checksum_is_rejected(self): with tempfile.TemporaryDirectory() as directory, self.mocked_individual_validators(): archive, source, release, metrics, records = self.fixture(directory) records["google-mailbox"][1]["environment"] = "https://other.example.invalid" with self.assertRaisesRegex(ValueError, "different environments"): bundle.validate_bundle(archive, source, release, records, metrics) with tempfile.TemporaryDirectory() as directory, self.mocked_individual_validators(): archive, source, release, metrics, records = self.fixture(directory) records["pilot-approval"][1]["capacity"]["reportSha256"] = "0" * 64 with self.assertRaisesRegex(ValueError, "capacity report checksum"): bundle.validate_bundle(archive, source, release, records, metrics) def test_capacity_counts_and_latency_are_checked(self): report = { "schemaVersion": 1, "kind": "guestops-read-only-capacity", "releaseCommit": COMMIT, "releaseRecordSha256": "d" * 64, "paths": ["/health/ready", "/api/hotel", "/api/conversations/page"], "concurrency": 10, "requests": 10, "successes": 9, "failures": 1, "errorRate": 0.1, "latencyMs": {"median": 10, "p95": 20, "maximum": 30}, } bundle.validate_capacity(report, COMMIT, "d" * 64) report["failures"] = 2 with self.assertRaisesRegex(ValueError, "do not match requests"): bundle.validate_capacity(report, COMMIT, "d" * 64) if __name__ == "__main__": unittest.main()