test_systemd_backup_paths.py 3.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
  1. """Reinstalling must not silently take away a writable path (#2544).
  2. ``ProtectSystem=strict`` means the unit's ``ReadWritePaths`` is the *complete*
  3. list of places Bambuddy can write. An operator who backs up to a NAS adds their
  4. share to it by hand — and both installers overwrite the unit file wholesale, so
  5. that line used to vanish on the next install. The backups then failed with EROFS
  6. every night, which looks like a NAS permission problem and is not one.
  7. So the installers keep the operator's extra paths, and the unit says why they
  8. matter.
  9. """
  10. from __future__ import annotations
  11. from pathlib import Path
  12. import pytest
  13. REPO = Path(__file__).resolve().parents[3]
  14. INSTALLERS = ["install/install.sh", "spoolbuddy/install/install.sh"]
  15. # The service unit + install scripts these tests read live at the repo root and
  16. # are not copied into the Docker test image (Dockerfile.test ships only backend/,
  17. # pyproject.toml, gcode_viewer/ and requirements). In a source checkout they are
  18. # always present and the guard below is live; in the stripped test image there is
  19. # nothing to check, so skip rather than fail. `frontend/package.json` exists in
  20. # every checkout but never in the test image, so it distinguishes the two.
  21. pytestmark = pytest.mark.skipif(
  22. not (REPO / "frontend" / "package.json").is_file(),
  23. reason="launcher config files aren't shipped in the Docker test image; verified in native runs",
  24. )
  25. def _read(rel: str) -> str:
  26. path = REPO / rel
  27. assert path.is_file(), f"launcher moved or was removed: {rel}"
  28. return path.read_text()
  29. class TestUnitTemplate:
  30. def test_readwritepaths_still_grants_the_three_app_dirs(self):
  31. unit = _read("deploy/bambuddy.service")
  32. line = next(line for line in unit.splitlines() if line.startswith("ReadWritePaths="))
  33. assert "DATA_DIR" in line and "LOG_DIR" in line and "INSTALL_PATH" in line
  34. def test_unit_explains_how_to_add_a_backup_share(self):
  35. """Whoever reads this unit next has to be able to work out why their NAS
  36. is read-only for the service but not for their shell.
  37. """
  38. unit = _read("deploy/bambuddy.service")
  39. assert "systemctl edit" in unit, "the unit should show how to add a writable path via a drop-in"
  40. class TestInstallersPreserveCustomPaths:
  41. @pytest.mark.parametrize("installer", INSTALLERS)
  42. def test_generated_unit_appends_the_carried_over_paths(self, installer):
  43. script = _read(installer)
  44. line = next(line for line in script.splitlines() if line.startswith("ReadWritePaths="))
  45. assert "$extra_rw" in line, (
  46. f"{installer} writes ReadWritePaths without $extra_rw, so a NAS share the operator "
  47. "added to the unit is dropped on reinstall:\n" + line
  48. )
  49. @pytest.mark.parametrize("installer", INSTALLERS)
  50. def test_existing_unit_is_read_for_custom_paths_and_backed_up(self, installer):
  51. script = _read(installer)
  52. assert "ReadWritePaths=" in script and "extra_rw+=" in script, (
  53. f"{installer} no longer carries the previous unit's ReadWritePaths forward"
  54. )
  55. assert ".bak-" in script, f"{installer} overwrites the unit without backing it up first"