coulomb-social/tests/test_paired_backup.py
tegwick 11cab7298b
Some checks failed
Use-case regression gate / usecase-regressions (push) Failing after 6m27s
Guard paired capture and mirror scratch capacity inside the writer lock
Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a17f16-d99c-7ea3-95f0-8212897fba6b
2026-10-30 00:37:37 +01:00

308 lines
13 KiB
Python

"""Coherent backup publication needs version-pinned readback, never PUT alone."""
import fcntl
import hashlib
import importlib.util
import json
import subprocess
import sys
import tarfile
from datetime import UTC, datetime, timedelta
from pathlib import Path
import pytest
@pytest.fixture
def backup(monkeypatch):
path = Path(__file__).resolve().parents[1] / "scripts/paired-backup.py"
monkeypatch.syspath_prepend(str(path.parent))
spec = importlib.util.spec_from_file_location("csoc_paired_backup", path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
# Other cases test publication/capture under an already bounded executor.
# Actual RLIMIT behavior is exercised in separate disposable subprocesses.
monkeypatch.setattr(module.capacity, "active_file_limit", lambda: module.capacity.FILE_LIMIT)
monkeypatch.setattr(module.capacity, "disk_free", lambda path: 128 * 1024**3)
monkeypatch.setattr(module.capacity, "free_inodes", lambda path: 1000000)
monkeypatch.setattr(module.capacity, "enforce_file_limit", lambda: module.capacity.FILE_LIMIT)
return module
NOW = datetime(2026, 10, 11, 3, 15, tzinfo=UTC)
KEY = (
"platform-pg/application-archives/coulomb-social/2026/10/11/20261011T031500Z-"
+ "a" * 24
+ ".tar.age"
)
def pair(directory):
directory.mkdir(mode=0o700)
receipt = {
"status": "captured",
"writers_quiesced": True,
"app_resumed": True,
"database": "coulomb_social_db",
"image": "synthetic@sha256:fixture",
"started_at": NOW.isoformat(),
"elapsed_seconds": 45,
}
for name, key, data in [
("database.dump", "database", b"PGDMPsynthetic"),
("content.tar.gz", "content", b"synthetic-content"),
]:
(directory / name).write_bytes(data)
(directory / name).chmod(0o600)
receipt[key + "_bytes"] = len(data)
receipt[key + "_sha256"] = hashlib.sha256(data).hexdigest()
(directory / "receipt.json").write_text(json.dumps(receipt))
(directory / "receipt.json").chmod(0o600)
return receipt
def setup(backup, monkeypatch, tmp_path, failure=None):
state = tmp_path / "state"
calls, publications = [], []
def command(argv, **kwargs):
calls.append(argv)
if "--execute" in argv:
if failure == "capture":
raise backup.Refused("capture_failed")
pair(Path(argv[-1]))
else:
assert argv[:3] == ["age", "-r", "age1-public-test-key"]
with tarfile.open(argv[-1]) as bundle:
assert set(bundle.getnames()) == {"database.dump", "content.tar.gz", "receipt.json"}
assert bundle.extractfile("database.dump").read() == b"PGDMPsynthetic"
if failure == "encrypt":
raise backup.Refused("encryption_failed")
Path(argv[4]).write_bytes(b"age-encryption.org/v1\nsynthetic-ciphertext")
def publish(encrypted, fetched, transfer_receipt, captured_at, tool, python):
assert encrypted.stat().st_mode & 0o777 == 0o600
publications.append(encrypted.read_bytes())
if failure == "upload":
raise backup.Refused("upload_failed")
fetched.write_bytes(encrypted.read_bytes())
fetched.chmod(0o600)
return {
"object_key": KEY,
"destination": "s3://synthetic/" + KEY,
"version_id": "version-1",
}
monkeypatch.setattr(backup, "command", command)
monkeypatch.setattr(backup, "publish", publish)
return state, calls, publications
def test_success_keeps_one_fetched_pair_and_records_no_application_restore(
backup, monkeypatch, tmp_path
):
state, calls, publications = setup(backup, monkeypatch, tmp_path)
receipt = backup.run_backup(state, tmp_path / "primary.py", "age1-public-test-key", NOW)
assert receipt["status"] == "offsite_fetched_verified"
assert receipt["remote_retrieval_verified"]
assert not receipt["application_restore_verified"]
assert receipt["version_id"] == "version-1"
assert receipt["age_recipient"] == "age1-public-test-key"
assert len(publications) == 1 and len(calls) == 2
assert not list(state.glob("pending-*"))
assert not list(state.rglob("database.dump"))
assert state.stat().st_mode & 0o777 == 0o700
assert (state / "last-success.json").stat().st_mode & 0o777 == 0o600
assert json.loads((state / "inventory.json").read_text()) == [receipt]
assert backup.check_freshness(state, NOW + timedelta(hours=25))["status"] == "fresh"
with pytest.raises(backup.Refused, match="backup_stale"):
backup.check_freshness(state, NOW + timedelta(hours=27))
(state / receipt["local_archive"]).write_bytes(b"corrupt")
with pytest.raises(backup.Refused, match="local_encrypted_hash_mismatch"):
backup.check_freshness(state, NOW)
@pytest.mark.parametrize("failure", ["capture", "encrypt", "upload"])
def test_failures_keep_private_evidence_and_block_further_capture(
backup, monkeypatch, tmp_path, failure
):
state, _, _ = setup(backup, monkeypatch, tmp_path, failure)
with pytest.raises(backup.Refused):
backup.run_backup(state, tmp_path / "primary.py", "age1-public-test-key", NOW)
assert not (state / "last-success.json").exists()
assert not (state / "inventory.json").exists()
pending = list(state.glob("pending-*"))
assert len(pending) == 1 and pending[0].stat().st_mode & 0o777 == 0o700
with pytest.raises(backup.Refused, match="pending_backup_requires_review"):
backup.run_backup(state, tmp_path / "primary.py", "age1-public-test-key", NOW)
assert len(list(state.glob("pending-*"))) == 1
def test_live_lock_and_nonprivate_state_refuse_before_capture(backup, monkeypatch, tmp_path):
state, calls, _ = setup(backup, monkeypatch, tmp_path)
state.mkdir(mode=0o700)
with (state / "backup.lock").open("w") as lock:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
with pytest.raises(backup.Refused, match="backup_already_running"):
backup.run_backup(state, tmp_path / "primary.py", "age1-public-test-key", NOW)
state.chmod(0o755)
with pytest.raises(backup.Refused, match="state_directory_not_private"):
backup.run_backup(state, tmp_path / "primary.py", "age1-public-test-key", NOW)
assert calls == []
def test_pair_hashes_and_app_resume_required(backup, tmp_path):
directory = tmp_path / "pair"
receipt = pair(directory)
assert backup.validate_pair(directory) == receipt
(directory / "database.dump").write_bytes(b"corrupt")
with pytest.raises(backup.Refused, match="pair_hash_mismatch"):
backup.validate_pair(directory)
receipt["app_resumed"] = False
(directory / "receipt.json").write_text(json.dumps(receipt))
with pytest.raises(backup.Refused, match="pair_incomplete"):
backup.validate_pair(directory)
def test_pending_retry_reuses_ciphertext_without_recapture_or_reencrypt(
backup, monkeypatch, tmp_path
):
state, _, publications = setup(backup, monkeypatch, tmp_path, "upload")
with pytest.raises(backup.Refused):
backup.run_backup(state, tmp_path / "primary.py", "age1-public-test-key", NOW)
ciphertext = publications[0]
_, calls, publications = setup(backup, monkeypatch, tmp_path)
receipt = backup.run_backup(
state,
tmp_path / "primary.py",
"age1-public-test-key",
NOW + timedelta(hours=1),
resume=True,
)
assert calls == [] and publications == [ciphertext]
assert receipt["captured_at"] == NOW.isoformat()
assert not list(state.glob("pending-*"))
def test_pending_ciphertext_tamper_refuses_before_publication(backup, monkeypatch, tmp_path):
state, _, _ = setup(backup, monkeypatch, tmp_path, "upload")
with pytest.raises(backup.Refused):
backup.run_backup(state, tmp_path / "primary.py", "age1-public-test-key", NOW)
next(state.glob("pending-*/packet.tar.age")).write_bytes(b"tampered")
_, calls, publications = setup(backup, monkeypatch, tmp_path)
with pytest.raises(backup.Refused, match="sealed_packet_binding_mismatch"):
backup.run_backup(state, tmp_path / "primary.py", "age1-public-test-key", NOW, resume=True)
assert calls == [] and publications == []
@pytest.mark.parametrize("value", [None, "different-recipient"])
def test_pending_recipient_binding_drift_refuses_before_publication(
backup, monkeypatch, tmp_path, value
):
state, _, _ = setup(backup, monkeypatch, tmp_path, "upload")
with pytest.raises(backup.Refused):
backup.run_backup(state, tmp_path / "primary.py", "age1-public-test-key", NOW)
sealed_path = next(state.glob("pending-*/sealed.json"))
sealed = json.loads(sealed_path.read_text())
if value is None:
sealed.pop("age_recipient")
else:
sealed["age_recipient"] = value
sealed_path.write_text(json.dumps(sealed))
_, calls, publications = setup(backup, monkeypatch, tmp_path)
with pytest.raises(backup.Refused, match="sealed_packet_binding_mismatch"):
backup.run_backup(state, tmp_path / "primary.py", "age1-public-test-key", NOW, resume=True)
assert calls == [] and publications == []
@pytest.mark.parametrize("recipient", [None, "malformed"])
def test_cli_requires_explicit_recipient_before_commands_or_state(
backup, monkeypatch, tmp_path, recipient
):
monkeypatch.delenv("COULOMB_SOCIAL_BACKUP_AGE_RECIPIENT", raising=False)
if recipient is not None:
monkeypatch.setenv("COULOMB_SOCIAL_BACKUP_AGE_RECIPIENT", recipient)
state = tmp_path / "state"
monkeypatch.setattr(sys, "argv", ["paired-backup.py", "--execute", "--state-dir", str(state)])
def unexpected(*args, **kwargs):
raise AssertionError("No command may run before recipient configuration")
monkeypatch.setattr(backup, "command", unexpected)
with pytest.raises(backup.Refused, match="explicit_age_recipient_required"):
backup.main()
assert not state.exists()
def test_cli_checks_recipient_before_provider_auth_or_capture(backup, monkeypatch, tmp_path):
recipient = "age1" + "q" * 58 # Synthetic lexical fixture; checksum is mocked below.
monkeypatch.setenv("COULOMB_SOCIAL_BACKUP_AGE_RECIPIENT", recipient)
primary = tmp_path / "primary.py"
primary.touch()
python = tmp_path / "python"
python.touch()
state = tmp_path / "state"
monkeypatch.setattr(sys, "argv", [
"paired-backup.py", "--execute", "--state-dir", str(state),
"--primary-tool", str(primary), "--primary-python", str(python),
])
calls = []
def invalid_checksum(argv, **kwargs):
calls.append(argv)
raise backup.Refused("backup_command_failed")
monkeypatch.setattr(backup, "command", invalid_checksum)
with pytest.raises(backup.Refused, match="backup_command_failed"):
backup.main()
assert calls == [["age", "-r", recipient, "/dev/null"]]
assert not state.exists()
def test_recipient_preflight_accepts_a_real_ephemeral_public_key(backup):
identity = subprocess.run(["age-keygen"], capture_output=True, check=True).stdout
recipient = subprocess.run(["age-keygen", "-y", "/dev/stdin"], input=identity,
capture_output=True, check=True).stdout.decode().strip()
backup.command(["age", "-r", recipient, "/dev/null"])
@pytest.mark.parametrize(
"invalid", ["hash", "version", "retrieval", "immutable", "capture", "ciphertext"]
)
def test_publication_requires_complete_bound_readback_receipt(
backup, monkeypatch, tmp_path, invalid
):
encrypted, fetched, receipt_path = [
tmp_path / name for name in ["a.age", "readback.age", "receipt.json"]
]
encrypted.write_bytes(b"synthetic ciphertext")
receipt = {
"status": "offsite_fetched_verified",
"application": "coulomb-social",
"captured_at": NOW.isoformat(),
"remote_retrieval_verified": True,
"immutable_create": True,
"version_id": "version-1",
"ciphertext_sha256": backup.digest(encrypted),
"ciphertext_bytes": encrypted.stat().st_size,
}
changes = {
"hash": ("ciphertext_sha256", "bad"),
"version": ("version_id", "null"),
"retrieval": ("remote_retrieval_verified", False),
"immutable": ("immutable_create", False),
"capture": ("captured_at", "other"),
}
if invalid in changes:
key, value = changes[invalid]
receipt[key] = value
def command(argv, **kwargs):
fetched.write_bytes(b"corrupted" if invalid == "ciphertext" else encrypted.read_bytes())
receipt_path.write_text(json.dumps(receipt))
monkeypatch.setattr(backup, "command", command)
with pytest.raises(backup.Refused, match="primary_receipt_not_verified"):
backup.publish(
encrypted, fetched, receipt_path, NOW.isoformat(), Path("tool"), Path("python")
)