"""Tests for DELETE /virtual-printers/{vp_id} orphan cleanup. Before the fix, deleting a VP only stopped the running instance and removed the row. The on-disk ``base_dir/uploads//`` directory lingered, and any ``PendingUpload`` rows that pointed into it remained in ``pending`` status — showing up as phantom entries in ``/pending-uploads/``. The route now (a) marks those rows as ``discarded`` and (b) ``shutil.rmtree``s the upload_dir after the DB commit succeeds. """ from unittest.mock import AsyncMock, MagicMock, patch import pytest from backend.app.api.routes.virtual_printers import delete_virtual_printer @pytest.mark.asyncio async def test_delete_vp_marks_orphan_pending_uploads_discarded(tmp_path): """A VP with PendingUpload rows pointing at its upload_dir: after DELETE, those rows must be flipped to ``discarded`` and the on-disk directory must be gone.""" vp_id = 77 upload_dir = tmp_path / "uploads" / str(vp_id) upload_dir.mkdir(parents=True) (upload_dir / "stale.3mf").write_bytes(b"orphaned content") # Build PendingUpload-like mocks. The route mutates `.status`. pending_a = MagicMock() pending_a.file_path = str(upload_dir / "stale.3mf") pending_a.status = "pending" pending_b = MagicMock() pending_b.file_path = str(upload_dir / "another.3mf") pending_b.status = "pending" # Unrelated PendingUpload that does NOT belong to this VP — must # be left alone. other_pending = MagicMock() other_pending.file_path = str(tmp_path / "uploads" / "99" / "not-mine.3mf") other_pending.status = "pending" # Mock VP row. vp_row = MagicMock() vp_row.id = vp_id vp_row.name = "DeleteMe" # Mock DB session with the route's two .execute() calls + flush + commit. select_calls = {"i": 0} async def fake_execute(query): # noqa: ARG001 """Return the VP row on the first call (vp lookup) and the in-range PendingUpload rows on the second call (orphan query). Third call is the DELETE which doesn't need a result.""" select_calls["i"] += 1 result = MagicMock() if select_calls["i"] == 1: result.scalar_one_or_none = MagicMock(return_value=vp_row) elif select_calls["i"] == 2: scalars = MagicMock() scalars.all = MagicMock(return_value=[pending_a, pending_b]) result.scalars = MagicMock(return_value=scalars) return result db = AsyncMock() db.execute = fake_execute db.flush = AsyncMock() db.commit = AsyncMock() # Mock the manager: remove_instance, _base_dir, sync_from_db. fake_manager = MagicMock() fake_manager.remove_instance = AsyncMock() fake_manager.sync_from_db = AsyncMock() fake_manager._base_dir = tmp_path with patch( "backend.app.services.virtual_printer.virtual_printer_manager", fake_manager, ): await delete_virtual_printer(vp_id=vp_id, db=db, _=None) # Both in-range PendingUpload rows must be flipped to "discarded". assert pending_a.status == "discarded" assert pending_b.status == "discarded" # The unrelated row was never returned from the query — left alone. assert other_pending.status == "pending" # The on-disk upload_dir is gone. assert not upload_dir.exists() # The running instance was stopped before the row was removed. fake_manager.remove_instance.assert_awaited_once_with(vp_id) @pytest.mark.asyncio async def test_delete_vp_with_no_orphan_uploads_still_succeeds(tmp_path): """A VP with no PendingUpload rows and no upload_dir on disk: the cleanup path must be a clean no-op, not raise.""" vp_id = 88 vp_row = MagicMock() vp_row.id = vp_id vp_row.name = "EmptyDelete" select_calls = {"i": 0} async def fake_execute(query): # noqa: ARG001 select_calls["i"] += 1 result = MagicMock() if select_calls["i"] == 1: result.scalar_one_or_none = MagicMock(return_value=vp_row) elif select_calls["i"] == 2: # No PendingUpload rows match. scalars = MagicMock() scalars.all = MagicMock(return_value=[]) result.scalars = MagicMock(return_value=scalars) return result db = AsyncMock() db.execute = fake_execute db.flush = AsyncMock() db.commit = AsyncMock() fake_manager = MagicMock() fake_manager.remove_instance = AsyncMock() fake_manager.sync_from_db = AsyncMock() fake_manager._base_dir = tmp_path # no uploads/ exists with patch( "backend.app.services.virtual_printer.virtual_printer_manager", fake_manager, ): await delete_virtual_printer(vp_id=vp_id, db=db, _=None) fake_manager.remove_instance.assert_awaited_once_with(vp_id) # No directory to remove — and we didn't crash trying to. assert not (tmp_path / "uploads" / str(vp_id)).exists()