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

208 lines
7.2 KiB
Python

"""Tests for view mixins: LibreNMSAPIMixin and CacheMixin."""
from unittest.mock import MagicMock, patch
class TestLibreNMSAPIMixinLazyInit:
"""LibreNMSAPIMixin.librenms_api is lazy — not created until first access."""
def _make_mixin(self):
from netbox_librenms_plugin.views.mixins import LibreNMSAPIMixin
mixin = object.__new__(LibreNMSAPIMixin)
mixin._librenms_api = None
return mixin
def test_starts_with_none(self):
mixin = self._make_mixin()
assert mixin._librenms_api is None
def test_first_access_creates_instance(self):
mixin = self._make_mixin()
fake_api = MagicMock()
with patch("netbox_librenms_plugin.views.mixins.LibreNMSAPI", return_value=fake_api):
api = mixin.librenms_api
assert api is fake_api
def test_second_access_returns_same_instance(self):
mixin = self._make_mixin()
fake_api = MagicMock()
with patch("netbox_librenms_plugin.views.mixins.LibreNMSAPI", return_value=fake_api) as mock_cls:
api1 = mixin.librenms_api
api2 = mixin.librenms_api
assert api1 is api2
mock_cls.assert_called_once() # constructor called only once
def test_librenms_api_is_property_descriptor(self):
from netbox_librenms_plugin.views.mixins import LibreNMSAPIMixin
assert isinstance(LibreNMSAPIMixin.__dict__["librenms_api"], property)
class TestLibreNMSAPIMixinGetServerInfo:
"""get_server_info() returns correct structure for multi-server and legacy configs."""
def _make_mixin_with_api(self, server_key="default"):
from netbox_librenms_plugin.views.mixins import LibreNMSAPIMixin
mixin = object.__new__(LibreNMSAPIMixin)
fake_api = MagicMock()
fake_api.server_key = server_key
mixin._librenms_api = fake_api
return mixin
def test_multi_server_returns_display_name_and_url(self, mock_multi_server_config):
mixin = self._make_mixin_with_api("default")
with patch("netbox.plugins.get_plugin_config") as mock_config:
mock_config.side_effect = lambda _plugin, key: mock_multi_server_config if key == "servers" else None
info = mixin.get_server_info()
assert info["display_name"] == "default" # falls back to server_key (no display_name in fixture)
assert info["url"] == mock_multi_server_config["default"]["librenms_url"]
assert info["is_legacy"] is False
assert info["server_key"] == "default"
def test_legacy_config_sets_is_legacy_true(self, mock_legacy_config):
mixin = self._make_mixin_with_api("default")
def mock_plugin_config(_plugin, key):
if key == "servers":
return None
if key == "librenms_url":
return mock_legacy_config["librenms_url"]
return None
with patch("netbox.plugins.get_plugin_config", side_effect=mock_plugin_config):
info = mixin.get_server_info()
assert info["is_legacy"] is True
assert info["url"] == mock_legacy_config["librenms_url"]
def test_returns_error_info_on_exception(self):
mixin = self._make_mixin_with_api("default")
with patch("netbox.plugins.get_plugin_config", side_effect=ImportError):
info = mixin.get_server_info()
assert "is_legacy" in info
assert info["is_legacy"] is True
class TestCacheMixinKeyGeneration:
"""CacheMixin generates consistent, predictable cache keys."""
def _make_mixin(self):
from netbox_librenms_plugin.views.mixins import CacheMixin
return object.__new__(CacheMixin)
def test_get_cache_key_format(self):
mixin = self._make_mixin()
obj = MagicMock()
obj._meta.model_name = "device"
obj.pk = 5
key = mixin.get_cache_key(obj, "ports")
assert key == "librenms_ports_device_5"
def test_get_cache_key_includes_server_key(self):
"""
Cache keys must be namespaced per server so two servers' data never collide.
Without server_key isolation a second server's stale ports list could be
returned to the wrong sync session.
"""
mixin = self._make_mixin()
obj = MagicMock()
obj._meta.model_name = "device"
obj.pk = 5
key = mixin.get_cache_key(obj, "ports", server_key="srv1")
assert "srv1" in key
assert key == "librenms_ports_device_5_srv1"
def test_get_cache_key_includes_model_name(self):
mixin = self._make_mixin()
obj = MagicMock()
obj._meta.model_name = "virtualmachine"
obj.pk = 10
key = mixin.get_cache_key(obj, "interfaces")
assert "virtualmachine" in key
assert "10" in key
def test_get_cache_key_different_data_types(self):
mixin = self._make_mixin()
obj = MagicMock()
obj._meta.model_name = "device"
obj.pk = 1
key_ports = mixin.get_cache_key(obj, "ports", server_key="prod")
key_ips = mixin.get_cache_key(obj, "ips", server_key="prod")
assert key_ports != key_ips
def test_get_last_fetched_key_format(self):
mixin = self._make_mixin()
obj = MagicMock()
obj._meta.model_name = "device"
obj.pk = 3
key = mixin.get_last_fetched_key(obj, "ports")
assert key == "librenms_ports_last_fetched_device_3" # exact string
def test_get_last_fetched_key_includes_server_key(self):
"""
The last-fetched timestamp key must also be server-scoped.
If two servers share the same key the cache countdown would reflect the
wrong server's fetch time.
"""
mixin = self._make_mixin()
obj = MagicMock()
obj._meta.model_name = "device"
obj.pk = 3
key = mixin.get_last_fetched_key(obj, "ports", server_key="srv1")
assert key == "librenms_ports_last_fetched_device_3_srv1" # exact string
def test_cache_key_different_pks_differ(self):
mixin = self._make_mixin()
obj1 = MagicMock()
obj1._meta.model_name = "device"
obj1.pk = 1
obj2 = MagicMock()
obj2._meta.model_name = "device"
obj2.pk = 2
assert mixin.get_cache_key(obj1, "ports") != mixin.get_cache_key(obj2, "ports")
def test_get_vlan_overrides_key_exists_and_differs_from_data_key(self):
"""VLAN group overrides use a separate cache key from the VLAN data key."""
mixin = self._make_mixin()
obj = MagicMock()
obj._meta.model_name = "device"
obj.pk = 7
vlan_key = mixin.get_vlan_overrides_key(obj)
assert vlan_key == "librenms_vlan_group_overrides_device_7"
data_key = mixin.get_cache_key(obj, "vlans")
assert vlan_key != data_key
def test_get_vlan_overrides_key_server_scoped(self):
"""VLAN overrides key includes server_key to avoid cross-server leakage."""
mixin = self._make_mixin()
obj = MagicMock()
obj._meta.model_name = "device"
obj.pk = 7
key_no_server = mixin.get_vlan_overrides_key(obj)
key_with_server = mixin.get_vlan_overrides_key(obj, server_key="prod")
assert key_with_server == "librenms_vlan_group_overrides_device_7_prod"
assert key_no_server != key_with_server