class ChannelChecksTestCase(BaseTestCase): def setUp(self): super(ChannelChecksTestCase, self).setUp() self.channel = Channel(user=self.alice, kind="email") self.channel.value = "*****@*****.**" self.channel.save() def test_it_works(self): url = "/integrations/%s/checks/" % self.channel.code self.client.login(username="******", password="******") r = self.client.get(url) self.assertContains(r, "*****@*****.**", status_code=200) def test_it_checks_owner(self): mallory = User(username="******", email="*****@*****.**") mallory.set_password("password") mallory.save() # channel does not belong to mallory so this should come back # with 403 Forbidden: url = "/integrations/%s/checks/" % self.channel.code self.client.login(username="******", password="******") r = self.client.get(url) assert r.status_code == 403 def test_missing_channel(self): # Valid UUID but there is no channel for it: url = "/integrations/6837d6ec-fc08-4da5-a67f-08a9ed1ccf62/checks/" self.client.login(username="******", password="******") r = self.client.get(url) assert r.status_code == 404
class VerifyEmailTestCase(BaseTestCase): def setUp(self): super(VerifyEmailTestCase, self).setUp() self.channel = Channel(user=self.alice, kind="email") self.channel.value = "*****@*****.**" self.channel.save() def test_it_works(self): token = self.channel.make_token() url = "/integrations/%s/verify/%s/" % (self.channel.code, token) r = self.client.post(url) assert r.status_code == 200, r.status_code channel = Channel.objects.get(code=self.channel.code) assert channel.email_verified def test_it_handles_bad_token(self): url = "/integrations/%s/verify/bad-token/" % self.channel.code r = self.client.post(url) assert r.status_code == 200, r.status_code channel = Channel.objects.get(code=self.channel.code) assert not channel.email_verified def test_missing_channel(self): # Valid UUID, and even valid token but there is no channel for it: code = "6837d6ec-fc08-4da5-a67f-08a9ed1ccf62" token = self.channel.make_token() url = "/integrations/%s/verify/%s/" % (code, token) r = self.client.post(url) assert r.status_code == 404
def test_it_shows_channel_list_with_pushbullet(self): self.client.login(username="******", password="******") ch = Channel(user=self.alice, kind="pushbullet", value="test-token") ch.save() r = self.client.get("/admin/api/channel/") self.assertContains(r, "Pushbullet")
def test_it_assigns_channels(self): channel = Channel(user=self.alice) channel.save() r = self.post({"api_key": "abc", "channels": "*"}) self.assertEqual(r.status_code, 201) check = Check.objects.get() self.assertEqual(check.channel_set.get(), channel)
def add_hipchat(request): if "installable_url" in request.GET: url = request.GET["installable_url"] assert url.startswith("https://api.hipchat.com") response = requests.get(url) if "oauthId" not in response.json(): messages.warning(request, "Something went wrong!") return redirect("hc-channels") channel = Channel(kind="hipchat") channel.user = request.team.user channel.value = response.text channel.save() channel.refresh_hipchat_access_token() channel.assign_all_checks() messages.success(request, "The HipChat integration has been added!") return redirect("hc-channels") install_url = "https://www.hipchat.com/addons/install?" + urlencode({ "url": settings.SITE_ROOT + reverse("hc-hipchat-capabilities") }) ctx = { "page": "channels", "install_url": install_url } return render(request, "integrations/add_hipchat.html", ctx)
def test_it_shows_pushover_notifications(self): ch = Channel(kind="po", user=self.alice) ch.save() Notification(owner=self.check, channel=ch, check_status="down").save() url = "/checks/%s/log/" % self.check.code self.client.login(username="******", password="******") r = self.client.get(url) self.assertContains(r, "Sent a Pushover notification", status_code=200)
def test_it_shows_webhook_notifications(self): ch = Channel(kind="webhook", user=self.alice, value="foo/$NAME") ch.save() Notification(owner=self.check, channel=ch, check_status="down").save() url = "/checks/%s/log/" % self.check.code self.client.login(username="******", password="******") r = self.client.get(url) self.assertContains(r, "Called webhook foo/$NAME", status_code=200)
def test_it_refreshes_hipchat_access_token(self, mock_post): mock_post.return_value.json.return_value = {"expires_in": 100} channel = Channel(kind="hipchat", user=self.alice, value=json.dumps({ "oauthId": "foo", "oauthSecret": "bar" })) channel.refresh_hipchat_access_token() # It should request a token using a correct tokenUrl mock_post.assert_called() self.assertTrue("expires_at" in channel.value)
class RemoveChannelTestCase(BaseTestCase): def setUp(self): super(RemoveChannelTestCase, self).setUp() self.channel = Channel(user=self.alice, kind="email") self.channel.value = "*****@*****.**" self.channel.save() def test_it_works(self): url = "/integrations/%s/remove/" % self.channel.code self.client.login(username="******", password="******") r = self.client.post(url) self.assertRedirects(r, "/integrations/") assert Channel.objects.count() == 0 def test_team_access_works(self): url = "/integrations/%s/remove/" % self.channel.code self.client.login(username="******", password="******") self.client.post(url) assert Channel.objects.count() == 0 def test_it_handles_bad_uuid(self): url = "/integrations/not-uuid/remove/" self.client.login(username="******", password="******") r = self.client.post(url) assert r.status_code == 400 def test_it_checks_owner(self): url = "/integrations/%s/remove/" % self.channel.code self.client.login(username="******", password="******") r = self.client.post(url) assert r.status_code == 403 def test_it_handles_missing_uuid(self): # Valid UUID but there is no channel for it: url = "/integrations/6837d6ec-fc08-4da5-a67f-08a9ed1ccf62/remove/" self.client.login(username="******", password="******") r = self.client.post(url) assert r.status_code == 302 def test_it_rejects_get(self): url = "/integrations/%s/remove/" % self.channel.code self.client.login(username="******", password="******") r = self.client.get(url) self.assertEqual(r.status_code, 405)
def add_slack_btn(request): code = request.GET.get("code", "") if len(code) < 8: return HttpResponseBadRequest() result = requests.post("https://slack.com/api/oauth.access", { "client_id": settings.SLACK_CLIENT_ID, "client_secret": settings.SLACK_CLIENT_SECRET, "code": code }) doc = result.json() if doc.get("ok"): channel = Channel() channel.user = request.team.user channel.kind = "slack" channel.value = result.text channel.save() channel.assign_all_checks() messages.success(request, "The Slack integration has been added!") else: s = doc.get("error") messages.warning(request, "Error message from slack: %s" % s) return redirect("hc-channels")
def test_it_checks_check_user(self): charlies_channel = Channel(user=self.charlie, kind="email") charlies_channel.email = "*****@*****.**" charlies_channel.save() payload = { "channel": charlies_channel.code, "check-%s" % self.check.code: True } self.client.login(username="******", password="******") r = self.client.post("/integrations/", data=payload) # mc belongs to charlie but self.check does not-- assert r.status_code == 403
def test_it_unassigns_channels(self): channel = Channel(user=self.alice) channel.save() self.check.assign_all_channels() r = self.post(self.check.code, { "api_key": "abc", "channels": "" }) self.assertEqual(r.status_code, 200) check = Check.objects.get() self.assertEqual(check.channel_set.count(), 0)
def test_it_formats_complex_slack_value(self): ch = Channel(kind="slack", user=self.alice) ch.value = json.dumps({ "ok": True, "team_name": "foo-team", "incoming_webhook": { "url": "http://example.org", "channel": "#bar" } }) ch.save() self.client.login(username="******", password="******") r = self.client.get("/integrations/") self.assertContains(r, "foo-team", status_code=200) self.assertContains(r, "#bar")
def setUp(self): super(UpdateChannelTestCase, self).setUp() self.check = Check(user=self.alice) self.check.save() self.channel = Channel(user=self.alice, kind="email") self.channel.email = "*****@*****.**" self.channel.save()
class RemoveChannelTestCase(TestCase): def setUp(self): super(RemoveChannelTestCase, self).setUp() self.alice = User(username="******", email="*****@*****.**") self.alice.set_password("password") self.alice.save() self.channel = Channel(user=self.alice, kind="email") self.channel.value = "*****@*****.**" self.channel.save() def test_it_works(self): url = "/integrations/%s/remove/" % self.channel.code self.client.login(username="******", password="******") r = self.client.post(url) self.assertRedirects(r, "/integrations/") assert Channel.objects.count() == 0 def test_it_handles_bad_uuid(self): url = "/integrations/not-uuid/remove/" self.client.login(username="******", password="******") r = self.client.post(url) assert r.status_code == 400 def test_it_checks_owner(self): url = "/integrations/%s/remove/" % self.channel.code mallory = User(username="******", email="*****@*****.**") mallory.set_password("password") mallory.save() self.client.login(username="******", password="******") r = self.client.post(url) assert r.status_code == 403 def test_it_handles_missing_uuid(self): # Valid UUID but there is no channel for it: url = "/integrations/6837d6ec-fc08-4da5-a67f-08a9ed1ccf62/remove/" self.client.login(username="******", password="******") r = self.client.post(url) assert r.status_code == 404
def setUp(self): self.alice = User(username="******") self.alice.set_password("password") self.alice.save() self.channel = Channel(user=self.alice, kind="email") self.channel.value = "*****@*****.**" self.channel.save()
def setUp(self): super(ChannelChecksTestCase, self).setUp() self.alice = User(username="******", email="*****@*****.**") self.alice.set_password("password") self.alice.save() self.channel = Channel(user=self.alice, kind="email") self.channel.value = "*****@*****.**" self.channel.save()
def test_it_checks_check_user(self): mallory = User(username="******", email="*****@*****.**") mallory.set_password("password") mallory.save() mc = Channel(user=mallory, kind="email") mc.email = "*****@*****.**" mc.save() payload = { "channel": mc.code, "check-%s" % self.check.code: True } self.client.login(username="******", password="******") r = self.client.post("/integrations/", data=payload) # mc belongs to mallorym but self.check does not-- assert r.status_code == 403
def _setup_data(self, kind, value, status="down", email_verified=True): self.check = Check() self.check.status = status self.check.save() self.channel = Channel(user=self.alice) self.channel.kind = kind self.channel.value = value self.channel.email_verified = email_verified self.channel.save() self.channel.checks.add(self.check)
def add_telegram(request): chat_id, chat_type, chat_name = None, None, None qs = request.META["QUERY_STRING"] if qs: chat_id, chat_type, chat_name = signing.loads(qs, max_age=600) if request.method == "POST": channel = Channel(user=request.team.user, kind="telegram") channel.value = json.dumps({ "id": chat_id, "type": chat_type, "name": chat_name }) channel.save() channel.assign_all_checks() messages.success(request, "The Telegram integration has been added!") return redirect("hc-channels") ctx = { "chat_id": chat_id, "chat_type": chat_type, "chat_name": chat_name, "bot_name": settings.TELEGRAM_BOT_NAME } return render(request, "integrations/add_telegram.html", ctx)
def add_zendesk(request): if settings.ZENDESK_CLIENT_ID is None: raise Http404("zendesk integration is not available") if request.method == "POST": domain = request.POST.get("subdomain") request.session["subdomain"] = domain redirect_uri = settings.SITE_ROOT + reverse("hc-add-zendesk") auth_url = "https://%s.zendesk.com/oauth/authorizations/new?" % domain auth_url += urlencode({ "client_id": settings.ZENDESK_CLIENT_ID, "redirect_uri": redirect_uri, "response_type": "code", "scope": "requests:read requests:write", "state": _prepare_state(request, "zendesk") }) return redirect(auth_url) if "code" in request.GET: code = _get_validated_code(request, "zendesk") if code is None: return HttpResponseBadRequest() domain = request.session.pop("subdomain") url = "https://%s.zendesk.com/oauth/tokens" % domain redirect_uri = settings.SITE_ROOT + reverse("hc-add-zendesk") result = requests.post(url, { "client_id": settings.ZENDESK_CLIENT_ID, "client_secret": settings.ZENDESK_CLIENT_SECRET, "code": code, "grant_type": "authorization_code", "redirect_uri": redirect_uri, "scope": "read" }) doc = result.json() if "access_token" in doc: doc["subdomain"] = domain channel = Channel(kind="zendesk") channel.user = request.team.user channel.value = json.dumps(doc) channel.save() channel.assign_all_checks() messages.success(request, "The Zendesk integration has been added!") else: messages.warning(request, "Something went wrong") return redirect("hc-channels") ctx = {"page": "channels"} return render(request, "integrations/add_zendesk.html", ctx)
class UnsubscribeEmailTestCase(BaseTestCase): def setUp(self): super(UnsubscribeEmailTestCase, self).setUp() self.channel = Channel(user=self.alice, kind="email") self.channel.value = "*****@*****.**" self.channel.save() def test_it_works(self): token = self.channel.make_token() url = "/integrations/%s/unsub/%s/" % (self.channel.code, token) r = self.client.get(url) self.assertContains(r, "has been unsubscribed", status_code=200) q = Channel.objects.filter(code=self.channel.code) self.assertEqual(q.count(), 0) def test_it_checks_token(self): url = "/integrations/%s/unsub/faketoken/" % self.channel.code r = self.client.get(url) self.assertContains(r, "link you just used is incorrect", status_code=200) def test_it_checks_channel_kind(self): self.channel.kind = "webhook" self.channel.save() token = self.channel.make_token() url = "/integrations/%s/unsub/%s/" % (self.channel.code, token) r = self.client.get(url) self.assertEqual(r.status_code, 400)
def add_slack(request): if not settings.SLACK_CLIENT_ID and not request.user.is_authenticated: return redirect("hc-login") if request.method == "POST": form = AddUrlForm(request.POST) if form.is_valid(): channel = Channel(user=request.team.user, kind="slack") channel.value = form.cleaned_data["value"] channel.save() channel.assign_all_checks() return redirect("hc-channels") else: form = AddUrlForm() ctx = { "page": "channels", "form": form, "slack_client_id": settings.SLACK_CLIENT_ID } if settings.SLACK_CLIENT_ID: ctx["state"] = _prepare_state(request, "slack") return render(request, "integrations/add_slack.html", ctx)
class ChannelChecksTestCase(BaseTestCase): def setUp(self): super(ChannelChecksTestCase, self).setUp() self.channel = Channel(user=self.alice, kind="email") self.channel.value = "*****@*****.**" self.channel.save() def test_it_works(self): url = "/integrations/%s/checks/" % self.channel.code self.client.login(username="******", password="******") r = self.client.get(url) self.assertContains(r, "Assign Checks to Channel", status_code=200) def test_team_access_works(self): url = "/integrations/%s/checks/" % self.channel.code # Logging in as bob, not alice. Bob has team access so this # should work. self.client.login(username="******", password="******") r = self.client.get(url) self.assertContains(r, "Assign Checks to Channel", status_code=200) def test_it_checks_owner(self): # channel does not belong to mallory so this should come back # with 403 Forbidden: url = "/integrations/%s/checks/" % self.channel.code self.client.login(username="******", password="******") r = self.client.get(url) assert r.status_code == 403 def test_missing_channel(self): # Valid UUID but there is no channel for it: url = "/integrations/6837d6ec-fc08-4da5-a67f-08a9ed1ccf62/checks/" self.client.login(username="******", password="******") r = self.client.get(url) assert r.status_code == 404
def add_pushover(request): if settings.PUSHOVER_API_TOKEN is None or settings.PUSHOVER_SUBSCRIPTION_URL is None: raise Http404("pushover integration is not available") if request.method == "POST": # Initiate the subscription nonce = get_random_string() request.session["po_nonce"] = nonce failure_url = settings.SITE_ROOT + reverse("hc-channels") success_url = settings.SITE_ROOT + reverse("hc-add-pushover") + "?" + urlencode({ "nonce": nonce, "prio": request.POST.get("po_priority", "0"), }) subscription_url = settings.PUSHOVER_SUBSCRIPTION_URL + "?" + urlencode({ "success": success_url, "failure": failure_url, }) return redirect(subscription_url) # Handle successful subscriptions if "pushover_user_key" in request.GET: if "nonce" not in request.GET or "prio" not in request.GET: return HttpResponseBadRequest() # Validate nonce if request.GET["nonce"] != request.session.get("po_nonce"): return HttpResponseForbidden() # Validate priority if request.GET["prio"] not in ("-2", "-1", "0", "1", "2"): return HttpResponseBadRequest() # All looks well-- del request.session["po_nonce"] if request.GET.get("pushover_unsubscribed") == "1": # Unsubscription: delete all Pushover channels for this user Channel.objects.filter(user=request.user, kind="po").delete() return redirect("hc-channels") else: # Subscription user_key = request.GET["pushover_user_key"] priority = int(request.GET["prio"]) channel = Channel(user=request.team.user, kind="po") channel.value = "%s|%d" % (user_key, priority) channel.save() channel.assign_all_checks() return redirect("hc-channels") # Show Integration Settings form ctx = { "page": "channels", "po_retry_delay": td(seconds=settings.PUSHOVER_EMERGENCY_RETRY_DELAY), "po_expiration": td(seconds=settings.PUSHOVER_EMERGENCY_EXPIRATION), } return render(request, "integrations/add_pushover.html", ctx)
def _setup_data(self, channel_kind, channel_value, email_verified=True): self.alice = User(username="******") self.alice.save() self.check = Check() self.check.status = "down" self.check.save() self.channel = Channel(user=self.alice) self.channel.kind = channel_kind self.channel.value = channel_value self.channel.email_verified = email_verified self.channel.save() self.channel.checks.add(self.check)
def add_email(request): if request.method == "POST": form = AddEmailForm(request.POST) if form.is_valid(): channel = Channel(user=request.team.user, kind="email") channel.value = form.cleaned_data["value"] channel.save() channel.assign_all_checks() channel.send_verify_link() return redirect("hc-channels") else: form = AddEmailForm() ctx = {"page": "channels", "form": form} return render(request, "integrations/add_email.html", ctx)
def _make_user(email): username = str(uuid.uuid4())[:30] user = User(username=username, email=email) user.save() channel = Channel() channel.user = user channel.kind = "email" channel.value = email channel.email_verified = True channel.save() return user
def _make_user(email): username = str(uuid.uuid4())[:30] user = User(username=username, email=email) user.set_unusable_password() user.save() # Ensure a profile gets created Profile.objects.for_user(user) channel = Channel() channel.user = user channel.kind = "email" channel.value = email channel.email_verified = True channel.save() return user
def _make_user(email): username = str(uuid.uuid4())[:30] user = User(username=username, email=email) user.set_unusable_password() user.save() profile = Profile(user=user) profile.save() channel = Channel() channel.user = user channel.kind = "email" channel.value = email channel.email_verified = True channel.save() return user
class NotifyTestCase(TestCase): def _setup_data(self, channel_kind, channel_value, email_verified=True): self.alice = User(username="******") self.alice.save() self.check = Check() self.check.status = "down" self.check.save() self.channel = Channel(user=self.alice) self.channel.kind = channel_kind self.channel.value = channel_value self.channel.email_verified = email_verified self.channel.save() self.channel.checks.add(self.check) @patch("hc.api.models.requests.get") def test_webhook(self, mock_get): self._setup_data("webhook", "http://example") self.channel.notify(self.check) mock_get.assert_called_with(u"http://example", timeout=5) @patch("hc.api.models.requests.get", side_effect=ReadTimeout) def test_webhooks_handle_timeouts(self, mock_get): self._setup_data("webhook", "http://example") self.channel.notify(self.check) assert Notification.objects.count() == 1 def test_email(self): self._setup_data("email", "*****@*****.**") self.channel.notify(self.check) assert Notification.objects.count() == 1 # And email should have been sent self.assertEqual(len(mail.outbox), 1) def test_it_skips_unverified_email(self): self._setup_data("email", "*****@*****.**", email_verified=False) self.channel.notify(self.check) assert Notification.objects.count() == 0 self.assertEqual(len(mail.outbox), 0) @patch("hc.api.models.requests.post") def test_pd(self, mock_post): self._setup_data("pd", "123") self.channel.notify(self.check) assert Notification.objects.count() == 1 args, kwargs = mock_post.call_args assert "trigger" in kwargs["data"]
class NotifySlackTestCase(BaseTestCase): def _setup_data(self, value, status="down", email_verified=True): self.check = Check(project=self.project) self.check.name = "Foobar" 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") # The payload should not contain check's code serialized = json.dumps(payload) self.assertNotIn(str(self.check.code), serialized) @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.")
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 = "zulip" 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": "*****@*****.**", "api_key": "fake-key", "mtype": "stream", "to": "general", } self._setup_data(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": "*****@*****.**", "api_key": "fake-key", "mtype": "stream", "to": "general", } self._setup_data(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": "*****@*****.**", "site": "https://custom.example.org", "api_key": "fake-key", "mtype": "stream", "to": "general", } self._setup_data(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"]) @override_settings(ZULIP_ENABLED=False) def test_it_requires_zulip_enabled(self): definition = { "bot_email": "*****@*****.**", "api_key": "fake-key", "mtype": "stream", "to": "general", } self._setup_data(json.dumps(definition)) self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "Zulip notifications are not enabled.")
def add_pushover(request): if settings.PUSHOVER_API_TOKEN is None or settings.PUSHOVER_SUBSCRIPTION_URL is None: raise Http404("pushover integration is not available") if request.method == "POST": # Initiate the subscription nonce = get_random_string() request.session["po_nonce"] = nonce failure_url = settings.SITE_ROOT + reverse("hc-channels") success_url = settings.SITE_ROOT + reverse( "hc-add-pushover") + "?" + urlencode( { "nonce": nonce, "prio": request.POST.get("po_priority", "0"), }) subscription_url = settings.PUSHOVER_SUBSCRIPTION_URL + "?" + urlencode( { "success": success_url, "failure": failure_url, }) return redirect(subscription_url) # Handle successful subscriptions if "pushover_user_key" in request.GET: if "nonce" not in request.GET or "prio" not in request.GET: return HttpResponseBadRequest() # Validate nonce if request.GET["nonce"] != request.session.get("po_nonce"): return HttpResponseForbidden() # Validate priority if request.GET["prio"] not in ("-2", "-1", "0", "1", "2"): return HttpResponseBadRequest() # All looks well-- del request.session["po_nonce"] if request.GET.get("pushover_unsubscribed") == "1": # Unsubscription: delete all Pushover channels for this user Channel.objects.filter(user=request.user, kind="po").delete() return redirect("hc-channels") else: # Subscription user_key = request.GET["pushover_user_key"] priority = int(request.GET["prio"]) channel = Channel(user=request.team.user, kind="po") channel.value = "%s|%d" % (user_key, priority) channel.save() channel.assign_all_checks() return redirect("hc-channels") # Show Integration Settings form ctx = { "page": "channels", "po_retry_delay": td(seconds=settings.PUSHOVER_EMERGENCY_RETRY_DELAY), "po_expiration": td(seconds=settings.PUSHOVER_EMERGENCY_EXPIRATION), } return render(request, "integrations/add_pushover.html", ctx)
def test_it_handles_legacy_opsgenie_value(self): c = Channel(kind="opsgenie", value="foo123") self.assertEqual(c.opsgenie_key, "foo123") self.assertEqual(c.opsgenie_region, "us")
class EditWebhookTestCase(BaseTestCase): def setUp(self): super().setUp() definition = { "method_down": "GET", "url_down": "http://example.org/down", "body_down": "$NAME is down", "headers_down": { "User-Agent": "My-Custom-UA" }, "method_up": "GET", "url_up": "http://example.org/up", "body_up": "$NAME is up", "headers_up": {}, } self.channel = Channel(project=self.project, kind="webhook") self.channel.name = "Call example.org" self.channel.value = json.dumps(definition) self.channel.save() self.url = "/integrations/%s/edit_webhook/" % self.channel.code def test_it_shows_form(self): self.client.login(username="******", password="******") r = self.client.get(self.url) self.assertContains(r, "Webhook Settings") self.assertContains(r, "Call example.org") # down self.assertContains(r, "http://example.org/down") self.assertContains(r, "My-Custom-UA") self.assertContains(r, "$NAME is down") # up self.assertContains(r, "http://example.org/up") self.assertContains(r, "$NAME is up") def test_it_saves_form_and_redirects(self): form = { "name": "Call foo.com / bar.com", "method_down": "POST", "url_down": "http://foo.com", "headers_down": "X-Foo: 1\nX-Bar: 2", "body_down": "going down", "method_up": "POST", "url_up": "https://bar.com", "headers_up": "Content-Type: text/plain", "body_up": "going up", } self.client.login(username="******", password="******") r = self.client.post(self.url, form) self.assertRedirects(r, self.channels_url) self.channel.refresh_from_db() self.assertEqual(self.channel.name, "Call foo.com / bar.com") down_spec = self.channel.down_webhook_spec self.assertEqual(down_spec["method"], "POST") self.assertEqual(down_spec["url"], "http://foo.com") self.assertEqual(down_spec["body"], "going down") self.assertEqual(down_spec["headers"], {"X-Foo": "1", "X-Bar": "2"}) up_spec = self.channel.up_webhook_spec self.assertEqual(up_spec["method"], "POST") self.assertEqual(up_spec["url"], "https://bar.com") self.assertEqual(up_spec["body"], "going up") self.assertEqual(up_spec["headers"], {"Content-Type": "text/plain"}) def test_it_requires_kind_webhook(self): self.channel.kind = "email" self.channel.value = "*****@*****.**" self.channel.save() self.client.login(username="******", password="******") r = self.client.get(self.url) self.assertEqual(r.status_code, 400) def test_it_requires_rw_access(self): self.bobs_membership.rw = False self.bobs_membership.save() self.client.login(username="******", password="******") r = self.client.post(self.url, {}) self.assertEqual(r.status_code, 403)
def setUp(self): super(VerifyEmailTestCase, self).setUp() self.channel = Channel(user=self.alice, kind="email") self.channel.value = "*****@*****.**" self.channel.save()
class EditEmailTestCase(BaseTestCase): def setUp(self): super().setUp() self.check = Check.objects.create(project=self.project) self.channel = Channel(project=self.project, kind="email") self.channel.value = json.dumps({ "value": "*****@*****.**", "up": True, "down": True }) self.channel.email_verified = True self.channel.save() self.url = f"/integrations/{self.channel.code}/edit/" def test_it_shows_form(self): self.client.login(username="******", password="******") r = self.client.get(self.url) self.assertContains( r, "Get an email message when check goes up or down.") self.assertContains(r, "*****@*****.**") self.assertContains(r, "Email Settings") def test_it_saves_changes(self): form = {"value": "*****@*****.**", "down": "true", "up": "false"} self.client.login(username="******", password="******") r = self.client.post(self.url, form) self.assertRedirects(r, self.channels_url) self.channel.refresh_from_db() self.assertEqual(self.channel.email_value, "*****@*****.**") self.assertTrue(self.channel.email_notify_down) self.assertFalse(self.channel.email_notify_up) # It should send a verification link email = mail.outbox[0] self.assertTrue(email.subject.startswith("Verify email address on")) self.assertEqual(email.to[0], "*****@*****.**") # Make sure it does not call assign_all_checks self.assertFalse(self.channel.checks.exists()) def test_it_skips_verification_if_email_unchanged(self): form = {"value": "*****@*****.**", "down": "false", "up": "true"} self.client.login(username="******", password="******") self.client.post(self.url, form) self.channel.refresh_from_db() self.assertEqual(self.channel.email_value, "*****@*****.**") self.assertFalse(self.channel.email_notify_down) self.assertTrue(self.channel.email_notify_up) self.assertTrue(self.channel.email_verified) # The email address did not change, so we should skip verification self.assertEqual(len(mail.outbox), 0) def test_team_access_works(self): form = {"value": "*****@*****.**", "down": "true", "up": "true"} self.client.login(username="******", password="******") self.client.post(self.url, form) self.channel.refresh_from_db() self.assertEqual(self.channel.email_value, "*****@*****.**") @override_settings(EMAIL_USE_VERIFICATION=False) def test_it_hides_confirmation_needed_notice(self): self.client.login(username="******", password="******") r = self.client.get(self.url) self.assertNotContains(r, "Requires confirmation") @override_settings(EMAIL_USE_VERIFICATION=False) def test_it_auto_verifies_email(self): form = {"value": "*****@*****.**", "down": "true", "up": "true"} self.client.login(username="******", password="******") r = self.client.post(self.url, form) self.assertRedirects(r, self.channels_url) self.channel.refresh_from_db() self.assertEqual(self.channel.email_value, "*****@*****.**") # Email should *not* have been sent self.assertEqual(len(mail.outbox), 0) def test_it_auto_verifies_own_email(self): form = {"value": "*****@*****.**", "down": "true", "up": "true"} self.client.login(username="******", password="******") r = self.client.post(self.url, form) self.assertRedirects(r, self.channels_url) self.channel.refresh_from_db() self.assertEqual(self.channel.email_value, "*****@*****.**") # Email should *not* have been sent self.assertEqual(len(mail.outbox), 0) def test_it_resets_disabled_flag(self): self.channel.disabled = True self.channel.save() form = { "value": "*****@*****.**", "down": "true", "up": "true" } self.client.login(username="******", password="******") r = self.client.post(self.url, form) self.assertRedirects(r, self.channels_url) self.channel.refresh_from_db() self.assertFalse(self.channel.disabled) self.assertFalse(self.channel.email_verified) # It should send a verification link email = mail.outbox[0] self.assertTrue(email.subject.startswith("Verify email address on")) def test_it_requires_rw_access(self): self.bobs_membership.role = "r" self.bobs_membership.save() self.client.login(username="******", password="******") r = self.client.get(self.url) self.assertEqual(r.status_code, 403)
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_webhook(self, mock_get): self._setup_data("webhook", "http://example") mock_get.return_value.status_code = 200 self.channel.notify(self.check) mock_get.assert_called_with( "get", "http://example", headers={"User-Agent": "healthchecks.io"}, timeout=5, ) @patch("hc.api.transports.requests.request", side_effect=Timeout) def test_webhooks_handle_timeouts(self, mock_get): self._setup_data("webhook", "http://example") self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "Connection timed out") @patch("hc.api.transports.requests.request", side_effect=ConnectionError) def test_webhooks_handle_connection_errors(self, mock_get): self._setup_data("webhook", "http://example") self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "Connection failed") @patch("hc.api.transports.requests.request") def test_webhooks_ignore_up_events(self, mock_get): self._setup_data("webhook", "http://example", status="up") self.channel.notify(self.check) self.assertFalse(mock_get.called) self.assertEqual(Notification.objects.count(), 0) @patch("hc.api.transports.requests.request") def test_webhooks_handle_500(self, mock_get): self._setup_data("webhook", "http://example") mock_get.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") def test_webhooks_support_variables(self, mock_get): template = "http://host/$CODE/$STATUS/$TAG1/$TAG2/?name=$NAME" self._setup_data("webhook", template) self.check.name = "Hello World" self.check.tags = "foo bar" self.check.save() self.channel.notify(self.check) url = "http://host/%s/down/foo/bar/?name=Hello%%20World" % self.check.code args, kwargs = mock_get.call_args self.assertEqual(args[0], "get") self.assertEqual(args[1], url) self.assertEqual(kwargs["headers"], {"User-Agent": "healthchecks.io"}) self.assertEqual(kwargs["timeout"], 5) @patch("hc.api.transports.requests.request") def test_webhooks_handle_variable_variables(self, mock_get): self._setup_data("webhook", "http://host/$$NAMETAG1") self.check.tags = "foo bar" self.check.save() self.channel.notify(self.check) # $$NAMETAG1 should *not* get transformed to "foo" args, kwargs = mock_get.call_args self.assertEqual(args[1], "http://host/$TAG1") @patch("hc.api.transports.requests.request") def test_webhooks_support_post(self, mock_request): template = "http://example.com\n\nThe Time Is $NOW" self._setup_data("webhook", template) self.check.save() self.channel.notify(self.check) args, kwargs = mock_request.call_args self.assertEqual(args[0], "post") self.assertEqual(args[1], "http://example.com") # spaces should not have been urlencoded: payload = kwargs["data"].decode() self.assertTrue(payload.startswith("The Time Is 2")) @patch("hc.api.transports.requests.request") def test_webhooks_dollarsign_escaping(self, mock_get): # If name or tag contains what looks like a variable reference, # that should be left alone: template = "http://host/$NAME" self._setup_data("webhook", template) self.check.name = "$TAG1" self.check.tags = "foo" self.check.save() self.channel.notify(self.check) url = "http://host/%24TAG1" mock_get.assert_called_with( "get", url, headers={"User-Agent": "healthchecks.io"}, timeout=5 ) @patch("hc.api.transports.requests.request") def test_webhook_fires_on_up_event(self, mock_get): self._setup_data("webhook", "http://foo\nhttp://bar", status="up") self.channel.notify(self.check) mock_get.assert_called_with( "get", "http://bar", headers={"User-Agent": "healthchecks.io"}, timeout=5 ) @patch("hc.api.transports.requests.request") def test_webhooks_handle_unicode_post_body(self, mock_request): template = "http://example.com\n\n(╯°□°)╯︵ ┻━┻" self._setup_data("webhook", template) self.check.save() self.channel.notify(self.check) args, kwargs = mock_request.call_args # unicode should be encoded into utf-8 self.assertIsInstance(kwargs["data"], bytes) @patch("hc.api.transports.requests.request") def test_webhooks_handle_json_value(self, mock_request): definition = { "method_down": "GET", "url_down": "http://foo.com", "body_down": "", "headers_down": {}, } self._setup_data("webhook", json.dumps(definition)) self.channel.notify(self.check) headers = {"User-Agent": "healthchecks.io"} mock_request.assert_called_with( "get", "http://foo.com", headers=headers, timeout=5 ) @patch("hc.api.transports.requests.request") def test_webhooks_handle_json_up_event(self, mock_request): definition = { "method_up": "GET", "url_up": "http://bar", "body_up": "", "headers_up": {}, } self._setup_data("webhook", json.dumps(definition), status="up") self.channel.notify(self.check) headers = {"User-Agent": "healthchecks.io"} mock_request.assert_called_with("get", "http://bar", headers=headers, timeout=5) @patch("hc.api.transports.requests.request") def test_webhooks_handle_post_headers(self, mock_request): definition = { "method_down": "POST", "url_down": "http://foo.com", "body_down": "data", "headers_down": {"Content-Type": "application/json"}, } self._setup_data("webhook", json.dumps(definition)) self.channel.notify(self.check) headers = {"User-Agent": "healthchecks.io", "Content-Type": "application/json"} mock_request.assert_called_with( "post", "http://foo.com", data=b"data", headers=headers, timeout=5 ) @patch("hc.api.transports.requests.request") def test_webhooks_handle_get_headers(self, mock_request): definition = { "method_down": "GET", "url_down": "http://foo.com", "body_down": "", "headers_down": {"Content-Type": "application/json"}, } self._setup_data("webhook", json.dumps(definition)) self.channel.notify(self.check) headers = {"User-Agent": "healthchecks.io", "Content-Type": "application/json"} mock_request.assert_called_with( "get", "http://foo.com", headers=headers, timeout=5 ) @patch("hc.api.transports.requests.request") def test_webhooks_allow_user_agent_override(self, mock_request): definition = { "method_down": "GET", "url_down": "http://foo.com", "body_down": "", "headers_down": {"User-Agent": "My-Agent"}, } self._setup_data("webhook", json.dumps(definition)) self.channel.notify(self.check) headers = {"User-Agent": "My-Agent"} mock_request.assert_called_with( "get", "http://foo.com", headers=headers, timeout=5 ) @patch("hc.api.transports.requests.request") def test_webhooks_support_variables_in_headers(self, mock_request): definition = { "method_down": "GET", "url_down": "http://foo.com", "body_down": "", "headers_down": {"X-Message": "$NAME is DOWN"}, } self._setup_data("webhook", json.dumps(definition)) self.check.name = "Foo" self.check.save() self.channel.notify(self.check) headers = {"User-Agent": "healthchecks.io", "X-Message": "Foo is DOWN"} mock_request.assert_called_with( "get", "http://foo.com", headers=headers, timeout=5 ) def test_email(self): self._setup_data("email", "*****@*****.**") self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "") # And email should have been sent self.assertEqual(len(mail.outbox), 1) email = mail.outbox[0] self.assertEqual(email.to[0], "*****@*****.**") self.assertTrue("X-Bounce-Url" in email.extra_headers) self.assertTrue("List-Unsubscribe" in email.extra_headers) self.assertTrue("List-Unsubscribe-Post" in email.extra_headers) def test_email_transport_handles_json_value(self): payload = {"value": "*****@*****.**", "up": True, "down": True} self._setup_data("email", json.dumps(payload)) self.channel.notify(self.check) # And email should have been sent self.assertEqual(len(mail.outbox), 1) email = mail.outbox[0] self.assertEqual(email.to[0], "*****@*****.**") def test_it_skips_unverified_email(self): self._setup_data("email", "*****@*****.**", email_verified=False) self.channel.notify(self.check) # If an email is not verified, it should be skipped over # without logging a notification: self.assertEqual(Notification.objects.count(), 0) self.assertEqual(len(mail.outbox), 0) def test_email_checks_up_down_flags(self): payload = {"value": "*****@*****.**", "up": True, "down": False} self._setup_data("email", json.dumps(payload)) self.channel.notify(self.check) # This channel should not notify on "down" events: self.assertEqual(Notification.objects.count(), 0) self.assertEqual(len(mail.outbox), 0) @patch("hc.api.transports.requests.request") def test_pd(self, mock_post): self._setup_data("pd", "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"] self.assertEqual(payload["event_type"], "trigger") self.assertEqual(payload["service_key"], "123") @patch("hc.api.transports.requests.request") def test_pd_complex(self, mock_post): self._setup_data("pd", json.dumps({"service_key": "456"})) 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"] self.assertEqual(payload["event_type"], "trigger") self.assertEqual(payload["service_key"], "456") @patch("hc.api.transports.requests.request") def test_pagertree(self, mock_post): self._setup_data("pagertree", "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"] self.assertEqual(payload["event_type"], "trigger") @patch("hc.api.transports.requests.request") def test_pagerteam(self, mock_post): self._setup_data("pagerteam", "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"] self.assertEqual(payload["event_type"], "trigger") @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") self.channel.notify(self.check) self.assertFalse(mock_post.called) self.assertEqual(Notification.objects.count(), 0) @patch("hc.api.transports.requests.request") def test_opsgenie_with_legacy_value(self, mock_post): self._setup_data("opsgenie", "123") mock_post.return_value.status_code = 202 self.channel.notify(self.check) n = Notification.objects.first() self.assertEqual(n.error, "") self.assertEqual(mock_post.call_count, 1) args, kwargs = mock_post.call_args self.assertIn("api.opsgenie.com", args[1]) payload = kwargs["json"] self.assertIn("DOWN", payload["message"]) @patch("hc.api.transports.requests.request") def test_opsgenie_up(self, mock_post): self._setup_data("opsgenie", "123", status="up") mock_post.return_value.status_code = 202 self.channel.notify(self.check) n = Notification.objects.first() self.assertEqual(n.error, "") self.assertEqual(mock_post.call_count, 1) args, kwargs = mock_post.call_args method, url = args self.assertTrue(str(self.check.code) in url) @patch("hc.api.transports.requests.request") def test_opsgenie_with_json_value(self, mock_post): self._setup_data("opsgenie", json.dumps({"key": "456", "region": "eu"})) mock_post.return_value.status_code = 202 self.channel.notify(self.check) n = Notification.objects.first() self.assertEqual(n.error, "") self.assertEqual(mock_post.call_count, 1) args, kwargs = mock_post.call_args self.assertIn("api.eu.opsgenie.com", args[1]) @patch("hc.api.transports.requests.request") def test_pushover(self, mock_post): self._setup_data("po", "123|0") 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["title"]) @patch("hc.api.transports.requests.request") def test_pushover_up_priority(self, mock_post): self._setup_data("po", "123|0|2", status="up") 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("UP", payload["title"]) self.assertEqual(payload["priority"], 2) self.assertIn("retry", payload) self.assertIn("expire", payload) @patch("hc.api.transports.requests.request") def test_victorops(self, mock_post): self._setup_data("victorops", "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"] self.assertEqual(payload["message_type"], "CRITICAL") @patch("hc.api.transports.requests.request") def test_discord(self, mock_post): v = json.dumps({"webhook": {"url": "123"}}) self._setup_data("discord", v) 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_pushbullet(self, mock_post): self._setup_data("pushbullet", "fake-token") mock_post.return_value.status_code = 200 self.channel.notify(self.check) assert Notification.objects.count() == 1 _, kwargs = mock_post.call_args self.assertEqual(kwargs["json"]["type"], "note") self.assertEqual(kwargs["headers"]["Access-Token"], "fake-token") @patch("hc.api.transports.requests.request") def test_telegram(self, mock_post): v = json.dumps({"id": 123}) self._setup_data("telegram", v) 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"] self.assertEqual(payload["chat_id"], 123) self.assertTrue("The check" in payload["text"]) @patch("hc.api.transports.requests.request") def test_sms(self, mock_post): self._setup_data("sms", "+1234567890") self.check.last_ping = now() - td(hours=2) mock_post.return_value.status_code = 200 self.channel.notify(self.check) self.assertEqual(Notification.objects.count(), 1) args, kwargs = mock_post.call_args payload = kwargs["data"] self.assertEqual(payload["To"], "+1234567890") self.assertFalse("\xa0" in payload["Body"]) # sent SMS counter should go up self.profile.refresh_from_db() self.assertEqual(self.profile.sms_sent, 1) @patch("hc.api.transports.requests.request") def test_sms_handles_json_value(self, mock_post): value = {"label": "foo", "value": "+1234567890"} self._setup_data("sms", json.dumps(value)) self.check.last_ping = now() - td(hours=2) 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.assertEqual(payload["To"], "+1234567890") @patch("hc.api.transports.requests.request") def test_sms_limit(self, mock_post): # At limit already: self.profile.last_sms_date = now() self.profile.sms_sent = 50 self.profile.save() self._setup_data("sms", "+1234567890") self.channel.notify(self.check) self.assertFalse(mock_post.called) n = Notification.objects.get() self.assertTrue("Monthly SMS limit exceeded" in n.error) # And email should have been sent self.assertEqual(len(mail.outbox), 1) email = mail.outbox[0] self.assertEqual(email.to[0], "*****@*****.**") self.assertEqual(email.subject, "Monthly SMS Limit Reached") @patch("hc.api.transports.requests.request") def test_sms_limit_reset(self, mock_post): # At limit, but also into a new month self.profile.sms_sent = 50 self.profile.last_sms_date = now() - td(days=100) self.profile.save() self._setup_data("sms", "+1234567890") mock_post.return_value.status_code = 200 self.channel.notify(self.check) self.assertTrue(mock_post.called) @patch("hc.api.transports.requests.request") def test_whatsapp(self, mock_post): definition = {"value": "+1234567890", "up": True, "down": True} self._setup_data("whatsapp", json.dumps(definition)) self.check.last_ping = now() - td(hours=2) mock_post.return_value.status_code = 200 self.channel.notify(self.check) self.assertEqual(Notification.objects.count(), 1) args, kwargs = mock_post.call_args payload = kwargs["data"] self.assertEqual(payload["To"], "whatsapp:+1234567890") # sent SMS counter should go up self.profile.refresh_from_db() self.assertEqual(self.profile.sms_sent, 1) @patch("hc.api.transports.requests.request") def test_whatsapp_obeys_up_down_flags(self, mock_post): definition = {"value": "+1234567890", "up": True, "down": False} self._setup_data("whatsapp", json.dumps(definition)) self.check.last_ping = now() - td(hours=2) self.channel.notify(self.check) self.assertEqual(Notification.objects.count(), 0) self.assertFalse(mock_post.called) @patch("hc.api.transports.requests.request") def test_whatsapp_limit(self, mock_post): # At limit already: self.profile.last_sms_date = now() self.profile.sms_sent = 50 self.profile.save() definition = {"value": "+1234567890", "up": True, "down": True} self._setup_data("whatsapp", json.dumps(definition)) self.channel.notify(self.check) self.assertFalse(mock_post.called) n = Notification.objects.get() self.assertTrue("Monthly message limit exceeded" in n.error) # And email should have been sent self.assertEqual(len(mail.outbox), 1) email = mail.outbox[0] self.assertEqual(email.to[0], "*****@*****.**") self.assertEqual(email.subject, "Monthly WhatsApp Limit Reached") @patch("apprise.Apprise") @override_settings(APPRISE_ENABLED=True) def test_apprise_enabled(self, mock_apprise): self._setup_data("apprise", "123") mock_aobj = Mock() mock_aobj.add.return_value = True mock_aobj.notify.return_value = True mock_apprise.return_value = mock_aobj self.channel.notify(self.check) self.assertEqual(Notification.objects.count(), 1) self.check.status = "up" self.assertEqual(Notification.objects.count(), 1) @patch("apprise.Apprise") @override_settings(APPRISE_ENABLED=False) def test_apprise_disabled(self, mock_apprise): self._setup_data("apprise", "123") mock_aobj = Mock() mock_aobj.add.return_value = True mock_aobj.notify.return_value = True mock_apprise.return_value = mock_aobj self.channel.notify(self.check) self.assertEqual(Notification.objects.count(), 1) def test_not_implimented(self): self._setup_data("webhook", "http://example") self.channel.kind = "invalid" with self.assertRaises(NotImplementedError): self.channel.notify(self.check) @patch("hc.api.transports.requests.request") def test_msteams(self, mock_post): self._setup_data("msteams", "http://example.com/webhook") 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"] self.assertEqual(payload["@type"], "MessageCard") @patch("hc.api.transports.os.system") @override_settings(SHELL_ENABLED=True) def test_shell(self, mock_system): definition = {"cmd_down": "logger hello", "cmd_up": ""} self._setup_data("shell", json.dumps(definition)) mock_system.return_value = 0 self.channel.notify(self.check) mock_system.assert_called_with("logger hello") @patch("hc.api.transports.os.system") @override_settings(SHELL_ENABLED=True) def test_shell_handles_nonzero_exit_code(self, mock_system): definition = {"cmd_down": "logger hello", "cmd_up": ""} self._setup_data("shell", json.dumps(definition)) mock_system.return_value = 123 self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "Command returned exit code 123") @patch("hc.api.transports.os.system") @override_settings(SHELL_ENABLED=True) def test_shell_supports_variables(self, mock_system): definition = {"cmd_down": "logger $NAME is $STATUS ($TAG1)", "cmd_up": ""} self._setup_data("shell", json.dumps(definition)) mock_system.return_value = 0 self.check.name = "Database" self.check.tags = "foo bar" self.check.save() self.channel.notify(self.check) mock_system.assert_called_with("logger Database is down (foo)") @patch("hc.api.transports.os.system") @override_settings(SHELL_ENABLED=False) def test_shell_disabled(self, mock_system): definition = {"cmd_down": "logger hello", "cmd_up": ""} self._setup_data("shell", json.dumps(definition)) self.channel.notify(self.check) self.assertFalse(mock_system.called) n = Notification.objects.get() self.assertEqual(n.error, "Shell commands are not enabled")
class NotificationStatusTestCase(BaseTestCase): def setUp(self): super().setUp() self.check = Check(project=self.project, status="up") self.check.save() self.channel = Channel(project=self.project, kind="email") self.channel.value = "*****@*****.**" self.channel.email_verified = True self.channel.save() self.n = Notification(owner=self.check, channel=self.channel) self.n.save() self.url = "/api/v1/notifications/%s/status" % self.n.code def test_it_handles_twilio_failed_status(self): r = self.client.post(self.url, {"MessageStatus": "failed"}) self.assertEqual(r.status_code, 200) self.n.refresh_from_db() self.assertEqual(self.n.error, "Delivery failed (status=failed).") self.channel.refresh_from_db() self.assertEqual(self.channel.last_error, "Delivery failed (status=failed).") self.assertTrue(self.channel.email_verified) def test_it_handles_twilio_undelivered_status(self): r = self.client.post(self.url, {"MessageStatus": "undelivered"}) self.assertEqual(r.status_code, 200) self.n.refresh_from_db() self.assertEqual(self.n.error, "Delivery failed (status=undelivered).") self.channel.refresh_from_db() self.assertIn("status=undelivered", self.channel.last_error) def test_it_handles_twilio_delivered_status(self): r = self.client.post(self.url, {"MessageStatus": "delivered"}) self.assertEqual(r.status_code, 200) self.n.refresh_from_db() self.assertEqual(self.n.error, "") self.channel.refresh_from_db() self.assertEqual(self.channel.last_error, "") def test_it_checks_ttl(self): self.n.created = self.n.created - timedelta(minutes=61) self.n.save() r = self.client.post(self.url, {"MessageStatus": "failed"}) self.assertEqual(r.status_code, 200) # The notification should not have the error field set: self.n.refresh_from_db() self.assertEqual(self.n.error, "") def test_it_handles_missing_notification(self): fake_code = "07c2f548-9850-4b27-af5d-6c9dc157ec02" url = f"/api/v1/notifications/{fake_code}/status" r = self.client.post(url, {"MessageStatus": "failed"}) self.assertEqual(r.status_code, 200) def test_it_requires_post(self): r = self.client.get(self.url) self.assertEqual(r.status_code, 405) def test_it_handles_error_key(self): r = self.client.post(self.url, {"error": "Something went wrong."}) self.assertEqual(r.status_code, 200) self.n.refresh_from_db() self.assertEqual(self.n.error, "Something went wrong.") self.channel.refresh_from_db() self.assertEqual(self.channel.last_error, "Something went wrong.") self.assertTrue(self.channel.email_verified) def test_it_handles_mark_not_verified_key(self): payload = {"error": "Received complaint.", "mark_not_verified": "1"} r = self.client.post(self.url, payload) self.assertEqual(r.status_code, 200) self.channel.refresh_from_db() self.assertEqual(self.channel.last_error, "Received complaint.") self.assertFalse(self.channel.email_verified) def test_it_handles_twilio_call_status_failed(self): r = self.client.post(self.url, {"CallStatus": "failed"}) self.assertEqual(r.status_code, 200) self.n.refresh_from_db() self.assertEqual(self.n.error, "Delivery failed (status=failed).") self.channel.refresh_from_db() self.assertEqual(self.channel.last_error, "Delivery failed (status=failed).") self.assertTrue(self.channel.email_verified)
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 = "opsgenie" 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_opsgenie_with_legacy_value(self, mock_post): self._setup_data("123") mock_post.return_value.status_code = 202 self.channel.notify(self.check) n = Notification.objects.first() self.assertEqual(n.error, "") self.assertEqual(mock_post.call_count, 1) args, kwargs = mock_post.call_args self.assertIn("api.opsgenie.com", args[1]) payload = kwargs["json"] self.assertIn("DOWN", payload["message"]) @patch("hc.api.transports.requests.request") def test_opsgenie_up(self, mock_post): self._setup_data("123", status="up") mock_post.return_value.status_code = 202 self.channel.notify(self.check) n = Notification.objects.first() self.assertEqual(n.error, "") self.assertEqual(mock_post.call_count, 1) args, kwargs = mock_post.call_args method, url = args self.assertTrue(str(self.check.code) in url) @patch("hc.api.transports.requests.request") def test_opsgenie_with_json_value(self, mock_post): self._setup_data(json.dumps({"key": "456", "region": "eu"})) mock_post.return_value.status_code = 202 self.channel.notify(self.check) n = Notification.objects.first() self.assertEqual(n.error, "") self.assertEqual(mock_post.call_count, 1) args, kwargs = mock_post.call_args self.assertIn("api.eu.opsgenie.com", args[1]) @patch("hc.api.transports.requests.request") def test_opsgenie_returns_error(self, mock_post): self._setup_data("123") mock_post.return_value.status_code = 403 mock_post.return_value.json.return_value = {"message": "Nice try"} self.channel.notify(self.check) n = Notification.objects.first() self.assertEqual( n.error, 'Received status code 403 with a message: "Nice try"') @override_settings(OPSGENIE_ENABLED=False) def test_it_requires_opsgenie_enabled(self): self._setup_data("123") self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "Opsgenie notifications are not enabled.")
class NotifyTestCase(BaseTestCase): def setUp(self): super().setUp() self.check = Check(project=self.project) self.check.name = "Daily Backup" self.check.desc = "Line 1\nLine2" self.check.tags = "foo bar" self.check.status = "down" self.check.last_ping = now() - td(minutes=61) self.check.n_pings = 112233 self.check.save() self.ping = Ping(owner=self.check) self.ping.remote_addr = "1.2.3.4" self.ping.body = "Body Line 1\nBody Line 2" self.ping.save() self.channel = Channel(project=self.project) self.channel.kind = "email" self.channel.value = "*****@*****.**" self.channel.email_verified = True self.channel.save() self.channel.checks.add(self.check) def test_email(self): self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "") # And email should have been sent self.assertEqual(len(mail.outbox), 1) email = mail.outbox[0] self.assertEqual(email.to[0], "*****@*****.**") self.assertTrue("X-Status-Url" in email.extra_headers) self.assertTrue("List-Unsubscribe" in email.extra_headers) self.assertTrue("List-Unsubscribe-Post" in email.extra_headers) html = email.alternatives[0][0] self.assertIn("Daily Backup", html) self.assertIn("Line 1<br>Line2", html) self.assertIn("Alices Project", html) self.assertIn("foo</code>", html) self.assertIn("bar</code>", html) self.assertIn("1 day", html) self.assertIn("from 1.2.3.4", html) self.assertIn("112233", html) self.assertIn("Body Line 1<br>Body Line 2", html) def test_it_shows_cron_schedule(self): self.check.kind = "cron" self.check.schedule = "0 18-23,0-8 * * *" self.check.save() self.channel.notify(self.check) email = mail.outbox[0] html = email.alternatives[0][0] self.assertIn("<code>0 18-23,0-8 * * *</code>", html) def test_it_truncates_long_body(self): self.ping.body = "X" * 10000 + ", and the rest gets cut off" self.ping.save() self.channel.notify(self.check) email = mail.outbox[0] html = email.alternatives[0][0] self.assertIn("[truncated]", html) self.assertNotIn("the rest gets cut off", html) def test_it_handles_missing_ping_object(self): self.ping.delete() self.channel.notify(self.check) email = mail.outbox[0] html = email.alternatives[0][0] self.assertIn("Daily Backup", html) def test_it_handles_missing_profile(self): self.channel.value = "*****@*****.**" self.channel.save() self.channel.notify(self.check) email = mail.outbox[0] self.assertEqual(email.to[0], "*****@*****.**") html = email.alternatives[0][0] self.assertIn("Daily Backup", html) self.assertNotIn("Projects Overview", html) def test_email_transport_handles_json_value(self): payload = {"value": "*****@*****.**", "up": True, "down": True} self.channel.value = json.dumps(payload) self.channel.save() self.channel.notify(self.check) # And email should have been sent self.assertEqual(len(mail.outbox), 1) email = mail.outbox[0] self.assertEqual(email.to[0], "*****@*****.**") def test_it_reports_unverified_email(self): self.channel.email_verified = False self.channel.save() self.channel.notify(self.check) # If an email is not verified, it should say so in the notification: n = Notification.objects.get() self.assertEqual(n.error, "Email not verified") def test_email_checks_up_down_flags(self): payload = {"value": "*****@*****.**", "up": True, "down": False} self.channel.value = json.dumps(payload) self.channel.save() self.channel.notify(self.check) # This channel should not notify on "down" events: self.assertEqual(Notification.objects.count(), 0) self.assertEqual(len(mail.outbox), 0) def test_email_handles_amperstand(self): self.check.name = "Foo & Bar" self.check.save() self.channel.notify(self.check) email = mail.outbox[0] self.assertEqual(email.subject, "DOWN | Foo & Bar")
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 = "msteams" 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_msteams(self, mock_post): self._setup_data("http://example.com/webhook") mock_post.return_value.status_code = 200 self.check.name = "_underscores_ & more" self.channel.notify(self.check) assert Notification.objects.count() == 1 args, kwargs = mock_post.call_args payload = kwargs["json"] self.assertEqual(payload["@type"], "MessageCard") # summary and title should be the same, except # title should have any special HTML characters escaped self.assertEqual(payload["summary"], "“_underscores_ & more” is DOWN.") self.assertEqual(payload["title"], "“_underscores_ & more” is DOWN.") # The payload should not contain check's code serialized = json.dumps(payload) self.assertNotIn(str(self.check.code), serialized) @patch("hc.api.transports.requests.request") def test_msteams_escapes_html_and_markdown_in_desc(self, mock_post): self._setup_data("http://example.com/webhook") mock_post.return_value.status_code = 200 self.check.desc = """ TEST _underscore_ `backticks` <u>underline</u> \\backslash\\ "quoted" """ self.channel.notify(self.check) args, kwargs = mock_post.call_args text = kwargs["json"]["sections"][0]["text"] self.assertIn(r"\_underscore\_", text) self.assertIn(r"\`backticks\`", text) self.assertIn("<u>underline</u>", text) self.assertIn(r"\\backslash\\ ", text) self.assertIn(""quoted"", text) @override_settings(MSTEAMS_ENABLED=False) def test_it_requires_msteams_enabled(self): self._setup_data("http://example.com/webhook") self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "MS Teams notifications are not enabled.")
class NotifySignalTestCase(BaseTestCase): def setUp(self): super().setUp() self.check = Check(project=self.project) self.check.name = "Daily Backup" self.check.status = "down" self.check.last_ping = now() - td(minutes=61) self.check.save() payload = {"value": "+123456789", "up": True, "down": True} self.channel = Channel(project=self.project) self.channel.kind = "signal" self.channel.value = json.dumps(payload) self.channel.save() self.channel.checks.add(self.check) @patch("hc.api.transports.socket.socket") def test_it_works(self, socket): socketobj = setup_mock(socket, {}) self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "") params = socketobj.req["params"] self.assertIn("is DOWN", params["message"]) self.assertIn("+123456789", params["recipient"]) # Only one check in the project, so there should be no note about # other checks: self.assertNotIn("All the other checks are up.", params["message"]) @patch("hc.api.transports.socket.socket") def test_it_obeys_down_flag(self, socket): payload = {"value": "+123456789", "up": True, "down": False} self.channel.value = json.dumps(payload) self.channel.save() self.channel.notify(self.check) # This channel should not notify on "down" events: self.assertEqual(Notification.objects.count(), 0) self.assertFalse(socket.called) @patch("hc.api.transports.socket.socket") def test_it_requires_signal_cli_socket(self, socket): with override_settings(SIGNAL_CLI_SOCKET=None): self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "Signal notifications are not enabled") self.assertFalse(socket.called) @patch("hc.api.transports.socket.socket") def test_it_does_not_escape_special_characters(self, socket): socketobj = setup_mock(socket, {}) self.check.name = "Foo & Bar" self.check.save() self.channel.notify(self.check) self.assertIn("Foo & Bar", socketobj.req["params"]["message"]) @override_settings(SECRET_KEY="test-secret") @patch("hc.api.transports.socket.socket") def test_it_obeys_rate_limit(self, socket): # "2862..." is sha1("+123456789test-secret") obj = TokenBucket( value="signal-2862991ccaa15c8856e7ee0abaf3448fb3c292e0") obj.tokens = 0 obj.save() self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "Rate limit exceeded") self.assertFalse(socket.called) @patch("hc.api.transports.socket.socket") def test_it_shows_all_other_checks_up_note(self, socket): socketobj = setup_mock(socket, {}) other = Check(project=self.project) other.name = "Foobar" other.status = "up" other.last_ping = now() - td(minutes=61) other.save() self.channel.notify(self.check) message = socketobj.req["params"]["message"] self.assertIn("All the other checks are up.", message) @patch("hc.api.transports.socket.socket") def test_it_lists_other_down_checks(self, socket): socketobj = setup_mock(socket, {}) other = Check(project=self.project) other.name = "Foobar" other.status = "down" other.last_ping = now() - td(minutes=61) other.save() self.channel.notify(self.check) message = socketobj.req["params"]["message"] self.assertIn("The following checks are also down", message) self.assertIn("Foobar", message) @patch("hc.api.transports.socket.socket") def test_it_does_not_show_more_than_10_other_checks(self, socket): socketobj = setup_mock(socket, {}) for i in range(0, 11): other = Check(project=self.project) other.name = f"Foobar #{i}" other.status = "down" other.last_ping = now() - td(minutes=61) other.save() self.channel.notify(self.check) message = socketobj.req["params"]["message"] self.assertNotIn("Foobar", message) self.assertIn("11 other checks are also down.", message) @patch("hc.api.transports.socket.socket") def test_it_handles_unregistered_user(self, socket): setup_mock(socket, {"error": {"message": "UnregisteredUserException"}}) self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "Recipient not found") @patch("hc.api.transports.socket.socket") def test_it_handles_error_code(self, socket): setup_mock(socket, {"error": {"code": 123}}) self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "signal-cli call failed (123)") @patch("hc.api.transports.socket.socket") def test_it_handles_oserror(self, socket): setup_mock(socket, {}, side_effect=OSError("oops")) self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "signal-cli call failed (oops)") @patch("hc.api.transports.socket.socket") def test_it_checks_jsonrpc_id(self, socket): socketobj = setup_mock(socket, {}) # Add a message with an unexpected id in the outbox. # The socket reader should skip over it. socketobj.outbox += b'{"id": "surprise"}\n' self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "") # outbox should be empty now self.assertEqual(socketobj.outbox, b"")
class NotifyPushoverTestCase(BaseTestCase): def _setup_data(self, value, status="down", email_verified=True): self.check = Check(project=self.project) self.check.name = "Foo" self.check.status = status self.check.last_ping = now() - td(minutes=61) self.check.save() self.channel = Channel(project=self.project) self.channel.kind = "po" 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_it_works(self, mock_post): self._setup_data("123|0") mock_post.return_value.status_code = 200 self.channel.notify(self.check) self.assertEqual(Notification.objects.count(), 1) args, kwargs = mock_post.call_args self.assertEqual(args[1], API + "/messages.json") payload = kwargs["data"] self.assertEqual(payload["title"], "Foo is DOWN") self.assertIn(self.check.cloaked_url(), payload["message"]) # Only one check in the project, so there should be no note about # other checks: self.assertNotIn("All the other checks are up.", payload["message"]) self.assertEqual(payload["tags"], self.check.unique_key) @patch("hc.api.transports.requests.request") def test_it_supports_up_priority(self, mock_post): self._setup_data("123|0|2", status="up") mock_post.return_value.status_code = 200 self.channel.notify(self.check) self.assertEqual(Notification.objects.count(), 1) args, kwargs = mock_post.call_args payload = kwargs["data"] self.assertIn("UP", payload["title"]) self.assertEqual(payload["priority"], 2) self.assertIn("retry", payload) self.assertIn("expire", payload) @override_settings(SECRET_KEY="test-secret") @patch("hc.api.transports.requests.request") def test_it_obeys_rate_limit(self, mock_post): self._setup_data("123|0") # "c0ca..." is sha1("123test-secret") obj = TokenBucket(value="po-c0ca2a9774952af32cabf86453f69e442c4ed0eb") obj.tokens = 0 obj.save() self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "Rate limit exceeded") @patch("hc.api.transports.requests.request") def test_it_cancels_emergency_notification(self, mock_post): self._setup_data("123|2|0", status="up") mock_post.return_value.status_code = 200 self.channel.notify(self.check) self.assertEqual(Notification.objects.count(), 1) self.assertEqual(mock_post.call_count, 2) cancel_args, cancel_kwargs = mock_post.call_args_list[0] expected = "/receipts/cancel_by_tag/%s.json" % self.check.unique_key self.assertEqual(cancel_args[1], API + expected) up_args, up_kwargs = mock_post.call_args_list[1] payload = up_kwargs["data"] self.assertIn("UP", payload["title"]) @patch("hc.api.transports.requests.request") def test_it_shows_all_other_checks_up_note(self, mock_post): self._setup_data("123|0") mock_post.return_value.status_code = 200 other = Check(project=self.project) other.name = "Foobar" other.status = "up" other.last_ping = now() - td(minutes=61) other.save() self.channel.notify(self.check) args, kwargs = mock_post.call_args payload = kwargs["data"] self.assertIn("All the other checks are up.", payload["message"]) @patch("hc.api.transports.requests.request") def test_it_lists_other_down_checks(self, mock_post): self._setup_data("123|0") mock_post.return_value.status_code = 200 other = Check(project=self.project) other.name = "Foobar" other.status = "down" other.last_ping = now() - td(minutes=61) other.save() self.channel.notify(self.check) args, kwargs = mock_post.call_args payload = kwargs["data"] self.assertIn("The following checks are also down", payload["message"]) self.assertIn("Foobar", payload["message"]) self.assertIn(other.cloaked_url(), payload["message"]) @patch("hc.api.transports.requests.request") def test_it_does_not_show_more_than_10_other_checks(self, mock_post): self._setup_data("123|0") mock_post.return_value.status_code = 200 for i in range(0, 11): other = Check(project=self.project) other.name = f"Foobar #{i}" other.status = "down" other.last_ping = now() - td(minutes=61) other.save() self.channel.notify(self.check) args, kwargs = mock_post.call_args payload = kwargs["data"] self.assertNotIn("Foobar", payload["message"]) self.assertIn("11 other checks are also down.", payload["message"]) @patch("hc.api.transports.requests.request") def test_it_does_not_escape_title(self, mock_post): self._setup_data("123|0") self.check.name = "Foo & Bar" self.check.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["title"], "Foo & Bar is DOWN")
class UpdateChannelTestCase(BaseTestCase): def setUp(self): super(UpdateChannelTestCase, self).setUp() self.check = Check(user=self.alice) self.check.save() self.channel = Channel(user=self.alice, kind="email") self.channel.email = "*****@*****.**" self.channel.save() def test_it_works(self): payload = { "channel": self.channel.code, "check-%s" % self.check.code: True } self.client.login(username="******", password="******") r = self.client.post("/integrations/", data=payload) self.assertRedirects(r, "/integrations/") channel = Channel.objects.get(code=self.channel.code) checks = channel.checks.all() assert len(checks) == 1 assert checks[0].code == self.check.code def test_team_access_works(self): payload = { "channel": self.channel.code, "check-%s" % self.check.code: True } # Logging in as bob, not alice. Bob has team access so this # should work. self.client.login(username="******", password="******") r = self.client.post("/integrations/", data=payload, follow=True) self.assertEqual(r.status_code, 200) def test_it_checks_channel_user(self): payload = {"channel": self.channel.code} self.client.login(username="******", password="******") r = self.client.post("/integrations/", data=payload) # self.channel does not belong to charlie, this should fail-- assert r.status_code == 403 def test_it_checks_check_user(self): charlies_channel = Channel(user=self.charlie, kind="email") charlies_channel.email = "*****@*****.**" charlies_channel.save() payload = { "channel": charlies_channel.code, "check-%s" % self.check.code: True } self.client.login(username="******", password="******") r = self.client.post("/integrations/", data=payload) # mc belongs to charlie but self.check does not-- assert r.status_code == 403 def test_it_handles_missing_channel(self): # Correct UUID but there is no channel for it: payload = {"channel": "6837d6ec-fc08-4da5-a67f-08a9ed1ccf62"} self.client.login(username="******", password="******") r = self.client.post("/integrations/", data=payload) assert r.status_code == 400 def test_it_handles_missing_check(self): # check- key has a correct UUID but there's no check object for it payload = { "channel": self.channel.code, "check-6837d6ec-fc08-4da5-a67f-08a9ed1ccf62": True } self.client.login(username="******", password="******") r = self.client.post("/integrations/", data=payload) assert r.status_code == 400
def test_it_handles_legacy_sms_json_value(self): c = Channel(kind="sms", value=json.dumps({"value": "+123123123"})) self.assertTrue(c.sms_notify_down) self.assertFalse(c.sms_notify_up)
class SendTestNotificationTestCase(BaseTestCase): def setUp(self): super().setUp() self.channel = Channel(kind="email", project=self.project) self.channel.email_verified = True self.channel.value = "*****@*****.**" self.channel.save() self.url = "/integrations/%s/test/" % self.channel.code def test_it_sends_test_email(self): self.client.login(username="******", password="******") r = self.client.post(self.url, {}, follow=True) self.assertRedirects(r, self.channels_url) self.assertContains(r, "Test notification sent!") # And email should have been sent self.assertEqual(len(mail.outbox), 1) email = mail.outbox[0] self.assertEqual(email.to[0], "*****@*****.**") self.assertTrue("X-Status-Url" in email.extra_headers) self.assertTrue("List-Unsubscribe" in email.extra_headers) # It should create a notification n = Notification.objects.get() self.assertEqual(n.channel, self.channel) self.assertEqual(n.error, "") def test_it_clears_channel_last_error(self): self.channel.last_error = "Something went wrong" self.channel.save() self.client.login(username="******", password="******") self.client.post(self.url, {}) self.channel.refresh_from_db() self.assertEqual(self.channel.last_error, "") def test_it_sets_channel_last_error(self): self.channel.email_verified = False self.channel.save() self.client.login(username="******", password="******") r = self.client.post(self.url, {}, follow=True) self.assertContains(r, "Could not send a test notification") self.assertContains(r, "Email not verified") self.channel.refresh_from_db() self.assertEqual(self.channel.last_error, "Email not verified") @patch("hc.api.transports.requests.request") def test_it_handles_webhooks_with_no_down_url(self, mock_get): mock_get.return_value.status_code = 200 self.channel.kind = "webhook" self.channel.value = json.dumps({ "method_down": "GET", "url_down": "", "body_down": "", "headers_down": {}, "method_up": "GET", "url_up": "http://example-url", "body_up": "", "headers_up": {}, }) self.channel.save() self.client.login(username="******", password="******") r = self.client.post(self.url, {}, follow=True) self.assertRedirects(r, self.channels_url) self.assertContains(r, "Test notification sent!") def test_it_handles_webhooks_with_no_urls(self): self.channel.kind = "webhook" self.channel.value = json.dumps({ "method_down": "GET", "url_down": "", "body_down": "", "headers_down": {}, "method_up": "GET", "url_up": "", "body_up": "", "headers_up": {}, }) self.channel.save() self.client.login(username="******", password="******") r = self.client.post(self.url, {}, follow=True) self.assertRedirects(r, self.channels_url) self.assertContains(r, "Could not send a test notification") def test_it_checks_channel_ownership(self): self.client.login(username="******", password="******") r = self.client.post(self.url, {}, follow=True) self.assertEqual(r.status_code, 404)
def test_it_handles_json_opsgenie_value(self): c = Channel(kind="opsgenie") c.value = json.dumps({"key": "abc", "region": "eu"}) self.assertEqual(c.opsgenie_key, "abc") self.assertEqual(c.opsgenie_region, "eu")
def add_pushover(request): if (settings.PUSHOVER_API_TOKEN is None or settings.PUSHOVER_SUBSCRIPTION_URL is None): raise Http404("pushover integration is not available") if not request.user.is_authenticated: ctx = {"page": "channels"} return render(request, "integrations/add_pushover.html", ctx) if request.method == "POST": # Initiate the subscription state = _prepare_state(request, "pushover") failure_url = settings.SITE_ROOT + reverse("hc-channels") success_url = ( settings.SITE_ROOT + reverse("hc-add-pushover") + "?" + urlencode({ "state": state, "prio": request.POST.get("po_priority", "0"), "prio_up": request.POST.get("po_priority_up", "0"), })) subscription_url = (settings.PUSHOVER_SUBSCRIPTION_URL + "?" + urlencode({ "success": success_url, "failure": failure_url })) return redirect(subscription_url) # Handle successful subscriptions if "pushover_user_key" in request.GET: key = _get_validated_code(request, "pushover", "pushover_user_key") if key is None: return HttpResponseBadRequest() # Validate priority prio = request.GET.get("prio") if prio not in ("-2", "-1", "0", "1", "2"): return HttpResponseBadRequest() prio_up = request.GET.get("prio_up") if prio_up not in ("-2", "-1", "0", "1", "2"): return HttpResponseBadRequest() if request.GET.get("pushover_unsubscribed") == "1": # Unsubscription: delete all Pushover channels for this project Channel.objects.filter(project=request.project, kind="po").delete() return redirect("hc-channels") # Subscription channel = Channel(project=request.project, kind="po") channel.value = "%s|%s|%s" % (key, prio, prio_up) channel.save() channel.assign_all_checks() messages.success(request, "The Pushover integration has been added!") return redirect("hc-channels") # Show Integration Settings form ctx = { "page": "channels", "project": request.project, "po_retry_delay": td(seconds=settings.PUSHOVER_EMERGENCY_RETRY_DELAY), "po_expiration": td(seconds=settings.PUSHOVER_EMERGENCY_EXPIRATION), } return render(request, "integrations/add_pushover.html", ctx)
class NotifyTestCase(BaseTestCase): def _setup_data(self, kind, value, status="down", email_verified=True): self.check = Check() self.check.status = status self.check.user = self.alice self.check.last_ping = now() - td(minutes=61) self.check.save() self.channel = Channel(user=self.alice) 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_webhook(self, mock_get): self._setup_data("webhook", "http://example") mock_get.return_value.status_code = 200 self.channel.notify(self.check) mock_get.assert_called_with("get", u"http://example", headers={"User-Agent": "healthchecks.io"}, timeout=5) @patch("hc.api.transports.requests.request", side_effect=Timeout) def test_webhooks_handle_timeouts(self, mock_get): self._setup_data("webhook", "http://example") self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "Connection timed out") @patch("hc.api.transports.requests.request", side_effect=ConnectionError) def test_webhooks_handle_connection_errors(self, mock_get): self._setup_data("webhook", "http://example") self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "Connection failed") @patch("hc.api.transports.requests.request") def test_webhooks_ignore_up_events(self, mock_get): self._setup_data("webhook", "http://example", status="up") self.channel.notify(self.check) self.assertFalse(mock_get.called) self.assertEqual(Notification.objects.count(), 0) @patch("hc.api.transports.requests.request") def test_webhooks_handle_500(self, mock_get): self._setup_data("webhook", "http://example") mock_get.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") def test_webhooks_support_variables(self, mock_get): template = "http://host/$CODE/$STATUS/$TAG1/$TAG2/?name=$NAME" self._setup_data("webhook", template) self.check.name = "Hello World" self.check.tags = "foo bar" self.check.save() self.channel.notify(self.check) url = u"http://host/%s/down/foo/bar/?name=Hello%%20World" \ % self.check.code args, kwargs = mock_get.call_args self.assertEqual(args[0], "get") self.assertEqual(args[1], url) self.assertEqual(kwargs["headers"], {"User-Agent": "healthchecks.io"}) self.assertEqual(kwargs["timeout"], 5) @patch("hc.api.transports.requests.request") def test_webhooks_support_post(self, mock_request): template = "http://example.com\n\nThe Time Is $NOW" self._setup_data("webhook", template) self.check.save() self.channel.notify(self.check) args, kwargs = mock_request.call_args self.assertEqual(args[0], "post") self.assertEqual(args[1], "http://example.com") # spaces should not have been urlencoded: payload = kwargs["data"].decode("utf-8") self.assertTrue(payload.startswith("The Time Is 2")) @patch("hc.api.transports.requests.request") def test_webhooks_dollarsign_escaping(self, mock_get): # If name or tag contains what looks like a variable reference, # that should be left alone: template = "http://host/$NAME" self._setup_data("webhook", template) self.check.name = "$TAG1" self.check.tags = "foo" self.check.save() self.channel.notify(self.check) url = u"http://host/%24TAG1" mock_get.assert_called_with("get", url, headers={"User-Agent": "healthchecks.io"}, timeout=5) @patch("hc.api.transports.requests.request") def test_webhook_fires_on_up_event(self, mock_get): self._setup_data("webhook", "http://foo\nhttp://bar", status="up") self.channel.notify(self.check) mock_get.assert_called_with("get", "http://bar", headers={"User-Agent": "healthchecks.io"}, timeout=5) @patch("hc.api.transports.requests.request") def test_webhooks_handle_unicode_post_body(self, mock_request): template = u"http://example.com\n\n(╯°□°)╯︵ ┻━┻" self._setup_data("webhook", template) self.check.save() self.channel.notify(self.check) args, kwargs = mock_request.call_args # unicode should be encoded into utf-8 self.assertTrue(isinstance(kwargs["data"], binary_type)) @patch("hc.api.transports.requests.request") def test_webhooks_handle_json_value(self, mock_request): definition = {"url_down": "http://foo.com"} self._setup_data("webhook", json.dumps(definition)) self.channel.notify(self.check) headers = {"User-Agent": "healthchecks.io"} mock_request.assert_called_with("get", "http://foo.com", headers=headers, timeout=5) @patch("hc.api.transports.requests.request") def test_webhooks_handle_json_up_event(self, mock_request): definition = {"url_up": "http://bar"} self._setup_data("webhook", json.dumps(definition), status="up") self.channel.notify(self.check) headers = {"User-Agent": "healthchecks.io"} mock_request.assert_called_with("get", "http://bar", headers=headers, timeout=5) @patch("hc.api.transports.requests.request") def test_webhooks_handle_post_headers(self, mock_request): definition = { "url_down": "http://foo.com", "post_data": "data", "headers": { "Content-Type": "application/json" } } self._setup_data("webhook", json.dumps(definition)) self.channel.notify(self.check) headers = { "User-Agent": "healthchecks.io", "Content-Type": "application/json" } mock_request.assert_called_with("post", "http://foo.com", data=b"data", headers=headers, timeout=5) @patch("hc.api.transports.requests.request") def test_webhooks_handle_get_headers(self, mock_request): definition = { "url_down": "http://foo.com", "headers": { "Content-Type": "application/json" } } self._setup_data("webhook", json.dumps(definition)) self.channel.notify(self.check) headers = { "User-Agent": "healthchecks.io", "Content-Type": "application/json" } mock_request.assert_called_with("get", "http://foo.com", headers=headers, timeout=5) @patch("hc.api.transports.requests.request") def test_webhooks_allow_user_agent_override(self, mock_request): definition = { "url_down": "http://foo.com", "headers": { "User-Agent": "My-Agent" } } self._setup_data("webhook", json.dumps(definition)) self.channel.notify(self.check) headers = {"User-Agent": "My-Agent"} mock_request.assert_called_with("get", "http://foo.com", headers=headers, timeout=5) @patch("hc.api.transports.requests.request") def test_webhooks_support_variables_in_headers(self, mock_request): definition = { "url_down": "http://foo.com", "headers": { "X-Message": "$NAME is DOWN" } } self._setup_data("webhook", json.dumps(definition)) self.check.name = "Foo" self.check.save() self.channel.notify(self.check) headers = {"User-Agent": "healthchecks.io", "X-Message": "Foo is DOWN"} mock_request.assert_called_with("get", "http://foo.com", headers=headers, timeout=5) def test_email(self): self._setup_data("email", "*****@*****.**") self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "") # And email should have been sent self.assertEqual(len(mail.outbox), 1) email = mail.outbox[0] self.assertTrue("X-Bounce-Url" in email.extra_headers) def test_it_skips_unverified_email(self): self._setup_data("email", "*****@*****.**", email_verified=False) self.channel.notify(self.check) # If an email is not verified, it should be skipped over # without logging a notification: self.assertEqual(Notification.objects.count(), 0) self.assertEqual(len(mail.outbox), 0) @patch("hc.api.transports.requests.request") def test_pd(self, mock_post): self._setup_data("pd", "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"] self.assertEqual(payload["event_type"], "trigger") self.assertEqual(payload["service_key"], "123") @patch("hc.api.transports.requests.request") def test_pd_complex(self, mock_post): self._setup_data("pd", json.dumps({"service_key": "456"})) 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"] self.assertEqual(payload["event_type"], "trigger") self.assertEqual(payload["service_key"], "456") @patch("hc.api.transports.requests.request") def test_pagertree(self, mock_post): self._setup_data("pagertree", "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"] self.assertEqual(payload["event_type"], "trigger") @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") mock_post.return_value.status_code = 204 self.channel.notify(self.check) n = Notification.objects.first() self.assertEqual(n.error, "") args, kwargs = mock_post.call_args payload = kwargs["json"] self.assertIn("DOWN", payload["message"]) @patch("hc.api.transports.requests.request") def test_opsgenie(self, mock_post): self._setup_data("opsgenie", "123") mock_post.return_value.status_code = 200 self.channel.notify(self.check) n = Notification.objects.first() self.assertEqual(n.error, "") args, kwargs = mock_post.call_args payload = kwargs["json"] self.assertIn("DOWN", payload["message"]) @patch("hc.api.transports.requests.request") def test_pushover(self, mock_post): self._setup_data("po", "123|0") 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["title"]) @patch("hc.api.transports.requests.request") def test_victorops(self, mock_post): self._setup_data("victorops", "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"] self.assertEqual(payload["message_type"], "CRITICAL") @patch("hc.api.transports.requests.request") def test_discord(self, mock_post): v = json.dumps({"webhook": {"url": "123"}}) self._setup_data("discord", v) 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_pushbullet(self, mock_post): self._setup_data("pushbullet", "fake-token") mock_post.return_value.status_code = 200 self.channel.notify(self.check) assert Notification.objects.count() == 1 _, kwargs = mock_post.call_args self.assertEqual(kwargs["json"]["type"], "note") self.assertEqual(kwargs["headers"]["Access-Token"], "fake-token") @patch("hc.api.transports.requests.request") def test_telegram(self, mock_post): v = json.dumps({"id": 123}) self._setup_data("telegram", v) 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"] self.assertEqual(payload["chat_id"], 123) self.assertTrue("The check" in payload["text"]) @patch("hc.api.transports.requests.request") def test_sms(self, mock_post): self._setup_data("sms", "+1234567890") self.check.last_ping = now() - td(hours=2) 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.assertEqual(payload["To"], "+1234567890") self.assertFalse(u"\xa0" in payload["Body"]) # sent SMS counter should go up self.profile.refresh_from_db() self.assertEqual(self.profile.sms_sent, 1) @patch("hc.api.transports.requests.request") def test_sms_limit(self, mock_post): # At limit already: self.profile.last_sms_date = now() self.profile.sms_sent = 50 self.profile.save() self._setup_data("sms", "+1234567890") self.channel.notify(self.check) self.assertFalse(mock_post.called) n = Notification.objects.get() self.assertTrue("Monthly SMS limit exceeded" in n.error) @patch("hc.api.transports.requests.request") def test_sms_limit_reset(self, mock_post): # At limit, but also into a new month self.profile.sms_sent = 50 self.profile.last_sms_date = now() - td(days=100) self.profile.save() self._setup_data("sms", "+1234567890") mock_post.return_value.status_code = 200 self.channel.notify(self.check) self.assertTrue(mock_post.called)
def add_email(request): if request.method == "POST": form = AddEmailForm(request.POST) if form.is_valid(): channel = Channel(project=request.project, kind="email") channel.value = json.dumps({ "value": form.cleaned_data["value"], "up": form.cleaned_data["up"], "down": form.cleaned_data["down"], }) channel.save() channel.assign_all_checks() is_own_email = form.cleaned_data["value"] == request.user.email if is_own_email or not settings.EMAIL_USE_VERIFICATION: # If user is subscribing *their own* address # we can skip the verification step. # Additionally, in self-hosted setting, administator has the # option to disable the email verification step altogether. channel.email_verified = True channel.save() else: channel.send_verify_link() return redirect("hc-channels") else: form = AddEmailForm() ctx = { "page": "channels", "project": request.project, "use_verification": settings.EMAIL_USE_VERIFICATION, "form": form, } return render(request, "integrations/add_email.html", ctx)
class NotifyPdTestCase(BaseTestCase): def _setup_data(self, value, status="down", email_verified=True): self.check = Check(project=self.project) self.check.name = "Foo" self.check.desc = "Description goes here" self.check.status = status self.check.last_ping = now() - td(minutes=61) self.check.save() self.channel = Channel(project=self.project) self.channel.kind = "pd" 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_it_works(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"] self.assertEqual(payload["description"], "Foo is DOWN") self.assertEqual(payload["details"]["Description"], "Description goes here") self.assertEqual(payload["event_type"], "trigger") self.assertEqual(payload["service_key"], "123") @patch("hc.api.transports.requests.request") def test_pd_complex(self, mock_post): self._setup_data(json.dumps({"service_key": "456"})) 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"] self.assertEqual(payload["event_type"], "trigger") self.assertEqual(payload["service_key"], "456") @override_settings(PD_ENABLED=False) def test_it_requires_pd_enabled(self): self._setup_data("123") self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "PagerDuty notifications are not enabled.") @patch("hc.api.transports.requests.request") def test_it_does_not_escape_description(self, mock_post): self._setup_data("123") self.check.name = "Foo & Bar" self.check.save() mock_post.return_value.status_code = 200 self.channel.notify(self.check) args, kwargs = mock_post.call_args payload = kwargs["json"] self.assertEqual(payload["description"], "Foo & Bar is DOWN")
class NotifyWebhookTestCase(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 = "webhook" 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_webhook(self, mock_get): definition = { "method_down": "GET", "url_down": "http://example", "body_down": "", "headers_down": {}, } self._setup_data(json.dumps(definition)) mock_get.return_value.status_code = 200 self.channel.notify(self.check) mock_get.assert_called_with( "get", "http://example", headers={"User-Agent": "healthchecks.io"}, timeout=10, ) @patch("hc.api.transports.requests.request", side_effect=Timeout) def test_webhooks_handle_timeouts(self, mock_get): definition = { "method_down": "GET", "url_down": "http://example", "body_down": "", "headers_down": {}, } self._setup_data(json.dumps(definition)) self.channel.notify(self.check) # The transport should have retried 3 times self.assertEqual(mock_get.call_count, 3) n = Notification.objects.get() self.assertEqual(n.error, "Connection timed out") self.channel.refresh_from_db() self.assertEqual(self.channel.last_error, "Connection timed out") @patch("hc.api.transports.requests.request", side_effect=ConnectionError) def test_webhooks_handle_connection_errors(self, mock_get): definition = { "method_down": "GET", "url_down": "http://example", "body_down": "", "headers_down": {}, } self._setup_data(json.dumps(definition)) self.channel.notify(self.check) # The transport should have retried 3 times self.assertEqual(mock_get.call_count, 3) n = Notification.objects.get() self.assertEqual(n.error, "Connection failed") @patch("hc.api.transports.requests.request") def test_webhooks_handle_500(self, mock_get): definition = { "method_down": "GET", "url_down": "http://example", "body_down": "", "headers_down": {}, } self._setup_data(json.dumps(definition)) mock_get.return_value.status_code = 500 self.channel.notify(self.check) # The transport should have retried 3 times self.assertEqual(mock_get.call_count, 3) n = Notification.objects.get() self.assertEqual(n.error, "Received status code 500") @patch("hc.api.transports.requests.request", side_effect=Timeout) def test_webhooks_dont_retry_when_sending_test_notifications(self, mock_get): definition = { "method_down": "GET", "url_down": "http://example", "body_down": "", "headers_down": {}, } self._setup_data(json.dumps(definition)) self.channel.notify(self.check, is_test=True) # is_test flag is set, the transport should not retry: self.assertEqual(mock_get.call_count, 1) n = Notification.objects.get() self.assertEqual(n.error, "Connection timed out") @patch("hc.api.transports.requests.request") def test_webhooks_support_variables(self, mock_get): definition = { "method_down": "GET", "url_down": "http://host/$CODE/$STATUS/$TAG1/$TAG2/?name=$NAME", "body_down": "", "headers_down": {}, } self._setup_data(json.dumps(definition)) self.check.name = "Hello World" self.check.tags = "foo bar" self.check.save() self.channel.notify(self.check) url = "http://host/%s/down/foo/bar/?name=Hello%%20World" % self.check.code args, kwargs = mock_get.call_args self.assertEqual(args[0], "get") self.assertEqual(args[1], url) self.assertEqual(kwargs["headers"], {"User-Agent": "healthchecks.io"}) self.assertEqual(kwargs["timeout"], 10) @patch("hc.api.transports.requests.request") def test_webhooks_handle_variable_variables(self, mock_get): definition = { "method_down": "GET", "url_down": "http://host/$$NAMETAG1", "body_down": "", "headers_down": {}, } self._setup_data(json.dumps(definition)) self.check.tags = "foo bar" self.check.save() self.channel.notify(self.check) # $$NAMETAG1 should *not* get transformed to "foo" args, kwargs = mock_get.call_args self.assertEqual(args[1], "http://host/$TAG1") @patch("hc.api.transports.requests.request") def test_webhooks_support_post(self, mock_request): definition = { "method_down": "POST", "url_down": "http://example.com", "body_down": "The Time Is $NOW", "headers_down": {}, } self._setup_data(json.dumps(definition)) self.check.save() self.channel.notify(self.check) args, kwargs = mock_request.call_args self.assertEqual(args[0], "post") self.assertEqual(args[1], "http://example.com") # spaces should not have been urlencoded: payload = kwargs["data"].decode() self.assertTrue(payload.startswith("The Time Is 2")) @patch("hc.api.transports.requests.request") def test_webhooks_dollarsign_escaping(self, mock_get): # If name or tag contains what looks like a variable reference, # that should be left alone: definition = { "method_down": "GET", "url_down": "http://host/$NAME", "body_down": "", "headers_down": {}, } self._setup_data(json.dumps(definition)) self.check.name = "$TAG1" self.check.tags = "foo" self.check.save() self.channel.notify(self.check) url = "http://host/%24TAG1" mock_get.assert_called_with( "get", url, headers={"User-Agent": "healthchecks.io"}, timeout=10 ) @patch("hc.api.transports.requests.request") def test_webhooks_handle_up_events(self, mock_get): definition = { "method_up": "GET", "url_up": "http://bar", "body_up": "", "headers_up": {}, } self._setup_data(json.dumps(definition), status="up") self.channel.notify(self.check) mock_get.assert_called_with( "get", "http://bar", headers={"User-Agent": "healthchecks.io"}, timeout=10 ) @patch("hc.api.transports.requests.request") def test_webhooks_handle_noop_up_events(self, mock_get): definition = { "method_up": "GET", "url_up": "", "body_up": "", "headers_up": {}, } self._setup_data(json.dumps(definition), status="up") self.channel.notify(self.check) self.assertFalse(mock_get.called) self.assertEqual(Notification.objects.count(), 0) @patch("hc.api.transports.requests.request") def test_webhooks_handle_unicode_post_body(self, mock_request): definition = { "method_down": "POST", "url_down": "http://foo.com", "body_down": "(╯°□°)╯︵ ┻━┻", "headers_down": {}, } self._setup_data(json.dumps(definition)) self.check.save() self.channel.notify(self.check) args, kwargs = mock_request.call_args # unicode should be encoded into utf-8 self.assertIsInstance(kwargs["data"], bytes) @patch("hc.api.transports.requests.request") def test_webhooks_handle_post_headers(self, mock_request): definition = { "method_down": "POST", "url_down": "http://foo.com", "body_down": "data", "headers_down": {"Content-Type": "application/json"}, } self._setup_data(json.dumps(definition)) self.channel.notify(self.check) headers = {"User-Agent": "healthchecks.io", "Content-Type": "application/json"} mock_request.assert_called_with( "post", "http://foo.com", data=b"data", headers=headers, timeout=10 ) @patch("hc.api.transports.requests.request") def test_webhooks_handle_get_headers(self, mock_request): definition = { "method_down": "GET", "url_down": "http://foo.com", "body_down": "", "headers_down": {"Content-Type": "application/json"}, } self._setup_data(json.dumps(definition)) self.channel.notify(self.check) headers = {"User-Agent": "healthchecks.io", "Content-Type": "application/json"} mock_request.assert_called_with( "get", "http://foo.com", headers=headers, timeout=10 ) @patch("hc.api.transports.requests.request") def test_webhooks_allow_user_agent_override(self, mock_request): definition = { "method_down": "GET", "url_down": "http://foo.com", "body_down": "", "headers_down": {"User-Agent": "My-Agent"}, } self._setup_data(json.dumps(definition)) self.channel.notify(self.check) headers = {"User-Agent": "My-Agent"} mock_request.assert_called_with( "get", "http://foo.com", headers=headers, timeout=10 ) @patch("hc.api.transports.requests.request") def test_webhooks_support_variables_in_headers(self, mock_request): definition = { "method_down": "GET", "url_down": "http://foo.com", "body_down": "", "headers_down": {"X-Message": "$NAME is DOWN"}, } self._setup_data(json.dumps(definition)) self.check.name = "Foo" self.check.save() self.channel.notify(self.check) headers = {"User-Agent": "healthchecks.io", "X-Message": "Foo is DOWN"} mock_request.assert_called_with( "get", "http://foo.com", headers=headers, timeout=10 ) @override_settings(WEBHOOKS_ENABLED=False) def test_it_requires_webhooks_enabled(self): definition = { "method_down": "GET", "url_down": "http://example", "body_down": "", "headers_down": {}, } self._setup_data(json.dumps(definition)) self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "Webhook notifications are not enabled.") @patch("hc.api.transports.requests.request") def test_webhooks_handle_non_ascii_in_headers(self, mock_request): definition = { "method_down": "GET", "url_down": "http://foo.com", "headers_down": {"X-Foo": "bār"}, "body_down": "", } self._setup_data(json.dumps(definition)) self.check.save() self.channel.notify(self.check) args, kwargs = mock_request.call_args self.assertEqual(kwargs["headers"]["X-Foo"], "bār") @patch("hc.api.transports.requests.request") def test_webhooks_handle_latin1_in_headers(self, mock_request): definition = { "method_down": "GET", "url_down": "http://foo.com", "headers_down": {"X-Foo": "½"}, "body_down": "", } self._setup_data(json.dumps(definition)) self.check.save() self.channel.notify(self.check) args, kwargs = mock_request.call_args self.assertEqual(kwargs["headers"]["X-Foo"], "½")
def setUp(self): super(UpdateChannelNameTestCase, self).setUp() self.channel = Channel(kind="email", project=self.project) self.channel.save() self.url = "/integrations/%s/name/" % self.channel.code
class NotifyTestCase(BaseTestCase): def _setup_data(self, kind, value, status="down", email_verified=True): self.check = Check() self.check.status = status self.check.user = self.alice self.check.save() self.channel = Channel(user=self.alice) 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_webhook(self, mock_get): self._setup_data("webhook", "http://example") mock_get.return_value.status_code = 200 self.channel.notify(self.check) mock_get.assert_called_with( "get", u"http://example", headers={"User-Agent": "healthchecks.io"}, timeout=5) @patch("hc.api.transports.requests.request", side_effect=Timeout) def test_webhooks_handle_timeouts(self, mock_get): self._setup_data("webhook", "http://example") self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "Connection timed out") @patch("hc.api.transports.requests.request") def test_webhooks_ignore_up_events(self, mock_get): self._setup_data("webhook", "http://example", status="up") self.channel.notify(self.check) self.assertFalse(mock_get.called) self.assertEqual(Notification.objects.count(), 0) @patch("hc.api.transports.requests.request") def test_webhooks_support_variables(self, mock_get): template = "http://host/$CODE/$STATUS/$TAG1/$TAG2/?name=$NAME" self._setup_data("webhook", template) self.check.name = "Hello World" self.check.tags = "foo bar" self.check.save() self.channel.notify(self.check) url = u"http://host/%s/down/foo/bar/?name=Hello%%20World" \ % self.check.code mock_get.assert_called_with( "get", url, headers={"User-Agent": "healthchecks.io"}, timeout=5) @patch("hc.api.transports.requests.request") def test_webhooks_dollarsign_escaping(self, mock_get): # If name or tag contains what looks like a variable reference, # that should be left alone: template = "http://host/$NAME" self._setup_data("webhook", template) self.check.name = "$TAG1" self.check.tags = "foo" self.check.save() self.channel.notify(self.check) url = u"http://host/%24TAG1" mock_get.assert_called_with( "get", url, headers={"User-Agent": "healthchecks.io"}, timeout=5) @patch("hc.api.transports.requests.request") def test_webhook_fires_on_up_event(self, mock_get): self._setup_data("webhook", "http://foo\nhttp://bar", status="up") self.channel.notify(self.check) mock_get.assert_called_with( "get", "http://bar", headers={"User-Agent": "healthchecks.io"}, timeout=5) def test_email(self): self._setup_data("email", "*****@*****.**") self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "") # And email should have been sent self.assertEqual(len(mail.outbox), 1) def test_it_skips_unverified_email(self): self._setup_data("email", "*****@*****.**", email_verified=False) self.channel.notify(self.check) assert Notification.objects.count() == 1 n = Notification.objects.first() self.assertEqual(n.error, "Email not verified") self.assertEqual(len(mail.outbox), 0) @override_settings(USE_PAYMENTS=True) def test_email_contains_upgrade_notice(self): self._setup_data("email", "*****@*****.**", status="up") self.profile.team_access_allowed = False self.profile.save() self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "") # Check is up, payments are enabled, and the user does not have team # access: the email should contain upgrade note message = mail.outbox[0] html, _ = message.alternatives[0] assert "/pricing/" in html @patch("hc.api.transports.requests.request") def test_pd(self, mock_post): self._setup_data("pd", "123") mock_post.return_value.status_code = 200 self.channel.notify(self.check) assert Notification.objects.count() == 1 args, kwargs = mock_post.call_args json = kwargs["json"] self.assertEqual(json["event_type"], "trigger") @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 json = kwargs["json"] attachment = json["attachments"][0] fields = {f["title"]: f["value"] for f in attachment["fields"]} self.assertEqual(fields["Last Ping"], "Never") @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_hipchat(self, mock_post): self._setup_data("hipchat", "123") mock_post.return_value.status_code = 204 self.channel.notify(self.check) n = Notification.objects.first() self.assertEqual(n.error, "") args, kwargs = mock_post.call_args json = kwargs["json"] self.assertIn("DOWN", json["message"]) @patch("hc.api.transports.requests.request") def test_pushover(self, mock_post): self._setup_data("po", "123|0") mock_post.return_value.status_code = 200 self.channel.notify(self.check) assert Notification.objects.count() == 1 args, kwargs = mock_post.call_args json = kwargs["data"] self.assertIn("DOWN", json["title"]) @patch("hc.api.transports.requests.request") def test_victorops(self, mock_post): self._setup_data("victorops", "123") mock_post.return_value.status_code = 200 self.channel.notify(self.check) assert Notification.objects.count() == 1 args, kwargs = mock_post.call_args json = kwargs["json"] self.assertEqual(json["message_type"], "CRITICAL") ### Test that the web hooks handle connection errors and error 500s @patch("hc.api.transports.requests.request", side_effect=ConnectionError) def test_webhook_connection_error(self, mock_post): self._setup_data("webhook", "http://example") mock_post.return_value.status_code = 500 self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "Connection failed")
class CreateCheckTestCase(BaseTestCase): URL = "/api/v1/checks/" def setUp(self): super(CreateCheckTestCase, self).setUp() def post(self, data, expected_error=None): r = self.client.post(self.URL, json.dumps(data), content_type="application/json") if expected_error: self.assertEqual(r.status_code, 400) ### Assert that the expected error is the response error self.assertEqual(r.json()['error'], expected_error) return r def test_it_works(self): r = self.post({ "api_key": "abc", "name": "Foo", "tags": "bar,baz", "timeout": 3600, "grace": 60 }) self.assertEqual(r.status_code, 201) doc = r.json() assert "ping_url" in doc self.assertEqual(doc["name"], "Foo") self.assertEqual(doc["tags"], "bar,baz") ### Assert the expected last_ping and n_pings values self.assertEqual(doc['n_pings'], 0) self.assertEqual(doc['last_ping'], None) self.assertEqual(Check.objects.count(), 1) check = Check.objects.get() self.assertEqual(check.name, "Foo") self.assertEqual(check.tags, "bar,baz") self.assertEqual(check.timeout.total_seconds(), 3600) self.assertEqual(check.grace.total_seconds(), 60) def test_it_accepts_api_key_in_header(self): payload = json.dumps({"name": "Foo"}) ### Make the post request and get the response # r = {'status_code': 201} ### This is just a placeholder variable r = self.client.post(self.URL, payload, content_type="application/json", HTTP_X_API_KEY="abc") self.assertEqual(r.status_code, 201) def test_it_handles_missing_request_body(self): ### Make the post request with a missing body and get the response # r = {'status_code': 400, 'error': "wrong api_key"} ### This is just a placeholder variable r = self.post({}) self.assertEqual(r.status_code, 400) self.assertEqual(r.json()['error'], "wrong api_key") def test_it_handles_invalid_json(self): ### Make the post request with invalid json data type # r = {'status_code': 400, 'error': "could not parse request body"} ### This is just a placeholder variable payload = {"name": "", "api_key": "abc"} #without json.dumps() r = self.client.post(self.URL, payload, content_type="application/json", HTTP_X_API_KEY="abc") self.assertEqual(r.status_code, 400) self.assertEqual(r.json()["error"], "could not parse request body") def test_it_rejects_wrong_api_key(self): self.post({"api_key": "wrong"}, expected_error="wrong api_key") def test_it_rejects_non_number_timeout(self): self.post({ "api_key": "abc", "timeout": "oops" }, expected_error="timeout is not a number") def test_it_rejects_non_string_name(self): self.post({ "api_key": "abc", "name": False }, expected_error="name is not a string") ### Test for the assignment of channels def test_assigns_channels(self): self.channel = Channel(user=self.alice) self.channel.kind = "webhook" self.channel.value = "http://example.com" self.channel.email_verified = True self.channel.save() r = self.post({ "api_key": "abc", "name": "Foo", "tags": "bar,baz", "timeout": 3600, "grace": 60, "channels": "*" }) check_channel = [ channel for channel in Check.objects.get().channel_set.all() ][0] self.assertEqual(check_channel.value, "http://example.com") ### Test for the 'timeout is too small' and 'timeout is too large' errors # test timeout is too small def test_timeout_is_too_small(self): r = self.post({ "api_key": "abc", "name": "Foo", "tags": "bar,baz", "timeout": 50, "grace": 60, "channels": "*" }) self.assertEqual(r.json()['error'], "timeout is too small") # test timeout is too large def test_timeout_is_too_large(self): r = self.post({ "api_key": "abc", "name": "Foo", "tags": "bar,baz", "timeout": 604801, "grace": 60, "channels": "*" }) self.assertEqual(r.json()['error'], "timeout is too large")
class NotifySignalTestCase(BaseTestCase): def setUp(self): super().setUp() self.check = Check(project=self.project) self.check.name = "Daily Backup" self.check.status = "down" self.check.last_ping = now() - td(minutes=61) self.check.save() payload = {"value": "+123456789", "up": True, "down": True} self.channel = Channel(project=self.project) self.channel.kind = "signal" self.channel.value = json.dumps(payload) self.channel.save() self.channel.checks.add(self.check) @patch("hc.api.transports.dbus") def test_it_works(self, mock_bus): self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "") args, kwargs = mock_bus.SystemBus.return_value.call_blocking.call_args message, attachments, recipients = args[-1] self.assertIn("is DOWN", message) self.assertEqual(recipients, ["+123456789"]) # Only one check in the project, so there should be no note about # other checks: self.assertNotIn("All the other checks are up.", message) @patch("hc.api.transports.dbus") def test_it_obeys_down_flag(self, mock_bus): payload = {"value": "+123456789", "up": True, "down": False} self.channel.value = json.dumps(payload) self.channel.save() self.channel.notify(self.check) # This channel should not notify on "down" events: self.assertEqual(Notification.objects.count(), 0) self.assertFalse(mock_bus.SystemBus.called) @patch("hc.api.transports.dbus") def test_it_requires_signal_cli_enabled(self, mock_bus): with override_settings(SIGNAL_CLI_ENABLED=False): self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "Signal notifications are not enabled") self.assertFalse(mock_bus.SystemBus.called) @patch("hc.api.transports.dbus") def test_it_does_not_escape_special_characters(self, mock_bus): self.check.name = "Foo & Bar" self.check.save() self.channel.notify(self.check) args, kwargs = mock_bus.SystemBus.return_value.call_blocking.call_args message, attachments, recipients = args[-1] self.assertIn("Foo & Bar", message) @override_settings(SECRET_KEY="test-secret") @patch("hc.api.transports.dbus") def test_it_obeys_rate_limit(self, mock_bus): # "2862..." is sha1("+123456789test-secret") obj = TokenBucket( value="signal-2862991ccaa15c8856e7ee0abaf3448fb3c292e0") obj.tokens = 0 obj.save() self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "Rate limit exceeded") self.assertFalse(mock_bus.SysemBus.called) @patch("hc.api.transports.dbus") def test_it_shows_all_other_checks_up_note(self, mock_bus): other = Check(project=self.project) other.name = "Foobar" other.status = "up" other.last_ping = now() - td(minutes=61) other.save() self.channel.notify(self.check) args, kwargs = mock_bus.SystemBus.return_value.call_blocking.call_args message, attachments, recipients = args[-1] self.assertIn("All the other checks are up.", message) @patch("hc.api.transports.dbus") def test_it_lists_other_down_checks(self, mock_bus): other = Check(project=self.project) other.name = "Foobar" other.status = "down" other.last_ping = now() - td(minutes=61) other.save() self.channel.notify(self.check) args, kwargs = mock_bus.SystemBus.return_value.call_blocking.call_args message, attachments, recipients = args[-1] self.assertIn("The following checks are also down", message) self.assertIn("Foobar", message) @patch("hc.api.transports.dbus") def test_it_does_not_show_more_than_10_other_checks(self, mock_bus): for i in range(0, 11): other = Check(project=self.project) other.name = f"Foobar #{i}" other.status = "down" other.last_ping = now() - td(minutes=61) other.save() self.channel.notify(self.check) args, kwargs = mock_bus.SystemBus.return_value.call_blocking.call_args message, attachments, recipients = args[-1] self.assertNotIn("Foobar", message) self.assertIn("11 other checks are also down.", message)
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_pagerteam(self, mock_post): self._setup_data("pagerteam", "123") self.channel.notify(self.check) self.assertFalse(mock_post.called) self.assertEqual(Notification.objects.count(), 0) @patch("hc.api.transports.requests.request") def test_hipchat(self, mock_post): self._setup_data("hipchat", "123") self.channel.notify(self.check) self.assertFalse(mock_post.called) self.assertEqual(Notification.objects.count(), 0) @patch("hc.api.transports.requests.request") def test_discord(self, mock_post): v = json.dumps({"webhook": {"url": "123"}}) self._setup_data("discord", v) 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_discord_rewrites_discordapp_com(self, mock_post): v = json.dumps({"webhook": {"url": "https://discordapp.com/foo"}}) self._setup_data("discord", v) mock_post.return_value.status_code = 200 self.channel.notify(self.check) assert Notification.objects.count() == 1 args, kwargs = mock_post.call_args url = args[1] # discordapp.com is deprecated. For existing webhook URLs, wwe should # rewrite discordapp.com to discord.com: self.assertEqual(url, "https://discord.com/foo/slack") @patch("hc.api.transports.requests.request") def test_pushbullet(self, mock_post): self._setup_data("pushbullet", "fake-token") mock_post.return_value.status_code = 200 self.channel.notify(self.check) assert Notification.objects.count() == 1 _, kwargs = mock_post.call_args self.assertEqual(kwargs["json"]["type"], "note") self.assertEqual(kwargs["headers"]["Access-Token"], "fake-token") @patch("hc.api.transports.requests.request") def test_call(self, mock_post): self.profile.call_limit = 1 self.profile.save() value = {"label": "foo", "value": "+1234567890"} self._setup_data("call", json.dumps(value)) self.check.last_ping = now() - td(hours=2) mock_post.return_value.status_code = 200 self.channel.notify(self.check) args, kwargs = mock_post.call_args payload = kwargs["data"] self.assertEqual(payload["To"], "+1234567890") n = Notification.objects.get() callback_path = f"/api/v1/notifications/{n.code}/status" self.assertTrue(payload["StatusCallback"].endswith(callback_path)) @patch("hc.api.transports.requests.request") def test_call_limit(self, mock_post): # At limit already: self.profile.call_limit = 50 self.profile.last_call_date = now() self.profile.calls_sent = 50 self.profile.save() definition = {"value": "+1234567890"} self._setup_data("call", json.dumps(definition)) self.channel.notify(self.check) self.assertFalse(mock_post.called) n = Notification.objects.get() self.assertTrue("Monthly phone call limit exceeded" in n.error) # And email should have been sent self.assertEqual(len(mail.outbox), 1) email = mail.outbox[0] self.assertEqual(email.to[0], "*****@*****.**") self.assertEqual(email.subject, "Monthly Phone Call Limit Reached") @patch("hc.api.transports.requests.request") def test_call_limit_reset(self, mock_post): # At limit, but also into a new month self.profile.call_limit = 50 self.profile.calls_sent = 50 self.profile.last_call_date = now() - td(days=100) self.profile.save() self._setup_data("call", "+1234567890") mock_post.return_value.status_code = 200 self.channel.notify(self.check) self.assertTrue(mock_post.called) def test_not_implimented(self): self._setup_data("webhook", "http://example") self.channel.kind = "invalid" with self.assertRaises(NotImplementedError): self.channel.notify(self.check) @patch("hc.api.transports.os.system") @override_settings(SHELL_ENABLED=True) def test_shell(self, mock_system): definition = {"cmd_down": "logger hello", "cmd_up": ""} self._setup_data("shell", json.dumps(definition)) mock_system.return_value = 0 self.channel.notify(self.check) mock_system.assert_called_with("logger hello") @patch("hc.api.transports.os.system") @override_settings(SHELL_ENABLED=True) def test_shell_handles_nonzero_exit_code(self, mock_system): definition = {"cmd_down": "logger hello", "cmd_up": ""} self._setup_data("shell", json.dumps(definition)) mock_system.return_value = 123 self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "Command returned exit code 123") @patch("hc.api.transports.os.system") @override_settings(SHELL_ENABLED=True) def test_shell_supports_variables(self, mock_system): definition = { "cmd_down": "logger $NAME is $STATUS ($TAG1)", "cmd_up": "" } self._setup_data("shell", json.dumps(definition)) mock_system.return_value = 0 self.check.name = "Database" self.check.tags = "foo bar" self.check.save() self.channel.notify(self.check) mock_system.assert_called_with("logger Database is down (foo)") @patch("hc.api.transports.os.system") @override_settings(SHELL_ENABLED=False) def test_shell_disabled(self, mock_system): definition = {"cmd_down": "logger hello", "cmd_up": ""} self._setup_data("shell", json.dumps(definition)) self.channel.notify(self.check) self.assertFalse(mock_system.called) n = Notification.objects.get() self.assertEqual(n.error, "Shell commands are not enabled")
class NotifyWhatsAppTestCase(BaseTestCase): def _setup_data(self, notify_up=True, notify_down=True): self.check = Check(project=self.project) self.check.status = "down" self.check.last_ping = now() - td(minutes=61) self.check.save() definition = { "value": "+1234567890", "up": notify_up, "down": notify_down } self.channel = Channel(project=self.project, kind="whatsapp") self.channel.value = json.dumps(definition) self.channel.save() self.channel.checks.add(self.check) @patch("hc.api.transports.requests.request") def test_it_works(self, mock_post): mock_post.return_value.status_code = 200 self._setup_data() self.channel.notify(self.check) args, kwargs = mock_post.call_args payload = kwargs["data"] self.assertEqual(payload["To"], "whatsapp:+1234567890") n = Notification.objects.get() callback_path = f"/api/v1/notifications/{n.code}/status" self.assertTrue(payload["StatusCallback"].endswith(callback_path)) # sent SMS counter should go up self.profile.refresh_from_db() self.assertEqual(self.profile.sms_sent, 1) @patch("hc.api.transports.requests.request") def test_it_obeys_up_down_flags(self, mock_post): self._setup_data(notify_down=False) self.check.last_ping = now() - td(hours=2) self.channel.notify(self.check) self.assertEqual(Notification.objects.count(), 0) self.assertFalse(mock_post.called) @patch("hc.api.transports.requests.request") def test_it_enforces_limit(self, mock_post): # At limit already: self.profile.last_sms_date = now() self.profile.sms_sent = 50 self.profile.save() self._setup_data() self.channel.notify(self.check) self.assertFalse(mock_post.called) n = Notification.objects.get() self.assertTrue("Monthly message limit exceeded" in n.error) # And email should have been sent self.assertEqual(len(mail.outbox), 1) email = mail.outbox[0] self.assertEqual(email.to[0], "*****@*****.**") self.assertEqual(email.subject, "Monthly WhatsApp Limit Reached") @patch("hc.api.transports.requests.request") def test_it_does_not_escape_special_characters(self, mock_post): self._setup_data() self.check.name = "Foo > Bar & Co" mock_post.return_value.status_code = 200 self.channel.notify(self.check) args, kwargs = mock_post.call_args payload = kwargs["data"] self.assertIn("Foo > Bar & Co", payload["Body"])