import hashlib import io import sys from pathlib import Path import pytest sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) import application_recovery_kit as kit class Missing(Exception): def __init__(self): self.response = {"ResponseMetadata": {"HTTPStatusCode": 404}} class Provider: def __init__(self): self.data, self.calls = None, [] self.version = "synthetic-kit-version" self.corrupt = False self.rules = [] def get_bucket_versioning(self, **args): return {"Status": "Enabled"} def get_bucket_lifecycle_configuration(self, **args): return {"Rules": self.rules} def head_object(self, **args): self.calls.append("head") if self.data is None: raise Missing() return {"ContentLength": len(self.data), "VersionId": self.version, "Metadata": {"sha256": hashlib.sha256(self.data).hexdigest()}} def put_object(self, **args): self.calls.append("put") assert args["Bucket"] == kit.BUCKET and args["IfNoneMatch"] == "*" body = args["Body"] self.data = body.read() if hasattr(body, "read") else body return {"VersionId": self.version} def get_object(self, **args): self.calls.append("get") assert args["Bucket"] == kit.BUCKET and args["VersionId"] == self.version body = b"X" * len(self.data) if self.corrupt else self.data return {"VersionId": self.version, "ContentLength": len(body), "Body": io.BytesIO(body)} @pytest.fixture def fixture(tmp_path, monkeypatch): tmp_path.chmod(0o700) # Real outside-Git enforcement is covered in identity tests; isolate the # synthetic provider paths from a workstation's possible /tmp Git ancestor. monkeypatch.setattr(kit, "private_directory", kit.archive.private_directory) source = tmp_path / "kit.tar" source.write_bytes(b"synthetic-public-kit-no-private-keys") source.chmod(0o600) return source, Provider() def test_immutable_versioned_kit_readback_and_idempotent_publication(fixture, tmp_path): source, client = fixture ref = kit.publish(client, source) assert ref["automatic_expiry"] is False assert ref["object_key"] == "kits/" + hashlib.sha256(source.read_bytes()).hexdigest() + "/recovery-kit.tar" assert kit.download(client, ref, tmp_path / "fetched.tar")["independently_retrieved_hash_verified"] assert kit.publish(client, source) == ref assert client.calls.count("put") == 1 def test_corruption_is_not_accepted_as_independent_kit(fixture, tmp_path): source, client = fixture ref = kit.publish(client, source) client.corrupt = True with pytest.raises(kit.Refused, match="kit_readback_hash_differs"): kit.download(client, ref, tmp_path / "fetched.tar") @pytest.mark.parametrize("field,value", [("project_id", "foreign"), ("bucket", "foreign"), ("object_key", "kits/foreign.tar")]) def test_foreign_reference_refused_before_get(fixture, tmp_path, field, value): source, client = fixture ref = kit.publish(client, source) ref[field] = value with pytest.raises(kit.Refused): kit.download(client, ref, tmp_path / "fetched.tar") assert "get" not in client.calls def test_existing_output_is_never_overwritten(fixture, tmp_path): source, client = fixture ref = kit.publish(client, source) output = tmp_path / "fetched.tar" output.write_bytes(b"keep") with pytest.raises(kit.Refused): kit.download(client, ref, output) assert output.read_bytes() == b"keep" def test_protected_bucket_expiry_refuses_before_upload(fixture): source, client = fixture client.rules = [{"Expiration": {"Days": 30}, "Status": "Enabled"}] with pytest.raises(kit.Refused, match="no_lifecycle_expiry"): kit.publish(client, source) assert client.calls == [] def test_recovery_proof_is_conditionally_retained_and_independently_verified(): client = Provider() receipt = {"status": "application_restored_verified", "application": "coulomb-social"} ref = kit.protect_receipt(client, client, receipt) assert ref["independent_readback_verified"] is True assert ref["object_key"] == "kits/receipts/" + ref["sha256"] + "/recovery-proof.json" assert kit.protect_receipt(client, client, receipt) == ref assert client.calls.count("put") == 1 client.corrupt = True with pytest.raises(kit.Refused, match="proof_readback_differs"): kit.protect_receipt(client, client, receipt)