""" 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 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 '' 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 = "sync" 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 = "matched" 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 = "sync" 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 = "sync" 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"