0
0
Fork 0
mirror of https://github.com/healthchecks/healthchecks.git synced 2025-04-03 12:25:31 +00:00

Add support for custom topics in Zulip notifications

Fixes: 
This commit is contained in:
Pēteris Caune 2022-10-09 11:23:14 +03:00
parent 4a5123d67b
commit 4d69ff937e
No known key found for this signature in database
GPG key ID: E28D7679E9A9EDE2
10 changed files with 80 additions and 20 deletions

View file

@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file.
- Upgrade to cronsim 2.3
- Add support for the $BODY placeholder in webhook payloads (#708)
- Implement the "Clear Events" function
- Add support for custom topics in Zulip notifications (#583)
### Bug Fixes
- Fix the handling of TooManyRedirects exceptions

View file

@ -952,6 +952,11 @@ class Channel(models.Model):
assert self.kind == "zulip"
return self.json["to"]
@property
def zulip_topic(self):
assert self.kind == "zulip"
return self.json.get("topic", "")
@property
def linenotify_token(self):
assert self.kind == "linenotify"

View file

@ -20,18 +20,21 @@ class NotifyZulipTestCase(BaseTestCase):
self.check.last_ping = now() - td(minutes=61)
self.check.save()
definition = {
self.channel = Channel(project=self.project)
self.channel.kind = "zulip"
self.channel.value = json.dumps(self.definition())
self.channel.save()
self.channel.checks.add(self.check)
def definition(self, **kwargs):
d = {
"bot_email": "bot@example.org",
"api_key": "fake-key",
"mtype": "stream",
"to": "general",
}
self.channel = Channel(project=self.project)
self.channel.kind = "zulip"
self.channel.value = json.dumps(definition)
self.channel.save()
self.channel.checks.add(self.check)
d.update(kwargs)
return d
@patch("hc.api.transports.curl.request")
def test_it_works(self, mock_post):
@ -51,6 +54,18 @@ class NotifyZulipTestCase(BaseTestCase):
serialized = json.dumps(payload)
self.assertNotIn(str(self.check.code), serialized)
@patch("hc.api.transports.curl.request")
def test_it_uses_custom_topic(self, mock_post):
self.channel.value = json.dumps(self.definition(topic="foo"))
self.channel.save()
mock_post.return_value.status_code = 200
self.channel.notify(self.check)
args, kwargs = mock_post.call_args
payload = kwargs["data"]
self.assertEqual(payload["topic"], "foo")
@patch("hc.api.transports.curl.request")
def test_it_returns_error(self, mock_post):
mock_post.return_value.status_code = 403

View file

@ -262,7 +262,9 @@ class HttpTransport(Transport):
class Webhook(HttpTransport):
def prepare(self, template: str, check, urlencode=False, latin1=False, allow_ping_body=False) -> str:
def prepare(
self, template: str, check, urlencode=False, latin1=False, allow_ping_body=False
) -> str:
"""Replace variables with actual values."""
def safe(s: str) -> str:
@ -821,12 +823,16 @@ class Zulip(HttpTransport):
if not settings.ZULIP_ENABLED:
raise TransportError("Zulip notifications are not enabled.")
topic = self.channel.zulip_topic
if not topic:
topic = tmpl("zulip_topic.html", check=check)
url = self.channel.zulip_site + "/api/v1/messages"
auth = (self.channel.zulip_bot_email, self.channel.zulip_api_key)
data = {
"type": self.channel.zulip_type,
"to": self.channel.zulip_to,
"topic": tmpl("zulip_topic.html", check=check),
"topic": topic,
"content": tmpl("zulip_content.html", check=check),
}

View file

@ -311,6 +311,7 @@ class AddZulipForm(forms.Form):
site = forms.URLField(max_length=100, validators=[WebhookValidator()])
mtype = forms.ChoiceField(choices=ZULIP_TARGETS)
to = forms.CharField(max_length=100)
topic = forms.CharField(max_length=100, required=False)
def get_value(self):
return json.dumps(dict(self.cleaned_data), sort_keys=True)
@ -349,3 +350,7 @@ class SeekForm(forms.Form):
def clean_end(self):
return datetime.fromtimestamp(self.cleaned_data["end"], tz=timezone.utc)
class TransferForm(forms.Form):
project = forms.UUIDField()

View file

@ -10,6 +10,7 @@ def _get_payload(**kwargs):
"site": "https://example.org",
"mtype": "stream",
"to": "general",
"topic": "foo",
}
payload.update(kwargs)
@ -37,6 +38,7 @@ class AddZulipTestCase(BaseTestCase):
self.assertEqual(c.zulip_api_key, "fake-key")
self.assertEqual(c.zulip_type, "stream")
self.assertEqual(c.zulip_to, "general")
self.assertEqual(c.zulip_topic, "foo")
def test_it_rejects_bad_email(self):
payload = _get_payload(bot_email="not@an@email")

View file

@ -73,3 +73,9 @@ class TransferTestCase(BaseTestCase):
self.client.login(username="bob@example.org", password="password")
r = self.client.post(self.url, payload)
self.assertEqual(r.status_code, 403)
def test_it_handles_bad_project_uuid(self):
self.client.login(username="bob@example.org", password="password")
payload = {"project": "not-uuid"}
r = self.client.post(self.url, payload)
self.assertEqual(r.status_code, 400)

View file

@ -139,10 +139,10 @@ def _get_rw_channel_for_user(request, code):
return channel
def _get_project_for_user(request, project_code):
def _get_project_for_user(request: HttpRequest, code: UUID) -> Tuple[Project, bool]:
"""Check access, return (project, rw) tuple."""
project = get_object_or_404(Project, code=project_code)
project = get_object_or_404(Project, code=code)
if request.user.is_superuser:
return project, True
@ -154,10 +154,10 @@ def _get_project_for_user(request, project_code):
return project, membership.is_rw
def _get_rw_project_for_user(request, project_code):
def _get_rw_project_for_user(request: HttpRequest, code: UUID) -> Project:
"""Check access, return (project, rw) tuple."""
project, rw = _get_project_for_user(request, project_code)
project, rw = _get_project_for_user(request, code)
if not rw:
raise PermissionDenied
@ -818,11 +818,15 @@ def uncloak(request, unique_key):
@login_required
def transfer(request, code):
def transfer(request: HttpRequest, code: UUID) -> HttpResponse:
check = _get_rw_check_for_user(request, code)
if request.method == "POST":
target_project = _get_rw_project_for_user(request, request.POST["project"])
form = forms.TransferForm(request.POST)
if not form.is_valid():
return HttpResponseBadRequest()
target_project = _get_rw_project_for_user(request, form.cleaned_data["project"])
if target_project.num_checks_available() <= 0:
return HttpResponseBadRequest()
@ -1696,7 +1700,7 @@ def add_victorops(request, code):
@require_setting("ZULIP_ENABLED")
@login_required
def add_zulip(request, code):
def add_zulip(request: HttpRequest, code: UUID) -> HttpResponse:
project = _get_rw_project_for_user(request, code)
if request.method == "POST":

View file

@ -2,13 +2,15 @@ $(function() {
function updateForm() {
var mType = $('input[name=mtype]:checked').val();
if (mType == "stream") {
$("#z-to-label").text("Stream Name");
$("#z-to-label").text("Stream");
$("#z-to-help").text('Example: "general"');
}
if (mType == "private") {
$("#z-to-label").text("User's Email");
$("#z-to-help").text('Example: "alice@example.org"');
}
$("#z-topic-group").toggleClass("hide", mType == "private");
}
// Update form labels when user clicks on radio buttons

View file

@ -104,8 +104,6 @@
</div>
</div>
<div id="z-mtype-group" class="form-group {{ form.mtype.css_classes }}">
<label class="col-sm-2 control-label">Post To</label>
<div class="col-sm-4">
@ -135,7 +133,7 @@
{% if form.mtype.value == "private" %}
User's Email
{% else %}
Stream Name
Stream
{% endif %}
</label>
<div class="col-sm-4">
@ -156,6 +154,22 @@
</div>
</div>
<div id="z-topic-group" class="form-group {% if form.mtype.value == 'private' %}hide{% endif %} {{ form.topic.css_classes }}">
<label for="z-topic" class="col-sm-2 control-label">Topic</label>
<div class="col-sm-4">
<input
id="z-topic"
type="text"
class="form-control"
name="topic"
value="{{ form.topic.value|default:"" }}">
<div class="help-block">
Example: "Alerts". Leave empty to use auto-generated topics in the form:
"{check name} is {status}".
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button