Browse Source

Change Zulip onboarding, ask for the zuliprc file

Fixes: #202
pull/470/head
Pēteris Caune 4 years ago
parent
commit
d45dc2f6a3
No known key found for this signature in database GPG Key ID: E28D7679E9A9EDE2
10 changed files with 173 additions and 110 deletions
  1. +1
    -0
      CHANGELOG.md
  2. +12
    -0
      hc/api/models.py
  3. +0
    -35
      hc/api/tests/test_notify.py
  4. +86
    -0
      hc/api/tests/test_notify_zulip.py
  5. +1
    -2
      hc/api/transports.py
  6. +1
    -0
      hc/front/forms.py
  7. +35
    -41
      hc/front/tests/test_add_zulip.py
  8. BIN
      static/img/integrations/setup_zulip_3.png
  9. +21
    -0
      static/js/add_zulip.js
  10. +16
    -32
      templates/integrations/add_zulip.html

+ 1
- 0
CHANGELOG.md View File

@ -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


+ 12
- 0
hc/api/models.py View File

@ -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"


+ 0
- 35
hc/api/tests/test_notify.py View File

@ -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": "[email protected]",
"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": "[email protected]",
"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
- 0
hc/api/tests/test_notify_zulip.py View 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": "[email protected]",
"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": "[email protected]",
"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": "[email protected]",
"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"])

+ 1
- 2
hc/api/transports.py View File

@ -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 = "https://%s/api/v1/messages" % domain
url = self.channel.zulip_site + "/api/v1/messages"
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,


+ 1
- 0
hc/front/forms.py View File

@ -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)


+ 35
- 41
hc/front/tests/test_add_zulip.py View File

@ -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": "[email protected]",
"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": "[email protected]",
"api_key": "fake-key",
"mtype": "stream",
"to": "general",
}
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", 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 = {
"bot_email": "not@an@email",
"api_key": "fake-key",
"mtype": "stream",
"to": "general",
}
payload = _get_payload(bot_email="not@an@email")
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")
r = self.client.post(self.url, form)
self.assertContains(r, "Enter a valid email address.")
r = self.client.post(self.url, payload)
self.assertContains(r, "Invalid file format.")
def test_it_rejects_missing_api_key(self): def test_it_rejects_missing_api_key(self):
form = {
"bot_email": "[email protected]",
"api_key": "",
"mtype": "stream",
"to": "general",
}
payload = _get_payload(api_key="")
self.client.login(username="[email protected]", password="password")
r = self.client.post(self.url, payload)
self.assertContains(r, "Invalid file format.")
def test_it_rejects_missing_site(self):
payload = _get_payload(site="")
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", password="password")
r = self.client.post(self.url, form)
self.assertContains(r, "This field is required.")
r = self.client.post(self.url, payload)
self.assertContains(r, "Invalid file format.")
def test_it_rejects_bad_mtype(self):
form = {
"bot_email": "[email protected]",
"api_key": "fake-key",
"mtype": "this-should-not-work",
"to": "general",
}
def test_it_rejects_malformed_site(self):
payload = _get_payload(site="not-an-url")
self.client.login(username="[email protected]", password="password")
r = self.client.post(self.url, payload)
self.assertContains(r, "Invalid file format.")
def test_it_rejects_bad_mtype(self):
payload = _get_payload(mtype="this-should-not-work")
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", 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 = {
"bot_email": "[email protected]",
"api_key": "fake-key",
"mtype": "stream",
"to": "",
}
payload = _get_payload(to="")
self.client.login(username="[email protected]", password="password") self.client.login(username="[email protected]", 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):


BIN
static/img/integrations/setup_zulip_3.png View File

Before After
Width: 464  |  Height: 300  |  Size: 22 KiB Width: 464  |  Height: 300  |  Size: 34 KiB

+ 21
- 0
static/js/add_zulip.js View File

@ -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);
});
})
}); });

+ 16
- 32
templates/integrations/add_zulip.html View File

@ -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 }}">
<label for="bot-email" class="col-sm-2 control-label">Bot Email</label>
<div class="form-group">
<label for="zuliprc" class="col-sm-2 control-label">The zuliprc File</label>
<div class="col-sm-4"> <div class="col-sm-4">
<input
id="bot-email"
type="text"
class="form-control"
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: [email protected]
<input id="zuliprc" type="file">
<div id="zuliprc-help" class="help-block">
{% if form.api_key.errors or form.bot_email.errors or form.site.errors %}
Invalid file format.
{% 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…
Cancel
Save