Repository: nautobot/nautobot

Issue:
Update test test_bulk_delete_form_contains_all_filtered failure message
<!--
    NOTE: This template is for use by maintainers only. Please do not submit
    an issue using this template unless you have been specifically asked to
    do so.
-->
### Proposed Changes

`nautobot.core.testing.views.ViewTestCases.BulkDeleteObjectsViewTestCase.test_bulk_delete_form_contains_all_filtered` fails with the following message when fewer than 3 objects are returned from the test's `_get_queryset()`:

```
======================================================================
ERROR: test_bulk_delete_form_contains_all_filtered (nautobot_device_onboarding.tests.test_views.OnboardingTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/usr/local/lib/python3.8/site-packages/django/test/utils.py", line 387, in inner
    return func(*args, **kwargs)
  File "/usr/local/lib/python3.8/site-packages/nautobot/core/testing/views.py", line 1224, in test_bulk_delete_form_contains_all_filtered
    third_pk = next(pk_iter)
StopIteration
```

We need to update this test so that the error message is more clear about what is broken and how to fix it.

<!-- Provide justification for the proposed change(s). -->
### Justification

Developer experience


Discussion:

Resolving patch:
diff --git a/nautobot/dcim/forms.py b/nautobot/dcim/forms.py
--- a/nautobot/dcim/forms.py
+++ b/nautobot/dcim/forms.py
@@ -2139,9 +2139,6 @@ class InterfaceForm(InterfaceCommonForm, NautobotModelForm):
         queryset=Interface.objects.all(),
         required=False,
         label="Parent interface",
-        query_params={
-            "kind": "physical",
-        },
         help_text="Assigned parent interface",
     )
     bridge = DynamicModelChoiceField(
diff --git a/nautobot/dcim/models/device_components.py b/nautobot/dcim/models/device_components.py
--- a/nautobot/dcim/models/device_components.py
+++ b/nautobot/dcim/models/device_components.py
@@ -604,18 +604,13 @@ def clean(self):
             # An interface cannot be its own parent
             if self.parent_interface_id == self.pk:
                 raise ValidationError({"parent_interface": "An interface cannot be its own parent."})
+
             # A physical interface cannot have a parent interface
             if hasattr(self, "type") and self.type != InterfaceTypeChoices.TYPE_VIRTUAL:
                 raise ValidationError(
                     {"parent_interface": "Only virtual interfaces may be assigned to a parent interface."}
                 )
 
-            # A virtual interface cannot be a parent interface
-            if getattr(self.parent_interface, "type", None) == InterfaceTypeChoices.TYPE_VIRTUAL:
-                raise ValidationError(
-                    {"parent_interface": "Virtual interfaces may not be parents of other interfaces."}
-                )
-
             # An interface's parent must belong to the same device or virtual chassis
             if self.parent_interface.device != self.device:
                 if getattr(self.device, "virtual_chassis", None) is None:
diff --git a/tasks.py b/tasks.py
--- a/tasks.py
+++ b/tasks.py
@@ -979,7 +979,7 @@ def eslint(context, autoformat=False):
         docker_compose(
             context,
             "run --workdir='/opt/nautobot/ui' -e NODE_ENV=test "
-            f"--entrypoint '{eslint_command} /opt/nautobot/ui' nautobot",
+            f"--entrypoint '{eslint_command} /opt/nautobot/ui' nodejs",
         )
 
 


Test patch:
diff --git a/nautobot/core/testing/views.py b/nautobot/core/testing/views.py
--- a/nautobot/core/testing/views.py
+++ b/nautobot/core/testing/views.py
@@ -1061,9 +1061,12 @@ def test_bulk_edit_form_contains_all_filtered(self):
             self.add_permissions(f"{self.model._meta.app_label}.change_{self.model._meta.model_name}")
 
             pk_iter = iter(self._get_queryset().values_list("pk", flat=True))
-            first_pk = next(pk_iter)
-            second_pk = next(pk_iter)
-            third_pk = next(pk_iter)
+            try:
+                first_pk = next(pk_iter)
+                second_pk = next(pk_iter)
+                third_pk = next(pk_iter)
+            except StopIteration:
+                self.fail(f"Test requires at least three instances of {self.model._meta.model_name} to be defined.")
 
             post_data = testing.post_data(self.bulk_edit_data)
 
@@ -1219,9 +1222,12 @@ def test_bulk_delete_form_contains_all_filtered(self):
             self.add_permissions(f"{self.model._meta.app_label}.delete_{self.model._meta.model_name}")
 
             pk_iter = iter(self._get_queryset().values_list("pk", flat=True))
-            first_pk = next(pk_iter)
-            second_pk = next(pk_iter)
-            third_pk = next(pk_iter)
+            try:
+                first_pk = next(pk_iter)
+                second_pk = next(pk_iter)
+                third_pk = next(pk_iter)
+            except StopIteration:
+                self.fail(f"Test requires at least three instances of {self.model._meta.model_name} to be defined.")
 
             # Open bulk delete form with first two objects
             selected_data = {
diff --git a/nautobot/dcim/tests/test_views.py b/nautobot/dcim/tests/test_views.py
--- a/nautobot/dcim/tests/test_views.py
+++ b/nautobot/dcim/tests/test_views.py
@@ -1805,6 +1805,7 @@ def setUpTestData(cls):
                 device=device, name="BRIDGE", status=status_active, type=InterfaceTypeChoices.TYPE_BRIDGE
             ),
         )
+        cls.lag_interface = interfaces[3]
         # Required by ViewTestCases.DeviceComponentViewTestCase.test_bulk_rename
         cls.selected_objects = interfaces
         cls.selected_objects_parent_name = device.name
@@ -1897,6 +1898,25 @@ def setUpTestData(cls):
             f"1000base-t,Interface 6,{device.name},{device.location.name},{statuses[1].name}",
         )
 
+    @override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
+    def test_create_virtual_interface_with_parent_lag(self):
+        """https://github.com/nautobot/nautobot/issues/4436."""
+        self.add_permissions("dcim.add_interface")
+        form_data = self.form_data.copy()
+        del form_data["name"]
+        form_data["name_pattern"] = "LAG.0"
+        form_data["type"] = InterfaceTypeChoices.TYPE_VIRTUAL
+        form_data["parent_interface"] = self.lag_interface
+        del form_data["lag"]
+        request = {
+            "path": self._get_url("add"),
+            "data": post_data(form_data),
+        }
+        self.assertHttpStatus(self.client.post(**request), 302)
+        instance = self._get_queryset().order_by("last_updated").last()
+        self.assertEqual(instance.type, InterfaceTypeChoices.TYPE_VIRTUAL)
+        self.assertEqual(instance.parent_interface, self.lag_interface)
+
 
 class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
     model = FrontPort