"""Immutable archive transfer with a synthetic provider; no live credentials.""" import hashlib import importlib.util import io from pathlib import Path import pytest @pytest.fixture def archive(): spec = importlib.util.spec_from_file_location( "primary_application_archive", Path(__file__).resolve().parents[1] / "scripts/primary_application_archive.py", ) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module class NotFound(Exception): response = {"ResponseMetadata": {"HTTPStatusCode": 404}} class Provider: def __init__(self, data, failure=None, exists=False): self.data, self.failure, self.exists = data, failure, exists self.calls = [] self.version = "synthetic-version-1" def head_object(self, **kwargs): self.calls.append(("head", kwargs)) if self.failure == "permission": raise PermissionError("synthetic provider failure") if not self.exists: raise NotFound() return { "ContentLength": len(self.data), "VersionId": self.version, "Metadata": {"sha256": hashlib.sha256(self.data).hexdigest()}, } def create_multipart_upload(self, **kwargs): self.calls.append(("start", kwargs)) return {"UploadId": "own-unfinished-upload"} def upload_part(self, **kwargs): self.calls.append(("part", kwargs)) if self.failure == "part": raise TimeoutError("synthetic timeout") assert kwargs["Body"] == self.data return {"ETag": "synthetic-part-etag"} def complete_multipart_upload(self, **kwargs): self.calls.append(("complete", kwargs)) assert kwargs["IfNoneMatch"] == "*" self.exists = True if self.failure == "lost-response": raise TimeoutError("synthetic lost acknowledgement") return {"VersionId": None if self.failure == "unversioned" else self.version} def abort_multipart_upload(self, **kwargs): self.calls.append(("abort", kwargs)) assert kwargs["UploadId"] == "own-unfinished-upload" def get_object(self, **kwargs): self.calls.append(("get", kwargs)) assert kwargs["VersionId"] == self.version data = b"X" * len(self.data) if self.failure == "corrupt" else self.data if self.failure == "oversize": data += b"extra" return { "ContentLength": len(self.data), "VersionId": "other-version" if self.failure == "wrong-version" else self.version, "Body": io.BytesIO(data), } def fixture(archive, tmp_path, failure=None, exists=False): tmp_path.chmod(0o700) source = tmp_path / "packet.tar.age" source.write_bytes(b"age-encryption.org/v1\nsynthetic-encrypted-pair") source.chmod(0o600) receipt = {"object_key": archive.key_for("coulomb-social", "2026-10-07T20:00:00Z")} return source, tmp_path / "fetched.age", receipt, Provider(source.read_bytes(), failure, exists) def test_conditional_create_and_version_pinned_readback(archive, tmp_path): source, output, receipt, client = fixture(archive, tmp_path) saved = [] result = archive.transfer( client, "coulomb-social", source, output, receipt, lambda: saved.append(dict(receipt)) ) assert result["status"] == "offsite_fetched_verified" assert result["remote_retrieval_verified"] and result["immutable_create"] assert result["version_id"] == client.version assert output.read_bytes() == source.read_bytes() assert output.stat().st_mode & 0o777 == 0o600 assert [call[0] for call in client.calls] == ["head", "start", "part", "complete", "get"] assert not saved[0]["remote_retrieval_verified"] assert saved[-1]["remote_retrieval_verified"] @pytest.mark.parametrize( "failure", ["part", "lost-response", "unversioned", "corrupt", "oversize", "wrong-version", "permission"], ) def test_failure_never_declares_retrieval_or_deletes_retained_objects(archive, tmp_path, failure): source, output, receipt, client = fixture(archive, tmp_path, failure) with pytest.raises((ValueError, TimeoutError, PermissionError)): archive.transfer(client, "coulomb-social", source, output, receipt) assert not receipt["remote_retrieval_verified"] assert not any(name == "delete" for name, _ in client.calls) if failure in {"part", "lost-response"}: assert len([call for call in client.calls if call[0] == "abort"]) == 1 else: assert not any(name == "abort" for name, _ in client.calls) def test_uncertain_completion_retries_readback_without_another_upload(archive, tmp_path): source, output, receipt, client = fixture(archive, tmp_path, "lost-response") with pytest.raises(TimeoutError): archive.transfer(client, "coulomb-social", source, output, receipt) key = receipt["object_key"] client.failure = None archive.transfer(client, "coulomb-social", source, output, receipt) assert receipt["object_key"] == key and receipt["reused_verified_binding"] assert len([call for call in client.calls if call[0] == "start"]) == 1 assert receipt["remote_retrieval_verified"] def test_existing_key_must_bind_bytes_and_previous_version(archive, tmp_path): source, output, receipt, client = fixture(archive, tmp_path, exists=True) client.data = b"other ciphertext" with pytest.raises(ValueError, match="existing_archive_binding_mismatch"): archive.transfer(client, "coulomb-social", source, output, receipt) assert [name for name, _ in client.calls] == ["head"] client.data = source.read_bytes() receipt["version_id"] = "previous-version" with pytest.raises(ValueError, match="existing_archive_version_changed"): archive.transfer(client, "coulomb-social", source, output, receipt) assert not output.exists() @pytest.mark.parametrize( "invalid", ["plaintext", "scope", "receipt-hash", "linked-source", "linked-output"] ) def test_invalid_packet_refuses_before_provider_contact(archive, tmp_path, invalid): source, output, receipt, client = fixture(archive, tmp_path) if invalid == "plaintext": source.write_bytes(b"private unencrypted data") elif invalid == "scope": receipt["object_key"] = archive.key_for("email-connect", "2026-10-07T20:00:00Z") elif invalid == "receipt-hash": receipt["ciphertext_sha256"] = "bad" elif invalid == "linked-source": target = tmp_path / "real.age" source.rename(target) source.symlink_to(target) else: output.symlink_to(tmp_path / "absent") with pytest.raises(ValueError): archive.transfer(client, "coulomb-social", source, output, receipt) assert client.calls == [] def test_retrieve_after_source_loss_uses_saved_version_without_provider_writes(archive, tmp_path): source, output, receipt, client = fixture(archive, tmp_path) receipt.update( schema="platform.application-primary-archive.v1", captured_at="2026-10-07T20:00:00Z" ) archive.transfer(client, "coulomb-social", source, output, receipt) source.unlink() output.unlink() client.calls.clear() result = archive.retrieve(client, "coulomb-social", output, receipt) assert result["status"] == "retrieved_verified" assert not result["provider_mutations"] and not result["application_restore_verified"] assert output.read_bytes() == client.data assert [name for name, _ in client.calls] == ["get"] @pytest.mark.parametrize( "field,value", [ ("remote_retrieval_verified", False), ("destination", "s3://another-bucket/key"), ("ciphertext_sha256", "bad"), ("version_id", "null"), ("ciphertext_bytes", -1), ("application", "email-connect"), ], ) def test_retrieval_rejects_unbound_receipt_before_contact(archive, tmp_path, field, value): source, output, receipt, client = fixture(archive, tmp_path) receipt.update(schema="platform.application-primary-archive.v1") archive.transfer(client, "coulomb-social", source, output, receipt) output.unlink() client.calls.clear() receipt[field] = value with pytest.raises(ValueError): archive.retrieve(client, "coulomb-social", output, receipt) assert client.calls == []