mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 19:56:53 +00:00
# Conflicts: # .github/workflows/call-images-build-ci.yml # .github/workflows/call-test-deploy.yml # .github/workflows/call-test-install-pkgmgr.yml # .github/workflows/call-test-workspace.yml # roles/svc-bkp-local-2-device/README.md # roles/svc-storage-nfs-client/README.md # roles/web-app-pixelfed/meta/services.yml # scripts/tests/deploy/act/workflow.sh # tests/unit/python/utils/recovery/test_base.py
127 lines
4.6 KiB
Python
127 lines
4.6 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import tempfile
|
|
import unittest
|
|
import unittest.mock as mock
|
|
from pathlib import Path
|
|
|
|
from utils.recovery.base import DirectoryRecovery
|
|
|
|
|
|
class _Recovery(DirectoryRecovery):
|
|
unit_pattern = "svc-bkp-test*.service"
|
|
|
|
|
|
class TestDirectoryRecovery(unittest.TestCase):
|
|
def _mk(self, td: str) -> tuple[Path, Path]:
|
|
root = Path(td)
|
|
source = root / "source"
|
|
target = root / "target"
|
|
source.mkdir()
|
|
target.mkdir()
|
|
(source / "restored.txt").write_text("from-snapshot")
|
|
(target / "live.txt").write_text("pre-recover")
|
|
return source, target
|
|
|
|
def test_run_starts_backup_unit_then_mirrors_source(self) -> None:
|
|
with tempfile.TemporaryDirectory() as td:
|
|
source, target = self._mk(td)
|
|
recovery = _Recovery(str(source), str(target))
|
|
calls: list[list[str]] = []
|
|
|
|
def fake_run(cmd, **kwargs):
|
|
calls.append(cmd)
|
|
result = mock.Mock()
|
|
if cmd[:2] == ["systemctl", "list-unit-files"]:
|
|
result.stdout = "svc-bkp-test.1.example.service enabled\n"
|
|
return result
|
|
if cmd[:2] == ["systemctl", "start"]:
|
|
return result
|
|
return mock.DEFAULT
|
|
|
|
with mock.patch(
|
|
"utils.recovery.base.subprocess.run", side_effect=fake_run
|
|
) as run:
|
|
run.side_effect = fake_run
|
|
recovery.backup_target()
|
|
|
|
self.assertEqual(
|
|
calls[1], ["systemctl", "start", "svc-bkp-test.1.example.service"]
|
|
)
|
|
|
|
recovery.restore()
|
|
restored = target / "restored.txt"
|
|
text = restored.read_text() # nocheck: cache-read - tempdir fixture
|
|
self.assertEqual(text, "from-snapshot")
|
|
self.assertFalse((target / "live.txt").exists())
|
|
|
|
def test_restore_carries_extended_attributes(self) -> None:
|
|
with tempfile.TemporaryDirectory() as td:
|
|
source, target = self._mk(td)
|
|
recovery = _Recovery(str(source), str(target))
|
|
with mock.patch("utils.recovery.base.subprocess.run") as run:
|
|
recovery.restore()
|
|
self.assertEqual(
|
|
run.call_args.args[0][:2],
|
|
["rsync", "-aHAX"],
|
|
"a restore without -X returns the payload byte-identical while "
|
|
"every xattr-backed application (OpenCloud posixfs) reads the "
|
|
"tree as unknown",
|
|
)
|
|
|
|
def test_restore_keeps_user_xattrs(self) -> None:
|
|
with tempfile.TemporaryDirectory() as td:
|
|
source, target = self._mk(td)
|
|
os.setxattr(source / "restored.txt", "user.oc.id", b"space-1")
|
|
_Recovery(str(source), str(target)).restore()
|
|
self.assertEqual(
|
|
os.getxattr(target / "restored.txt", "user.oc.id"), b"space-1"
|
|
)
|
|
|
|
def test_missing_unit_refuses(self) -> None:
|
|
with tempfile.TemporaryDirectory() as td:
|
|
source, target = self._mk(td)
|
|
recovery = _Recovery(str(source), str(target))
|
|
|
|
def fake_run(cmd, **kwargs):
|
|
result = mock.Mock()
|
|
result.stdout = ""
|
|
return result
|
|
|
|
with (
|
|
mock.patch("utils.recovery.base.subprocess.run", side_effect=fake_run),
|
|
self.assertRaises(SystemExit),
|
|
):
|
|
recovery.backup_target()
|
|
|
|
def test_no_safety_backup_skips_unit_run(self) -> None:
|
|
with tempfile.TemporaryDirectory() as td:
|
|
source, target = self._mk(td)
|
|
recovery = _Recovery(str(source), str(target), service_backup=False)
|
|
with mock.patch.object(recovery, "backup_target") as backup:
|
|
recovery.run()
|
|
backup.assert_not_called()
|
|
restored = target / "restored.txt"
|
|
text = restored.read_text() # nocheck: cache-read - tempdir fixture
|
|
self.assertEqual(text, "from-snapshot")
|
|
|
|
def test_missing_target_refuses(self) -> None:
|
|
with tempfile.TemporaryDirectory() as td:
|
|
source, target = self._mk(td)
|
|
with self.assertRaises(SystemExit):
|
|
_Recovery(str(source), str(target / "absent"))
|
|
|
|
def test_unit_pattern_is_mandatory(self) -> None:
|
|
class _NoUnit(DirectoryRecovery):
|
|
pass
|
|
|
|
with tempfile.TemporaryDirectory() as td:
|
|
source, target = self._mk(td)
|
|
with self.assertRaises(ValueError):
|
|
_NoUnit(str(source), str(target))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|