303 lines
12 KiB
Python
303 lines
12 KiB
Python
"""Unit tests for SyncInterfacesView: update_interface_attributes and handle_mac_address."""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
|
|
class TestUpdateInterfaceAttributes:
|
|
"""update_interface_attributes() must set fields respecting exclude_columns."""
|
|
|
|
@pytest.fixture
|
|
def view(self, mock_librenms_api):
|
|
"""Return a SyncInterfacesView wired to the shared mock API fixture."""
|
|
from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView
|
|
|
|
v = object.__new__(SyncInterfacesView)
|
|
v._librenms_api = mock_librenms_api
|
|
v.request = MagicMock()
|
|
v._lookup_maps = {}
|
|
return v
|
|
|
|
def _make_device_interface(self, **extra):
|
|
"""Return a MagicMock mimicking a dcim.Interface."""
|
|
from dcim.models import Interface # noqa: F401
|
|
|
|
iface = MagicMock(
|
|
spec=[
|
|
"name",
|
|
"type",
|
|
"speed",
|
|
"description",
|
|
"mtu",
|
|
"enabled",
|
|
"save",
|
|
"cf",
|
|
"custom_field_data",
|
|
"mac_addresses",
|
|
"primary_mac_address",
|
|
]
|
|
)
|
|
iface.cf = {"librenms_id": {"default": 1}}
|
|
iface.__class__ = Interface
|
|
for k, v in extra.items():
|
|
setattr(iface, k, v)
|
|
return iface
|
|
|
|
def test_sets_speed_via_convert(self, view):
|
|
iface = self._make_device_interface()
|
|
librenms_data = {"ifName": "eth0", "ifSpeed": 1_000_000_000}
|
|
|
|
with patch(
|
|
"netbox_librenms_plugin.views.sync.interfaces.convert_speed_to_kbps", return_value=1_000_000
|
|
) as mock_convert:
|
|
with patch("netbox_librenms_plugin.views.sync.interfaces.set_librenms_device_id"):
|
|
view.update_interface_attributes(iface, librenms_data, "1000base-t", set(), "ifName")
|
|
|
|
mock_convert.assert_called_once_with(1_000_000_000)
|
|
assert iface.speed == 1_000_000
|
|
|
|
def test_skips_excluded_columns(self, view):
|
|
speed_sentinel = object()
|
|
iface = self._make_device_interface(speed=speed_sentinel)
|
|
librenms_data = {"ifName": "eth0", "ifSpeed": 1_000_000_000, "ifAlias": "uplink"}
|
|
|
|
with patch("netbox_librenms_plugin.views.sync.interfaces.convert_speed_to_kbps", return_value=1_000_000):
|
|
with patch("netbox_librenms_plugin.views.sync.interfaces.set_librenms_device_id"):
|
|
view.update_interface_attributes(iface, librenms_data, "1000base-t", {"speed"}, "ifName")
|
|
|
|
# speed should NOT have been mutated (excluded)
|
|
assert iface.speed is speed_sentinel
|
|
|
|
def test_sets_type_for_device_interface(self, view):
|
|
from dcim.models import Interface
|
|
|
|
iface = MagicMock()
|
|
iface.__class__ = Interface
|
|
iface.cf = {}
|
|
iface.mac_addresses = MagicMock()
|
|
librenms_data = {"ifName": "eth0", "ifType": "ethernetCsmacd"}
|
|
|
|
with patch("netbox_librenms_plugin.views.sync.interfaces.convert_speed_to_kbps", return_value=None):
|
|
view.update_interface_attributes(iface, librenms_data, "1000base-t", set(), "ifName")
|
|
|
|
assert iface.type == "1000base-t"
|
|
|
|
def test_does_not_set_type_for_vm_interface(self, view):
|
|
from virtualization.models import VMInterface
|
|
|
|
iface = MagicMock()
|
|
iface.__class__ = VMInterface
|
|
iface.cf = {}
|
|
iface.mac_addresses = MagicMock()
|
|
original_type = "some_type"
|
|
iface.type = original_type
|
|
librenms_data = {"ifName": "eth0", "ifType": "ethernetCsmacd"}
|
|
|
|
with patch("netbox_librenms_plugin.views.sync.interfaces.convert_speed_to_kbps", return_value=None):
|
|
view.update_interface_attributes(iface, librenms_data, "1000base-t", set(), "ifName")
|
|
|
|
# type is NOT in the mapping for non-device interfaces (type set only if is_device_interface)
|
|
assert iface.type == original_type
|
|
|
|
def test_sets_description_only_when_alias_differs_from_name(self, view):
|
|
from dcim.models import Interface
|
|
|
|
iface = MagicMock()
|
|
iface.__class__ = Interface
|
|
iface.cf = {}
|
|
iface.mac_addresses = MagicMock()
|
|
desc_sentinel = object()
|
|
iface.description = desc_sentinel
|
|
|
|
# ifAlias == interface name field value → description should NOT be set
|
|
librenms_data = {"ifName": "eth0", "ifAlias": "eth0"}
|
|
|
|
with patch("netbox_librenms_plugin.views.sync.interfaces.convert_speed_to_kbps", return_value=None):
|
|
view.update_interface_attributes(iface, librenms_data, None, {"type", "speed", "mtu"}, "ifName")
|
|
|
|
assert iface.description is desc_sentinel # untouched: alias == name, no update
|
|
|
|
def test_sets_description_when_alias_differs(self, view):
|
|
from dcim.models import Interface
|
|
|
|
iface = MagicMock()
|
|
iface.__class__ = Interface
|
|
iface.cf = {}
|
|
iface.mac_addresses = MagicMock()
|
|
|
|
librenms_data = {"ifName": "eth0", "ifAlias": "uplink-port"}
|
|
|
|
with patch("netbox_librenms_plugin.views.sync.interfaces.convert_speed_to_kbps", return_value=None):
|
|
view.update_interface_attributes(iface, librenms_data, None, {"type", "speed", "mtu"}, "ifName")
|
|
|
|
assert iface.description == "uplink-port"
|
|
|
|
def test_sets_librenms_id_when_port_id_present(self, view):
|
|
"""
|
|
set_librenms_device_id() is called unconditionally when port_id is not None.
|
|
|
|
Historically the call was guarded by ``"librenms_id" in interface.cf``, which
|
|
prevented the mapping from being created for brand-new interfaces. This test
|
|
ensures the mapping is created even when no existing custom-field mapping is present.
|
|
"""
|
|
from dcim.models import Interface
|
|
|
|
iface = MagicMock()
|
|
iface.__class__ = Interface
|
|
iface.cf = {} # empty — first-time write, no existing mapping
|
|
iface.mac_addresses = MagicMock()
|
|
librenms_data = {"ifName": "eth0", "port_id": 77}
|
|
|
|
with patch("netbox_librenms_plugin.views.sync.interfaces.convert_speed_to_kbps", return_value=None):
|
|
with patch("netbox_librenms_plugin.views.sync.interfaces.set_librenms_device_id") as mock_set:
|
|
view.update_interface_attributes(iface, librenms_data, None, {"type", "speed", "mtu"}, "ifName")
|
|
|
|
mock_set.assert_called_once_with(iface, 77, view._librenms_api.server_key)
|
|
|
|
def test_does_not_set_librenms_id_when_port_id_none(self, view):
|
|
from dcim.models import Interface
|
|
|
|
iface = MagicMock()
|
|
iface.__class__ = Interface
|
|
iface.cf = {"librenms_id": {"default": 1}}
|
|
iface.mac_addresses = MagicMock()
|
|
librenms_data = {"ifName": "eth0", "port_id": None}
|
|
|
|
with patch("netbox_librenms_plugin.views.sync.interfaces.convert_speed_to_kbps", return_value=None):
|
|
with patch("netbox_librenms_plugin.views.sync.interfaces.set_librenms_device_id") as mock_set:
|
|
view.update_interface_attributes(iface, librenms_data, None, {"type", "speed", "mtu"}, "ifName")
|
|
|
|
mock_set.assert_not_called()
|
|
|
|
def test_sets_enabled_true_when_admin_status_none(self, view):
|
|
from dcim.models import Interface
|
|
|
|
iface = MagicMock()
|
|
iface.__class__ = Interface
|
|
iface.cf = {}
|
|
iface.mac_addresses = MagicMock()
|
|
librenms_data = {"ifName": "eth0", "ifAdminStatus": None}
|
|
|
|
with patch("netbox_librenms_plugin.views.sync.interfaces.convert_speed_to_kbps", return_value=None):
|
|
view.update_interface_attributes(iface, librenms_data, None, {"type", "speed", "mtu"}, "ifName")
|
|
|
|
assert iface.enabled is True
|
|
|
|
def test_sets_enabled_based_on_admin_status_string(self, view):
|
|
from dcim.models import Interface
|
|
|
|
iface = MagicMock()
|
|
iface.__class__ = Interface
|
|
iface.cf = {}
|
|
iface.mac_addresses = MagicMock()
|
|
librenms_data = {"ifName": "eth0", "ifAdminStatus": "down"}
|
|
|
|
with patch("netbox_librenms_plugin.views.sync.interfaces.convert_speed_to_kbps", return_value=None):
|
|
view.update_interface_attributes(iface, librenms_data, None, {"type", "speed", "mtu"}, "ifName")
|
|
|
|
assert iface.enabled is False
|
|
|
|
def test_calls_save_at_end(self, view):
|
|
from dcim.models import Interface
|
|
|
|
iface = MagicMock()
|
|
iface.__class__ = Interface
|
|
iface.cf = {}
|
|
iface.mac_addresses = MagicMock()
|
|
librenms_data = {"ifName": "eth0"}
|
|
|
|
with patch("netbox_librenms_plugin.views.sync.interfaces.convert_speed_to_kbps", return_value=None):
|
|
view.update_interface_attributes(iface, librenms_data, None, {"type", "speed", "mtu"}, "ifName")
|
|
|
|
iface.save.assert_called_once()
|
|
|
|
def test_excludes_mac_address_when_in_excluded(self, view):
|
|
from dcim.models import Interface
|
|
|
|
iface = MagicMock()
|
|
iface.__class__ = Interface
|
|
iface.cf = {}
|
|
iface.mac_addresses = MagicMock()
|
|
librenms_data = {"ifName": "eth0", "ifPhysAddress": "aa:bb:cc:dd:ee:ff"}
|
|
|
|
with patch("netbox_librenms_plugin.views.sync.interfaces.convert_speed_to_kbps", return_value=None):
|
|
with patch.object(view, "handle_mac_address") as mock_mac:
|
|
view.update_interface_attributes(iface, librenms_data, None, {"mac_address"}, "ifName")
|
|
|
|
mock_mac.assert_not_called()
|
|
|
|
|
|
class TestHandleMacAddress:
|
|
"""
|
|
handle_mac_address() must work for both Interface (has primary_mac_address)
|
|
and VMInterface (does not have primary_mac_address)."""
|
|
|
|
@pytest.fixture
|
|
def view(self, mock_librenms_api):
|
|
"""Return a SyncInterfacesView wired to the shared mock API fixture."""
|
|
from netbox_librenms_plugin.views.sync.interfaces import SyncInterfacesView
|
|
|
|
v = object.__new__(SyncInterfacesView)
|
|
v._librenms_api = mock_librenms_api
|
|
v.request = MagicMock()
|
|
v._lookup_maps = {}
|
|
return v
|
|
|
|
def test_creates_new_mac_and_adds_to_interface(self, view):
|
|
iface = MagicMock()
|
|
iface.mac_addresses = MagicMock()
|
|
iface.mac_addresses.filter.return_value.first.return_value = None
|
|
new_mac = MagicMock()
|
|
|
|
with patch("netbox_librenms_plugin.views.sync.interfaces.MACAddress") as mock_cls:
|
|
mock_cls.objects.create.return_value = new_mac
|
|
view.handle_mac_address(iface, "aa:bb:cc:dd:ee:ff")
|
|
|
|
mock_cls.objects.create.assert_called_once_with(mac_address="aa:bb:cc:dd:ee:ff")
|
|
iface.mac_addresses.add.assert_called_once_with(new_mac)
|
|
|
|
def test_reuses_existing_mac(self, view):
|
|
existing_mac = MagicMock()
|
|
iface = MagicMock()
|
|
iface.mac_addresses = MagicMock()
|
|
iface.mac_addresses.filter.return_value.first.return_value = existing_mac
|
|
|
|
with patch("netbox_librenms_plugin.views.sync.interfaces.MACAddress") as mock_cls:
|
|
view.handle_mac_address(iface, "aa:bb:cc:dd:ee:ff")
|
|
|
|
mock_cls.objects.create.assert_not_called()
|
|
iface.mac_addresses.add.assert_called_once_with(existing_mac)
|
|
|
|
def test_sets_primary_mac_when_attribute_present(self, view):
|
|
mac_obj = MagicMock()
|
|
iface = MagicMock(spec=["mac_addresses", "primary_mac_address"])
|
|
iface.mac_addresses = MagicMock()
|
|
iface.mac_addresses.filter.return_value.first.return_value = None
|
|
|
|
with patch("netbox_librenms_plugin.views.sync.interfaces.MACAddress") as mock_cls:
|
|
mock_cls.objects.create.return_value = mac_obj
|
|
view.handle_mac_address(iface, "aa:bb:cc:dd:ee:ff")
|
|
|
|
assert iface.primary_mac_address is mac_obj
|
|
|
|
def test_no_error_when_primary_mac_attribute_absent(self, view):
|
|
"""VMInterface does not have primary_mac_address — handle_mac_address must not raise."""
|
|
mac_obj = MagicMock()
|
|
iface = MagicMock(spec=["mac_addresses"]) # no primary_mac_address attr
|
|
iface.mac_addresses = MagicMock()
|
|
iface.mac_addresses.filter.return_value.first.return_value = None
|
|
|
|
with patch("netbox_librenms_plugin.views.sync.interfaces.MACAddress") as mock_cls:
|
|
mock_cls.objects.create.return_value = mac_obj
|
|
# Must not raise AttributeError
|
|
view.handle_mac_address(iface, "aa:bb:cc:dd:ee:ff")
|
|
|
|
def test_noop_when_mac_address_is_falsy(self, view):
|
|
iface = MagicMock()
|
|
with patch("netbox_librenms_plugin.views.sync.interfaces.MACAddress") as mock_cls:
|
|
view.handle_mac_address(iface, "")
|
|
view.handle_mac_address(iface, None)
|
|
|
|
mock_cls.objects.create.assert_not_called()
|