Просмотр исходного кода

Enable bulk creation tests for device components

Jeremy Stretch 6 лет назад
Родитель
Сommit
0ad613e6b4

+ 3 - 2
netbox/dcim/forms.py

@@ -2952,9 +2952,10 @@ class FrontPortCreateForm(ComponentForm):
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
         super().__init__(*args, **kwargs)
 
 
-        parent = Device.objects.get(pk=self.initial['device'])
+        parent = Device.objects.get(pk=self.initial.get('device'))
 
 
-        # Determine which rear port positions are occupied. These will be excluded from the list of available mappings.
+        # Determine which rear port positions are occupied. These will be excluded from the list of available
+        # mappings.
         occupied_port_positions = [
         occupied_port_positions = [
             (front_port.rear_port_id, front_port.rear_port_position)
             (front_port.rear_port_id, front_port.rear_port_position)
             for front_port in parent.frontports.all()
             for front_port in parent.frontports.all()

+ 104 - 3
netbox/dcim/tests/test_views.py

@@ -682,10 +682,11 @@ class ConsolePortTestCase(StandardTestCases.Views):
 
 
     # Disable inapplicable views
     # Disable inapplicable views
     test_get_object = None
     test_get_object = None
+    test_create_object = None
     test_bulk_edit_objects = None
     test_bulk_edit_objects = None
 
 
-    # TODO
-    test_create_object = None
+    def test_bulk_create_objects(self):
+        return self._test_bulk_create_objects(expected_count=3)
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
@@ -717,6 +718,14 @@ class ConsolePortTestCase(StandardTestCases.Views):
             "Device 1,Console Port 6",
             "Device 1,Console Port 6",
         )
         )
 
 
+        cls.bulk_create_data = {
+            'device': device.pk,
+            'name_pattern': 'Console Port [4-6]',
+            'type': ConsolePortTypeChoices.TYPE_RJ45,
+            'description': 'A console port',
+            'tags': 'Alpha,Bravo,Charlie',
+        }
+
 
 
 class ConsoleServerPortTestCase(StandardTestCases.Views):
 class ConsoleServerPortTestCase(StandardTestCases.Views):
     model = ConsoleServerPort
     model = ConsoleServerPort
@@ -727,6 +736,9 @@ class ConsoleServerPortTestCase(StandardTestCases.Views):
     # TODO
     # TODO
     test_create_object = None
     test_create_object = None
 
 
+    def test_bulk_create_objects(self):
+        return self._test_bulk_create_objects(expected_count=3)
+
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
         device = create_test_device('Device 1')
         device = create_test_device('Device 1')
@@ -756,6 +768,14 @@ class ConsoleServerPortTestCase(StandardTestCases.Views):
             "Device 1,Console Server Port 6",
             "Device 1,Console Server Port 6",
         )
         )
 
 
+        cls.bulk_create_data = {
+            'device': device.pk,
+            'name_pattern': 'Console Server Port [4-6]',
+            'type': ConsolePortTypeChoices.TYPE_RJ45,
+            'description': 'A console server port',
+            'tags': 'Alpha,Bravo,Charlie',
+        }
+
         cls.bulk_edit_data = {
         cls.bulk_edit_data = {
             'device': device.pk,
             'device': device.pk,
             'type': ConsolePortTypeChoices.TYPE_RJ45,
             'type': ConsolePortTypeChoices.TYPE_RJ45,
@@ -773,6 +793,9 @@ class PowerPortTestCase(StandardTestCases.Views):
     # TODO
     # TODO
     test_create_object = None
     test_create_object = None
 
 
+    def test_bulk_create_objects(self):
+        return self._test_bulk_create_objects(expected_count=3)
+
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
         device = create_test_device('Device 1')
         device = create_test_device('Device 1')
@@ -804,6 +827,16 @@ class PowerPortTestCase(StandardTestCases.Views):
             "Device 1,Power Port 6",
             "Device 1,Power Port 6",
         )
         )
 
 
+        cls.bulk_create_data = {
+            'device': device.pk,
+            'name_pattern': 'Power Port [4-6]]',
+            'type': PowerPortTypeChoices.TYPE_IEC_C14,
+            'maximum_draw': 100,
+            'allocated_draw': 50,
+            'description': 'A power port',
+            'tags': 'Alpha,Bravo,Charlie',
+        }
+
 
 
 class PowerOutletTestCase(StandardTestCases.Views):
 class PowerOutletTestCase(StandardTestCases.Views):
     model = PowerOutlet
     model = PowerOutlet
