class NotifyMattermostTestCase(BaseTestCase): def setUp(self): super().setUp() self.check = Check(project=self.project) self.check.status = "down" self.check.last_ping = now() - td(minutes=61) self.check.save() self.channel = Channel(project=self.project) self.channel.kind = "mattermost" self.channel.value = "123" self.channel.save() self.channel.checks.add(self.check) @override_settings(MATTERMOST_ENABLED=False) def test_it_requires_mattermost_enabled(self): self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "Mattermost notifications are not enabled.") @patch("hc.api.transports.requests.request") def test_it_does_not_disable_channel_on_404(self, mock_post): mock_post.return_value.status_code = 404 self.channel.notify(self.check) self.channel.refresh_from_db() self.assertFalse(self.channel.disabled)
class NotifyLineTestCase(BaseTestCase): def setUp(self): super().setUp() self.check = Check(project=self.project) self.check.name = "Foo" self.check.status = "down" self.check.last_ping = now() - td(minutes=61) self.check.save() self.channel = Channel(project=self.project) self.channel.kind = "linenotify" self.channel.value = "fake-token" 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.channel.notify(self.check) assert Notification.objects.count() == 1 args, kwargs = mock_post.call_args headers = kwargs["headers"] params = kwargs["params"] self.assertEqual(headers["Authorization"], "Bearer fake-token") self.assertIn("""The check "Foo" is DOWN""", params["message"])
class NotifyPagertreeTestCase(BaseTestCase): def setUp(self): super().setUp() self.check = Check(project=self.project) self.check.status = "down" self.check.last_ping = now() - td(minutes=61) self.check.save() self.channel = Channel(project=self.project) self.channel.kind = "pagertree" self.channel.value = "123" self.channel.save() self.channel.checks.add(self.check) @patch("hc.api.transports.requests.request") def test_pagertree(self, mock_post): 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") @override_settings(PAGERTREE_ENABLED=False) def test_it_requires_pagertree_enabled(self): self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "PagerTree notifications are not enabled.")
class NotifyAppriseTestCase(BaseTestCase): def setUp(self): super().setUp() self.check = Check(project=self.project) self.check.status = "down" self.check.last_ping = now() - td(minutes=61) self.check.save() self.channel = Channel(project=self.project) self.channel.kind = "apprise" self.channel.value = "123" self.channel.save() self.channel.checks.add(self.check) @patch("apprise.Apprise") @override_settings(APPRISE_ENABLED=True) def test_apprise_enabled(self, mock_apprise): 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.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "Apprise is disabled and/or not installed")
class NotifyTrelloTestCase(BaseTestCase): def setUp(self): super().setUp() self.check = Check(project=self.project) self.check.name = "Foo" self.check.status = "down" self.check.last_ping = now() - td(minutes=61) self.check.save() self.channel = Channel(project=self.project) self.channel.kind = "trello" self.channel.value = json.dumps({ "token": "fake-token", "board_name": "My Board", "list_name": "My List", "list_id": "fake-list-id", }) 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.channel.notify(self.check) assert Notification.objects.count() == 1 args, kwargs = mock_post.call_args params = kwargs["params"] self.assertEqual(params["idList"], "fake-list-id") self.assertEqual(params["name"], "Down: Foo") self.assertIn("Full Details", params["desc"]) self.assertEqual(params["key"], "fake-trello-app-key") self.assertEqual(params["token"], "fake-token")
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 = "victorops" 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_victorops(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["message_type"], "CRITICAL") @override_settings(VICTOROPS_ENABLED=False) def test_it_requires_victorops_enabled(self): self._setup_data("123") self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "VictorOps notifications are not enabled.")
class NotifySpikeTestCase(BaseTestCase): def setUp(self): super().setUp() self.check = Check(project=self.project) self.check.name = "Foo" self.check.status = "down" self.check.last_ping = now() - td(minutes=61) self.check.save() self.channel = Channel(project=self.project) self.channel.kind = "spike" self.channel.value = "https://spike.example.org" 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.channel.notify(self.check) assert Notification.objects.count() == 1 args, kwargs = mock_post.call_args payload = kwargs["json"] self.assertEqual(payload["check_id"], str(self.check.code)) self.assertEqual(payload["title"], "Foo is DOWN") @override_settings(SPIKE_ENABLED=False) def test_it_requires_spike_enabled(self): self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "Spike notifications are not enabled.")
class NotifyConnectWiseManageTestCase(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 = "connectwisemanage" self.channel.value = value self.channel.email_verified = email_verified self.channel.save() self.channel.checks.add(self.check) @override_settings(CONNECTWISEMANAGE_ENABLED=False) def test_it_requires_connectwise_enabled(self): self._setup_data("123") self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "ConnectWise Manage ticketing is not enabled.") @override_settings(CONNECTWISEMANAGE_CLIENTID=None) def test_it_requires_connectwise_clientid(self): self._setup_data("123") self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "ConnectWise Manage Developer ClientID is not set.")
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") mock_get.return_value.status_code = 200 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") mock_post.return_value.status_code = 200 self.channel.notify(self.check) assert Notification.objects.count() == 1 args, kwargs = mock_post.call_args assert "trigger" in kwargs["data"]
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") mock_get.return_value.status_code = 200 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") mock_post.return_value.status_code = 200 self.channel.notify(self.check) assert Notification.objects.count() == 1 args, kwargs = mock_post.call_args assert "trigger" in kwargs["data"]
class NotifyPushbulletTestCase(BaseTestCase): def setUp(self): super().setUp() self.check = Check(project=self.project) self.check.name = "Foo" self.check.status = "up" self.check.last_ping = now() - td(minutes=61) self.check.save() self.channel = Channel(project=self.project) self.channel.kind = "pushbullet" self.channel.value = "fake-token" 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.channel.notify(self.check) assert Notification.objects.count() == 1 _, kwargs = mock_post.call_args self.assertEqual(kwargs["json"]["type"], "note") self.assertEqual( kwargs["json"]["body"], 'The check "Foo" received a ping and is now UP.' ) self.assertEqual(kwargs["headers"]["Access-Token"], "fake-token") @patch("hc.api.transports.requests.request") def test_it_escapes_body(self, mock_post): mock_post.return_value.status_code = 200 self.check.name = "Foo & Bar" self.check.save() self.channel.notify(self.check) _, kwargs = mock_post.call_args self.assertEqual( kwargs["json"]["body"], 'The check "Foo & Bar" received a ping and is now UP.', )
class NotifyMattermostTestCase(BaseTestCase): def setUp(self): super().setUp() self.check = Check(project=self.project) self.check.status = "down" self.check.last_ping = now() - td(minutes=61) self.check.save() self.channel = Channel(project=self.project) self.channel.kind = "mattermost" self.channel.value = "123" self.channel.save() self.channel.checks.add(self.check) @override_settings(MATTERMOST_ENABLED=False) def test_it_requires_mattermost_enabled(self): self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "Mattermost 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 = "apprise" self.channel.value = value self.channel.email_verified = email_verified self.channel.save() self.channel.checks.add(self.check) @patch("apprise.Apprise") @override_settings(APPRISE_ENABLED=True) def test_apprise_enabled(self, mock_apprise): self._setup_data("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("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)
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 = "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_pushover(self, mock_post): self._setup_data("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("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) @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.first() self.assertEqual(n.error, "Rate limit exceeded")
class NotifyVictorOpsTestCase(BaseTestCase): def setUp(self): super().setUp() self.check = Check(project=self.project) self.check.status = "down" self.check.last_ping = now() - td(minutes=61) self.check.save() self.channel = Channel(project=self.project) self.channel.kind = "victorops" self.channel.value = "123" 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.channel.notify(self.check) assert Notification.objects.count() == 1 args, kwargs = mock_post.call_args payload = kwargs["json"] self.assertEqual(payload["message_type"], "CRITICAL") @override_settings(VICTOROPS_ENABLED=False) def test_it_requires_victorops_enabled(self): self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "Splunk On-Call notifications are not enabled.") @patch("hc.api.transports.requests.request") def test_it_does_not_escape_description(self, mock_post): mock_post.return_value.status_code = 200 self.check.name = "Foo & Bar" self.check.status = "up" self.check.save() self.channel.notify(self.check) args, kwargs = mock_post.call_args payload = kwargs["json"] self.assertEqual(payload["state_message"], "Foo & Bar received a ping and is now UP")
class NotifyPdTestCase(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 = "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_pd(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["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.")
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_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_telegram_returns_error(self, mock_post): self._setup_data("telegram", json.dumps({"id": 123})) mock_post.return_value.status_code = 400 mock_post.return_value.json.return_value = {"description": "Hi"} self.channel.notify(self.check) n = Notification.objects.first() self.assertEqual(n.error, 'Received status code 400 with a message: "Hi"') def test_telegram_obeys_rate_limit(self): self._setup_data("telegram", json.dumps({"id": 123})) TokenBucket.objects.create(value="tg-123", tokens=0) self.channel.notify(self.check) n = Notification.objects.first() self.assertEqual(n.error, "Rate limit exceeded") @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.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.calls_sent = 50 self.profile.last_call_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 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 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"]) @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.first() self.assertEqual(n.error, "Rate limit exceeded") self.assertFalse(mock_bus.SysemBus.called)
class NotifyTestCase(BaseTestCase): def _setup_data(self, value): self.check = Check(project=self.project) self.check.status = "down" self.check.last_ping = now() - td(minutes=61) self.check.save() self.channel = Channel(project=self.project, kind="sms") self.channel.value = value 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("+1234567890") 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") self.assertFalse("\xa0" in payload["Body"]) 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_handles_json_value(self, mock_post): value = {"label": "foo", "value": "+1234567890"} self._setup_data(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_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("+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_it_resets_limit_next_month(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("+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_it_does_not_escape_special_characters(self, mock_post): self._setup_data("+1234567890") self.check.name = "Foo > Bar & Co" 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.assertIn("Foo > Bar & Co", payload["Body"])
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", side_effect=ContentDecodingError) def test_webhooks_handle_content_decoding_error(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, "Failed to decode response") @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"], "½")
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: self.assertTrue(kwargs["data"].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) 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) assert Notification.objects.count() == 1 n = Notification.objects.first() self.assertEqual(n.error, "Email not verified") 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") @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") 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") # 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)
class NotifyEmailTestCase(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, 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): definition = { "method_down": "GET", "url_down": "http://example", "body_down": "", "headers_down": {}, } self._setup_data("webhook", 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=5, ) @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("webhook", json.dumps(definition)) self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "Connection timed out") 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("webhook", json.dumps(definition)) self.channel.notify(self.check) 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("webhook", json.dumps(definition)) 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): definition = { "method_down": "GET", "url_down": "http://host/$CODE/$STATUS/$TAG1/$TAG2/?name=$NAME", "body_down": "", "headers_down": {}, } self._setup_data("webhook", 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"], 5) @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("webhook", 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("webhook", 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("webhook", 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=5) @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("webhook", json.dumps(definition), 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_noop_up_events(self, mock_get): definition = { "method_up": "GET", "url_up": "", "body_up": "", "headers_up": {}, } self._setup_data("webhook", 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("webhook", 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("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_reports_unverified_email(self): self._setup_data("email", "*****@*****.**", email_verified=False) 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._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) def test_email_handles_amperstand(self): self._setup_data("email", "*****@*****.**") self.check.name = "Foo & Bar" self.channel.notify(self.check) email = mail.outbox[0] self.assertEqual(email.subject, "DOWN | Foo & Bar") @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") self.channel.notify(self.check) self.assertFalse(mock_post.called) self.assertEqual(Notification.objects.count(), 0) @patch("hc.api.transports.requests.request") def test_slack(self, mock_post): self._setup_data("slack", "123") mock_post.return_value.status_code = 200 self.channel.notify(self.check) assert Notification.objects.count() == 1 args, kwargs = mock_post.call_args payload = kwargs["json"] attachment = payload["attachments"][0] fields = {f["title"]: f["value"] for f in attachment["fields"]} self.assertEqual(fields["Last Ping"], "an hour ago") @patch("hc.api.transports.requests.request") def test_slack_with_complex_value(self, mock_post): v = json.dumps({"incoming_webhook": {"url": "123"}}) self._setup_data("slack", v) mock_post.return_value.status_code = 200 self.channel.notify(self.check) assert Notification.objects.count() == 1 args, kwargs = mock_post.call_args self.assertEqual(args[1], "123") @patch("hc.api.transports.requests.request") def test_slack_handles_500(self, mock_post): self._setup_data("slack", "123") mock_post.return_value.status_code = 500 self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "Received status code 500") @patch("hc.api.transports.requests.request", side_effect=Timeout) def test_slack_handles_timeout(self, mock_post): self._setup_data("slack", "123") self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "Connection timed out") @patch("hc.api.transports.requests.request") def test_slack_with_tabs_in_schedule(self, mock_post): self._setup_data("slack", "123") self.check.kind = "cron" self.check.schedule = "*\t* * * *" self.check.save() mock_post.return_value.status_code = 200 self.channel.notify(self.check) self.assertEqual(Notification.objects.count(), 1) self.assertTrue(mock_post.called) @patch("hc.api.transports.requests.request") def test_hipchat(self, mock_post): self._setup_data("hipchat", "123") 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_opsgenie_returns_error(self, mock_post): self._setup_data("opsgenie", "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"') @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_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_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_telegram_returns_error(self, mock_post): self._setup_data("telegram", json.dumps({"id": 123})) mock_post.return_value.status_code = 400 mock_post.return_value.json.return_value = {"description": "Hi"} self.channel.notify(self.check) n = Notification.objects.first() self.assertEqual(n.error, 'Received status code 400 with a message: "Hi"') def test_telegram_obeys_rate_limit(self): self._setup_data("telegram", json.dumps({"id": 123})) TokenBucket.objects.create(value="tg-123", tokens=0) self.channel.notify(self.check) n = Notification.objects.first() self.assertEqual(n.error, "Rate limit exceeded") @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") @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("zulip", json.dumps(definition)) mock_post.return_value.status_code = 200 self.channel.notify(self.check) assert Notification.objects.count() == 1 args, kwargs = mock_post.call_args payload = kwargs["data"] self.assertIn("DOWN", payload["topic"]) @patch("hc.api.transports.requests.request") def test_zulip_returns_error(self, mock_post): definition = { "bot_email": "*****@*****.**", "api_key": "fake-key", "mtype": "stream", "to": "general", } self._setup_data("zulip", json.dumps(definition)) mock_post.return_value.status_code = 403 mock_post.return_value.json.return_value = {"msg": "Nice try"} self.channel.notify(self.check) n = Notification.objects.first() self.assertEqual( n.error, 'Received status code 403 with a message: "Nice try"')
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): definition = { "method_down": "GET", "url_down": "http://example", "body_down": "", "headers_down": {}, } self._setup_data("webhook", 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=5, ) @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("webhook", 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("webhook", 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("webhook", 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("webhook", 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("webhook", 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"], 5) @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("webhook", 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("webhook", 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("webhook", 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=5) @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("webhook", json.dumps(definition), 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_noop_up_events(self, mock_get): definition = { "method_up": "GET", "url_up": "", "body_up": "", "headers_up": {}, } self._setup_data("webhook", 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("webhook", 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("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) @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") self.channel.notify(self.check) self.assertFalse(mock_post.called) self.assertEqual(Notification.objects.count(), 0) @patch("hc.api.transports.requests.request") def test_slack(self, mock_post): self._setup_data("slack", "123") mock_post.return_value.status_code = 200 self.channel.notify(self.check) assert Notification.objects.count() == 1 args, kwargs = mock_post.call_args payload = kwargs["json"] attachment = payload["attachments"][0] fields = {f["title"]: f["value"] for f in attachment["fields"]} self.assertEqual(fields["Last Ping"], "an hour ago") @patch("hc.api.transports.requests.request") def test_slack_with_complex_value(self, mock_post): v = json.dumps({"incoming_webhook": {"url": "123"}}) self._setup_data("slack", v) mock_post.return_value.status_code = 200 self.channel.notify(self.check) assert Notification.objects.count() == 1 args, kwargs = mock_post.call_args self.assertEqual(args[1], "123") @patch("hc.api.transports.requests.request") def test_slack_handles_500(self, mock_post): self._setup_data("slack", "123") mock_post.return_value.status_code = 500 self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "Received status code 500") @patch("hc.api.transports.requests.request", side_effect=Timeout) def test_slack_handles_timeout(self, mock_post): self._setup_data("slack", "123") self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "Connection timed out") @patch("hc.api.transports.requests.request") def test_slack_with_tabs_in_schedule(self, mock_post): self._setup_data("slack", "123") self.check.kind = "cron" self.check.schedule = "*\t* * * *" self.check.save() mock_post.return_value.status_code = 200 self.channel.notify(self.check) self.assertEqual(Notification.objects.count(), 1) self.assertTrue(mock_post.called) @patch("hc.api.transports.requests.request") def test_hipchat(self, mock_post): self._setup_data("hipchat", "123") 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_opsgenie_returns_error(self, mock_post): self._setup_data("opsgenie", "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"') @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_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_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_telegram_returns_error(self, mock_post): self._setup_data("telegram", json.dumps({"id": 123})) mock_post.return_value.status_code = 400 mock_post.return_value.json.return_value = {"description": "Hi"} self.channel.notify(self.check) n = Notification.objects.first() self.assertEqual(n.error, 'Received status code 400 with a message: "Hi"') def test_telegram_obeys_rate_limit(self): self._setup_data("telegram", json.dumps({"id": 123})) TokenBucket.objects.create(value="tg-123", tokens=0) self.channel.notify(self.check) n = Notification.objects.first() self.assertEqual(n.error, "Rate limit exceeded") @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.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.calls_sent = 50 self.profile.last_call_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 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.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.") @patch("hc.api.transports.requests.request") def test_msteams_escapes_html_and_markdown_in_desc(self, mock_post): self._setup_data("msteams", "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) @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 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) @patch("hc.api.transports.requests.request") def test_zendesk_down(self, mock_post): v = json.dumps({"access_token": "fake-token", "subdomain": "foo"}) self._setup_data("zendesk", v) 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(method, "post") self.assertTrue("foo.zendesk.com" in url) payload = kwargs["json"] self.assertEqual(payload["request"]["type"], "incident") self.assertTrue("down" in payload["request"]["subject"]) headers = kwargs["headers"] self.assertEqual(headers["Authorization"], "Bearer fake-token") @patch("hc.api.transports.requests.request") @patch("hc.api.transports.requests.get") def test_zendesk_up(self, mock_get, mock_post): v = json.dumps({"access_token": "fake-token", "subdomain": "foo"}) self._setup_data("zendesk", v, status="up") mock_post.return_value.status_code = 200 mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = { "requests": [{ "url": "https://foo.example.org/comment", "description": "code is %s" % self.check.code }] } self.channel.notify(self.check) assert Notification.objects.count() == 1 args, kwargs = mock_post.call_args self.assertTrue("foo.example.org" in args[1]) payload = kwargs["json"] self.assertEqual(payload["request"]["type"], "incident") self.assertTrue("UP" in payload["request"]["subject"]) headers = kwargs["headers"] self.assertEqual(headers["Authorization"], "Bearer fake-token") @patch("hc.api.transports.requests.request") @patch("hc.api.transports.requests.get") def test_zendesk_up_with_no_existing_ticket(self, mock_get, mock_post): v = json.dumps({"access_token": "fake-token", "subdomain": "foo"}) self._setup_data("zendesk", v, status="up") mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = {"requests": []} self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "Could not find a ticket to update") self.assertFalse(mock_post.called)
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", 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 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_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 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")
class NotifyPushoverTestCase(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 = "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.assertIn("DOWN", payload["title"]) 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"])
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 = 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 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_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_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) @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_zendesk_down(self, mock_post): v = json.dumps({"access_token": "fake-token", "subdomain": "foo"}) self._setup_data("zendesk", v) 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(method, "post") self.assertTrue("foo.zendesk.com" in url) payload = kwargs["json"] self.assertEqual(payload["request"]["type"], "incident") self.assertTrue("down" in payload["request"]["subject"]) headers = kwargs["headers"] self.assertEqual(headers["Authorization"], "Bearer fake-token") @patch("hc.api.transports.requests.request") @patch("hc.api.transports.requests.get") def test_zendesk_up(self, mock_get, mock_post): v = json.dumps({"access_token": "fake-token", "subdomain": "foo"}) self._setup_data("zendesk", v, status="up") mock_post.return_value.status_code = 200 mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = { "requests": [{ "url": "https://foo.example.org/comment", "description": "code is %s" % self.check.code }] } self.channel.notify(self.check) assert Notification.objects.count() == 1 args, kwargs = mock_post.call_args self.assertTrue("foo.example.org" in args[1]) payload = kwargs["json"] self.assertEqual(payload["request"]["type"], "incident") self.assertTrue("UP" in payload["request"]["subject"]) headers = kwargs["headers"] self.assertEqual(headers["Authorization"], "Bearer fake-token") @patch("hc.api.transports.requests.request") @patch("hc.api.transports.requests.get") def test_zendesk_up_with_no_existing_ticket(self, mock_get, mock_post): v = json.dumps({"access_token": "fake-token", "subdomain": "foo"}) self._setup_data("zendesk", v, status="up") mock_get.return_value.status_code = 200 mock_get.return_value.json.return_value = {"requests": []} self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "Could not find a ticket to update") self.assertFalse(mock_post.called)
class NotifyTestCase(BaseTestCase): 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) @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") 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) @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_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")
class NotifyTestCase(BaseTestCase): def _setup_data(self, value, status="down", email_verified=True): self.check = Check(project=self.project) self.check.status = status self.check.last_ping = now() - td(minutes=61) self.check.save() self.channel = Channel(project=self.project) self.channel.kind = "slack" self.channel.value = value self.channel.email_verified = email_verified self.channel.save() self.channel.checks.add(self.check) @patch("hc.api.transports.requests.request") def test_slack(self, mock_post): self._setup_data("123") mock_post.return_value.status_code = 200 self.channel.notify(self.check) assert Notification.objects.count() == 1 args, kwargs = mock_post.call_args payload = kwargs["json"] attachment = payload["attachments"][0] fields = {f["title"]: f["value"] for f in attachment["fields"]} self.assertEqual(fields["Last Ping"], "an hour ago") uncloak_url = "/cloaked/%s/" % self.check.unique_key self.assertTrue(attachment["title_link"].endswith(uncloak_url)) @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 NotifyZulipTestCase(BaseTestCase): def setUp(self): super().setUp() self.check = Check(project=self.project) self.check.name = "Foobar" self.check.status = "down" self.check.last_ping = now() - td(minutes=61) self.check.save() definition = { "bot_email": "*****@*****.**", "api_key": "fake-key", "mtype": "stream", "to": "general", } self.channel = Channel(project=self.project) self.channel.kind = "zulip" self.channel.value = json.dumps(definition) self.channel.save() self.channel.checks.add(self.check) @patch("hc.api.transports.requests.request") def test_it_works(self, mock_post): 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"]) # 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_it_returns_error(self, mock_post): 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.get() self.assertEqual(n.error, 'Received status code 403 with a message: "Nice try"') @patch("hc.api.transports.requests.request") def test_it_handles_non_json_error_response(self, mock_post): mock_post.return_value.status_code = 403 mock_post.return_value.json = Mock(side_effect=ValueError) self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "Received status code 403") @patch("hc.api.transports.requests.request") def test_it_uses_site_parameter(self, mock_post): mock_post.return_value.status_code = 200 definition = { "bot_email": "*****@*****.**", "site": "https://custom.example.org", "api_key": "fake-key", "mtype": "stream", "to": "general", } self.channel.value = json.dumps(definition) 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): self.channel.notify(self.check) n = Notification.objects.get() self.assertEqual(n.error, "Zulip notifications are not enabled.")
class NotifyTestCase(BaseTestCase): def _setup_data(self, kind, value, status="down", email_verified=True): self.check = Check(project=self.project) self.check.status = status self.check.last_ping = now() - td(minutes=61) self.check.save() self.channel = Channel(project=self.project) self.channel.kind = kind self.channel.value = value self.channel.email_verified = email_verified self.channel.save() self.channel.checks.add(self.check) @patch("hc.api.transports.requests.request") def test_zulip(self, mock_post): definition = { "bot_email": "*****@*****.**", "api_key": "fake-key", "mtype": "stream", "to": "general", } self._setup_data("zulip", json.dumps(definition)) mock_post.return_value.status_code = 200 self.channel.notify(self.check) assert Notification.objects.count() == 1 args, kwargs = mock_post.call_args method, url = args self.assertEqual(url, "https://example.org/api/v1/messages") payload = kwargs["data"] self.assertIn("DOWN", payload["topic"]) @patch("hc.api.transports.requests.request") def test_zulip_returns_error(self, mock_post): definition = { "bot_email": "*****@*****.**", "api_key": "fake-key", "mtype": "stream", "to": "general", } self._setup_data("zulip", json.dumps(definition)) mock_post.return_value.status_code = 403 mock_post.return_value.json.return_value = {"msg": "Nice try"} self.channel.notify(self.check) n = Notification.objects.first() self.assertEqual( n.error, 'Received status code 403 with a message: "Nice try"') @patch("hc.api.transports.requests.request") def test_zulip_uses_site_parameter(self, mock_post): definition = { "bot_email": "*****@*****.**", "site": "https://custom.example.org", "api_key": "fake-key", "mtype": "stream", "to": "general", } self._setup_data("zulip", json.dumps(definition)) mock_post.return_value.status_code = 200 self.channel.notify(self.check) assert Notification.objects.count() == 1 args, kwargs = mock_post.call_args method, url = args self.assertEqual(url, "https://custom.example.org/api/v1/messages") payload = kwargs["data"] self.assertIn("DOWN", payload["topic"])
class NotifyTestCase(BaseTestCase): def _setup_data(self, channel_kind, channel_value, email_verified=True): 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") mock_get.return_value.status_code = 200 self.channel.notify(self.check) mock_get.assert_called_with( u"http://example", headers={"User-Agent": "healthchecks.io"}, 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") mock_post.return_value.status_code = 200 self.channel.notify(self.check) assert Notification.objects.count() == 1 args, kwargs = mock_post.call_args assert "trigger" in kwargs["data"] @patch("hc.api.models.requests.post") 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")
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") def test_webhook(self, mock_get): self._setup_data("webhook", "http://example") mock_get.return_value.status_code = 500 self.channel.notify(self.check) all_notifications = Notification.objects.get() self.assertEqual(all_notifications.error, 'Received status code 500') @patch("hc.api.transports.requests.request", side_effect=Timeout) def test_connection_time_out(self, mock_get): self._setup_data("webhook", "http://example") self.channel.notify(self.check) all_notifications = Notification.objects.get() self.assertEqual(all_notifications.error, "Connection timed out") @patch("hc.api.transports.requests.request", side_effect=ConnectionError) def test_connection_error(self, mock_get): self._setup_data("webhook", "http://example") self.channel.notify(self.check) all_notifications = Notification.objects.get() self.assertEqual(all_notifications.error, "Connection failed")
class NotifyTestCase(BaseTestCase): def _setup_data(self, value, status="down"): 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, kind="whatsapp") self.channel.value = value self.channel.save() self.channel.checks.add(self.check) @patch("hc.api.transports.requests.request") def test_it_works(self, mock_post): definition = {"value": "+1234567890", "up": True, "down": True} self._setup_data(json.dumps(definition)) 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"], "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): definition = {"value": "+1234567890", "up": True, "down": False} self._setup_data(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_it_enforces_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(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("hc.api.transports.requests.request") def test_it_does_not_escape_special_characters(self, mock_post): definition = {"value": "+1234567890", "up": True, "down": True} self._setup_data(json.dumps(definition)) 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"])