Files
netbox-librenms-plugin/netbox_librenms_plugin/tests/test_coverage_device_fields.py
Vlastislav Svatek 673e67106e
Some checks failed
ci / deploy (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
first commit
2026-06-05 10:39:05 +02:00

2040 lines
92 KiB
Python

"""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