@@ -814,6 +847,9 @@ class PowerOutletTestCase(StandardTestCases.Views):
     # TODO
     # TODO
     test_create_object = None
     test_create_object = None
 
 
+    def test_bulk_create_objects(self):
+        return self._test_bulk_create_objects(expected_count=3)
+
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
         device = create_test_device('Device 1')
         device = create_test_device('Device 1')
@@ -851,6 +887,16 @@ class PowerOutletTestCase(StandardTestCases.Views):
             "Device 1,Power Outlet 6",
             "Device 1,Power Outlet 6",
         )
         )
 
 
+        cls.bulk_create_data = {
+            'device': device.pk,
+            'name_pattern': 'Power Outlet [4-6]',
+            'type': PowerOutletTypeChoices.TYPE_IEC_C13,
+            'power_port': powerports[1].pk,
+            'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B,
+            'description': 'A power outlet',
+            'tags': 'Alpha,Bravo,Charlie',
+        }
+
         cls.bulk_edit_data = {
         cls.bulk_edit_data = {
             'device': device.pk,
             'device': device.pk,
             'type': PowerOutletTypeChoices.TYPE_IEC_C13,
             'type': PowerOutletTypeChoices.TYPE_IEC_C13,
@@ -866,6 +912,9 @@ class InterfaceTestCase(StandardTestCases.Views):
     # TODO
     # TODO
     test_create_object = None
     test_create_object = None
 
 
+    def test_bulk_create_objects(self):
+        return self._test_bulk_create_objects(expected_count=3)
+
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
         device = create_test_device('Device 1')
         device = create_test_device('Device 1')
@@ -914,6 +963,22 @@ class InterfaceTestCase(StandardTestCases.Views):
             "Device 1,Interface 6,1000BASE-T (1GE)",
             "Device 1,Interface 6,1000BASE-T (1GE)",
         )
         )
 
 
+        cls.bulk_create_data = {
+            'device': device.pk,
+            'name_pattern': 'Interface [4-6]',
+            'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
+            'enabled': False,
+            'lag': interfaces[3].pk,
+            'mac_address': EUI('01:02:03:04:05:06'),
+            'mtu': 2000,
+            'mgmt_only': True,
+            'description': 'A front port',
+            'mode': InterfaceModeChoices.MODE_TAGGED,
+            'untagged_vlan': vlans[0].pk,
+            'tagged_vlans': [v.pk for v in vlans[1:4]],
+            'tags': 'Alpha,Bravo,Charlie',
+        }
+
         cls.bulk_edit_data = {
         cls.bulk_edit_data = {
             'device': device.pk,
             'device': device.pk,
             'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
             'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
@@ -938,6 +1003,9 @@ class FrontPortTestCase(StandardTestCases.Views):
     # TODO
     # TODO
     test_create_object = None
     test_create_object = None
 
 
+    def test_bulk_create_objects(self):
+        return self._test_bulk_create_objects(expected_count=3)
+
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
         device = create_test_device('Device 1')
         device = create_test_device('Device 1')
@@ -978,6 +1046,17 @@ class FrontPortTestCase(StandardTestCases.Views):
             "Device 1,Front Port 6,8P8C,Rear Port 6,1",
             "Device 1,Front Port 6,8P8C,Rear Port 6,1",
         )
         )
 
 
+        cls.bulk_create_data = {
+            'device': device.pk,
+            'name_pattern': 'Front Port [4-6]',
+            'type': PortTypeChoices.TYPE_8P8C,
+            'rear_port_set': [
+                '{}:1'.format(rp.pk) for rp in rearports[3:6]
+            ],
+            'description': 'New description',
+            'tags': 'Alpha,Bravo,Charlie',
+        }
+
         cls.bulk_edit_data = {
         cls.bulk_edit_data = {
             'type': PortTypeChoices.TYPE_8P8C,
             'type': PortTypeChoices.TYPE_8P8C,
             'description': 'New description',
             'description': 'New description',
@@ -993,6 +1072,9 @@ class RearPortTestCase(StandardTestCases.Views):
     # TODO
     # TODO
     test_create_object = None
     test_create_object = None
 
 
+    def test_bulk_create_objects(self):
+        return self._test_bulk_create_objects(expected_count=3)
+
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
         device = create_test_device('Device 1')
         device = create_test_device('Device 1')
@@ -1022,6 +1104,15 @@ class RearPortTestCase(StandardTestCases.Views):
             "Device 1,Rear Port 6,8P8C,1",
             "Device 1,Rear Port 6,8P8C,1",
         )
         )
 
 
