Some checks failed
Use-case regression gate / usecase-regressions (push) Failing after 6m27s
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a17f16-d99c-7ea3-95f0-8212897fba6b
308 lines
13 KiB
Python
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")
|
|
)
|