forked from GithubBackups/healthchecks
parent
9a0888aacd
commit
d45dc2f6a3
@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
- Update the email notification template to include more check and last ping details
|
- Update the email notification template to include more check and last ping details
|
||||||
- Improve the crontab snippet in the "Check Details" page (#465)
|
- Improve the crontab snippet in the "Check Details" page (#465)
|
||||||
- Add Signal integration (#428)
|
- Add Signal integration (#428)
|
||||||
|
- Change Zulip onboarding, ask for the zuliprc file (#202)
|
||||||
|
|
||||||
## Bug Fixes
|
## Bug Fixes
|
||||||
- Fix unwanted HTML escaping in SMS and WhatsApp notifications
|
- Fix unwanted HTML escaping in SMS and WhatsApp notifications
|
||||||
|
@ -754,6 +754,18 @@ class Channel(models.Model):
|
|||||||
doc = json.loads(self.value)
|
doc = json.loads(self.value)
|
||||||
return doc["bot_email"]
|
return doc["bot_email"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def zulip_site(self):
|
||||||
|
assert self.kind == "zulip"
|
||||||
|
doc = json.loads(self.value)
|
||||||
|
if "site" in doc:
|
||||||
|
return doc["site"]
|
||||||
|
|
||||||
|
# Fallback if we don't have the site value:
|
||||||
|
# derive it from bot's email
|
||||||
|
_, domain = doc["bot_email"].split("@")
|
||||||
|
return "https://" + domain
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def zulip_api_key(self):
|
def zulip_api_key(self):
|
||||||
assert self.kind == "zulip"
|
assert self.kind == "zulip"
|
||||||
|
@ -783,38 +783,3 @@ class NotifyTestCase(BaseTestCase):
|
|||||||
|
|
||||||
n = Notification.objects.get()
|
n = Notification.objects.get()
|
||||||
self.assertEqual(n.error, "Shell commands are not enabled")
|
self.assertEqual(n.error, "Shell commands are not enabled")
|
||||||
|
|
||||||
@patch("hc.api.transports.requests.request")
|
|
||||||
def test_zulip(self, mock_post):
|
|
||||||
definition = {
|
|
||||||
"bot_email": "bot@example.org",
|
|
||||||
"api_key": "fake-key",
|
|
||||||
"mtype": "stream",
|
|
||||||
"to": "general",
|
|
||||||
}
|
|
||||||
self._setup_data("zulip", json.dumps(definition))
|
|
||||||
mock_post.return_value.status_code = 200
|
|
||||||
|
|
||||||
self.channel.notify(self.check)
|
|
||||||
assert Notification.objects.count() == 1
|
|
||||||
|
|
||||||
args, kwargs = mock_post.call_args
|
|
||||||
payload = kwargs["data"]
|
|
||||||
self.assertIn("DOWN", payload["topic"])
|
|
||||||
|
|
||||||
@patch("hc.api.transports.requests.request")
|
|
||||||
def test_zulip_returns_error(self, mock_post):
|
|
||||||
definition = {
|
|
||||||
"bot_email": "bot@example.org",
|
|
||||||
"api_key": "fake-key",
|
|
||||||
"mtype": "stream",
|
|
||||||
"to": "general",
|
|
||||||
}
|
|
||||||
self._setup_data("zulip", json.dumps(definition))
|
|
||||||
mock_post.return_value.status_code = 403
|
|
||||||
mock_post.return_value.json.return_value = {"msg": "Nice try"}
|
|
||||||
|
|
||||||
self.channel.notify(self.check)
|
|
||||||
|
|
||||||
n = Notification.objects.first()
|
|
||||||
self.assertEqual(n.error, 'Received status code 403 with a message: "Nice try"')
|
|
||||||
|
86
hc/api/tests/test_notify_zulip.py
Normal file
86
hc/api/tests/test_notify_zulip.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
|
||||||
|
from datetime import timedelta as td
|
||||||
|
import json
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from hc.api.models import Channel, Check, Notification
|
||||||
|
from hc.test import BaseTestCase
|
||||||
|
|
||||||
|
|
||||||
|
class NotifyTestCase(BaseTestCase):
|
||||||
|
def _setup_data(self, kind, value, status="down", email_verified=True):
|
||||||
|
self.check = Check(project=self.project)
|
||||||
|
self.check.status = status
|
||||||
|
self.check.last_ping = now() - td(minutes=61)
|
||||||
|
self.check.save()
|
||||||
|
|
||||||
|
self.channel = Channel(project=self.project)
|
||||||
|
self.channel.kind = kind
|
||||||
|
self.channel.value = value
|
||||||
|
self.channel.email_verified = email_verified
|
||||||
|
self.channel.save()
|
||||||
|
self.channel.checks.add(self.check)
|
||||||
|
|
||||||
|
@patch("hc.api.transports.requests.request")
|
||||||
|
def test_zulip(self, mock_post):
|
||||||
|
definition = {
|
||||||
|
"bot_email": "bot@example.org",
|
||||||
|
"api_key": "fake-key",
|
||||||
|
"mtype": "stream",
|
||||||
|
"to": "general",
|
||||||
|
}
|
||||||
|
self._setup_data("zulip", json.dumps(definition))
|
||||||
|
mock_post.return_value.status_code = 200
|
||||||
|
|
||||||
|
self.channel.notify(self.check)
|
||||||
|
assert Notification.objects.count() == 1
|
||||||
|
|
||||||
|
args, kwargs = mock_post.call_args
|
||||||
|
|
||||||
|
method, url = args
|
||||||
|
self.assertEqual(url, "https://example.org/api/v1/messages")
|
||||||
|
|
||||||
|
payload = kwargs["data"]
|
||||||
|
self.assertIn("DOWN", payload["topic"])
|
||||||
|
|
||||||
|
@patch("hc.api.transports.requests.request")
|
||||||
|
def test_zulip_returns_error(self, mock_post):
|
||||||
|
definition = {
|
||||||
|
"bot_email": "bot@example.org",
|
||||||
|
"api_key": "fake-key",
|
||||||
|
"mtype": "stream",
|
||||||
|
"to": "general",
|
||||||
|
}
|
||||||
|
self._setup_data("zulip", json.dumps(definition))
|
||||||
|
mock_post.return_value.status_code = 403
|
||||||
|
mock_post.return_value.json.return_value = {"msg": "Nice try"}
|
||||||
|
|
||||||
|
self.channel.notify(self.check)
|
||||||
|
|
||||||
|
n = Notification.objects.first()
|
||||||
|
self.assertEqual(n.error, 'Received status code 403 with a message: "Nice try"')
|
||||||
|
|
||||||
|
@patch("hc.api.transports.requests.request")
|
||||||
|
def test_zulip_uses_site_parameter(self, mock_post):
|
||||||
|
definition = {
|
||||||
|
"bot_email": "bot@example.org",
|
||||||
|
"site": "https://custom.example.org",
|
||||||
|
"api_key": "fake-key",
|
||||||
|
"mtype": "stream",
|
||||||
|
"to": "general",
|
||||||
|
}
|
||||||
|
self._setup_data("zulip", json.dumps(definition))
|
||||||
|
mock_post.return_value.status_code = 200
|
||||||
|
|
||||||
|
self.channel.notify(self.check)
|
||||||
|
assert Notification.objects.count() == 1
|
||||||
|
|
||||||
|
args, kwargs = mock_post.call_args
|
||||||
|
|
||||||
|
method, url = args
|
||||||
|
self.assertEqual(url, "https://custom.example.org/api/v1/messages")
|
||||||
|
|
||||||
|
payload = kwargs["data"]
|
||||||
|
self.assertIn("DOWN", payload["topic"])
|
@ -629,8 +629,7 @@ class Zulip(HttpTransport):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def notify(self, check):
|
def notify(self, check):
|
||||||
_, domain = self.channel.zulip_bot_email.split("@")
|
url = self.channel.zulip_site + "/api/v1/messages"
|
||||||
url = "https://%s/api/v1/messages" % domain
|
|
||||||
auth = (self.channel.zulip_bot_email, self.channel.zulip_api_key)
|
auth = (self.channel.zulip_bot_email, self.channel.zulip_api_key)
|
||||||
data = {
|
data = {
|
||||||
"type": self.channel.zulip_type,
|
"type": self.channel.zulip_type,
|
||||||
|
@ -274,6 +274,7 @@ class AddZulipForm(forms.Form):
|
|||||||
error_css_class = "has-error"
|
error_css_class = "has-error"
|
||||||
bot_email = forms.EmailField(max_length=100)
|
bot_email = forms.EmailField(max_length=100)
|
||||||
api_key = forms.CharField(max_length=50)
|
api_key = forms.CharField(max_length=50)
|
||||||
|
site = forms.URLField(max_length=100, validators=[WebhookValidator()])
|
||||||
mtype = forms.ChoiceField(choices=ZULIP_TARGETS)
|
mtype = forms.ChoiceField(choices=ZULIP_TARGETS)
|
||||||
to = forms.CharField(max_length=100)
|
to = forms.CharField(max_length=100)
|
||||||
|
|
||||||
|
@ -2,6 +2,19 @@ from hc.api.models import Channel
|
|||||||
from hc.test import BaseTestCase
|
from hc.test import BaseTestCase
|
||||||
|
|
||||||
|
|
||||||
|
def _get_payload(**kwargs):
|
||||||
|
payload = {
|
||||||
|
"bot_email": "foo@example.org",
|
||||||
|
"api_key": "fake-key",
|
||||||
|
"site": "https://example.org",
|
||||||
|
"mtype": "stream",
|
||||||
|
"to": "general",
|
||||||
|
}
|
||||||
|
|
||||||
|
payload.update(kwargs)
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
class AddZulipTestCase(BaseTestCase):
|
class AddZulipTestCase(BaseTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
@ -13,15 +26,8 @@ class AddZulipTestCase(BaseTestCase):
|
|||||||
self.assertContains(r, "open-source group chat app")
|
self.assertContains(r, "open-source group chat app")
|
||||||
|
|
||||||
def test_it_works(self):
|
def test_it_works(self):
|
||||||
form = {
|
|
||||||
"bot_email": "foo@example.org",
|
|
||||||
"api_key": "fake-key",
|
|
||||||
"mtype": "stream",
|
|
||||||
"to": "general",
|
|
||||||
}
|
|
||||||
|
|
||||||
self.client.login(username="alice@example.org", password="password")
|
self.client.login(username="alice@example.org", password="password")
|
||||||
r = self.client.post(self.url, form)
|
r = self.client.post(self.url, _get_payload())
|
||||||
self.assertRedirects(r, self.channels_url)
|
self.assertRedirects(r, self.channels_url)
|
||||||
|
|
||||||
c = Channel.objects.get()
|
c = Channel.objects.get()
|
||||||
@ -32,51 +38,39 @@ class AddZulipTestCase(BaseTestCase):
|
|||||||
self.assertEqual(c.zulip_to, "general")
|
self.assertEqual(c.zulip_to, "general")
|
||||||
|
|
||||||
def test_it_rejects_bad_email(self):
|
def test_it_rejects_bad_email(self):
|
||||||
form = {
|
payload = _get_payload(bot_email="not@an@email")
|
||||||
"bot_email": "not@an@email",
|
|
||||||
"api_key": "fake-key",
|
|
||||||
"mtype": "stream",
|
|
||||||
"to": "general",
|
|
||||||
}
|
|
||||||
|
|
||||||
self.client.login(username="alice@example.org", password="password")
|
self.client.login(username="alice@example.org", password="password")
|
||||||
r = self.client.post(self.url, form)
|
r = self.client.post(self.url, payload)
|
||||||
self.assertContains(r, "Enter a valid email address.")
|
self.assertContains(r, "Invalid file format.")
|
||||||
|
|
||||||
def test_it_rejects_missing_api_key(self):
|
def test_it_rejects_missing_api_key(self):
|
||||||
form = {
|
payload = _get_payload(api_key="")
|
||||||
"bot_email": "foo@example.org",
|
|
||||||
"api_key": "",
|
|
||||||
"mtype": "stream",
|
|
||||||
"to": "general",
|
|
||||||
}
|
|
||||||
|
|
||||||
self.client.login(username="alice@example.org", password="password")
|
self.client.login(username="alice@example.org", password="password")
|
||||||
r = self.client.post(self.url, form)
|
r = self.client.post(self.url, payload)
|
||||||
self.assertContains(r, "This field is required.")
|
self.assertContains(r, "Invalid file format.")
|
||||||
|
|
||||||
|
def test_it_rejects_missing_site(self):
|
||||||
|
payload = _get_payload(site="")
|
||||||
|
self.client.login(username="alice@example.org", password="password")
|
||||||
|
r = self.client.post(self.url, payload)
|
||||||
|
self.assertContains(r, "Invalid file format.")
|
||||||
|
|
||||||
|
def test_it_rejects_malformed_site(self):
|
||||||
|
payload = _get_payload(site="not-an-url")
|
||||||
|
self.client.login(username="alice@example.org", password="password")
|
||||||
|
r = self.client.post(self.url, payload)
|
||||||
|
self.assertContains(r, "Invalid file format.")
|
||||||
|
|
||||||
def test_it_rejects_bad_mtype(self):
|
def test_it_rejects_bad_mtype(self):
|
||||||
form = {
|
payload = _get_payload(mtype="this-should-not-work")
|
||||||
"bot_email": "foo@example.org",
|
|
||||||
"api_key": "fake-key",
|
|
||||||
"mtype": "this-should-not-work",
|
|
||||||
"to": "general",
|
|
||||||
}
|
|
||||||
|
|
||||||
self.client.login(username="alice@example.org", password="password")
|
self.client.login(username="alice@example.org", password="password")
|
||||||
r = self.client.post(self.url, form)
|
r = self.client.post(self.url, payload)
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
|
|
||||||
def test_it_rejects_missing_stream_name(self):
|
def test_it_rejects_missing_stream_name(self):
|
||||||
form = {
|
payload = _get_payload(to="")
|
||||||
"bot_email": "foo@example.org",
|
|
||||||
"api_key": "fake-key",
|
|
||||||
"mtype": "stream",
|
|
||||||
"to": "",
|
|
||||||
}
|
|
||||||
|
|
||||||
self.client.login(username="alice@example.org", password="password")
|
self.client.login(username="alice@example.org", password="password")
|
||||||
r = self.client.post(self.url, form)
|
r = self.client.post(self.url, payload)
|
||||||
self.assertContains(r, "This field is required.")
|
self.assertContains(r, "This field is required.")
|
||||||
|
|
||||||
def test_it_requires_rw_access(self):
|
def test_it_requires_rw_access(self):
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 34 KiB |
@ -14,4 +14,25 @@ $(function() {
|
|||||||
// Update form labels when user clicks on radio buttons
|
// Update form labels when user clicks on radio buttons
|
||||||
$('input[type=radio][name=mtype]').change(updateForm);
|
$('input[type=radio][name=mtype]').change(updateForm);
|
||||||
|
|
||||||
|
$("#zuliprc").change(function() {
|
||||||
|
this.files[0].text().then(function(contents) {
|
||||||
|
var keyMatch = contents.match(/key=(.*)/);
|
||||||
|
var emailMatch = contents.match(/email=(.*@.*)/);
|
||||||
|
var siteMatch = contents.match(/site=(.*)/);
|
||||||
|
|
||||||
|
if (!keyMatch || !emailMatch || !siteMatch) {
|
||||||
|
$("#zuliprc-help").text("Invalid file format.");
|
||||||
|
$("#save-integration").prop("disabled", true);
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#zulip-api-key").val(keyMatch[1]);
|
||||||
|
$("#zulip-bot-email").val(emailMatch[1]);
|
||||||
|
$("#zulip-site").val(siteMatch[1]);
|
||||||
|
$("#zuliprc-help").text("");
|
||||||
|
|
||||||
|
$("#save-integration").prop("disabled", false);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -67,7 +67,8 @@
|
|||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<span class="step-no"></span>
|
<span class="step-no"></span>
|
||||||
<p>
|
<p>
|
||||||
Copy the displayed bot's credentials into the form below.
|
Download the bot's <code>zuliprc</code> file by clicking on the
|
||||||
|
<strong>cyan download icon</strong>, and upload it in the form below.
|
||||||
Also specify the stream or the private user you want {{ site_name }}
|
Also specify the stream or the private user you want {{ site_name }}
|
||||||
to post notifications to.
|
to post notifications to.
|
||||||
</p>
|
</p>
|
||||||
@ -87,44 +88,23 @@
|
|||||||
|
|
||||||
<form method="post" class="form-horizontal">
|
<form method="post" class="form-horizontal">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="api_key" id="zulip-api-key">
|
||||||
|
<input type="hidden" name="bot_email" id="zulip-bot-email">
|
||||||
|
<input type="hidden" name="site" id="zulip-site">
|
||||||
|
|
||||||
<div class="form-group {{ form.bot_email.css_classes }}">
|
<div class="form-group">
|
||||||
<label for="bot-email" class="col-sm-2 control-label">Bot Email</label>
|
<label for="zuliprc" class="col-sm-2 control-label">The zuliprc File</label>
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4">
|
||||||
<input
|
<input id="zuliprc" type="file">
|
||||||
id="bot-email"
|
<div id="zuliprc-help" class="help-block">
|
||||||
type="text"
|
{% if form.api_key.errors or form.bot_email.errors or form.site.errors %}
|
||||||
class="form-control"
|
Invalid file format.
|
||||||
name="bot_email"
|
|
||||||
value="{{ form.bot_email.value|default:"" }}">
|
|
||||||
|
|
||||||
<div class="help-block">
|
|
||||||
{% if form.bot_email.errors %}
|
|
||||||
{{ form.bot_email.errors|join:"" }}
|
|
||||||
{% else %}
|
|
||||||
Example: healthchecks-bot@zulip.mycompany.com
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group {{ form.api_key.css_classes }}">
|
|
||||||
<label for="api-key" class="col-sm-2 control-label">API Key</label>
|
|
||||||
<div class="col-sm-4">
|
|
||||||
<input
|
|
||||||
id="api-key"
|
|
||||||
type="text"
|
|
||||||
class="form-control"
|
|
||||||
name="api_key"
|
|
||||||
value="{{ form.api_key.value|default:"" }}">
|
|
||||||
|
|
||||||
{% if form.api_key.errors %}
|
|
||||||
<div class="help-block">
|
|
||||||
{{ form.api_key.errors|join:"" }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="z-mtype-group" class="form-group {{ form.mtype.css_classes }}">
|
<div id="z-mtype-group" class="form-group {{ form.mtype.css_classes }}">
|
||||||
<label class="col-sm-2 control-label">Post To</label>
|
<label class="col-sm-2 control-label">Post To</label>
|
||||||
@ -178,7 +158,11 @@
|
|||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-offset-2 col-sm-10">
|
<div class="col-sm-offset-2 col-sm-10">
|
||||||
<button type="submit" class="btn btn-primary">Save Integration</button>
|
<button
|
||||||
|
id="save-integration"
|
||||||
|
type="submit"
|
||||||
|
disabled
|
||||||
|
class="btn btn-primary">Save Integration</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user