315 lines
13 KiB
Python
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)
|