Integration webhooks API (#3954)
# What this PR does Adds internal API endpoints for `alert_receive_channels/<id>/webhooks/<webhook_id>`. ## Which issue(s) this PR fixes Related to https://github.com/grafana/oncall-private/issues/2541 ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required)
This commit is contained in:
parent
c2ffd28675
commit
7549a688b0
3 changed files with 215 additions and 2 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import json
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import ANY, patch
|
||||
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
|
|
@ -1696,3 +1696,157 @@ def test_team_not_updated_if_not_in_data(
|
|||
|
||||
alert_receive_channel.refresh_from_db()
|
||||
assert alert_receive_channel.team == team
|
||||
|
||||
|
||||
def _webhook_data(webhook_id=ANY, webhook_name=ANY, webhook_url=ANY, alert_receive_channel_id=ANY):
|
||||
return {
|
||||
"authorization_header": None,
|
||||
"data": None,
|
||||
"forward_all": True,
|
||||
"headers": None,
|
||||
"http_method": "POST",
|
||||
"id": webhook_id,
|
||||
"integration_filter": [alert_receive_channel_id],
|
||||
"is_legacy": False,
|
||||
"is_webhook_enabled": True,
|
||||
"labels": [],
|
||||
"last_response_log": {
|
||||
"content": "",
|
||||
"event_data": "",
|
||||
"request_data": "",
|
||||
"request_headers": "",
|
||||
"request_trigger": "",
|
||||
"status_code": None,
|
||||
"timestamp": None,
|
||||
"url": "",
|
||||
},
|
||||
"name": webhook_name,
|
||||
"password": None,
|
||||
"preset": None,
|
||||
"team": None,
|
||||
"trigger_template": None,
|
||||
"trigger_type": "0",
|
||||
"trigger_type_name": "Escalation step",
|
||||
"url": webhook_url,
|
||||
"username": None,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_alert_receive_channel_webhooks_get(
|
||||
make_organization_and_user_with_plugin_token,
|
||||
make_alert_receive_channel,
|
||||
make_custom_webhook,
|
||||
make_user_auth_headers,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_plugin_token()
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
webhook = make_custom_webhook(organization, is_from_connected_integration=True)
|
||||
webhook.filtered_integrations.set([alert_receive_channel])
|
||||
|
||||
# create 2 webhooks that are not connected to the integration
|
||||
make_custom_webhook(organization)
|
||||
webhook2 = make_custom_webhook(organization, is_from_connected_integration=False)
|
||||
webhook2.filtered_integrations.set([alert_receive_channel])
|
||||
|
||||
client = APIClient()
|
||||
url = reverse(
|
||||
"api-internal:alert_receive_channel-webhooks-get", kwargs={"pk": alert_receive_channel.public_primary_key}
|
||||
)
|
||||
response = client.get(url, **make_user_auth_headers(user, token))
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == [
|
||||
_webhook_data(
|
||||
webhook_id=webhook.public_primary_key,
|
||||
alert_receive_channel_id=alert_receive_channel.public_primary_key,
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_alert_receive_channel_webhooks_post(
|
||||
make_organization_and_user_with_plugin_token,
|
||||
make_alert_receive_channel,
|
||||
make_user_auth_headers,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_plugin_token()
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
|
||||
client = APIClient()
|
||||
url = reverse(
|
||||
"api-internal:alert_receive_channel-webhooks-get", kwargs={"pk": alert_receive_channel.public_primary_key}
|
||||
)
|
||||
|
||||
data = {
|
||||
"name": None,
|
||||
"enabled": True,
|
||||
"url": "http://example.com/",
|
||||
"http_method": "POST",
|
||||
"trigger_type": "0",
|
||||
"trigger_template": None,
|
||||
}
|
||||
response = client.post(url, data, format="json", **make_user_auth_headers(user, token))
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.json() == _webhook_data(
|
||||
webhook_url="http://example.com/",
|
||||
alert_receive_channel_id=alert_receive_channel.public_primary_key,
|
||||
)
|
||||
assert alert_receive_channel.webhooks.get().is_from_connected_integration is True
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_alert_receive_channel_webhooks_put(
|
||||
make_organization_and_user_with_plugin_token,
|
||||
make_alert_receive_channel,
|
||||
make_custom_webhook,
|
||||
make_user_auth_headers,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_plugin_token()
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
webhook = make_custom_webhook(organization, is_from_connected_integration=True)
|
||||
webhook.filtered_integrations.set([alert_receive_channel])
|
||||
|
||||
client = APIClient()
|
||||
url = reverse(
|
||||
"api-internal:alert_receive_channel-webhooks-put",
|
||||
kwargs={"pk": alert_receive_channel.public_primary_key, "webhook_id": webhook.public_primary_key},
|
||||
)
|
||||
|
||||
data = _webhook_data(
|
||||
webhook_id=webhook.public_primary_key,
|
||||
webhook_name="Test",
|
||||
webhook_url="http://example.com/",
|
||||
alert_receive_channel_id=alert_receive_channel.public_primary_key,
|
||||
)
|
||||
response = client.put(url, data, format="json", **make_user_auth_headers(user, token))
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
webhook.refresh_from_db()
|
||||
assert webhook.url == "http://example.com/"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_alert_receive_channel_webhooks_delete(
|
||||
make_organization_and_user_with_plugin_token,
|
||||
make_alert_receive_channel,
|
||||
make_custom_webhook,
|
||||
make_user_auth_headers,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_plugin_token()
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
webhook = make_custom_webhook(organization, is_from_connected_integration=True)
|
||||
webhook.filtered_integrations.set([alert_receive_channel])
|
||||
|
||||
client = APIClient()
|
||||
url = reverse(
|
||||
"api-internal:alert_receive_channel-webhooks-put",
|
||||
kwargs={"pk": alert_receive_channel.public_primary_key, "webhook_id": webhook.public_primary_key},
|
||||
)
|
||||
response = client.delete(url, **make_user_auth_headers(user, token))
|
||||
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
webhook.refresh_from_db()
|
||||
assert webhook.deleted_at is not None
|
||||
assert alert_receive_channel.webhooks.count() == 0
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import typing
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db.models import Q
|
||||
from django_filters import rest_framework as filters
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
|
|
@ -7,6 +8,7 @@ from drf_spectacular.plumbing import resolve_type_hint
|
|||
from drf_spectacular.utils import PolymorphicProxySerializer, extend_schema, extend_schema_view, inline_serializer
|
||||
from rest_framework import serializers, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import NotFound
|
||||
from rest_framework.filters import SearchFilter
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
|
@ -22,6 +24,7 @@ from apps.api.serializers.alert_receive_channel import (
|
|||
AlertReceiveChannelUpdateSerializer,
|
||||
FilterAlertReceiveChannelSerializer,
|
||||
)
|
||||
from apps.api.serializers.webhook import WebhookSerializer
|
||||
from apps.api.throttlers import DemoAlertThrottler
|
||||
from apps.api.views.labels import schedule_update_label_cache
|
||||
from apps.auth_token.auth import PluginAuthentication
|
||||
|
|
@ -148,6 +151,10 @@ class AlertReceiveChannelView(
|
|||
"connect_contact_point": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
|
||||
"create_contact_point": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
|
||||
"disconnect_contact_point": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
|
||||
"webhooks_get": [RBACPermission.Permissions.INTEGRATIONS_READ],
|
||||
"webhooks_post": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
|
||||
"webhooks_put": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
|
||||
"webhooks_delete": [RBACPermission.Permissions.INTEGRATIONS_WRITE],
|
||||
}
|
||||
|
||||
def perform_update(self, serializer):
|
||||
|
|
@ -622,3 +629,50 @@ class AlertReceiveChannelView(
|
|||
if not disconnected:
|
||||
raise BadRequest(detail=error)
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
@extend_schema(request=None, responses=WebhookSerializer(many=True))
|
||||
@action(detail=True, methods=["get"], url_path="webhooks")
|
||||
def webhooks_get(self, request, pk):
|
||||
instance = self.get_object()
|
||||
return Response(
|
||||
WebhookSerializer(
|
||||
instance.webhooks.filter(is_from_connected_integration=True),
|
||||
many=True,
|
||||
context={"request": request},
|
||||
).data
|
||||
)
|
||||
|
||||
@extend_schema(request=WebhookSerializer, responses=WebhookSerializer)
|
||||
@webhooks_get.mapping.post
|
||||
# https://www.django-rest-framework.org/api-guide/viewsets/#routing-additional-http-methods-for-extra-actions
|
||||
def webhooks_post(self, request, pk):
|
||||
instance = self.get_object()
|
||||
serializer = WebhookSerializer(data=request.data, context={"request": request})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save(filtered_integrations=[instance], is_from_connected_integration=True)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
@extend_schema(request=WebhookSerializer, responses=WebhookSerializer)
|
||||
@action(detail=True, methods=["put"], url_path=r"webhooks/(?P<webhook_id>\w+)")
|
||||
def webhooks_put(self, request, pk, webhook_id):
|
||||
instance = self.get_object()
|
||||
try:
|
||||
webhook = instance.webhooks.get(is_from_connected_integration=True, public_primary_key=webhook_id)
|
||||
except ObjectDoesNotExist:
|
||||
raise NotFound
|
||||
serializer = WebhookSerializer(webhook, data=request.data, context={"request": request})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@extend_schema(request=None, responses=None)
|
||||
@webhooks_put.mapping.delete
|
||||
# https://www.django-rest-framework.org/api-guide/viewsets/#routing-additional-http-methods-for-extra-actions
|
||||
def webhooks_delete(self, request, pk, webhook_id):
|
||||
instance = self.get_object()
|
||||
try:
|
||||
webhook = instance.webhooks.get(is_from_connected_integration=True, public_primary_key=webhook_id)
|
||||
except ObjectDoesNotExist:
|
||||
raise NotFound
|
||||
webhook.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import re
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
|
@ -27,7 +28,11 @@ def test_internal_api_detail_actions_get_object(
|
|||
organization, user, token = make_organization_and_user_with_plugin_token()
|
||||
client = APIClient()
|
||||
|
||||
url = reverse(f"api-internal:{basename}-{action.url_name}", kwargs={"pk": "NONEXISTENT"})
|
||||
# get additional kwargs based on url_path regex
|
||||
# example: for /alert_receive_channel/<pk>/webhooks/<webhook_id>, url_path_kwargs = {"webhook_id": "NONEXISTENT"}
|
||||
url_path_kwargs = {key: "NONEXISTENT" for key in re.compile(action.url_path).groupindex.keys()}
|
||||
|
||||
url = reverse(f"api-internal:{basename}-{action.url_name}", kwargs={"pk": "NONEXISTENT", **url_path_kwargs})
|
||||
|
||||
with patch.object(viewset_class, "get_object", side_effect=NotFound) as mock_get_object:
|
||||
method = list(action.mapping.keys())[0] # get the first allowed method
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue