Procházet zdrojové kódy

Address review feedback on #22989

* Drop the non-BaseModelSerializer fallback in FixSerializedPKRelatedField.
  SerializedPKRelatedField.to_representation() passes nested unconditionally, so a
  serializer which doesn't accept it raises TypeError on every read; the branch
  documented a component for a configuration the API cannot serve.
* Generate the OpenAPI schema once per class rather than once per test method.
* Exercise the component.ref and request-schema return paths, and use SimpleTestCase
  for the tests which don't touch the database.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Jeremy Stretch před 11 hodinami
rodič
revize
9a37694d22
2 změnil soubory, kde provedl 44 přidání a 41 odebrání
  1. 4 11
      netbox/core/api/schema.py
  2. 40 30
      netbox/core/tests/test_openapi_schema.py

+ 4 - 11
netbox/core/api/schema.py

@@ -1,4 +1,3 @@
-import inspect
 import re
 import typing
 from collections import OrderedDict
@@ -18,7 +17,7 @@ from drf_spectacular.types import OpenApiTypes
 from drf_spectacular.utils import Direction, OpenApiParameter
 
 from netbox.api.fields import ChoiceField
-from netbox.api.serializers import BaseModelSerializer, WritableNestedSerializer
+from netbox.api.serializers import WritableNestedSerializer
 from netbox.api.viewsets import NetBoxModelViewSet
 
 # see netbox.api.routers.NetBoxRouter
@@ -391,15 +390,9 @@ class FixSerializedPKRelatedField(OpenApiSerializerFieldExtension):
     def map_serializer_field(self, auto_schema, direction):
         if direction == "response":
             # Resolve an instance of the serializer carrying the field's nested setting, so that the brief
-            # component is referenced wherever the field renders a brief representation. Serializers which
-            # don't derive from BaseModelSerializer (e.g. a plain ModelSerializer employed by a plugin) don't
-            # accept the nested kwarg, so instantiate those without it.
-            serializer = self.target.serializer
-            if inspect.isclass(serializer):
-                if issubclass(serializer, BaseModelSerializer):
-                    serializer = serializer(nested=self.target.nested)
-                else:
-                    serializer = serializer()
+            # component is referenced wherever the field renders a brief representation. (The field's
+            # to_representation() passes nested in the same manner.) See #22989.
+            serializer = self.target.serializer(nested=self.target.nested)
             component = auto_schema.resolve_serializer(serializer, direction)
             return component.ref if component else None
         return build_basic_type(OpenApiTypes.INT)

+ 40 - 30
netbox/core/tests/test_openapi_schema.py

@@ -5,8 +5,7 @@ Refs: #20638
 """
 import json
 
-from django.test import TestCase
-from rest_framework import serializers
+from django.test import SimpleTestCase, TestCase
 
 from core.api.schema import FixSerializedPKRelatedField
 from dcim.api.serializers import SiteSerializer
@@ -17,11 +16,17 @@ from netbox.api.fields import SerializedPKRelatedField
 class OpenAPISchemaTestCase(TestCase):
     """Tests for OpenAPI schema generation."""
 
-    def setUp(self):
-        """Fetch schema via API endpoint."""
-        response = self.client.get('/api/schema/', {'format': 'json'})
-        self.assertEqual(response.status_code, 200)
-        self.schema = json.loads(response.content)
+    @classmethod
+    def setUpClass(cls):
+        """
+        Fetch the schema via the API endpoint. Schema generation is expensive and its output is
+        immutable across these tests, so do this once for the class rather than per test method.
+        """
+        super().setUpClass()
+
+        response = cls.client_class().get('/api/schema/', {'format': 'json'})
+        assert response.status_code == 200, f'Failed to generate OpenAPI schema (HTTP {response.status_code})'
+        cls.schema = json.loads(response.content)
 
     def test_post_operation_documents_single_or_array(self):
         """
@@ -174,30 +179,26 @@ class OpenAPISchemaTestCase(TestCase):
                 self.assertEqual(components[component]['properties'][field]['items']['type'], 'integer')
 
 
-class SerializedPKRelatedFieldSchemaTestCase(TestCase):
+class SerializedPKRelatedFieldSchemaTestCase(SimpleTestCase):
     """Tests for the schema extension which maps SerializedPKRelatedField."""
 
-    class PlainSerializer(serializers.ModelSerializer):
-        """A serializer which does not derive from BaseModelSerializer, as a plugin might employ."""
-
-        class Meta:
-            model = Site
-            fields = ('id', 'name')
+    class DummyComponent:
+        ref = {'$ref': '#/components/schemas/Dummy'}
 
-    def resolve_serializer(self, field):
-        """Invoke the schema extension for a field, returning the serializer instance it resolved."""
-        resolved = []
+    class DummyAutoSchema:
+        """Records the serializer resolved by the extension, in place of generating a component."""
 
-        class DummyAutoSchema:
-            def resolve_serializer(self, serializer, direction):
-                resolved.append(serializer)
+        def __init__(self):
+            self.resolved = []
 
-        FixSerializedPKRelatedField(field).map_serializer_field(DummyAutoSchema(), 'response')
-        return resolved[0]
+        def resolve_serializer(self, serializer, direction):
+            self.resolved.append(serializer)
+            return SerializedPKRelatedFieldSchemaTestCase.DummyComponent
 
-    def test_nested_flag_is_passed_to_netbox_serializers(self):
+    def test_nested_flag_is_passed_to_serializer(self):
         """
-        A serializer derived from BaseModelSerializer must be instantiated with the field's nested setting.
+        The field's serializer must be instantiated with the field's nested setting, so that the
+        component matching the rendered representation is referenced.
 
         Refs: #22989
         """
@@ -208,16 +209,25 @@ class SerializedPKRelatedFieldSchemaTestCase(TestCase):
                     queryset=Site.objects.all(),
                     nested=nested
                 )
-                serializer = self.resolve_serializer(field)
+                auto_schema = self.DummyAutoSchema()
+
+                schema = FixSerializedPKRelatedField(field).map_serializer_field(auto_schema, 'response')
+
+                serializer = auto_schema.resolved[0]
                 self.assertIsInstance(serializer, SiteSerializer)
                 self.assertEqual(serializer.nested, nested)
+                self.assertEqual(schema, self.DummyComponent.ref)
 
-    def test_serializer_without_nested_support(self):
+    def test_request_schema_is_an_integer(self):
         """
-        A serializer which does not accept the nested kwarg must still resolve, rather than breaking
-        generation of the entire schema.
+        Request schemas must document an integer primary key, regardless of the nested setting.
 
         Refs: #22989
         """
-        field = SerializedPKRelatedField(serializer=self.PlainSerializer, queryset=Site.objects.all())
-        self.assertIsInstance(self.resolve_serializer(field), self.PlainSerializer)
+        field = SerializedPKRelatedField(serializer=SiteSerializer, queryset=Site.objects.all(), nested=True)
+        auto_schema = self.DummyAutoSchema()
+
+        schema = FixSerializedPKRelatedField(field).map_serializer_field(auto_schema, 'request')
+
+        self.assertEqual(schema['type'], 'integer')
+        self.assertEqual(auto_schema.resolved, [])