"""Coverage tests for views/sync/device_fields.py (target >95%).""" from unittest.mock import MagicMock, patch # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _make_view(ViewClass): """Create a view instance bypassing __init__, with a mock LibreNMS API.""" view = object.__new__(ViewClass) view._librenms_api = MagicMock() view._librenms_api.server_key = "default" view.require_all_permissions = MagicMock(return_value=None) return view def _make_request(post_data=None): req = MagicMock() req.POST = post_data or {} return req # --------------------------------------------------------------------------- # UpdateDeviceNameView # --------------------------------------------------------------------------- class TestUpdateDeviceNameView: def _view(self): from netbox_librenms_plugin.views.sync.device_fields import UpdateDeviceNameView return _make_view(UpdateDeviceNameView) def test_permission_denied_returns_error(self): view = self._view() error_response = MagicMock() view.require_all_permissions = MagicMock(return_value=error_response) with patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404") as mock_get: result = view.post(_make_request(), pk=1) assert result is error_response mock_get.assert_not_called() def test_no_librenms_id_returns_error(self): view = self._view() view._librenms_api.get_librenms_id.return_value = None mock_device = MagicMock() with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_device), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect") as mock_redir, ): view.post(_make_request(), pk=1) mock_msg.error.assert_called_once() mock_redir.assert_called_once() def test_get_device_info_failure(self): view = self._view() view._librenms_api.get_librenms_id.return_value = 42 view._librenms_api.get_device_info.return_value = (False, None) mock_device = MagicMock() with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_device), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request(), pk=1) mock_msg.error.assert_called_once() def test_get_device_info_empty_dict(self): """An empty (falsy) device_info dict triggers the 'Failed to retrieve' error path.""" view = self._view() view._librenms_api.get_librenms_id.return_value = 42 view._librenms_api.get_device_info.return_value = (True, {}) mock_device = MagicMock() with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_device), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request(), pk=1) # empty dict is falsy → triggers "Failed to retrieve device info" error mock_msg.error.assert_called_once() def test_no_sysname_returns_warning(self): view = self._view() view._librenms_api.get_librenms_id.return_value = 42 view._librenms_api.get_device_info.return_value = (True, {"sysName": None}) mock_device = MagicMock() with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_device), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request(), pk=1) mock_msg.warning.assert_called_once() def test_save_success(self): view = self._view() view._librenms_api.get_librenms_id.return_value = 42 view._librenms_api.get_device_info.return_value = (True, {"sysName": "router1"}) mock_device = MagicMock() mock_device.name = "old-name" mock_device.virtual_chassis = None with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_device), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect") as mock_redir, ): view.post(_make_request(), pk=1) mock_device.full_clean.assert_called_once() mock_device.save.assert_called_once() assert mock_device.name == "router1" mock_msg.success.assert_called_once() mock_redir.assert_called_once() def test_save_validation_error_with_message_dict(self): from django.core.exceptions import ValidationError view = self._view() view._librenms_api.get_librenms_id.return_value = 42 view._librenms_api.get_device_info.return_value = (True, {"sysName": "router1"}) mock_device = MagicMock() mock_device.name = "old-name" mock_device.virtual_chassis = None exc = ValidationError({"name": ["duplicate"]}) mock_device.full_clean.side_effect = exc with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_device), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request(), pk=1) mock_msg.error.assert_called_once() # Name should be restored assert mock_device.name == "old-name" def test_save_integrity_error_without_message_dict(self): from django.db import IntegrityError view = self._view() view._librenms_api.get_librenms_id.return_value = 42 view._librenms_api.get_device_info.return_value = (True, {"sysName": "router1"}) mock_device = MagicMock() mock_device.name = "old-name" mock_device.virtual_chassis = None mock_device.full_clean.side_effect = IntegrityError("duplicate key") with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_device), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request(), pk=1) mock_msg.error.assert_called_once() # --------------------------------------------------------------------------- # UpdateDeviceSerialView # --------------------------------------------------------------------------- class TestUpdateDeviceSerialView: def _view(self): from netbox_librenms_plugin.views.sync.device_fields import UpdateDeviceSerialView return _make_view(UpdateDeviceSerialView) def test_permission_denied(self): view = self._view() err = MagicMock() view.require_all_permissions = MagicMock(return_value=err) with patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404") as mock_get: result = view.post(_make_request(), pk=1) assert result is err mock_get.assert_not_called() def test_no_librenms_id(self): view = self._view() view._librenms_api.get_librenms_id.return_value = None with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=MagicMock()), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request(), pk=1) mock_msg.error.assert_called_once() def test_get_device_info_failure(self): view = self._view() view._librenms_api.get_librenms_id.return_value = 5 view._librenms_api.get_device_info.return_value = (False, None) with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=MagicMock()), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request(), pk=1) mock_msg.error.assert_called_once() def test_serial_is_none(self): view = self._view() view._librenms_api.get_librenms_id.return_value = 5 view._librenms_api.get_device_info.return_value = (True, {"serial": None}) with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=MagicMock()), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request(), pk=1) mock_msg.warning.assert_called_once() def test_serial_is_dash(self): view = self._view() view._librenms_api.get_librenms_id.return_value = 5 view._librenms_api.get_device_info.return_value = (True, {"serial": "-"}) with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=MagicMock()), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request(), pk=1) mock_msg.warning.assert_called_once() def test_save_success_with_old_serial(self): view = self._view() view._librenms_api.get_librenms_id.return_value = 5 view._librenms_api.get_device_info.return_value = (True, {"serial": "SN001"}) mock_device = MagicMock() mock_device.serial = "OLDSERIAL" with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_device), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request(), pk=1) mock_msg.success.assert_called_once() assert "OLDSERIAL" in mock_msg.success.call_args[0][1] assert mock_device.serial == "SN001" mock_device.full_clean.assert_called_once() mock_device.save.assert_called_once() def test_save_success_no_old_serial(self): view = self._view() view._librenms_api.get_librenms_id.return_value = 5 view._librenms_api.get_device_info.return_value = (True, {"serial": "SN001"}) mock_device = MagicMock() mock_device.serial = "" # No old serial with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_device), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request(), pk=1) mock_msg.success.assert_called_once() assert "set to" in mock_msg.success.call_args[0][1] assert mock_device.serial == "SN001" mock_device.full_clean.assert_called_once() mock_device.save.assert_called_once() def test_save_validation_error_with_message_dict(self): from django.core.exceptions import ValidationError view = self._view() view._librenms_api.get_librenms_id.return_value = 5 view._librenms_api.get_device_info.return_value = (True, {"serial": "SN001"}) mock_device = MagicMock() mock_device.serial = "OLD" mock_device.full_clean.side_effect = ValidationError({"serial": ["err"]}) with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_device), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request(), pk=1) mock_msg.error.assert_called_once() assert mock_device.serial == "OLD" def test_save_integrity_error(self): from django.db import IntegrityError view = self._view() view._librenms_api.get_librenms_id.return_value = 5 view._librenms_api.get_device_info.return_value = (True, {"serial": "SN001"}) mock_device = MagicMock() mock_device.serial = "OLD" mock_device.full_clean.side_effect = IntegrityError("dup") with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_device), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request(), pk=1) mock_msg.error.assert_called_once() # --------------------------------------------------------------------------- # UpdateDeviceTypeView # --------------------------------------------------------------------------- class TestUpdateDeviceTypeView: def _view(self): from netbox_librenms_plugin.views.sync.device_fields import UpdateDeviceTypeView return _make_view(UpdateDeviceTypeView) def test_permission_denied(self): view = self._view() err = MagicMock() view.require_all_permissions = MagicMock(return_value=err) with patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404") as mock_get: result = view.post(_make_request(), pk=1) assert result is err mock_get.assert_not_called() def test_no_librenms_id(self): view = self._view() view._librenms_api.get_librenms_id.return_value = None with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=MagicMock()), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request(), pk=1) mock_msg.error.assert_called_once() def test_get_device_info_failure(self): view = self._view() view._librenms_api.get_librenms_id.return_value = 7 view._librenms_api.get_device_info.return_value = (False, None) with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=MagicMock()), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request(), pk=1) mock_msg.error.assert_called_once() def test_no_hardware(self): view = self._view() view._librenms_api.get_librenms_id.return_value = 7 view._librenms_api.get_device_info.return_value = (True, {"hardware": None}) with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=MagicMock()), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request(), pk=1) mock_msg.warning.assert_called_once() def test_no_match_result(self): view = self._view() view._librenms_api.get_librenms_id.return_value = 7 view._librenms_api.get_device_info.return_value = (True, {"hardware": "Cisco 3750"}) with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=MagicMock()), patch( "netbox_librenms_plugin.views.sync.device_fields.match_librenms_hardware_to_device_type", return_value={"matched": False}, ), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request(), pk=1) mock_msg.error.assert_called_once() def test_save_success(self): view = self._view() view._librenms_api.get_librenms_id.return_value = 7 view._librenms_api.get_device_info.return_value = (True, {"hardware": "Cisco 3750"}) mock_dt = MagicMock() mock_device = MagicMock() with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_device), patch( "netbox_librenms_plugin.views.sync.device_fields.match_librenms_hardware_to_device_type", return_value={"matched": True, "device_type": mock_dt}, ), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request(), pk=1) mock_device.full_clean.assert_called_once() mock_device.save.assert_called_once() assert mock_device.device_type is mock_dt mock_msg.success.assert_called_once() def test_save_validation_error_with_message_dict(self): from django.core.exceptions import ValidationError view = self._view() view._librenms_api.get_librenms_id.return_value = 7 view._librenms_api.get_device_info.return_value = (True, {"hardware": "Cisco 3750"}) mock_dt = MagicMock() mock_device = MagicMock() mock_device.full_clean.side_effect = ValidationError({"device_type": ["err"]}) with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_device), patch( "netbox_librenms_plugin.views.sync.device_fields.match_librenms_hardware_to_device_type", return_value={"matched": True, "device_type": mock_dt}, ), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request(), pk=1) mock_msg.error.assert_called_once() def test_save_integrity_error(self): from django.db import IntegrityError view = self._view() view._librenms_api.get_librenms_id.return_value = 7 view._librenms_api.get_device_info.return_value = (True, {"hardware": "Cisco 3750"}) mock_dt = MagicMock() mock_device = MagicMock() mock_device.full_clean.side_effect = IntegrityError("dup") with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_device), patch( "netbox_librenms_plugin.views.sync.device_fields.match_librenms_hardware_to_device_type", return_value={"matched": True, "device_type": mock_dt}, ), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request(), pk=1) mock_msg.error.assert_called_once() # --------------------------------------------------------------------------- # UpdateDevicePlatformView # --------------------------------------------------------------------------- class TestUpdateDevicePlatformView: def _view(self): from netbox_librenms_plugin.views.sync.device_fields import UpdateDevicePlatformView return _make_view(UpdateDevicePlatformView) def test_permission_denied(self): view = self._view() err = MagicMock() view.require_all_permissions = MagicMock(return_value=err) with patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404"): result = view.post(_make_request(), pk=1) assert result is err def test_no_librenms_id(self): view = self._view() view._librenms_api.get_librenms_id.return_value = None with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=MagicMock()), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request(), pk=1) mock_msg.error.assert_called_once() def test_get_device_info_failure(self): view = self._view() view._librenms_api.get_librenms_id.return_value = 3 view._librenms_api.get_device_info.return_value = (False, None) with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=MagicMock()), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request(), pk=1) mock_msg.error.assert_called_once() def test_no_os(self): view = self._view() view._librenms_api.get_librenms_id.return_value = 3 view._librenms_api.get_device_info.return_value = (True, {"os": None}) with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=MagicMock()), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request(), pk=1) mock_msg.warning.assert_called_once() def test_platform_does_not_exist(self): view = self._view() view._librenms_api.get_librenms_id.return_value = 3 view._librenms_api.get_device_info.return_value = (True, {"os": "ios"}) mock_platform_cls = MagicMock() mock_platform_cls.DoesNotExist = type("DoesNotExist", (Exception,), {}) mock_platform_cls.objects.get.side_effect = mock_platform_cls.DoesNotExist() with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=MagicMock()), patch("netbox_librenms_plugin.views.sync.device_fields.Platform", mock_platform_cls), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request(), pk=1) mock_msg.error.assert_called_once() def test_save_success_with_old_platform(self): view = self._view() view._librenms_api.get_librenms_id.return_value = 3 view._librenms_api.get_device_info.return_value = (True, {"os": "ios"}) mock_platform = MagicMock() mock_platform_cls = MagicMock() mock_platform_cls.DoesNotExist = type("DoesNotExist", (Exception,), {}) mock_platform_cls.objects.get.return_value = mock_platform mock_device = MagicMock() mock_device.platform = MagicMock() # old platform exists with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_device), patch("netbox_librenms_plugin.views.sync.device_fields.Platform", mock_platform_cls), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request(), pk=1) mock_msg.success.assert_called_once() assert "updated from" in mock_msg.success.call_args[0][1] assert mock_device.platform is mock_platform mock_device.full_clean.assert_called_once() mock_device.save.assert_called_once() def test_save_success_no_old_platform(self): view = self._view() view._librenms_api.get_librenms_id.return_value = 3 view._librenms_api.get_device_info.return_value = (True, {"os": "ios"}) mock_platform = MagicMock() mock_platform_cls = MagicMock() mock_platform_cls.DoesNotExist = type("DoesNotExist", (Exception,), {}) mock_platform_cls.objects.get.return_value = mock_platform mock_device = MagicMock() mock_device.platform = None # no old platform with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_device), patch("netbox_librenms_plugin.views.sync.device_fields.Platform", mock_platform_cls), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request(), pk=1) mock_msg.success.assert_called_once() assert "set to" in mock_msg.success.call_args[0][1] assert mock_device.platform is mock_platform mock_device.full_clean.assert_called_once() mock_device.save.assert_called_once() def test_save_validation_error(self): from django.core.exceptions import ValidationError view = self._view() view._librenms_api.get_librenms_id.return_value = 3 view._librenms_api.get_device_info.return_value = (True, {"os": "ios"}) mock_platform = MagicMock() mock_platform_cls = MagicMock() mock_platform_cls.DoesNotExist = type("DoesNotExist", (Exception,), {}) mock_platform_cls.objects.get.return_value = mock_platform mock_device = MagicMock() mock_device.platform = None mock_device.full_clean.side_effect = ValidationError({"platform": ["err"]}) with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_device), patch("netbox_librenms_plugin.views.sync.device_fields.Platform", mock_platform_cls), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request(), pk=1) mock_msg.error.assert_called_once() # --------------------------------------------------------------------------- # CreateAndAssignPlatformView # --------------------------------------------------------------------------- class TestCreateAndAssignPlatformView: def _view(self): from netbox_librenms_plugin.views.sync.device_fields import CreateAndAssignPlatformView return _make_view(CreateAndAssignPlatformView) def test_permission_denied(self): view = self._view() err = MagicMock() view.require_all_permissions = MagicMock(return_value=err) with patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404"): result = view.post(_make_request(), pk=1) assert result is err def test_no_platform_name(self): view = self._view() req = _make_request({"platform_name": ""}) with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=MagicMock()), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(req, pk=1) mock_msg.error.assert_called_once() assert "required" in mock_msg.error.call_args[0][1].lower() def test_platform_already_exists(self): view = self._view() req = _make_request({"platform_name": "ios", "manufacturer": ""}) mock_platform_cls = MagicMock() mock_platform_cls.objects.filter.return_value.exists.return_value = True with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=MagicMock()), patch("netbox_librenms_plugin.views.sync.device_fields.Platform", mock_platform_cls), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(req, pk=1) mock_msg.warning.assert_called_once() def test_manufacturer_not_found(self): """manufacturer_id provided but Manufacturer.DoesNotExist: manufacturer stays None.""" view = self._view() req = _make_request({"platform_name": "ios", "manufacturer": "99"}) mock_platform_cls = MagicMock() mock_platform_cls.objects.filter.return_value.exists.return_value = False mock_platform_instance = MagicMock() mock_platform_cls.return_value = mock_platform_instance mock_manuf_cls = MagicMock() mock_manuf_cls.DoesNotExist = type("DoesNotExist", (Exception,), {}) mock_manuf_cls.objects.get.side_effect = mock_manuf_cls.DoesNotExist() mock_device_cls = MagicMock() mock_device_cls.DoesNotExist = type("DoesNotExist", (Exception,), {}) mock_locked = MagicMock() mock_device_cls.objects.select_for_update.return_value.get.return_value = mock_locked with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=MagicMock()), patch("netbox_librenms_plugin.views.sync.device_fields.Platform", mock_platform_cls), patch("netbox_librenms_plugin.views.sync.device_fields.Manufacturer", mock_manuf_cls), patch("netbox_librenms_plugin.views.sync.device_fields.Device", mock_device_cls), patch("netbox_librenms_plugin.views.sync.device_fields.transaction"), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(req, pk=1) # Should succeed (manufacturer silently ignored) mock_msg.success.assert_called_once() assert mock_locked.platform == mock_platform_instance mock_locked.save.assert_called_once() def test_success_no_manufacturer(self): view = self._view() req = _make_request({"platform_name": "ios", "manufacturer": ""}) mock_platform_cls = MagicMock() mock_platform_cls.objects.filter.return_value.exists.return_value = False mock_platform_instance = MagicMock() mock_platform_cls.return_value = mock_platform_instance mock_device_cls = MagicMock() mock_device_cls.DoesNotExist = type("DoesNotExist", (Exception,), {}) mock_locked = MagicMock() mock_device_cls.objects.select_for_update.return_value.get.return_value = mock_locked with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=MagicMock()), patch("netbox_librenms_plugin.views.sync.device_fields.Platform", mock_platform_cls), patch("netbox_librenms_plugin.views.sync.device_fields.Device", mock_device_cls), patch("netbox_librenms_plugin.views.sync.device_fields.transaction"), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(req, pk=1) mock_msg.success.assert_called_once() assert mock_locked.platform == mock_platform_instance mock_locked.save.assert_called_once() def test_platform_validation_error(self): from django.core.exceptions import ValidationError view = self._view() req = _make_request({"platform_name": "ios", "manufacturer": ""}) mock_platform_cls = MagicMock() mock_platform_cls.objects.filter.return_value.exists.return_value = False mock_platform_instance = MagicMock() mock_platform_instance.full_clean.side_effect = ValidationError({"name": ["err"]}) mock_platform_cls.return_value = mock_platform_instance mock_txn = MagicMock() mock_txn.set_rollback = MagicMock() with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=MagicMock()), patch("netbox_librenms_plugin.views.sync.device_fields.Platform", mock_platform_cls), patch("netbox_librenms_plugin.views.sync.device_fields.transaction", mock_txn), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(req, pk=1) mock_msg.error.assert_called_once() mock_txn.set_rollback.assert_called_once_with(True) def test_device_does_not_exist_inside_transaction(self): view = self._view() req = _make_request({"platform_name": "ios", "manufacturer": ""}) mock_platform_cls = MagicMock() mock_platform_cls.objects.filter.return_value.exists.return_value = False mock_platform_instance = MagicMock() mock_platform_cls.return_value = mock_platform_instance DoesNotExist = type("DoesNotExist", (Exception,), {}) mock_device_cls = MagicMock() mock_device_cls.DoesNotExist = DoesNotExist mock_device_cls.objects.select_for_update.return_value.get.side_effect = DoesNotExist() mock_txn = MagicMock() mock_txn.set_rollback = MagicMock() with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=MagicMock()), patch("netbox_librenms_plugin.views.sync.device_fields.Platform", mock_platform_cls), patch("netbox_librenms_plugin.views.sync.device_fields.Device", mock_device_cls), patch("netbox_librenms_plugin.views.sync.device_fields.transaction", mock_txn), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(req, pk=1) mock_msg.error.assert_called_once() mock_txn.set_rollback.assert_called_once_with(True) def test_device_validation_error(self): from django.core.exceptions import ValidationError view = self._view() req = _make_request({"platform_name": "ios", "manufacturer": ""}) mock_platform_cls = MagicMock() mock_platform_cls.objects.filter.return_value.exists.return_value = False mock_platform_instance = MagicMock() mock_platform_cls.return_value = mock_platform_instance DoesNotExist = type("DoesNotExist", (Exception,), {}) mock_locked = MagicMock() mock_locked.full_clean.side_effect = ValidationError({"platform": ["err"]}) mock_device_cls = MagicMock() mock_device_cls.DoesNotExist = DoesNotExist mock_device_cls.objects.select_for_update.return_value.get.return_value = mock_locked mock_txn = MagicMock() mock_txn.set_rollback = MagicMock() with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=MagicMock()), patch("netbox_librenms_plugin.views.sync.device_fields.Platform", mock_platform_cls), patch("netbox_librenms_plugin.views.sync.device_fields.Device", mock_device_cls), patch("netbox_librenms_plugin.views.sync.device_fields.transaction", mock_txn), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(req, pk=1) mock_msg.error.assert_called_once() mock_txn.set_rollback.assert_called_once_with(True) def test_integrity_error(self): from django.db import IntegrityError view = self._view() req = _make_request({"platform_name": "ios", "manufacturer": ""}) mock_platform_cls = MagicMock() mock_platform_cls.objects.filter.return_value.exists.return_value = False mock_platform_instance = MagicMock() # Make save raise IntegrityError mock_platform_instance.save.side_effect = IntegrityError("duplicate") mock_platform_cls.return_value = mock_platform_instance # transaction.atomic().__exit__ must return False so IntegrityError propagates mock_atomic_cm = MagicMock() mock_atomic_cm.__enter__ = MagicMock(return_value=None) mock_atomic_cm.__exit__ = MagicMock(return_value=False) mock_txn = MagicMock() mock_txn.atomic.return_value = mock_atomic_cm mock_txn.set_rollback = MagicMock() with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=MagicMock()), patch("netbox_librenms_plugin.views.sync.device_fields.Platform", mock_platform_cls), patch("netbox_librenms_plugin.views.sync.device_fields.transaction", mock_txn), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(req, pk=1) mock_msg.error.assert_called_once() mock_txn.set_rollback.assert_called_once_with(True) # --------------------------------------------------------------------------- # AssignVCSerialView # --------------------------------------------------------------------------- class TestAssignVCSerialView: def _view(self): from netbox_librenms_plugin.views.sync.device_fields import AssignVCSerialView return _make_view(AssignVCSerialView) def test_permission_denied(self): view = self._view() err = MagicMock() view.require_all_permissions = MagicMock(return_value=err) with patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404"): result = view.post(_make_request(), pk=1) assert result is err def test_not_virtual_chassis(self): view = self._view() mock_device = MagicMock() mock_device.virtual_chassis = None with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_device), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request(), pk=1) mock_msg.error.assert_called_once() def test_no_serial_assignments_no_errors(self): """Loop doesn't execute — no serial_N keys in POST.""" view = self._view() mock_device = MagicMock() mock_device.virtual_chassis = MagicMock() with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_device), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request({}), pk=1) mock_msg.info.assert_called_once() def test_member_id_missing(self): """member_id_{N} key is absent → counter incremented, no assignment.""" view = self._view() mock_device = MagicMock() mock_device.virtual_chassis = MagicMock() # serial_1 exists but member_id_1 is empty req = _make_request({"serial_1": "SN100", "member_id_1": ""}) with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_device), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(req, pk=1) mock_msg.info.assert_called_once() def test_member_not_found(self): view = self._view() mock_device = MagicMock() mock_device.virtual_chassis = MagicMock(pk=10) DoesNotExist = type("DoesNotExist", (Exception,), {}) mock_device_cls = MagicMock() mock_device_cls.DoesNotExist = DoesNotExist mock_device_cls.objects.get.side_effect = DoesNotExist() req = _make_request({"serial_1": "SN100", "member_id_1": "99"}) with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_device), patch("netbox_librenms_plugin.views.sync.device_fields.Device", mock_device_cls), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(req, pk=1) # Should call error for the missing device mock_msg.error.assert_called() def test_member_different_chassis(self): view = self._view() vc = MagicMock(pk=10) mock_device = MagicMock() mock_device.virtual_chassis = vc member = MagicMock() member.name = "sw-member" member.virtual_chassis = MagicMock(pk=99) # different VC! DoesNotExist = type("DoesNotExist", (Exception,), {}) mock_device_cls = MagicMock() mock_device_cls.DoesNotExist = DoesNotExist mock_device_cls.objects.get.return_value = member req = _make_request({"serial_1": "SN100", "member_id_1": "5"}) with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_device), patch("netbox_librenms_plugin.views.sync.device_fields.Device", mock_device_cls), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(req, pk=1) mock_msg.error.assert_called() def test_member_save_validation_error(self): from django.core.exceptions import ValidationError view = self._view() vc = MagicMock(pk=10) mock_device = MagicMock() mock_device.virtual_chassis = vc member = MagicMock() member.name = "sw-member" member.virtual_chassis = vc # same VC member.serial = "OLD" member.full_clean.side_effect = ValidationError({"serial": ["err"]}) DoesNotExist = type("DoesNotExist", (Exception,), {}) mock_device_cls = MagicMock() mock_device_cls.DoesNotExist = DoesNotExist mock_device_cls.objects.get.return_value = member req = _make_request({"serial_1": "SN100", "member_id_1": "5"}) with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_device), patch("netbox_librenms_plugin.views.sync.device_fields.Device", mock_device_cls), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(req, pk=1) mock_msg.error.assert_called() def test_member_save_success(self): view = self._view() vc = MagicMock(pk=10) mock_device = MagicMock() mock_device.virtual_chassis = vc member = MagicMock() member.name = "sw-member" member.virtual_chassis = vc member.serial = "OLD" member.save = MagicMock() DoesNotExist = type("DoesNotExist", (Exception,), {}) mock_device_cls = MagicMock() mock_device_cls.DoesNotExist = DoesNotExist mock_device_cls.objects.get.return_value = member req = _make_request({"serial_1": "SN100", "member_id_1": "5"}) with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_device), patch("netbox_librenms_plugin.views.sync.device_fields.Device", mock_device_cls), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(req, pk=1) mock_msg.success.assert_called_once() assert member.serial == "SN100" member.save.assert_called_once() def test_assignments_and_errors_both_reported(self): """One success + one error → both messages emitted.""" from django.core.exceptions import ValidationError view = self._view() vc = MagicMock(pk=10) mock_device = MagicMock() mock_device.virtual_chassis = vc good_member = MagicMock() good_member.name = "sw1" good_member.virtual_chassis = vc good_member.serial = "" bad_member = MagicMock() bad_member.name = "sw2" bad_member.virtual_chassis = vc bad_member.serial = "" bad_member.full_clean.side_effect = ValidationError({"serial": ["dup"]}) DoesNotExist = type("DoesNotExist", (Exception,), {}) mock_device_cls = MagicMock() mock_device_cls.DoesNotExist = DoesNotExist mock_device_cls.objects.get.side_effect = [good_member, bad_member] req = _make_request( { "serial_1": "SN001", "member_id_1": "1", "serial_2": "SN002", "member_id_2": "2", } ) with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_device), patch("netbox_librenms_plugin.views.sync.device_fields.Device", mock_device_cls), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(req, pk=1) mock_msg.success.assert_called() mock_msg.error.assert_called() assert good_member.serial == "SN001" good_member.save.assert_called_once() # --------------------------------------------------------------------------- # RemoveServerMappingView — helper methods # --------------------------------------------------------------------------- class TestRemoveServerMappingViewHelpers: def _view(self): from netbox_librenms_plugin.views.sync.device_fields import RemoveServerMappingView view = object.__new__(RemoveServerMappingView) view.require_all_permissions = MagicMock(return_value=None) return view def test_get_object_device(self): view = self._view() mock_device = MagicMock() with patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_device): obj, model = view._get_object("device", 1) assert obj is mock_device def test_get_object_vm(self): view = self._view() mock_vm = MagicMock() with patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_vm): obj, model = view._get_object("vm", 1) assert obj is mock_vm def test_sync_url_name_device(self): view = self._view() assert view._sync_url_name("device") == "plugins:netbox_librenms_plugin:device_librenms_sync" def test_sync_url_name_vm(self): view = self._view() assert view._sync_url_name("vm") == "plugins:netbox_librenms_plugin:vm_librenms_sync" def test_normalize_bool(self): view = self._view() assert view._normalize_librenms_mapping(True) == {} assert view._normalize_librenms_mapping(False) == {} def test_normalize_int(self): view = self._view() assert view._normalize_librenms_mapping(42) == {"default": 42} def test_normalize_string_digit(self): view = self._view() assert view._normalize_librenms_mapping("99") == {"default": 99} def test_normalize_dict(self): view = self._view() d = {"server1": 10} assert view._normalize_librenms_mapping(d) == d def test_normalize_non_digit_string_returns_empty(self): view = self._view() assert view._normalize_librenms_mapping("not-a-number") == {} def test_normalize_none_returns_empty(self): view = self._view() assert view._normalize_librenms_mapping(None) == {} # --------------------------------------------------------------------------- # RemoveServerMappingView — post() # --------------------------------------------------------------------------- class TestRemoveServerMappingViewPost: def _view(self): from netbox_librenms_plugin.views.sync.device_fields import RemoveServerMappingView view = object.__new__(RemoveServerMappingView) view.require_all_permissions = MagicMock(return_value=None) return view def test_invalid_object_type_returns_400(self): view = self._view() req = _make_request({"object_type": "badtype"}) result = view.post(req, pk=1) assert result.status_code == 400 def test_virtualmachine_object_type_normalized_to_vm(self): """object_type='virtualmachine' is normalised to 'vm'.""" view = self._view() mock_obj = MagicMock() mock_obj.custom_field_data = {"librenms_id": {"orphan": 5}} req = _make_request({"object_type": "virtualmachine", "server_key": "orphan"}) DoesNotExist = type("DoesNotExist", (Exception,), {}) mock_vm_cls = MagicMock() mock_vm_cls.DoesNotExist = DoesNotExist mock_locked = MagicMock() mock_locked.custom_field_data = {"librenms_id": {"orphan": 5}} mock_vm_cls.objects.select_for_update.return_value.get.return_value = mock_locked mock_cfg = {"netbox_librenms_plugin": {"servers": {}, "librenms_url": ""}} with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_obj), patch("netbox_librenms_plugin.views.sync.device_fields.VirtualMachine", mock_vm_cls), patch("netbox_librenms_plugin.views.sync.device_fields.transaction"), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), patch("django.conf.settings") as mock_settings, ): mock_settings.PLUGINS_CONFIG = mock_cfg view.post(req, pk=1) mock_msg.success.assert_called_once() def test_permission_denied(self): view = self._view() err = MagicMock() view.require_all_permissions = MagicMock(return_value=err) req = _make_request({"object_type": "device", "server_key": "x"}) result = view.post(req, pk=1) assert result is err def test_no_server_key(self): view = self._view() req = _make_request({"object_type": "device", "server_key": ""}) with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=MagicMock()), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(req, pk=1) mock_msg.error.assert_called_once() def test_mapping_not_found_wrong_type(self): """cf_value is not a dict → warning.""" view = self._view() mock_obj = MagicMock() mock_obj.custom_field_data = {"librenms_id": None} req = _make_request({"object_type": "device", "server_key": "default"}) with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_obj), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(req, pk=1) mock_msg.warning.assert_called_once() def test_mapping_not_found_missing_key(self): """server_key not in cf_value dict → warning.""" view = self._view() mock_obj = MagicMock() mock_obj.custom_field_data = {"librenms_id": {"other": 5}} req = _make_request({"object_type": "device", "server_key": "default"}) with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_obj), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(req, pk=1) mock_msg.warning.assert_called_once() def test_configured_servers_non_dict_treated_as_empty(self): """servers config is a list (non-dict) → treated as empty dict, orphan key can be removed.""" view = self._view() mock_obj = MagicMock() mock_obj.custom_field_data = {"librenms_id": {"orphan": 5}} req = _make_request({"object_type": "device", "server_key": "orphan"}) DoesNotExist = type("DoesNotExist", (Exception,), {}) mock_locked = MagicMock() mock_locked.custom_field_data = {"librenms_id": {"orphan": 5}} mock_device_cls = MagicMock() mock_device_cls.__name__ = "Device" mock_device_cls.DoesNotExist = DoesNotExist mock_device_cls.objects.select_for_update.return_value.get.return_value = mock_locked # servers is a list (non-dict) → line 496 normalises it to {} mock_cfg = {"netbox_librenms_plugin": {"servers": ["not", "a", "dict"], "librenms_url": ""}} with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_obj), patch("netbox_librenms_plugin.views.sync.device_fields.Device", mock_device_cls), patch("netbox_librenms_plugin.views.sync.device_fields.transaction"), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), patch("django.conf.settings") as mock_settings, ): mock_settings.PLUGINS_CONFIG = mock_cfg view.post(req, pk=1) mock_msg.success.assert_called_once() def test_configured_server_key_in_servers_dict(self): """server_key is in configured servers → error.""" view = self._view() mock_obj = MagicMock() mock_obj.custom_field_data = {"librenms_id": {"production": 10}} req = _make_request({"object_type": "device", "server_key": "production"}) mock_cfg = {"netbox_librenms_plugin": {"servers": {"production": {}}, "librenms_url": ""}} with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_obj), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), patch("django.conf.settings") as mock_settings, ): mock_settings.PLUGINS_CONFIG = mock_cfg view.post(req, pk=1) mock_msg.error.assert_called_once() assert "Cannot remove" in mock_msg.error.call_args[0][1] def test_legacy_default_server_protected(self): """Legacy mode with librenms_url set and server_key='default' → error.""" view = self._view() mock_obj = MagicMock() mock_obj.custom_field_data = {"librenms_id": {"default": 7}} req = _make_request({"object_type": "device", "server_key": "default"}) mock_cfg = {"netbox_librenms_plugin": {"servers": {}, "librenms_url": "https://librenms.example.com"}} with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_obj), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), patch("django.conf.settings") as mock_settings, ): mock_settings.PLUGINS_CONFIG = mock_cfg view.post(req, pk=1) mock_msg.error.assert_called_once() def test_object_no_longer_exists_inside_transaction(self): view = self._view() mock_obj = MagicMock() mock_obj.custom_field_data = {"librenms_id": {"orphan": 5}} req = _make_request({"object_type": "device", "server_key": "orphan"}) DoesNotExist = type("DoesNotExist", (Exception,), {}) mock_device_cls = MagicMock() mock_device_cls.__name__ = "Device" mock_device_cls.DoesNotExist = DoesNotExist mock_device_cls.objects.select_for_update.return_value.get.side_effect = DoesNotExist() mock_cfg = {"netbox_librenms_plugin": {"servers": {}, "librenms_url": ""}} with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_obj), patch("netbox_librenms_plugin.views.sync.device_fields.Device", mock_device_cls), patch("netbox_librenms_plugin.views.sync.device_fields.transaction"), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), patch("django.conf.settings") as mock_settings, ): mock_settings.PLUGINS_CONFIG = mock_cfg view.post(req, pk=1) mock_msg.error.assert_called_once() def test_mapping_already_removed_in_lock(self): """server_key is gone from the locked object's cf → warning.""" view = self._view() mock_obj = MagicMock() mock_obj.custom_field_data = {"librenms_id": {"orphan": 5}} req = _make_request({"object_type": "device", "server_key": "orphan"}) DoesNotExist = type("DoesNotExist", (Exception,), {}) mock_locked = MagicMock() # Key was removed between the first read and the lock mock_locked.custom_field_data = {"librenms_id": {}} mock_device_cls = MagicMock() mock_device_cls.DoesNotExist = DoesNotExist mock_device_cls.objects.select_for_update.return_value.get.return_value = mock_locked mock_cfg = {"netbox_librenms_plugin": {"servers": {}, "librenms_url": ""}} with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_obj), patch("netbox_librenms_plugin.views.sync.device_fields.Device", mock_device_cls), patch("netbox_librenms_plugin.views.sync.device_fields.transaction"), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), patch("django.conf.settings") as mock_settings, ): mock_settings.PLUGINS_CONFIG = mock_cfg view.post(req, pk=1) mock_msg.warning.assert_called_once() def test_validation_error_on_save(self): from django.core.exceptions import ValidationError view = self._view() mock_obj = MagicMock() mock_obj.custom_field_data = {"librenms_id": {"orphan": 5}} req = _make_request({"object_type": "device", "server_key": "orphan"}) DoesNotExist = type("DoesNotExist", (Exception,), {}) mock_locked = MagicMock() mock_locked.custom_field_data = {"librenms_id": {"orphan": 5}} mock_locked.full_clean.side_effect = ValidationError({"librenms_id": ["err"]}) mock_device_cls = MagicMock() mock_device_cls.DoesNotExist = DoesNotExist mock_device_cls.objects.select_for_update.return_value.get.return_value = mock_locked mock_txn = MagicMock() mock_txn.set_rollback = MagicMock() mock_cfg = {"netbox_librenms_plugin": {"servers": {}, "librenms_url": ""}} with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_obj), patch("netbox_librenms_plugin.views.sync.device_fields.Device", mock_device_cls), patch("netbox_librenms_plugin.views.sync.device_fields.transaction", mock_txn), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), patch("django.conf.settings") as mock_settings, ): mock_settings.PLUGINS_CONFIG = mock_cfg view.post(req, pk=1) mock_msg.error.assert_called_once() mock_txn.set_rollback.assert_called_once_with(True) def test_unexpected_error_on_save(self): view = self._view() mock_obj = MagicMock() mock_obj.custom_field_data = {"librenms_id": {"orphan": 5}} req = _make_request({"object_type": "device", "server_key": "orphan"}) DoesNotExist = type("DoesNotExist", (Exception,), {}) mock_locked = MagicMock() mock_locked.custom_field_data = {"librenms_id": {"orphan": 5}} mock_locked.full_clean.side_effect = RuntimeError("disk full") mock_device_cls = MagicMock() mock_device_cls.DoesNotExist = DoesNotExist mock_device_cls.objects.select_for_update.return_value.get.return_value = mock_locked mock_txn = MagicMock() mock_txn.set_rollback = MagicMock() mock_cfg = {"netbox_librenms_plugin": {"servers": {}, "librenms_url": ""}} with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_obj), patch("netbox_librenms_plugin.views.sync.device_fields.Device", mock_device_cls), patch("netbox_librenms_plugin.views.sync.device_fields.transaction", mock_txn), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), patch("django.conf.settings") as mock_settings, ): mock_settings.PLUGINS_CONFIG = mock_cfg view.post(req, pk=1) mock_msg.error.assert_called_once() mock_txn.set_rollback.assert_called_once_with(True) def test_success_removes_mapping(self): """Happy path: mapping removed, last entry → cf set to None.""" view = self._view() mock_obj = MagicMock() mock_obj.custom_field_data = {"librenms_id": {"orphan": 5}} req = _make_request({"object_type": "device", "server_key": "orphan"}) DoesNotExist = type("DoesNotExist", (Exception,), {}) mock_locked = MagicMock() mock_locked.custom_field_data = {"librenms_id": {"orphan": 5}} mock_device_cls = MagicMock() mock_device_cls.DoesNotExist = DoesNotExist mock_device_cls.objects.select_for_update.return_value.get.return_value = mock_locked mock_cfg = {"netbox_librenms_plugin": {"servers": {}, "librenms_url": ""}} with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_obj), patch("netbox_librenms_plugin.views.sync.device_fields.Device", mock_device_cls), patch("netbox_librenms_plugin.views.sync.device_fields.transaction"), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), patch("django.conf.settings") as mock_settings, ): mock_settings.PLUGINS_CONFIG = mock_cfg view.post(req, pk=1) mock_msg.success.assert_called_once() mock_locked.full_clean.assert_called_once() mock_locked.save.assert_called_once() # After deleting the last key, cf should be set to None assert mock_locked.custom_field_data["librenms_id"] is None def test_success_keeps_remaining_mappings(self): """Happy path: mapping removed, other entries remain → cf retains them.""" view = self._view() mock_obj = MagicMock() mock_obj.custom_field_data = {"librenms_id": {"orphan": 5, "other": 6}} req = _make_request({"object_type": "device", "server_key": "orphan"}) DoesNotExist = type("DoesNotExist", (Exception,), {}) mock_locked = MagicMock() mock_locked.custom_field_data = {"librenms_id": {"orphan": 5, "other": 6}} mock_device_cls = MagicMock() mock_device_cls.DoesNotExist = DoesNotExist mock_device_cls.objects.select_for_update.return_value.get.return_value = mock_locked mock_cfg = {"netbox_librenms_plugin": {"servers": {}, "librenms_url": ""}} with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_obj), patch("netbox_librenms_plugin.views.sync.device_fields.Device", mock_device_cls), patch("netbox_librenms_plugin.views.sync.device_fields.transaction"), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), patch("django.conf.settings") as mock_settings, ): mock_settings.PLUGINS_CONFIG = mock_cfg view.post(req, pk=1) mock_msg.success.assert_called_once() mock_locked.full_clean.assert_called_once() mock_locked.save.assert_called_once() assert mock_locked.custom_field_data["librenms_id"] == {"other": 6} # --------------------------------------------------------------------------- # ConvertLegacyLibreNMSIdView — helper methods # --------------------------------------------------------------------------- class TestConvertLegacyLibreNMSIdViewHelpers: def _view(self): from netbox_librenms_plugin.views.sync.device_fields import ConvertLegacyLibreNMSIdView view = object.__new__(ConvertLegacyLibreNMSIdView) view._librenms_api = MagicMock() view._librenms_api.server_key = "default" view.require_all_permissions = MagicMock(return_value=None) return view def test_get_model_and_object_device(self): view = self._view() mock_device = MagicMock() with patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_device): model, obj = view._get_model_and_object("device", 1) assert obj is mock_device def test_get_model_and_object_vm(self): view = self._view() mock_vm = MagicMock() with patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_vm): model, obj = view._get_model_and_object("vm", 1) assert obj is mock_vm def test_sync_url_device(self): view = self._view() with patch("netbox_librenms_plugin.views.sync.device_fields.redirect") as mock_redir: view._sync_url("device", 1) mock_redir.assert_called_once_with("plugins:netbox_librenms_plugin:device_librenms_sync", pk=1) def test_sync_url_vm(self): view = self._view() with patch("netbox_librenms_plugin.views.sync.device_fields.redirect") as mock_redir: view._sync_url("vm", 1) mock_redir.assert_called_once_with("plugins:netbox_librenms_plugin:vm_librenms_sync", pk=1) # --------------------------------------------------------------------------- # ConvertLegacyLibreNMSIdView — post() # --------------------------------------------------------------------------- class TestConvertLegacyLibreNMSIdViewPost: def _view(self): from netbox_librenms_plugin.views.sync.device_fields import ConvertLegacyLibreNMSIdView view = object.__new__(ConvertLegacyLibreNMSIdView) view._librenms_api = MagicMock() view._librenms_api.server_key = "default" view.require_all_permissions = MagicMock(return_value=None) return view def test_invalid_object_type_returns_400(self): view = self._view() req = _make_request({"object_type": "badtype"}) with patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404"): result = view.post(req, pk=1) assert result.status_code == 400 def test_virtualmachine_object_type_normalised(self): """object_type='virtualmachine' is accepted as 'vm'.""" view = self._view() # Provide a legacy string int as cf_value mock_obj = MagicMock() mock_obj.custom_field_data = {"librenms_id": "42"} mock_obj.serial = "SN-MATCH" view._librenms_api.get_device_info.return_value = (True, {"serial": "SN-MATCH"}) view._librenms_api.server_key = "default" DoesNotExist = type("DoesNotExist", (Exception,), {}) mock_locked = MagicMock() mock_locked.custom_field_data = {"librenms_id": "42"} mock_locked.serial = "SN-MATCH" mock_vm_cls = MagicMock() mock_vm_cls.DoesNotExist = DoesNotExist mock_vm_cls.objects.select_for_update.return_value.get.return_value = mock_locked with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_obj), patch("netbox_librenms_plugin.views.sync.device_fields.VirtualMachine", mock_vm_cls), patch("netbox_librenms_plugin.views.sync.device_fields.find_by_librenms_id", return_value=None), patch( "netbox_librenms_plugin.views.sync.device_fields.migrate_legacy_librenms_id", return_value=True ) as mock_migrate, patch("netbox_librenms_plugin.views.sync.device_fields.transaction"), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request({"object_type": "virtualmachine"}), pk=1) mock_msg.success.assert_called_once() mock_migrate.assert_called_once() mock_locked.full_clean.assert_called_once() mock_locked.save.assert_called_once() def test_permission_denied(self): view = self._view() err = MagicMock() view.require_all_permissions = MagicMock(return_value=err) req = _make_request({"object_type": "device"}) with patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404"): result = view.post(req, pk=1) assert result is err def test_already_json_format_dict(self): view = self._view() mock_obj = MagicMock() mock_obj.custom_field_data = {"librenms_id": {"default": 5}} with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_obj), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request({"object_type": "device"}), pk=1) mock_msg.warning.assert_called_once() assert "already" in mock_msg.warning.call_args[0][1].lower() def test_already_json_format_bool(self): view = self._view() mock_obj = MagicMock() mock_obj.custom_field_data = {"librenms_id": True} with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_obj), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request({"object_type": "device"}), pk=1) mock_msg.error.assert_called_once() def test_non_digit_string_cf_value(self): view = self._view() mock_obj = MagicMock() mock_obj.custom_field_data = {"librenms_id": "not-a-number"} with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_obj), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request({"object_type": "device"}), pk=1) mock_msg.error.assert_called_once() def test_get_device_info_failure(self): view = self._view() mock_obj = MagicMock() mock_obj.custom_field_data = {"librenms_id": 42} view._librenms_api.get_device_info.return_value = (False, None) with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_obj), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request({"object_type": "device"}), pk=1) mock_msg.error.assert_called_once() def test_serial_mismatch_empty_netbox_serial(self): view = self._view() mock_obj = MagicMock() mock_obj.custom_field_data = {"librenms_id": 42} mock_obj.serial = "" view._librenms_api.get_device_info.return_value = (True, {"serial": "SN-ABC"}) with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_obj), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request({"object_type": "device"}), pk=1) mock_msg.error.assert_called_once() assert "Serial" in mock_msg.error.call_args[0][1] def test_serial_mismatch_different(self): view = self._view() mock_obj = MagicMock() mock_obj.custom_field_data = {"librenms_id": 42} mock_obj.serial = "SN-XYZ" view._librenms_api.get_device_info.return_value = (True, {"serial": "SN-ABC"}) with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_obj), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request({"object_type": "device"}), pk=1) mock_msg.error.assert_called_once() def test_object_no_longer_exists_in_lock(self): view = self._view() mock_obj = MagicMock() mock_obj.custom_field_data = {"librenms_id": 42} mock_obj.serial = "SN-MATCH" view._librenms_api.get_device_info.return_value = (True, {"serial": "SN-MATCH"}) DoesNotExist = type("DoesNotExist", (Exception,), {}) mock_device_cls = MagicMock() mock_device_cls.__name__ = "Device" mock_device_cls.DoesNotExist = DoesNotExist mock_device_cls.objects.select_for_update.return_value.get.side_effect = DoesNotExist() with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_obj), patch("netbox_librenms_plugin.views.sync.device_fields.Device", mock_device_cls), patch("netbox_librenms_plugin.views.sync.device_fields.transaction"), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request({"object_type": "device"}), pk=1) mock_msg.error.assert_called_once() def test_cf_value_changed_to_json_after_lock(self): """Locked row shows cf_value already as dict → warning.""" view = self._view() mock_obj = MagicMock() mock_obj.custom_field_data = {"librenms_id": 42} mock_obj.serial = "SN-MATCH" view._librenms_api.get_device_info.return_value = (True, {"serial": "SN-MATCH"}) DoesNotExist = type("DoesNotExist", (Exception,), {}) mock_locked = MagicMock() mock_locked.custom_field_data = {"librenms_id": {"default": 42}} # already dict mock_device_cls = MagicMock() mock_device_cls.DoesNotExist = DoesNotExist mock_device_cls.objects.select_for_update.return_value.get.return_value = mock_locked with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_obj), patch("netbox_librenms_plugin.views.sync.device_fields.Device", mock_device_cls), patch("netbox_librenms_plugin.views.sync.device_fields.transaction"), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request({"object_type": "device"}), pk=1) mock_msg.warning.assert_called_once() def test_cf_value_not_int_after_lock(self): """Locked row shows non-digit string → error: cannot convert.""" view = self._view() mock_obj = MagicMock() mock_obj.custom_field_data = {"librenms_id": 42} mock_obj.serial = "SN-MATCH" view._librenms_api.get_device_info.return_value = (True, {"serial": "SN-MATCH"}) DoesNotExist = type("DoesNotExist", (Exception,), {}) mock_locked = MagicMock() mock_locked.custom_field_data = {"librenms_id": "not-a-digit"} mock_device_cls = MagicMock() mock_device_cls.DoesNotExist = DoesNotExist mock_device_cls.objects.select_for_update.return_value.get.return_value = mock_locked with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_obj), patch("netbox_librenms_plugin.views.sync.device_fields.Device", mock_device_cls), patch("netbox_librenms_plugin.views.sync.device_fields.transaction"), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request({"object_type": "device"}), pk=1) mock_msg.error.assert_called_once() def test_data_changed_before_lock(self): """locked_id or locked_serial differs → error: aborting.""" view = self._view() mock_obj = MagicMock() mock_obj.custom_field_data = {"librenms_id": 42} mock_obj.serial = "SN-MATCH" view._librenms_api.get_device_info.return_value = (True, {"serial": "SN-MATCH"}) DoesNotExist = type("DoesNotExist", (Exception,), {}) mock_locked = MagicMock() mock_locked.custom_field_data = {"librenms_id": 99} # different id mock_locked.serial = "SN-MATCH" mock_device_cls = MagicMock() mock_device_cls.DoesNotExist = DoesNotExist mock_device_cls.objects.select_for_update.return_value.get.return_value = mock_locked with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_obj), patch("netbox_librenms_plugin.views.sync.device_fields.Device", mock_device_cls), patch("netbox_librenms_plugin.views.sync.device_fields.transaction"), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request({"object_type": "device"}), pk=1) mock_msg.error.assert_called_once() def test_conflict_with_another_object(self): """Another object already has the same librenms_id for this server.""" view = self._view() mock_obj = MagicMock() mock_obj.custom_field_data = {"librenms_id": 42} mock_obj.serial = "SN-MATCH" view._librenms_api.get_device_info.return_value = (True, {"serial": "SN-MATCH"}) view._librenms_api.server_key = "default" DoesNotExist = type("DoesNotExist", (Exception,), {}) mock_locked = MagicMock() mock_locked.pk = 1 mock_locked.custom_field_data = {"librenms_id": 42} mock_locked.serial = "SN-MATCH" other_obj = MagicMock() other_obj.pk = 99 # different pk → conflict mock_device_cls = MagicMock() mock_device_cls.__name__ = "Device" mock_device_cls.DoesNotExist = DoesNotExist mock_device_cls.objects.select_for_update.return_value.get.return_value = mock_locked mock_txn = MagicMock() mock_txn.set_rollback = MagicMock() with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_obj), patch("netbox_librenms_plugin.views.sync.device_fields.Device", mock_device_cls), patch("netbox_librenms_plugin.views.sync.device_fields.find_by_librenms_id", return_value=other_obj), patch("netbox_librenms_plugin.views.sync.device_fields.transaction", mock_txn), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request({"object_type": "device"}), pk=1) mock_msg.error.assert_called_once() mock_txn.set_rollback.assert_called_once_with(True) def test_migrate_returns_false(self): """migrate_legacy_librenms_id returns False → warning.""" view = self._view() mock_obj = MagicMock() mock_obj.custom_field_data = {"librenms_id": 42} mock_obj.serial = "SN-MATCH" view._librenms_api.get_device_info.return_value = (True, {"serial": "SN-MATCH"}) view._librenms_api.server_key = "default" DoesNotExist = type("DoesNotExist", (Exception,), {}) mock_locked = MagicMock() mock_locked.pk = 1 mock_locked.custom_field_data = {"librenms_id": 42} mock_locked.serial = "SN-MATCH" mock_device_cls = MagicMock() mock_device_cls.DoesNotExist = DoesNotExist mock_device_cls.objects.select_for_update.return_value.get.return_value = mock_locked with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_obj), patch("netbox_librenms_plugin.views.sync.device_fields.Device", mock_device_cls), patch("netbox_librenms_plugin.views.sync.device_fields.find_by_librenms_id", return_value=None), patch("netbox_librenms_plugin.views.sync.device_fields.migrate_legacy_librenms_id", return_value=False), patch("netbox_librenms_plugin.views.sync.device_fields.transaction"), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request({"object_type": "device"}), pk=1) mock_msg.warning.assert_called_once() def test_validation_error_on_save(self): from django.core.exceptions import ValidationError view = self._view() mock_obj = MagicMock() mock_obj.custom_field_data = {"librenms_id": 42} mock_obj.serial = "SN-MATCH" view._librenms_api.get_device_info.return_value = (True, {"serial": "SN-MATCH"}) view._librenms_api.server_key = "default" DoesNotExist = type("DoesNotExist", (Exception,), {}) mock_locked = MagicMock() mock_locked.pk = 1 mock_locked.custom_field_data = {"librenms_id": 42} mock_locked.serial = "SN-MATCH" mock_locked.full_clean.side_effect = ValidationError({"librenms_id": ["err"]}) mock_device_cls = MagicMock() mock_device_cls.DoesNotExist = DoesNotExist mock_device_cls.objects.select_for_update.return_value.get.return_value = mock_locked mock_txn = MagicMock() mock_txn.set_rollback = MagicMock() with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_obj), patch("netbox_librenms_plugin.views.sync.device_fields.Device", mock_device_cls), patch("netbox_librenms_plugin.views.sync.device_fields.find_by_librenms_id", return_value=None), patch("netbox_librenms_plugin.views.sync.device_fields.migrate_legacy_librenms_id", return_value=True), patch("netbox_librenms_plugin.views.sync.device_fields.transaction", mock_txn), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request({"object_type": "device"}), pk=1) mock_msg.error.assert_called_once() mock_txn.set_rollback.assert_called_once_with(True) def test_unexpected_error_on_save(self): view = self._view() mock_obj = MagicMock() mock_obj.custom_field_data = {"librenms_id": 42} mock_obj.serial = "SN-MATCH" view._librenms_api.get_device_info.return_value = (True, {"serial": "SN-MATCH"}) view._librenms_api.server_key = "default" DoesNotExist = type("DoesNotExist", (Exception,), {}) mock_locked = MagicMock() mock_locked.pk = 1 mock_locked.custom_field_data = {"librenms_id": 42} mock_locked.serial = "SN-MATCH" mock_locked.full_clean.side_effect = RuntimeError("disk full") mock_device_cls = MagicMock() mock_device_cls.DoesNotExist = DoesNotExist mock_device_cls.objects.select_for_update.return_value.get.return_value = mock_locked mock_txn = MagicMock() mock_txn.set_rollback = MagicMock() with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_obj), patch("netbox_librenms_plugin.views.sync.device_fields.Device", mock_device_cls), patch("netbox_librenms_plugin.views.sync.device_fields.find_by_librenms_id", return_value=None), patch("netbox_librenms_plugin.views.sync.device_fields.migrate_legacy_librenms_id", return_value=True), patch("netbox_librenms_plugin.views.sync.device_fields.transaction", mock_txn), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request({"object_type": "device"}), pk=1) mock_msg.error.assert_called_once() mock_txn.set_rollback.assert_called_once_with(True) def test_success_integer_cf_value(self): """Happy path with integer cf_value → success message.""" view = self._view() mock_obj = MagicMock() mock_obj.custom_field_data = {"librenms_id": 42} mock_obj.serial = "SN-MATCH" view._librenms_api.get_device_info.return_value = (True, {"serial": "SN-MATCH"}) view._librenms_api.server_key = "default" DoesNotExist = type("DoesNotExist", (Exception,), {}) mock_locked = MagicMock() mock_locked.pk = 1 mock_locked.custom_field_data = {"librenms_id": 42} mock_locked.serial = "SN-MATCH" mock_device_cls = MagicMock() mock_device_cls.DoesNotExist = DoesNotExist mock_device_cls.objects.select_for_update.return_value.get.return_value = mock_locked with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_obj), patch("netbox_librenms_plugin.views.sync.device_fields.Device", mock_device_cls), patch("netbox_librenms_plugin.views.sync.device_fields.find_by_librenms_id", return_value=None), patch("netbox_librenms_plugin.views.sync.device_fields.migrate_legacy_librenms_id", return_value=True), patch("netbox_librenms_plugin.views.sync.device_fields.transaction"), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request({"object_type": "device"}), pk=1) mock_msg.success.assert_called_once() mock_locked.full_clean.assert_called_once() mock_locked.save.assert_called_once() assert "42" in mock_msg.success.call_args[0][1] def test_success_string_cf_value(self): """Happy path with string digit cf_value → success message.""" view = self._view() mock_obj = MagicMock() mock_obj.custom_field_data = {"librenms_id": "42"} mock_obj.serial = "SN-MATCH" view._librenms_api.get_device_info.return_value = (True, {"serial": "SN-MATCH"}) view._librenms_api.server_key = "default" DoesNotExist = type("DoesNotExist", (Exception,), {}) mock_locked = MagicMock() mock_locked.pk = 1 mock_locked.custom_field_data = {"librenms_id": "42"} mock_locked.serial = "SN-MATCH" mock_device_cls = MagicMock() mock_device_cls.DoesNotExist = DoesNotExist mock_device_cls.objects.select_for_update.return_value.get.return_value = mock_locked with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_obj), patch("netbox_librenms_plugin.views.sync.device_fields.Device", mock_device_cls), patch("netbox_librenms_plugin.views.sync.device_fields.find_by_librenms_id", return_value=None), patch("netbox_librenms_plugin.views.sync.device_fields.migrate_legacy_librenms_id", return_value=True), patch("netbox_librenms_plugin.views.sync.device_fields.transaction"), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request({"object_type": "device"}), pk=1) mock_msg.success.assert_called_once() mock_locked.full_clean.assert_called_once() mock_locked.save.assert_called_once() def test_conflict_same_object_is_not_conflict(self): """find_by_librenms_id returns the same object → no conflict, proceeds.""" view = self._view() mock_obj = MagicMock() mock_obj.custom_field_data = {"librenms_id": 42} mock_obj.serial = "SN-MATCH" view._librenms_api.get_device_info.return_value = (True, {"serial": "SN-MATCH"}) view._librenms_api.server_key = "default" DoesNotExist = type("DoesNotExist", (Exception,), {}) mock_locked = MagicMock() mock_locked.pk = 1 mock_locked.custom_field_data = {"librenms_id": 42} mock_locked.serial = "SN-MATCH" # find_by_librenms_id returns the SAME object → match.pk == locked.pk → no conflict same_obj = MagicMock() same_obj.pk = 1 mock_device_cls = MagicMock() mock_device_cls.DoesNotExist = DoesNotExist mock_device_cls.objects.select_for_update.return_value.get.return_value = mock_locked with ( patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404", return_value=mock_obj), patch("netbox_librenms_plugin.views.sync.device_fields.Device", mock_device_cls), patch("netbox_librenms_plugin.views.sync.device_fields.find_by_librenms_id", return_value=same_obj), patch("netbox_librenms_plugin.views.sync.device_fields.migrate_legacy_librenms_id", return_value=True), patch("netbox_librenms_plugin.views.sync.device_fields.transaction"), patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_msg, patch("netbox_librenms_plugin.views.sync.device_fields.redirect"), ): view.post(_make_request({"object_type": "device"}), pk=1) mock_msg.success.assert_called_once() mock_locked.full_clean.assert_called_once() mock_locked.save.assert_called_once() # --------------------------------------------------------------------------- # Wiring assertions — ensure views keep required mixins and permissions # --------------------------------------------------------------------------- class TestDeviceFieldsViewWiring: """Structural checks: views must retain required mixins and permissions.""" def test_convert_legacy_id_has_librenms_api_mixin(self): from netbox_librenms_plugin.views.mixins import LibreNMSAPIMixin from netbox_librenms_plugin.views.sync.device_fields import ConvertLegacyLibreNMSIdView assert issubclass(ConvertLegacyLibreNMSIdView, LibreNMSAPIMixin) def test_convert_legacy_id_has_required_object_permissions(self): from netbox_librenms_plugin.views.sync.device_fields import ConvertLegacyLibreNMSIdView assert "POST" in ConvertLegacyLibreNMSIdView.required_object_permissions def test_remove_server_mapping_has_required_object_permissions(self): from netbox_librenms_plugin.views.sync.device_fields import RemoveServerMappingView assert "POST" in RemoveServerMappingView.required_object_permissions