FluxIP/tests/test_backup.py

165 lines
5.3 KiB
Python

from __future__ import annotations
import base64
import sqlite3
import sys
import tarfile
from pathlib import Path
import pytest
from deploy import backup
def create_database(path: Path) -> None:
with sqlite3.connect(path) as connection:
connection.execute("CREATE TABLE sample (value TEXT NOT NULL)")
connection.execute("INSERT INTO sample VALUES ('ready')")
def create_backup_set(output: Path, kind: str, timestamp: str) -> tuple[Path, Path]:
archive = output / f"fluxip-{kind}-backup-{timestamp}.tar.gz"
checksum = archive.with_name(f"{archive.name}.sha256")
archive.write_bytes(b"archive")
checksum.write_text("checksum\n", encoding="ascii")
return archive, checksum
def test_default_backup_excludes_master_key(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
database = tmp_path / "fluxip.db"
output = tmp_path / "backups"
create_database(database)
monkeypatch.setattr(
sys,
"argv",
["backup.py", "--database", str(database), "--output-dir", str(output)],
)
assert backup.main() == 0
archive = next(output.glob("fluxip-database-backup-*.tar.gz"))
with tarfile.open(archive, "r:gz") as package:
assert package.getnames() == ["fluxip.db"]
assert archive.with_name(f"{archive.name}.sha256").is_file()
def test_complete_backup_requires_explicit_plaintext_confirmation(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
database = tmp_path / "fluxip.db"
master_key = tmp_path / "master.key"
output = tmp_path / "backups"
create_database(database)
master_key.write_bytes(base64.urlsafe_b64encode(b"k" * 32))
base_args = [
"backup.py",
"--database",
str(database),
"--master-key",
str(master_key),
"--output-dir",
str(output),
]
monkeypatch.setattr(sys, "argv", base_args)
with pytest.raises(RuntimeError, match="拒绝把主密钥写入未加密备份"):
backup.main()
monkeypatch.setattr(sys, "argv", [*base_args, "--allow-plaintext-key-archive"])
assert backup.main() == 0
archive = next(output.glob("fluxip-complete-backup-*.tar.gz"))
with tarfile.open(archive, "r:gz") as package:
assert package.getnames() == ["fluxip.db", "master.key"]
def test_failed_backup_removes_private_temporary_archive(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
database = tmp_path / "fluxip.db"
output = tmp_path / "backups"
create_database(database)
monkeypatch.setattr(
sys,
"argv",
["backup.py", "--database", str(database), "--output-dir", str(output)],
)
monkeypatch.setattr(
backup.tarfile,
"open",
lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("archive failed")),
)
with pytest.raises(RuntimeError, match="archive failed"):
backup.main()
assert not list(output.glob("*.tmp"))
def test_keep_last_rejects_negative_value(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(sys, "argv", ["backup.py", "--keep-last", "-1"])
with pytest.raises(SystemExit) as exc_info:
backup.parse_args()
assert exc_info.value.code == 2
def test_prune_backups_is_type_scoped_and_stays_in_output_dir(tmp_path: Path) -> None:
output = tmp_path / "backups"
output.mkdir()
timestamps = ["20260101T000000Z", "20260102T000000Z", "20260103T000000Z"]
database_sets = [create_backup_set(output, "database", value) for value in timestamps]
complete_sets = [create_backup_set(output, "complete", value) for value in timestamps]
unrelated = output / "fluxip-database-backup-20250101T000000Z.tar.gz.bak"
unrelated.write_bytes(b"keep")
outside = tmp_path / "fluxip-database-backup-20240101T000000Z.tar.gz"
outside.write_bytes(b"keep")
assert backup.prune_backups(output, "database", 2) == 1
assert not database_sets[0][0].exists()
assert not database_sets[0][1].exists()
assert all(path.exists() for pair in database_sets[1:] for path in pair)
assert all(path.exists() for pair in complete_sets for path in pair)
assert unrelated.exists()
assert outside.exists()
assert backup.prune_backups(output, "complete", 1) == 2
assert all(not path.exists() for pair in complete_sets[:2] for path in pair)
assert all(path.exists() for path in complete_sets[2])
def test_keep_last_zero_disables_cleanup(tmp_path: Path) -> None:
output = tmp_path / "backups"
output.mkdir()
backup_set = create_backup_set(output, "database", "20260101T000000Z")
assert backup.prune_backups(output, "database", 0) == 0
assert all(path.exists() for path in backup_set)
def test_main_applies_retention_after_success(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
database = tmp_path / "fluxip.db"
output = tmp_path / "backups"
output.mkdir()
create_database(database)
old_set = create_backup_set(output, "database", "20000101T000000Z")
monkeypatch.setattr(
sys,
"argv",
[
"backup.py",
"--database",
str(database),
"--output-dir",
str(output),
"--keep-last",
"1",
],
)
assert backup.main() == 0
assert all(not path.exists() for path in old_set)
assert len(list(output.glob("fluxip-database-backup-*.tar.gz"))) == 1