mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
#1558: cached-as-base push_status only forced gcode_state=IDLE while letting the real printer's live-progress fields (mc_percent, stg_cur, layer_num, ...) leak through. Bambu Studio's Send pre-flight read them as busy and refused. The cached branch now overrides the activity-field set the same way it already overrode storage indicators (#1228) and protocol fields. Same bundle ships a multi-round VP audit that found adjacent bugs in the same family: - #1558: cached branch zeroes mc_print_stage / mc_percent / mc_remaining_time / stg / stg_cur / layer_num / total_layer_num / print_error - MQTT auth: per-IP rate-limit (5/60s lockout), hmac.compare_digest, access_code redacted in DEBUG log - FTP cmd_STOR streams chunks to disk + 4 GiB cap (was buffering whole upload) - Sticky-keys allowlist extended with upgrade_state / xcam / hw_switch_state / nozzle_diameter / nozzle_type / online / ams_status - _pending_files cleanup in finally for archive / queue / dispatch handlers - _add_to_print_queue position uses MAX+1 (was hardcoded 1) - DELETE VP removes orphan PendingUpload rows + upload_dir from disk - Per-VP cert regenerates on shared-CA rotation (real signature verification, not DN match) - DHCP target-IP refresh + queue_force_color_match toggle now restart proxy VPs - Per-slicer bridge-response routing (multi-slicer cross-leak fix via sequence_id map) - Child-service readiness barrier (FTP / MQTT / Bind / SSDP) — no false is_running before sockets bind - H2D Pro O1E / O2D model codes added (experimental, needs field confirmation) - FTP passive port range widened 50000-51000; docker-compose + wiki updated - VP refresh_loop crash now unbinds raw_message_handler; tailscale catches asyncio.TimeoutError; SlicerProxyManager lifecycle hardening
138 lines
4.8 KiB
Python
138 lines
4.8 KiB
Python
"""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/<vp_id>/`` 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/<vp_id> 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()
|