""" Coverage tests for views/sync/interfaces.py SyncInterfacesView + DeleteNetBoxInterfacesView Target: 95%+ coverage """ from unittest.mock import MagicMock, patch # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _make_request(post_data=None, get_data=None): """Build a mock request with proper POST / GET dicts.""" req = MagicMock() _post = post_data or {} post_mock = MagicMock() post_mock.get = lambda k, d=None: _post.get(k, d) post_mock.getlist = lambda k: _post[k] if isinstance(_post.get(k), list) else ([] if k not in _post else [_post[k]]) req.POST = post_mock req.GET = get_data or {} return req def _denied_response(): resp = MagicMock() resp.status_code = 403 return resp # =========================================================================== # SyncInterfacesView.get_required_permissions_for_object_type # =========================================================================== class TestSyncInterfacesViewPermissions: def test_device_type_returns_interface_perms(self): from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView from dcim.models import Interface view = object.__new__(SyncInterfacesView) perms = view.get_required_permissions_for_object_type("device") assert ("change", Interface) in perms def test_vm_type_returns_vminterface_perms(self): from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView from virtualization.models import VMInterface view = object.__new__(SyncInterfacesView) perms = view.get_required_permissions_for_object_type("virtualmachine") assert ("change", VMInterface) in perms def test_invalid_type_raises_http404(self): from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView from django.http import Http404 import pytest view = object.__new__(SyncInterfacesView) with pytest.raises(Http404): view.get_required_permissions_for_object_type("invalid") # =========================================================================== # SyncInterfacesView.get_object # =========================================================================== class TestSyncInterfacesViewGetObject: def test_get_device(self): from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView view = object.__new__(SyncInterfacesView) mock_device = MagicMock() with patch("netbox_librenms_plugin.views.sync.interfaces.get_object_or_404", return_value=mock_device): result = view.get_object("device", 1) assert result is mock_device def test_get_vm(self): from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView view = object.__new__(SyncInterfacesView) mock_vm = MagicMock() with patch("netbox_librenms_plugin.views.sync.interfaces.get_object_or_404", return_value=mock_vm): result = view.get_object("virtualmachine", 2) assert result is mock_vm def test_invalid_type_raises_http404(self): from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView from django.http import Http404 import pytest view = object.__new__(SyncInterfacesView) with pytest.raises(Http404): view.get_object("invalid", 1) # =========================================================================== # SyncInterfacesView.get_selected_interfaces # =========================================================================== class TestSyncInterfacesViewGetSelectedInterfaces: def test_empty_selection_returns_none(self): from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView view = object.__new__(SyncInterfacesView) req = _make_request(post_data={}) with patch("netbox_librenms_plugin.views.sync.interfaces.messages") as mock_msgs: result = view.get_selected_interfaces(req, "ifName") assert result is None mock_msgs.error.assert_called_once() def test_with_selection_returns_list(self): from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView view = object.__new__(SyncInterfacesView) req = _make_request(post_data={"select": ["Gi0/1", "Gi0/2"]}) result = view.get_selected_interfaces(req, "ifName") assert result == ["Gi0/1", "Gi0/2"] # =========================================================================== # SyncInterfacesView.get_cached_ports_data # =========================================================================== class TestSyncInterfacesViewGetCachedPortsData: def test_cache_miss_warns_and_returns_none(self): from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView view = object.__new__(SyncInterfacesView) view.get_cache_key = MagicMock(return_value="k") req = _make_request() mock_obj = MagicMock(pk=1) with ( patch("netbox_librenms_plugin.views.sync.interfaces.cache") as mock_cache, patch("netbox_librenms_plugin.views.sync.interfaces.messages") as mock_msgs, ): mock_cache.get.return_value = None result = view.get_cached_ports_data(req, mock_obj, "default") assert result is None mock_msgs.warning.assert_called_once() def test_cache_hit_returns_ports(self): from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView view = object.__new__(SyncInterfacesView) view.get_cache_key = MagicMock(return_value="k") req = _make_request() mock_obj = MagicMock(pk=1) ports = [{"ifName": "Gi0/1"}] with patch("netbox_librenms_plugin.views.sync.interfaces.cache") as mock_cache: mock_cache.get.return_value = {"ports": ports} result = view.get_cached_ports_data(req, mock_obj, "default") assert result == ports def test_no_server_key_uses_librenms_api(self): from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView view = object.__new__(SyncInterfacesView) view.get_cache_key = MagicMock(return_value="k") mock_api = MagicMock(server_key="mykey") req = _make_request() mock_obj = MagicMock(pk=1) with ( patch("netbox_librenms_plugin.views.sync.interfaces.cache") as mock_cache, patch("netbox_librenms_plugin.views.sync.interfaces.messages"), patch.object(type(view), "librenms_api", new_callable=lambda: property(lambda s: mock_api)), ): mock_cache.get.return_value = None view.get_cached_ports_data(req, mock_obj, None) # get_cache_key should have been called with (obj, "ports", resolved_server_key) view.get_cache_key.assert_called_once_with(mock_obj, "ports", "mykey") # =========================================================================== # SyncInterfacesView.post — full flows # =========================================================================== class TestSyncInterfacesViewPost: def test_permission_denied_returns_early(self): from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView view = object.__new__(SyncInterfacesView) view.require_all_permissions = MagicMock(return_value=_denied_response()) view.get_required_permissions_for_object_type = MagicMock(return_value=[]) req = _make_request(post_data={"select": ["Gi0/1"]}) result = view.post(req, "device", 1) assert result.status_code == 403 def test_no_selection_redirects(self): from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView view = object.__new__(SyncInterfacesView) view.require_all_permissions = MagicMock(return_value=None) view.get_required_permissions_for_object_type = MagicMock(return_value=[]) mock_api = MagicMock(server_key="default") mock_device = MagicMock(pk=1) req = _make_request(post_data={}) # No selection with ( patch("netbox_librenms_plugin.views.sync.interfaces.get_object_or_404", return_value=mock_device), patch("netbox_librenms_plugin.views.sync.interfaces.get_interface_name_field", return_value="ifName"), patch("netbox_librenms_plugin.views.sync.interfaces.messages") as mock_msgs, patch("netbox_librenms_plugin.views.sync.interfaces.redirect") as mock_redirect, patch("netbox_librenms_plugin.views.sync.interfaces.reverse", return_value="/sync/"), patch.object(type(view), "librenms_api", new_callable=lambda: property(lambda s: mock_api)), ): view.post(req, "device", 1) mock_msgs.error.assert_called_once() mock_redirect.assert_called_once() def test_cache_miss_redirects(self): from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView view = object.__new__(SyncInterfacesView) view.require_all_permissions = MagicMock(return_value=None) view.get_required_permissions_for_object_type = MagicMock(return_value=[]) mock_api = MagicMock(server_key="default") mock_device = MagicMock(pk=1) req = _make_request(post_data={"select": ["Gi0/1"]}) with ( patch("netbox_librenms_plugin.views.sync.interfaces.get_object_or_404", return_value=mock_device), patch("netbox_librenms_plugin.views.sync.interfaces.get_interface_name_field", return_value="ifName"), patch("netbox_librenms_plugin.views.sync.interfaces.cache") as mock_cache, patch("netbox_librenms_plugin.views.sync.interfaces.messages"), patch("netbox_librenms_plugin.views.sync.interfaces.redirect") as mock_redirect, patch("netbox_librenms_plugin.views.sync.interfaces.reverse", return_value="/sync/"), patch.object(type(view), "get_vlan_groups_for_device", return_value=[]), patch.object(view.__class__, "get_cache_key", return_value="k"), patch.object(type(view), "librenms_api", new_callable=lambda: property(lambda s: mock_api)), ): mock_cache.get.return_value = None view.post(req, "device", 1) mock_redirect.assert_called() def test_device_post_success(self): from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView view = object.__new__(SyncInterfacesView) view.require_all_permissions = MagicMock(return_value=None) view.get_required_permissions_for_object_type = MagicMock(return_value=[]) mock_api = MagicMock(server_key="default") mock_device = MagicMock(pk=1) ports = [{"ifName": "Gi0/1", "port_id": 10}] req = _make_request(post_data={"select": ["Gi0/1"], "server_key": "default"}) with ( patch("netbox_librenms_plugin.views.sync.interfaces.get_object_or_404", return_value=mock_device), patch("netbox_librenms_plugin.views.sync.interfaces.get_interface_name_field", return_value="ifName"), patch("netbox_librenms_plugin.views.sync.interfaces.cache") as mock_cache, patch("netbox_librenms_plugin.views.sync.interfaces.messages") as mock_msgs, patch("netbox_librenms_plugin.views.sync.interfaces.redirect") as mock_redirect, patch("netbox_librenms_plugin.views.sync.interfaces.reverse", return_value="/sync/"), patch("netbox_librenms_plugin.views.sync.interfaces.transaction"), patch.object(view, "sync_interface"), patch.object(type(view), "get_vlan_groups_for_device", return_value=[]), patch.object(view.__class__, "get_cache_key", return_value="k"), patch.object(view.__class__, "_build_vlan_lookup_maps", return_value={}), patch.object(type(view), "librenms_api", new_callable=lambda: property(lambda s: mock_api)), ): mock_cache.get.return_value = {"ports": ports} view.post(req, "device", 1) mock_msgs.success.assert_called_once() mock_redirect.assert_called_once() def test_vm_post_success(self): from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView view = object.__new__(SyncInterfacesView) view.require_all_permissions = MagicMock(return_value=None) view.get_required_permissions_for_object_type = MagicMock(return_value=[]) mock_api = MagicMock(server_key="default") mock_vm = MagicMock(pk=5) ports = [{"ifName": "eth0", "port_id": 20}] req = _make_request(post_data={"select": ["eth0"], "server_key": "default"}) with ( patch("netbox_librenms_plugin.views.sync.interfaces.get_object_or_404", return_value=mock_vm), patch("netbox_librenms_plugin.views.sync.interfaces.get_interface_name_field", return_value="ifName"), patch("netbox_librenms_plugin.views.sync.interfaces.cache") as mock_cache, patch("netbox_librenms_plugin.views.sync.interfaces.messages") as mock_msgs, patch("netbox_librenms_plugin.views.sync.interfaces.redirect") as mock_redirect, patch("netbox_librenms_plugin.views.sync.interfaces.reverse", return_value="/sync/"), patch("netbox_librenms_plugin.views.sync.interfaces.transaction"), patch.object(view, "sync_interface"), patch.object(type(view), "get_vlan_groups_for_device", return_value=[]), patch.object(view.__class__, "get_cache_key", return_value="k"), patch.object(view.__class__, "_build_vlan_lookup_maps", return_value={}), patch.object(type(view), "librenms_api", new_callable=lambda: property(lambda s: mock_api)), ): mock_cache.get.return_value = {"ports": ports} view.post(req, "virtualmachine", 5) mock_msgs.success.assert_called_once() mock_redirect.assert_called_once() # =========================================================================== # SyncInterfacesView.sync_interface — Device paths # =========================================================================== class TestSyncInterfacesViewSyncInterfaceDevice: def _make_view(self): from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView view = object.__new__(SyncInterfacesView) view.request = _make_request() view._post_server_key = "default" view._lookup_maps = {} view.interface_name_field = "ifName" view.update_interface_attributes = MagicMock() view._sync_interface_vlans = MagicMock() return view def test_device_interface_created(self): from dcim.models import Device view = self._make_view() # __class__ = Device makes isinstance(mock_device, Device) → True mock_device = MagicMock() mock_device.__class__ = Device mock_device.virtual_chassis = None mock_interface = MagicMock() librenms_port = {"ifName": "Gi0/1", "port_id": None} with patch("netbox_librenms_plugin.views.sync.interfaces.Interface") as mock_intf_cls: mock_intf_cls.objects.get_or_create.return_value = (mock_interface, True) view.get_netbox_interface_type = MagicMock(return_value="1000base-t") view.sync_interface(mock_device, librenms_port, [], "ifName") mock_intf_cls.objects.get_or_create.assert_called_once() view.update_interface_attributes.assert_called_once() def test_device_selection_with_vc_valid(self): from dcim.models import Device view = self._make_view() view.request = _make_request(post_data={"device_selection_Gi0/1": "2"}) mock_device = MagicMock() mock_device.__class__ = Device mock_device.id = 1 mock_vc = MagicMock() mock_vc.members.values_list.return_value = [1, 2] mock_device.virtual_chassis = mock_vc mock_target_device = MagicMock() mock_target_device.__class__ = Device mock_target_device.id = 2 mock_interface = MagicMock() librenms_port = {"ifName": "Gi0/1", "port_id": None} with ( patch("netbox_librenms_plugin.views.sync.interfaces.Interface") as mock_intf_cls, patch.object(Device, "objects") as mock_device_objects, ): mock_device_objects.get.return_value = mock_target_device mock_intf_cls.objects.get_or_create.return_value = (mock_interface, True) view.get_netbox_interface_type = MagicMock(return_value="other") view.sync_interface(mock_device, librenms_port, [], "ifName") mock_intf_cls.objects.get_or_create.assert_called_once_with(device=mock_target_device, name="Gi0/1") def test_device_selection_invalid_defaults_to_obj(self): from dcim.models import Device view = self._make_view() view.request = _make_request(post_data={"device_selection_Gi0/1": "99"}) mock_device = MagicMock() mock_device.__class__ = Device mock_device.id = 1 mock_device.virtual_chassis = None mock_other_device = MagicMock() mock_other_device.__class__ = Device mock_other_device.id = 99 # Different id, no VC → falls back to obj mock_interface = MagicMock() librenms_port = {"ifName": "Gi0/1", "port_id": None} with ( patch("netbox_librenms_plugin.views.sync.interfaces.Interface") as mock_intf_cls, patch.object(Device, "objects") as mock_device_objects, ): mock_device_objects.get.return_value = mock_other_device mock_intf_cls.objects.get_or_create.return_value = (mock_interface, True) view.get_netbox_interface_type = MagicMock(return_value="other") view.sync_interface(mock_device, librenms_port, [], "ifName") # Should use mock_device (obj), not mock_other_device call_kwargs = mock_intf_cls.objects.get_or_create.call_args[1] assert call_kwargs["device"] is mock_device def test_device_selection_does_not_exist_defaults_to_obj(self): from dcim.models import Device view = self._make_view() view.request = _make_request(post_data={"device_selection_Gi0/1": "999"}) mock_device = MagicMock() mock_device.__class__ = Device mock_device.id = 1 mock_device.virtual_chassis = None mock_interface = MagicMock() librenms_port = {"ifName": "Gi0/1", "port_id": None} with ( patch("netbox_librenms_plugin.views.sync.interfaces.Interface") as mock_intf_cls, patch.object(Device, "objects") as mock_device_objects, ): mock_device_objects.get.side_effect = Device.DoesNotExist() mock_intf_cls.objects.get_or_create.return_value = (mock_interface, True) view.get_netbox_interface_type = MagicMock(return_value="other") view.sync_interface(mock_device, librenms_port, [], "ifName") call_kwargs = mock_intf_cls.objects.get_or_create.call_args[1] assert call_kwargs["device"] is mock_device class TestSyncInterfacesViewSyncInterfaceVM: def test_vm_interface_created(self): from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView from virtualization.models import VirtualMachine view = object.__new__(SyncInterfacesView) view.request = _make_request() view._post_server_key = "default" view._lookup_maps = {} view.interface_name_field = "ifName" view.update_interface_attributes = MagicMock() view._sync_interface_vlans = MagicMock() mock_vm = MagicMock() mock_vm.__class__ = VirtualMachine mock_vm_interface = MagicMock() librenms_port = {"ifName": "eth0", "port_id": None} with patch("netbox_librenms_plugin.views.sync.interfaces.VMInterface") as mock_vmintf_cls: mock_vmintf_cls.objects.get_or_create.return_value = (mock_vm_interface, True) view.sync_interface(mock_vm, librenms_port, [], "ifName") mock_vmintf_cls.objects.get_or_create.assert_called_once() view.update_interface_attributes.assert_called_once() def test_invalid_obj_raises_value_error(self): from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView import pytest view = object.__new__(SyncInterfacesView) view.request = _make_request() librenms_port = {"ifName": "eth0"} with pytest.raises(ValueError): view.sync_interface(MagicMock(), librenms_port, [], "ifName") # =========================================================================== # SyncInterfacesView.get_netbox_interface_type # =========================================================================== class TestSyncInterfacesViewGetNetboxInterfaceType: def test_with_speed_uses_speed_mapping(self): from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView view = object.__new__(SyncInterfacesView) mock_mapping = MagicMock() mock_mapping.netbox_type = "1000base-t" with ( patch("netbox_librenms_plugin.views.sync.interfaces.convert_speed_to_kbps", return_value=1000), patch("netbox_librenms_plugin.views.sync.interfaces.InterfaceTypeMapping") as mock_cls, ): # mappings = objects.filter(...); speed_mapping = mappings.filter(...).order_by(...).first() mock_cls.objects.filter.return_value.filter.return_value.order_by.return_value.first.return_value = ( mock_mapping ) result = view.get_netbox_interface_type({"ifType": "ethernetCsmacd", "ifSpeed": 1000000000}) assert result == "1000base-t" def test_no_speed_uses_null_mapping(self): from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView view = object.__new__(SyncInterfacesView) mock_mapping = MagicMock() mock_mapping.netbox_type = "virtual" with ( patch("netbox_librenms_plugin.views.sync.interfaces.convert_speed_to_kbps", return_value=None), patch("netbox_librenms_plugin.views.sync.interfaces.InterfaceTypeMapping") as mock_cls, ): mock_qs = MagicMock() mock_qs.filter.return_value.first.return_value = mock_mapping mock_cls.objects.filter.return_value = mock_qs result = view.get_netbox_interface_type({"ifType": "softwareLoopback", "ifSpeed": None}) assert result == "virtual" def test_no_mapping_returns_other(self): from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView view = object.__new__(SyncInterfacesView) with ( patch("netbox_librenms_plugin.views.sync.interfaces.convert_speed_to_kbps", return_value=None), patch("netbox_librenms_plugin.views.sync.interfaces.InterfaceTypeMapping") as mock_cls, ): mock_qs = MagicMock() mock_qs.filter.return_value.first.return_value = None mock_cls.objects.filter.return_value = mock_qs result = view.get_netbox_interface_type({"ifType": "unknown", "ifSpeed": None}) assert result == "other" def test_speed_mapping_falls_back_to_null(self): """When speed mapping returns None, falls back to null-speed mapping.""" from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView view = object.__new__(SyncInterfacesView) mock_null_mapping = MagicMock() mock_null_mapping.netbox_type = "other" with ( patch("netbox_librenms_plugin.views.sync.interfaces.convert_speed_to_kbps", return_value=1000), patch("netbox_librenms_plugin.views.sync.interfaces.InterfaceTypeMapping") as mock_cls, ): mock_speed_qs = MagicMock() mock_speed_qs.order_by.return_value.first.return_value = None # No speed match mock_null_qs = MagicMock() mock_null_qs.first.return_value = mock_null_mapping mock_qs = MagicMock() mock_qs.filter.side_effect = [mock_speed_qs, mock_null_qs] mock_cls.objects.filter.return_value = mock_qs result = view.get_netbox_interface_type({"ifType": "ethernetCsmacd", "ifSpeed": 1000}) assert result == "other" # =========================================================================== # SyncInterfacesView.handle_mac_address # =========================================================================== class TestSyncInterfacesViewHandleMacAddress: def test_no_mac_address_does_nothing(self): from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView view = object.__new__(SyncInterfacesView) interface = MagicMock() view.handle_mac_address(interface, None) interface.mac_addresses.add.assert_not_called() def test_new_mac_created_and_added(self): from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView view = object.__new__(SyncInterfacesView) interface = MagicMock() interface.mac_addresses.filter.return_value.first.return_value = None mock_mac = MagicMock() with patch("netbox_librenms_plugin.views.sync.interfaces.MACAddress") as mock_mac_cls: mock_mac_cls.objects.create.return_value = mock_mac view.handle_mac_address(interface, "aa:bb:cc:dd:ee:ff") interface.mac_addresses.add.assert_called_once_with(mock_mac) def test_existing_mac_added_without_create(self): from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView view = object.__new__(SyncInterfacesView) existing_mac = MagicMock() interface = MagicMock() interface.mac_addresses.filter.return_value.first.return_value = existing_mac with patch("netbox_librenms_plugin.views.sync.interfaces.MACAddress") as mock_mac_cls: view.handle_mac_address(interface, "aa:bb:cc:dd:ee:ff") mock_mac_cls.objects.create.assert_not_called() interface.mac_addresses.add.assert_called_once_with(existing_mac) def test_primary_mac_assigned_if_attribute_exists(self): from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView view = object.__new__(SyncInterfacesView) # Use a stub with explicit attributes to enforce the guard on primary_mac_address class InterfaceStub: primary_mac_address = None def __init__(self): self.mac_addresses = MagicMock() self.mac_addresses.filter.return_value.first.return_value = None interface = InterfaceStub() mock_mac = MagicMock() with patch("netbox_librenms_plugin.views.sync.interfaces.MACAddress") as mock_mac_cls: mock_mac_cls.objects.create.return_value = mock_mac view.handle_mac_address(interface, "aa:bb:cc:dd:ee:ff") assert interface.primary_mac_address == mock_mac # =========================================================================== # SyncInterfacesView.update_interface_attributes # =========================================================================== class TestSyncInterfacesViewUpdateInterfaceAttributes: def _make_view(self): from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView view = object.__new__(SyncInterfacesView) view.request = _make_request() view._post_server_key = "default" view.handle_mac_address = MagicMock() return view def test_basic_attributes_set(self): from dcim.models import Interface view = self._make_view() interface = MagicMock() interface.__class__ = Interface librenms_port = { "ifName": "Gi0/1", "ifType": "ethernetCsmacd", "ifSpeed": 1000000000, "ifAlias": "uplink", "ifMtu": 1500, "port_id": None, "ifAdminStatus": "up", } with patch("netbox_librenms_plugin.views.sync.interfaces.convert_speed_to_kbps", return_value=1000000): view.update_interface_attributes(interface, librenms_port, "1000base-t", [], "ifName") interface.save.assert_called_once() assert interface.name == "Gi0/1" assert interface.type == "1000base-t" assert interface.speed == 1000000 assert interface.description == "uplink" assert interface.mtu == 1500 assert interface.enabled is True def test_excluded_columns_skipped(self): from dcim.models import Interface view = self._make_view() # MagicMock(spec=Interface) passes isinstance check; explicit attr init makes changes detectable interface = MagicMock(spec=Interface) interface.name = None interface.type = None interface.speed = None interface.description = None interface.mtu = None interface.enabled = None interface.mac_address = None interface.save = MagicMock() librenms_port = { "ifName": "Gi0/1", "ifType": "ethernetCsmacd", "ifSpeed": 0, "ifAlias": None, "ifMtu": None, "port_id": None, "ifAdminStatus": None, } with patch("netbox_librenms_plugin.views.sync.interfaces.convert_speed_to_kbps", return_value=0): view.handle_mac_address = MagicMock() view.update_interface_attributes( interface, librenms_port, "other", ["name", "type", "speed", "description", "mtu", "enabled", "mac_address"], "ifName", ) # save still called interface.save.assert_called_once() # MAC handler must not be invoked when "mac_address" is in excluded_columns view.handle_mac_address.assert_not_called() # All other excluded attributes remain at their initial None assert interface.name is None assert interface.type is None assert interface.speed is None assert interface.description is None assert interface.mtu is None assert interface.enabled is None def test_admin_status_down_sets_disabled(self): from dcim.models import Interface view = self._make_view() interface = MagicMock() interface.__class__ = Interface librenms_port = { "ifName": "Gi0/1", "ifType": None, "ifSpeed": None, "ifAlias": None, "ifMtu": None, "port_id": None, "ifAdminStatus": "down", } with patch("netbox_librenms_plugin.views.sync.interfaces.convert_speed_to_kbps", return_value=None): view.update_interface_attributes(interface, librenms_port, None, [], "ifName") assert interface.enabled is False def test_port_id_calls_set_librenms_device_id(self): from dcim.models import Interface view = self._make_view() interface = MagicMock() interface.__class__ = Interface librenms_port = { "ifName": "Gi0/1", "ifType": None, "ifSpeed": None, "ifAlias": None, "ifMtu": None, "port_id": 42, "ifAdminStatus": "up", } with ( patch("netbox_librenms_plugin.views.sync.interfaces.convert_speed_to_kbps", return_value=None), patch("netbox_librenms_plugin.views.sync.interfaces.set_librenms_device_id") as mock_set, ): view.update_interface_attributes(interface, librenms_port, None, [], "ifName") mock_set.assert_called_once_with(interface, 42, "default") def test_ifalias_not_set_when_same_as_name(self): """ifAlias should not overwrite when equal to interface name.""" from dcim.models import Interface view = self._make_view() # MagicMock(spec=Interface) with explicit init so plain assignments are detectable interface = MagicMock(spec=Interface) interface.description = None interface.save = MagicMock() librenms_port = { "ifName": "Gi0/1", "ifType": None, "ifSpeed": None, "ifAlias": "Gi0/1", # Same as interface name → should not set description "ifMtu": None, "port_id": None, "ifAdminStatus": "up", } with patch("netbox_librenms_plugin.views.sync.interfaces.convert_speed_to_kbps", return_value=None): view.update_interface_attributes(interface, librenms_port, None, [], "ifName") # description should remain None since ifAlias == interface_name assert interface.description is None # =========================================================================== # SyncInterfacesView._sync_interface_vlans # =========================================================================== class TestSyncInterfacesViewSyncInterfaceVlans: def test_no_vlans_calls_update_with_empty(self): from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView view = object.__new__(SyncInterfacesView) view.request = _make_request() view._lookup_maps = {} view._update_interface_vlan_assignment = MagicMock() interface = MagicMock() librenms_port = {} view._sync_interface_vlans(interface, librenms_port, "Gi0/1") view._update_interface_vlan_assignment.assert_called_once() def test_with_vlans_builds_group_map(self): from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView view = object.__new__(SyncInterfacesView) view.request = _make_request(post_data={"vlan_group_Gi0_1_100": "5"}) view._lookup_maps = {} view._update_interface_vlan_assignment = MagicMock() interface = MagicMock() librenms_port = {"untagged_vlan": 100, "tagged_vlans": [200]} view._sync_interface_vlans(interface, librenms_port, "Gi0/1") call_args = view._update_interface_vlan_assignment.call_args vlan_group_map = call_args[0][2] assert vlan_group_map.get("100") == "5" # =========================================================================== # SyncInterfacesView.sync_selected_interfaces # =========================================================================== class TestSyncInterfacesViewSyncSelected: def test_syncs_matching_ports(self): from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView view = object.__new__(SyncInterfacesView) view.interface_name_field = "ifName" view.sync_interface = MagicMock() ports_data = [{"ifName": "Gi0/1"}, {"ifName": "Gi0/2"}] selected = ["Gi0/1"] with patch("netbox_librenms_plugin.views.sync.interfaces.transaction"): view.sync_selected_interfaces(MagicMock(), selected, ports_data, [], "ifName") assert view.sync_interface.call_count == 1 call_args = view.sync_interface.call_args assert call_args[0][1]["ifName"] == "Gi0/1" # =========================================================================== # DeleteNetBoxInterfacesView # =========================================================================== class TestDeleteNetBoxInterfacesViewPermissions: def test_device_returns_interface_delete_perm(self): from netbox_librenms_plugin.views.sync.interfaces import DeleteNetBoxInterfacesView from dcim.models import Interface view = object.__new__(DeleteNetBoxInterfacesView) perms = view.get_required_permissions_for_object_type("device") assert ("delete", Interface) in perms def test_vm_returns_vminterface_delete_perm(self): from netbox_librenms_plugin.views.sync.interfaces import DeleteNetBoxInterfacesView from virtualization.models import VMInterface view = object.__new__(DeleteNetBoxInterfacesView) perms = view.get_required_permissions_for_object_type("virtualmachine") assert ("delete", VMInterface) in perms def test_invalid_type_raises_http404(self): from netbox_librenms_plugin.views.sync.interfaces import DeleteNetBoxInterfacesView from django.http import Http404 import pytest view = object.__new__(DeleteNetBoxInterfacesView) with pytest.raises(Http404): view.get_required_permissions_for_object_type("invalid") class TestDeleteNetBoxInterfacesViewPost: def test_permission_denied_returns_early(self): from netbox_librenms_plugin.views.sync.interfaces import DeleteNetBoxInterfacesView view = object.__new__(DeleteNetBoxInterfacesView) view.get_required_permissions_for_object_type = MagicMock(return_value=[]) view.require_all_permissions_json = MagicMock(return_value=_denied_response()) req = _make_request(post_data={"interface_ids": ["1"]}) result = view.post(req, "device", 1) assert result.status_code == 403 def test_empty_interface_ids_returns_400(self): from netbox_librenms_plugin.views.sync.interfaces import DeleteNetBoxInterfacesView from django.http import JsonResponse view = object.__new__(DeleteNetBoxInterfacesView) view.get_required_permissions_for_object_type = MagicMock(return_value=[]) view.require_all_permissions_json = MagicMock(return_value=None) mock_device = MagicMock(pk=1) req = _make_request(post_data={}) # No interface_ids with patch("netbox_librenms_plugin.views.sync.interfaces.get_object_or_404", return_value=mock_device): result = view.post(req, "device", 1) assert isinstance(result, JsonResponse) assert result.status_code == 400 def test_invalid_object_type_raises_http404(self): """Invalid object_type raises Http404 from get_required_permissions_for_object_type.""" import pytest from django.http import Http404 from netbox_librenms_plugin.views.sync.interfaces import DeleteNetBoxInterfacesView view = object.__new__(DeleteNetBoxInterfacesView) view.require_all_permissions_json = MagicMock(return_value=None) req = _make_request(post_data={"interface_ids": ["1"]}) with pytest.raises(Http404): view.post(req, "badtype", 1) def test_device_interface_deleted_successfully(self): from netbox_librenms_plugin.views.sync.interfaces import DeleteNetBoxInterfacesView from django.http import JsonResponse import json view = object.__new__(DeleteNetBoxInterfacesView) view.get_required_permissions_for_object_type = MagicMock(return_value=[]) view.require_all_permissions_json = MagicMock(return_value=None) mock_device = MagicMock(pk=1) mock_device.id = 1 mock_device.virtual_chassis = None mock_interface = MagicMock() mock_interface.name = "Gi0/1" mock_interface.device_id = 1 req = _make_request(post_data={"interface_ids": ["5"]}) with ( patch("netbox_librenms_plugin.views.sync.interfaces.get_object_or_404", return_value=mock_device), patch("netbox_librenms_plugin.views.sync.interfaces.Interface") as mock_intf_cls, patch("netbox_librenms_plugin.views.sync.interfaces.transaction"), ): mock_intf_cls.objects.get.return_value = mock_interface class _DNE(Exception): pass mock_intf_cls.DoesNotExist = _DNE result = view.post(req, "device", 1) assert isinstance(result, JsonResponse) data = json.loads(result.content) assert data["deleted_count"] == 1 mock_interface.delete.assert_called_once() def test_vm_interface_deleted_successfully(self): from netbox_librenms_plugin.views.sync.interfaces import DeleteNetBoxInterfacesView from virtualization.models import VMInterface from django.http import JsonResponse import json view = object.__new__(DeleteNetBoxInterfacesView) view.get_required_permissions_for_object_type = MagicMock(return_value=[]) view.require_all_permissions_json = MagicMock(return_value=None) mock_vm = MagicMock(pk=2) mock_vm.id = 2 mock_interface = MagicMock(spec=VMInterface) mock_interface.name = "eth0" mock_interface.virtual_machine_id = 2 req = _make_request(post_data={"interface_ids": ["7"]}) with ( patch("netbox_librenms_plugin.views.sync.interfaces.get_object_or_404", return_value=mock_vm), patch("netbox_librenms_plugin.views.sync.interfaces.VMInterface") as mock_vmintf_cls, patch("netbox_librenms_plugin.views.sync.interfaces.transaction"), ): mock_vmintf_cls.objects.get.return_value = mock_interface class _DNE(Exception): pass mock_vmintf_cls.DoesNotExist = _DNE result = view.post(req, "virtualmachine", 2) assert isinstance(result, JsonResponse) data = json.loads(result.content) assert data["deleted_count"] == 1 mock_interface.delete.assert_called_once() def test_device_interface_wrong_device_adds_error(self): from netbox_librenms_plugin.views.sync.interfaces import DeleteNetBoxInterfacesView import json view = object.__new__(DeleteNetBoxInterfacesView) view.get_required_permissions_for_object_type = MagicMock(return_value=[]) view.require_all_permissions_json = MagicMock(return_value=None) mock_device = MagicMock(pk=1) mock_device.id = 1 mock_device.virtual_chassis = None mock_interface = MagicMock() mock_interface.name = "Gi0/1" mock_interface.device_id = 99 # Wrong device req = _make_request(post_data={"interface_ids": ["5"]}) with ( patch("netbox_librenms_plugin.views.sync.interfaces.get_object_or_404", return_value=mock_device), patch("netbox_librenms_plugin.views.sync.interfaces.Interface") as mock_intf_cls, patch("netbox_librenms_plugin.views.sync.interfaces.transaction"), ): mock_intf_cls.objects.get.return_value = mock_interface class _DNE(Exception): pass mock_intf_cls.DoesNotExist = _DNE result = view.post(req, "device", 1) data = json.loads(result.content) assert data["deleted_count"] == 0 assert len(data["errors"]) == 1 def test_device_interface_with_vc_wrong_member_adds_error(self): from netbox_librenms_plugin.views.sync.interfaces import DeleteNetBoxInterfacesView import json view = object.__new__(DeleteNetBoxInterfacesView) view.get_required_permissions_for_object_type = MagicMock(return_value=[]) view.require_all_permissions_json = MagicMock(return_value=None) mock_device = MagicMock(pk=1) mock_device.id = 1 mock_vc = MagicMock() mock_member = MagicMock() mock_member.id = 1 mock_vc.members.all.return_value = [mock_member] mock_device.virtual_chassis = mock_vc mock_interface = MagicMock() mock_interface.name = "Gi0/1" mock_interface.device_id = 999 # Not in VC req = _make_request(post_data={"interface_ids": ["5"]}) with ( patch("netbox_librenms_plugin.views.sync.interfaces.get_object_or_404", return_value=mock_device), patch("netbox_librenms_plugin.views.sync.interfaces.Interface") as mock_intf_cls, patch("netbox_librenms_plugin.views.sync.interfaces.transaction"), ): mock_intf_cls.objects.get.return_value = mock_interface class _DNE(Exception): pass mock_intf_cls.DoesNotExist = _DNE result = view.post(req, "device", 1) data = json.loads(result.content) assert data["deleted_count"] == 0 assert len(data["errors"]) == 1 def test_interface_not_found_adds_error(self): from netbox_librenms_plugin.views.sync.interfaces import DeleteNetBoxInterfacesView import json view = object.__new__(DeleteNetBoxInterfacesView) view.get_required_permissions_for_object_type = MagicMock(return_value=[]) view.require_all_permissions_json = MagicMock(return_value=None) mock_device = MagicMock(pk=1) mock_device.id = 1 mock_device.virtual_chassis = None req = _make_request(post_data={"interface_ids": ["999"]}) class _DNE(Exception): pass with ( patch("netbox_librenms_plugin.views.sync.interfaces.get_object_or_404", return_value=mock_device), patch("netbox_librenms_plugin.views.sync.interfaces.Interface") as mock_intf_cls, patch("netbox_librenms_plugin.views.sync.interfaces.transaction"), ): mock_intf_cls.DoesNotExist = _DNE mock_intf_cls.objects.get.side_effect = _DNE() result = view.post(req, "device", 1) data = json.loads(result.content) assert data["deleted_count"] == 0 assert len(data["errors"]) == 1 def test_vm_interface_wrong_vm_adds_error(self): from netbox_librenms_plugin.views.sync.interfaces import DeleteNetBoxInterfacesView from virtualization.models import VMInterface import json view = object.__new__(DeleteNetBoxInterfacesView) view.get_required_permissions_for_object_type = MagicMock(return_value=[]) view.require_all_permissions_json = MagicMock(return_value=None) mock_vm = MagicMock(pk=2) mock_vm.id = 2 mock_interface = MagicMock(spec=VMInterface) mock_interface.name = "eth0" mock_interface.virtual_machine_id = 99 # Wrong VM req = _make_request(post_data={"interface_ids": ["7"]}) with ( patch("netbox_librenms_plugin.views.sync.interfaces.get_object_or_404", return_value=mock_vm), patch("netbox_librenms_plugin.views.sync.interfaces.VMInterface") as mock_vmintf_cls, patch("netbox_librenms_plugin.views.sync.interfaces.transaction"), ): mock_vmintf_cls.objects.get.return_value = mock_interface class _DNE(Exception): pass mock_vmintf_cls.DoesNotExist = _DNE result = view.post(req, "virtualmachine", 2) data = json.loads(result.content) assert data["deleted_count"] == 0 assert len(data["errors"]) == 1 def test_response_includes_errors_in_message(self): """When errors exist, message mentions error count.""" from netbox_librenms_plugin.views.sync.interfaces import DeleteNetBoxInterfacesView from dcim.models import Interface import json view = object.__new__(DeleteNetBoxInterfacesView) view.get_required_permissions_for_object_type = MagicMock(return_value=[]) view.require_all_permissions_json = MagicMock(return_value=None) mock_device = MagicMock(pk=1) mock_device.id = 1 mock_device.virtual_chassis = None # Two interfaces: one that belongs, one that doesn't mock_interface_ok = MagicMock(spec=Interface) mock_interface_ok.name = "Gi0/1" mock_interface_ok.device_id = 1 mock_interface_bad = MagicMock(spec=Interface) mock_interface_bad.name = "Gi0/2" mock_interface_bad.device_id = 99 # Wrong device req = _make_request(post_data={"interface_ids": ["5", "6"]}) call_count = [0] def get_side_effect(id): call_count[0] += 1 if call_count[0] == 1: return mock_interface_ok return mock_interface_bad class _DNE(Exception): pass with ( patch("netbox_librenms_plugin.views.sync.interfaces.get_object_or_404", return_value=mock_device), patch("netbox_librenms_plugin.views.sync.interfaces.Interface") as mock_intf_cls, patch("netbox_librenms_plugin.views.sync.interfaces.transaction"), ): mock_intf_cls.DoesNotExist = _DNE mock_intf_cls.objects.get.side_effect = get_side_effect result = view.post(req, "device", 1) data = json.loads(result.content) assert data["deleted_count"] == 1 assert "error" in data["message"] mock_interface_ok.delete.assert_called_once()