|
@@ -310,6 +310,192 @@ class TestVirtualPrinterManager:
|
|
|
await manager.stop_all()
|
|
await manager.stop_all()
|
|
|
assert len(manager._instances) == 0
|
|
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:
|
|
class TestFTPSession:
|
|
|
"""Tests for FTP session handling."""
|
|
"""Tests for FTP session handling."""
|