""" Tests for device mismatch detection in get_librenms_device_info. Covers the identity cross-matching logic that determines whether a mismatched_device warning banner is shown on the LibreNMS Sync page, as well as the resolved_name computation that drives the name-match icon and sync button. Match rule: mismatch is False when ANY NetBox identity (device name, primary IP, DNS name) matches ANY LibreNMS identity (sysName, hostname, ip). """ from unittest.mock import MagicMock, patch def _make_view(librenms_id, device_info, librenms_url="https://librenms.example.com"): """Create a minimal BaseLibreNMSSyncView instance with mocked dependencies.""" from netbox_librenms_plugin.views.base.librenms_sync_view import BaseLibreNMSSyncView view = object.__new__(BaseLibreNMSSyncView) view.librenms_id = librenms_id api = MagicMock() api.librenms_url = librenms_url api.get_device_info.return_value = (True, device_info) api.get_device_inventory.return_value = (True, []) view._librenms_api = api return view def _make_obj(name, primary_ip=None, dns_name=None, virtual_chassis=None, cf=None, vc_position=None, serial=None): """Create a mock NetBox device object.""" obj = MagicMock() obj.name = name obj.cf = cf or {} if primary_ip: obj.primary_ip = MagicMock() obj.primary_ip.address.ip = primary_ip obj.primary_ip.dns_name = dns_name or "" else: obj.primary_ip = None obj.virtual_chassis = virtual_chassis obj.vc_position = vc_position obj.serial = serial return obj def _make_request(use_sysname=None, strip_domain=None): """Create a mock request with user preferences for naming settings.""" request = MagicMock() request.POST = {} request.GET = {} config = {} if use_sysname is not None: config["plugins.netbox_librenms_plugin.use_sysname"] = use_sysname if strip_domain is not None: config["plugins.netbox_librenms_plugin.strip_domain"] = strip_domain request.user.config.get = lambda path, default=None: config.get(path, default) return request class TestMismatchDetection: """Tests for identity cross-matching logic.""" # -- No device / API failure ------------------------------------------- @patch("netbox_librenms_plugin.views.base.librenms_sync_view.match_librenms_hardware_to_device_type") def test_no_librenms_id_returns_not_found(self, mock_hw): """No librenms_id means device is not found.""" view = _make_view(librenms_id=None, device_info=None) result = view.get_librenms_device_info(_make_obj("sw01")) assert result["found_in_librenms"] is False assert result["mismatched_device"] is False @patch("netbox_librenms_plugin.views.base.librenms_sync_view.match_librenms_hardware_to_device_type") def test_api_failure_returns_not_found(self, mock_hw): """API failure (success=False) means device is not found.""" view = _make_view(librenms_id=42, device_info=None) view.librenms_api.get_device_info.return_value = (False, None) result = view.get_librenms_device_info(_make_obj("sw01")) assert result["found_in_librenms"] is False assert result["mismatched_device"] is False # -- Name matches ------------------------------------------------------ @patch("netbox_librenms_plugin.views.base.librenms_sync_view.match_librenms_hardware_to_device_type") def test_exact_sysname_match(self, mock_hw): """NetBox name matches LibreNMS sysName (case-insensitive).""" view = _make_view(42, {"sysName": "SW01", "ip": "10.0.0.2"}) obj = _make_obj("sw01", primary_ip="10.0.0.1") result = view.get_librenms_device_info(obj) assert result["found_in_librenms"] is True assert result["mismatched_device"] is False @patch("netbox_librenms_plugin.views.base.librenms_sync_view.match_librenms_hardware_to_device_type") def test_netbox_name_matches_librenms_hostname(self, mock_hw): """NetBox name matches LibreNMS hostname field.""" view = _make_view(42, {"sysName": "something-else", "hostname": "sw01", "ip": "10.0.0.2"}) obj = _make_obj("sw01", primary_ip="10.0.0.1") result = view.get_librenms_device_info(obj) assert result["found_in_librenms"] is True assert result["mismatched_device"] is False @patch("netbox_librenms_plugin.views.base.librenms_sync_view.match_librenms_hardware_to_device_type") def test_fqdn_match(self, mock_hw): """Full FQDN match -- no mismatch.""" view = _make_view(42, {"sysName": "sw01.example.net", "ip": "10.0.0.2"}) obj = _make_obj("sw01.example.net", primary_ip="10.0.0.1") result = view.get_librenms_device_info(obj) assert result["found_in_librenms"] is True assert result["mismatched_device"] is False # -- IP matches -------------------------------------------------------- @patch("netbox_librenms_plugin.views.base.librenms_sync_view.match_librenms_hardware_to_device_type") def test_netbox_ip_matches_librenms_ip(self, mock_hw): """NetBox primary IP matches LibreNMS IP -- no mismatch.""" view = _make_view(42, {"sysName": "different", "ip": "10.0.0.1"}) obj = _make_obj("sw01", primary_ip="10.0.0.1") result = view.get_librenms_device_info(obj) assert result["found_in_librenms"] is True assert result["mismatched_device"] is False @patch("netbox_librenms_plugin.views.base.librenms_sync_view.match_librenms_hardware_to_device_type") def test_netbox_ip_matches_librenms_hostname_ip(self, mock_hw): """LibreNMS hostname is an IP that matches NetBox primary IP.""" view = _make_view(42, {"sysName": "different", "hostname": "10.0.0.1", "ip": "10.0.0.1"}) obj = _make_obj("sw01", primary_ip="10.0.0.1") result = view.get_librenms_device_info(obj) assert result["found_in_librenms"] is True assert result["mismatched_device"] is False # -- DNS name matches -------------------------------------------------- @patch("netbox_librenms_plugin.views.base.librenms_sync_view.match_librenms_hardware_to_device_type") def test_dns_name_matches_sysname(self, mock_hw): """NetBox DNS name matches LibreNMS sysName.""" view = _make_view(42, {"sysName": "sw01.example.net", "ip": "10.0.0.2"}) obj = _make_obj("sw01", primary_ip="10.0.0.1", dns_name="sw01.example.net") result = view.get_librenms_device_info(obj) assert result["found_in_librenms"] is True assert result["mismatched_device"] is False @patch("netbox_librenms_plugin.views.base.librenms_sync_view.match_librenms_hardware_to_device_type") def test_dns_name_matches_librenms_hostname(self, mock_hw): """NetBox DNS name matches LibreNMS hostname field.""" view = _make_view(42, {"sysName": "something", "hostname": "sw01.example.net", "ip": "10.0.0.2"}) obj = _make_obj("sw01", primary_ip="10.0.0.1", dns_name="sw01.example.net") result = view.get_librenms_device_info(obj) assert result["found_in_librenms"] is True assert result["mismatched_device"] is False # -- Mismatches -------------------------------------------------------- @patch("netbox_librenms_plugin.views.base.librenms_sync_view.match_librenms_hardware_to_device_type") def test_completely_different_is_mismatch(self, mock_hw): """No identities overlap -- mismatch.""" view = _make_view(42, {"sysName": "router-01", "hostname": "router-01.corp", "ip": "10.0.0.2"}) obj = _make_obj("switch-05", primary_ip="10.0.0.1", dns_name="switch-05.corp") result = view.get_librenms_device_info(obj) assert result["found_in_librenms"] is True assert result["mismatched_device"] is True @patch("netbox_librenms_plugin.views.base.librenms_sync_view.match_librenms_hardware_to_device_type") def test_short_vs_fqdn_matches_via_domain_strip(self, mock_hw): """Short name vs FQDN -- matches after domain stripping.""" view = _make_view(42, {"sysName": "sw01.example.net", "ip": "10.0.0.2"}) obj = _make_obj("sw01", primary_ip="10.0.0.1") result = view.get_librenms_device_info(obj) assert result["found_in_librenms"] is True assert result["mismatched_device"] is False @patch("netbox_librenms_plugin.views.base.librenms_sync_view.match_librenms_hardware_to_device_type") def test_fqdn_domain_differs_matches_via_domain_strip(self, mock_hw): """ Different FQDN domains -- matches because domain-stripped LibreNMS short name 'sw01' matches NetBox FQDN split 'sw01'. NetBox name 'sw01.example.net' is compared as-is (no stripping), but the LibreNMS domain-stripped 'sw01' does NOT appear in the NetBox identities since NetBox names are not domain-stripped. However, both sides share the short name via NetBox raw name normalization — actually NetBox keeps the full name. """ view = _make_view(42, {"sysName": "sw01.other.net", "ip": "10.0.0.2"}) obj = _make_obj("sw01.example.net", primary_ip="10.0.0.1") result = view.get_librenms_device_info(obj) assert result["found_in_librenms"] is True # NetBox identities: {"sw01.example.net", "10.0.0.1"} # LibreNMS identities: {"sw01.other.net", "sw01", "10.0.0.2"} # No overlap → mismatch assert result["mismatched_device"] is True @patch("netbox_librenms_plugin.views.base.librenms_sync_view.match_librenms_hardware_to_device_type") def test_no_netbox_name_no_ip_match(self, mock_hw): """No NetBox name and IPs differ -- mismatch.""" view = _make_view(42, {"sysName": "sw01", "ip": "10.0.0.2"}) obj = _make_obj(None, primary_ip="10.0.0.1") result = view.get_librenms_device_info(obj) assert result["found_in_librenms"] is True assert result["mismatched_device"] is True @patch("netbox_librenms_plugin.views.base.librenms_sync_view.match_librenms_hardware_to_device_type") def test_no_librenms_sysname_no_match(self, mock_hw): """No sysName, no hostname, IPs differ -- mismatch.""" view = _make_view(42, {"sysName": None, "ip": "10.0.0.2"}) obj = _make_obj("sw01", primary_ip="10.0.0.1") result = view.get_librenms_device_info(obj) assert result["found_in_librenms"] is True assert result["mismatched_device"] is True @patch("netbox_librenms_plugin.views.base.librenms_sync_view.match_librenms_hardware_to_device_type") def test_no_identities_at_all(self, mock_hw): """Both sides have no identities -- mismatch (cannot confirm).""" view = _make_view(42, {"sysName": None, "ip": None}) obj = _make_obj(None, primary_ip=None) result = view.get_librenms_device_info(obj) assert result["found_in_librenms"] is True assert result["mismatched_device"] is True # -- Virtual Chassis --------------------------------------------------- @patch("netbox_librenms_plugin.views.base.librenms_sync_view.match_librenms_hardware_to_device_type") def test_vc_suffix_stripped(self, mock_hw): """VC member suffix ' (1)' is stripped before comparison.""" view = _make_view(42, {"sysName": "switch-1", "ip": "10.0.0.2"}) obj = _make_obj("switch-1 (1)", primary_ip="10.0.0.1") result = view.get_librenms_device_info(obj) assert result["found_in_librenms"] is True assert result["mismatched_device"] is False @patch("netbox_librenms_plugin.views.base.librenms_sync_view.match_librenms_hardware_to_device_type") def test_vc_different_name_is_mismatch(self, mock_hw): """VC member with different name after suffix strip -- mismatch.""" vc = MagicMock() view = _make_view(42, {"sysName": "switch-1", "ip": "10.0.0.2"}) obj = _make_obj("switch-2 (2)", primary_ip="10.0.0.1", virtual_chassis=vc, cf={"librenms_id": 42}) result = view.get_librenms_device_info(obj) assert result["found_in_librenms"] is True assert result["mismatched_device"] is True # -- found_in_librenms always True with valid ID ----------------------- @patch("netbox_librenms_plugin.views.base.librenms_sync_view.match_librenms_hardware_to_device_type") def test_found_in_librenms_always_true_with_valid_id(self, mock_hw): """found_in_librenms is True even when identities mismatch.""" view = _make_view(42, {"sysName": "totally-different", "ip": "10.0.0.2"}) obj = _make_obj("my-device", primary_ip="10.0.0.1") result = view.get_librenms_device_info(obj) assert result["found_in_librenms"] is True # -- Domain stripping -------------------------------------------------- @patch("netbox_librenms_plugin.views.base.librenms_sync_view.match_librenms_hardware_to_device_type") def test_domain_strip_hostname(self, mock_hw): """LibreNMS hostname FQDN stripped to short name matches NetBox name.""" view = _make_view(42, {"sysName": "other", "hostname": "sw01.example.net", "ip": "10.0.0.2"}) obj = _make_obj("sw01", primary_ip="10.0.0.1") result = view.get_librenms_device_info(obj) assert result["mismatched_device"] is False @patch("netbox_librenms_plugin.views.base.librenms_sync_view.match_librenms_hardware_to_device_type") def test_domain_strip_sysname(self, mock_hw): """LibreNMS sysName FQDN stripped to short name matches NetBox name.""" view = _make_view(42, {"sysName": "sw01.corp.local", "ip": "10.0.0.2"}) obj = _make_obj("sw01", primary_ip="10.0.0.1") result = view.get_librenms_device_info(obj) assert result["mismatched_device"] is False @patch("netbox_librenms_plugin.views.base.librenms_sync_view.match_librenms_hardware_to_device_type") def test_domain_strip_no_false_positive(self, mock_hw): """Domain stripping doesn't cause false match when short names differ.""" view = _make_view(42, {"sysName": "router01.example.net", "ip": "10.0.0.2"}) obj = _make_obj("switch01", primary_ip="10.0.0.1") result = view.get_librenms_device_info(obj) assert result["mismatched_device"] is True # -- VC pattern stripping ---------------------------------------------- @patch("netbox_librenms_plugin.views.base.librenms_sync_view.match_librenms_hardware_to_device_type") @patch("netbox_librenms_plugin.models.LibreNMSSettings.objects") def test_vc_pattern_strip_default(self, mock_settings_qs, mock_hw): """Default VC pattern '-M{position}' is stripped from NetBox name.""" settings_obj = MagicMock() settings_obj.vc_member_name_pattern = "-M{position}" mock_settings_qs.first.return_value = settings_obj view = _make_view(42, {"sysName": "switch01", "ip": "10.0.0.2"}) obj = _make_obj("switch01-M2", primary_ip="10.0.0.1") result = view.get_librenms_device_info(obj) assert result["mismatched_device"] is False @patch("netbox_librenms_plugin.views.base.librenms_sync_view.match_librenms_hardware_to_device_type") @patch("netbox_librenms_plugin.models.LibreNMSSettings.objects") def test_vc_pattern_strip_custom(self, mock_settings_qs, mock_hw): """Custom VC pattern '-SW{position}' is stripped from NetBox name.""" settings_obj = MagicMock() settings_obj.vc_member_name_pattern = "-SW{position}" mock_settings_qs.first.return_value = settings_obj view = _make_view(42, {"sysName": "switch01", "ip": "10.0.0.2"}) obj = _make_obj("switch01-SW3", primary_ip="10.0.0.1") result = view.get_librenms_device_info(obj) assert result["mismatched_device"] is False @patch("netbox_librenms_plugin.views.base.librenms_sync_view.match_librenms_hardware_to_device_type") @patch("netbox_librenms_plugin.models.LibreNMSSettings.objects") def test_vc_pattern_no_match_leaves_name(self, mock_settings_qs, mock_hw): """VC pattern doesn't match -- name unchanged, still mismatched.""" settings_obj = MagicMock() settings_obj.vc_member_name_pattern = "-M{position}" mock_settings_qs.first.return_value = settings_obj view = _make_view(42, {"sysName": "switch01", "ip": "10.0.0.2"}) obj = _make_obj("switch99", primary_ip="10.0.0.1") result = view.get_librenms_device_info(obj) assert result["mismatched_device"] is True # --------------------------------------------------------------------------- # Tests for VC lookup delegation in get() # --------------------------------------------------------------------------- class TestVCLookupDelegation: """Verify that BaseLibreNMSSyncView.get() always delegates VC device resolution to get_librenms_sync_device(), even when the viewed member has its own librenms_id.""" @patch("netbox_librenms_plugin.views.base.librenms_sync_view.render") @patch("netbox_librenms_plugin.views.base.librenms_sync_view.get_object_or_404") @patch("netbox_librenms_plugin.views.base.librenms_sync_view.get_librenms_sync_device") def test_vc_member_with_own_id_delegates_to_sync_device(self, mock_sync_device, mock_get_object, mock_render): """A VC member with its own librenms_id should still delegate to get_librenms_sync_device, which may return a different member.""" from netbox_librenms_plugin.views.base.librenms_sync_view import BaseLibreNMSSyncView # Viewed device: member A with its own librenms_id member_a = MagicMock() member_a.pk = 1 member_a.cf = {"librenms_id": {"default": 42}} member_a.virtual_chassis = MagicMock() # Sync device: member B (returned by get_librenms_sync_device) member_b = MagicMock() member_b.pk = 2 mock_get_object.return_value = member_a mock_sync_device.return_value = member_b view = object.__new__(BaseLibreNMSSyncView) view.model = MagicMock() api = MagicMock() api.server_key = "default" api.get_librenms_id.return_value = 42 view._librenms_api = api view.tab = MagicMock() view.get_context_data = MagicMock(return_value={}) mock_render.return_value = MagicMock() request = MagicMock() view.get(request, pk=1) # get_librenms_sync_device must be called unconditionally for VC members mock_sync_device.assert_called_once_with(member_a, server_key="default") # get_librenms_id should be called on the sync device (member_b) api.get_librenms_id.assert_called_once_with(member_b) @patch("netbox_librenms_plugin.views.base.librenms_sync_view.render") @patch("netbox_librenms_plugin.views.base.librenms_sync_view.get_object_or_404") @patch("netbox_librenms_plugin.views.base.librenms_sync_view.get_librenms_sync_device") def test_non_vc_device_skips_sync_device_lookup(self, mock_sync_device, mock_get_object, mock_render): """A device without a virtual chassis should not call get_librenms_sync_device at all.""" from netbox_librenms_plugin.views.base.librenms_sync_view import BaseLibreNMSSyncView device = MagicMock() device.pk = 1 device.virtual_chassis = None mock_get_object.return_value = device view = object.__new__(BaseLibreNMSSyncView) view.model = MagicMock() api = MagicMock() api.server_key = "default" api.get_librenms_id.return_value = 42 view._librenms_api = api view.tab = MagicMock() view.get_context_data = MagicMock(return_value={}) mock_render.return_value = MagicMock() request = MagicMock() view.get(request, pk=1) mock_sync_device.assert_not_called() # --------------------------------------------------------------------------- # Tests for _build_all_server_mappings # --------------------------------------------------------------------------- class TestBuildAllServerMappings: """Tests for BaseLibreNMSSyncView._build_all_server_mappings.""" def test_returns_none_for_legacy_int(self, mock_netbox_device): from netbox_librenms_plugin.views.base.librenms_sync_view import BaseLibreNMSSyncView mock_netbox_device.custom_field_data = {"librenms_id": 42} result = BaseLibreNMSSyncView._build_all_server_mappings(mock_netbox_device, "production") assert result is None def test_returns_none_for_missing_cf(self, mock_netbox_device): from netbox_librenms_plugin.views.base.librenms_sync_view import BaseLibreNMSSyncView mock_netbox_device.custom_field_data = {"librenms_id": None} result = BaseLibreNMSSyncView._build_all_server_mappings(mock_netbox_device, "production") assert result is None def test_single_configured_server(self, mock_netbox_device, mock_plugins_config_single_server): from unittest.mock import patch from netbox_librenms_plugin.views.base.librenms_sync_view import BaseLibreNMSSyncView mock_netbox_device.custom_field_data = {"librenms_id": {"production": 42}} with patch("netbox_librenms_plugin.views.base.librenms_sync_view.django_settings") as mock_settings: mock_settings.PLUGINS_CONFIG = mock_plugins_config_single_server result = BaseLibreNMSSyncView._build_all_server_mappings(mock_netbox_device, "production") assert result is not None assert len(result) == 1 entry = result[0] assert entry["server_key"] == "production" assert entry["device_id"] == 42 assert entry["display_name"] == "Production LibreNMS" assert entry["is_configured"] is True assert entry["is_active"] is True assert entry["device_url"] == "https://librenms.example.com/device/device=42/" def test_orphaned_server_is_not_configured(self, mock_netbox_device, mock_plugins_config_empty_servers): from unittest.mock import patch from netbox_librenms_plugin.views.base.librenms_sync_view import BaseLibreNMSSyncView mock_netbox_device.custom_field_data = {"librenms_id": {"deleted-server": 77}} with patch("netbox_librenms_plugin.views.base.librenms_sync_view.django_settings") as mock_settings: mock_settings.PLUGINS_CONFIG = mock_plugins_config_empty_servers result = BaseLibreNMSSyncView._build_all_server_mappings(mock_netbox_device, "production") assert result is not None assert len(result) == 1 entry = result[0] assert entry["server_key"] == "deleted-server" assert entry["device_id"] == 77 assert entry["is_configured"] is False assert entry["is_active"] is False assert entry["device_url"] is None def test_multiple_servers_sorted_active_first(self, mock_netbox_device, mock_plugins_config_multi_server_mapping): from unittest.mock import patch from netbox_librenms_plugin.views.base.librenms_sync_view import BaseLibreNMSSyncView mock_netbox_device.custom_field_data = {"librenms_id": {"mock-dev": 99, "production": 42, "old-server": 11}} with patch("netbox_librenms_plugin.views.base.librenms_sync_view.django_settings") as mock_settings: mock_settings.PLUGINS_CONFIG = mock_plugins_config_multi_server_mapping result = BaseLibreNMSSyncView._build_all_server_mappings(mock_netbox_device, "production") assert result is not None assert len(result) == 3 # Active (production) first assert result[0]["server_key"] == "production" assert result[0]["is_active"] is True # Configured (mock-dev) second assert result[1]["server_key"] == "mock-dev" assert result[1]["is_configured"] is True assert result[1]["is_active"] is False # Orphaned last assert result[2]["server_key"] == "old-server" assert result[2]["is_configured"] is False class TestResolvedName: """Tests for resolved_name computation using naming preferences.""" @patch("netbox_librenms_plugin.views.base.librenms_sync_view.match_librenms_hardware_to_device_type") @patch("netbox_librenms_plugin.views.base.librenms_sync_view.resolve_naming_preferences") def test_resolved_name_uses_sysname_by_default(self, mock_prefs, mock_hw): """resolved_name defaults to sysName when use_sysname=True.""" mock_prefs.return_value = (True, False) view = _make_view(42, {"sysName": "sw01.example.com", "hostname": "10.0.0.2", "ip": "10.0.0.2"}) obj = _make_obj("sw01.example.com", primary_ip="10.0.0.2") request = _make_request(use_sysname=True, strip_domain=False) result = view.get_librenms_device_info(obj, request) assert result["librenms_device_details"]["resolved_name"] == "sw01.example.com" @patch("netbox_librenms_plugin.views.base.librenms_sync_view.match_librenms_hardware_to_device_type") @patch("netbox_librenms_plugin.views.base.librenms_sync_view.resolve_naming_preferences") def test_resolved_name_with_strip_domain(self, mock_prefs, mock_hw): """resolved_name strips domain when strip_domain=True.""" mock_prefs.return_value = (True, True) view = _make_view(42, {"sysName": "sw01.example.com", "hostname": "10.0.0.2", "ip": "10.0.0.2"}) obj = _make_obj("sw01", primary_ip="10.0.0.2") request = _make_request(use_sysname=True, strip_domain=True) result = view.get_librenms_device_info(obj, request) assert result["librenms_device_details"]["resolved_name"] == "sw01" @patch("netbox_librenms_plugin.views.base.librenms_sync_view.match_librenms_hardware_to_device_type") @patch("netbox_librenms_plugin.views.base.librenms_sync_view.resolve_naming_preferences") def test_resolved_name_use_hostname(self, mock_prefs, mock_hw): """resolved_name uses hostname when use_sysname=False.""" mock_prefs.return_value = (False, False) view = _make_view(42, {"sysName": "sw01.example.com", "hostname": "sw01-mgmt", "ip": "10.0.0.2"}) obj = _make_obj("sw01-mgmt", primary_ip="10.0.0.2") request = _make_request(use_sysname=False, strip_domain=False) result = view.get_librenms_device_info(obj, request) assert result["librenms_device_details"]["resolved_name"] == "sw01-mgmt" @patch("netbox_librenms_plugin.views.base.librenms_sync_view.match_librenms_hardware_to_device_type") @patch("netbox_librenms_plugin.views.base.librenms_sync_view.resolve_naming_preferences") @patch("netbox_librenms_plugin.views.base.librenms_sync_view._generate_vc_member_name") def test_resolved_name_vc_member(self, mock_vc_name, mock_prefs, mock_hw): """resolved_name applies VC member naming pattern for VC members.""" mock_prefs.return_value = (True, True) mock_vc_name.return_value = "sw01-M2" vc = MagicMock() view = _make_view(42, {"sysName": "sw01.example.com", "hostname": "10.0.0.2", "ip": "10.0.0.2"}) obj = _make_obj("sw01-M2", primary_ip="10.0.0.2", virtual_chassis=vc, vc_position=2, serial="ABC123") request = _make_request(use_sysname=True, strip_domain=True) result = view.get_librenms_device_info(obj, request) assert result["librenms_device_details"]["resolved_name"] == "sw01-M2" mock_vc_name.assert_called_once_with("sw01", 2, serial="ABC123") @patch("netbox_librenms_plugin.views.base.librenms_sync_view.match_librenms_hardware_to_device_type") def test_resolved_name_falls_back_to_sysname_without_request(self, mock_hw): """Without request, resolved_name falls back to raw sysName.""" view = _make_view(42, {"sysName": "sw01.example.com", "ip": "10.0.0.2"}) obj = _make_obj("sw01", primary_ip="10.0.0.2") result = view.get_librenms_device_info(obj) assert result["librenms_device_details"]["resolved_name"] == "sw01.example.com" @patch("netbox_librenms_plugin.views.base.librenms_sync_view.match_librenms_hardware_to_device_type") @patch("netbox_librenms_plugin.views.base.librenms_sync_view.resolve_naming_preferences") def test_resolved_name_non_vc_device_no_vc_pattern(self, mock_prefs, mock_hw): """Non-VC device does not apply VC member naming.""" mock_prefs.return_value = (True, True) view = _make_view(42, {"sysName": "sw01.example.com", "hostname": "10.0.0.2", "ip": "10.0.0.2"}) obj = _make_obj("sw01", primary_ip="10.0.0.2") obj.virtual_chassis = None obj.vc_position = None request = _make_request(use_sysname=True, strip_domain=True) result = view.get_librenms_device_info(obj, request) assert result["librenms_device_details"]["resolved_name"] == "sw01"