368 lines
14 KiB
Python
368 lines
14 KiB
Python
"""
|
|
Tests for multi-server librenms_id helpers.
|
|
|
|
Covers get_librenms_device_id, set_librenms_device_id, find_by_librenms_id,
|
|
and migrate_legacy_librenms_id.
|
|
"""
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
|
|
class TestGetLibreNMSDeviceId:
|
|
"""Tests for get_librenms_device_id()."""
|
|
|
|
def test_returns_none_when_cf_missing(self):
|
|
from netbox_librenms_plugin.utils import get_librenms_device_id
|
|
|
|
obj = MagicMock()
|
|
obj.cf = {}
|
|
result = get_librenms_device_id(obj, "default")
|
|
assert result is None
|
|
|
|
def test_returns_int_for_legacy_bare_integer(self):
|
|
from netbox_librenms_plugin.utils import get_librenms_device_id
|
|
|
|
obj = MagicMock()
|
|
obj.cf = {"librenms_id": 42}
|
|
result = get_librenms_device_id(obj, "default")
|
|
assert result == 42
|
|
|
|
def test_legacy_bare_int_returned_for_any_server_key(self):
|
|
"""
|
|
Legacy bare integers are returned as a universal fallback for any server_key.
|
|
|
|
Devices imported before multi-server support store a bare integer in
|
|
librenms_id. These must remain discoverable regardless of which server is
|
|
active, so the bare-int is returned as-is for any server_key.
|
|
"""
|
|
from netbox_librenms_plugin.utils import get_librenms_device_id
|
|
|
|
obj = MagicMock()
|
|
obj.cf = {"librenms_id": 99}
|
|
assert get_librenms_device_id(obj, "default") == 99
|
|
assert get_librenms_device_id(obj, "production") == 99
|
|
assert get_librenms_device_id(obj, "secondary") == 99
|
|
|
|
def test_returns_value_for_matching_server_key(self):
|
|
from netbox_librenms_plugin.utils import get_librenms_device_id
|
|
|
|
obj = MagicMock()
|
|
obj.cf = {"librenms_id": {"production": 7, "secondary": 12}}
|
|
assert get_librenms_device_id(obj, "production") == 7
|
|
|
|
def test_returns_none_for_missing_server_key_in_dict(self):
|
|
from netbox_librenms_plugin.utils import get_librenms_device_id
|
|
|
|
obj = MagicMock()
|
|
obj.cf = {"librenms_id": {"production": 7}}
|
|
result = get_librenms_device_id(obj, "secondary")
|
|
assert result is None
|
|
|
|
def test_returns_none_for_unexpected_type(self):
|
|
from netbox_librenms_plugin.utils import get_librenms_device_id
|
|
|
|
obj = MagicMock()
|
|
obj.cf = {"librenms_id": "not-an-int-or-dict"}
|
|
result = get_librenms_device_id(obj, "default")
|
|
assert result is None
|
|
|
|
def test_legacy_string_int_returned_for_any_server_key(self):
|
|
"""A bare string integer ('42') is coerced and returned for any server_key."""
|
|
from netbox_librenms_plugin.utils import get_librenms_device_id
|
|
|
|
obj = MagicMock()
|
|
obj.cf = {"librenms_id": "42"}
|
|
assert get_librenms_device_id(obj, "default") == 42
|
|
assert get_librenms_device_id(obj, "production") == 42
|
|
|
|
def test_returns_none_for_bare_boolean(self):
|
|
"""bool is a subclass of int; bare True/False must not be treated as a valid ID."""
|
|
from netbox_librenms_plugin.utils import get_librenms_device_id
|
|
|
|
obj = MagicMock()
|
|
obj.cf = {"librenms_id": True}
|
|
assert get_librenms_device_id(obj, "default") is None
|
|
|
|
obj.cf = {"librenms_id": False}
|
|
assert get_librenms_device_id(obj, "default") is None
|
|
|
|
def test_returns_none_for_boolean_inside_dict(self):
|
|
"""Boolean values inside the JSON dict must be rejected."""
|
|
from netbox_librenms_plugin.utils import get_librenms_device_id
|
|
|
|
obj = MagicMock()
|
|
obj.cf = {"librenms_id": {"default": True}}
|
|
assert get_librenms_device_id(obj, "default") is None
|
|
|
|
def test_default_server_key_is_default(self):
|
|
from netbox_librenms_plugin.utils import get_librenms_device_id
|
|
|
|
obj = MagicMock()
|
|
obj.cf = {"librenms_id": {"default": 5}}
|
|
assert get_librenms_device_id(obj) == 5
|
|
|
|
|
|
class TestFindByLibreNMSId:
|
|
"""Tests for find_by_librenms_id()."""
|
|
|
|
def test_queries_server_key_and_legacy_integer(self):
|
|
"""
|
|
find_by_librenms_id() issues a Q that covers both the JSON server-key branch
|
|
and the legacy bare-int branch in a single filter() call.
|
|
|
|
We inspect the Q object's children directly because the two branches must
|
|
coexist — matching only one would silently miss devices stored in the other
|
|
format.
|
|
"""
|
|
from unittest.mock import MagicMock
|
|
from django.db.models import Q
|
|
from netbox_librenms_plugin.utils import find_by_librenms_id
|
|
|
|
mock_model = MagicMock()
|
|
mock_qs = MagicMock()
|
|
mock_model.objects.filter.return_value = mock_qs
|
|
mock_qs.first.return_value = None
|
|
|
|
find_by_librenms_id(mock_model, 42, "default")
|
|
|
|
mock_model.objects.filter.assert_called_once()
|
|
# Verify the Q predicate covers both the server-key JSON branch and legacy bare-int/string branches
|
|
call_args = mock_model.objects.filter.call_args
|
|
q_arg = call_args[0][0]
|
|
assert isinstance(q_arg, Q)
|
|
assert q_arg.connector == "OR"
|
|
# The combined Q should contain four children: JSON key (int), JSON key (str), bare-int, bare-string
|
|
q_str = str(q_arg)
|
|
assert "librenms_id__default" in q_str
|
|
assert "custom_field_data__librenms_id__default" in q_str
|
|
assert "custom_field_data__librenms_id" in q_str
|
|
assert "42" in q_str
|
|
|
|
def test_returns_first_matching_object(self):
|
|
from netbox_librenms_plugin.utils import find_by_librenms_id
|
|
|
|
expected = MagicMock()
|
|
mock_model = MagicMock()
|
|
mock_qs = MagicMock()
|
|
mock_model.objects.filter.return_value = mock_qs
|
|
mock_qs.first.return_value = expected
|
|
|
|
result = find_by_librenms_id(mock_model, 42, "default")
|
|
assert result is expected
|
|
|
|
def test_returns_none_when_not_found(self):
|
|
from unittest.mock import MagicMock
|
|
from django.db.models import Q
|
|
from netbox_librenms_plugin.utils import find_by_librenms_id
|
|
|
|
mock_model = MagicMock()
|
|
mock_qs = MagicMock()
|
|
mock_model.objects.filter.return_value = mock_qs
|
|
mock_qs.first.return_value = None
|
|
|
|
result = find_by_librenms_id(mock_model, 999, "production")
|
|
assert result is None
|
|
|
|
# Any server_key must include legacy bare-int/string fallback conditions
|
|
# so that devices imported before multi-server support are still found.
|
|
call_args = mock_model.objects.filter.call_args
|
|
q_arg = call_args[0][0]
|
|
assert isinstance(q_arg, Q)
|
|
q_str = str(q_arg)
|
|
assert "custom_field_data__librenms_id__production" in q_str
|
|
assert "custom_field_data__librenms_id" in q_str
|
|
|
|
def test_default_server_key_is_default(self):
|
|
"""find_by_librenms_id() uses "default" as the server key when no key is passed.
|
|
|
|
We inspect the Q predicate's children to confirm the key embedded in the
|
|
JSON path is exactly "default", not some other fallback value.
|
|
"""
|
|
from unittest.mock import MagicMock
|
|
from django.db.models import Q
|
|
from netbox_librenms_plugin.utils import find_by_librenms_id
|
|
|
|
mock_model = MagicMock()
|
|
mock_qs = MagicMock()
|
|
mock_model.objects.filter.return_value = mock_qs
|
|
mock_qs.first.return_value = None
|
|
|
|
find_by_librenms_id(mock_model, 42)
|
|
|
|
mock_model.objects.filter.assert_called_once()
|
|
call_args = mock_model.objects.filter.call_args
|
|
q_arg = call_args[0][0]
|
|
assert isinstance(q_arg, Q)
|
|
assert q_arg.connector == "OR"
|
|
q_str = str(q_arg)
|
|
assert "custom_field_data__librenms_id__default" in q_str
|
|
|
|
|
|
class TestMigrateLegacyLibreNMSId:
|
|
"""Tests for migrate_legacy_librenms_id()."""
|
|
|
|
def test_returns_true_when_migrated(self):
|
|
from netbox_librenms_plugin.utils import migrate_legacy_librenms_id
|
|
|
|
obj = MagicMock()
|
|
obj.custom_field_data = {"librenms_id": 42}
|
|
result = migrate_legacy_librenms_id(obj, "default")
|
|
assert result is True
|
|
|
|
def test_migrates_integer_to_dict_format(self):
|
|
from netbox_librenms_plugin.utils import migrate_legacy_librenms_id
|
|
|
|
obj = MagicMock()
|
|
obj.custom_field_data = {"librenms_id": 42}
|
|
migrate_legacy_librenms_id(obj, "production")
|
|
assert obj.custom_field_data["librenms_id"] == {"production": 42}
|
|
|
|
def test_returns_false_when_already_dict(self):
|
|
from netbox_librenms_plugin.utils import migrate_legacy_librenms_id
|
|
|
|
obj = MagicMock()
|
|
obj.custom_field_data = {"librenms_id": {"default": 42}}
|
|
result = migrate_legacy_librenms_id(obj, "default")
|
|
assert result is False
|
|
|
|
def test_returns_false_when_value_is_none(self):
|
|
from netbox_librenms_plugin.utils import migrate_legacy_librenms_id
|
|
|
|
obj = MagicMock()
|
|
obj.custom_field_data = {"librenms_id": None}
|
|
result = migrate_legacy_librenms_id(obj, "default")
|
|
assert result is False
|
|
|
|
def test_returns_false_for_boolean_value(self):
|
|
"""bool is a subclass of int; True/False must not be migrated."""
|
|
from netbox_librenms_plugin.utils import migrate_legacy_librenms_id
|
|
|
|
obj = MagicMock()
|
|
obj.custom_field_data = {"librenms_id": True}
|
|
assert migrate_legacy_librenms_id(obj, "default") is False
|
|
assert obj.custom_field_data["librenms_id"] is True # unchanged
|
|
|
|
def test_does_not_call_save(self):
|
|
"""migrate_legacy_librenms_id must NOT call obj.save() — caller is responsible."""
|
|
from netbox_librenms_plugin.utils import migrate_legacy_librenms_id
|
|
|
|
obj = MagicMock()
|
|
obj.custom_field_data = {"librenms_id": 7}
|
|
migrate_legacy_librenms_id(obj, "default")
|
|
obj.save.assert_not_called()
|
|
|
|
def test_preserves_value_in_migrated_dict(self):
|
|
from netbox_librenms_plugin.utils import migrate_legacy_librenms_id
|
|
|
|
obj = MagicMock()
|
|
obj.custom_field_data = {"librenms_id": 99}
|
|
migrate_legacy_librenms_id(obj, "secondary")
|
|
assert obj.custom_field_data["librenms_id"]["secondary"] == 99
|
|
|
|
|
|
class TestLibreNMSIdRoundtrip:
|
|
"""get_librenms_device_id should see the value set by set_librenms_device_id."""
|
|
|
|
def test_set_then_get_returns_same_value(self):
|
|
from netbox_librenms_plugin.utils import get_librenms_device_id, set_librenms_device_id
|
|
|
|
obj = MagicMock()
|
|
obj.custom_field_data = {}
|
|
obj.cf = obj.custom_field_data # make cf a live view of custom_field_data
|
|
|
|
set_librenms_device_id(obj, 42, "production")
|
|
result = get_librenms_device_id(obj, "production")
|
|
assert result == 42
|
|
|
|
def test_set_multiple_servers_get_correct_each(self):
|
|
from netbox_librenms_plugin.utils import get_librenms_device_id, set_librenms_device_id
|
|
|
|
obj = MagicMock()
|
|
obj.custom_field_data = {}
|
|
obj.cf = obj.custom_field_data
|
|
|
|
set_librenms_device_id(obj, 10, "primary")
|
|
set_librenms_device_id(obj, 20, "secondary")
|
|
|
|
assert get_librenms_device_id(obj, "primary") == 10
|
|
assert get_librenms_device_id(obj, "secondary") == 20
|
|
|
|
def test_migrate_then_get_returns_value(self):
|
|
from netbox_librenms_plugin.utils import get_librenms_device_id, migrate_legacy_librenms_id
|
|
|
|
obj = MagicMock()
|
|
obj.custom_field_data = {"librenms_id": 55}
|
|
obj.cf = obj.custom_field_data
|
|
|
|
migrate_legacy_librenms_id(obj, "default")
|
|
result = get_librenms_device_id(obj, "default")
|
|
assert result == 55
|
|
|
|
|
|
class TestSetLibreNMSDeviceId:
|
|
"""Tests for set_librenms_device_id in utils.py."""
|
|
|
|
def test_stores_int_for_valid_device_id(self):
|
|
"""Valid integer device_id is stored under server_key."""
|
|
from netbox_librenms_plugin.utils import set_librenms_device_id
|
|
|
|
obj = MagicMock()
|
|
obj.custom_field_data = {"librenms_id": None}
|
|
set_librenms_device_id(obj, 42, server_key="primary")
|
|
assert obj.custom_field_data["librenms_id"] == {"primary": 42}
|
|
|
|
def test_invalid_device_id_not_stored(self):
|
|
"""Non-integer device_id is rejected and nothing is written."""
|
|
from netbox_librenms_plugin.utils import set_librenms_device_id
|
|
|
|
obj = MagicMock()
|
|
obj.custom_field_data = {}
|
|
set_librenms_device_id(obj, "not-an-int", server_key="primary")
|
|
assert "librenms_id" not in obj.custom_field_data
|
|
|
|
def test_invalid_device_id_does_not_overwrite_existing(self):
|
|
"""Existing valid value is preserved when new device_id is invalid."""
|
|
from netbox_librenms_plugin.utils import set_librenms_device_id
|
|
|
|
obj = MagicMock()
|
|
obj.custom_field_data = {"librenms_id": {"primary": 10}}
|
|
set_librenms_device_id(obj, None, server_key="primary")
|
|
assert obj.custom_field_data["librenms_id"] == {"primary": 10}
|
|
|
|
def test_legacy_bare_int_blocks_write(self):
|
|
"""Legacy bare-integer value blocks the write (no silent migration)."""
|
|
from netbox_librenms_plugin.utils import set_librenms_device_id
|
|
|
|
obj = MagicMock()
|
|
obj.custom_field_data = {"librenms_id": 7}
|
|
set_librenms_device_id(obj, 99, server_key="secondary")
|
|
# Write must be skipped; user must use the migration workflow.
|
|
assert obj.custom_field_data["librenms_id"] == 7
|
|
|
|
def test_adds_new_server_key_to_existing_dict(self):
|
|
"""Adding a new server key preserves existing keys."""
|
|
from netbox_librenms_plugin.utils import set_librenms_device_id
|
|
|
|
obj = MagicMock()
|
|
obj.custom_field_data = {"librenms_id": {"primary": 5}}
|
|
set_librenms_device_id(obj, 20, server_key="secondary")
|
|
assert obj.custom_field_data["librenms_id"] == {"primary": 5, "secondary": 20}
|
|
|
|
def test_string_integer_is_coerced(self):
|
|
"""String '42' is coerced to int 42."""
|
|
from netbox_librenms_plugin.utils import set_librenms_device_id
|
|
|
|
obj = MagicMock()
|
|
obj.custom_field_data = {}
|
|
set_librenms_device_id(obj, "42", server_key="primary")
|
|
assert obj.custom_field_data["librenms_id"] == {"primary": 42}
|
|
|
|
def test_unexpected_cf_type_reset_to_empty(self):
|
|
"""If custom_field_data has unexpected type for librenms_id, it is reset."""
|
|
from netbox_librenms_plugin.utils import set_librenms_device_id
|
|
|
|
obj = MagicMock()
|
|
obj.custom_field_data = {"librenms_id": "unexpected-string"}
|
|
set_librenms_device_id(obj, 5, server_key="primary")
|
|
assert obj.custom_field_data["librenms_id"] == {"primary": 5}
|