from types import SimpleNamespace from unittest.mock import MagicMock, patch from django.core.exceptions import ValidationError from django.test import SimpleTestCase from netbox_utilities.netbox_export import ( PATCH_MARKER, PATCH_VERSION, RACK_WIDTH_RECORD_KEY, WidthAwareDeferredDevicePlacement, _apply_rack_widths, _defer_rack_width, _serialize_rack_width, _width_aware_device_placement_conflicts, install_netbox_export_rack_width_support, ) class FakeQuerySet(list): def select_for_update(self): return self def select_related(self, *args): return self def filter(self, **kwargs): return self def exclude(self, **kwargs): pk = kwargs.get("pk") return FakeQuerySet(item for item in self if item.pk != pk) class FakeDevice: _meta = SimpleNamespace(label_lower="dcim.device") _default_manager = FakeQuerySet() def __init__(self, pk, *, width=1, horizontal_position=1, position=None, face="front", full_depth=False): self.pk = pk self.rack_id = 1 if position is not None else None self.position = position self.face = face self.device_type = SimpleNamespace(u_height=1, is_full_depth=full_depth) if width != 1: self.netbox_utilities_rack_placement = SimpleNamespace( width=width, horizontal_position=horizontal_position, ) class NetBoxExportRackWidthTest(SimpleTestCase): def test_export_embeds_partial_and_full_rack_widths_on_devices(self): partial = FakeDevice(1, width=2, horizontal_position=2) full = FakeDevice(2) original = lambda obj, *args, **kwargs: {"id": f"dcim.device:{obj.pk}"} partial_record = _serialize_rack_width(original, partial) full_record = _serialize_rack_width(original, full) self.assertEqual( partial_record[RACK_WIDTH_RECORD_KEY], {"schema_version": 1, "width": 2, "horizontal_position": 2}, ) self.assertEqual( full_record[RACK_WIDTH_RECORD_KEY], {"schema_version": 1, "width": 1, "horizontal_position": 1}, ) def test_import_reads_width_metadata_without_breaking_legacy_archives(self): native = SimpleNamespace(rack_spec={"ref": "dcim.rack:2"}, position=11, face="front") original = MagicMock(return_value=({"fields": {}}, native)) record = { "id": "dcim.device:334", RACK_WIDTH_RECORD_KEY: { "schema_version": 1, "width": 2, "horizontal_position": 2, }, } prepared, placement = _defer_rack_width(original, FakeDevice, record) self.assertEqual(prepared, {"fields": {}}) self.assertEqual( placement, WidthAwareDeferredDevicePlacement( rack_spec={"ref": "dcim.rack:2"}, position=11, face="front", width=2, horizontal_position=2, ), ) legacy_record = {"id": "dcim.device:334"} self.assertIs(_defer_rack_width(original, FakeDevice, legacy_record)[1], native) @patch("netbox_utilities.netbox_export.normalize_width_position", side_effect=ValidationError("invalid")) def test_invalid_width_metadata_falls_back_to_native_import(self, _normalize): native = SimpleNamespace(rack_spec=None, position=11, face="front") original = MagicMock(return_value=({}, native)) record = { "id": "dcim.device:334", RACK_WIDTH_RECORD_KEY: { "schema_version": 1, "width": 9, "horizontal_position": 9, }, } self.assertIs(_defer_rack_width(original, FakeDevice, record)[1], native) def test_import_conflicts_respect_horizontal_rack_positions(self): left = FakeDevice(1, width=2, horizontal_position=1, position=11) right = FakeDevice(2, width=2, horizontal_position=2) right.rack_id = 1 FakeDevice._default_manager = FakeQuerySet([left]) original = MagicMock() conflicts = _width_aware_device_placement_conflicts(original, right, object(), 11, "front") self.assertEqual(conflicts, []) original.assert_not_called() overlapping = FakeDevice(3, width=2, horizontal_position=1) overlapping.rack_id = 1 self.assertEqual( _width_aware_device_placement_conflicts(original, overlapping, object(), 11, "front"), [left], ) @patch("netbox_utilities.netbox_export.DeviceRackPlacement.objects") def test_import_persists_partial_width_and_removes_stale_full_width(self, placement_objects): partial = FakeDevice(1) full = FakeDevice(2, width=2, horizontal_position=2) partial_placement = WidthAwareDeferredDevicePlacement(None, 11, "front", 2, 1) full_placement = WidthAwareDeferredDevicePlacement(None, 12, "front", 1, 1) placements = [("partial", partial_placement), ("full", full_placement)] resolved = {"partial": partial, "full": full} def native_apply(received, *_args, **_kwargs): self.assertEqual(received, placements) self.assertEqual(partial._netbox_utilities_rack_width, 2) self.assertEqual(full._netbox_utilities_rack_width, 1) partial.rack_id = full.rack_id = 1 partial.position = 11 full.position = 12 return "saved" result = _apply_rack_widths(native_apply, placements, resolved, object(), object()) self.assertEqual(result, "saved") placement_objects.update_or_create.assert_called_once_with( device=partial, defaults={"width": 2, "horizontal_position": 1}, ) placement_objects.filter.assert_called_once_with(device=full) placement_objects.filter.return_value.delete.assert_called_once_with() @patch("netbox_utilities.netbox_export.import_module") @patch("netbox_utilities.netbox_export.apps.get_app_config") @patch("netbox_utilities.netbox_export.apps.is_installed", return_value=True) def test_installer_patches_supported_netbox_export(self, _installed, get_config, import_module): get_config.return_value = SimpleNamespace(version="0.3.11") exporter = SimpleNamespace(serialize_object=lambda *args, **kwargs: {}) graph = SimpleNamespace(is_exportable_model=lambda model: False) importer = SimpleNamespace( _defer_device_placement=lambda *args, **kwargs: ({}, None), _device_placement_conflicts=lambda *args, **kwargs: [], _apply_device_placements=lambda *args, **kwargs: None, ) import_module.side_effect = [exporter, graph, importer] self.assertTrue(install_netbox_export_rack_width_support()) for module, name in ( (exporter, "serialize_object"), (graph, "is_exportable_model"), (importer, "_defer_device_placement"), (importer, "_device_placement_conflicts"), (importer, "_apply_device_placements"), ): self.assertEqual(getattr(getattr(module, name), PATCH_MARKER), PATCH_VERSION) @patch("netbox_utilities.netbox_export.import_module") @patch("netbox_utilities.netbox_export.apps.get_app_config") @patch("netbox_utilities.netbox_export.apps.is_installed", return_value=True) def test_installer_ignores_unsupported_netbox_export(self, _installed, get_config, import_module): get_config.return_value = SimpleNamespace(version="0.4.0") self.assertFalse(install_netbox_export_rack_width_support()) import_module.assert_not_called()