first commit
This commit is contained in:
207
netbox_librenms_plugin/tests/test_mixins.py
Normal file
207
netbox_librenms_plugin/tests/test_mixins.py
Normal file
@@ -0,0 +1,207 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user