oncall-engine/engine/apps/webhooks/tests/test_webhook.py
Matias Bordese 576fecf6d3
feat: add personal webhook notification backend (#5426)
Related to https://github.com/grafana/irm/issues/332

(see https://github.com/grafana/oncall/pull/5440 and
https://github.com/grafana/oncall/pull/5446 for the related UI changes)

---------

Co-authored-by: Matt Thorning <matt.thorning@grafana.com>
2025-02-18 17:53:07 +00:00

417 lines
14 KiB
Python

from unittest.mock import call, patch
import httpretty
import pytest
from django.conf import settings
from requests.auth import HTTPBasicAuth
from apps.webhooks.models import Webhook
from apps.webhooks.utils import InvalidWebhookData, InvalidWebhookHeaders, InvalidWebhookTrigger, InvalidWebhookUrl
@pytest.mark.django_db
def test_soft_delete(make_organization, make_custom_webhook):
organization = make_organization()
webhook = make_custom_webhook(organization=organization)
assert webhook.deleted_at is None
webhook.delete()
webhook.refresh_from_db()
assert webhook.deleted_at is not None
assert Webhook.objects.all().count() == 0
assert Webhook.objects_with_deleted.all().count() == 1
@pytest.mark.django_db
def test_hard_delete(make_organization, make_custom_webhook):
organization = make_organization()
webhook = make_custom_webhook(organization=organization)
assert webhook.pk is not None
webhook.hard_delete()
assert webhook.pk is None
assert Webhook.objects.all().count() == 0
assert Webhook.objects_with_deleted.all().count() == 0
@pytest.mark.django_db
def test_build_request_kwargs_none(make_organization, make_custom_webhook):
organization = make_organization()
webhook = make_custom_webhook(organization=organization)
request_kwargs = webhook.build_request_kwargs({})
assert request_kwargs == {"headers": {}, "json": {}}
@pytest.mark.django_db
def test_build_request_kwargs_http_auth(make_organization, make_custom_webhook):
organization = make_organization()
webhook = make_custom_webhook(organization=organization, username="foo", password="bar")
request_kwargs = webhook.build_request_kwargs({})
expected = HTTPBasicAuth("foo", "bar")
assert request_kwargs == {"headers": {}, "json": {}, "auth": expected}
@pytest.mark.django_db
def test_build_request_kwargs_headers(make_organization, make_custom_webhook):
organization = make_organization()
# non json
headers = "non-json"
webhook = make_custom_webhook(organization=organization, headers=headers)
with pytest.raises(InvalidWebhookHeaders):
webhook.build_request_kwargs({})
# template error
headers = "{{{foo|invalid}}}"
webhook = make_custom_webhook(organization=organization, headers=headers)
with pytest.raises(InvalidWebhookHeaders):
webhook.build_request_kwargs({})
# ok (using event data)
headers = '{"{{foo}}": "bar"}'
webhook = make_custom_webhook(organization=organization, headers=headers)
assert webhook.forward_all
request_kwargs = webhook.build_request_kwargs({"foo": "bar"})
assert request_kwargs == {"headers": {"bar": "bar"}, "json": {"foo": "bar"}}
@pytest.mark.django_db
def test_build_request_kwargs_authorization_header(make_organization, make_custom_webhook):
organization = make_organization()
webhook = make_custom_webhook(organization=organization, authorization_header="some-token")
request_kwargs = webhook.build_request_kwargs({})
assert request_kwargs == {"headers": {"Authorization": "some-token"}, "json": {}}
@pytest.mark.django_db
def test_build_request_kwargs_http_get(make_organization, make_custom_webhook):
organization = make_organization()
webhook = make_custom_webhook(organization=organization, http_method="GET")
request_kwargs = webhook.build_request_kwargs({})
assert request_kwargs == {"headers": {}}
@pytest.mark.django_db
def test_build_request_kwargs_custom_data(make_organization, make_custom_webhook):
organization = make_organization()
webhook = make_custom_webhook(organization=organization, data="{{foo}}", forward_all=False)
request_kwargs = webhook.build_request_kwargs({"foo": "bar", "something": "else"})
assert request_kwargs == {"headers": {}, "data": "bar".encode("utf-8")}
@pytest.mark.django_db
def test_build_request_kwargs_is_legacy_custom_data(make_organization, make_custom_webhook):
organization = make_organization()
webhook = make_custom_webhook(
organization=organization,
data="{{alert_payload.message}}",
forward_all=False,
is_legacy=True,
)
event_data = {"alert_group_id": "bar", "alert_payload": {"message": "the-message"}}
request_kwargs = webhook.build_request_kwargs(event_data)
assert request_kwargs == {"headers": {}, "data": "the-message".encode("utf-8")}
@pytest.mark.django_db
def test_build_request_kwargs_is_legacy_forward_all(make_organization, make_custom_webhook):
organization = make_organization()
webhook = make_custom_webhook(
organization=organization,
forward_all=True,
is_legacy=True,
)
event_data = {"alert_group_id": "bar", "alert_payload": {"message": "the-message"}}
request_kwargs = webhook.build_request_kwargs(event_data)
assert request_kwargs == {"headers": {}, "json": event_data["alert_payload"]}
@pytest.mark.django_db
def test_build_request_kwargs_custom_data_error(make_organization, make_custom_webhook):
organization = make_organization()
webhook = make_custom_webhook(organization=organization, data="{{foo|invalid}}", forward_all=False)
# raise
with pytest.raises(InvalidWebhookData):
webhook.build_request_kwargs({"foo": "bar", "something": "else"}, raise_data_errors=True)
# do not raise
request_kwargs = webhook.build_request_kwargs({"foo": "bar", "something": "else"})
assert request_kwargs == {"headers": {}, "json": {"error": "Template Error: No filter named 'invalid'."}}
@pytest.mark.django_db
def test_build_url_invalid_template(make_organization, make_custom_webhook):
organization = make_organization()
webhook = make_custom_webhook(organization=organization, url="{{foo|invalid}}")
with pytest.raises(InvalidWebhookUrl):
webhook.build_url({})
@pytest.mark.django_db
def test_build_url_invalid_url(make_organization, make_custom_webhook):
organization = make_organization()
webhook = make_custom_webhook(organization=organization, url="{{foo}}")
with pytest.raises(InvalidWebhookUrl):
webhook.build_url({"foo": "invalid-url"})
@pytest.mark.django_db
def test_build_url_private_raises(make_organization, make_custom_webhook):
if settings.DANGEROUS_WEBHOOKS_ENABLED:
pytest.skip("Dangerous webhooks are enabled")
organization = make_organization()
webhook = make_custom_webhook(organization=organization, url="{{foo}}")
with pytest.raises(InvalidWebhookUrl):
with patch("apps.webhooks.utils.socket.gethostbyname") as mock_gethostbyname:
mock_gethostbyname.return_value = "127.0.0.1"
webhook.build_url({"foo": "http://oncall.url"})
@pytest.mark.django_db
def test_build_url_ok(make_organization, make_custom_webhook):
organization = make_organization()
webhook = make_custom_webhook(organization=organization, url="{{foo}}")
with patch("apps.webhooks.utils.socket.gethostbyname") as mock_gethostbyname:
mock_gethostbyname.return_value = "8.8.8.8"
url = webhook.build_url({"foo": "http://oncall.url"})
assert url == "http://oncall.url"
@pytest.mark.django_db
def test_check_trigger_empty(make_organization, make_custom_webhook):
organization = make_organization()
webhook = make_custom_webhook(organization=organization)
ok, result = webhook.check_trigger({})
assert ok
assert result == ""
@pytest.mark.django_db
def test_check_trigger_template_error(make_organization, make_custom_webhook):
organization = make_organization()
webhook = make_custom_webhook(organization=organization, trigger_template="{{foo|invalid}}")
with pytest.raises(InvalidWebhookTrigger):
webhook.check_trigger({"foo": "bar"})
@pytest.mark.django_db
def test_check_trigger_template_ok(make_organization, make_custom_webhook):
organization = make_organization()
webhook = make_custom_webhook(organization=organization, trigger_template="{{ foo }}")
ok, result = webhook.check_trigger({"foo": "true"})
assert ok
assert result == "true"
ok, result = webhook.check_trigger({"foo": "bar"})
assert not ok
assert result == "bar"
@pytest.mark.django_db
def test_make_request(make_organization, make_custom_webhook):
organization = make_organization()
with patch("apps.webhooks.models.webhook.WebhookSession.request") as mock_request:
for method in ("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"):
webhook = make_custom_webhook(organization=organization, http_method=method)
webhook.make_request("url", {"foo": "bar"})
assert mock_request.call_args == call(method, "url", timeout=settings.OUTGOING_WEBHOOK_TIMEOUT, foo="bar")
# invalid
webhook = make_custom_webhook(organization=organization, http_method="NOT")
with pytest.raises(ValueError):
webhook.make_request("url", {"foo": "bar"})
@httpretty.activate(verbose=True, allow_net_connect=False)
@pytest.mark.django_db
def test_make_request_bad_redirect(make_organization, make_custom_webhook):
if settings.DANGEROUS_WEBHOOKS_ENABLED:
pytest.skip("Dangerous webhooks are enabled")
organization = make_organization()
webhook = make_custom_webhook(organization=organization, http_method="POST")
url = "http://example.com"
response = httpretty.Response(body="Redirect", status=302, location="127.0.0.1")
httpretty.register_uri(httpretty.POST, url, responses=[response])
with pytest.raises(InvalidWebhookUrl):
webhook.make_request(url, {})
@pytest.mark.django_db
def test_escaping_payload_with_double_quotes(make_organization, make_custom_webhook):
organization = make_organization()
webhook = make_custom_webhook(
organization=organization,
data='{\n "text" : "{{ alert_payload.text }}"\n}',
forward_all=False,
)
payload = {
"alert_payload": {
"text": '"Hello world"',
}
}
request_kwargs = webhook.build_request_kwargs(payload)
assert request_kwargs == {"headers": {}, "json": {"text": '"Hello world"'}}
@pytest.mark.django_db
def test_escaping_payload_with_single_quote_in_string(make_organization, make_custom_webhook):
organization = make_organization()
webhook = make_custom_webhook(
organization=organization,
data='{"data" : "{{ alert_payload }}"}',
forward_all=False,
)
payload = {
"alert_payload": {
"text": "Hi, it's alert",
}
}
request_kwargs = webhook.build_request_kwargs(payload)
assert request_kwargs == {"headers": {}, "json": {"data": "{'text': \"Hi, it's alert\"}"}}
@pytest.mark.parametrize(
"data,expected_kwargs",
[
(
'{"data" : "{{ alert_payload.text }}"}',
{"json": {"data": "東京"}},
),
(
"😊",
{"data": "😊".encode("utf-8")},
),
],
)
@pytest.mark.django_db
def test_escaping_unicode_in_string(make_organization, make_custom_webhook, data, expected_kwargs):
organization = make_organization()
webhook = make_custom_webhook(
organization=organization,
data=data,
forward_all=False,
)
payload = {
"alert_payload": {
"text": "東京",
}
}
request_kwargs = webhook.build_request_kwargs(payload)
assert request_kwargs == {"headers": {}, **expected_kwargs}
@pytest.mark.django_db
def test_webhook_not_deleted_with_team(make_organization, make_team, make_custom_webhook):
organization = make_organization()
team = make_team(organization=organization)
webhook = make_custom_webhook(
organization=organization,
team=team,
)
assert webhook.team == team
webhook_pk = webhook.pk
team.delete()
webhook = Webhook.objects.get(pk=webhook_pk)
assert webhook.team is None
@pytest.mark.django_db
def test_delete_alert_receive_channel_webhooks_deleted(
make_organization, make_alert_receive_channel, make_custom_webhook
):
organization = make_organization()
channel = make_alert_receive_channel(organization=organization, additional_settings={})
webhook_from_connected_integration = make_custom_webhook(
organization=organization,
is_from_connected_integration=True,
)
other_webhook = make_custom_webhook(organization=organization)
webhook_from_connected_integration.filtered_integrations.add(channel)
other_webhook.filtered_integrations.add(channel)
channel.delete()
webhook_from_connected_integration.refresh_from_db()
assert webhook_from_connected_integration.deleted_at is not None
other_webhook.refresh_from_db()
assert other_webhook.deleted_at is None
@pytest.mark.django_db
def test_get_source_alert_receive_channel(make_organization, make_alert_receive_channel, make_custom_webhook):
organization = make_organization()
channel1 = make_alert_receive_channel(organization=organization, additional_settings={})
channel2 = make_alert_receive_channel(organization=organization, additional_settings={})
w1 = make_custom_webhook(
organization=organization,
is_from_connected_integration=True,
)
# source integration is the first added channel
w1.filtered_integrations.add(channel2)
w1.filtered_integrations.add(channel1)
w2 = make_custom_webhook(
organization=organization,
is_from_connected_integration=True,
)
# source integration is the first added channel
w2.filtered_integrations.add(channel1)
w2.filtered_integrations.add(channel2)
assert w1.get_source_alert_receive_channel() == channel2
assert w2.get_source_alert_receive_channel() == channel1
@pytest.mark.django_db
def test_personal_notification_webhook(
make_organization, make_user_for_organization, make_custom_webhook, make_personal_notification_webhook
):
organization = make_organization()
user = make_user_for_organization(organization=organization)
webhook = make_custom_webhook(organization=organization, trigger_type=Webhook.TRIGGER_PERSONAL_NOTIFICATION)
personal_webhook = make_personal_notification_webhook(user=user, webhook=webhook)
assert personal_webhook.user == user
assert personal_webhook.webhook == webhook
# default context data
assert personal_webhook.context_data == {}
# set context data
personal_webhook.context_data = {"foo": "bar"}
personal_webhook.refresh_from_db()
assert personal_webhook.context_data == {"foo": "bar"}
# set empty
personal_webhook.context_data = None
assert personal_webhook.context_data == {}