feat: add atomic multi-device rack reordering
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
from decimal import Decimal
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
|
||||
from netbox_utilities.reorder_rack import (
|
||||
RackLayoutError,
|
||||
apply_rack_layout,
|
||||
bulk_reorder_update,
|
||||
install_reorder_rack_bulk_save,
|
||||
parse_device_placements,
|
||||
)
|
||||
|
||||
|
||||
def rack_layout(front=None, rear=None, other=None):
|
||||
return {
|
||||
"front": front or [],
|
||||
"rear": rear or [],
|
||||
"other": other or [],
|
||||
}
|
||||
|
||||
|
||||
class RackLayoutParsingTest(SimpleTestCase):
|
||||
def test_parses_multiple_device_positions(self):
|
||||
placements = parse_device_placements(
|
||||
rack_layout(
|
||||
front=[{"id": 10, "y": 4}, {"id": 11, "y": 8.5}],
|
||||
other=[{"id": 12, "y": None}],
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
[(item.device_id, item.position, item.face) for item in placements],
|
||||
[
|
||||
(10, Decimal(4), "front"),
|
||||
(11, Decimal("8.5"), "front"),
|
||||
(12, None, ""),
|
||||
],
|
||||
)
|
||||
|
||||
def test_rejects_duplicate_devices(self):
|
||||
with self.assertRaisesMessage(RackLayoutError, "mehrfach"):
|
||||
parse_device_placements(
|
||||
rack_layout(
|
||||
front=[{"id": 10, "y": 4}],
|
||||
rear=[{"id": 10, "y": 6}],
|
||||
)
|
||||
)
|
||||
|
||||
def test_rejects_invalid_positions(self):
|
||||
with self.assertRaisesMessage(RackLayoutError, "keine gültige Rack-Position"):
|
||||
parse_device_placements(rack_layout(front=[{"id": 10, "y": "not-a-position"}]))
|
||||
|
||||
|
||||
class ApplyRackLayoutTest(SimpleTestCase):
|
||||
@patch("netbox_utilities.reorder_rack.get_permission_for_model", return_value="dcim.change_device")
|
||||
@patch("netbox_utilities.reorder_rack.Device")
|
||||
def test_clears_all_old_positions_before_saving_targets(self, device_model, _get_permission):
|
||||
events = []
|
||||
first = MagicMock(pk=10, position=Decimal(1), face="front")
|
||||
second = MagicMock(pk=11, position=Decimal(2), face="front")
|
||||
first.snapshot.side_effect = lambda: events.append("snapshot-10")
|
||||
second.snapshot.side_effect = lambda: events.append("snapshot-11")
|
||||
first.full_clean.side_effect = lambda: events.append("validate-10")
|
||||
second.full_clean.side_effect = lambda: events.append("validate-11")
|
||||
first.save.side_effect = lambda: events.append("save-10")
|
||||
second.save.side_effect = lambda: events.append("save-11")
|
||||
|
||||
device_model.objects.select_for_update.return_value.filter.return_value = [first, second]
|
||||
device_model.objects.filter.return_value.update.side_effect = lambda **_kwargs: events.append("clear")
|
||||
user = MagicMock()
|
||||
user.has_perm.return_value = True
|
||||
placements = parse_device_placements(rack_layout(front=[{"id": 10, "y": 2}, {"id": 11, "y": 1}]))
|
||||
|
||||
changed = apply_rack_layout(rack=SimpleNamespace(pk=5), placements=placements, user=user)
|
||||
|
||||
self.assertEqual(changed, [first, second])
|
||||
self.assertLess(events.index("clear"), events.index("validate-10"))
|
||||
self.assertLess(events.index("clear"), events.index("validate-11"))
|
||||
device_model.objects.filter.return_value.update.assert_called_once_with(position=None, face="")
|
||||
self.assertEqual((first.position, first.face), (Decimal(2), "front"))
|
||||
self.assertEqual((second.position, second.face), (Decimal(1), "front"))
|
||||
|
||||
@patch("netbox_utilities.reorder_rack.get_permission_for_model", return_value="dcim.change_device")
|
||||
@patch("netbox_utilities.reorder_rack.Device")
|
||||
def test_rejects_changed_device_without_permission(self, device_model, _get_permission):
|
||||
device = MagicMock(pk=10, position=Decimal(1), face="front")
|
||||
device_model.objects.select_for_update.return_value.filter.return_value = [device]
|
||||
user = MagicMock()
|
||||
user.has_perm.return_value = False
|
||||
placements = parse_device_placements(rack_layout(front=[{"id": 10, "y": 2}]))
|
||||
|
||||
with self.assertRaises(PermissionDenied):
|
||||
apply_rack_layout(rack=SimpleNamespace(pk=5), placements=placements, user=user)
|
||||
|
||||
device_model.objects.filter.assert_not_called()
|
||||
device.save.assert_not_called()
|
||||
|
||||
|
||||
class ReorderRackPatchTest(SimpleTestCase):
|
||||
def test_patches_supported_plugin_only_once(self):
|
||||
original_update = object()
|
||||
|
||||
class SaveViewSet:
|
||||
update = original_update
|
||||
|
||||
views = SimpleNamespace(SaveViewSet=SaveViewSet)
|
||||
plugin_config = SimpleNamespace(version="1.1.4")
|
||||
|
||||
with (
|
||||
patch("netbox_utilities.reorder_rack.apps.is_installed", return_value=True),
|
||||
patch("netbox_utilities.reorder_rack.apps.get_app_config", return_value=plugin_config),
|
||||
patch("netbox_utilities.reorder_rack.import_module", return_value=views),
|
||||
):
|
||||
self.assertTrue(install_reorder_rack_bulk_save())
|
||||
self.assertTrue(install_reorder_rack_bulk_save())
|
||||
|
||||
self.assertIs(SaveViewSet.update, bulk_reorder_update)
|
||||
self.assertIs(SaveViewSet._netbox_utilities_original_update, original_update)
|
||||
|
||||
def test_skips_unsupported_plugin_version(self):
|
||||
with (
|
||||
patch("netbox_utilities.reorder_rack.apps.is_installed", return_value=True),
|
||||
patch(
|
||||
"netbox_utilities.reorder_rack.apps.get_app_config",
|
||||
return_value=SimpleNamespace(version="2.0.0"),
|
||||
),
|
||||
patch("netbox_utilities.reorder_rack.import_module") as import_module_mock,
|
||||
):
|
||||
self.assertFalse(install_reorder_rack_bulk_save())
|
||||
|
||||
import_module_mock.assert_not_called()
|
||||
Reference in New Issue
Block a user