test_slice_archive_output_path_2832.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. """Where a slice-to-archive actually writes (#2832).
  2. ``safe_path_component`` is unit-tested. It is only worth anything if the sink
  3. uses it: reverting the sanitiser leaves those tests green, because they never
  4. touch the code that builds the path. These drive ``slice_and_persist_as_archive``
  5. with the slicer stubbed, on the name from the report and on one that tries to
  6. leave the archive directory.
  7. """
  8. from unittest.mock import AsyncMock, patch
  9. import pytest
  10. from backend.app.api.routes.library import slice_and_persist_as_archive
  11. from backend.app.schemas.slicer import SliceRequest
  12. from backend.app.services.slicer_api import SliceResult
  13. pytestmark = [pytest.mark.integration, pytest.mark.asyncio]
  14. REPORTED = "Planter Pot with Drip Tray, 12 cm / 5 inches"
  15. @pytest.fixture
  16. def archive_root(monkeypatch, tmp_path):
  17. """Point both roots at a tmp dir, keeping their real relationship."""
  18. from backend.app.core.config import settings
  19. monkeypatch.setattr(settings, "base_dir", tmp_path)
  20. monkeypatch.setattr(settings, "archive_dir", tmp_path / "archive")
  21. return tmp_path / "archive"
  22. def _stub_slicer(content: bytes = b"PK\x03\x04 not-a-real-3mf"):
  23. return patch(
  24. "backend.app.api.routes.library._run_slicer_with_fallback",
  25. AsyncMock(return_value=(SliceResult(content, 3600, 12.5, 4200.0), False)),
  26. )
  27. async def _slice(db_session, source_archive, model_filename):
  28. with _stub_slicer():
  29. return await slice_and_persist_as_archive(
  30. db_session,
  31. model_bytes=b"source model",
  32. model_filename=model_filename,
  33. request=SliceRequest(printer_preset_id=1, process_preset_id=2, filament_preset_id=3),
  34. source_archive=source_archive,
  35. current_user_id=None,
  36. )
  37. async def _written_files(archive_root):
  38. return [p for p in archive_root.rglob("*") if p.is_file()]
  39. class TestTheReportedFailure:
  40. async def test_the_slice_lands_on_disk(self, db_session, archive_factory, printer_factory, archive_root):
  41. """The write used to fail with ENOENT: mkdir made the two directories
  42. the folder name implied, and the file's own join added a third that
  43. nobody had created."""
  44. printer = await printer_factory()
  45. source = await archive_factory(printer.id, print_name=REPORTED, filename=f"{REPORTED}.3mf")
  46. response = await _slice(db_session, source, f"{REPORTED}.3mf")
  47. written = await _written_files(archive_root)
  48. assert [p.name for p in written if p.suffix == ".3mf"] == [
  49. "Planter Pot with Drip Tray, 12 cm - 5 inches.gcode.3mf"
  50. ]
  51. # The display name keeps its punctuation -- only the path is reduced.
  52. assert "/" in response.name
  53. async def test_the_folder_is_one_level_deep(self, db_session, archive_factory, printer_factory, archive_root):
  54. """<archive>/<printer>/<timestamp>_<name>_sliced/<file> and no more.
  55. The slash used to add a level in the middle of the folder name."""
  56. printer = await printer_factory()
  57. source = await archive_factory(printer.id, print_name=REPORTED, filename=f"{REPORTED}.3mf")
  58. await _slice(db_session, source, f"{REPORTED}.3mf")
  59. written = [p for p in await _written_files(archive_root) if p.suffix == ".3mf"][0]
  60. assert written.relative_to(archive_root).parts[:1] == (str(printer.id),)
  61. assert len(written.relative_to(archive_root).parts) == 3
  62. async def test_the_archive_row_points_at_the_file(self, db_session, archive_factory, printer_factory, archive_root):
  63. """A row whose file_path does not exist is the same class of bug one
  64. step later -- every reprint and rescan reads it back."""
  65. from backend.app.core.config import settings
  66. from backend.app.models.archive import PrintArchive
  67. printer = await printer_factory()
  68. source = await archive_factory(printer.id, print_name=REPORTED, filename=f"{REPORTED}.3mf")
  69. response = await _slice(db_session, source, f"{REPORTED}.3mf")
  70. new_archive = await db_session.get(PrintArchive, response.archive_id)
  71. assert (settings.base_dir / new_archive.file_path).is_file()
  72. class TestItStaysInTheArchiveDirectory:
  73. @pytest.mark.parametrize(
  74. "name",
  75. [
  76. "../../../../etc/cron.d/x",
  77. "../escaped",
  78. "..",
  79. ],
  80. )
  81. async def test_a_traversing_name_writes_nowhere_else(
  82. self, db_session, archive_factory, printer_factory, archive_root, tmp_path, name
  83. ):
  84. """The display name is free text from the 3MF, so it is whatever its
  85. author put there."""
  86. printer = await printer_factory()
  87. source = await archive_factory(printer.id, print_name=name, filename="source.3mf")
  88. await _slice(db_session, source, f"{name}.3mf")
  89. written = await _written_files(archive_root)
  90. assert written, "nothing was written at all"
  91. for path in written:
  92. # Not merely inside the archive root: inside this slice's own
  93. # folder. A name that only climbs one level lands in the printer's
  94. # directory, which is still contained and still wrong.
  95. assert path.parent.name.endswith("_sliced"), path
  96. assert path.resolve().is_relative_to(archive_root.resolve())
  97. # And nothing appeared beside the archive root either.
  98. assert not [p for p in tmp_path.iterdir() if p.name != "archive"]
  99. class TestOrdinaryNamesAreUnchanged:
  100. async def test_a_plain_name_keeps_its_spelling(self, db_session, archive_factory, printer_factory, archive_root):
  101. printer = await printer_factory()
  102. source = await archive_factory(printer.id, print_name="Benchy", filename="Benchy.3mf")
  103. response = await _slice(db_session, source, "Benchy.3mf")
  104. assert response.name == "Benchy (re-sliced)"
  105. assert [p.name for p in await _written_files(archive_root) if p.suffix == ".3mf"] == ["Benchy.gcode.3mf"]