From 52435a9a0cefc07854464d91371b85544b99a4df Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?P=C4=93teris=20Caune?=
Date: Fri, 29 Jan 2021 11:59:33 +0200
Subject: [PATCH] Add the SLACK_ENABLED setting
---
CHANGELOG.md | 1 +
docker/.env | 1 +
hc/api/tests/test_notify.py | 58 ------------
hc/api/tests/test_notify_slack.py | 91 +++++++++++++++++++
hc/api/transports.py | 3 +
hc/front/tests/test_add_slack.py | 7 ++
hc/front/tests/test_add_slack_btn.py | 6 ++
hc/front/tests/test_add_slack_complete.py | 6 ++
hc/front/views.py | 6 ++
hc/settings.py | 1 +
templates/docs/self_hosted_configuration.html | 17 +++-
templates/docs/self_hosted_configuration.md | 20 +++-
templates/front/channels.html | 3 +
templates/front/welcome.html | 2 +
templates/integrations/add_slack.html | 4 +-
15 files changed, 156 insertions(+), 70 deletions(-)
create mode 100644 hc/api/tests/test_notify_slack.py
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 781e807a..3db5d2d7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file.
- Add experimental Dockerfile and docker-compose.yml
- Add rate limiting for Pushover notifications (6 notifications / user / minute)
- Add the WEBHOOKS_ENABLED setting (#471)
+- Add the SLACK_ENABLED setting (#471)
## Bug Fixes
- Fix unwanted HTML escaping in SMS and WhatsApp notifications
diff --git a/docker/.env b/docker/.env
index d96a3e17..adf2e164 100644
--- a/docker/.env
+++ b/docker/.env
@@ -45,6 +45,7 @@ SITE_NAME=Mychecks
SITE_ROOT=http://localhost:8000
SLACK_CLIENT_ID=
SLACK_CLIENT_SECRET=
+SLACK_ENABLED=True
TELEGRAM_BOT_NAME=ExampleBot
TELEGRAM_TOKEN=
TRELLO_APP_KEY=
diff --git a/hc/api/tests/test_notify.py b/hc/api/tests/test_notify.py
index 3a22c228..0eb73268 100644
--- a/hc/api/tests/test_notify.py
+++ b/hc/api/tests/test_notify.py
@@ -8,7 +8,6 @@ from django.core import mail
from django.utils.timezone import now
from hc.api.models import Channel, Check, Notification, TokenBucket
from hc.test import BaseTestCase
-from requests.exceptions import Timeout
from django.test.utils import override_settings
@@ -72,63 +71,6 @@ class NotifyTestCase(BaseTestCase):
self.assertFalse(mock_post.called)
self.assertEqual(Notification.objects.count(), 0)
- @patch("hc.api.transports.requests.request")
- def test_slack(self, mock_post):
- self._setup_data("slack", "123")
- 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["json"]
- attachment = payload["attachments"][0]
- fields = {f["title"]: f["value"] for f in attachment["fields"]}
- self.assertEqual(fields["Last Ping"], "an hour ago")
-
- @patch("hc.api.transports.requests.request")
- def test_slack_with_complex_value(self, mock_post):
- v = json.dumps({"incoming_webhook": {"url": "123"}})
- self._setup_data("slack", v)
- mock_post.return_value.status_code = 200
-
- self.channel.notify(self.check)
- assert Notification.objects.count() == 1
-
- args, kwargs = mock_post.call_args
- self.assertEqual(args[1], "123")
-
- @patch("hc.api.transports.requests.request")
- def test_slack_handles_500(self, mock_post):
- self._setup_data("slack", "123")
- mock_post.return_value.status_code = 500
-
- self.channel.notify(self.check)
-
- n = Notification.objects.get()
- self.assertEqual(n.error, "Received status code 500")
-
- @patch("hc.api.transports.requests.request", side_effect=Timeout)
- def test_slack_handles_timeout(self, mock_post):
- self._setup_data("slack", "123")
-
- self.channel.notify(self.check)
-
- n = Notification.objects.get()
- self.assertEqual(n.error, "Connection timed out")
-
- @patch("hc.api.transports.requests.request")
- def test_slack_with_tabs_in_schedule(self, mock_post):
- self._setup_data("slack", "123")
- self.check.kind = "cron"
- self.check.schedule = "*\t* * * *"
- self.check.save()
- mock_post.return_value.status_code = 200
-
- self.channel.notify(self.check)
- self.assertEqual(Notification.objects.count(), 1)
- self.assertTrue(mock_post.called)
-
@patch("hc.api.transports.requests.request")
def test_hipchat(self, mock_post):
self._setup_data("hipchat", "123")
diff --git a/hc/api/tests/test_notify_slack.py b/hc/api/tests/test_notify_slack.py
new file mode 100644
index 00000000..69202d7a
--- /dev/null
+++ b/hc/api/tests/test_notify_slack.py
@@ -0,0 +1,91 @@
+# 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
+from requests.exceptions import Timeout
+from django.test.utils import override_settings
+
+
+class NotifyTestCase(BaseTestCase):
+ def _setup_data(self, 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 = "slack"
+ 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_slack(self, mock_post):
+ self._setup_data("123")
+ 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["json"]
+ attachment = payload["attachments"][0]
+ fields = {f["title"]: f["value"] for f in attachment["fields"]}
+ self.assertEqual(fields["Last Ping"], "an hour ago")
+
+ @patch("hc.api.transports.requests.request")
+ def test_slack_with_complex_value(self, mock_post):
+ v = json.dumps({"incoming_webhook": {"url": "123"}})
+ self._setup_data(v)
+ mock_post.return_value.status_code = 200
+
+ self.channel.notify(self.check)
+ assert Notification.objects.count() == 1
+
+ args, kwargs = mock_post.call_args
+ self.assertEqual(args[1], "123")
+
+ @patch("hc.api.transports.requests.request")
+ def test_slack_handles_500(self, mock_post):
+ self._setup_data("123")
+ mock_post.return_value.status_code = 500
+
+ self.channel.notify(self.check)
+
+ n = Notification.objects.get()
+ self.assertEqual(n.error, "Received status code 500")
+
+ @patch("hc.api.transports.requests.request", side_effect=Timeout)
+ def test_slack_handles_timeout(self, mock_post):
+ self._setup_data("123")
+
+ self.channel.notify(self.check)
+
+ n = Notification.objects.get()
+ self.assertEqual(n.error, "Connection timed out")
+
+ @patch("hc.api.transports.requests.request")
+ def test_slack_with_tabs_in_schedule(self, mock_post):
+ self._setup_data("123")
+ self.check.kind = "cron"
+ self.check.schedule = "*\t* * * *"
+ self.check.save()
+ mock_post.return_value.status_code = 200
+
+ self.channel.notify(self.check)
+ self.assertEqual(Notification.objects.count(), 1)
+ self.assertTrue(mock_post.called)
+
+ @override_settings(SLACK_ENABLED=False)
+ def test_it_requires_slack_enabled(self):
+ self._setup_data("123")
+ self.channel.notify(self.check)
+
+ n = Notification.objects.get()
+ self.assertEqual(n.error, "Slack notifications are not enabled.")
diff --git a/hc/api/transports.py b/hc/api/transports.py
index 854c376f..39826256 100644
--- a/hc/api/transports.py
+++ b/hc/api/transports.py
@@ -263,6 +263,9 @@ class Webhook(HttpTransport):
class Slack(HttpTransport):
def notify(self, check):
+ if not settings.SLACK_ENABLED:
+ return "Slack notifications are not enabled."
+
text = tmpl("slack_message.json", check=check)
payload = json.loads(text)
return self.post(self.channel.slack_webhook_url, json=payload)
diff --git a/hc/front/tests/test_add_slack.py b/hc/front/tests/test_add_slack.py
index 02ab92b6..ccf588e7 100644
--- a/hc/front/tests/test_add_slack.py
+++ b/hc/front/tests/test_add_slack.py
@@ -1,3 +1,4 @@
+from django.test.utils import override_settings
from hc.api.models import Channel
from hc.test import BaseTestCase
@@ -38,3 +39,9 @@ class AddSlackTestCase(BaseTestCase):
self.client.login(username="bob@example.org", password="password")
r = self.client.get(self.url)
self.assertEqual(r.status_code, 403)
+
+ @override_settings(SLACK_ENABLED=False)
+ def test_it_handles_disabled_integration(self):
+ self.client.login(username="alice@example.org", password="password")
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 404)
diff --git a/hc/front/tests/test_add_slack_btn.py b/hc/front/tests/test_add_slack_btn.py
index 9dc7b22d..2fe0c335 100644
--- a/hc/front/tests/test_add_slack_btn.py
+++ b/hc/front/tests/test_add_slack_btn.py
@@ -34,3 +34,9 @@ class AddSlackBtnTestCase(BaseTestCase):
self.client.login(username="bob@example.org", password="password")
r = self.client.get(self.url)
self.assertEqual(r.status_code, 403)
+
+ @override_settings(SLACK_ENABLED=False)
+ def test_it_handles_disabled_integration(self):
+ self.client.login(username="alice@example.org", password="password")
+ r = self.client.get(self.url)
+ self.assertEqual(r.status_code, 404)
diff --git a/hc/front/tests/test_add_slack_complete.py b/hc/front/tests/test_add_slack_complete.py
index 1a1617ca..0bdba714 100644
--- a/hc/front/tests/test_add_slack_complete.py
+++ b/hc/front/tests/test_add_slack_complete.py
@@ -81,3 +81,9 @@ class AddSlackCompleteTestCase(BaseTestCase):
self.client.login(username="bob@example.org", password="password")
r = self.client.get("/integrations/add_slack_btn/?code=12345678&state=foo")
self.assertEqual(r.status_code, 403)
+
+ @override_settings(SLACK_ENABLED=False)
+ def test_it_requires_slack_enabled(self):
+ self.client.login(username="alice@example.org", password="password")
+ r = self.client.get("/integrations/add_slack_btn/?code=12345678&state=foo")
+ self.assertEqual(r.status_code, 404)
diff --git a/hc/front/views.py b/hc/front/views.py
index e78ad461..9b6b9171 100644
--- a/hc/front/views.py
+++ b/hc/front/views.py
@@ -300,6 +300,7 @@ def index(request):
"enable_pushover": settings.PUSHOVER_API_TOKEN is not None,
"enable_shell": settings.SHELL_ENABLED is True,
"enable_signal": settings.SIGNAL_CLI_ENABLED is True,
+ "enable_slack": settings.SLACK_ENABLED is True,
"enable_slack_btn": settings.SLACK_CLIENT_ID is not None,
"enable_sms": settings.TWILIO_AUTH is not None,
"enable_telegram": settings.TELEGRAM_TOKEN is not None,
@@ -767,6 +768,7 @@ def channels(request, code):
"enable_pushover": settings.PUSHOVER_API_TOKEN is not None,
"enable_shell": settings.SHELL_ENABLED is True,
"enable_signal": settings.SIGNAL_CLI_ENABLED is True,
+ "enable_slack": settings.SLACK_ENABLED is True,
"enable_slack_btn": settings.SLACK_CLIENT_ID is not None,
"enable_sms": settings.TWILIO_AUTH is not None,
"enable_telegram": settings.TELEGRAM_TOKEN is not None,
@@ -1112,6 +1114,7 @@ def add_pagertree(request, code):
return render(request, "integrations/add_pagertree.html", ctx)
+@require_setting("SLACK_ENABLED")
@login_required
def add_slack(request, code):
project = _get_rw_project_for_user(request, code)
@@ -1136,12 +1139,14 @@ def add_slack(request, code):
return render(request, "integrations/add_slack.html", ctx)
+@require_setting("SLACK_ENABLED")
@require_setting("SLACK_CLIENT_ID")
def slack_help(request):
ctx = {"page": "channels"}
return render(request, "integrations/add_slack_btn.html", ctx)
+@require_setting("SLACK_ENABLED")
@require_setting("SLACK_CLIENT_ID")
@login_required
def add_slack_btn(request, code):
@@ -1166,6 +1171,7 @@ def add_slack_btn(request, code):
return render(request, "integrations/add_slack_btn.html", ctx)
+@require_setting("SLACK_ENABLED")
@require_setting("SLACK_CLIENT_ID")
@login_required
def add_slack_complete(request):
diff --git a/hc/settings.py b/hc/settings.py
index 66f01388..4d053aba 100644
--- a/hc/settings.py
+++ b/hc/settings.py
@@ -221,6 +221,7 @@ SIGNAL_CLI_ENABLED = envbool("SIGNAL_CLI_ENABLED", "False")
# Slack integration
SLACK_CLIENT_ID = os.getenv("SLACK_CLIENT_ID")
SLACK_CLIENT_SECRET = os.getenv("SLACK_CLIENT_SECRET")
+SLACK_ENABLED = envbool("SLACK_ENABLED", "True")
# Telegram integration -- override in local_settings.py
TELEGRAM_BOT_NAME = os.getenv("TELEGRAM_BOT_NAME", "ExampleBot")
diff --git a/templates/docs/self_hosted_configuration.html b/templates/docs/self_hosted_configuration.html
index 0b240f63..c8249f85 100644
--- a/templates/docs/self_hosted_configuration.html
+++ b/templates/docs/self_hosted_configuration.html
@@ -267,10 +267,14 @@ its web UI and documentation.
it needs to construct absolute URLs.
SLACK_CLIENT_ID
Default: None
-The Slack Client ID, required by the Slack integration.
-Go to https://api.slack.com/apps/
-to create a Slack app, and look up its Client ID and Client Secret.
-When setting up the Slack app, make sure to:
+The Slack Client ID, used by the Slack integration.
+The Slack integration can work with or without the Slack Client ID. If
+the Slack Client ID is not set, in the "Integrations - Add Slack" page,
+Healthchecks will ask the user to provide a webhook URL for posting notifications.
+If the Slack Client is set, Healthchecks will use the OAuth2 flow
+to get the webhook URL from Slack. The OAuth2 flow is more user-friendly.
+To set it up, go to https://api.slack.com/apps/
+and create a Slack app. When setting up the Slack app, make sure to:
- Add the incoming-webhook
scope to the Bot Token Scopes.
@@ -280,8 +284,11 @@ to create a Slack app, and look up its Client ID and Clien
SLACK_CLIENT_SECRET
Default: None
-The Slack Client Secret, required by the Slack integration.
+
The Slack Client Secret, required if SLACK_CLIENT_ID
is set.
Look it up at https://api.slack.com/apps/.
+SLACK_ENABLED
+Default: True
+A boolean that turns on/off the Slack integration. Enabled by default.
TELEGRAM_BOT_NAME
Default: ExampleBot
The Telegram bot name, required by the Telegram integration.
diff --git a/templates/docs/self_hosted_configuration.md b/templates/docs/self_hosted_configuration.md
index 5aab1258..722e6d43 100644
--- a/templates/docs/self_hosted_configuration.md
+++ b/templates/docs/self_hosted_configuration.md
@@ -427,12 +427,16 @@ it needs to construct absolute URLs.
Default: `None`
-The Slack Client ID, required by the Slack integration.
+The Slack Client ID, used by the Slack integration.
-Go to [https://api.slack.com/apps/](https://api.slack.com/apps/)
-to create a _Slack app_, and look up its _Client ID_ and _Client Secret_.
+The Slack integration can work with or without the Slack Client ID. If
+the Slack Client ID is not set, in the "Integrations - Add Slack" page,
+Healthchecks will ask the user to provide a webhook URL for posting notifications.
-When setting up the Slack app, make sure to:
+If the Slack Client _is_ set, Healthchecks will use the OAuth2 flow
+to get the webhook URL from Slack. The OAuth2 flow is more user-friendly.
+To set it up, go to [https://api.slack.com/apps/](https://api.slack.com/apps/)
+and create a _Slack app_. When setting up the Slack app, make sure to:
* Add the [incoming-webhook](https://api.slack.com/scopes/incoming-webhook)
scope to the Bot Token Scopes.
@@ -444,9 +448,15 @@ When setting up the Slack app, make sure to:
Default: `None`
-The Slack Client Secret, required by the Slack integration.
+The Slack Client Secret. Required if `SLACK_CLIENT_ID` is set.
Look it up at [https://api.slack.com/apps/](https://api.slack.com/apps/).
+## `SLACK_ENABLED` {: #SLACK_ENABLED }
+
+Default: `True`
+
+A boolean that turns on/off the Slack integration. Enabled by default.
+
## `TELEGRAM_BOT_NAME` {: #TELEGRAM_BOT_NAME }
Default: `ExampleBot`
diff --git a/templates/front/channels.html b/templates/front/channels.html
index 54e25421..b3497a94 100644
--- a/templates/front/channels.html
+++ b/templates/front/channels.html
@@ -186,6 +186,7 @@
{% if rw %}
Add More
+ {% if enable_slack %}
-
@@ -198,6 +199,8 @@
Add Integration
{% endif %}
+ {% endif %}
+
-
diff --git a/templates/front/welcome.html b/templates/front/welcome.html
index 9e76c7ea..ea3f5cbb 100644
--- a/templates/front/welcome.html
+++ b/templates/front/welcome.html
@@ -393,6 +393,7 @@
{% endif %}
+ {% if enable_slack %}
{% endif %}
+ {% endif %}
{% if enable_apprise %}
diff --git a/templates/integrations/add_slack.html b/templates/integrations/add_slack.html
index 07c9ce43..93285fa9 100644
--- a/templates/integrations/add_slack.html
+++ b/templates/integrations/add_slack.html
@@ -40,7 +40,7 @@
-
Copy the displayed WebHook URL and paste it down below.
+
Copy the displayed Webhook URL and paste it down below.
Save the integration, and it's done!
@@ -57,7 +57,7 @@
{% csrf_token %}