| from __future__ import annotations |
|
|
| import os |
| import sys |
| from datetime import datetime, timedelta, timezone |
| from pathlib import Path |
|
|
| import pytest |
|
|
|
|
| SCRIPT_DIR = Path(__file__).resolve().parents[1] / "scripts" |
| if str(SCRIPT_DIR) not in sys.path: |
| sys.path.insert(0, str(SCRIPT_DIR)) |
|
|
| import database_backup |
| import database_restore |
|
|
|
|
| def test_parse_postgres_url_supports_sqlalchemy_driver_and_sslmode() -> None: |
| target = database_backup.parse_postgres_url( |
| "postgresql+psycopg://student:p%40ss@db.example.com:6543/docdoe?sslmode=require" |
| ) |
| assert target.host == "db.example.com" |
| assert target.port == 6543 |
| assert target.database == "docdoe" |
| assert target.username == "student" |
| assert target.password == "p@ss" |
| assert target.sslmode == "require" |
|
|
|
|
| @pytest.mark.parametrize("database_url", ["", "sqlite:///local.db", "postgresql://localhost"]) |
| def test_parse_postgres_url_rejects_unsafe_or_incomplete_targets(database_url: str) -> None: |
| with pytest.raises(ValueError): |
| database_backup.parse_postgres_url(database_url) |
|
|
|
|
| def test_process_environment_keeps_password_out_of_command_data() -> None: |
| target = database_backup.parse_postgres_url( |
| "postgresql://student:super-secret@db.example.com/docdoe" |
| ) |
| env = target.process_environment() |
| assert env["PGPASSWORD"] == "super-secret" |
| assert env["PGDATABASE"] == "docdoe" |
|
|
|
|
| def test_prune_expired_backups_only_removes_owned_backup_pattern(tmp_path: Path) -> None: |
| now = datetime(2026, 7, 16, tzinfo=timezone.utc) |
| old = tmp_path / "docdoe-20260101T000000Z.dump" |
| old_manifest = old.with_suffix(".manifest.json") |
| recent = tmp_path / "docdoe-20260715T000000Z.dump" |
| unrelated = tmp_path / "customer-export.dump" |
| for path in (old, old_manifest, recent, unrelated): |
| path.write_bytes(b"safe-test-data") |
|
|
| old_time = (now - timedelta(days=30)).timestamp() |
| recent_time = (now - timedelta(days=1)).timestamp() |
| os.utime(old, (old_time, old_time)) |
| os.utime(old_manifest, (old_time, old_time)) |
| os.utime(recent, (recent_time, recent_time)) |
|
|
| removed = database_backup.prune_expired_backups(tmp_path, 14, now) |
| assert set(removed) == {old, old_manifest} |
| assert recent.exists() |
| assert unrelated.exists() |
|
|
|
|
| def test_backup_dry_run_does_not_write_or_print_password(tmp_path: Path, capsys) -> None: |
| result = database_backup.create_backup( |
| database_url="postgresql://student:super-secret@db.example.com/docdoe", |
| output_dir=tmp_path, |
| retention_days=14, |
| verify=True, |
| dry_run=True, |
| now=datetime(2026, 7, 16, tzinfo=timezone.utc), |
| ) |
| assert result is None |
| assert list(tmp_path.iterdir()) == [] |
| output = capsys.readouterr().out |
| assert "super-secret" not in output |
| assert "db.example.com:5432/docdoe" in output |
|
|
|
|
| def test_restore_manifest_detects_tampering(tmp_path: Path) -> None: |
| backup = tmp_path / "docdoe-20260716T000000Z.dump" |
| backup.write_bytes(b"original") |
| backup.with_suffix(".manifest.json").write_text( |
| '{"sha256":"not-the-real-checksum"}', encoding="utf-8" |
| ) |
| with pytest.raises(ValueError, match="checksum"): |
| database_restore.verify_manifest(backup) |
|
|