|
|
|
|
@ -7,6 +7,7 @@ from pathlib import Path
|
|
|
|
|
from types import SimpleNamespace |
|
|
|
|
|
|
|
|
|
import pytest |
|
|
|
|
import psycopg |
|
|
|
|
from django.db import connection |
|
|
|
|
from psycopg import connect, sql |
|
|
|
|
|
|
|
|
|
@ -180,7 +181,7 @@ def test_backup_sanitizes_subprocess_failure_and_captures_diagnostics(
|
|
|
|
|
assert list(backup_root.iterdir()) == [] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_second_temporary_unlink_failure_leaves_no_partial_final_pair( |
|
|
|
|
def test_second_temporary_unlink_failure_preserves_complete_committed_pair( |
|
|
|
|
backup_root, database_settings, synthetic_snapshot, monkeypatch |
|
|
|
|
): |
|
|
|
|
original_unlink = Path.unlink |
|
|
|
|
@ -195,6 +196,25 @@ def test_second_temporary_unlink_failure_leaves_no_partial_final_pair(
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(Path, "unlink", fail_once) |
|
|
|
|
|
|
|
|
|
with pytest.raises(BackupError, match="cleanup_failed"): |
|
|
|
|
create_backup("synthetic.dump", runner=RecordingRunner()) |
|
|
|
|
|
|
|
|
|
assert (backup_root / "synthetic.dump").read_bytes() == b"PGDMP synthetic" |
|
|
|
|
assert (backup_root / "synthetic.dump.json").is_file() |
|
|
|
|
assert sorted(path.name for path in backup_root.iterdir()) == [ |
|
|
|
|
"synthetic.dump", |
|
|
|
|
"synthetic.dump.json", |
|
|
|
|
] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_directory_fsync_failure_rolls_back_both_owned_finals( |
|
|
|
|
backup_root, database_settings, synthetic_snapshot, monkeypatch |
|
|
|
|
): |
|
|
|
|
monkeypatch.setattr( |
|
|
|
|
"core.backup._fsync_directory", |
|
|
|
|
lambda _root: (_ for _ in ()).throw(OSError("synthetic fsync failure")), |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
with pytest.raises(BackupError, match="publication_failed"): |
|
|
|
|
create_backup("synthetic.dump", runner=RecordingRunner()) |
|
|
|
|
|
|
|
|
|
@ -409,6 +429,47 @@ def test_manifest_rejects_boolean_integer_fields(
|
|
|
|
|
verify_backup(result.name, target, runner=RecordingRunner()) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_manifest_rejects_boolean_version( |
|
|
|
|
backup_root, database_settings, synthetic_snapshot, settings |
|
|
|
|
): |
|
|
|
|
settings.BACKUP_VERIFY_ALLOWED = True |
|
|
|
|
result = create_backup("synthetic.dump", runner=RecordingRunner()) |
|
|
|
|
manifest = json.loads(result.manifest_path.read_text(encoding="utf-8")) |
|
|
|
|
manifest["version"] = True |
|
|
|
|
result.manifest_path.write_text(json.dumps(manifest), encoding="utf-8") |
|
|
|
|
target = DatabaseTarget( |
|
|
|
|
host="restore-db", port="5432", name="restore", user="user", password="secret" |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
with pytest.raises(BackupError, match="invalid_manifest"): |
|
|
|
|
verify_backup(result.name, target, runner=RecordingRunner()) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_synthetic_preflight_database_error_is_sanitized(monkeypatch, settings): |
|
|
|
|
import core.backup as backup |
|
|
|
|
|
|
|
|
|
settings.BACKUP_VERIFY_ALLOWED = True |
|
|
|
|
for name, value in { |
|
|
|
|
"BACKUP_SOURCE_HOST": "source", |
|
|
|
|
"BACKUP_SOURCE_NAME": "source", |
|
|
|
|
"BACKUP_SOURCE_USER": "user", |
|
|
|
|
"BACKUP_SOURCE_PASSWORD": "SYNTHETIC PRIVATE PASSWORD", |
|
|
|
|
}.items(): |
|
|
|
|
monkeypatch.setenv(name, value) |
|
|
|
|
monkeypatch.setattr( |
|
|
|
|
backup, |
|
|
|
|
"_read_snapshot", |
|
|
|
|
lambda _target: (_ for _ in ()).throw( |
|
|
|
|
psycopg.OperationalError("SYNTHETIC PRIVATE DIAGNOSTIC") |
|
|
|
|
), |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
with pytest.raises(BackupError, match="integration_verification_failed") as error: |
|
|
|
|
backup.run_synthetic_restore_verification() |
|
|
|
|
|
|
|
|
|
assert "PRIVATE" not in str(error.value) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_cleanup_attempts_every_resource_and_reports_safe_failure(monkeypatch, tmp_path): |
|
|
|
|
import core.backup as backup |
|
|
|
|
|
|
|
|
|
|