|
|
@@ -0,0 +1,108 @@
|
|
|
+"""
|
|
|
+Unit tests for OpenAPI schema generation.
|
|
|
+
|
|
|
+Refs: #20638
|
|
|
+"""
|
|
|
+import json
|
|
|
+from django.test import TestCase
|
|
|
+
|
|
|
+
|
|
|
+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)
|
|
|
+
|
|
|
+ def test_post_operation_documents_single_or_array(self):
|
|
|
+ """
|
|
|
+ POST operations on NetBoxModelViewSet endpoints should document
|
|
|
+ support for both single objects and arrays via oneOf.
|
|
|
+
|
|
|
+ Refs: #20638
|
|
|
+ """
|
|
|
+ # Test representative endpoints across different apps
|
|
|
+ test_paths = [
|
|
|
+ '/api/core/data-sources/',
|
|
|
+ '/api/dcim/sites/',
|
|
|
+ '/api/users/users/',
|
|
|
+ '/api/ipam/ip-addresses/',
|
|
|
+ ]
|
|
|
+
|
|
|
+ for path in test_paths:
|
|
|
+ with self.subTest(path=path):
|
|
|
+ operation = self.schema['paths'][path]['post']
|
|
|
+
|
|
|
+ # Get the request body schema
|
|
|
+ request_schema = operation['requestBody']['content']['application/json']['schema']
|
|
|
+
|
|
|
+ # Should have oneOf with two options
|
|
|
+ self.assertIn('oneOf', request_schema, f"POST {path} should have oneOf schema")
|
|
|
+ self.assertEqual(
|
|
|
+ len(request_schema['oneOf']), 2,
|
|
|
+ f"POST {path} oneOf should have exactly 2 options"
|
|
|
+ )
|
|
|
+
|
|
|
+ # First option: single object (has $ref or properties)
|
|
|
+ single_schema = request_schema['oneOf'][0]
|
|
|
+ self.assertTrue(
|
|
|
+ '$ref' in single_schema or 'properties' in single_schema,
|
|
|
+ f"POST {path} first oneOf option should be single object"
|
|
|
+ )
|
|
|
+
|
|
|
+ # Second option: array of objects
|
|
|
+ array_schema = request_schema['oneOf'][1]
|
|
|
+ self.assertEqual(
|
|
|
+ array_schema['type'], 'array',
|
|
|
+ f"POST {path} second oneOf option should be array"
|
|
|
+ )
|
|
|
+ self.assertIn('items', array_schema, f"POST {path} array should have items")
|
|
|
+
|
|
|
+ def test_bulk_update_operations_require_array_only(self):
|
|
|
+ """
|
|
|
+ Bulk update/patch operations should require arrays only, not oneOf.
|
|
|
+ They don't support single object input.
|
|
|
+
|
|
|
+ Refs: #20638
|
|
|
+ """
|
|
|
+ test_paths = [
|
|
|
+ '/api/dcim/sites/',
|
|
|
+ '/api/users/users/',
|
|
|
+ ]
|
|
|
+
|
|
|
+ for path in test_paths:
|
|
|
+ for method in ['put', 'patch']:
|
|
|
+ with self.subTest(path=path, method=method):
|
|
|
+ operation = self.schema['paths'][path][method]
|
|
|
+ request_schema = operation['requestBody']['content']['application/json']['schema']
|
|
|
+
|
|
|
+ # Should be array-only, not oneOf
|
|
|
+ self.assertNotIn(
|
|
|
+ 'oneOf', request_schema,
|
|
|
+ f"{method.upper()} {path} should NOT have oneOf (array-only)"
|
|
|
+ )
|
|
|
+ self.assertEqual(
|
|
|
+ request_schema['type'], 'array',
|
|
|
+ f"{method.upper()} {path} should require array"
|
|
|
+ )
|
|
|
+ self.assertIn(
|
|
|
+ 'items', request_schema,
|
|
|
+ f"{method.upper()} {path} array should have items"
|
|
|
+ )
|
|
|
+
|
|
|
+ def test_bulk_delete_requires_array(self):
|
|
|
+ """
|
|
|
+ Bulk delete operations should require arrays.
|
|
|
+
|
|
|
+ Refs: #20638
|
|
|
+ """
|
|
|
+ path = '/api/dcim/sites/'
|
|
|
+ operation = self.schema['paths'][path]['delete']
|
|
|
+ request_schema = operation['requestBody']['content']['application/json']['schema']
|
|
|
+
|
|
|
+ # Should be array-only
|
|
|
+ self.assertNotIn('oneOf', request_schema, "DELETE should NOT have oneOf")
|
|
|
+ self.assertEqual(request_schema['type'], 'array', "DELETE should require array")
|
|
|
+ self.assertIn('items', request_schema, "DELETE array should have items")
|