forked from GithubBackups/healthchecks
"Edit" function for webhook integrations (#176)
This commit is contained in:
parent
f12a649c72
commit
609f78c5ed
@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.
|
||||
### Improvements
|
||||
- Rate limiting for Telegram notifications (10 notifications per chat per minute)
|
||||
- Use Slack V2 OAuth flow
|
||||
- "Edit" function for webhook integrations (#176)
|
||||
|
||||
### Bug Fixes
|
||||
- "Get a single check" API call now supports read-only API keys (#346)
|
||||
|
@ -136,8 +136,9 @@ class AddUrlForm(forms.Form):
|
||||
METHODS = ("GET", "POST", "PUT")
|
||||
|
||||
|
||||
class AddWebhookForm(forms.Form):
|
||||
class WebhookForm(forms.Form):
|
||||
error_css_class = "has-error"
|
||||
name = forms.CharField(max_length=100, required=False)
|
||||
|
||||
method_down = forms.ChoiceField(initial="GET", choices=zip(METHODS, METHODS))
|
||||
body_down = forms.CharField(max_length=1000, required=False)
|
||||
@ -160,7 +161,8 @@ class AddWebhookForm(forms.Form):
|
||||
url_up = self.cleaned_data.get("url_up")
|
||||
|
||||
if not url_down and not url_up:
|
||||
self.add_error("url_down", "Enter a valid URL.")
|
||||
if not self.has_error("url_down"):
|
||||
self.add_error("url_down", "Enter a valid URL.")
|
||||
|
||||
def get_value(self):
|
||||
return json.dumps(dict(self.cleaned_data), sort_keys=True)
|
||||
|
@ -12,6 +12,22 @@ class AddWebhookTestCase(BaseTestCase):
|
||||
r = self.client.get(self.url)
|
||||
self.assertContains(r, "Executes an HTTP request")
|
||||
|
||||
def test_it_saves_name(self):
|
||||
form = {
|
||||
"name": "Call foo.com",
|
||||
"method_down": "GET",
|
||||
"url_down": "http://foo.com",
|
||||
"method_up": "GET",
|
||||
"url_up": "",
|
||||
}
|
||||
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.post(self.url, form)
|
||||
self.assertRedirects(r, self.channels_url)
|
||||
|
||||
c = Channel.objects.get()
|
||||
self.assertEqual(c.name, "Call foo.com")
|
||||
|
||||
def test_it_adds_two_webhook_urls_and_redirects(self):
|
||||
form = {
|
||||
"method_down": "GET",
|
||||
|
84
hc/front/tests/test_edit_webhook.py
Normal file
84
hc/front/tests/test_edit_webhook.py
Normal file
@ -0,0 +1,84 @@
|
||||
import json
|
||||
|
||||
from hc.api.models import Channel
|
||||
from hc.test import BaseTestCase
|
||||
|
||||
|
||||
class EditWebhookTestCase(BaseTestCase):
|
||||
def setUp(self):
|
||||
super(EditWebhookTestCase, self).setUp()
|
||||
|
||||
definition = {
|
||||
"method_down": "GET",
|
||||
"url_down": "http://example.org/down",
|
||||
"body_down": "$NAME is down",
|
||||
"headers_down": {"User-Agent": "My-Custom-UA"},
|
||||
"method_up": "GET",
|
||||
"url_up": "http://example.org/up",
|
||||
"body_up": "$NAME is up",
|
||||
"headers_up": {},
|
||||
}
|
||||
|
||||
self.channel = Channel(project=self.project, kind="webhook")
|
||||
self.channel.name = "Call example.org"
|
||||
self.channel.value = json.dumps(definition)
|
||||
self.channel.save()
|
||||
|
||||
self.url = "/integrations/%s/edit_webhook/" % self.channel.code
|
||||
|
||||
def test_it_shows_form(self):
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.get(self.url)
|
||||
self.assertContains(r, "Webhook Settings")
|
||||
|
||||
self.assertContains(r, "Call example.org")
|
||||
|
||||
# down
|
||||
self.assertContains(r, "http://example.org/down")
|
||||
self.assertContains(r, "My-Custom-UA")
|
||||
self.assertContains(r, "$NAME is down")
|
||||
|
||||
# up
|
||||
self.assertContains(r, "http://example.org/up")
|
||||
self.assertContains(r, "$NAME is up")
|
||||
|
||||
def test_it_saves_form_and_redirects(self):
|
||||
form = {
|
||||
"name": "Call foo.com / bar.com",
|
||||
"method_down": "POST",
|
||||
"url_down": "http://foo.com",
|
||||
"headers_down": "X-Foo: 1\nX-Bar: 2",
|
||||
"body_down": "going down",
|
||||
"method_up": "POST",
|
||||
"url_up": "https://bar.com",
|
||||
"headers_up": "Content-Type: text/plain",
|
||||
"body_up": "going up",
|
||||
}
|
||||
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.post(self.url, form)
|
||||
self.assertRedirects(r, self.channels_url)
|
||||
|
||||
self.channel.refresh_from_db()
|
||||
self.assertEqual(self.channel.name, "Call foo.com / bar.com")
|
||||
|
||||
down_spec = self.channel.down_webhook_spec
|
||||
self.assertEqual(down_spec["method"], "POST")
|
||||
self.assertEqual(down_spec["url"], "http://foo.com")
|
||||
self.assertEqual(down_spec["body"], "going down")
|
||||
self.assertEqual(down_spec["headers"], {"X-Foo": "1", "X-Bar": "2"})
|
||||
|
||||
up_spec = self.channel.up_webhook_spec
|
||||
self.assertEqual(up_spec["method"], "POST")
|
||||
self.assertEqual(up_spec["url"], "https://bar.com")
|
||||
self.assertEqual(up_spec["body"], "going up")
|
||||
self.assertEqual(up_spec["headers"], {"Content-Type": "text/plain"})
|
||||
|
||||
def test_it_requires_kind_webhook(self):
|
||||
self.channel.kind = "email"
|
||||
self.channel.value = "foo@example.org"
|
||||
self.channel.save()
|
||||
|
||||
self.client.login(username="alice@example.org", password="password")
|
||||
r = self.client.get(self.url)
|
||||
self.assertEqual(r.status_code, 400)
|
@ -7,10 +7,7 @@ class UpdateChannelTestCase(BaseTestCase):
|
||||
def setUp(self):
|
||||
super(UpdateChannelTestCase, self).setUp()
|
||||
self.check = Check.objects.create(project=self.project)
|
||||
|
||||
self.channel = Channel(project=self.project, kind="email")
|
||||
self.channel.email = "alice@example.org"
|
||||
self.channel.save()
|
||||
self.channel = Channel.objects.create(project=self.project, kind="email")
|
||||
|
||||
def test_it_works(self):
|
||||
payload = {"channel": self.channel.code, "check-%s" % self.check.code: True}
|
||||
|
@ -39,6 +39,7 @@ channel_urls = [
|
||||
path("add_trello/settings/", views.trello_settings, name="hc-trello-settings"),
|
||||
path("<uuid:code>/checks/", views.channel_checks, name="hc-channel-checks"),
|
||||
path("<uuid:code>/name/", views.update_channel_name, name="hc-channel-name"),
|
||||
path("<uuid:code>/edit_webhook/", views.edit_webhook, name="hc-edit-webhook"),
|
||||
path("<uuid:code>/test/", views.send_test_notification, name="hc-channel-test"),
|
||||
path("<uuid:code>/remove/", views.remove_channel, name="hc-remove-channel"),
|
||||
path(
|
||||
|
@ -843,16 +843,18 @@ def add_webhook(request, code):
|
||||
project = _get_project_for_user(request, code)
|
||||
|
||||
if request.method == "POST":
|
||||
form = forms.AddWebhookForm(request.POST)
|
||||
form = forms.WebhookForm(request.POST)
|
||||
if form.is_valid():
|
||||
channel = Channel(project=project, kind="webhook")
|
||||
channel.name = form.cleaned_data["name"]
|
||||
channel.value = form.get_value()
|
||||
channel.save()
|
||||
|
||||
channel.assign_all_checks()
|
||||
return redirect("hc-p-channels", project.code)
|
||||
|
||||
else:
|
||||
form = forms.AddWebhookForm()
|
||||
form = forms.WebhookForm()
|
||||
|
||||
ctx = {
|
||||
"page": "channels",
|
||||
@ -860,7 +862,43 @@ def add_webhook(request, code):
|
||||
"form": form,
|
||||
"now": timezone.now().replace(microsecond=0).isoformat(),
|
||||
}
|
||||
return render(request, "integrations/add_webhook.html", ctx)
|
||||
return render(request, "integrations/webhook_form.html", ctx)
|
||||
|
||||
|
||||
@login_required
|
||||
def edit_webhook(request, code):
|
||||
channel = _get_channel_for_user(request, code)
|
||||
if channel.kind != "webhook":
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
if request.method == "POST":
|
||||
form = forms.WebhookForm(request.POST)
|
||||
if form.is_valid():
|
||||
channel.name = form.cleaned_data["name"]
|
||||
channel.value = form.get_value()
|
||||
channel.save()
|
||||
|
||||
return redirect("hc-p-channels", channel.project.code)
|
||||
else:
|
||||
|
||||
def flatten(d):
|
||||
return "\n".join("%s: %s" % pair for pair in d.items())
|
||||
|
||||
doc = json.loads(channel.value)
|
||||
doc["headers_down"] = flatten(doc["headers_down"])
|
||||
doc["headers_up"] = flatten(doc["headers_up"])
|
||||
doc["name"] = channel.name
|
||||
|
||||
form = forms.WebhookForm(doc)
|
||||
|
||||
ctx = {
|
||||
"page": "channels",
|
||||
"project": channel.project,
|
||||
"channel": channel,
|
||||
"form": form,
|
||||
"now": timezone.now().replace(microsecond=0).isoformat(),
|
||||
}
|
||||
return render(request, "integrations/webhook_form.html", ctx)
|
||||
|
||||
|
||||
@require_setting("SHELL_ENABLED")
|
||||
|
@ -105,6 +105,10 @@ table.channels-table > tbody > tr > th {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.channel-row .actions {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.channel-row .actions form {
|
||||
display: inline;
|
||||
}
|
||||
|
@ -37,4 +37,8 @@
|
||||
|
||||
.label-up {
|
||||
color: #5cb85c
|
||||
}
|
||||
|
||||
#webhook-form-name {
|
||||
max-width: 400px;
|
||||
}
|
@ -22,7 +22,7 @@
|
||||
|
||||
<link rel="stylesheet" href="{% static 'css/add_project_modal.css' %}" type="text/css">
|
||||
<link rel="stylesheet" href="{% static 'css/add_pushover.css' %}" type="text/css">
|
||||
<link rel="stylesheet" href="{% static 'css/add_webhook.css' %}" type="text/css">
|
||||
<link rel="stylesheet" href="{% static 'css/webhook_form.css' %}" type="text/css">
|
||||
<link rel="stylesheet" href="{% static 'css/base.css' %}" type="text/css">
|
||||
<link rel="stylesheet" href="{% static 'css/billing.css' %}" type="text/css">
|
||||
<link rel="stylesheet" href="{% static 'css/channel_checks.css' %}" type="text/css">
|
||||
|
@ -131,6 +131,9 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="actions">
|
||||
{% if ch.kind == "webhook" %}
|
||||
<a class="btn btn-sm btn-default" href="{% url 'hc-edit-webhook' ch.code %}">Edit</a>
|
||||
{% endif %}
|
||||
<form action="{% url 'hc-channel-test' ch.code %}" method="post">
|
||||
{% csrf_token %}
|
||||
<button
|
||||
|
@ -1,7 +1,13 @@
|
||||
{% extends "base.html" %}
|
||||
{% load compress humanize static hc_extras %}
|
||||
|
||||
{% block title %}Add Webhook Integration - {% site_name %}{% endblock %}
|
||||
{% block title %}
|
||||
{% if channel %}
|
||||
Webhook Settings - {% site_name %}
|
||||
{% else %}
|
||||
Add Webhook Integration - {% site_name %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
@ -9,18 +15,30 @@
|
||||
<div class="col-sm-12">
|
||||
<h1>Webhook</h1>
|
||||
|
||||
<p>Executes an HTTP request to your specified URL when a check
|
||||
goes up or down.</p>
|
||||
|
||||
<p>
|
||||
You can use placeholders <strong>$NAME</strong>, <strong>$STATUS</strong> and others in webhook URLs,
|
||||
request body and header values
|
||||
<a href="#" data-toggle="modal" data-target="#reference-modal">(quick reference)</a>.
|
||||
Executes an HTTP request to your specified URL when a check
|
||||
goes up or down. You can use placeholders <strong>$NAME</strong>,
|
||||
<strong>$STATUS</strong> and others in webhook URLs, request body and
|
||||
header values
|
||||
<a href="#" data-toggle="modal" data-target="#reference-modal">(available placeholders)</a>.
|
||||
</p>
|
||||
|
||||
<form id="add-webhook-form" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="form-group {{ form.name.css_classes }}">
|
||||
<br>
|
||||
<label>Name</label>
|
||||
<input
|
||||
id="webhook-form-name"
|
||||
name="name"
|
||||
value="{{ form.name.value|default:"" }}"
|
||||
type="text"
|
||||
class="form-control" />
|
||||
|
||||
<div class="help-block">Give this integration a descriptive name, so you can easily recognize it later. </div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<h2>Execute when a check goes <strong class="label-down">down</strong></h2>
|
||||
@ -140,7 +158,13 @@
|
||||
<br>
|
||||
<br>
|
||||
<div class="text-right">
|
||||
<button type="submit" class="btn btn-primary">Save Integration</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{% if channel %}
|
||||
Save Changes
|
||||
{% else %}
|
||||
Save Integration
|
||||
{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
Loading…
x
Reference in New Issue
Block a user