Files
netbox-librenms-plugin/netbox_librenms_plugin/tests/test_sync_devices.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

315 lines
13 KiB
Python

"""Tests for device sync views: AddDeviceToLibreNMSView and field update views."""
from unittest.mock import MagicMock, patch
def _make_view(cls_name, module_path="netbox_librenms_plugin.views.sync.devices"):
import importlib
mod = importlib.import_module(module_path)
cls = getattr(mod, cls_name)
view = object.__new__(cls)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
view.request = MagicMock()
return view
def _make_field_view(cls_name):
return _make_view(cls_name, "netbox_librenms_plugin.views.sync.device_fields")
class TestAddDeviceToLibreNMSViewWiring:
"""AddDeviceToLibreNMSView must be correctly wired to LibreNMSAPIMixin."""
def test_has_librenms_api_mixin(self):
from netbox_librenms_plugin.views.sync.devices import AddDeviceToLibreNMSView
from netbox_librenms_plugin.views.mixins import LibreNMSAPIMixin
assert LibreNMSAPIMixin in AddDeviceToLibreNMSView.__mro__
def test_has_permission_mixin(self):
from netbox_librenms_plugin.views.sync.devices import AddDeviceToLibreNMSView
from netbox_librenms_plugin.views.mixins import LibreNMSPermissionMixin
assert LibreNMSPermissionMixin in AddDeviceToLibreNMSView.__mro__
class TestAddDeviceToLibreNMSViewFormValid:
"""form_valid() builds correct device_data payload and calls librenms_api.add_device."""
def _make_view(self):
from netbox_librenms_plugin.views.sync.devices import AddDeviceToLibreNMSView
view = object.__new__(AddDeviceToLibreNMSView)
view._librenms_api = MagicMock()
view.request = MagicMock()
view.object = MagicMock()
view.object.get_absolute_url.return_value = "/dcim/devices/1/"
return view
def _make_form(self, data):
form = MagicMock()
form.cleaned_data = data
return form
def test_v2c_form_includes_community(self):
view = self._make_view()
view._librenms_api.add_device.return_value = (True, "Device added")
form = self._make_form(
{
"hostname": "switch1.example.com",
"community": "public",
"force_add": False,
}
)
with patch("netbox_librenms_plugin.views.sync.devices.redirect"):
with patch("netbox_librenms_plugin.views.sync.devices.messages"):
view.form_valid(form, snmp_version="v2c")
call_args = view._librenms_api.add_device.call_args[0][0]
assert call_args["snmp_version"] == "v2c"
assert call_args["community"] == "public"
assert call_args["hostname"] == "switch1.example.com"
def test_v3_form_includes_auth_fields(self):
view = self._make_view()
view._librenms_api.add_device.return_value = (True, "Device added")
form = self._make_form(
{
"hostname": "switch2.example.com",
"authlevel": "authPriv",
"authname": "admin",
"authpass": "secret",
"authalgo": "SHA",
"cryptopass": "crypt",
"cryptoalgo": "AES",
"force_add": False,
}
)
with patch("netbox_librenms_plugin.views.sync.devices.redirect"):
with patch("netbox_librenms_plugin.views.sync.devices.messages"):
view.form_valid(form, snmp_version="v3")
call_args = view._librenms_api.add_device.call_args[0][0]
assert call_args["snmp_version"] == "v3"
assert call_args["authlevel"] == "authPriv"
assert "community" not in call_args
def test_api_failure_adds_error_message(self):
view = self._make_view()
view._librenms_api.add_device.return_value = (False, "Connection refused")
form = self._make_form(
{
"hostname": "fail.example.com",
"community": "public",
"force_add": False,
}
)
with patch("netbox_librenms_plugin.views.sync.devices.redirect"):
with patch("netbox_librenms_plugin.views.sync.devices.messages") as mock_msg:
view.form_valid(form, snmp_version="v2c")
mock_msg.error.assert_called_once()
class TestUpdateDeviceLocationView:
"""UpdateDeviceLocationView.post calls update_device_field with site name."""
def test_calls_update_device_field_with_site(self):
"""
post() resolves the NetBox site name and passes the exact API payload
expected by LibreNMS's PATCH /api/v0/devices/{id}/field endpoint:
``{"field": ["location", "override_sysLocation"], "data": [name, "1"]}``.
"""
from netbox_librenms_plugin.views.sync.devices import UpdateDeviceLocationView
view = object.__new__(UpdateDeviceLocationView)
view._librenms_api = MagicMock()
view._librenms_api.get_librenms_id.return_value = 42
view._librenms_api.update_device_field.return_value = (True, "ok")
view.request = MagicMock()
device = MagicMock()
device.site = MagicMock()
device.site.name = "London"
device.get_absolute_url.return_value = "/dcim/devices/1/"
with patch("netbox_librenms_plugin.views.sync.devices.get_object_or_404", return_value=device):
with patch("netbox_librenms_plugin.views.sync.devices.redirect"):
with patch("netbox_librenms_plugin.views.sync.devices.messages") as mock_msg:
view.post(view.request, pk=1)
view._librenms_api.update_device_field.assert_called_once_with(
42,
{"field": ["location", "override_sysLocation"], "data": ["London", "1"]},
)
mock_msg.success.assert_called_once()
def test_warning_when_no_site(self):
from netbox_librenms_plugin.views.sync.devices import UpdateDeviceLocationView
view = object.__new__(UpdateDeviceLocationView)
view._librenms_api = MagicMock()
view._librenms_api.get_librenms_id.return_value = 42
view.request = MagicMock()
device = MagicMock()
device.site = None
device.pk = 1
with patch("netbox_librenms_plugin.views.sync.devices.get_object_or_404", return_value=device):
with patch("netbox_librenms_plugin.views.sync.devices.redirect"):
with patch("netbox_librenms_plugin.views.sync.devices.messages") as mock_msg:
view.post(view.request, pk=1)
view._librenms_api.update_device_field.assert_not_called()
mock_msg.warning.assert_called_once()
class TestUpdateDeviceNameViewWiring:
def test_has_all_required_mixins(self):
from netbox_librenms_plugin.views.sync.device_fields import UpdateDeviceNameView
from netbox_librenms_plugin.views.mixins import (
LibreNMSAPIMixin,
LibreNMSPermissionMixin,
NetBoxObjectPermissionMixin,
)
mro = UpdateDeviceNameView.__mro__
assert LibreNMSAPIMixin in mro
assert LibreNMSPermissionMixin in mro
assert NetBoxObjectPermissionMixin in mro
def test_requires_change_device_permission(self):
from netbox_librenms_plugin.views.sync.device_fields import UpdateDeviceNameView
from dcim.models import Device
perms = UpdateDeviceNameView.required_object_permissions
assert "POST" in perms
assert any(action == "change" and model == Device for action, model in perms["POST"])
class TestUpdateDeviceSerialViewWiring:
def test_has_all_required_mixins(self):
from netbox_librenms_plugin.views.sync.device_fields import UpdateDeviceSerialView
from netbox_librenms_plugin.views.mixins import (
LibreNMSAPIMixin,
LibreNMSPermissionMixin,
NetBoxObjectPermissionMixin,
)
assert LibreNMSAPIMixin in UpdateDeviceSerialView.__mro__
assert LibreNMSPermissionMixin in UpdateDeviceSerialView.__mro__
assert NetBoxObjectPermissionMixin in UpdateDeviceSerialView.__mro__
def test_requires_change_device_permission(self):
from netbox_librenms_plugin.views.sync.device_fields import UpdateDeviceSerialView
from dcim.models import Device
perms = UpdateDeviceSerialView.required_object_permissions
assert "POST" in perms
assert any(action == "change" and model == Device for action, model in perms["POST"])
class TestCreatePlatformFullClean:
"""CreateAndAssignPlatformView must call full_clean() so ValidationError is catchable."""
def test_validation_error_caught_on_slug_collision(self):
"""When full_clean raises ValidationError, user sees error message instead of 500."""
from django.core.exceptions import ValidationError
from netbox_librenms_plugin.views.sync.device_fields import CreateAndAssignPlatformView
view = object.__new__(CreateAndAssignPlatformView)
request = MagicMock()
request.method = "POST"
request.POST = {"platform_name": "test-platform"}
request.user.has_perm.return_value = True
view.request = request
with (
patch("netbox_librenms_plugin.views.sync.device_fields.get_object_or_404"),
patch("netbox_librenms_plugin.views.sync.device_fields.Manufacturer"),
patch("netbox_librenms_plugin.views.sync.device_fields.Platform") as MockPlatform,
patch("netbox_librenms_plugin.views.sync.device_fields.transaction"),
patch("netbox_librenms_plugin.views.sync.device_fields.messages") as mock_messages,
patch("netbox_librenms_plugin.views.sync.device_fields.redirect"),
):
MockPlatform.objects.filter.return_value.exists.return_value = False
platform_instance = MagicMock()
platform_instance.full_clean.side_effect = ValidationError({"slug": ["Slug already exists"]})
MockPlatform.return_value = platform_instance
view.post(request, pk=1)
platform_instance.full_clean.assert_called_once()
platform_instance.save.assert_not_called()
mock_messages.error.assert_called_once()
error_msg = mock_messages.error.call_args[0][1]
assert "could not be created" in error_msg
assert "Slug already exists" in error_msg
class TestRemoveServerMappingViewWiring:
def test_does_not_have_librenms_api_mixin(self):
"""RemoveServerMappingView does not call LibreNMS API — it only modifies NetBox."""
from netbox_librenms_plugin.views.sync.device_fields import RemoveServerMappingView
from netbox_librenms_plugin.views.mixins import LibreNMSAPIMixin
assert LibreNMSAPIMixin not in RemoveServerMappingView.__mro__
def test_has_permission_mixin(self):
from netbox_librenms_plugin.views.sync.device_fields import RemoveServerMappingView
from netbox_librenms_plugin.views.mixins import LibreNMSPermissionMixin, NetBoxObjectPermissionMixin
assert LibreNMSPermissionMixin in RemoveServerMappingView.__mro__
assert NetBoxObjectPermissionMixin in RemoveServerMappingView.__mro__
def test_post_with_virtualmachine_sets_vm_permissions_and_redirects(self):
"""post() with object_type='virtualmachine' sets VirtualMachine permissions and redirects to VM URL."""
from netbox_librenms_plugin.views.sync.device_fields import RemoveServerMappingView
from virtualization.models import VirtualMachine
view = object.__new__(RemoveServerMappingView)
permissions_at_check = {}
def capture_perms(method):
permissions_at_check[method] = list(view.required_object_permissions.get(method, []))
return None # permission passes
mock_vm = MagicMock()
mock_vm.pk = 10
mock_vm.custom_field_data = {"librenms_id": {"orphaned-server": 42}}
# Use a mock model class so the select_for_update().get() call doesn't hit the DB
mock_model = MagicMock()
mock_model.objects.select_for_update.return_value.get.return_value = mock_vm
request = MagicMock()
request.POST = {"object_type": "virtualmachine", "server_key": "orphaned-server"}
with (
patch.object(view, "require_all_permissions", side_effect=capture_perms),
patch.object(view, "_get_object", return_value=(mock_vm, mock_model)),
patch("netbox_librenms_plugin.views.sync.device_fields.messages"),
patch("netbox_librenms_plugin.views.sync.device_fields.redirect") as mock_redirect,
patch("netbox_librenms_plugin.views.sync.device_fields.transaction"),
patch(
"django.conf.settings",
PLUGINS_CONFIG={"netbox_librenms_plugin": {}},
),
):
view.post(request, pk=10)
# required_object_permissions must be scoped to VirtualMachine, not Device
assert ("change", VirtualMachine) in permissions_at_check.get("POST", [])
# Response must redirect to the VM-specific sync URL
mock_redirect.assert_called_with("plugins:netbox_librenms_plugin:vm_librenms_sync", pk=10)