from functools import wraps from importlib import import_module from pathlib import Path from types import SimpleNamespace from unittest.mock import MagicMock, call, patch from django.conf import settings from django.contrib.auth.context_processors import PermWrapper from django.contrib.auth.models import AnonymousUser from django.core.exceptions import ValidationError from django.template.loader import render_to_string from django.test import RequestFactory, SimpleTestCase from django.urls import resolve, reverse from netbox_utilities.rack_width import ( _cleanup_unracked_placement, _install_rack_methods, _rack_elevation_devices, available_units_for_device, horizontal_interval, include_rack_width_in_fieldsets, intervals_cover_full_width, intervals_overlap, normalize_width_position, placement_rectangles_overlap, stage_width_position, ) class RackWidthTest(SimpleTestCase): def test_rack_width_javascript_synchronizes_netbox_tom_select(self): script = (Path(__file__).parents[1] / "static" / "netbox_utilities" / "forms.js").read_text(encoding="utf-8") self.assertIn("positionField.disabled = width === 1", script) self.assertIn("positionField.tomselect.sync()", script) def test_svg_devices_include_core_device_bay_annotation_and_optional_placement(self): from dcim.choices import DeviceFaceChoices from dcim.models import Rack devices = _rack_elevation_devices(Rack(pk=3), DeviceFaceChoices.FACE_FRONT) self.assertIn("devicebay_count", devices.query.annotations) self.assertIn("netbox_utilities_rack_placement", devices.query.select_related) self.assertIn("device_type", devices.query.select_related) self.assertIn("manufacturer", devices.query.select_related["device_type"]) self.assertNotIn("netbox_utilities_rack_placement__isnull", str(devices.query)) def test_core_rack_units_remain_available_to_other_plugins(self): from dcim.models import Rack self.assertEqual(Rack.get_rack_units.__module__, "dcim.models.racks") self.assertFalse(hasattr(Rack.get_rack_units, "__wrapped__")) def test_replaces_legacy_rack_unit_wrapper_during_upgrade(self): def native_rack_units(rack): return rack def native_available_units(rack, *args, **kwargs): return rack, args, kwargs @wraps(native_rack_units) def legacy_rack_units(rack): return native_rack_units(rack) @wraps(native_available_units) def legacy_available_units(rack, *args, **kwargs): return native_available_units(rack, *args, **kwargs) class LegacyRack: get_rack_units = legacy_rack_units get_available_units = legacy_available_units _netbox_utilities_rack_width_installed = True with patch.dict("sys.modules", {"dcim.models": SimpleNamespace(Rack=LegacyRack)}): _install_rack_methods() self.assertIs(LegacyRack.get_rack_units, native_rack_units) self.assertIs(LegacyRack.get_available_units.__wrapped__, native_available_units) self.assertEqual(LegacyRack._netbox_utilities_rack_width_version, 2) def test_svg_draws_all_partial_devices_sharing_one_unit_exactly_once(self): from dcim.svg.racks import RackElevationSVG regular = SimpleNamespace( pk=1, face="front", position=12, device_type=SimpleNamespace(u_height=1), netbox_utilities_rack_placement=None, ) partial_right = SimpleNamespace( pk=2, face="front", position=11, device_type=SimpleNamespace(u_height=1), netbox_utilities_rack_placement=SimpleNamespace(width=2, horizontal_position=2), ) partial_left = SimpleNamespace( pk=3, face="front", position=11, device_type=SimpleNamespace(u_height=1), netbox_utilities_rack_placement=SimpleNamespace(width=2, horizontal_position=1), ) rack = SimpleNamespace(get_rack_units=MagicMock()) elevation = SimpleNamespace( rack=rack, unit_width=200, unit_height=20, permitted_device_ids={1, 2, 3}, drawing=SimpleNamespace(add=MagicMock()), _get_device_coords=MagicMock(return_value=(10, 20)), draw_device_front=MagicMock(), draw_device_rear=MagicMock(), ) with patch( "netbox_utilities.rack_width._rack_elevation_devices", return_value=[regular, partial_left, partial_right], ): RackElevationSVG.draw_face(elevation, "front") rack.get_rack_units.assert_not_called() self.assertEqual( elevation.draw_device_front.call_args_list, [ call(regular, (10, 20), (200, 20)), call(partial_left, (10.0, 20), (100.0, 20)), call(partial_right, (110.0, 20), (100.0, 20)), ], ) def test_adds_rack_width_fields_to_existing_location_fieldset(self): from utilities.forms.rendering import FieldSet original = (FieldSet("site", "rack", "position", "latitude", name="Location"),) extended = include_rack_width_in_fieldsets(original) self.assertEqual( extended[0].items, ( "site", "rack", "position", "utilities_rack_width", "utilities_horizontal_position", "latitude", ), ) self.assertEqual(original[0].items, ("site", "rack", "position", "latitude")) def test_appends_visible_fallback_fieldset_when_position_is_missing(self): from utilities.forms.rendering import FieldSet extended = include_rack_width_in_fieldsets((FieldSet("name", name="Device"),)) self.assertEqual(extended[-1].name, "Rackbreite im Rack") self.assertEqual(extended[-1].items, ("utilities_rack_width", "utilities_horizontal_position")) def test_device_edit_view_uses_explicit_rack_width_form(self): from netbox.registry import registry for action in ("add", "edit"): device_view = next(view["view"] for view in registry["views"]["dcim"]["device"] if view["name"] == action) self.assertEqual(device_view.__name__, "RackWidthDeviceEditView") self.assertEqual(device_view.form.__name__, "RackWidthDeviceForm") self.assertEqual(device_view.template_name, "netbox_utilities/device_edit.html") self.assertEqual(device_view.htmx_template_name, "netbox_utilities/device_edit_form.html") self.assertIn("utilities_rack_width", device_view.form.base_fields) self.assertIn("utilities_horizontal_position", device_view.form.base_fields) def test_resolved_device_edit_page_renders_rack_width_fields(self): from dcim.models import Device object_type = SimpleNamespace(pk=1, model_class=lambda: Device) with ( patch("core.models.ObjectType.objects.get_for_model", return_value=object_type), patch( "django.contrib.contenttypes.models.ContentType.objects.get_for_model", return_value=object_type, ), patch("extras.models.CustomField.objects.get_for_model", return_value=[]), patch( "netbox_utilities.runtime._get_database_settings", return_value={"tenant_required": True}, ), ): url = reverse("dcim:device_edit", kwargs={"pk": 1}) view_class = resolve(url).func.view_class form = view_class.form() request = self._anonymous_request() context = { "form": form, "object": SimpleNamespace( pk=None, _meta=Device._meta, device_type=SimpleNamespace(is_child_device=False), parent_bay=None, ), "return_url": "/dcim/devices/", "request": request, "settings": settings, "csrf_token": "", "perms": PermWrapper(request.user), } rendered_pages = ( render_to_string(view_class.htmx_template_name, context), render_to_string(view_class.template_name, context), ) rack_position = 'name="position"' width = 'name="utilities_rack_width"' horizontal_position = 'name="utilities_horizontal_position"' latitude = 'name="latitude"' for html in rendered_pages: self.assertIn(width, html) self.assertIn(horizontal_position, html) self.assertLess(html.index(rack_position), html.index(width)) self.assertLess(html.index(width), html.index(horizontal_position)) self.assertLess(html.index(horizontal_position), html.index(latitude)) def test_resolved_device_add_page_renders_rack_width_fields(self): from dcim.models import Device object_type = SimpleNamespace(pk=1, model_class=lambda: Device) with ( patch("core.models.ObjectType.objects.get_for_model", return_value=object_type), patch( "django.contrib.contenttypes.models.ContentType.objects.get_for_model", return_value=object_type, ), patch("extras.models.CustomField.objects.get_for_model", return_value=[]), patch( "netbox_utilities.runtime._get_database_settings", return_value={"tenant_required": True}, ), ): view_class = resolve(reverse("dcim:device_add")).func.view_class form = view_class.form() request = self._anonymous_request() context = { "form": form, "object": SimpleNamespace( pk=None, _meta=Device._meta, device_type=SimpleNamespace(is_child_device=False), parent_bay=None, ), "return_url": "/dcim/devices/", "request": request, "settings": settings, "csrf_token": "", "perms": PermWrapper(request.user), } rendered_pages = ( render_to_string(view_class.htmx_template_name, context), render_to_string(view_class.template_name, context), ) for html in rendered_pages: self.assertIn('name="utilities_rack_width"', html) self.assertIn('name="utilities_horizontal_position"', html) def test_bound_device_form_clean_handles_netbox_clean_returning_none(self): from dcim.models import Device from netbox.registry import registry edit_view = next(view["view"] for view in registry["views"]["dcim"]["device"] if view["name"] == "edit") object_type = SimpleNamespace(pk=1, model_class=lambda: Device) with ( patch("core.models.ObjectType.objects.get_for_model", return_value=object_type), patch( "django.contrib.contenttypes.models.ContentType.objects.get_for_model", return_value=object_type, ), patch("extras.models.CustomField.objects.get_for_model", return_value=[]), patch( "netbox_utilities.runtime._get_database_settings", return_value={"tenant_required": True}, ), ): form = edit_view.form(data={}) # NetBox 4.6.7's CheckLastUpdatedMixin.clean() updates # self.cleaned_data but deliberately returns None. with patch.object(form, "_post_clean"): self.assertFalse(form.is_valid()) self.assertIsInstance(form.cleaned_data, dict) @staticmethod def _anonymous_request(): request = RequestFactory().get("/dcim/devices/1/edit/") request.user = AnonymousUser() return request def test_normalizes_full_width_to_first_position(self): self.assertEqual(normalize_width_position(1, 4), (1, 1)) def test_rejects_position_outside_selected_width(self): with self.assertRaises(ValidationError): normalize_width_position(2, 3) def test_adjacent_half_widths_do_not_overlap(self): self.assertFalse(intervals_overlap(horizontal_interval(2, 1), horizontal_interval(2, 2))) def test_half_width_overlaps_full_width(self): self.assertTrue(intervals_overlap(horizontal_interval(1, 1), horizontal_interval(2, 2))) def test_two_halves_cover_full_width(self): self.assertTrue(intervals_cover_full_width([horizontal_interval(2, 1), horizontal_interval(2, 2)])) def test_single_half_does_not_cover_full_width(self): self.assertFalse(intervals_cover_full_width([horizontal_interval(2, 1)])) def test_same_unit_adjacent_devices_do_not_collide(self): self.assertFalse(placement_rectangles_overlap(10, 1, 2, 1, 10, 1, 2, 2)) def test_multi_unit_device_collides_only_on_same_horizontal_area(self): self.assertTrue(placement_rectangles_overlap(10, 2, 2, 1, 11, 1, 2, 1)) self.assertFalse(placement_rectangles_overlap(10, 2, 2, 1, 11, 1, 2, 2)) def test_stages_width_without_requiring_an_already_assigned_rack(self): device = SimpleNamespace() stage_width_position(device, 2, 2) self.assertEqual(device._netbox_utilities_rack_width, 2) self.assertEqual(device._netbox_utilities_horizontal_position, 2) @patch("netbox_utilities.rack_width.find_placement_conflict", return_value=None) def test_current_position_is_available_without_conflict(self, _find_conflict): rack = SimpleNamespace(units=[1, 1.5, 2, 2.5]) device = SimpleNamespace( position=1, device_type=SimpleNamespace(u_height=1), ) self.assertEqual(available_units_for_device(rack, device), [1]) @patch("netbox_utilities.rack_width.DeviceRackPlacement.objects") def test_unracked_device_removes_stale_width(self, placement_objects): device = SimpleNamespace(pk=12, rack_id=None, position=None) _cleanup_unracked_placement(None, device, using="default") placement_objects.using.assert_called_once_with("default") placement_objects.using.return_value.filter.assert_called_once_with(device_id=12) placement_objects.using.return_value.filter.return_value.delete.assert_called_once_with() class RackWidthMigrationTest(SimpleTestCase): def test_postgresql_migration_drops_core_uniqueness(self): migration = import_module("netbox_utilities.migrations.0007_devicerackplacement") schema_editor = SimpleNamespace( connection=SimpleNamespace(vendor="postgresql"), execute=MagicMock(), ) migration.drop_core_rack_position_constraint(None, schema_editor) statement = schema_editor.execute.call_args.args[0] self.assertIn("DROP CONSTRAINT IF EXISTS", statement) self.assertIn("dcim_device_unique_rack_position_face", statement)