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

2038 lines
76 KiB
Python

"""
Additional coverage tests for:
- views/base/cables_view.py (currently ~59%)
- views/base/ip_addresses_view.py (~62%)
All tests follow strict project conventions:
- Plain pytest classes, NO @pytest.mark.django_db
- Mock ALL database interactions with MagicMock
- Inline imports inside test methods
- assert x == y style
- Use object.__new__(ClassName) to bypass __init__
- No RequestFactory — mock request objects directly
"""
from unittest.mock import MagicMock, patch
# =============================================================================
# Helpers
# =============================================================================
def _mock_obj(model_name="device", pk=1, name="test-device"):
obj = MagicMock()
obj._meta = MagicMock()
obj._meta.model_name = model_name
obj.pk = pk
obj.name = name
obj.virtual_chassis = None
return obj
def _mock_request(path="/plugins/librenms/device/1/cables/"):
req = MagicMock()
req.path = path
req.GET = {}
req.POST = {}
req.headers = {}
return req
# =============================================================================
# TestLibreNMSIdQ — _librenms_id_q edge cases
# =============================================================================
class TestLibreNMSIdQ:
"""Tests for _librenms_id_q edge cases (lines 29-43)."""
def test_bool_true_returns_match_nothing_q(self):
"""Boolean True → Q(pk__isnull=True) & Q(pk__isnull=False) (matches nothing)."""
from netbox_librenms_plugin.views.base.cables_view import _librenms_id_q
from django.db.models import Q
result = _librenms_id_q("default", True)
expected = Q(pk__isnull=True) & Q(pk__isnull=False)
assert str(result) == str(expected)
def test_bool_false_returns_match_nothing_q(self):
"""Boolean False also returns match-nothing Q."""
from netbox_librenms_plugin.views.base.cables_view import _librenms_id_q
from django.db.models import Q
result = _librenms_id_q("default", False)
expected = Q(pk__isnull=True) & Q(pk__isnull=False)
assert str(result) == str(expected)
def test_string_int_value_adds_integer_variant(self):
"""String '10': int_val=10 != '10' (value is str) → adds integer variants (lines 37-38)."""
from netbox_librenms_plugin.views.base.cables_view import _librenms_id_q
from django.db.models import Q
result = _librenms_id_q("default", "10")
# The Q should include both the string "10" form and the integer 10 form.
# Verify the result is a compound Q that references integer 10.
result_str = str(result)
assert "10" in result_str
# It should NOT just be the base Q; confirm the extra integer variant was added
base_only = Q(custom_field_data__librenms_id__default="10") | Q(custom_field_data__librenms_id="10")
assert str(result) != str(base_only)
def test_int_value_adds_string_variant(self):
"""Integer 10: str_val='10' != 10 (value is int) → adds string variants (lines 39-41)."""
from netbox_librenms_plugin.views.base.cables_view import _librenms_id_q
from django.db.models import Q
result = _librenms_id_q("default", 10)
result_str = str(result)
assert "10" in result_str
# Confirm the extra string variant was added
base_only = Q(custom_field_data__librenms_id__default=10) | Q(custom_field_data__librenms_id=10)
assert str(result) != str(base_only)
def test_non_int_string_value_except_caught(self):
"""Non-convertible string 'abc' → ValueError caught, base Q returned (lines 42-43)."""
from netbox_librenms_plugin.views.base.cables_view import _librenms_id_q
# Should NOT raise — the except catches ValueError
result = _librenms_id_q("default", "abc")
assert result is not None
def test_none_value_typeerror_caught(self):
"""None → TypeError on int(None) caught, base Q returned (lines 42-43)."""
from netbox_librenms_plugin.views.base.cables_view import _librenms_id_q
result = _librenms_id_q("default", None)
assert result is not None
# =============================================================================
# TestGetObjectAndIpAddress — BaseCableTableView trivial wrappers
# =============================================================================
class TestGetObjectAndIpAddress:
"""Tests for BaseCableTableView.get_object (line 57) and get_ip_address (lines 61-63)."""
def _make_view(self):
from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView
view = object.__new__(BaseCableTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
view.model = MagicMock()
return view
def test_get_object_calls_get_object_or_404(self):
"""get_object calls get_object_or_404 with view.model and given pk."""
view = self._make_view()
mock_device = MagicMock()
with patch(
"netbox_librenms_plugin.views.base.cables_view.get_object_or_404",
return_value=mock_device,
) as mock_get:
result = view.get_object(42)
mock_get.assert_called_once_with(view.model, pk=42)
assert result is mock_device
def test_get_ip_address_with_primary_ip(self):
"""get_ip_address returns the string representation of primary_ip when present."""
view = self._make_view()
obj = MagicMock()
obj.primary_ip.address.ip = "192.168.1.1"
result = view.get_ip_address(obj)
assert result == "192.168.1.1"
def test_get_ip_address_without_primary_ip(self):
"""get_ip_address returns None when obj has no primary_ip."""
view = self._make_view()
obj = MagicMock()
obj.primary_ip = None
result = view.get_ip_address(obj)
assert result is None
# =============================================================================
# TestGetPortsDataFailure — get_ports_data failure path (line 73)
# =============================================================================
class TestGetPortsDataFailure:
"""Tests for BaseCableTableView.get_ports_data failure path."""
def _make_view(self):
from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView
view = object.__new__(BaseCableTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
view.librenms_id = 42
return view
def test_returns_empty_ports_on_api_failure(self):
"""When librenms_api.get_ports() returns failure, returns {'ports': []}."""
view = self._make_view()
view._librenms_api.get_ports.return_value = (False, {})
obj = _mock_obj()
with patch("netbox_librenms_plugin.views.base.cables_view.cache") as mock_cache:
mock_cache.get.return_value = None
with patch.object(view, "get_cache_key", return_value="test-key"):
result = view.get_ports_data(obj)
assert result == {"ports": []}
def test_returns_cached_data_without_api_call(self):
"""When cached data exists, returns it without hitting the API."""
view = self._make_view()
cached = {"ports": [{"port_id": 1, "ifName": "Gi0/0"}]}
obj = _mock_obj()
with patch("netbox_librenms_plugin.views.base.cables_view.cache") as mock_cache:
mock_cache.get.return_value = cached
with patch.object(view, "get_cache_key", return_value="test-key"):
result = view.get_ports_data(obj)
assert result is cached
view._librenms_api.get_ports.assert_not_called()
# =============================================================================
# TestGetLinksDataPortNameNone — continue branch when port_name is None (line 98)
# =============================================================================
class TestGetLinksDataPortNameNone:
"""Tests for get_links_data when port.get(interface_name_field) is None → skipped."""
def _make_view(self):
from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView
view = object.__new__(BaseCableTableView)
view.request = _mock_request()
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
view.librenms_id = 42
return view
def test_port_name_none_excluded_from_local_ports_map(self):
"""Port with None for interface_name_field is skipped; local_port maps to None."""
view = self._make_view()
links_data = {
"links": [
{
"local_port_id": 10,
"remote_port": "Gi0/1",
"remote_hostname": "switch-b",
"remote_port_id": 20,
"remote_device_id": 99,
}
]
}
view._librenms_api.get_device_links.return_value = (True, links_data)
view._librenms_api.get_librenms_id.return_value = 42
ports_data = {
"ports": [
{"port_id": 10, "ifName": None}, # port_name is None → continue
]
}
obj = _mock_obj()
with (
patch.object(view, "get_ports_data", return_value=ports_data),
patch(
"netbox_librenms_plugin.views.base.cables_view.get_interface_name_field",
return_value="ifName",
),
):
result = view.get_links_data(obj)
assert result is not None
# local_port is None because port_id=10 was skipped from the map
assert result[0]["local_port"] is None
# =============================================================================
# TestGetDeviceByIdOrNameEdgeCases — MultipleObjectsReturned, FQDN fallback
# =============================================================================
class TestGetDeviceByIdOrNameEdgeCases:
"""Tests for get_device_by_id_or_name edge cases (lines 123-126, 144-145)."""
def _make_view(self):
from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView
view = object.__new__(BaseCableTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
return view
def test_multiple_objects_returned_for_librenms_id(self):
"""MultipleObjectsReturned on librenms_id → (None, False, error message)."""
from django.core.exceptions import MultipleObjectsReturned
view = self._make_view()
with patch("netbox_librenms_plugin.views.base.cables_view.Device") as MockDevice:
# Use a narrow DoesNotExist so it doesn't swallow MultipleObjectsReturned
class _DoesNotExist(Exception):
pass
MockDevice.DoesNotExist = _DoesNotExist
MockDevice.objects.get.side_effect = MultipleObjectsReturned
device, found, error = view.get_device_by_id_or_name(42, "switch.example.com")
assert device is None
assert found is False
assert error is not None
assert "42" in error
def test_fqdn_fails_simple_hostname_succeeds(self):
"""FQDN lookup raises DoesNotExist; short hostname lookup succeeds (lines 144-145)."""
from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView
view = object.__new__(BaseCableTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
mock_device = MagicMock()
with patch("netbox_librenms_plugin.views.base.cables_view.Device") as MockDevice:
class _DoesNotExist(Exception):
pass
MockDevice.DoesNotExist = _DoesNotExist
# remote_device_id=None → skip librenms_id lookup, go straight to name
# First get() (FQDN "switch.example.com") raises DoesNotExist
# Second get() (simple "switch") succeeds
MockDevice.objects.get.side_effect = [_DoesNotExist, mock_device]
device, found, error = view.get_device_by_id_or_name(None, "switch.example.com")
assert found is True
assert device is mock_device
assert error is None
# =============================================================================
# TestEnrichLocalPortVC — VC member path in enrich_local_port (line 174)
# =============================================================================
class TestEnrichLocalPortVC:
"""Tests for enrich_local_port when obj.virtual_chassis is truthy."""
def _make_view(self):
from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView
view = object.__new__(BaseCableTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
return view
def test_vc_path_calls_get_virtual_chassis_member(self):
"""VC device → get_virtual_chassis_member called; interface URL set."""
view = self._make_view()
obj = MagicMock()
obj.virtual_chassis = MagicMock() # truthy
mock_interface = MagicMock()
mock_interface.pk = 99
mock_member = MagicMock()
mock_member.interfaces.filter.return_value.first.return_value = mock_interface
link = {"local_port": "Gi0/0", "local_port_id": 10}
with (
patch(
"netbox_librenms_plugin.views.base.cables_view.get_virtual_chassis_member",
return_value=mock_member,
) as mock_vc,
patch(
"netbox_librenms_plugin.views.base.cables_view.reverse",
return_value="/dcim/interfaces/99/",
),
):
view.enrich_local_port(link, obj)
mock_vc.assert_called_once_with(obj, "Gi0/0")
assert link.get("local_port_url") == "/dcim/interfaces/99/"
assert link.get("netbox_local_interface_id") == 99
# =============================================================================
# TestEnrichRemotePort — VC and non-VC paths (lines 190-227)
# =============================================================================
class TestEnrichRemotePort:
"""Tests for enrich_remote_port VC and non-VC paths."""
def _make_view(self):
from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView
view = object.__new__(BaseCableTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
return view
def test_vc_path_finds_by_librenms_id(self):
"""VC device: remote interface found by librenms_id; URL/name/id set."""
view = self._make_view()
device = MagicMock()
device.virtual_chassis = MagicMock() # truthy
mock_interface = MagicMock()
mock_interface.pk = 77
mock_interface.name = "Gi1/0/1"
mock_member = MagicMock()
mock_member.interfaces.filter.return_value.first.return_value = mock_interface
link = {"remote_port": "Gi1/0/1", "remote_port_id": 20}
with (
patch(
"netbox_librenms_plugin.views.base.cables_view.get_virtual_chassis_member",
return_value=mock_member,
),
patch(
"netbox_librenms_plugin.views.base.cables_view.reverse",
return_value="/dcim/interfaces/77/",
),
):
result = view.enrich_remote_port(link, device)
assert result["netbox_remote_interface_id"] == 77
assert result["remote_port_url"] == "/dcim/interfaces/77/"
assert result["remote_port_name"] == "Gi1/0/1"
def test_vc_path_falls_back_to_name_when_librenms_id_miss(self):
"""VC device: librenms_id lookup returns None → falls back to name match."""
view = self._make_view()
device = MagicMock()
device.virtual_chassis = MagicMock() # truthy
mock_interface = MagicMock()
mock_interface.pk = 55
mock_interface.name = "Gi1/0/2"
mock_member = MagicMock()
# remote_port_id=20 (truthy) → librenms_id filter called (returns None),
# then name filter called (returns interface)
mock_member.interfaces.filter.return_value.first.side_effect = [None, mock_interface]
link = {"remote_port": "Gi1/0/2", "remote_port_id": 20}
with (
patch(
"netbox_librenms_plugin.views.base.cables_view.get_virtual_chassis_member",
return_value=mock_member,
),
patch(
"netbox_librenms_plugin.views.base.cables_view.reverse",
return_value="/dcim/interfaces/55/",
),
):
result = view.enrich_remote_port(link, device)
assert result["netbox_remote_interface_id"] == 55
def test_non_vc_path_finds_by_librenms_id(self):
"""Non-VC device: remote interface found by librenms_id; URL/name/id set."""
view = self._make_view()
device = MagicMock()
device.virtual_chassis = None # falsy
mock_interface = MagicMock()
mock_interface.pk = 33
mock_interface.name = "eth0"
device.interfaces.filter.return_value.first.return_value = mock_interface
link = {"remote_port": "eth0", "remote_port_id": 15}
with patch(
"netbox_librenms_plugin.views.base.cables_view.reverse",
return_value="/dcim/interfaces/33/",
):
result = view.enrich_remote_port(link, device)
assert result["netbox_remote_interface_id"] == 33
assert result["remote_port_url"] == "/dcim/interfaces/33/"
assert result["remote_port_name"] == "eth0"
def test_non_vc_path_falls_back_to_name(self):
"""Non-VC device: librenms_id lookup returns None → falls back to name match."""
view = self._make_view()
device = MagicMock()
device.virtual_chassis = None # falsy
mock_interface = MagicMock()
mock_interface.pk = 44
mock_interface.name = "eth1"
# remote_port_id=15 (truthy) → librenms_id filter called first (returns None),
# then name filter called (returns interface)
device.interfaces.filter.return_value.first.side_effect = [None, mock_interface]
link = {"remote_port": "eth1", "remote_port_id": 15}
with patch(
"netbox_librenms_plugin.views.base.cables_view.reverse",
return_value="/dcim/interfaces/44/",
):
result = view.enrich_remote_port(link, device)
assert result["netbox_remote_interface_id"] == 44
def test_no_remote_port_key_returns_none(self):
"""When link has no 'remote_port', method falls through and returns None."""
view = self._make_view()
device = MagicMock()
link = {} # No remote_port key
result = view.enrich_remote_port(link, device)
assert result is None
def test_interface_not_found_does_not_set_url(self):
"""When no remote interface found, url/id keys are not set."""
view = self._make_view()
device = MagicMock()
device.virtual_chassis = None
# Both lookups return None
device.interfaces.filter.return_value.first.return_value = None
link = {"remote_port": "eth2", "remote_port_id": 99}
result = view.enrich_remote_port(link, device)
assert "remote_port_url" not in result
assert "netbox_remote_interface_id" not in result
# =============================================================================
# TestProcessRemoteDevice — found=True and found=False paths (lines 264-283)
# =============================================================================
class TestProcessRemoteDevice:
"""Tests for process_remote_device found=True and found=False paths."""
def _make_view(self):
from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView
view = object.__new__(BaseCableTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
return view
def test_found_true_sets_remote_device_url_and_calls_enrich(self):
"""found=True → sets remote_device_url, netbox_remote_device_id, calls enrich_remote_port."""
view = self._make_view()
mock_device = MagicMock()
mock_device.pk = 5
link = {"remote_port": "Gi0/1", "remote_port_id": None}
with (
patch.object(view, "get_device_by_id_or_name", return_value=(mock_device, True, None)),
patch.object(
view, "enrich_remote_port", side_effect=lambda link, *_args, **_kwargs: dict(link)
) as mock_enrich,
patch(
"netbox_librenms_plugin.views.base.cables_view.reverse",
return_value="/dcim/devices/5/",
),
):
result = view.process_remote_device(link, "switch-b", 99)
assert result["remote_device_url"] == "/dcim/devices/5/"
assert result["netbox_remote_device_id"] == 5
mock_enrich.assert_called_once()
def test_found_false_with_error_message(self):
"""found=False with error_message → cable_status set to the error."""
view = self._make_view()
link = {"remote_port": "Gi0/1", "remote_port_id": None}
with patch.object(
view,
"get_device_by_id_or_name",
return_value=(None, False, "Multiple devices found: 99"),
):
result = view.process_remote_device(link, "switch-b", 99)
assert result["cable_status"] == "Multiple devices found: 99"
assert result["can_create_cable"] is False
def test_found_false_without_error_message_uses_default(self):
"""found=False, error_message=None → cable_status = 'Device Not Found in NetBox'."""
view = self._make_view()
link = {"remote_port": "Gi0/1", "remote_port_id": None}
with patch.object(view, "get_device_by_id_or_name", return_value=(None, False, None)):
result = view.process_remote_device(link, "switch-b", None)
assert result["cable_status"] == "Device Not Found in NetBox"
assert result["can_create_cable"] is False
# =============================================================================
# TestGetTableOverride — BaseCableTableView.get_table (lines 302-305)
# =============================================================================
class TestGetTableOverride:
"""Tests for BaseCableTableView.get_table — sets htmx_url after calling super()."""
def _make_testable_view(self, server_key="default", path="/cables/"):
"""Create a testable subclass that injects a concrete get_table via MRO."""
from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView
mock_table = MagicMock()
class _FakeParent:
def get_table(self, data, obj):
return mock_table
class _TestableCableView(BaseCableTableView, _FakeParent):
pass
view = object.__new__(_TestableCableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = server_key
view.request = _mock_request(path)
return view, mock_table
def test_sets_htmx_url_with_server_key(self):
"""get_table sets htmx_url including server_key when present."""
view, mock_table = self._make_testable_view(server_key="default", path="/cables/")
result = view.get_table([], MagicMock())
assert result is mock_table
assert result.htmx_url == "/cables/?tab=cables&server_key=default"
def test_htmx_url_without_server_key(self):
"""When server_key is falsy, htmx_url has no server_key parameter."""
view, mock_table = self._make_testable_view(server_key=None, path="/cables/")
result = view.get_table([], MagicMock())
assert result.htmx_url == "/cables/?tab=cables"
# =============================================================================
# TestPostHandlerVC — SingleCableVerifyView.post() VC resolution (line 471)
# =============================================================================
class TestPostHandlerVC:
"""Tests for SingleCableVerifyView.post() VC member resolution path."""
def _make_view(self):
from netbox_librenms_plugin.views.base.cables_view import SingleCableVerifyView
view = object.__new__(SingleCableVerifyView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
return view
def test_vc_member_resolution_calls_get_virtual_chassis_member(self):
"""VC device → get_virtual_chassis_member called with device and local_port."""
import json
view = self._make_view()
mock_request = MagicMock()
mock_request.body = json.dumps(
{
"device_id": 1,
"local_port_id": 10,
"server_key": "default",
}
).encode()
mock_device = MagicMock()
mock_device.virtual_chassis = MagicMock() # truthy
mock_device.id = 1
mock_member = MagicMock()
mock_interface = MagicMock()
mock_interface.pk = 99
# librenms_id lookup returns the interface
mock_member.interfaces.filter.return_value.first.return_value = mock_interface
cached_links = {
"links": [
{
"local_port_id": 10,
"local_port": "Gi0/0",
"remote_port": "Gi0/1",
"remote_device": "switch-b",
"remote_port_id": 20,
"remote_device_id": 99,
}
]
}
with (
patch(
"netbox_librenms_plugin.views.base.cables_view.get_object_or_404",
return_value=mock_device,
),
patch(
"netbox_librenms_plugin.views.base.cables_view.get_librenms_sync_device",
return_value=mock_device,
),
patch("netbox_librenms_plugin.views.base.cables_view.cache") as mock_cache,
patch.object(view, "get_cache_key", return_value="test-key"),
patch(
"netbox_librenms_plugin.views.base.cables_view.get_virtual_chassis_member",
return_value=mock_member,
) as mock_vc,
patch.object(
view,
"process_remote_device",
return_value={
"local_port": "Gi0/0",
"remote_port": "Gi0/1",
"remote_device": "switch-b",
"remote_port_id": 20,
"remote_device_id": 99,
},
) as mock_process_remote,
patch(
"netbox_librenms_plugin.views.base.cables_view.reverse",
return_value="/interface/99/",
),
patch(
"netbox_librenms_plugin.views.base.cables_view.get_token",
return_value="csrf-token",
),
patch(
"netbox_librenms_plugin.views.base.cables_view.escape",
side_effect=lambda x: x,
),
):
mock_cache.get.return_value = cached_links
view.post(mock_request)
mock_vc.assert_called_once_with(mock_device, "Gi0/0")
# Verify server_key is forwarded to process_remote_device
assert mock_process_remote.called
call_kwargs = mock_process_remote.call_args[1]
assert call_kwargs.get("server_key") == "default"
# =============================================================================
# TestPostHandlerInterfaceNotFound — lines 534-561
# =============================================================================
class TestPostHandlerInterfaceNotFound:
"""Tests for SingleCableVerifyView.post() interface-not-found and cable_url branches."""
def _make_view(self):
from netbox_librenms_plugin.views.base.cables_view import SingleCableVerifyView
view = object.__new__(SingleCableVerifyView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
return view
def test_interface_not_found_fills_formatted_row(self):
"""When no local interface found, formatted_row reflects missing interface."""
import json as json_mod
view = self._make_view()
mock_request = MagicMock()
mock_request.body = json_mod.dumps(
{
"device_id": 1,
"local_port_id": 10,
"server_key": "default",
}
).encode()
mock_device = MagicMock()
mock_device.virtual_chassis = None # non-VC
mock_device.id = 1
# Both interface lookups return None
mock_device.interfaces.filter.return_value.first.return_value = None
cached_links = {
"links": [
{
"local_port_id": 10,
"local_port": "Gi0/0",
"remote_port": "Gi0/1",
"remote_device": "switch-b",
"remote_port_id": 20,
"remote_device_id": 99,
}
]
}
process_result = {
"local_port": "Gi0/0",
"remote_port": "Gi0/1",
"remote_device": "switch-b",
"remote_port_id": 20,
"remote_device_id": 99,
"remote_device_url": "/device/5/",
"remote_port_url": "/interface/20/",
"remote_port_name": "Gi0/1",
}
with (
patch(
"netbox_librenms_plugin.views.base.cables_view.get_object_or_404",
return_value=mock_device,
),
patch(
"netbox_librenms_plugin.views.base.cables_view.get_librenms_sync_device",
return_value=mock_device,
),
patch("netbox_librenms_plugin.views.base.cables_view.cache") as mock_cache,
patch.object(view, "get_cache_key", return_value="test-key"),
patch.object(view, "process_remote_device", return_value=process_result),
patch(
"netbox_librenms_plugin.views.base.cables_view.escape",
side_effect=lambda x: x,
),
):
mock_cache.get.return_value = cached_links
response = view.post(mock_request)
import json as json_mod2
data = json_mod2.loads(response.content)
assert data["status"] == "success"
row = data["formatted_row"]
# local_port is text (not a link) because interface was not found
assert row["local_port"] == "Gi0/0"
assert "cable_status" in row
def test_cable_url_present_wraps_cable_status_in_anchor(self):
"""When cable_url is in link_data, cable_status is wrapped in an <a> tag (line 514)."""
import json as json_mod
view = self._make_view()
mock_request = MagicMock()
mock_request.body = json_mod.dumps(
{
"device_id": 1,
"local_port_id": 10,
"server_key": "default",
}
).encode()
mock_device = MagicMock()
mock_device.virtual_chassis = None
mock_device.id = 1
mock_interface = MagicMock()
mock_interface.pk = 99
# librenms_id lookup returns the interface; name lookup not needed
mock_device.interfaces.filter.return_value.first.side_effect = [mock_interface, None]
cached_links = {
"links": [
{
"local_port_id": 10,
"local_port": "Gi0/0",
"remote_port": "Gi0/1",
"remote_device": "switch-b",
"remote_port_id": 20,
"remote_device_id": 99,
}
]
}
process_result = {
"local_port": "Gi0/0",
"remote_port": "Gi0/1",
"remote_device": "switch-b",
"remote_port_id": 20,
"remote_device_id": 99,
"netbox_remote_device_id": 5,
"remote_device_url": "/device/5/",
"remote_port_url": "/interface/20/",
"remote_port_name": "Gi0/1",
"cable_status": "Cable Found",
"cable_url": "/dcim/cables/42/",
"can_create_cable": False,
}
with (
patch(
"netbox_librenms_plugin.views.base.cables_view.get_object_or_404",
return_value=mock_device,
),
patch(
"netbox_librenms_plugin.views.base.cables_view.get_librenms_sync_device",
return_value=mock_device,
),
patch("netbox_librenms_plugin.views.base.cables_view.cache") as mock_cache,
patch.object(view, "get_cache_key", return_value="test-key"),
patch.object(view, "process_remote_device", return_value=process_result),
patch.object(view, "check_cable_status", return_value=process_result),
patch(
"netbox_librenms_plugin.views.base.cables_view.reverse",
return_value="/dcim/interfaces/99/",
),
patch(
"netbox_librenms_plugin.views.base.cables_view.escape",
side_effect=lambda x: x,
),
patch(
"netbox_librenms_plugin.views.base.cables_view.get_token",
return_value="csrf-token",
),
):
mock_cache.get.return_value = cached_links
response = view.post(mock_request)
import json as json_mod2
data = json_mod2.loads(response.content)
assert data["status"] == "success"
# cable_url was present → cable_status should be wrapped in an anchor tag
cable_status = data["formatted_row"]["cable_status"]
assert '<a href="/dcim/cables/42/">' in cable_status
# =============================================================================
# TestIpAddressViewMethods — get_object (line 27), get_ip_addresses (lines 31-32)
# =============================================================================
class TestIpAddressViewMethods:
"""Tests for BaseIPAddressTableView.get_object and get_ip_addresses."""
def _make_view(self):
from netbox_librenms_plugin.views.base.ip_addresses_view import BaseIPAddressTableView
view = object.__new__(BaseIPAddressTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
view.model = MagicMock()
return view
def test_get_object_calls_get_object_or_404(self):
"""get_object delegates to get_object_or_404 with the view's model."""
view = self._make_view()
mock_device = MagicMock()
with patch(
"netbox_librenms_plugin.views.base.ip_addresses_view.get_object_or_404",
return_value=mock_device,
) as mock_get:
result = view.get_object(42)
mock_get.assert_called_once_with(view.model, pk=42)
assert result is mock_device
def test_get_ip_addresses_calls_api(self):
"""get_ip_addresses calls get_librenms_id then get_device_ips; stores librenms_id."""
view = self._make_view()
view._librenms_api.get_librenms_id.return_value = 99
view._librenms_api.get_device_ips.return_value = (True, [{"port_id": 1}])
obj = _mock_obj()
result = view.get_ip_addresses(obj)
view._librenms_api.get_librenms_id.assert_called_once_with(obj)
view._librenms_api.get_device_ips.assert_called_once_with(99)
assert result == (True, [{"port_id": 1}])
assert view.librenms_id == 99
# =============================================================================
# TestEnrichIpDataPortInfo — port_info truthy branch (lines 68-69)
# =============================================================================
class TestEnrichIpDataPortInfo:
"""Tests for enrich_ip_data when port_info is truthy → sets enriched_ip['interface_name']."""
def _make_view(self):
from netbox_librenms_plugin.views.base.ip_addresses_view import BaseIPAddressTableView
view = object.__new__(BaseIPAddressTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
return view
def test_port_info_truthy_sets_interface_name(self):
"""When _get_port_info returns a dict, interface_name is set from it."""
view = self._make_view()
ip_data = [{"port_id": 1, "ip_address": "10.0.0.1", "prefix_length": 24}]
obj = _mock_obj()
obj.get_absolute_url.return_value = "/device/1/"
port_info = {"ifName": "Gi0/0"}
base_entry = {
"ip_address": "10.0.0.1",
"prefix_length": 24,
"ip_with_mask": "10.0.0.1/24",
"port_id": 1,
"device": "test-device",
"device_url": "/device/1/",
"vrf_id": None,
"vrfs": [],
}
prefetched = {
"interfaces_by_librenms_id": {},
"interfaces_by_name": {},
"all_interfaces": [],
"device": obj,
"ip_addresses_map": {},
"vrfs": [],
}
with (
patch.object(view, "_prefetch_netbox_data", return_value=prefetched),
patch.object(view, "_get_port_info", return_value=port_info),
patch.object(view, "_create_base_ip_entry", return_value=dict(base_entry)),
patch.object(view, "_add_interface_info_to_ip"),
):
result = view.enrich_ip_data(ip_data, obj, "ifName")
assert len(result) == 1
assert result[0]["interface_name"] == "Gi0/0"
def test_port_info_none_does_not_set_interface_name(self):
"""When _get_port_info returns None, interface_name is not set from it."""
view = self._make_view()
ip_data = [{"port_id": 1, "ip_address": "10.0.0.1", "prefix_length": 24}]
obj = _mock_obj()
obj.get_absolute_url.return_value = "/device/1/"
base_entry = {
"ip_address": "10.0.0.1",
"prefix_length": 24,
"ip_with_mask": "10.0.0.1/24",
"port_id": 1,
"device": "test-device",
"device_url": "/device/1/",
"vrf_id": None,
"vrfs": [],
}
prefetched = {
"interfaces_by_librenms_id": {},
"interfaces_by_name": {},
"all_interfaces": [],
"device": obj,
"ip_addresses_map": {},
"vrfs": [],
}
with (
patch.object(view, "_prefetch_netbox_data", return_value=prefetched),
patch.object(view, "_get_port_info", return_value=None),
patch.object(view, "_create_base_ip_entry", return_value=dict(base_entry)),
patch.object(view, "_add_interface_info_to_ip"),
):
result = view.enrich_ip_data(ip_data, obj, "ifName")
assert len(result) == 1
assert "interface_name" not in result[0]
# =============================================================================
# TestPreparContextInterfaceNameFieldNone — line 237
# =============================================================================
class TestPrepareContextInterfaceNameFieldNone:
"""Tests for _prepare_context when interface_name_field is None (line 237)."""
def _make_view(self):
from netbox_librenms_plugin.views.base.ip_addresses_view import BaseIPAddressTableView
view = object.__new__(BaseIPAddressTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
return view
def test_calls_get_interface_name_field_when_none(self):
"""When interface_name_field=None, _prepare_context calls get_interface_name_field."""
view = self._make_view()
obj = _mock_obj()
request = _mock_request()
with (
patch("netbox_librenms_plugin.views.base.ip_addresses_view.cache") as mock_cache,
patch.object(view, "get_cache_key", return_value="test-key"),
patch(
"netbox_librenms_plugin.views.base.ip_addresses_view.get_interface_name_field",
return_value="ifName",
) as mock_gif,
):
mock_cache.get.return_value = None # no cached data → returns None early
result = view._prepare_context(request, obj, None, fetch_fresh=False)
mock_gif.assert_called_once_with(request)
assert result is None # returns None because cache miss
# =============================================================================
# TestSingleIPAddressVerifyViewGetObject — _get_object (lines 325-339)
# =============================================================================
class TestSingleIPAddressVerifyViewGetObject:
"""Tests for SingleIPAddressVerifyView._get_object."""
def _make_view(self):
from netbox_librenms_plugin.views.base.ip_addresses_view import SingleIPAddressVerifyView
view = object.__new__(SingleIPAddressVerifyView)
return view
def test_device_type_calls_get_object_or_404_for_device(self):
"""object_type='device' → get_object_or_404(Device, pk=object_id)."""
from dcim.models import Device
view = self._make_view()
mock_device = MagicMock()
with patch(
"netbox_librenms_plugin.views.base.ip_addresses_view.get_object_or_404",
return_value=mock_device,
) as mock_get:
result = view._get_object(1, "device")
mock_get.assert_called_once_with(Device, pk=1)
assert result is mock_device
def test_vm_type_calls_get_object_or_404_for_vm(self):
"""object_type='virtualmachine' → get_object_or_404(VirtualMachine, pk=object_id)."""
from virtualization.models import VirtualMachine
view = self._make_view()
mock_vm = MagicMock()
with patch(
"netbox_librenms_plugin.views.base.ip_addresses_view.get_object_or_404",
return_value=mock_vm,
) as mock_get:
result = view._get_object(2, "virtualmachine")
mock_get.assert_called_once_with(VirtualMachine, pk=2)
assert result is mock_vm
def test_no_type_finds_device(self):
"""No type given → tries Device.objects.filter; returns device when found."""
view = self._make_view()
mock_device = MagicMock()
with patch("netbox_librenms_plugin.views.base.ip_addresses_view.Device") as MockDevice:
MockDevice.objects.filter.return_value.first.return_value = mock_device
result = view._get_object(1, None)
assert result is mock_device
def test_no_type_device_not_found_tries_vm(self):
"""No type, Device not found → tries VirtualMachine; returns VM when found."""
view = self._make_view()
mock_vm = MagicMock()
with (
patch("netbox_librenms_plugin.views.base.ip_addresses_view.Device") as MockDevice,
patch("netbox_librenms_plugin.views.base.ip_addresses_view.VirtualMachine") as MockVM,
):
MockDevice.objects.filter.return_value.first.return_value = None
MockVM.objects.filter.return_value.first.return_value = mock_vm
result = view._get_object(2, None)
assert result is mock_vm
def test_no_type_neither_found_raises_http404(self):
"""No type, nothing found → raises Http404."""
from django.http import Http404
view = self._make_view()
with (
patch("netbox_librenms_plugin.views.base.ip_addresses_view.Device") as MockDevice,
patch("netbox_librenms_plugin.views.base.ip_addresses_view.VirtualMachine") as MockVM,
):
MockDevice.objects.filter.return_value.first.return_value = None
MockVM.objects.filter.return_value.first.return_value = None
try:
view._get_object(99, None)
assert False, "Expected Http404"
except Http404:
pass
# =============================================================================
# TestSingleIPAddressVerifyViewParseIp — _parse_ip_address (lines 346-356)
# =============================================================================
class TestSingleIPAddressVerifyViewParseIp:
"""Tests for SingleIPAddressVerifyView._parse_ip_address."""
def _make_view(self):
from netbox_librenms_plugin.views.base.ip_addresses_view import SingleIPAddressVerifyView
return object.__new__(SingleIPAddressVerifyView)
def test_valid_ipv4_with_prefix(self):
"""'192.168.1.1/24' → ('192.168.1.1', 24)."""
view = self._make_view()
addr, prefix = view._parse_ip_address("192.168.1.1/24")
assert addr == "192.168.1.1"
assert prefix == 24
def test_valid_ipv6_with_prefix(self):
"""'2001:db8::1/64' → ('2001:db8::1', 64)."""
view = self._make_view()
addr, prefix = view._parse_ip_address("2001:db8::1/64")
assert addr == "2001:db8::1"
assert prefix == 64
def test_invalid_prefix_raises_value_error(self):
"""'192.168.1.1/abc' → ValueError with 'Invalid prefix length'."""
view = self._make_view()
try:
view._parse_ip_address("192.168.1.1/abc")
assert False, "Expected ValueError"
except ValueError as exc:
assert "Invalid prefix length" in str(exc)
def test_missing_prefix_raises_value_error(self):
"""'192.168.1.1' (no slash) → ValueError with 'Prefix length is missing'."""
view = self._make_view()
try:
view._parse_ip_address("192.168.1.1")
assert False, "Expected ValueError"
except ValueError as exc:
assert "Prefix length is missing" in str(exc)
# =============================================================================
# TestSingleIPAddressVerifyViewFindInCache — _find_in_cache (lines 360-367)
# =============================================================================
class TestSingleIPAddressVerifyViewFindInCache:
"""Tests for SingleIPAddressVerifyView._find_in_cache."""
def _make_view(self):
from netbox_librenms_plugin.views.base.ip_addresses_view import SingleIPAddressVerifyView
return object.__new__(SingleIPAddressVerifyView)
def test_no_cached_data_returns_triple_none(self):
"""cached_data=None → (None, None, None)."""
view = self._make_view()
result = view._find_in_cache(None, "192.168.1.1", 24)
assert result == (None, None, None)
def test_empty_cache_returns_triple_none(self):
"""cached_data with no ip_addresses → (None, None, None)."""
view = self._make_view()
result = view._find_in_cache({"ip_addresses": []}, "192.168.1.1", 24)
assert result == (None, None, None)
def test_match_returns_entry_vrf_id_port_id(self):
"""Matching entry → (entry, vrf_id, port_id)."""
view = self._make_view()
entry = {"ip_address": "192.168.1.1", "prefix_length": 24, "vrf_id": 5, "port_id": 10}
cached = {"ip_addresses": [entry]}
ip_entry, vrf_id, port_id = view._find_in_cache(cached, "192.168.1.1", 24)
assert ip_entry is entry
assert vrf_id == 5
assert port_id == 10
def test_no_match_returns_triple_none(self):
"""Entries present but no match → (None, None, None)."""
view = self._make_view()
entry = {"ip_address": "10.0.0.1", "prefix_length": 16, "vrf_id": None, "port_id": 1}
cached = {"ip_addresses": [entry]}
result = view._find_in_cache(cached, "192.168.1.1", 24)
assert result == (None, None, None)
# =============================================================================
# TestSingleIPAddressVerifyViewFindExistingIp — _find_existing_ip (lines 373-387)
# =============================================================================
class TestSingleIPAddressVerifyViewFindExistingIp:
"""Tests for SingleIPAddressVerifyView._find_existing_ip."""
def _make_view(self):
from netbox_librenms_plugin.views.base.ip_addresses_view import SingleIPAddressVerifyView
return object.__new__(SingleIPAddressVerifyView)
def test_ip_not_found_returns_false_false_none(self):
"""IP not in NetBox → (False, False, None)."""
view = self._make_view()
with patch("netbox_librenms_plugin.views.base.ip_addresses_view.IPAddress") as MockIP:
MockIP.objects.filter.return_value.first.return_value = None
result = view._find_existing_ip("192.168.1.1", 24, vrf_id=None)
assert result == (False, False, None)
def test_ip_found_with_vrf_id_checks_specific_vrf(self):
"""IP exists; vrf_id given → queries for specific VRF membership."""
view = self._make_view()
mock_ip = MagicMock()
mock_ip.get_absolute_url.return_value = "/ip/1/"
with patch("netbox_librenms_plugin.views.base.ip_addresses_view.IPAddress") as MockIP:
MockIP.objects.filter.return_value.first.return_value = mock_ip
MockIP.objects.filter.return_value.exists.return_value = True
exists_any, exists_vrf, url = view._find_existing_ip("192.168.1.1", 24, vrf_id=5)
assert exists_any is True
assert exists_vrf is True
assert url == "/ip/1/"
# Verify VRF-scoped second query was made
MockIP.objects.filter.assert_any_call(address="192.168.1.1/24", vrf__id=5)
def test_ip_found_without_vrf_id_checks_global(self):
"""IP exists; vrf_id=None → queries for global VRF (vrf__isnull=True)."""
view = self._make_view()
mock_ip = MagicMock()
mock_ip.get_absolute_url.return_value = "/ip/2/"
with patch("netbox_librenms_plugin.views.base.ip_addresses_view.IPAddress") as MockIP:
MockIP.objects.filter.return_value.first.return_value = mock_ip
MockIP.objects.filter.return_value.exists.return_value = True
exists_any, exists_vrf, url = view._find_existing_ip("10.0.0.1", 8, vrf_id=None)
assert exists_any is True
assert exists_vrf is True
assert url == "/ip/2/"
# Verify global VRF second query was made
MockIP.objects.filter.assert_any_call(address="10.0.0.1/8", vrf__isnull=True)
# =============================================================================
# TestSingleIPAddressVerifyViewDetermineStatus — _determine_status (lines 393-404)
# =============================================================================
class TestSingleIPAddressVerifyViewDetermineStatus:
"""Tests for SingleIPAddressVerifyView._determine_status."""
def _make_view(self):
from netbox_librenms_plugin.views.base.ip_addresses_view import SingleIPAddressVerifyView
return object.__new__(SingleIPAddressVerifyView)
def test_exists_and_in_specific_vrf_returns_matched(self):
"""IP exists AND is in the specified VRF → 'matched'."""
view = self._make_view()
result = view._determine_status(True, True, None, 5)
assert result == "matched"
def test_exists_not_in_specific_vrf_returns_update(self):
"""IP exists but NOT in the specified VRF → 'update'."""
view = self._make_view()
result = view._determine_status(True, False, None, 5)
assert result == "update"
def test_not_exists_restoring_original_vrf_returns_matched(self):
"""IP doesn't exist; original_vrf_id == vrf_id → 'matched' (restoring original)."""
view = self._make_view()
result = view._determine_status(False, False, 5, 5)
assert result == "matched"
def test_not_exists_different_vrf_returns_sync(self):
"""IP doesn't exist; vrf_id differs from original → 'sync'."""
view = self._make_view()
result = view._determine_status(False, False, 3, 5)
assert result == "sync"
def test_not_exists_no_original_vrf_returns_sync(self):
"""IP doesn't exist; original_vrf_id=None → 'sync'."""
view = self._make_view()
result = view._determine_status(False, False, None, None)
assert result == "sync"
# =============================================================================
# TestSingleIPAddressVerifyViewPost — post() method (lines 410-495)
# =============================================================================
class TestSingleIPAddressVerifyViewPost:
"""Tests for SingleIPAddressVerifyView.post()."""
def _make_view(self):
from netbox_librenms_plugin.views.base.ip_addresses_view import SingleIPAddressVerifyView
view = object.__new__(SingleIPAddressVerifyView)
# CacheMixin needs server_key attr indirectly via get_cache_key
view._librenms_api = MagicMock()
return view
def test_no_ip_address_returns_400(self):
"""Missing ip_address → JsonResponse 400."""
import json as json_mod
view = self._make_view()
req = MagicMock()
req.body = json_mod.dumps({"device_id": 1}).encode()
response = view.post(req)
assert response.status_code == 400
data = json_mod.loads(response.content)
assert data["status"] == "error"
def test_no_object_id_returns_400(self):
"""Missing device_id → JsonResponse 400."""
import json as json_mod
view = self._make_view()
req = MagicMock()
req.body = json_mod.dumps({"ip_address": "10.0.0.1/24"}).encode()
response = view.post(req)
assert response.status_code == 400
def test_http404_on_get_object_returns_404(self):
"""When _get_object raises Http404 → JsonResponse 404."""
import json as json_mod
from django.http import Http404
view = self._make_view()
req = MagicMock()
req.body = json_mod.dumps({"ip_address": "10.0.0.1/24", "device_id": 999}).encode()
with patch.object(view, "_get_object", side_effect=Http404("not found")):
response = view.post(req)
assert response.status_code == 404
def test_invalid_ip_parse_returns_400(self):
"""ValueError from _parse_ip_address → JsonResponse 400."""
import json as json_mod
view = self._make_view()
req = MagicMock()
req.body = json_mod.dumps({"ip_address": "bad-ip", "device_id": 1}).encode()
mock_obj = MagicMock()
with patch.object(view, "_get_object", return_value=mock_obj):
with patch.object(view, "_parse_ip_address", side_effect=ValueError("Prefix length is missing")):
response = view.post(req)
assert response.status_code == 400
def test_success_returns_formatted_row(self):
"""Valid request → JsonResponse 200 with status, ip_address, formatted_row."""
import json as json_mod
view = self._make_view()
req = MagicMock()
req.body = json_mod.dumps(
{
"ip_address": "10.0.0.1/24",
"device_id": 1,
"vrf_id": None,
"server_key": "default",
}
).encode()
mock_obj = MagicMock()
mock_obj.name = "device1"
mock_obj.get_absolute_url.return_value = "/device/1/"
mock_obj.interfaces.first.return_value = None
with (
patch.object(view, "_get_object", return_value=mock_obj),
patch.object(view, "_parse_ip_address", return_value=("10.0.0.1", 24)),
patch.object(view, "get_cache_key", return_value="cache-key"),
patch("netbox_librenms_plugin.views.base.ip_addresses_view.cache") as mock_cache,
patch.object(view, "_find_in_cache", return_value=(None, None, None)),
patch.object(view, "_find_existing_ip", return_value=(False, False, None)),
patch.object(view, "_determine_status", return_value="sync"),
patch("netbox_librenms_plugin.views.base.ip_addresses_view.IPAddressTable") as MockTable,
):
mock_cache.get.return_value = None
mock_table_instance = MagicMock()
mock_table_instance.render_status.return_value = "<span>sync</span>"
MockTable.return_value = mock_table_instance
response = view.post(req)
assert response.status_code == 200
data = json_mod.loads(response.content)
assert data["status"] == "success"
assert data["ip_address"] == "10.0.0.1/24"
assert "formatted_row" in data
def test_success_with_cache_entry_updates_record(self):
"""When cache has an entry for the IP, updated_record is enriched with it."""
import json as json_mod
view = self._make_view()
req = MagicMock()
req.body = json_mod.dumps(
{
"ip_address": "10.0.0.1/24",
"device_id": 1,
"vrf_id": None,
"server_key": "default",
}
).encode()
mock_obj = MagicMock()
mock_obj.name = "device1"
mock_obj.get_absolute_url.return_value = "/device/1/"
cache_entry = {
"ip_address": "10.0.0.1",
"prefix_length": 24,
"interface_name": "eth0",
"interface_url": "/interface/1/",
"vrf_id": 5,
"status": "update",
}
with (
patch.object(view, "_get_object", return_value=mock_obj),
patch.object(view, "_parse_ip_address", return_value=("10.0.0.1", 24)),
patch.object(view, "get_cache_key", return_value="cache-key"),
patch("netbox_librenms_plugin.views.base.ip_addresses_view.cache") as mock_cache,
patch.object(
view,
"_find_in_cache",
return_value=(cache_entry, 5, 10),
),
patch.object(view, "_find_existing_ip", return_value=(True, True, "/ip/1/")),
patch.object(view, "_determine_status", return_value="matched"),
patch("netbox_librenms_plugin.views.base.ip_addresses_view.IPAddressTable") as MockTable,
):
mock_cache.get.return_value = {"ip_addresses": [cache_entry]}
mock_table_instance = MagicMock()
mock_table_instance.render_status.return_value = "<span>matched</span>"
MockTable.return_value = mock_table_instance
response = view.post(req)
assert response.status_code == 200
data = json_mod.loads(response.content)
assert data["status"] == "success"
# Verify cache entry fields (interface_name, interface_url) were merged into
# the updated_record that is passed to render_status
assert mock_table_instance.render_status.call_count == 1
rendered_record = mock_table_instance.render_status.call_args[0][1]
assert rendered_record["interface_name"] == "eth0"
assert rendered_record["interface_url"] == "/interface/1/"
def test_invalid_json_returns_400(self):
"""Malformed JSON body → JsonResponse 400."""
import json as json_mod
view = self._make_view()
req = MagicMock()
req.body = b"not-json" # will cause json.loads to fail
response = view.post(req)
assert response.status_code == 400
data = json_mod.loads(response.content)
assert data["status"] == "error"
def test_interface_from_device_used_when_no_cache(self):
"""When no cache entry (port_id=None), first device interface is used."""
import json as json_mod
view = self._make_view()
req = MagicMock()
req.body = json_mod.dumps(
{
"ip_address": "10.0.0.1/24",
"device_id": 1,
"vrf_id": None,
"server_key": "default",
}
).encode()
mock_obj = MagicMock()
mock_obj.name = "device1"
mock_obj.get_absolute_url.return_value = "/device/1/"
mock_iface = MagicMock()
mock_iface.name = "eth0"
mock_iface.get_absolute_url.return_value = "/interface/1/"
mock_obj.interfaces.first.return_value = mock_iface
with (
patch.object(view, "_get_object", return_value=mock_obj),
patch.object(view, "_parse_ip_address", return_value=("10.0.0.1", 24)),
patch.object(view, "get_cache_key", return_value="cache-key"),
patch("netbox_librenms_plugin.views.base.ip_addresses_view.cache") as mock_cache,
patch.object(
view,
"_find_in_cache",
# Truthy cache_entry whose port_id (third element) is None
# → both cache enrichment and interfaces.first() fallback run
return_value=({"ip_address": "10.0.0.1", "prefix_length": 24}, None, None),
),
patch.object(view, "_find_existing_ip", return_value=(False, False, None)),
patch.object(view, "_determine_status", return_value="sync"),
patch("netbox_librenms_plugin.views.base.ip_addresses_view.IPAddressTable") as MockTable,
):
mock_cache.get.return_value = None
mock_table_instance = MagicMock()
mock_table_instance.render_status.return_value = "<span>sync</span>"
MockTable.return_value = mock_table_instance
response = view.post(req)
assert response.status_code == 200
# Cache entry was truthy but had no port_id → first device interface used
mock_obj.interfaces.first.assert_called_once()
def test_verify_with_non_default_server_key(self):
"""server_key='secondary' propagates to get_cache_key call."""
import json as json_mod
view = self._make_view()
req = MagicMock()
req.body = json_mod.dumps(
{
"ip_address": "192.168.1.1/24",
"device_id": 2,
"vrf_id": None,
"server_key": "secondary",
}
).encode()
mock_obj = MagicMock()
mock_obj.name = "device2"
mock_obj.get_absolute_url.return_value = "/device/2/"
mock_obj.interfaces.first.return_value = None
with (
patch.object(view, "_get_object", return_value=mock_obj),
patch.object(view, "_parse_ip_address", return_value=("192.168.1.1", 24)),
patch.object(view, "get_cache_key", return_value="secondary-cache-key") as mock_get_cache_key,
patch("netbox_librenms_plugin.views.base.ip_addresses_view.cache") as mock_cache,
patch.object(view, "_find_in_cache", return_value=(None, None, None)),
patch.object(view, "_find_existing_ip", return_value=(False, False, None)),
patch.object(view, "_determine_status", return_value="sync"),
patch("netbox_librenms_plugin.views.base.ip_addresses_view.IPAddressTable") as MockTable,
):
mock_cache.get.return_value = None
mock_table_instance = MagicMock()
mock_table_instance.render_status.return_value = "<span>sync</span>"
MockTable.return_value = mock_table_instance
response = view.post(req)
assert response.status_code == 200
mock_get_cache_key.assert_called_once_with(mock_obj, "ip_addresses", "secondary")
mock_cache.get.assert_called_once_with("secondary-cache-key")
# =============================================================================
# TestGetDeviceByIdOrNameLine124 — librenms_id DoesNotExist fallthrough (line 124)
# =============================================================================
class TestGetDeviceByIdOrNameLine124:
"""Test that DoesNotExist on librenms_id lookup falls through to name lookup (line 124)."""
def test_librenms_id_doesnotexist_falls_through_to_name(self):
"""remote_device_id provided but DoesNotExist → falls through to name match."""
from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView
view = object.__new__(BaseCableTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
mock_device = MagicMock()
with patch("netbox_librenms_plugin.views.base.cables_view.Device") as MockDevice:
class _DoesNotExist(Exception):
pass
MockDevice.DoesNotExist = _DoesNotExist
# First call: librenms_id lookup → DoesNotExist (line 124: pass)
# Second call: name lookup → success
MockDevice.objects.get.side_effect = [_DoesNotExist, mock_device]
device, found, error = view.get_device_by_id_or_name(42, "switch-a")
assert found is True
assert device is mock_device
# =============================================================================
# TestGetDeviceByIdOrNameSimpleHostnameMultiple — lines 144-145
# =============================================================================
class TestGetDeviceByIdOrNameSimpleHostnameMultiple:
"""MultipleObjectsReturned when searching by simple hostname (lines 144-145)."""
def test_simple_hostname_multiple_returns_error(self):
"""FQDN DoesNotExist, simple hostname raises MultipleObjectsReturned → (None, False, msg)."""
from django.core.exceptions import MultipleObjectsReturned
from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView
view = object.__new__(BaseCableTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
with patch("netbox_librenms_plugin.views.base.cables_view.Device") as MockDevice:
class _DoesNotExist(Exception):
pass
MockDevice.DoesNotExist = _DoesNotExist
# remote_device_id=None → skip librenms_id
# First get() (FQDN) → DoesNotExist
# Second get() (simple hostname) → MultipleObjectsReturned
MockDevice.objects.get.side_effect = [_DoesNotExist, MultipleObjectsReturned]
device, found, error = view.get_device_by_id_or_name(None, "switch.example.com")
assert device is None
assert found is False
assert error is not None
assert "switch.example.com" in error
# =============================================================================
# TestEnrichLocalPortVCNameFallback — line 174 (VC name fallback)
# =============================================================================
class TestEnrichLocalPortVCNameFallback:
"""Tests for enrich_local_port VC path name fallback when librenms_id miss (line 174)."""
def test_vc_name_fallback_when_librenms_id_miss(self):
"""VC path: librenms_id lookup returns None → falls back to name lookup (line 174)."""
from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView
view = object.__new__(BaseCableTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
obj = MagicMock()
obj.virtual_chassis = MagicMock() # truthy
mock_interface = MagicMock()
mock_interface.pk = 88
mock_member = MagicMock()
# librenms_id lookup (first .first()) returns None → triggers name fallback
# name lookup (second .first()) returns the interface
mock_member.interfaces.filter.return_value.first.side_effect = [None, mock_interface]
link = {"local_port": "Gi0/0", "local_port_id": 10}
with (
patch(
"netbox_librenms_plugin.views.base.cables_view.get_virtual_chassis_member",
return_value=mock_member,
),
patch(
"netbox_librenms_plugin.views.base.cables_view.reverse",
return_value="/dcim/interfaces/88/",
),
):
view.enrich_local_port(link, obj)
assert link.get("netbox_local_interface_id") == 88
def test_vc_no_local_port_id_goes_straight_to_name(self):
"""VC path with local_port_id=None → skips librenms_id, goes to name lookup (line 174)."""
from netbox_librenms_plugin.views.base.cables_view import BaseCableTableView
view = object.__new__(BaseCableTableView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
obj = MagicMock()
obj.virtual_chassis = MagicMock() # truthy
mock_interface = MagicMock()
mock_interface.pk = 77
mock_member = MagicMock()
mock_member.interfaces.filter.return_value.first.return_value = mock_interface
# local_port_id=None → `if local_port_id:` is False → skips librenms_id
# → goes directly to line 174 (name lookup)
link = {"local_port": "Gi0/0", "local_port_id": None}
with (
patch(
"netbox_librenms_plugin.views.base.cables_view.get_virtual_chassis_member",
return_value=mock_member,
),
patch(
"netbox_librenms_plugin.views.base.cables_view.reverse",
return_value="/dcim/interfaces/77/",
),
):
view.enrich_local_port(link, obj)
assert link.get("netbox_local_interface_id") == 77
# =============================================================================
# TestPostHandlerCanCreateCable — lines 519-525 (can_create_cable form)
# =============================================================================
class TestPostHandlerCanCreateCable:
"""Tests for SingleCableVerifyView.post() can_create_cable branch (lines 519-525)."""
def _make_view(self):
from netbox_librenms_plugin.views.base.cables_view import SingleCableVerifyView
view = object.__new__(SingleCableVerifyView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
return view
def test_can_create_cable_adds_form_action(self):
"""can_create_cable=True → formatted_row['actions'] contains form."""
import json as json_mod
view = self._make_view()
mock_request = MagicMock()
mock_request.body = json_mod.dumps(
{
"device_id": 1,
"local_port_id": 10,
"server_key": "default",
}
).encode()
mock_device = MagicMock()
mock_device.virtual_chassis = None
mock_device.id = 1
mock_interface = MagicMock()
mock_interface.pk = 99
mock_device.interfaces.filter.return_value.first.side_effect = [mock_interface, None]
cached_links = {
"links": [
{
"local_port_id": 10,
"local_port": "Gi0/0",
"remote_port": "Gi0/1",
"remote_device": "switch-b",
"remote_port_id": 20,
"remote_device_id": 99,
}
]
}
process_result = {
"local_port": "Gi0/0",
"remote_port": "Gi0/1",
"remote_device": "switch-b",
"remote_port_id": 20,
"remote_device_id": 99,
"netbox_remote_device_id": 5,
"remote_device_url": "/device/5/",
"remote_port_url": "/interface/20/",
"remote_port_name": "Gi0/1",
"cable_status": "No Cable",
"can_create_cable": True, # triggers lines 519-525
}
with (
patch(
"netbox_librenms_plugin.views.base.cables_view.get_object_or_404",
return_value=mock_device,
),
patch(
"netbox_librenms_plugin.views.base.cables_view.get_librenms_sync_device",
return_value=mock_device,
),
patch("netbox_librenms_plugin.views.base.cables_view.cache") as mock_cache,
patch.object(view, "get_cache_key", return_value="test-key"),
patch.object(view, "process_remote_device", return_value=process_result),
patch.object(view, "check_cable_status", return_value=process_result),
patch(
"netbox_librenms_plugin.views.base.cables_view.reverse",
side_effect=[
"/dcim/interfaces/99/",
"/plugins/librenms/sync/cables/1/",
],
),
patch(
"netbox_librenms_plugin.views.base.cables_view.escape",
side_effect=lambda x: x,
),
patch(
"netbox_librenms_plugin.views.base.cables_view.get_token",
return_value="csrf-token",
),
):
mock_cache.get.return_value = cached_links
response = view.post(mock_request)
import json as json_mod2
data = json_mod2.loads(response.content)
assert data["status"] == "success"
# can_create_cable=True → actions should contain a form
assert "form" in data["formatted_row"]["actions"]
assert "Sync Cable" in data["formatted_row"]["actions"]
# =============================================================================
# TestPostHandlerInterfaceNotFoundBranches — lines 554, 559
# =============================================================================
class TestPostHandlerInterfaceNotFoundBranches:
"""Tests for the cable_status branches in interface-not-found path (lines 554, 559)."""
def _make_view(self):
from netbox_librenms_plugin.views.base.cables_view import SingleCableVerifyView
view = object.__new__(SingleCableVerifyView)
view._librenms_api = MagicMock()
view._librenms_api.server_key = "default"
return view
def _run_post(self, view, process_result):
"""Helper: run the post with a given process_result dict."""
import json as json_mod
mock_request = MagicMock()
mock_request.body = json_mod.dumps(
{
"device_id": 1,
"local_port_id": 10,
"server_key": "default",
}
).encode()
mock_device = MagicMock()
mock_device.virtual_chassis = None
mock_device.id = 1
mock_device.interfaces.filter.return_value.first.return_value = None # no interface
cached_links = {
"links": [
{
"local_port_id": 10,
"local_port": "Gi0/0",
"remote_port": "Gi0/1",
"remote_device": "switch-b",
"remote_port_id": 20,
"remote_device_id": 99,
}
]
}
with (
patch(
"netbox_librenms_plugin.views.base.cables_view.get_object_or_404",
return_value=mock_device,
),
patch(
"netbox_librenms_plugin.views.base.cables_view.get_librenms_sync_device",
return_value=mock_device,
),
patch("netbox_librenms_plugin.views.base.cables_view.cache") as mock_cache,
patch.object(view, "get_cache_key", return_value="test-key"),
patch.object(view, "process_remote_device", return_value=process_result),
patch(
"netbox_librenms_plugin.views.base.cables_view.escape",
side_effect=lambda x: x,
),
):
mock_cache.get.return_value = cached_links
response = view.post(mock_request)
import json as json_mod2
return json_mod2.loads(response.content)
def test_no_remote_device_url_sets_device_not_found(self):
"""remote_device present, no remote_device_url → 'Device Not Found in NetBox' (line 554)."""
view = self._make_view()
process_result = {
"local_port": "Gi0/0",
"remote_port": "Gi0/1",
"remote_device": "switch-b", # truthy remote_device_name
"remote_port_id": 20,
# No remote_device_url → triggers line 554
"remote_port_name": "Gi0/1",
}
data = self._run_post(view, process_result)
assert data["status"] == "success"
assert data["formatted_row"]["cable_status"] == "Device Not Found in NetBox"
def test_device_url_but_no_port_url_sets_missing_interface(self):
"""remote_device_url present, no remote_port_url → 'Missing Interface' (line 559)."""
view = self._make_view()
process_result = {
"local_port": "Gi0/0",
"remote_port": "Gi0/1",
"remote_device": "switch-b",
"remote_port_id": 20,
"remote_device_url": "/device/5/", # device found
# No remote_port_url → elif condition False → else line 559
"remote_port_name": "Gi0/1",
}
data = self._run_post(view, process_result)
assert data["status"] == "success"
assert data["formatted_row"]["cable_status"] == "Missing Interface"