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