"""Coverage tests for virtual_chassis.py lines 431 and 435.""" from contextlib import contextmanager from unittest.mock import MagicMock, patch def _make_master_device(serial="MASTER001"): """Build a mock master Device for VC creation tests.""" master = MagicMock() master.name = "switch-master" master.serial = serial master.pk = 1 master.rack = None master.location = None master.device_type = MagicMock() master.role = MagicMock() master.site = MagicMock() master.platform = MagicMock() return master class TestCreateVirtualChassisWithMembersPositionConflict: """Tests specifically for lines 431 and 435 - position conflict resolution.""" @patch("netbox_librenms_plugin.import_utils.virtual_chassis.transaction") @patch("netbox_librenms_plugin.import_utils.virtual_chassis._load_vc_member_name_pattern") @patch("netbox_librenms_plugin.import_utils.virtual_chassis.VirtualChassis") @patch("netbox_librenms_plugin.import_utils.virtual_chassis.Device") def test_line_431_position_conflict_sets_discovered_pos_to_none( self, mock_Device, mock_VirtualChassis, mock_load_pattern, mock_transaction ): """ Line 431: discovered_pos = None when position already in used_positions. Scenario: master is at position 1 (used_positions = {1}). First member takes position 2. Second member also claims position 2 → discovered_pos set to None → falls back to sequential (position 3). """ from netbox_librenms_plugin.import_utils.virtual_chassis import create_virtual_chassis_with_members # Make transaction.atomic() a no-op context manager @contextmanager def noop_atomic(): yield mock_transaction.atomic = noop_atomic mock_load_pattern.return_value = "-M{position}" master = _make_master_device("MASTER001") vc_mock = MagicMock() vc_mock.members.count.return_value = 3 mock_VirtualChassis.objects.create.return_value = vc_mock # Device.objects.filter(...).exists() → False (no conflicts) mock_filter = MagicMock() mock_filter.exists.return_value = False mock_filter.exclude.return_value = mock_filter mock_Device.objects.filter.return_value = mock_filter mock_Device.objects.create.return_value = MagicMock() # Members: first at position 2, second ALSO at position 2 (conflict) members_info = [ {"serial": "SN002", "position": 2, "name": "Member2"}, {"serial": "SN003", "position": 2, "name": "Member3-conflict"}, # triggers line 431 ] libre_device = {"device_id": 99} create_virtual_chassis_with_members(master, members_info, libre_device) # VC should be created mock_VirtualChassis.objects.create.assert_called_once() # Two Device.objects.create calls for the two non-master members create_calls = mock_Device.objects.create.call_args_list assert len(create_calls) == 2 # Map serial -> vc_position for precise identity assertions serial_to_pos = {c.kwargs.get("serial"): c.kwargs.get("vc_position") for c in create_calls} # First member (SN002) takes its explicit position 2 assert serial_to_pos.get("SN002") == 2 # Second member (SN003) conflicts at 2, falls back to 3 assert serial_to_pos.get("SN003") == 3 @patch("netbox_librenms_plugin.import_utils.virtual_chassis.transaction") @patch("netbox_librenms_plugin.import_utils.virtual_chassis._load_vc_member_name_pattern") @patch("netbox_librenms_plugin.import_utils.virtual_chassis.VirtualChassis") @patch("netbox_librenms_plugin.import_utils.virtual_chassis.Device") def test_line_435_while_loop_skips_taken_slots( self, mock_Device, mock_VirtualChassis, mock_load_pattern, mock_transaction ): """Line 435: position += 1 in while loop when sequential slot is taken.""" from netbox_librenms_plugin.import_utils.virtual_chassis import create_virtual_chassis_with_members @contextmanager def noop_atomic(): yield mock_transaction.atomic = noop_atomic mock_load_pattern.return_value = "-M{position}" master = _make_master_device("MASTER001") vc_mock = MagicMock() vc_mock.members.count.return_value = 3 mock_VirtualChassis.objects.create.return_value = vc_mock mock_filter = MagicMock() mock_filter.exists.return_value = False mock_filter.exclude.return_value = mock_filter mock_Device.objects.filter.return_value = mock_filter mock_Device.objects.create.return_value = MagicMock() # Member A explicitly at position 2 # Member B has no position → sequential starts at 2 → taken → increments to 3 (line 435) members_info = [ {"serial": "SN002", "position": 2, "name": "Member-explicit-2"}, {"serial": "SN003", "position": None, "name": "Member-no-pos"}, # triggers line 435 ] libre_device = {"device_id": 99} create_virtual_chassis_with_members(master, members_info, libre_device) mock_VirtualChassis.objects.create.assert_called_once() create_calls = mock_Device.objects.create.call_args_list positions_used = [c.kwargs.get("vc_position") for c in create_calls] # First member gets explicit position 2; second (no position) gets 3 after 2 is taken assert sorted(positions_used) == [2, 3] actual_entries = sorted([(c.kwargs.get("serial"), c.kwargs.get("vc_position")) for c in create_calls]) assert actual_entries == [("SN002", 2), ("SN003", 3)] @patch("netbox_librenms_plugin.import_utils.virtual_chassis.transaction") @patch("netbox_librenms_plugin.import_utils.virtual_chassis._load_vc_member_name_pattern") @patch("netbox_librenms_plugin.import_utils.virtual_chassis.VirtualChassis") @patch("netbox_librenms_plugin.import_utils.virtual_chassis.Device") def test_multiple_sequential_slots_taken_skips_all( self, mock_Device, mock_VirtualChassis, mock_load_pattern, mock_transaction ): """Multiple sequential increments: position = 2, 3 all taken → gets 4.""" from netbox_librenms_plugin.import_utils.virtual_chassis import create_virtual_chassis_with_members @contextmanager def noop_atomic(): yield mock_transaction.atomic = noop_atomic mock_load_pattern.return_value = "-M{position}" master = _make_master_device("MASTER001") vc_mock = MagicMock() vc_mock.members.count.return_value = 4 mock_VirtualChassis.objects.create.return_value = vc_mock mock_filter = MagicMock() mock_filter.exists.return_value = False mock_filter.exclude.return_value = mock_filter mock_Device.objects.filter.return_value = mock_filter mock_Device.objects.create.return_value = MagicMock() # Members at positions 2 and 3; then one with no position → should get 4 members_info = [ {"serial": "SN002", "position": 2, "name": "M2"}, {"serial": "SN003", "position": 3, "name": "M3"}, {"serial": "SN004", "position": None, "name": "M-no-pos"}, # should get 4 ] libre_device = {"device_id": 10} create_virtual_chassis_with_members(master, members_info, libre_device) create_calls = mock_Device.objects.create.call_args_list positions_used = [c.kwargs.get("vc_position") for c in create_calls] # Members at 2 and 3 are explicit; the member with no position gets 4 assert sorted(positions_used) == [2, 3, 4] actual_entries = sorted([(c.kwargs.get("serial"), c.kwargs.get("vc_position")) for c in create_calls]) assert actual_entries == [("SN002", 2), ("SN003", 3), ("SN004", 4)] @patch("netbox_librenms_plugin.import_utils.virtual_chassis.transaction") @patch("netbox_librenms_plugin.import_utils.virtual_chassis._load_vc_member_name_pattern") @patch("netbox_librenms_plugin.import_utils.virtual_chassis.VirtualChassis") @patch("netbox_librenms_plugin.import_utils.virtual_chassis.Device") def test_member_with_same_serial_as_master_is_skipped( self, mock_Device, mock_VirtualChassis, mock_load_pattern, mock_transaction ): """Members with same serial as master device should be skipped.""" from netbox_librenms_plugin.import_utils.virtual_chassis import create_virtual_chassis_with_members @contextmanager def noop_atomic(): yield mock_transaction.atomic = noop_atomic mock_load_pattern.return_value = "-M{position}" master = _make_master_device("MASTER_SERIAL") vc_mock = MagicMock() vc_mock.members.count.return_value = 1 mock_VirtualChassis.objects.create.return_value = vc_mock mock_filter = MagicMock() mock_filter.exists.return_value = False mock_filter.exclude.return_value = mock_filter mock_Device.objects.filter.return_value = mock_filter mock_Device.objects.create.return_value = MagicMock() members_info = [ {"serial": "MASTER_SERIAL", "position": 2, "name": "Master-dup"}, # skipped {"serial": "SN999", "position": 3, "name": "Real member"}, ] libre_device = {"device_id": 5} create_virtual_chassis_with_members(master, members_info, libre_device) # Only one Device.objects.create for the non-duplicate member create_calls = mock_Device.objects.create.call_args_list assert len(create_calls) == 1 assert create_calls[0].kwargs.get("serial") == "SN999" class TestCreateVirtualChassisServerKeyDomain: """Tests for server_key parameter in create_virtual_chassis_with_members domain.""" @patch("netbox_librenms_plugin.import_utils.virtual_chassis.transaction") @patch("netbox_librenms_plugin.import_utils.virtual_chassis._load_vc_member_name_pattern") @patch("netbox_librenms_plugin.import_utils.virtual_chassis.VirtualChassis") @patch("netbox_librenms_plugin.import_utils.virtual_chassis.Device") def test_server_key_included_in_domain(self, mock_Device, mock_VirtualChassis, mock_load_pattern, mock_transaction): """With server_key='production', domain should contain 'librenms-production-'.""" from contextlib import contextmanager from netbox_librenms_plugin.import_utils.virtual_chassis import create_virtual_chassis_with_members @contextmanager def noop_atomic(): yield mock_transaction.atomic = noop_atomic mock_load_pattern.return_value = "-M{position}" master = _make_master_device("SN001") vc_mock = MagicMock() vc_mock.members.count.return_value = 1 mock_VirtualChassis.objects.create.return_value = vc_mock mock_filter = MagicMock() mock_filter.exists.return_value = False mock_filter.exclude.return_value = mock_filter mock_Device.objects.filter.return_value = mock_filter mock_Device.objects.create.return_value = MagicMock() libre_device = {"device_id": 42} create_virtual_chassis_with_members(master, [], libre_device, server_key="production") call_kwargs = mock_VirtualChassis.objects.create.call_args.kwargs assert "librenms-production-" in call_kwargs["domain"], f"domain was: {call_kwargs['domain']}" assert "42" in call_kwargs["domain"] @patch("netbox_librenms_plugin.import_utils.virtual_chassis.transaction") @patch("netbox_librenms_plugin.import_utils.virtual_chassis._load_vc_member_name_pattern") @patch("netbox_librenms_plugin.import_utils.virtual_chassis.VirtualChassis") @patch("netbox_librenms_plugin.import_utils.virtual_chassis.Device") def test_no_server_key_domain_prefix_is_librenms( self, mock_Device, mock_VirtualChassis, mock_load_pattern, mock_transaction ): """Without server_key, domain should start with 'librenms-' (no server suffix).""" from contextlib import contextmanager from netbox_librenms_plugin.import_utils.virtual_chassis import create_virtual_chassis_with_members @contextmanager def noop_atomic(): yield mock_transaction.atomic = noop_atomic mock_load_pattern.return_value = "-M{position}" master = _make_master_device("SN002") vc_mock = MagicMock() vc_mock.members.count.return_value = 1 mock_VirtualChassis.objects.create.return_value = vc_mock mock_filter = MagicMock() mock_filter.exists.return_value = False mock_filter.exclude.return_value = mock_filter mock_Device.objects.filter.return_value = mock_filter mock_Device.objects.create.return_value = MagicMock() libre_device = {"device_id": 99} create_virtual_chassis_with_members(master, [], libre_device, server_key=None) call_kwargs = mock_VirtualChassis.objects.create.call_args.kwargs domain = call_kwargs["domain"] assert domain.startswith("librenms-"), f"domain was: {domain}" # Should not have a second prefix like 'librenms-None-' assert "librenms-None" not in domain assert "99" in domain