Fix virtual printer config changes ignored on running instances

sync_from_db() skipped VPs already in self._instances without checking
  if their config had changed. Mode, model, access code, bind IP, remote
  interface IP, and target printer changes were silently ignored until
  manual toggle off/on or full restart. Now detects config drift and
  restarts affected instances.
This commit is contained in:
maziggy
2026-03-01 09:40:39 +01:00
parent 8843af6665
commit f39464d073
3 changed files with 211 additions and 0 deletions
@@ -310,6 +310,192 @@ class TestVirtualPrinterManager:
await manager.stop_all()
assert len(manager._instances) == 0
# ========================================================================
# Tests for sync_from_db config change detection
# ========================================================================
def _make_db_vp(self, **overrides):
"""Create a mock VirtualPrinter DB object."""
defaults = {
"id": 1,
"name": "TestVP",
"enabled": True,
"mode": "immediate",
"model": "C11",
"access_code": "12345678",
"serial_suffix": "391800001",
"bind_ip": "",
"remote_interface_ip": "",
"target_printer_id": None,
"position": 0,
}
defaults.update(overrides)
vp = MagicMock()
for k, v in defaults.items():
setattr(vp, k, v)
return vp
def _setup_sync_mocks(self, manager, enabled_vps, tmp_path):
"""Wire up session_factory mock for sync_from_db."""
mock_result = MagicMock()
mock_result.scalars.return_value.all.return_value = enabled_vps
mock_db = AsyncMock()
mock_db.execute = AsyncMock(return_value=mock_result)
mock_db.__aenter__ = AsyncMock(return_value=mock_db)
mock_db.__aexit__ = AsyncMock(return_value=False)
manager._session_factory = MagicMock(return_value=mock_db)
manager._base_dir = tmp_path
@pytest.mark.asyncio
async def test_sync_from_db_restarts_on_mode_change(self, manager, tmp_path):
"""Verify sync_from_db restarts VP when mode changes."""
from backend.app.services.virtual_printer.manager import VirtualPrinterInstance
inst = VirtualPrinterInstance(
vp_id=1,
name="TestVP",
mode="immediate",
model="C11",
access_code="12345678",
serial_suffix="391800001",
base_dir=tmp_path,
)
inst.stop_server = AsyncMock()
manager._instances[1] = inst
# DB says mode changed to "archive"
db_vp = self._make_db_vp(mode="archive")
self._setup_sync_mocks(manager, [db_vp], tmp_path)
with patch.object(manager, "remove_instance", new_callable=AsyncMock) as mock_remove:
# Patch VirtualPrinterInstance to prevent actual start
with patch("backend.app.services.virtual_printer.manager.VirtualPrinterInstance") as MockInst:
mock_new = MagicMock()
mock_new.start_server = AsyncMock()
MockInst.return_value = mock_new
await manager.sync_from_db()
mock_remove.assert_called_once_with(1)
@pytest.mark.asyncio
async def test_sync_from_db_restarts_on_access_code_change(self, manager, tmp_path):
"""Verify sync_from_db restarts VP when access_code changes."""
from backend.app.services.virtual_printer.manager import VirtualPrinterInstance
inst = VirtualPrinterInstance(
vp_id=1,
name="TestVP",
mode="immediate",
model="C11",
access_code="12345678",
serial_suffix="391800001",
base_dir=tmp_path,
)
inst.stop_server = AsyncMock()
manager._instances[1] = inst
db_vp = self._make_db_vp(access_code="newcode99")
self._setup_sync_mocks(manager, [db_vp], tmp_path)
with patch.object(manager, "remove_instance", new_callable=AsyncMock) as mock_remove:
with patch("backend.app.services.virtual_printer.manager.VirtualPrinterInstance") as MockInst:
mock_new = MagicMock()
mock_new.start_server = AsyncMock()
MockInst.return_value = mock_new
await manager.sync_from_db()
mock_remove.assert_called_once_with(1)
@pytest.mark.asyncio
async def test_sync_from_db_skips_unchanged_instance(self, manager, tmp_path):
"""Verify sync_from_db does NOT restart when config is identical."""
from backend.app.services.virtual_printer.manager import VirtualPrinterInstance
inst = VirtualPrinterInstance(
vp_id=1,
name="TestVP",
mode="immediate",
model="C11",
access_code="12345678",
serial_suffix="391800001",
base_dir=tmp_path,
)
manager._instances[1] = inst
# DB matches running config exactly
db_vp = self._make_db_vp()
self._setup_sync_mocks(manager, [db_vp], tmp_path)
with patch.object(manager, "remove_instance", new_callable=AsyncMock) as mock_remove:
await manager.sync_from_db()
mock_remove.assert_not_called()
@pytest.mark.asyncio
async def test_sync_from_db_restarts_on_bind_ip_change(self, manager, tmp_path):
"""Verify sync_from_db restarts VP when bind_ip changes."""
from backend.app.services.virtual_printer.manager import VirtualPrinterInstance
inst = VirtualPrinterInstance(
vp_id=1,
name="TestVP",
mode="immediate",
model="C11",
access_code="12345678",
serial_suffix="391800001",
bind_ip="192.168.1.10",
base_dir=tmp_path,
)
inst.stop_server = AsyncMock()
manager._instances[1] = inst
db_vp = self._make_db_vp(bind_ip="192.168.1.20")
self._setup_sync_mocks(manager, [db_vp], tmp_path)
with patch.object(manager, "remove_instance", new_callable=AsyncMock) as mock_remove:
with patch("backend.app.services.virtual_printer.manager.VirtualPrinterInstance") as MockInst:
mock_new = MagicMock()
mock_new.start_server = AsyncMock()
MockInst.return_value = mock_new
await manager.sync_from_db()
mock_remove.assert_called_once_with(1)
@pytest.mark.asyncio
async def test_sync_from_db_restarts_on_model_change(self, manager, tmp_path):
"""Verify sync_from_db restarts VP when model changes."""
from backend.app.services.virtual_printer.manager import VirtualPrinterInstance
inst = VirtualPrinterInstance(
vp_id=1,
name="TestVP",
mode="immediate",
model="C11",
access_code="12345678",
serial_suffix="391800001",
base_dir=tmp_path,
)
inst.stop_server = AsyncMock()
manager._instances[1] = inst
db_vp = self._make_db_vp(model="C12")
self._setup_sync_mocks(manager, [db_vp], tmp_path)
with patch.object(manager, "remove_instance", new_callable=AsyncMock) as mock_remove:
with patch("backend.app.services.virtual_printer.manager.VirtualPrinterInstance") as MockInst:
mock_new = MagicMock()
mock_new.start_server = AsyncMock()
MockInst.return_value = mock_new
await manager.sync_from_db()
mock_remove.assert_called_once_with(1)
class TestFTPSession:
"""Tests for FTP session handling."""