2023-03-10 14:00:06 -03:00
|
|
|
import json
|
|
|
|
|
from unittest.mock import patch
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
from django.urls import reverse
|
|
|
|
|
from rest_framework import status
|
|
|
|
|
from rest_framework.response import Response
|
|
|
|
|
from rest_framework.test import APIClient
|
|
|
|
|
|
|
|
|
|
from apps.api.permissions import LegacyAccessControlRole
|
2023-07-11 21:03:34 +03:00
|
|
|
from apps.api.views.webhooks import RECENT_RESPONSE_LIMIT, WEBHOOK_URL
|
2023-03-10 14:00:06 -03:00
|
|
|
from apps.webhooks.models import Webhook
|
2023-06-06 01:59:12 -06:00
|
|
|
from apps.webhooks.models.webhook import WEBHOOK_FIELD_PLACEHOLDER
|
2023-03-10 14:00:06 -03:00
|
|
|
|
|
|
|
|
TEST_URL = "https://some-url"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
|
|
|
|
def webhook_internal_api_setup(make_organization_and_user_with_plugin_token, make_custom_webhook):
|
|
|
|
|
organization, user, token = make_organization_and_user_with_plugin_token()
|
|
|
|
|
webhook = make_custom_webhook(
|
|
|
|
|
name="some_webhook",
|
|
|
|
|
url="https://github.com/",
|
|
|
|
|
username="Chris Vanstras",
|
|
|
|
|
password="qwerty",
|
|
|
|
|
data='{"name": "{{ alert_payload }}"}',
|
|
|
|
|
authorization_header="auth_token",
|
|
|
|
|
organization=organization,
|
|
|
|
|
forward_all=False,
|
|
|
|
|
)
|
|
|
|
|
return user, token, webhook
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.django_db
|
2024-02-23 12:57:57 -03:00
|
|
|
def test_get_list_webhooks(webhook_internal_api_setup, make_custom_webhook, make_user_auth_headers):
|
2023-03-10 14:00:06 -03:00
|
|
|
user, token, webhook = webhook_internal_api_setup
|
2024-02-23 12:57:57 -03:00
|
|
|
# connected integration webhooks are not included
|
|
|
|
|
make_custom_webhook(organization=user.organization, is_from_connected_integration=True)
|
|
|
|
|
|
2023-03-10 14:00:06 -03:00
|
|
|
client = APIClient()
|
|
|
|
|
url = reverse("api-internal:webhooks-list")
|
|
|
|
|
|
|
|
|
|
expected_payload = [
|
|
|
|
|
{
|
|
|
|
|
"id": webhook.public_primary_key,
|
|
|
|
|
"name": "some_webhook",
|
|
|
|
|
"team": None,
|
|
|
|
|
"url": "https://github.com/",
|
|
|
|
|
"data": '{"name": "{{ alert_payload }}"}',
|
|
|
|
|
"username": "Chris Vanstras",
|
2023-06-06 01:59:12 -06:00
|
|
|
"password": WEBHOOK_FIELD_PLACEHOLDER,
|
|
|
|
|
"authorization_header": WEBHOOK_FIELD_PLACEHOLDER,
|
2023-03-10 14:00:06 -03:00
|
|
|
"forward_all": False,
|
|
|
|
|
"headers": None,
|
|
|
|
|
"http_method": "POST",
|
2024-02-23 08:55:44 -03:00
|
|
|
"integration_filter": [],
|
2023-04-13 12:52:29 -06:00
|
|
|
"is_webhook_enabled": True,
|
Webhook labels (#3383)
This PR add labels for webhooks.
1. Make webhook "labelable" with ability to filter by labels.
2. Add labels to the webhook payload. It contain new field webhook with
it's name, id and labels. Field integration and alert_group has a
corresponding label field as well. See example of a new payload below:
```
{
"event": {
"type": "escalation"
},
"user": null,
"alert_group": {
"id": "IRFN6ZD31N31B",
"integration_id": "CTWM7U4A2QG97",
"route_id": "RUE7U7Z46SKGY",
"alerts_count": 1,
"state": "firing",
"created_at": "2023-11-22T08:54:55.178243Z",
"resolved_at": null,
"acknowledged_at": null,
"title": "Incident",
"permalinks": {
"slack": null,
"telegram": null,
"web": "http://grafana:3000/a/grafana-oncall-app/alert-groups/IRFN6ZD31N31B"
},
"labels": {
"severity": "critical"
}
},
"alert_group_id": "IRFN6ZD31N31B",
"alert_payload": {
"message": "This alert was sent by user for demonstration purposes"
},
"integration": {
"id": "CTWM7U4A2QG97",
"type": "webhook",
"name": "hi - Webhook",
"team": null,
"labels": {
"hello": "world",
"severity": "critical"
}
},
"notified_users": [],
"users_to_be_notified": [],
"webhook": {
"id": "WHAXK4BTC7TAEQ",
"name": "test",
"labels": {
"hello": "kesha"
}
}
}
```
I feel that there is an opportunity to make code cleaner - remove all
label logic from serializers, views and utils to models or dedicated
LabelerService and introduce Labelable interface with something like
label_verbal, update_labels methods. However, I don't want to tie
webhook labels with a refactoring.
---------
Co-authored-by: Dominik <dominik.broj@grafana.com>
2023-11-22 19:17:41 +08:00
|
|
|
"labels": [],
|
Add is_legacy column to handle webhook migration (#1813)
Legacy webhooks won't be editable at first. Keep data templates
compatibility.
Possible migration code:
```python
from apps.webhooks.models import Webhook
from apps.alerts.models import CustomButton, EscalationPolicy
custom_buttons = CustomButton.objects.all()
for cb in custom_buttons:
webhook, _ = Webhook.objects.get_or_create(
organization=cb.organization,
team=cb.team,
name=cb.name,
is_legacy=True,
defaults=dict(
created_at=cb.created_at,
url=cb.webhook,
username=cb.user,
password=cb.password,
authorization_header=cb.authorization_header,
trigger_type=Webhook.TRIGGER_ESCALATION_STEP,
forward_all=cb.forward_whole_payload,
data=cb.data,
)
)
# migrate related escalation policies
policies = EscalationPolicy.objects.filter(
step=EscalationPolicy.STEP_TRIGGER_CUSTOM_BUTTON,
custom_button_trigger=cb,
).update(
step=EscalationPolicy.STEP_TRIGGER_CUSTOM_WEBHOOK,
custom_webhook=webhook,
)
```
2023-04-25 11:22:56 -03:00
|
|
|
"is_legacy": False,
|
2023-03-21 10:43:37 -03:00
|
|
|
"last_response_log": {
|
|
|
|
|
"request_data": "",
|
|
|
|
|
"request_headers": "",
|
|
|
|
|
"timestamp": None,
|
|
|
|
|
"content": "",
|
|
|
|
|
"status_code": None,
|
|
|
|
|
"request_trigger": "",
|
2023-03-10 14:00:06 -03:00
|
|
|
"url": "",
|
2023-07-11 21:03:34 +03:00
|
|
|
"event_data": "",
|
2023-03-10 14:00:06 -03:00
|
|
|
},
|
|
|
|
|
"trigger_template": None,
|
2023-08-22 14:05:52 -06:00
|
|
|
"trigger_type": "0",
|
2024-09-09 09:17:23 -03:00
|
|
|
"trigger_type_name": "Manual or escalation step",
|
2023-09-27 07:22:52 -06:00
|
|
|
"preset": None,
|
2023-03-10 14:00:06 -03:00
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
response = client.get(url, format="json", **make_user_auth_headers(user, token))
|
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
|
|
|
assert response.json() == expected_payload
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.django_db
|
|
|
|
|
def test_get_detail_webhook(webhook_internal_api_setup, make_user_auth_headers):
|
|
|
|
|
user, token, webhook = webhook_internal_api_setup
|
|
|
|
|
client = APIClient()
|
|
|
|
|
url = reverse("api-internal:webhooks-detail", kwargs={"pk": webhook.public_primary_key})
|
|
|
|
|
|
|
|
|
|
expected_payload = {
|
|
|
|
|
"id": webhook.public_primary_key,
|
|
|
|
|
"name": "some_webhook",
|
|
|
|
|
"team": None,
|
|
|
|
|
"url": "https://github.com/",
|
|
|
|
|
"data": '{"name": "{{ alert_payload }}"}',
|
|
|
|
|
"username": "Chris Vanstras",
|
2023-06-06 01:59:12 -06:00
|
|
|
"password": WEBHOOK_FIELD_PLACEHOLDER,
|
|
|
|
|
"authorization_header": WEBHOOK_FIELD_PLACEHOLDER,
|
2023-03-10 14:00:06 -03:00
|
|
|
"forward_all": False,
|
|
|
|
|
"headers": None,
|
|
|
|
|
"http_method": "POST",
|
2024-02-23 08:55:44 -03:00
|
|
|
"integration_filter": [],
|
2023-04-13 12:52:29 -06:00
|
|
|
"is_webhook_enabled": True,
|
Webhook labels (#3383)
This PR add labels for webhooks.
1. Make webhook "labelable" with ability to filter by labels.
2. Add labels to the webhook payload. It contain new field webhook with
it's name, id and labels. Field integration and alert_group has a
corresponding label field as well. See example of a new payload below:
```
{
"event": {
"type": "escalation"
},
"user": null,
"alert_group": {
"id": "IRFN6ZD31N31B",
"integration_id": "CTWM7U4A2QG97",
"route_id": "RUE7U7Z46SKGY",
"alerts_count": 1,
"state": "firing",
"created_at": "2023-11-22T08:54:55.178243Z",
"resolved_at": null,
"acknowledged_at": null,
"title": "Incident",
"permalinks": {
"slack": null,
"telegram": null,
"web": "http://grafana:3000/a/grafana-oncall-app/alert-groups/IRFN6ZD31N31B"
},
"labels": {
"severity": "critical"
}
},
"alert_group_id": "IRFN6ZD31N31B",
"alert_payload": {
"message": "This alert was sent by user for demonstration purposes"
},
"integration": {
"id": "CTWM7U4A2QG97",
"type": "webhook",
"name": "hi - Webhook",
"team": null,
"labels": {
"hello": "world",
"severity": "critical"
}
},
"notified_users": [],
"users_to_be_notified": [],
"webhook": {
"id": "WHAXK4BTC7TAEQ",
"name": "test",
"labels": {
"hello": "kesha"
}
}
}
```
I feel that there is an opportunity to make code cleaner - remove all
label logic from serializers, views and utils to models or dedicated
LabelerService and introduce Labelable interface with something like
label_verbal, update_labels methods. However, I don't want to tie
webhook labels with a refactoring.
---------
Co-authored-by: Dominik <dominik.broj@grafana.com>
2023-11-22 19:17:41 +08:00
|
|
|
"labels": [],
|
Add is_legacy column to handle webhook migration (#1813)
Legacy webhooks won't be editable at first. Keep data templates
compatibility.
Possible migration code:
```python
from apps.webhooks.models import Webhook
from apps.alerts.models import CustomButton, EscalationPolicy
custom_buttons = CustomButton.objects.all()
for cb in custom_buttons:
webhook, _ = Webhook.objects.get_or_create(
organization=cb.organization,
team=cb.team,
name=cb.name,
is_legacy=True,
defaults=dict(
created_at=cb.created_at,
url=cb.webhook,
username=cb.user,
password=cb.password,
authorization_header=cb.authorization_header,
trigger_type=Webhook.TRIGGER_ESCALATION_STEP,
forward_all=cb.forward_whole_payload,
data=cb.data,
)
)
# migrate related escalation policies
policies = EscalationPolicy.objects.filter(
step=EscalationPolicy.STEP_TRIGGER_CUSTOM_BUTTON,
custom_button_trigger=cb,
).update(
step=EscalationPolicy.STEP_TRIGGER_CUSTOM_WEBHOOK,
custom_webhook=webhook,
)
```
2023-04-25 11:22:56 -03:00
|
|
|
"is_legacy": False,
|
2023-03-21 10:43:37 -03:00
|
|
|
"last_response_log": {
|
|
|
|
|
"request_data": "",
|
|
|
|
|
"request_headers": "",
|
|
|
|
|
"timestamp": None,
|
|
|
|
|
"content": "",
|
|
|
|
|
"status_code": None,
|
|
|
|
|
"request_trigger": "",
|
2023-03-10 14:00:06 -03:00
|
|
|
"url": "",
|
2023-07-11 21:03:34 +03:00
|
|
|
"event_data": "",
|
2023-03-10 14:00:06 -03:00
|
|
|
},
|
|
|
|
|
"trigger_template": None,
|
2023-08-22 14:05:52 -06:00
|
|
|
"trigger_type": "0",
|
2024-09-09 09:17:23 -03:00
|
|
|
"trigger_type_name": "Manual or escalation step",
|
2023-09-27 07:22:52 -06:00
|
|
|
"preset": None,
|
2023-03-10 14:00:06 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
response = client.get(url, format="json", **make_user_auth_headers(user, token))
|
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
2024-04-08 11:13:17 -03:00
|
|
|
assert response.json() == expected_payload
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.django_db
|
|
|
|
|
def test_get_detail_connected_integration_webhook(
|
|
|
|
|
webhook_internal_api_setup, make_custom_webhook, make_user_auth_headers
|
|
|
|
|
):
|
|
|
|
|
user, token, _ = webhook_internal_api_setup
|
|
|
|
|
# it is possible to get details for a connected integration webhook
|
|
|
|
|
webhook = make_custom_webhook(organization=user.organization, is_from_connected_integration=True)
|
|
|
|
|
|
|
|
|
|
client = APIClient()
|
|
|
|
|
url = reverse("api-internal:webhooks-detail", kwargs={"pk": webhook.public_primary_key})
|
|
|
|
|
|
|
|
|
|
expected_payload = {
|
|
|
|
|
"id": webhook.public_primary_key,
|
|
|
|
|
"name": webhook.name,
|
|
|
|
|
"team": None,
|
|
|
|
|
"url": webhook.url,
|
|
|
|
|
"data": webhook.data,
|
|
|
|
|
"username": None,
|
|
|
|
|
"password": None,
|
|
|
|
|
"authorization_header": None,
|
|
|
|
|
"forward_all": True,
|
|
|
|
|
"headers": None,
|
|
|
|
|
"http_method": "POST",
|
|
|
|
|
"integration_filter": [],
|
|
|
|
|
"is_webhook_enabled": True,
|
|
|
|
|
"labels": [],
|
|
|
|
|
"is_legacy": False,
|
|
|
|
|
"last_response_log": {
|
|
|
|
|
"request_data": "",
|
|
|
|
|
"request_headers": "",
|
|
|
|
|
"timestamp": None,
|
|
|
|
|
"content": "",
|
|
|
|
|
"status_code": None,
|
|
|
|
|
"request_trigger": "",
|
|
|
|
|
"url": "",
|
|
|
|
|
"event_data": "",
|
|
|
|
|
},
|
|
|
|
|
"trigger_template": None,
|
|
|
|
|
"trigger_type": "0",
|
2024-09-09 09:17:23 -03:00
|
|
|
"trigger_type_name": "Manual or escalation step",
|
2024-04-08 11:13:17 -03:00
|
|
|
"preset": None,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
response = client.get(url, format="json", **make_user_auth_headers(user, token))
|
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
2023-03-10 14:00:06 -03:00
|
|
|
assert response.json() == expected_payload
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.django_db
|
2023-08-02 20:07:47 +02:00
|
|
|
def test_create_webhook(webhook_internal_api_setup, make_user_auth_headers):
|
2023-03-10 14:00:06 -03:00
|
|
|
user, token, webhook = webhook_internal_api_setup
|
|
|
|
|
client = APIClient()
|
|
|
|
|
url = reverse("api-internal:webhooks-list")
|
|
|
|
|
|
|
|
|
|
data = {
|
|
|
|
|
"name": "the_webhook",
|
|
|
|
|
"url": TEST_URL,
|
2023-09-27 07:22:52 -06:00
|
|
|
"trigger_type": Webhook.TRIGGER_ALERT_GROUP_CREATED,
|
|
|
|
|
"http_method": "POST",
|
2023-03-10 14:00:06 -03:00
|
|
|
"team": None,
|
|
|
|
|
}
|
|
|
|
|
response = client.post(url, data, format="json", **make_user_auth_headers(user, token))
|
|
|
|
|
webhook = Webhook.objects.get(public_primary_key=response.json()["id"])
|
|
|
|
|
expected_response = data | {
|
|
|
|
|
"id": webhook.public_primary_key,
|
|
|
|
|
"data": None,
|
|
|
|
|
"username": None,
|
2023-04-05 09:03:55 -03:00
|
|
|
"password": None,
|
|
|
|
|
"authorization_header": None,
|
2023-03-10 14:00:06 -03:00
|
|
|
"forward_all": True,
|
|
|
|
|
"headers": None,
|
|
|
|
|
"http_method": "POST",
|
2024-02-23 08:55:44 -03:00
|
|
|
"integration_filter": [],
|
2023-04-13 12:52:29 -06:00
|
|
|
"is_webhook_enabled": True,
|
Webhook labels (#3383)
This PR add labels for webhooks.
1. Make webhook "labelable" with ability to filter by labels.
2. Add labels to the webhook payload. It contain new field webhook with
it's name, id and labels. Field integration and alert_group has a
corresponding label field as well. See example of a new payload below:
```
{
"event": {
"type": "escalation"
},
"user": null,
"alert_group": {
"id": "IRFN6ZD31N31B",
"integration_id": "CTWM7U4A2QG97",
"route_id": "RUE7U7Z46SKGY",
"alerts_count": 1,
"state": "firing",
"created_at": "2023-11-22T08:54:55.178243Z",
"resolved_at": null,
"acknowledged_at": null,
"title": "Incident",
"permalinks": {
"slack": null,
"telegram": null,
"web": "http://grafana:3000/a/grafana-oncall-app/alert-groups/IRFN6ZD31N31B"
},
"labels": {
"severity": "critical"
}
},
"alert_group_id": "IRFN6ZD31N31B",
"alert_payload": {
"message": "This alert was sent by user for demonstration purposes"
},
"integration": {
"id": "CTWM7U4A2QG97",
"type": "webhook",
"name": "hi - Webhook",
"team": null,
"labels": {
"hello": "world",
"severity": "critical"
}
},
"notified_users": [],
"users_to_be_notified": [],
"webhook": {
"id": "WHAXK4BTC7TAEQ",
"name": "test",
"labels": {
"hello": "kesha"
}
}
}
```
I feel that there is an opportunity to make code cleaner - remove all
label logic from serializers, views and utils to models or dedicated
LabelerService and introduce Labelable interface with something like
label_verbal, update_labels methods. However, I don't want to tie
webhook labels with a refactoring.
---------
Co-authored-by: Dominik <dominik.broj@grafana.com>
2023-11-22 19:17:41 +08:00
|
|
|
"labels": [],
|
Add is_legacy column to handle webhook migration (#1813)
Legacy webhooks won't be editable at first. Keep data templates
compatibility.
Possible migration code:
```python
from apps.webhooks.models import Webhook
from apps.alerts.models import CustomButton, EscalationPolicy
custom_buttons = CustomButton.objects.all()
for cb in custom_buttons:
webhook, _ = Webhook.objects.get_or_create(
organization=cb.organization,
team=cb.team,
name=cb.name,
is_legacy=True,
defaults=dict(
created_at=cb.created_at,
url=cb.webhook,
username=cb.user,
password=cb.password,
authorization_header=cb.authorization_header,
trigger_type=Webhook.TRIGGER_ESCALATION_STEP,
forward_all=cb.forward_whole_payload,
data=cb.data,
)
)
# migrate related escalation policies
policies = EscalationPolicy.objects.filter(
step=EscalationPolicy.STEP_TRIGGER_CUSTOM_BUTTON,
custom_button_trigger=cb,
).update(
step=EscalationPolicy.STEP_TRIGGER_CUSTOM_WEBHOOK,
custom_webhook=webhook,
)
```
2023-04-25 11:22:56 -03:00
|
|
|
"is_legacy": False,
|
2023-03-21 10:43:37 -03:00
|
|
|
"last_response_log": {
|
|
|
|
|
"request_data": "",
|
|
|
|
|
"request_headers": "",
|
|
|
|
|
"timestamp": None,
|
|
|
|
|
"content": "",
|
|
|
|
|
"status_code": None,
|
|
|
|
|
"request_trigger": "",
|
2023-03-10 14:00:06 -03:00
|
|
|
"url": "",
|
2023-07-11 21:03:34 +03:00
|
|
|
"event_data": "",
|
2023-03-10 14:00:06 -03:00
|
|
|
},
|
|
|
|
|
"trigger_template": None,
|
2023-09-27 07:22:52 -06:00
|
|
|
"trigger_type": str(data["trigger_type"]),
|
2023-04-26 15:55:08 -06:00
|
|
|
"trigger_type_name": "Alert Group Created",
|
2023-09-27 07:22:52 -06:00
|
|
|
"preset": None,
|
2023-03-10 14:00:06 -03:00
|
|
|
}
|
|
|
|
|
assert response.status_code == status.HTTP_201_CREATED
|
|
|
|
|
assert response.json() == expected_response
|
|
|
|
|
# user creating the webhook is set
|
|
|
|
|
assert webhook.user == user
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.django_db
|
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
"field_name,value",
|
|
|
|
|
[
|
|
|
|
|
("data", '{"name": "{{ alert_payload }}"}'),
|
|
|
|
|
("headers", '"request-id": "{{ alert_payload.id }}"'),
|
|
|
|
|
("trigger_template", "integration_id == {{ some_var_value }}"),
|
|
|
|
|
("url", "https://myserver/{{ alert_payload.id }}/triggered"),
|
|
|
|
|
],
|
|
|
|
|
)
|
2023-08-02 20:07:47 +02:00
|
|
|
def test_create_valid_templated_field(webhook_internal_api_setup, make_user_auth_headers, field_name, value):
|
2023-03-10 14:00:06 -03:00
|
|
|
user, token, webhook = webhook_internal_api_setup
|
|
|
|
|
client = APIClient()
|
|
|
|
|
url = reverse("api-internal:webhooks-list")
|
|
|
|
|
|
|
|
|
|
data = {
|
|
|
|
|
"name": "webhook_with_valid_data",
|
|
|
|
|
"url": TEST_URL,
|
|
|
|
|
field_name: value,
|
2023-09-27 07:22:52 -06:00
|
|
|
"trigger_type": Webhook.TRIGGER_ALERT_GROUP_CREATED,
|
|
|
|
|
"http_method": "POST",
|
2023-03-10 14:00:06 -03:00
|
|
|
"team": None,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
response = client.post(url, data, format="json", **make_user_auth_headers(user, token))
|
|
|
|
|
# modify initial data by adding id and None for optional fields
|
|
|
|
|
webhook = Webhook.objects.get(public_primary_key=response.data["id"])
|
|
|
|
|
expected_response = data | {
|
|
|
|
|
"id": webhook.public_primary_key,
|
|
|
|
|
"username": None,
|
2023-04-05 09:03:55 -03:00
|
|
|
"password": None,
|
|
|
|
|
"authorization_header": None,
|
2023-03-10 14:00:06 -03:00
|
|
|
"forward_all": True,
|
|
|
|
|
"headers": None,
|
|
|
|
|
"data": None,
|
|
|
|
|
"http_method": "POST",
|
2024-02-23 08:55:44 -03:00
|
|
|
"integration_filter": [],
|
2023-04-13 12:52:29 -06:00
|
|
|
"is_webhook_enabled": True,
|
Webhook labels (#3383)
This PR add labels for webhooks.
1. Make webhook "labelable" with ability to filter by labels.
2. Add labels to the webhook payload. It contain new field webhook with
it's name, id and labels. Field integration and alert_group has a
corresponding label field as well. See example of a new payload below:
```
{
"event": {
"type": "escalation"
},
"user": null,
"alert_group": {
"id": "IRFN6ZD31N31B",
"integration_id": "CTWM7U4A2QG97",
"route_id": "RUE7U7Z46SKGY",
"alerts_count": 1,
"state": "firing",
"created_at": "2023-11-22T08:54:55.178243Z",
"resolved_at": null,
"acknowledged_at": null,
"title": "Incident",
"permalinks": {
"slack": null,
"telegram": null,
"web": "http://grafana:3000/a/grafana-oncall-app/alert-groups/IRFN6ZD31N31B"
},
"labels": {
"severity": "critical"
}
},
"alert_group_id": "IRFN6ZD31N31B",
"alert_payload": {
"message": "This alert was sent by user for demonstration purposes"
},
"integration": {
"id": "CTWM7U4A2QG97",
"type": "webhook",
"name": "hi - Webhook",
"team": null,
"labels": {
"hello": "world",
"severity": "critical"
}
},
"notified_users": [],
"users_to_be_notified": [],
"webhook": {
"id": "WHAXK4BTC7TAEQ",
"name": "test",
"labels": {
"hello": "kesha"
}
}
}
```
I feel that there is an opportunity to make code cleaner - remove all
label logic from serializers, views and utils to models or dedicated
LabelerService and introduce Labelable interface with something like
label_verbal, update_labels methods. However, I don't want to tie
webhook labels with a refactoring.
---------
Co-authored-by: Dominik <dominik.broj@grafana.com>
2023-11-22 19:17:41 +08:00
|
|
|
"labels": [],
|
Add is_legacy column to handle webhook migration (#1813)
Legacy webhooks won't be editable at first. Keep data templates
compatibility.
Possible migration code:
```python
from apps.webhooks.models import Webhook
from apps.alerts.models import CustomButton, EscalationPolicy
custom_buttons = CustomButton.objects.all()
for cb in custom_buttons:
webhook, _ = Webhook.objects.get_or_create(
organization=cb.organization,
team=cb.team,
name=cb.name,
is_legacy=True,
defaults=dict(
created_at=cb.created_at,
url=cb.webhook,
username=cb.user,
password=cb.password,
authorization_header=cb.authorization_header,
trigger_type=Webhook.TRIGGER_ESCALATION_STEP,
forward_all=cb.forward_whole_payload,
data=cb.data,
)
)
# migrate related escalation policies
policies = EscalationPolicy.objects.filter(
step=EscalationPolicy.STEP_TRIGGER_CUSTOM_BUTTON,
custom_button_trigger=cb,
).update(
step=EscalationPolicy.STEP_TRIGGER_CUSTOM_WEBHOOK,
custom_webhook=webhook,
)
```
2023-04-25 11:22:56 -03:00
|
|
|
"is_legacy": False,
|
2023-03-21 10:43:37 -03:00
|
|
|
"last_response_log": {
|
|
|
|
|
"request_data": "",
|
|
|
|
|
"request_headers": "",
|
|
|
|
|
"timestamp": None,
|
|
|
|
|
"content": "",
|
|
|
|
|
"status_code": None,
|
|
|
|
|
"request_trigger": "",
|
2023-03-10 14:00:06 -03:00
|
|
|
"url": "",
|
2023-07-11 21:03:34 +03:00
|
|
|
"event_data": "",
|
2023-03-10 14:00:06 -03:00
|
|
|
},
|
|
|
|
|
"trigger_template": None,
|
2023-09-27 07:22:52 -06:00
|
|
|
"trigger_type": str(data["trigger_type"]),
|
2023-04-26 15:55:08 -06:00
|
|
|
"trigger_type_name": "Alert Group Created",
|
2023-09-27 07:22:52 -06:00
|
|
|
"preset": None,
|
2023-03-10 14:00:06 -03:00
|
|
|
}
|
|
|
|
|
# update expected value for changed field
|
|
|
|
|
expected_response[field_name] = value
|
|
|
|
|
assert response.status_code == status.HTTP_201_CREATED
|
|
|
|
|
assert response.json() == expected_response
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.django_db
|
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
"field_name,value",
|
|
|
|
|
[
|
|
|
|
|
("data", "{{%"),
|
|
|
|
|
("headers", '"request-id": "{{}}"'),
|
|
|
|
|
("trigger_template", "integration_id == {{}}"),
|
|
|
|
|
("url", "invalid-url/{{}}/triggered"),
|
|
|
|
|
],
|
|
|
|
|
)
|
2023-08-02 20:07:47 +02:00
|
|
|
def test_create_invalid_templated_field(webhook_internal_api_setup, make_user_auth_headers, field_name, value):
|
2023-03-10 14:00:06 -03:00
|
|
|
user, token, webhook = webhook_internal_api_setup
|
|
|
|
|
client = APIClient()
|
|
|
|
|
url = reverse("api-internal:webhooks-list")
|
|
|
|
|
|
|
|
|
|
data = {
|
|
|
|
|
"name": "webhook_with_valid_data",
|
|
|
|
|
"url": TEST_URL,
|
|
|
|
|
field_name: value,
|
2023-09-27 07:22:52 -06:00
|
|
|
"trigger_type": Webhook.TRIGGER_ALERT_GROUP_CREATED,
|
|
|
|
|
"http_method": "POST",
|
2023-03-10 14:00:06 -03:00
|
|
|
"team": None,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
response = client.post(url, data, format="json", **make_user_auth_headers(user, token))
|
|
|
|
|
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.django_db
|
2023-08-02 20:07:47 +02:00
|
|
|
def test_update_webhook(webhook_internal_api_setup, make_user_auth_headers):
|
2023-03-10 14:00:06 -03:00
|
|
|
user, token, webhook = webhook_internal_api_setup
|
|
|
|
|
client = APIClient()
|
|
|
|
|
url = reverse("api-internal:webhooks-detail", kwargs={"pk": webhook.public_primary_key})
|
|
|
|
|
|
|
|
|
|
data = {
|
|
|
|
|
"name": "github_button_updated",
|
|
|
|
|
"url": "https://github.com/",
|
2023-09-27 07:22:52 -06:00
|
|
|
"trigger_type": Webhook.TRIGGER_ALERT_GROUP_CREATED,
|
|
|
|
|
"http_method": "POST",
|
2023-03-10 14:00:06 -03:00
|
|
|
"team": None,
|
|
|
|
|
}
|
|
|
|
|
response = client.put(
|
|
|
|
|
url, data=json.dumps(data), content_type="application/json", **make_user_auth_headers(user, token)
|
|
|
|
|
)
|
|
|
|
|
updated_instance = Webhook.objects.get(public_primary_key=webhook.public_primary_key)
|
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
|
|
|
assert updated_instance.name == "github_button_updated"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.django_db
|
2023-08-02 20:07:47 +02:00
|
|
|
def test_delete_webhook(webhook_internal_api_setup, make_user_auth_headers):
|
2023-03-10 14:00:06 -03:00
|
|
|
user, token, webhook = webhook_internal_api_setup
|
|
|
|
|
client = APIClient()
|
|
|
|
|
url = reverse("api-internal:webhooks-detail", kwargs={"pk": webhook.public_primary_key})
|
|
|
|
|
|
|
|
|
|
response = client.delete(url, **make_user_auth_headers(user, token))
|
|
|
|
|
assert response.status_code == status.HTTP_204_NO_CONTENT
|
|
|
|
|
|
|
|
|
|
|
2024-02-23 08:55:44 -03:00
|
|
|
@pytest.mark.django_db
|
|
|
|
|
def test_webhook_integration_filter(webhook_internal_api_setup, make_alert_receive_channel, make_user_auth_headers):
|
|
|
|
|
user, token, webhook = webhook_internal_api_setup
|
|
|
|
|
alert_receive_channel_1 = make_alert_receive_channel(user.organization)
|
|
|
|
|
alert_receive_channel_2 = make_alert_receive_channel(user.organization)
|
|
|
|
|
|
|
|
|
|
client = APIClient()
|
|
|
|
|
|
|
|
|
|
# create webhook setting integrations filter
|
|
|
|
|
url = reverse("api-internal:webhooks-list")
|
|
|
|
|
data = {
|
|
|
|
|
"name": "the_webhook",
|
|
|
|
|
"url": TEST_URL,
|
|
|
|
|
"trigger_type": Webhook.TRIGGER_ALERT_GROUP_CREATED,
|
|
|
|
|
"http_method": "POST",
|
|
|
|
|
"team": None,
|
|
|
|
|
"integration_filter": [alert_receive_channel_1.public_primary_key],
|
|
|
|
|
}
|
|
|
|
|
response = client.post(url, data, format="json", **make_user_auth_headers(user, token))
|
|
|
|
|
assert response.status_code == status.HTTP_201_CREATED
|
|
|
|
|
webhook = Webhook.objects.get(public_primary_key=response.json()["id"])
|
|
|
|
|
assert list(webhook.filtered_integrations.all()) == [alert_receive_channel_1]
|
|
|
|
|
assert response.json()["integration_filter"] == [alert_receive_channel_1.public_primary_key]
|
|
|
|
|
|
|
|
|
|
# update filter
|
|
|
|
|
url = reverse("api-internal:webhooks-detail", kwargs={"pk": webhook.public_primary_key})
|
|
|
|
|
data = {
|
|
|
|
|
"name": "github_button_updated",
|
|
|
|
|
"url": "https://github.com/",
|
|
|
|
|
"trigger_type": Webhook.TRIGGER_ALERT_GROUP_CREATED,
|
|
|
|
|
"http_method": "POST",
|
|
|
|
|
"team": None,
|
|
|
|
|
"integration_filter": [alert_receive_channel_1.public_primary_key, alert_receive_channel_2.public_primary_key],
|
|
|
|
|
}
|
|
|
|
|
response = client.put(
|
|
|
|
|
url, data=json.dumps(data), content_type="application/json", **make_user_auth_headers(user, token)
|
|
|
|
|
)
|
|
|
|
|
webhook.refresh_from_db()
|
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
|
|
|
assert list(webhook.filtered_integrations.all()) == [alert_receive_channel_1, alert_receive_channel_2]
|
|
|
|
|
assert response.json()["integration_filter"] == [
|
|
|
|
|
alert_receive_channel_1.public_primary_key,
|
|
|
|
|
alert_receive_channel_2.public_primary_key,
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
# clear filter
|
|
|
|
|
url = reverse("api-internal:webhooks-detail", kwargs={"pk": webhook.public_primary_key})
|
|
|
|
|
data = {
|
|
|
|
|
"name": "github_button_updated",
|
|
|
|
|
"url": "https://github.com/",
|
|
|
|
|
"trigger_type": Webhook.TRIGGER_ALERT_GROUP_CREATED,
|
|
|
|
|
"http_method": "POST",
|
|
|
|
|
"team": None,
|
|
|
|
|
"integration_filter": [],
|
|
|
|
|
}
|
|
|
|
|
response = client.put(
|
|
|
|
|
url, data=json.dumps(data), content_type="application/json", **make_user_auth_headers(user, token)
|
|
|
|
|
)
|
|
|
|
|
webhook.refresh_from_db()
|
2024-03-05 14:11:47 -03:00
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
|
|
|
assert list(webhook.filtered_integrations.all()) == []
|
|
|
|
|
assert response.json()["integration_filter"] == []
|
|
|
|
|
|
|
|
|
|
# clear filter also works if set to None
|
|
|
|
|
url = reverse("api-internal:webhooks-detail", kwargs={"pk": webhook.public_primary_key})
|
|
|
|
|
data = {
|
|
|
|
|
"name": "github_button_updated",
|
|
|
|
|
"url": "https://github.com/",
|
|
|
|
|
"trigger_type": Webhook.TRIGGER_ALERT_GROUP_CREATED,
|
|
|
|
|
"http_method": "POST",
|
|
|
|
|
"team": None,
|
|
|
|
|
"integration_filter": None,
|
|
|
|
|
}
|
|
|
|
|
response = client.put(
|
|
|
|
|
url, data=json.dumps(data), content_type="application/json", **make_user_auth_headers(user, token)
|
|
|
|
|
)
|
|
|
|
|
webhook.refresh_from_db()
|
2024-02-23 08:55:44 -03:00
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
|
|
|
assert list(webhook.filtered_integrations.all()) == []
|
|
|
|
|
assert response.json()["integration_filter"] == []
|
|
|
|
|
|
|
|
|
|
|
2023-03-10 14:00:06 -03:00
|
|
|
@pytest.mark.django_db
|
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
"role,expected_status",
|
|
|
|
|
[
|
|
|
|
|
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
|
|
|
|
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
|
|
|
|
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
2023-10-19 14:39:08 -03:00
|
|
|
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
2023-03-10 14:00:06 -03:00
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
def test_webhook_create_permissions(
|
|
|
|
|
make_organization_and_user_with_plugin_token,
|
|
|
|
|
make_user_auth_headers,
|
|
|
|
|
role,
|
|
|
|
|
expected_status,
|
|
|
|
|
):
|
|
|
|
|
_, user, token = make_organization_and_user_with_plugin_token(role)
|
|
|
|
|
client = APIClient()
|
|
|
|
|
|
|
|
|
|
url = reverse("api-internal:webhooks-list")
|
|
|
|
|
|
|
|
|
|
with patch(
|
|
|
|
|
"apps.api.views.webhooks.WebhooksView.create",
|
|
|
|
|
return_value=Response(
|
|
|
|
|
status=status.HTTP_200_OK,
|
|
|
|
|
),
|
|
|
|
|
):
|
|
|
|
|
response = client.post(url, format="json", **make_user_auth_headers(user, token))
|
|
|
|
|
|
|
|
|
|
assert response.status_code == expected_status
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.django_db
|
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
"role,expected_status",
|
|
|
|
|
[
|
|
|
|
|
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
|
|
|
|
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
|
|
|
|
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
2023-10-19 14:39:08 -03:00
|
|
|
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
2023-03-10 14:00:06 -03:00
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
def test_webhook_update_permissions(
|
|
|
|
|
make_organization_and_user_with_plugin_token,
|
|
|
|
|
make_custom_webhook,
|
|
|
|
|
make_user_auth_headers,
|
|
|
|
|
role,
|
|
|
|
|
expected_status,
|
|
|
|
|
):
|
|
|
|
|
organization, user, token = make_organization_and_user_with_plugin_token(role)
|
|
|
|
|
webhook = make_custom_webhook(organization=organization)
|
|
|
|
|
client = APIClient()
|
|
|
|
|
|
|
|
|
|
url = reverse("api-internal:webhooks-detail", kwargs={"pk": webhook.public_primary_key})
|
|
|
|
|
|
|
|
|
|
with patch(
|
|
|
|
|
"apps.api.views.webhooks.WebhooksView.update",
|
|
|
|
|
return_value=Response(
|
|
|
|
|
status=status.HTTP_200_OK,
|
|
|
|
|
),
|
|
|
|
|
):
|
|
|
|
|
response = client.put(url, format="json", **make_user_auth_headers(user, token))
|
|
|
|
|
|
|
|
|
|
assert response.status_code == expected_status
|
|
|
|
|
|
|
|
|
|
response = client.patch(url, format="json", **make_user_auth_headers(user, token))
|
|
|
|
|
|
|
|
|
|
assert response.status_code == expected_status
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.django_db
|
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
"role,expected_status",
|
|
|
|
|
[
|
|
|
|
|
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
|
|
|
|
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
|
|
|
|
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
2023-10-19 14:39:08 -03:00
|
|
|
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
2023-03-10 14:00:06 -03:00
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
def test_webhook_list_permissions(
|
|
|
|
|
make_organization_and_user_with_plugin_token,
|
|
|
|
|
make_custom_webhook,
|
|
|
|
|
make_user_auth_headers,
|
|
|
|
|
role,
|
|
|
|
|
expected_status,
|
|
|
|
|
):
|
|
|
|
|
organization, user, token = make_organization_and_user_with_plugin_token(role)
|
|
|
|
|
make_custom_webhook(organization=organization)
|
|
|
|
|
client = APIClient()
|
|
|
|
|
|
|
|
|
|
url = reverse("api-internal:webhooks-list")
|
|
|
|
|
|
|
|
|
|
with patch(
|
|
|
|
|
"apps.api.views.webhooks.WebhooksView.list",
|
|
|
|
|
return_value=Response(
|
|
|
|
|
status=status.HTTP_200_OK,
|
|
|
|
|
),
|
|
|
|
|
):
|
|
|
|
|
response = client.get(url, format="json", **make_user_auth_headers(user, token))
|
|
|
|
|
|
|
|
|
|
assert response.status_code == expected_status
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.django_db
|
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
"role,expected_status",
|
|
|
|
|
[
|
|
|
|
|
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
|
|
|
|
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
|
|
|
|
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
2023-10-19 14:39:08 -03:00
|
|
|
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
2023-03-10 14:00:06 -03:00
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
def test_webhook_retrieve_permissions(
|
|
|
|
|
make_organization_and_user_with_plugin_token,
|
|
|
|
|
make_custom_webhook,
|
|
|
|
|
make_user_auth_headers,
|
|
|
|
|
role,
|
|
|
|
|
expected_status,
|
|
|
|
|
):
|
|
|
|
|
organization, user, token = make_organization_and_user_with_plugin_token(role)
|
|
|
|
|
webhook = make_custom_webhook(organization=organization)
|
|
|
|
|
client = APIClient()
|
|
|
|
|
|
|
|
|
|
url = reverse("api-internal:webhooks-detail", kwargs={"pk": webhook.public_primary_key})
|
|
|
|
|
|
|
|
|
|
with patch(
|
|
|
|
|
"apps.api.views.webhooks.WebhooksView.retrieve",
|
|
|
|
|
return_value=Response(
|
|
|
|
|
status=status.HTTP_200_OK,
|
|
|
|
|
),
|
|
|
|
|
):
|
|
|
|
|
response = client.get(url, format="json", **make_user_auth_headers(user, token))
|
|
|
|
|
|
|
|
|
|
assert response.status_code == expected_status
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.django_db
|
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
"role,expected_status",
|
|
|
|
|
[
|
|
|
|
|
(LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT),
|
|
|
|
|
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
|
|
|
|
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
2023-10-19 14:39:08 -03:00
|
|
|
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
2023-03-10 14:00:06 -03:00
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
def test_webhook_delete_permissions(
|
|
|
|
|
make_organization_and_user_with_plugin_token,
|
|
|
|
|
make_custom_webhook,
|
|
|
|
|
make_user_auth_headers,
|
|
|
|
|
role,
|
|
|
|
|
expected_status,
|
|
|
|
|
):
|
|
|
|
|
organization, user, token = make_organization_and_user_with_plugin_token(role)
|
|
|
|
|
webhook = make_custom_webhook(organization=organization)
|
|
|
|
|
client = APIClient()
|
|
|
|
|
|
|
|
|
|
url = reverse("api-internal:webhooks-detail", kwargs={"pk": webhook.public_primary_key})
|
|
|
|
|
|
|
|
|
|
with patch(
|
|
|
|
|
"apps.api.views.webhooks.WebhooksView.destroy",
|
|
|
|
|
return_value=Response(
|
|
|
|
|
status=status.HTTP_204_NO_CONTENT,
|
|
|
|
|
),
|
|
|
|
|
):
|
|
|
|
|
response = client.delete(url, format="json", **make_user_auth_headers(user, token))
|
|
|
|
|
|
|
|
|
|
assert response.status_code == expected_status
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.django_db
|
|
|
|
|
def test_get_webhook_from_other_team_with_flag(
|
|
|
|
|
make_organization_and_user_with_plugin_token,
|
|
|
|
|
make_team,
|
|
|
|
|
make_user_auth_headers,
|
|
|
|
|
make_custom_webhook,
|
|
|
|
|
):
|
|
|
|
|
organization, user, token = make_organization_and_user_with_plugin_token()
|
|
|
|
|
|
|
|
|
|
team = make_team(organization)
|
|
|
|
|
|
|
|
|
|
webhook = make_custom_webhook(organization=organization, team=team)
|
|
|
|
|
client = APIClient()
|
|
|
|
|
|
|
|
|
|
url = reverse("api-internal:webhooks-detail", kwargs={"pk": webhook.public_primary_key})
|
|
|
|
|
url = f"{url}?from_organization=true"
|
|
|
|
|
|
|
|
|
|
response = client.get(url, format="json", **make_user_auth_headers(user, token))
|
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.django_db
|
|
|
|
|
def test_webhook_from_other_team_without_flag(
|
|
|
|
|
make_organization_and_user_with_plugin_token,
|
|
|
|
|
make_team,
|
|
|
|
|
make_user_auth_headers,
|
|
|
|
|
make_custom_webhook,
|
|
|
|
|
):
|
|
|
|
|
organization, user, token = make_organization_and_user_with_plugin_token()
|
|
|
|
|
|
|
|
|
|
team = make_team(organization)
|
|
|
|
|
|
|
|
|
|
webhook = make_custom_webhook(organization=organization, team=team)
|
|
|
|
|
client = APIClient()
|
|
|
|
|
|
|
|
|
|
url = reverse("api-internal:webhooks-detail", kwargs={"pk": webhook.public_primary_key})
|
|
|
|
|
|
|
|
|
|
response = client.get(url, format="json", **make_user_auth_headers(user, token))
|
2023-03-22 00:57:20 +08:00
|
|
|
assert response.status_code == status.HTTP_200_OK
|
2023-07-11 21:03:34 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.django_db
|
|
|
|
|
def test_get_webhook_responses(
|
|
|
|
|
make_organization_and_user_with_plugin_token,
|
|
|
|
|
make_team,
|
|
|
|
|
make_user_auth_headers,
|
|
|
|
|
make_custom_webhook,
|
|
|
|
|
make_webhook_response,
|
|
|
|
|
):
|
|
|
|
|
organization, user, token = make_organization_and_user_with_plugin_token()
|
|
|
|
|
team = make_team(organization)
|
|
|
|
|
webhook = make_custom_webhook(
|
|
|
|
|
organization=organization, team=team, trigger_type=Webhook.TRIGGER_ALERT_GROUP_CREATED
|
|
|
|
|
)
|
|
|
|
|
for i in range(0, RECENT_RESPONSE_LIMIT + 1):
|
|
|
|
|
make_webhook_response(
|
|
|
|
|
webhook=webhook,
|
|
|
|
|
trigger_type=webhook.trigger_type,
|
|
|
|
|
status_code=200,
|
|
|
|
|
content=json.dumps({"id": "third-party-id"}),
|
|
|
|
|
event_data=json.dumps({"test": f"{i}"}),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
client = APIClient()
|
|
|
|
|
url = reverse("api-internal:webhooks-responses", kwargs={"pk": webhook.public_primary_key})
|
|
|
|
|
response = client.get(url, format="json", **make_user_auth_headers(user, token))
|
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
|
|
|
assert len(response.data) == RECENT_RESPONSE_LIMIT
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.django_db
|
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
"test_template, test_payload, expected_result",
|
|
|
|
|
[
|
|
|
|
|
("https://test.com", None, "https://test.com"),
|
|
|
|
|
("https://test.com", "", "https://test.com"),
|
|
|
|
|
("{{ name }}", {"name": "test_1"}, "test_1"),
|
|
|
|
|
("{{ name }}", '{"name": "test_1"}', "test_1"),
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
def test_webhook_preview_template(
|
|
|
|
|
webhook_internal_api_setup, make_user_auth_headers, test_template, test_payload, expected_result
|
|
|
|
|
):
|
|
|
|
|
user, token, webhook = webhook_internal_api_setup
|
|
|
|
|
client = APIClient()
|
|
|
|
|
url = reverse("api-internal:webhooks-preview-template", kwargs={"pk": webhook.public_primary_key})
|
|
|
|
|
data = {
|
|
|
|
|
"template_name": WEBHOOK_URL,
|
|
|
|
|
"template_body": test_template,
|
|
|
|
|
"payload": test_payload,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
response = client.post(url, data, format="json", **make_user_auth_headers(user, token))
|
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
|
|
|
assert response.data["preview"] == expected_result
|
2023-07-20 15:23:33 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.django_db
|
2023-08-02 20:07:47 +02:00
|
|
|
def test_webhook_field_masking(webhook_internal_api_setup, make_user_auth_headers):
|
2023-07-20 15:23:33 -06:00
|
|
|
user, token, webhook = webhook_internal_api_setup
|
|
|
|
|
client = APIClient()
|
|
|
|
|
url = reverse("api-internal:webhooks-list")
|
|
|
|
|
|
|
|
|
|
data = {
|
|
|
|
|
"name": "the_webhook",
|
|
|
|
|
"url": TEST_URL,
|
2023-09-27 07:22:52 -06:00
|
|
|
"trigger_type": Webhook.TRIGGER_ALERT_GROUP_CREATED,
|
|
|
|
|
"http_method": "POST",
|
2023-07-20 15:23:33 -06:00
|
|
|
"team": None,
|
|
|
|
|
"password": "secret_password",
|
|
|
|
|
"authorization_header": "auth 1234",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
response = client.post(url, data, format="json", **make_user_auth_headers(user, token))
|
|
|
|
|
webhook = Webhook.objects.get(public_primary_key=response.data["id"])
|
|
|
|
|
|
|
|
|
|
expected_response = data | {
|
|
|
|
|
"id": webhook.public_primary_key,
|
|
|
|
|
"data": None,
|
|
|
|
|
"username": None,
|
|
|
|
|
"password": WEBHOOK_FIELD_PLACEHOLDER,
|
|
|
|
|
"authorization_header": WEBHOOK_FIELD_PLACEHOLDER,
|
|
|
|
|
"forward_all": True,
|
|
|
|
|
"headers": None,
|
|
|
|
|
"http_method": "POST",
|
2024-02-23 08:55:44 -03:00
|
|
|
"integration_filter": [],
|
2023-07-20 15:23:33 -06:00
|
|
|
"is_webhook_enabled": True,
|
Webhook labels (#3383)
This PR add labels for webhooks.
1. Make webhook "labelable" with ability to filter by labels.
2. Add labels to the webhook payload. It contain new field webhook with
it's name, id and labels. Field integration and alert_group has a
corresponding label field as well. See example of a new payload below:
```
{
"event": {
"type": "escalation"
},
"user": null,
"alert_group": {
"id": "IRFN6ZD31N31B",
"integration_id": "CTWM7U4A2QG97",
"route_id": "RUE7U7Z46SKGY",
"alerts_count": 1,
"state": "firing",
"created_at": "2023-11-22T08:54:55.178243Z",
"resolved_at": null,
"acknowledged_at": null,
"title": "Incident",
"permalinks": {
"slack": null,
"telegram": null,
"web": "http://grafana:3000/a/grafana-oncall-app/alert-groups/IRFN6ZD31N31B"
},
"labels": {
"severity": "critical"
}
},
"alert_group_id": "IRFN6ZD31N31B",
"alert_payload": {
"message": "This alert was sent by user for demonstration purposes"
},
"integration": {
"id": "CTWM7U4A2QG97",
"type": "webhook",
"name": "hi - Webhook",
"team": null,
"labels": {
"hello": "world",
"severity": "critical"
}
},
"notified_users": [],
"users_to_be_notified": [],
"webhook": {
"id": "WHAXK4BTC7TAEQ",
"name": "test",
"labels": {
"hello": "kesha"
}
}
}
```
I feel that there is an opportunity to make code cleaner - remove all
label logic from serializers, views and utils to models or dedicated
LabelerService and introduce Labelable interface with something like
label_verbal, update_labels methods. However, I don't want to tie
webhook labels with a refactoring.
---------
Co-authored-by: Dominik <dominik.broj@grafana.com>
2023-11-22 19:17:41 +08:00
|
|
|
"labels": [],
|
2023-07-20 15:23:33 -06:00
|
|
|
"is_legacy": False,
|
|
|
|
|
"last_response_log": {
|
|
|
|
|
"request_data": "",
|
|
|
|
|
"request_headers": "",
|
|
|
|
|
"timestamp": None,
|
|
|
|
|
"content": "",
|
|
|
|
|
"status_code": None,
|
|
|
|
|
"request_trigger": "",
|
|
|
|
|
"url": "",
|
|
|
|
|
"event_data": "",
|
|
|
|
|
},
|
|
|
|
|
"trigger_template": None,
|
2023-09-27 07:22:52 -06:00
|
|
|
"trigger_type": str(data["trigger_type"]),
|
2023-07-20 15:23:33 -06:00
|
|
|
"trigger_type_name": "Alert Group Created",
|
2023-09-27 07:22:52 -06:00
|
|
|
"preset": None,
|
2023-07-20 15:23:33 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
assert response.status_code == status.HTTP_201_CREATED
|
|
|
|
|
assert response.json() == expected_response
|
|
|
|
|
assert webhook.password == data["password"]
|
|
|
|
|
assert webhook.authorization_header == data["authorization_header"]
|
|
|
|
|
assert webhook.user == user
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.django_db
|
2023-08-02 20:07:47 +02:00
|
|
|
def test_webhook_copy(webhook_internal_api_setup, make_user_auth_headers):
|
2023-07-20 15:23:33 -06:00
|
|
|
user, token, webhook = webhook_internal_api_setup
|
|
|
|
|
client = APIClient()
|
|
|
|
|
url = reverse("api-internal:webhooks-list")
|
|
|
|
|
|
|
|
|
|
data = {
|
|
|
|
|
"name": "the_webhook",
|
|
|
|
|
"url": TEST_URL,
|
2023-09-27 07:22:52 -06:00
|
|
|
"trigger_type": Webhook.TRIGGER_ALERT_GROUP_CREATED,
|
|
|
|
|
"http_method": "POST",
|
2023-07-20 15:23:33 -06:00
|
|
|
"team": None,
|
|
|
|
|
"password": "secret_password",
|
|
|
|
|
"authorization_header": "auth 1234",
|
|
|
|
|
}
|
|
|
|
|
response1 = client.post(url, data, format="json", **make_user_auth_headers(user, token))
|
|
|
|
|
get_url = reverse("api-internal:webhooks-detail", kwargs={"pk": response1.data["id"]})
|
|
|
|
|
response2 = client.get(get_url, format="json", **make_user_auth_headers(user, token))
|
|
|
|
|
to_copy = response2.json()
|
|
|
|
|
to_copy["name"] = "copied_webhook"
|
|
|
|
|
response3 = client.post(url, to_copy, format="json", **make_user_auth_headers(user, token))
|
|
|
|
|
webhook = Webhook.objects.get(public_primary_key=response3.data["id"])
|
|
|
|
|
|
|
|
|
|
expected_response = data | {
|
|
|
|
|
"id": webhook.public_primary_key,
|
|
|
|
|
"name": to_copy["name"],
|
|
|
|
|
"data": None,
|
|
|
|
|
"username": None,
|
|
|
|
|
"password": WEBHOOK_FIELD_PLACEHOLDER,
|
|
|
|
|
"authorization_header": WEBHOOK_FIELD_PLACEHOLDER,
|
|
|
|
|
"forward_all": True,
|
|
|
|
|
"headers": None,
|
|
|
|
|
"http_method": "POST",
|
2024-02-23 08:55:44 -03:00
|
|
|
"integration_filter": [],
|
2023-07-20 15:23:33 -06:00
|
|
|
"is_webhook_enabled": True,
|
Webhook labels (#3383)
This PR add labels for webhooks.
1. Make webhook "labelable" with ability to filter by labels.
2. Add labels to the webhook payload. It contain new field webhook with
it's name, id and labels. Field integration and alert_group has a
corresponding label field as well. See example of a new payload below:
```
{
"event": {
"type": "escalation"
},
"user": null,
"alert_group": {
"id": "IRFN6ZD31N31B",
"integration_id": "CTWM7U4A2QG97",
"route_id": "RUE7U7Z46SKGY",
"alerts_count": 1,
"state": "firing",
"created_at": "2023-11-22T08:54:55.178243Z",
"resolved_at": null,
"acknowledged_at": null,
"title": "Incident",
"permalinks": {
"slack": null,
"telegram": null,
"web": "http://grafana:3000/a/grafana-oncall-app/alert-groups/IRFN6ZD31N31B"
},
"labels": {
"severity": "critical"
}
},
"alert_group_id": "IRFN6ZD31N31B",
"alert_payload": {
"message": "This alert was sent by user for demonstration purposes"
},
"integration": {
"id": "CTWM7U4A2QG97",
"type": "webhook",
"name": "hi - Webhook",
"team": null,
"labels": {
"hello": "world",
"severity": "critical"
}
},
"notified_users": [],
"users_to_be_notified": [],
"webhook": {
"id": "WHAXK4BTC7TAEQ",
"name": "test",
"labels": {
"hello": "kesha"
}
}
}
```
I feel that there is an opportunity to make code cleaner - remove all
label logic from serializers, views and utils to models or dedicated
LabelerService and introduce Labelable interface with something like
label_verbal, update_labels methods. However, I don't want to tie
webhook labels with a refactoring.
---------
Co-authored-by: Dominik <dominik.broj@grafana.com>
2023-11-22 19:17:41 +08:00
|
|
|
"labels": [],
|
2023-07-20 15:23:33 -06:00
|
|
|
"is_legacy": False,
|
|
|
|
|
"last_response_log": {
|
|
|
|
|
"request_data": "",
|
|
|
|
|
"request_headers": "",
|
|
|
|
|
"timestamp": None,
|
|
|
|
|
"content": "",
|
|
|
|
|
"status_code": None,
|
|
|
|
|
"request_trigger": "",
|
|
|
|
|
"url": "",
|
|
|
|
|
"event_data": "",
|
|
|
|
|
},
|
|
|
|
|
"trigger_template": None,
|
2023-09-27 07:22:52 -06:00
|
|
|
"trigger_type": str(data["trigger_type"]),
|
2023-07-20 15:23:33 -06:00
|
|
|
"trigger_type_name": "Alert Group Created",
|
2023-09-27 07:22:52 -06:00
|
|
|
"preset": None,
|
2023-07-20 15:23:33 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
assert response3.status_code == status.HTTP_201_CREATED
|
|
|
|
|
assert response3.json() == expected_response
|
|
|
|
|
assert webhook.password == data["password"]
|
|
|
|
|
assert webhook.authorization_header == data["authorization_header"]
|
|
|
|
|
assert webhook.id != to_copy["id"]
|
|
|
|
|
assert webhook.user == user
|
2023-09-27 07:22:52 -06:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.django_db
|
|
|
|
|
def test_create_invalid_missing_fields(webhook_internal_api_setup, make_user_auth_headers):
|
|
|
|
|
user, token, webhook = webhook_internal_api_setup
|
|
|
|
|
client = APIClient()
|
|
|
|
|
url = reverse("api-internal:webhooks-list")
|
|
|
|
|
|
|
|
|
|
data = {"url": TEST_URL, "trigger_type": Webhook.TRIGGER_ALERT_GROUP_CREATED, "http_method": "POST"}
|
|
|
|
|
response = client.post(url, data, format="json", **make_user_auth_headers(user, token))
|
|
|
|
|
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
|
|
|
|
assert response.json()["name"][0] == "This field is required."
|
|
|
|
|
|
|
|
|
|
data = {"name": "test webhook 1", "trigger_type": Webhook.TRIGGER_ALERT_GROUP_CREATED, "http_method": "POST"}
|
|
|
|
|
response = client.post(url, data, format="json", **make_user_auth_headers(user, token))
|
|
|
|
|
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
|
|
|
|
assert response.json()["url"][0] == "This field is required."
|
|
|
|
|
|
|
|
|
|
data = {"name": "test webhook 2", "url": TEST_URL, "http_method": "POST"}
|
|
|
|
|
response = client.post(url, data, format="json", **make_user_auth_headers(user, token))
|
|
|
|
|
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
|
|
|
|
assert response.json()["trigger_type"][0] == "This field is required."
|
|
|
|
|
|
|
|
|
|
data = {
|
|
|
|
|
"name": "test webhook 3",
|
|
|
|
|
"url": TEST_URL,
|
|
|
|
|
"trigger_type": Webhook.TRIGGER_ALERT_GROUP_CREATED,
|
|
|
|
|
}
|
|
|
|
|
response = client.post(url, data, format="json", **make_user_auth_headers(user, token))
|
|
|
|
|
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
2023-12-20 19:19:50 +05:30
|
|
|
assert (
|
|
|
|
|
response.json()["http_method"][0]
|
|
|
|
|
== "This field must be one of ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH']."
|
|
|
|
|
)
|
2023-09-27 07:22:52 -06:00
|
|
|
|
|
|
|
|
data = {
|
|
|
|
|
"name": "test webhook 3",
|
|
|
|
|
"url": TEST_URL,
|
|
|
|
|
"trigger_type": Webhook.TRIGGER_ALERT_GROUP_CREATED,
|
|
|
|
|
"http_method": "TOAST",
|
|
|
|
|
}
|
|
|
|
|
response = client.post(url, data, format="json", **make_user_auth_headers(user, token))
|
|
|
|
|
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
2023-12-20 19:19:50 +05:30
|
|
|
assert (
|
|
|
|
|
response.json()["http_method"][0]
|
|
|
|
|
== "This field must be one of ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH']."
|
|
|
|
|
)
|
2023-09-27 07:22:52 -06:00
|
|
|
|
|
|
|
|
data = {"name": "test webhook 3", "url": TEST_URL, "trigger_type": 2000000, "http_method": "POST"}
|
|
|
|
|
response = client.post(url, data, format="json", **make_user_auth_headers(user, token))
|
|
|
|
|
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
|
|
|
|
assert response.json()["trigger_type"][0] == "This field is required."
|
Webhook labels (#3383)
This PR add labels for webhooks.
1. Make webhook "labelable" with ability to filter by labels.
2. Add labels to the webhook payload. It contain new field webhook with
it's name, id and labels. Field integration and alert_group has a
corresponding label field as well. See example of a new payload below:
```
{
"event": {
"type": "escalation"
},
"user": null,
"alert_group": {
"id": "IRFN6ZD31N31B",
"integration_id": "CTWM7U4A2QG97",
"route_id": "RUE7U7Z46SKGY",
"alerts_count": 1,
"state": "firing",
"created_at": "2023-11-22T08:54:55.178243Z",
"resolved_at": null,
"acknowledged_at": null,
"title": "Incident",
"permalinks": {
"slack": null,
"telegram": null,
"web": "http://grafana:3000/a/grafana-oncall-app/alert-groups/IRFN6ZD31N31B"
},
"labels": {
"severity": "critical"
}
},
"alert_group_id": "IRFN6ZD31N31B",
"alert_payload": {
"message": "This alert was sent by user for demonstration purposes"
},
"integration": {
"id": "CTWM7U4A2QG97",
"type": "webhook",
"name": "hi - Webhook",
"team": null,
"labels": {
"hello": "world",
"severity": "critical"
}
},
"notified_users": [],
"users_to_be_notified": [],
"webhook": {
"id": "WHAXK4BTC7TAEQ",
"name": "test",
"labels": {
"hello": "kesha"
}
}
}
```
I feel that there is an opportunity to make code cleaner - remove all
label logic from serializers, views and utils to models or dedicated
LabelerService and introduce Labelable interface with something like
label_verbal, update_labels methods. However, I don't want to tie
webhook labels with a refactoring.
---------
Co-authored-by: Dominik <dominik.broj@grafana.com>
2023-11-22 19:17:41 +08:00
|
|
|
|
|
|
|
|
|
2024-09-09 09:17:23 -03:00
|
|
|
@pytest.mark.django_db
|
|
|
|
|
def test_webhook_filter_by_trigger_type(
|
|
|
|
|
make_organization_and_user_with_plugin_token,
|
|
|
|
|
make_custom_webhook,
|
|
|
|
|
make_user_auth_headers,
|
|
|
|
|
):
|
|
|
|
|
organization, user, token = make_organization_and_user_with_plugin_token()
|
|
|
|
|
webhook_on_ack = make_custom_webhook(organization, trigger_type=Webhook.TRIGGER_ACKNOWLEDGE)
|
|
|
|
|
make_custom_webhook(organization, trigger_type=Webhook.TRIGGER_MANUAL)
|
|
|
|
|
|
|
|
|
|
client = APIClient()
|
|
|
|
|
|
|
|
|
|
# no filter
|
|
|
|
|
url = reverse("api-internal:webhooks-list")
|
|
|
|
|
response = client.get(
|
|
|
|
|
url,
|
|
|
|
|
content_type="application/json",
|
|
|
|
|
**make_user_auth_headers(user, token),
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
|
|
|
assert len(response.json()) == 2
|
|
|
|
|
|
|
|
|
|
# test filter on type
|
|
|
|
|
url = reverse("api-internal:webhooks-list")
|
|
|
|
|
response = client.get(
|
|
|
|
|
f"{url}?trigger_type={Webhook.TRIGGER_ACKNOWLEDGE}",
|
|
|
|
|
content_type="application/json",
|
|
|
|
|
**make_user_auth_headers(user, token),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
|
|
|
assert len(response.json()) == 1
|
|
|
|
|
assert response.json()[0]["id"] == webhook_on_ack.public_primary_key
|
|
|
|
|
|
|
|
|
|
# test filter empty results
|
|
|
|
|
response = client.get(
|
|
|
|
|
f"{url}?trigger_type={Webhook.TRIGGER_STATUS_CHANGE}",
|
|
|
|
|
content_type="application/json",
|
|
|
|
|
**make_user_auth_headers(user, token),
|
|
|
|
|
)
|
|
|
|
|
assert len(response.json()) == 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.django_db
|
|
|
|
|
def test_webhook_filter_by_integration(
|
|
|
|
|
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()
|
|
|
|
|
webhook_all = make_custom_webhook(organization)
|
|
|
|
|
integration = make_alert_receive_channel(organization)
|
|
|
|
|
webhook_for_integration = make_custom_webhook(organization)
|
|
|
|
|
webhook_for_integration.filtered_integrations.add(integration)
|
|
|
|
|
another_integration = make_alert_receive_channel(organization)
|
|
|
|
|
another_webhook = make_custom_webhook(organization)
|
|
|
|
|
another_webhook.filtered_integrations.add(another_integration)
|
|
|
|
|
|
|
|
|
|
client = APIClient()
|
|
|
|
|
|
|
|
|
|
# no filter
|
|
|
|
|
url = reverse("api-internal:webhooks-list")
|
|
|
|
|
response = client.get(
|
|
|
|
|
url,
|
|
|
|
|
content_type="application/json",
|
|
|
|
|
**make_user_auth_headers(user, token),
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
|
|
|
assert len(response.json()) == 3
|
|
|
|
|
|
|
|
|
|
# test filter on integration
|
|
|
|
|
url = reverse("api-internal:webhooks-list")
|
|
|
|
|
response = client.get(
|
|
|
|
|
f"{url}?integration={integration.public_primary_key}",
|
|
|
|
|
content_type="application/json",
|
|
|
|
|
**make_user_auth_headers(user, token),
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
|
|
|
assert len(response.json()) == 2
|
|
|
|
|
expected = {webhook_all.public_primary_key, webhook_for_integration.public_primary_key}
|
|
|
|
|
assert set(w["id"] for w in response.json()) == expected
|
|
|
|
|
|
|
|
|
|
|
Webhook labels (#3383)
This PR add labels for webhooks.
1. Make webhook "labelable" with ability to filter by labels.
2. Add labels to the webhook payload. It contain new field webhook with
it's name, id and labels. Field integration and alert_group has a
corresponding label field as well. See example of a new payload below:
```
{
"event": {
"type": "escalation"
},
"user": null,
"alert_group": {
"id": "IRFN6ZD31N31B",
"integration_id": "CTWM7U4A2QG97",
"route_id": "RUE7U7Z46SKGY",
"alerts_count": 1,
"state": "firing",
"created_at": "2023-11-22T08:54:55.178243Z",
"resolved_at": null,
"acknowledged_at": null,
"title": "Incident",
"permalinks": {
"slack": null,
"telegram": null,
"web": "http://grafana:3000/a/grafana-oncall-app/alert-groups/IRFN6ZD31N31B"
},
"labels": {
"severity": "critical"
}
},
"alert_group_id": "IRFN6ZD31N31B",
"alert_payload": {
"message": "This alert was sent by user for demonstration purposes"
},
"integration": {
"id": "CTWM7U4A2QG97",
"type": "webhook",
"name": "hi - Webhook",
"team": null,
"labels": {
"hello": "world",
"severity": "critical"
}
},
"notified_users": [],
"users_to_be_notified": [],
"webhook": {
"id": "WHAXK4BTC7TAEQ",
"name": "test",
"labels": {
"hello": "kesha"
}
}
}
```
I feel that there is an opportunity to make code cleaner - remove all
label logic from serializers, views and utils to models or dedicated
LabelerService and introduce Labelable interface with something like
label_verbal, update_labels methods. However, I don't want to tie
webhook labels with a refactoring.
---------
Co-authored-by: Dominik <dominik.broj@grafana.com>
2023-11-22 19:17:41 +08:00
|
|
|
@pytest.mark.django_db
|
|
|
|
|
def test_webhook_filter_by_labels(
|
|
|
|
|
make_organization_and_user_with_plugin_token,
|
|
|
|
|
make_custom_webhook,
|
|
|
|
|
make_webhook_label_association,
|
|
|
|
|
make_label_key_and_value,
|
|
|
|
|
make_user_auth_headers,
|
|
|
|
|
):
|
|
|
|
|
organization, user, token = make_organization_and_user_with_plugin_token()
|
|
|
|
|
webhook_with_label = make_custom_webhook(organization)
|
|
|
|
|
label = make_webhook_label_association(organization, webhook_with_label)
|
|
|
|
|
|
|
|
|
|
webhook_with_another_label = make_custom_webhook(organization)
|
|
|
|
|
another_label = make_webhook_label_association(organization, webhook_with_another_label)
|
|
|
|
|
|
|
|
|
|
not_attached_key, not_attached_value = make_label_key_and_value(organization)
|
|
|
|
|
|
|
|
|
|
client = APIClient()
|
|
|
|
|
|
|
|
|
|
# test filter by label, which is attached to only one webhook
|
|
|
|
|
url = reverse("api-internal:webhooks-list")
|
|
|
|
|
response = client.get(
|
|
|
|
|
f"{url}?label={label.key_id}:{label.value_id}",
|
|
|
|
|
content_type="application/json",
|
|
|
|
|
**make_user_auth_headers(user, token),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
|
|
|
assert len(response.json()) == 1
|
|
|
|
|
assert response.json()[0]["id"] == webhook_with_label.public_primary_key
|
|
|
|
|
|
|
|
|
|
url = reverse("api-internal:webhooks-list")
|
|
|
|
|
response = client.get(
|
|
|
|
|
f"{url}?label={another_label.key_id}:{another_label.value_id}",
|
|
|
|
|
content_type="application/json",
|
|
|
|
|
**make_user_auth_headers(user, token),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
|
|
|
assert len(response.json()) == 1
|
|
|
|
|
assert response.json()[0]["id"] == webhook_with_another_label.public_primary_key
|
|
|
|
|
|
|
|
|
|
# test filter by label which is not attached to any webhooks
|
|
|
|
|
response = client.get(
|
|
|
|
|
f"{url}?label={not_attached_key.id}:{not_attached_value.id}",
|
|
|
|
|
content_type="application/json",
|
|
|
|
|
**make_user_auth_headers(user, token),
|
|
|
|
|
)
|
|
|
|
|
assert len(response.json()) == 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.django_db
|
|
|
|
|
def test_update_webhook_labels(
|
|
|
|
|
webhook_internal_api_setup,
|
|
|
|
|
make_user_auth_headers,
|
|
|
|
|
):
|
|
|
|
|
user, token, webhook = webhook_internal_api_setup
|
|
|
|
|
client = APIClient()
|
|
|
|
|
|
|
|
|
|
url = reverse("api-internal:webhooks-detail", kwargs={"pk": webhook.public_primary_key})
|
|
|
|
|
key_id = "testkey"
|
|
|
|
|
value_id = "testvalue"
|
2024-02-20 14:42:51 +08:00
|
|
|
data = {
|
|
|
|
|
"labels": [
|
|
|
|
|
{
|
|
|
|
|
"key": {"id": key_id, "name": "test", "prescribed": False},
|
|
|
|
|
"value": {"id": value_id, "name": "testv", "prescribed": False},
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
}
|
Webhook labels (#3383)
This PR add labels for webhooks.
1. Make webhook "labelable" with ability to filter by labels.
2. Add labels to the webhook payload. It contain new field webhook with
it's name, id and labels. Field integration and alert_group has a
corresponding label field as well. See example of a new payload below:
```
{
"event": {
"type": "escalation"
},
"user": null,
"alert_group": {
"id": "IRFN6ZD31N31B",
"integration_id": "CTWM7U4A2QG97",
"route_id": "RUE7U7Z46SKGY",
"alerts_count": 1,
"state": "firing",
"created_at": "2023-11-22T08:54:55.178243Z",
"resolved_at": null,
"acknowledged_at": null,
"title": "Incident",
"permalinks": {
"slack": null,
"telegram": null,
"web": "http://grafana:3000/a/grafana-oncall-app/alert-groups/IRFN6ZD31N31B"
},
"labels": {
"severity": "critical"
}
},
"alert_group_id": "IRFN6ZD31N31B",
"alert_payload": {
"message": "This alert was sent by user for demonstration purposes"
},
"integration": {
"id": "CTWM7U4A2QG97",
"type": "webhook",
"name": "hi - Webhook",
"team": null,
"labels": {
"hello": "world",
"severity": "critical"
}
},
"notified_users": [],
"users_to_be_notified": [],
"webhook": {
"id": "WHAXK4BTC7TAEQ",
"name": "test",
"labels": {
"hello": "kesha"
}
}
}
```
I feel that there is an opportunity to make code cleaner - remove all
label logic from serializers, views and utils to models or dedicated
LabelerService and introduce Labelable interface with something like
label_verbal, update_labels methods. However, I don't want to tie
webhook labels with a refactoring.
---------
Co-authored-by: Dominik <dominik.broj@grafana.com>
2023-11-22 19:17:41 +08:00
|
|
|
response = client.patch(
|
|
|
|
|
url,
|
|
|
|
|
data=json.dumps(data),
|
|
|
|
|
content_type="application/json",
|
|
|
|
|
**make_user_auth_headers(user, token),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
webhook.refresh_from_db()
|
|
|
|
|
|
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
|
|
|
assert webhook.labels.count() == 1
|
|
|
|
|
label = webhook.labels.first()
|
|
|
|
|
assert label.key_id == key_id
|
|
|
|
|
assert label.value_id == value_id
|
|
|
|
|
|
|
|
|
|
response = client.patch(
|
|
|
|
|
url,
|
|
|
|
|
data=json.dumps({"labels": []}),
|
|
|
|
|
content_type="application/json",
|
|
|
|
|
**make_user_auth_headers(user, token),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
webhook.refresh_from_db()
|
|
|
|
|
|
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
|
|
|
assert webhook.labels.count() == 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.django_db
|
|
|
|
|
def test_create_webhook_with_labels(
|
|
|
|
|
make_organization_and_user_with_plugin_token,
|
|
|
|
|
make_user_auth_headers,
|
|
|
|
|
):
|
|
|
|
|
organization, user, token = make_organization_and_user_with_plugin_token()
|
|
|
|
|
client = APIClient()
|
|
|
|
|
|
|
|
|
|
url = reverse("api-internal:webhooks-list")
|
|
|
|
|
|
|
|
|
|
key_id = "testkey"
|
|
|
|
|
value_id = "testvalue"
|
|
|
|
|
data = {
|
|
|
|
|
"name": "the_webhook",
|
|
|
|
|
"url": TEST_URL,
|
|
|
|
|
"trigger_type": Webhook.TRIGGER_ALERT_GROUP_CREATED,
|
|
|
|
|
"http_method": "POST",
|
2024-02-20 14:42:51 +08:00
|
|
|
"labels": [
|
|
|
|
|
{
|
|
|
|
|
"key": {"id": key_id, "name": "test", "prescribed": False},
|
|
|
|
|
"value": {"id": value_id, "name": "testv", "prescribed": False},
|
|
|
|
|
}
|
|
|
|
|
],
|
Webhook labels (#3383)
This PR add labels for webhooks.
1. Make webhook "labelable" with ability to filter by labels.
2. Add labels to the webhook payload. It contain new field webhook with
it's name, id and labels. Field integration and alert_group has a
corresponding label field as well. See example of a new payload below:
```
{
"event": {
"type": "escalation"
},
"user": null,
"alert_group": {
"id": "IRFN6ZD31N31B",
"integration_id": "CTWM7U4A2QG97",
"route_id": "RUE7U7Z46SKGY",
"alerts_count": 1,
"state": "firing",
"created_at": "2023-11-22T08:54:55.178243Z",
"resolved_at": null,
"acknowledged_at": null,
"title": "Incident",
"permalinks": {
"slack": null,
"telegram": null,
"web": "http://grafana:3000/a/grafana-oncall-app/alert-groups/IRFN6ZD31N31B"
},
"labels": {
"severity": "critical"
}
},
"alert_group_id": "IRFN6ZD31N31B",
"alert_payload": {
"message": "This alert was sent by user for demonstration purposes"
},
"integration": {
"id": "CTWM7U4A2QG97",
"type": "webhook",
"name": "hi - Webhook",
"team": null,
"labels": {
"hello": "world",
"severity": "critical"
}
},
"notified_users": [],
"users_to_be_notified": [],
"webhook": {
"id": "WHAXK4BTC7TAEQ",
"name": "test",
"labels": {
"hello": "kesha"
}
}
}
```
I feel that there is an opportunity to make code cleaner - remove all
label logic from serializers, views and utils to models or dedicated
LabelerService and introduce Labelable interface with something like
label_verbal, update_labels methods. However, I don't want to tie
webhook labels with a refactoring.
---------
Co-authored-by: Dominik <dominik.broj@grafana.com>
2023-11-22 19:17:41 +08:00
|
|
|
"team": None,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
response = client.post(
|
|
|
|
|
url,
|
|
|
|
|
data=json.dumps(data),
|
|
|
|
|
content_type="application/json",
|
|
|
|
|
**make_user_auth_headers(user, token),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 201
|
|
|
|
|
webhook = Webhook.objects.get(public_primary_key=response.json()["id"])
|
|
|
|
|
expected_response = data | {
|
|
|
|
|
"id": webhook.public_primary_key,
|
|
|
|
|
"data": None,
|
|
|
|
|
"username": None,
|
|
|
|
|
"password": None,
|
|
|
|
|
"authorization_header": None,
|
|
|
|
|
"forward_all": True,
|
|
|
|
|
"headers": None,
|
|
|
|
|
"http_method": "POST",
|
2024-02-23 08:55:44 -03:00
|
|
|
"integration_filter": [],
|
Webhook labels (#3383)
This PR add labels for webhooks.
1. Make webhook "labelable" with ability to filter by labels.
2. Add labels to the webhook payload. It contain new field webhook with
it's name, id and labels. Field integration and alert_group has a
corresponding label field as well. See example of a new payload below:
```
{
"event": {
"type": "escalation"
},
"user": null,
"alert_group": {
"id": "IRFN6ZD31N31B",
"integration_id": "CTWM7U4A2QG97",
"route_id": "RUE7U7Z46SKGY",
"alerts_count": 1,
"state": "firing",
"created_at": "2023-11-22T08:54:55.178243Z",
"resolved_at": null,
"acknowledged_at": null,
"title": "Incident",
"permalinks": {
"slack": null,
"telegram": null,
"web": "http://grafana:3000/a/grafana-oncall-app/alert-groups/IRFN6ZD31N31B"
},
"labels": {
"severity": "critical"
}
},
"alert_group_id": "IRFN6ZD31N31B",
"alert_payload": {
"message": "This alert was sent by user for demonstration purposes"
},
"integration": {
"id": "CTWM7U4A2QG97",
"type": "webhook",
"name": "hi - Webhook",
"team": null,
"labels": {
"hello": "world",
"severity": "critical"
}
},
"notified_users": [],
"users_to_be_notified": [],
"webhook": {
"id": "WHAXK4BTC7TAEQ",
"name": "test",
"labels": {
"hello": "kesha"
}
}
}
```
I feel that there is an opportunity to make code cleaner - remove all
label logic from serializers, views and utils to models or dedicated
LabelerService and introduce Labelable interface with something like
label_verbal, update_labels methods. However, I don't want to tie
webhook labels with a refactoring.
---------
Co-authored-by: Dominik <dominik.broj@grafana.com>
2023-11-22 19:17:41 +08:00
|
|
|
"is_webhook_enabled": True,
|
|
|
|
|
"is_legacy": False,
|
|
|
|
|
"last_response_log": {
|
|
|
|
|
"request_data": "",
|
|
|
|
|
"request_headers": "",
|
|
|
|
|
"timestamp": None,
|
|
|
|
|
"content": "",
|
|
|
|
|
"status_code": None,
|
|
|
|
|
"request_trigger": "",
|
|
|
|
|
"url": "",
|
|
|
|
|
"event_data": "",
|
|
|
|
|
},
|
|
|
|
|
"trigger_template": None,
|
|
|
|
|
"trigger_type": str(data["trigger_type"]),
|
|
|
|
|
"trigger_type_name": "Alert Group Created",
|
|
|
|
|
"preset": None,
|
|
|
|
|
}
|
|
|
|
|
assert response.status_code == status.HTTP_201_CREATED
|
|
|
|
|
assert response.json() == expected_response
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.django_db
|
|
|
|
|
def test_update_webhook_labels_duplicate_key(
|
|
|
|
|
webhook_internal_api_setup,
|
|
|
|
|
make_user_auth_headers,
|
|
|
|
|
):
|
|
|
|
|
user, token, webhook = webhook_internal_api_setup
|
|
|
|
|
client = APIClient()
|
|
|
|
|
|
|
|
|
|
url = reverse("api-internal:webhooks-detail", kwargs={"pk": webhook.public_primary_key})
|
|
|
|
|
key_id = "testkey"
|
|
|
|
|
data = {
|
|
|
|
|
"labels": [
|
|
|
|
|
{"key": {"id": key_id, "name": "test"}, "value": {"id": "testvalue1", "name": "testv1"}},
|
|
|
|
|
{"key": {"id": key_id, "name": "test"}, "value": {"id": "testvalue2", "name": "testv2"}},
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
response = client.patch(
|
|
|
|
|
url,
|
|
|
|
|
data=json.dumps(data),
|
|
|
|
|
content_type="application/json",
|
|
|
|
|
**make_user_auth_headers(user, token),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
webhook.refresh_from_db()
|
|
|
|
|
|
|
|
|
|
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
|
|
|
|
assert webhook.labels.count() == 0
|
2023-12-07 15:44:52 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.django_db
|
|
|
|
|
def test_team_not_updated_if_not_in_data(
|
|
|
|
|
make_organization_and_user_with_plugin_token,
|
|
|
|
|
make_team,
|
|
|
|
|
make_custom_webhook,
|
|
|
|
|
make_user_auth_headers,
|
|
|
|
|
):
|
|
|
|
|
organization, user, token = make_organization_and_user_with_plugin_token()
|
|
|
|
|
team = make_team(organization)
|
|
|
|
|
webhook = make_custom_webhook(
|
|
|
|
|
name="some_webhook",
|
|
|
|
|
url="https://github.com/",
|
|
|
|
|
organization=organization,
|
|
|
|
|
forward_all=False,
|
|
|
|
|
team=team,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert webhook.team == team
|
|
|
|
|
|
|
|
|
|
client = APIClient()
|
|
|
|
|
url = reverse("api-internal:webhooks-detail", kwargs={"pk": webhook.public_primary_key})
|
|
|
|
|
data = {"name": "renamed"}
|
|
|
|
|
response = client.put(url, data=data, format="json", **make_user_auth_headers(user, token))
|
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
|
|
|
assert response.json()["team"] == webhook.team.public_primary_key
|
|
|
|
|
|
|
|
|
|
webhook.refresh_from_db()
|
|
|
|
|
assert webhook.team == team
|
2024-09-09 09:17:23 -03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.django_db
|
|
|
|
|
def test_webhook_trigger_manual(
|
|
|
|
|
make_organization_and_user_with_plugin_token,
|
|
|
|
|
make_organization,
|
|
|
|
|
make_alert_receive_channel,
|
|
|
|
|
make_alert_group,
|
|
|
|
|
make_custom_webhook,
|
|
|
|
|
make_user_auth_headers,
|
|
|
|
|
):
|
|
|
|
|
organization, user, token = make_organization_and_user_with_plugin_token()
|
|
|
|
|
integration = make_alert_receive_channel(organization)
|
|
|
|
|
alert_group = make_alert_group(integration)
|
|
|
|
|
webhook_on_ack = make_custom_webhook(organization, trigger_type=Webhook.TRIGGER_ACKNOWLEDGE)
|
|
|
|
|
webhook_manual = make_custom_webhook(organization, trigger_type=Webhook.TRIGGER_MANUAL)
|
|
|
|
|
|
|
|
|
|
client = APIClient()
|
|
|
|
|
url = reverse("api-internal:webhooks-trigger-manual", kwargs={"pk": webhook_manual.public_primary_key})
|
|
|
|
|
data = {"alert_group": alert_group.public_primary_key}
|
|
|
|
|
|
|
|
|
|
# success
|
|
|
|
|
with patch("apps.api.views.webhooks.execute_webhook") as mock_execute:
|
|
|
|
|
response = client.post(
|
|
|
|
|
url,
|
|
|
|
|
data=json.dumps(data),
|
|
|
|
|
content_type="application/json",
|
|
|
|
|
**make_user_auth_headers(user, token),
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
|
|
|
mock_execute.apply_async.assert_called_once_with(
|
|
|
|
|
(webhook_manual.pk, alert_group.pk, user.pk, None), kwargs={"trigger_type": Webhook.TRIGGER_MANUAL}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# filtering integration
|
|
|
|
|
webhook_manual.filtered_integrations.add(integration)
|
|
|
|
|
with patch("apps.api.views.webhooks.execute_webhook") as mock_execute:
|
|
|
|
|
response = client.post(
|
|
|
|
|
url,
|
|
|
|
|
data=json.dumps(data),
|
|
|
|
|
content_type="application/json",
|
|
|
|
|
**make_user_auth_headers(user, token),
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
|
|
|
mock_execute.apply_async.assert_called_once_with(
|
|
|
|
|
(webhook_manual.pk, alert_group.pk, user.pk, None), kwargs={"trigger_type": Webhook.TRIGGER_MANUAL}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# exclude integration
|
|
|
|
|
another_integration = make_alert_receive_channel(organization)
|
|
|
|
|
webhook_manual.filtered_integrations.set([another_integration])
|
|
|
|
|
with patch("apps.api.views.webhooks.execute_webhook") as mock_execute:
|
|
|
|
|
response = client.post(
|
|
|
|
|
url,
|
|
|
|
|
data=json.dumps(data),
|
|
|
|
|
content_type="application/json",
|
|
|
|
|
**make_user_auth_headers(user, token),
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == status.HTTP_404_NOT_FOUND
|
|
|
|
|
assert mock_execute.apply_async.call_count == 0
|
|
|
|
|
|
|
|
|
|
# invalid trigger type
|
|
|
|
|
url = reverse("api-internal:webhooks-trigger-manual", kwargs={"pk": webhook_on_ack.public_primary_key})
|
|
|
|
|
data = {"alert_group": alert_group.public_primary_key}
|
|
|
|
|
|
|
|
|
|
with patch("apps.api.views.webhooks.execute_webhook") as mock_execute:
|
|
|
|
|
response = client.post(
|
|
|
|
|
url,
|
|
|
|
|
data=json.dumps(data),
|
|
|
|
|
content_type="application/json",
|
|
|
|
|
**make_user_auth_headers(user, token),
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
|
|
|
|
assert mock_execute.apply_async.call_count == 0
|
|
|
|
|
|
|
|
|
|
# alert group from different org
|
|
|
|
|
another_org = make_organization()
|
|
|
|
|
another_org_integration = make_alert_receive_channel(another_org)
|
|
|
|
|
another_org_alert_group = make_alert_group(another_org_integration)
|
|
|
|
|
url = reverse("api-internal:webhooks-trigger-manual", kwargs={"pk": webhook_manual.public_primary_key})
|
|
|
|
|
data = {"alert_group": another_org_alert_group.public_primary_key}
|
|
|
|
|
with patch("apps.api.views.webhooks.execute_webhook") as mock_execute:
|
|
|
|
|
response = client.post(
|
|
|
|
|
url,
|
|
|
|
|
data=json.dumps(data),
|
|
|
|
|
content_type="application/json",
|
|
|
|
|
**make_user_auth_headers(user, token),
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == status.HTTP_404_NOT_FOUND
|
|
|
|
|
assert mock_execute.apply_async.call_count == 0
|