diff --git a/netbox/dcim/api/serializers_/racks.py b/netbox/dcim/api/serializers_/racks.py index a088333e2..e021e78f4 100644 --- a/netbox/dcim/api/serializers_/racks.py +++ b/netbox/dcim/api/serializers_/racks.py @@ -86,6 +86,9 @@ class RackBaseSerializer(PrimaryModelSerializer): class RackTypeSerializer(RackBaseSerializer): + # Unlike Rack.form_factor (optional & nullable), RackType.form_factor is required + # (blank=False, no default), so override RackBaseSerializer's optional declaration. + form_factor = ChoiceField(choices=RackFormFactorChoices) manufacturer = ManufacturerSerializer(nested=True) rack_count = serializers.IntegerField(read_only=True) diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 5ba406ee5..74588d2c2 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -1206,6 +1206,28 @@ class RackTypeTestCase(APIViewTestCases.APIViewTestCase): }, ] + def test_form_factor_required(self): + """ + Regression test for #23154: form_factor must be reported as required by + OPTIONS, and a POST omitting it must be rejected with a normal "required" + validation error rather than a model-level "cannot be blank" error. + """ + self.add_permissions('dcim.add_racktype') + + response = self.client.options(self._get_list_url(), **self.header) + self.assertHttpStatus(response, status.HTTP_200_OK) + self.assertTrue(response.data['actions']['POST']['form_factor']['required']) + + manufacturer = Manufacturer.objects.first() + data = { + 'manufacturer': manufacturer.pk, + 'model': 'Rack Type Missing Form Factor', + 'slug': 'rack-type-missing-form-factor', + } + response = self.client.post(self._get_list_url(), data, format='json', **self.header) + self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST) + self.assertIn('form_factor', response.data) + class RackTestCase(APIViewTestCases.APIViewTestCase): model = Rack diff --git a/netbox/vpn/api/serializers_/l2vpn.py b/netbox/vpn/api/serializers_/l2vpn.py index 874ae342e..b02fa958e 100644 --- a/netbox/vpn/api/serializers_/l2vpn.py +++ b/netbox/vpn/api/serializers_/l2vpn.py @@ -16,7 +16,7 @@ __all__ = ( class L2VPNSerializer(PrimaryModelSerializer): - type = ChoiceField(choices=L2VPNTypeChoices, required=False) + type = ChoiceField(choices=L2VPNTypeChoices, required=True) import_targets = SerializedPKRelatedField( queryset=RouteTarget.objects.all(), serializer=RouteTargetSerializer, diff --git a/netbox/vpn/tests/test_api.py b/netbox/vpn/tests/test_api.py index 25695bbf0..2db24f65e 100644 --- a/netbox/vpn/tests/test_api.py +++ b/netbox/vpn/tests/test_api.py @@ -601,6 +601,26 @@ class L2VPNTestCase(APIViewTestCases.APIViewTestCase): self.assertHttpStatus(response, status.HTTP_200_OK) self.assertEqual(response_data['count'], 1) + def test_type_required(self): + """ + Regression test for #23154: type must be reported as required by OPTIONS, + and a POST omitting it must be rejected with a normal "required" validation + error rather than a model-level "cannot be blank" error. + """ + self.add_permissions('vpn.add_l2vpn') + + response = self.client.options(self._get_list_url(), **self.header) + self.assertHttpStatus(response, status.HTTP_200_OK) + self.assertTrue(response.data['actions']['POST']['type']['required']) + + data = { + 'name': 'L2VPN Missing Type', + 'slug': 'l2vpn-missing-type', + } + response = self.client.post(self._get_list_url(), data, format='json', **self.header) + self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST) + self.assertIn('type', response.data) + class L2VPNTerminationTestCase(APIViewTestCases.APIViewTestCase): model = L2VPNTermination