Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions drf_spectacular/plumbing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1518,6 +1518,13 @@ def process_webhooks(webhooks: List[OpenApiWebhook], registry: ComponentRegistry
)
operation = {}

if (
hasattr(mocked_view.schema, 'has_explicit_operation_id') and
mocked_view.schema.has_explicit_operation_id()
):
operation_id = mocked_view.schema.get_operation_id()
operation['operationId'] = operation_id

description = mocked_view.schema.get_description()
if description:
operation['description'] = description
Expand Down
7 changes: 5 additions & 2 deletions drf_spectacular/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ def __init__(


def extend_schema(
operation_id: Optional[str] = None,
operation_id: Optional[str] = empty,
parameters: Optional[Sequence[Union[OpenApiParameter, _SerializerType]]] = None,
request: Any = empty,
responses: Any = empty,
Expand Down Expand Up @@ -456,10 +456,13 @@ def is_excluded(self):
return super().is_excluded()

def get_operation_id(self):
if operation_id and is_in_scope(self):
if operation_id is not empty and is_in_scope(self):
return operation_id
return super().get_operation_id()

def has_explicit_operation_id(self):
return operation_id is not empty and operation_id is not None and is_in_scope(self)

def get_override_parameters(self):
if parameters and is_in_scope(self):
return super().get_override_parameters() + parameters
Expand Down
80 changes: 80 additions & 0 deletions tests/test_webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,25 @@ class EventSerializer(serializers.Serializer):
)


# Test webhook with custom operationId
subscription_event_with_operation_id = OpenApiWebhook(
name='SubscriptionEventWithOperationId',
decorator=extend_schema(
operation_id='custom_webhook_operation_id',
summary="webhook with custom operation id",
description='webhook that should include operationId in the generated schema',
tags=["webhooks"],
request={
'application/json': EventSerializer,
},
responses={
200: OpenApiResponse(description='event was successfully received'),
'4XX': OpenApiResponse(description='event will be retried shortly'),
},
),
)


@pytest.mark.urls(__name__)
@mock.patch('drf_spectacular.settings.spectacular_settings.OAS_VERSION', '3.1.0')
@mock.patch('drf_spectacular.settings.spectacular_settings.WEBHOOKS', [subscription_event])
Expand All @@ -41,3 +60,64 @@ def test_webhooks_settings(no_warnings):
SchemaGenerator().get_schema(request=None, public=True),
'tests/test_webhooks.yml'
)


@pytest.mark.urls(__name__)
@mock.patch('drf_spectacular.settings.spectacular_settings.OAS_VERSION', '3.1.0')
@mock.patch('drf_spectacular.settings.spectacular_settings.WEBHOOKS', [subscription_event_with_operation_id])
def test_webhooks_operation_id(no_warnings):
"""Test that operationId is included in webhook schema when specified in @extend_schema decorator."""
assert_schema(
SchemaGenerator().get_schema(request=None, public=True),
'tests/test_webhooks_operation_id.yml'
)


@pytest.mark.urls(__name__)
@mock.patch('drf_spectacular.settings.spectacular_settings.OAS_VERSION', '3.1.0')
@mock.patch('drf_spectacular.settings.spectacular_settings.WEBHOOKS', [subscription_event])
def test_webhooks_no_auto_generated_operation_id(no_warnings):
"""Test that operationId is NOT included for webhooks without explicit operation_id."""
schema = SchemaGenerator().get_schema(request=None, public=True)

# Check that webhooks section exists
assert 'webhooks' in schema
assert 'SubscriptionEvent' in schema['webhooks']

# Check that operationId is NOT included when not explicitly provided
webhook_operation = schema['webhooks']['SubscriptionEvent']['post']
assert 'operationId' not in webhook_operation


# Test webhook with explicit None operationId (should also not be included)
subscription_event_with_none_operation_id = OpenApiWebhook(
name='SubscriptionEventWithNoneOperationId',
decorator=extend_schema(
operation_id=None,
summary="webhook with explicit None operation id",
description='webhook with None operation_id should not include operationId',
tags=["webhooks"],
request={
'application/json': EventSerializer,
},
responses={
200: OpenApiResponse(description='event was successfully received'),
},
),
)


@pytest.mark.urls(__name__)
@mock.patch('drf_spectacular.settings.spectacular_settings.OAS_VERSION', '3.1.0')
@mock.patch('drf_spectacular.settings.spectacular_settings.WEBHOOKS', [subscription_event_with_none_operation_id])
def test_webhooks_explicit_none_operation_id(no_warnings):
"""Test that operationId is NOT included when explicitly set to None."""
schema = SchemaGenerator().get_schema(request=None, public=True)

# Check that webhooks section exists
assert 'webhooks' in schema
assert 'SubscriptionEventWithNoneOperationId' in schema['webhooks']

# Check that operationId is NOT included when explicitly set to None
webhook_operation = schema['webhooks']['SubscriptionEventWithNoneOperationId']['post']
assert 'operationId' not in webhook_operation
41 changes: 41 additions & 0 deletions tests/test_webhooks_operation_id.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
openapi: 3.1.0
info:
title: ''
version: 0.0.0
paths: {}
components:
schemas:
Event:
type: object
properties:
id:
type: string
readOnly: true
change:
type: string
external_id:
type: string
writeOnly: true
required:
- change
- external_id
- id
webhooks:
SubscriptionEventWithOperationId:
post:
operationId: custom_webhook_operation_id
description: webhook that should include operationId in the generated schema
summary: webhook with custom operation id
tags:
- webhooks
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Event'
required: true
responses:
'200':
description: event was successfully received
4XX:
description: event will be retried shortly