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

1194 lines
48 KiB
Python

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