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:

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