2038 lines
76 KiB
Python
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"
|