+        cls.bulk_create_data = {
+            'device': device.pk,
+            'name_pattern': 'Rear Port [4-6]',
+            'type': PortTypeChoices.TYPE_8P8C,
+            'positions': 3,
+            'description': 'A rear port',
+            'tags': 'Alpha,Bravo,Charlie',
+        }
+
         cls.bulk_edit_data = {
         cls.bulk_edit_data = {
             'type': PortTypeChoices.TYPE_8P8C,
             'type': PortTypeChoices.TYPE_8P8C,
             'description': 'New description',
             'description': 'New description',
@@ -1033,11 +1124,14 @@ class DeviceBayTestCase(StandardTestCases.Views):
 
 
     # Disable inapplicable views
     # Disable inapplicable views
     test_get_object = None
     test_get_object = None
+    test_create_object = None
 
 
     # TODO
     # TODO
-    test_create_object = None
     test_bulk_edit_objects = None
     test_bulk_edit_objects = None
 
 
+    def test_bulk_create_objects(self):
+        return self._test_bulk_create_objects(expected_count=3)
+
     @classmethod
     @classmethod
     def setUpTestData(cls):
     def setUpTestData(cls):
         device1 = create_test_device('Device 1')
         device1 = create_test_device('Device 1')
@@ -1069,6 +1163,13 @@ class DeviceBayTestCase(StandardTestCases.Views):
             "Device 1,Device Bay 6",
             "Device 1,Device Bay 6",
         )
         )
 
 
+        cls.bulk_create_data = {
+            'device': device2.pk,
+            'name_pattern': 'Device Bay [4-6]',
+            'description': 'A device bay',
+            'tags': 'Alpha,Bravo,Charlie',
+        }
+
 
 
 class InventoryItemTestCase(StandardTestCases.Views):
 class InventoryItemTestCase(StandardTestCases.Views):
     model = InventoryItem
     model = InventoryItem

+ 68 - 0
netbox/utilities/testing/testcases.py

@@ -1,5 +1,6 @@
 from django.contrib.auth.models import Permission, User
 from django.contrib.auth.models import Permission, User
 from django.core.exceptions import ObjectDoesNotExist
 from django.core.exceptions import ObjectDoesNotExist
+from django.forms.models import model_to_dict as model_to_dict_
 from django.test import Client, TestCase as _TestCase, override_settings
 from django.test import Client, TestCase as _TestCase, override_settings
 from django.urls import reverse, NoReverseMatch
 from django.urls import reverse, NoReverseMatch
 from rest_framework.test import APIClient
 from rest_framework.test import APIClient
@@ -56,6 +57,30 @@ class TestCase(_TestCase):
             expected_status, response.status_code, getattr(response, 'data', 'No data')
             expected_status, response.status_code, getattr(response, 'data', 'No data')
         ))
         ))
 
 
+    def assertInstanceEquals(self, instance, data):
+        """
+        Compare a model instance to a dictionary, checking that its attribute values match those specified
+        in the dictionary.
+        """
+        model_dict = model_to_dict_(instance, fields=data.keys())
+
+        for key in list(model_dict.keys()):
+
+            # TODO: Differentiate between tags assigned to the instance and a M2M field for tags (ex: ConfigContext)
+            if key == 'tags':
+                model_dict[key] = ','.join(sorted([tag.name for tag in model_dict['tags']]))
+
+            # Convert ManyToManyField to list of instance PKs
+            elif model_dict[key] and type(model_dict[key]) in (list, tuple) and hasattr(model_dict[key][0], 'pk'):
+                model_dict[key] = [obj.pk for obj in model_dict[key]]
+
+        # Omit any dictionary keys which are not instance attributes
+        relevant_data = {
+            k: v for k, v in data.items() if hasattr(instance, k)
+        }
+
+        self.assertDictEqual(model_dict, relevant_data)
+
 
 
 class APITestCase(TestCase):
 class APITestCase(TestCase):
     client_class = APIClient
     client_class = APIClient
@@ -92,6 +117,9 @@ class StandardTestCases:
         # CSV lines used for bulk import of new objects
         # CSV lines used for bulk import of new objects
         csv_data = ()
         csv_data = ()
 
 
+        # Form data used when creating multiple objects
+        bulk_create_data = {}
+
         # Form data to be used when editing multiple objects at once
         # Form data to be used when editing multiple objects at once
         bulk_edit_data = {}
         bulk_edit_data = {}
 
 
@@ -104,6 +132,10 @@ class StandardTestCases:
             if self.model is None:
             if self.model is None:
                 raise Exception("Test case requires model to be defined")
                 raise Exception("Test case requires model to be defined")
 
 
