Тесты и CI были добавлены
Каркас pytest с изоляцией путей в tmp: 40 тестов на валидаторы имён, защиту от path traversal, парсеры virsh-вывода, cloud-init генерацию, ротацию бэкапов, CSRF, rate-limit и рендеринг ключевых страниц. GitHub Actions прогоняет compileall, bash -n всех скриптов и pytest. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PKUoZwHMCHnytfswFVRUHw
Этот коммит содержится в:
@@ -0,0 +1,87 @@
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import backup_core
|
||||
|
||||
|
||||
def test_backup_filename_validation():
|
||||
assert backup_core.valid_backup_filename("vm1_20260815_010101.qcow2")
|
||||
assert backup_core.valid_backup_filename("a.qcow2")
|
||||
assert not backup_core.valid_backup_filename("../evil.qcow2")
|
||||
assert not backup_core.valid_backup_filename("x.img")
|
||||
assert not backup_core.valid_backup_filename("")
|
||||
|
||||
|
||||
def test_backup_path_traversal_guard():
|
||||
assert backup_core.backup_path("vm1", "vm1_x.qcow2") is not None
|
||||
assert backup_core.backup_path("vm1", "../../etc/passwd") is None
|
||||
assert backup_core.backup_path("../vm1", "x.qcow2") is None
|
||||
assert backup_core.backup_path("vm1", "nested/x.qcow2") is None
|
||||
|
||||
|
||||
def test_new_backup_path_shape():
|
||||
path = backup_core.new_backup_path("vm1")
|
||||
assert path is not None
|
||||
assert path.name.startswith("vm1_")
|
||||
assert path.suffix == ".qcow2"
|
||||
assert backup_core.new_backup_path("bad name") is None
|
||||
|
||||
|
||||
def test_rotation_keeps_newest():
|
||||
vm_dir = backup_core.backup_dir_for("vm1")
|
||||
vm_dir.mkdir(parents=True)
|
||||
for index in range(5):
|
||||
item = vm_dir / f"vm1_{index}.qcow2"
|
||||
item.write_text("x")
|
||||
stamp = time.time() - (5 - index) * 60
|
||||
import os
|
||||
os.utime(item, (stamp, stamp))
|
||||
removed = backup_core.rotate_backups("vm1", keep=2)
|
||||
assert len(removed) == 3
|
||||
remaining = sorted(p.name for p in vm_dir.glob("*.qcow2"))
|
||||
assert remaining == ["vm1_3.qcow2", "vm1_4.qcow2"]
|
||||
|
||||
|
||||
def test_rotation_ignores_bad_input():
|
||||
assert backup_core.rotate_backups("no-such-vm", keep=3) == []
|
||||
assert backup_core.rotate_backups("vm1", keep=0) == []
|
||||
|
||||
|
||||
def test_schedule_roundtrip():
|
||||
config = backup_core.save_schedule(True, ["vm2", "vm1", "bad name", "vm1"], 99)
|
||||
assert config == {"enabled": True, "vms": ["vm1", "vm2"], "keep": 30}
|
||||
assert backup_core.load_schedule() == config
|
||||
config = backup_core.save_schedule(False, [], 0)
|
||||
assert backup_core.load_schedule() == {"enabled": False, "vms": [], "keep": 1}
|
||||
|
||||
|
||||
def test_schedule_defaults_when_missing():
|
||||
assert backup_core.load_schedule() == {"enabled": False, "vms": [], "keep": backup_core.DEFAULT_KEEP}
|
||||
|
||||
|
||||
def test_backup_script_contents():
|
||||
target = Path("/var/lib/virtuality/backups/vm1/vm1_x.qcow2")
|
||||
script = backup_core.build_backup_script("vm1", "/var/lib/virtuality/images/vm1.qcow2", target)
|
||||
assert "virsh suspend vm1" in script
|
||||
assert "qemu-img convert -p -c -O qcow2" in script
|
||||
assert "virsh resume vm1" in script
|
||||
assert f"{target}.part" in script # атомарная запись через .part
|
||||
restore = backup_core.build_restore_script("/var/lib/virtuality/images/vm1.qcow2", target)
|
||||
assert "qemu-img convert -p -O qcow2" in restore
|
||||
assert ".restore" in restore
|
||||
|
||||
|
||||
def test_first_disk_of_parsing(monkeypatch):
|
||||
sample = """ Type Device Target Source
|
||||
------------------------------------------------
|
||||
file disk vda /var/lib/virtuality/images/vm1.qcow2
|
||||
file cdrom sda /var/lib/virtuality/iso/ubuntu.iso
|
||||
"""
|
||||
|
||||
class FakeResult:
|
||||
returncode = 0
|
||||
stdout = sample
|
||||
stderr = ""
|
||||
|
||||
monkeypatch.setattr(backup_core, "_run", lambda cmd, timeout=60: FakeResult())
|
||||
assert backup_core.first_disk_of("vm1") == "/var/lib/virtuality/images/vm1.qcow2"
|
||||
Ссылка в новой задаче
Block a user