+        #
+        # URL functions
+        #
+
         def _get_base_url(self):
         def _get_base_url(self):
             """
             """
             Return the base format for a URL for the test's model. Override this to test for a model which belongs
             Return the base format for a URL for the test's model. Override this to test for a model which belongs
@@ -138,6 +170,13 @@ class StandardTestCases:
             else:
             else:
                 raise Exception("Invalid action for URL resolution: {}".format(action))
                 raise Exception("Invalid action for URL resolution: {}".format(action))
 
 
+        #
+        # Standard view tests
+        # These methods will run by default. To disable a test, nullify its method on the subclasses TestCase:
+        #
+        #     test_list_objects = None
+        #
+
         @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
         @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
         def test_list_objects(self):
         def test_list_objects(self):
             # Attempt to make the request without required permissions
             # Attempt to make the request without required permissions
@@ -331,3 +370,32 @@ class StandardTestCases:
 
 
             # Check that all objects were deleted
             # Check that all objects were deleted
             self.assertEqual(self.model.objects.count(), 0)
             self.assertEqual(self.model.objects.count(), 0)
+
+        #
+        # Optional view tests
+        # These methods will run only if the required data
+        #
+
+        @override_settings(EXEMPT_VIEW_PERMISSIONS=[])
+        def _test_bulk_create_objects(self, expected_count):
+            initial_count = self.model.objects.count()
+            request = {
+                'path': self._get_url('add'),
+                'data': post_data(self.bulk_create_data),
+                'follow': False,  # Do not follow 302 redirects
+            }
+
+            # Attempt to make the request without required permissions
+            with disable_warnings('django.request'):
+                self.assertHttpStatus(self.client.post(**request), 403)
+
+            # Assign the required permission and submit again
+            self.add_permissions(
+                '{}.add_{}'.format(self.model._meta.app_label, self.model._meta.model_name)
+            )
+            response = self.client.post(**request)
+            self.assertHttpStatus(response, 302)
+
+            self.assertEqual(initial_count + expected_count, self.model.objects.count())
+            for instance in self.model.objects.order_by('-pk')[:expected_count]:
+                self.assertInstanceEquals(instance, self.bulk_create_data)

+ 1 - 1
netbox/virtualization/forms.py

@@ -812,7 +812,7 @@ class InterfaceCreateForm(ComponentForm):
                 (group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
                 (group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
             )
             )
 
 
-        parent = VirtualMachine.objects.get(pk=self.initial['virtual_machine'])
+        parent = VirtualMachine.objects.get(pk=self.initial.get('virtual_machine'))
         site = getattr(parent.cluster, 'site', None)
         site = getattr(parent.cluster, 'site', None)
         if site is not None:
         if site is not None:
 
 

+ 18 - 2
netbox/virtualization/tests/test_views.py

@@ -198,10 +198,11 @@ class InterfaceTestCase(StandardTestCases.Views):
 
 
     # Disable inapplicable tests
     # Disable inapplicable tests
     test_list_objects = None
     test_list_objects = None
+    test_create_object = None
     test_import_objects = None
     test_import_objects = None
 
 
-    # TODO
-    test_create_object = None
+    def test_bulk_create_objects(self):
+        return self._test_bulk_create_objects(expected_count=3)
 
 
     def _get_base_url(self):
     def _get_base_url(self):
         # Interface belongs to the DCIM app, so we have to override the base URL
         # Interface belongs to the DCIM app, so we have to override the base URL
@@ -262,6 +263,21 @@ class InterfaceTestCase(StandardTestCases.Views):
             "Device 1,Interface 6,1000BASE-T (1GE)",
             "Device 1,Interface 6,1000BASE-T (1GE)",
         )
         )
 
 
+        cls.bulk_create_data = {
+            'virtual_machine': virtualmachines[1].pk,
+            'name_pattern': 'Interface [4-6]',
+            'type': InterfaceTypeChoices.TYPE_VIRTUAL,
+            'enabled': False,
+            'mgmt_only': False,
+            'mac_address': EUI('01-02-03-04-05-06'),
+            'mtu': 2000,
+            'description': 'New description',
+            'mode': InterfaceModeChoices.MODE_TAGGED,
+            'untagged_vlan': vlans[0].pk,
+            'tagged_vlans': [v.pk for v in vlans[1:4]],
+            'tags': 'Alpha,Bravo,Charlie',
+        }
+
         cls.bulk_edit_data = {
         cls.bulk_edit_data = {
             'virtual_machine': virtualmachines[1].pk,
             'virtual_machine': virtualmachines[1].pk,
             'enabled': False,
             'enabled': False,