You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

459 lines
16 KiB

9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
  1. # coding: utf-8
  2. from datetime import timedelta as td
  3. import json
  4. from django.core import mail
  5. from django.utils.timezone import now
  6. from hc.api.models import Channel, Check, Notification
  7. from hc.test import BaseTestCase
  8. from mock import patch
  9. from requests.exceptions import ConnectionError, Timeout
  10. from six import binary_type
  11. class NotifyTestCase(BaseTestCase):
  12. def _setup_data(self, kind, value, status="down", email_verified=True):
  13. self.check = Check()
  14. self.check.status = status
  15. self.check.user = self.alice
  16. self.check.last_ping = now() - td(minutes=61)
  17. self.check.save()
  18. self.channel = Channel(user=self.alice)
  19. self.channel.kind = kind
  20. self.channel.value = value
  21. self.channel.email_verified = email_verified
  22. self.channel.save()
  23. self.channel.checks.add(self.check)
  24. @patch("hc.api.transports.requests.request")
  25. def test_webhook(self, mock_get):
  26. self._setup_data("webhook", "http://example")
  27. mock_get.return_value.status_code = 200
  28. self.channel.notify(self.check)
  29. mock_get.assert_called_with(
  30. "get", u"http://example",
  31. headers={"User-Agent": "healthchecks.io"}, timeout=5)
  32. @patch("hc.api.transports.requests.request", side_effect=Timeout)
  33. def test_webhooks_handle_timeouts(self, mock_get):
  34. self._setup_data("webhook", "http://example")
  35. self.channel.notify(self.check)
  36. n = Notification.objects.get()
  37. self.assertEqual(n.error, "Connection timed out")
  38. @patch("hc.api.transports.requests.request", side_effect=ConnectionError)
  39. def test_webhooks_handle_connection_errors(self, mock_get):
  40. self._setup_data("webhook", "http://example")
  41. self.channel.notify(self.check)
  42. n = Notification.objects.get()
  43. self.assertEqual(n.error, "Connection failed")
  44. @patch("hc.api.transports.requests.request")
  45. def test_webhooks_ignore_up_events(self, mock_get):
  46. self._setup_data("webhook", "http://example", status="up")
  47. self.channel.notify(self.check)
  48. self.assertFalse(mock_get.called)
  49. self.assertEqual(Notification.objects.count(), 0)
  50. @patch("hc.api.transports.requests.request")
  51. def test_webhooks_handle_500(self, mock_get):
  52. self._setup_data("webhook", "http://example")
  53. mock_get.return_value.status_code = 500
  54. self.channel.notify(self.check)
  55. n = Notification.objects.get()
  56. self.assertEqual(n.error, "Received status code 500")
  57. @patch("hc.api.transports.requests.request")
  58. def test_webhooks_support_variables(self, mock_get):
  59. template = "http://host/$CODE/$STATUS/$TAG1/$TAG2/?name=$NAME"
  60. self._setup_data("webhook", template)
  61. self.check.name = "Hello World"
  62. self.check.tags = "foo bar"
  63. self.check.save()
  64. self.channel.notify(self.check)
  65. url = u"http://host/%s/down/foo/bar/?name=Hello%%20World" \
  66. % self.check.code
  67. args, kwargs = mock_get.call_args
  68. self.assertEqual(args[0], "get")
  69. self.assertEqual(args[1], url)
  70. self.assertEqual(kwargs["headers"], {"User-Agent": "healthchecks.io"})
  71. self.assertEqual(kwargs["timeout"], 5)
  72. @patch("hc.api.transports.requests.request")
  73. def test_webhooks_support_post(self, mock_request):
  74. template = "http://example.com\n\nThe Time Is $NOW"
  75. self._setup_data("webhook", template)
  76. self.check.save()
  77. self.channel.notify(self.check)
  78. args, kwargs = mock_request.call_args
  79. self.assertEqual(args[0], "post")
  80. self.assertEqual(args[1], "http://example.com")
  81. # spaces should not have been urlencoded:
  82. payload = kwargs["data"].decode("utf-8")
  83. self.assertTrue(payload.startswith("The Time Is 2"))
  84. @patch("hc.api.transports.requests.request")
  85. def test_webhooks_dollarsign_escaping(self, mock_get):
  86. # If name or tag contains what looks like a variable reference,
  87. # that should be left alone:
  88. template = "http://host/$NAME"
  89. self._setup_data("webhook", template)
  90. self.check.name = "$TAG1"
  91. self.check.tags = "foo"
  92. self.check.save()
  93. self.channel.notify(self.check)
  94. url = u"http://host/%24TAG1"
  95. mock_get.assert_called_with(
  96. "get", url, headers={"User-Agent": "healthchecks.io"}, timeout=5)
  97. @patch("hc.api.transports.requests.request")
  98. def test_webhook_fires_on_up_event(self, mock_get):
  99. self._setup_data("webhook", "http://foo\nhttp://bar", status="up")
  100. self.channel.notify(self.check)
  101. mock_get.assert_called_with(
  102. "get", "http://bar", headers={"User-Agent": "healthchecks.io"},
  103. timeout=5)
  104. @patch("hc.api.transports.requests.request")
  105. def test_webhooks_handle_unicode_post_body(self, mock_request):
  106. template = u"http://example.com\n\n(╯°□°)╯︵ ┻━┻"
  107. self._setup_data("webhook", template)
  108. self.check.save()
  109. self.channel.notify(self.check)
  110. args, kwargs = mock_request.call_args
  111. # unicode should be encoded into utf-8
  112. self.assertTrue(isinstance(kwargs["data"], binary_type))
  113. @patch("hc.api.transports.requests.request")
  114. def test_webhooks_handle_json_value(self, mock_request):
  115. definition = {"url_down": "http://foo.com"}
  116. self._setup_data("webhook", json.dumps(definition))
  117. self.channel.notify(self.check)
  118. headers = {"User-Agent": "healthchecks.io"}
  119. mock_request.assert_called_with(
  120. "get", "http://foo.com", headers=headers, timeout=5)
  121. @patch("hc.api.transports.requests.request")
  122. def test_webhooks_handle_json_up_event(self, mock_request):
  123. definition = {"url_up": "http://bar"}
  124. self._setup_data("webhook", json.dumps(definition), status="up")
  125. self.channel.notify(self.check)
  126. headers = {"User-Agent": "healthchecks.io"}
  127. mock_request.assert_called_with(
  128. "get", "http://bar", headers=headers, timeout=5)
  129. @patch("hc.api.transports.requests.request")
  130. def test_webhooks_handle_post_headers(self, mock_request):
  131. definition = {
  132. "url_down": "http://foo.com",
  133. "post_data": "data",
  134. "headers": {"Content-Type": "application/json"}
  135. }
  136. self._setup_data("webhook", json.dumps(definition))
  137. self.channel.notify(self.check)
  138. headers = {
  139. "User-Agent": "healthchecks.io",
  140. "Content-Type": "application/json"
  141. }
  142. mock_request.assert_called_with(
  143. "post", "http://foo.com", data=b"data", headers=headers, timeout=5)
  144. @patch("hc.api.transports.requests.request")
  145. def test_webhooks_handle_get_headers(self, mock_request):
  146. definition = {
  147. "url_down": "http://foo.com",
  148. "headers": {"Content-Type": "application/json"}
  149. }
  150. self._setup_data("webhook", json.dumps(definition))
  151. self.channel.notify(self.check)
  152. headers = {
  153. "User-Agent": "healthchecks.io",
  154. "Content-Type": "application/json"
  155. }
  156. mock_request.assert_called_with(
  157. "get", "http://foo.com", headers=headers, timeout=5)
  158. @patch("hc.api.transports.requests.request")
  159. def test_webhooks_allow_user_agent_override(self, mock_request):
  160. definition = {
  161. "url_down": "http://foo.com",
  162. "headers": {"User-Agent": "My-Agent"}
  163. }
  164. self._setup_data("webhook", json.dumps(definition))
  165. self.channel.notify(self.check)
  166. headers = {"User-Agent": "My-Agent"}
  167. mock_request.assert_called_with(
  168. "get", "http://foo.com", headers=headers, timeout=5)
  169. def test_email(self):
  170. self._setup_data("email", "[email protected]")
  171. self.channel.notify(self.check)
  172. n = Notification.objects.get()
  173. self.assertEqual(n.error, "")
  174. # And email should have been sent
  175. self.assertEqual(len(mail.outbox), 1)
  176. email = mail.outbox[0]
  177. self.assertTrue("X-Bounce-Url" in email.extra_headers)
  178. def test_it_skips_unverified_email(self):
  179. self._setup_data("email", "[email protected]", email_verified=False)
  180. self.channel.notify(self.check)
  181. # If an email is not verified, it should be skipped over
  182. # without logging a notification:
  183. self.assertEqual(Notification.objects.count(), 0)
  184. self.assertEqual(len(mail.outbox), 0)
  185. @patch("hc.api.transports.requests.request")
  186. def test_pd(self, mock_post):
  187. self._setup_data("pd", "123")
  188. mock_post.return_value.status_code = 200
  189. self.channel.notify(self.check)
  190. assert Notification.objects.count() == 1
  191. args, kwargs = mock_post.call_args
  192. payload = kwargs["json"]
  193. self.assertEqual(payload["event_type"], "trigger")
  194. self.assertEqual(payload["service_key"], "123")
  195. @patch("hc.api.transports.requests.request")
  196. def test_pd_complex(self, mock_post):
  197. self._setup_data("pd", json.dumps({"service_key": "456"}))
  198. mock_post.return_value.status_code = 200
  199. self.channel.notify(self.check)
  200. assert Notification.objects.count() == 1
  201. args, kwargs = mock_post.call_args
  202. payload = kwargs["json"]
  203. self.assertEqual(payload["event_type"], "trigger")
  204. self.assertEqual(payload["service_key"], "456")
  205. @patch("hc.api.transports.requests.request")
  206. def test_slack(self, mock_post):
  207. self._setup_data("slack", "123")
  208. mock_post.return_value.status_code = 200
  209. self.channel.notify(self.check)
  210. assert Notification.objects.count() == 1
  211. args, kwargs = mock_post.call_args
  212. payload = kwargs["json"]
  213. attachment = payload["attachments"][0]
  214. fields = {f["title"]: f["value"] for f in attachment["fields"]}
  215. self.assertEqual(fields["Last Ping"], "an hour ago")
  216. @patch("hc.api.transports.requests.request")
  217. def test_slack_with_complex_value(self, mock_post):
  218. v = json.dumps({"incoming_webhook": {"url": "123"}})
  219. self._setup_data("slack", v)
  220. mock_post.return_value.status_code = 200
  221. self.channel.notify(self.check)
  222. assert Notification.objects.count() == 1
  223. args, kwargs = mock_post.call_args
  224. self.assertEqual(args[1], "123")
  225. @patch("hc.api.transports.requests.request")
  226. def test_slack_handles_500(self, mock_post):
  227. self._setup_data("slack", "123")
  228. mock_post.return_value.status_code = 500
  229. self.channel.notify(self.check)
  230. n = Notification.objects.get()
  231. self.assertEqual(n.error, "Received status code 500")
  232. @patch("hc.api.transports.requests.request", side_effect=Timeout)
  233. def test_slack_handles_timeout(self, mock_post):
  234. self._setup_data("slack", "123")
  235. self.channel.notify(self.check)
  236. n = Notification.objects.get()
  237. self.assertEqual(n.error, "Connection timed out")
  238. @patch("hc.api.transports.requests.request")
  239. def test_slack_with_tabs_in_schedule(self, mock_post):
  240. self._setup_data("slack", "123")
  241. self.check.kind = "cron"
  242. self.check.schedule = "*\t* * * *"
  243. self.check.save()
  244. mock_post.return_value.status_code = 200
  245. self.channel.notify(self.check)
  246. self.assertEqual(Notification.objects.count(), 1)
  247. self.assertTrue(mock_post.called)
  248. @patch("hc.api.transports.requests.request")
  249. def test_hipchat(self, mock_post):
  250. self._setup_data("hipchat", "123")
  251. mock_post.return_value.status_code = 204
  252. self.channel.notify(self.check)
  253. n = Notification.objects.first()
  254. self.assertEqual(n.error, "")
  255. args, kwargs = mock_post.call_args
  256. payload = kwargs["json"]
  257. self.assertIn("DOWN", payload["message"])
  258. @patch("hc.api.transports.requests.request")
  259. def test_opsgenie(self, mock_post):
  260. self._setup_data("opsgenie", "123")
  261. mock_post.return_value.status_code = 200
  262. self.channel.notify(self.check)
  263. n = Notification.objects.first()
  264. self.assertEqual(n.error, "")
  265. args, kwargs = mock_post.call_args
  266. payload = kwargs["json"]
  267. self.assertIn("DOWN", payload["message"])
  268. @patch("hc.api.transports.requests.request")
  269. def test_pushover(self, mock_post):
  270. self._setup_data("po", "123|0")
  271. mock_post.return_value.status_code = 200
  272. self.channel.notify(self.check)
  273. assert Notification.objects.count() == 1
  274. args, kwargs = mock_post.call_args
  275. payload = kwargs["data"]
  276. self.assertIn("DOWN", payload["title"])
  277. @patch("hc.api.transports.requests.request")
  278. def test_victorops(self, mock_post):
  279. self._setup_data("victorops", "123")
  280. mock_post.return_value.status_code = 200
  281. self.channel.notify(self.check)
  282. assert Notification.objects.count() == 1
  283. args, kwargs = mock_post.call_args
  284. payload = kwargs["json"]
  285. self.assertEqual(payload["message_type"], "CRITICAL")
  286. @patch("hc.api.transports.requests.request")
  287. def test_discord(self, mock_post):
  288. v = json.dumps({"webhook": {"url": "123"}})
  289. self._setup_data("discord", v)
  290. mock_post.return_value.status_code = 200
  291. self.channel.notify(self.check)
  292. assert Notification.objects.count() == 1
  293. args, kwargs = mock_post.call_args
  294. payload = kwargs["json"]
  295. attachment = payload["attachments"][0]
  296. fields = {f["title"]: f["value"] for f in attachment["fields"]}
  297. self.assertEqual(fields["Last Ping"], "an hour ago")
  298. @patch("hc.api.transports.requests.request")
  299. def test_pushbullet(self, mock_post):
  300. self._setup_data("pushbullet", "fake-token")
  301. mock_post.return_value.status_code = 200
  302. self.channel.notify(self.check)
  303. assert Notification.objects.count() == 1
  304. _, kwargs = mock_post.call_args
  305. self.assertEqual(kwargs["json"]["type"], "note")
  306. self.assertEqual(kwargs["headers"]["Access-Token"], "fake-token")
  307. @patch("hc.api.transports.requests.request")
  308. def test_telegram(self, mock_post):
  309. v = json.dumps({"id": 123})
  310. self._setup_data("telegram", v)
  311. mock_post.return_value.status_code = 200
  312. self.channel.notify(self.check)
  313. assert Notification.objects.count() == 1
  314. args, kwargs = mock_post.call_args
  315. payload = kwargs["json"]
  316. self.assertEqual(payload["chat_id"], 123)
  317. self.assertTrue("The check" in payload["text"])
  318. @patch("hc.api.transports.requests.request")
  319. def test_sms(self, mock_post):
  320. self._setup_data("sms", "+1234567890")
  321. self.check.last_ping = now() - td(hours=2)
  322. mock_post.return_value.status_code = 200
  323. self.channel.notify(self.check)
  324. assert Notification.objects.count() == 1
  325. args, kwargs = mock_post.call_args
  326. payload = kwargs["data"]
  327. self.assertEqual(payload["To"], "+1234567890")
  328. self.assertFalse(u"\xa0" in payload["Body"])
  329. # sent SMS counter should go up
  330. self.profile.refresh_from_db()
  331. self.assertEqual(self.profile.sms_sent, 1)
  332. @patch("hc.api.transports.requests.request")
  333. def test_sms_limit(self, mock_post):
  334. # At limit already:
  335. self.profile.last_sms_date = now()
  336. self.profile.sms_sent = 50
  337. self.profile.save()
  338. self._setup_data("sms", "+1234567890")
  339. self.channel.notify(self.check)
  340. self.assertFalse(mock_post.called)
  341. n = Notification.objects.get()
  342. self.assertTrue("Monthly SMS limit exceeded" in n.error)
  343. @patch("hc.api.transports.requests.request")
  344. def test_sms_limit_reset(self, mock_post):
  345. # At limit, but also into a new month
  346. self.profile.sms_sent = 50
  347. self.profile.last_sms_date = now() - td(days=100)
  348. self.profile.save()
  349. self._setup_data("sms", "+1234567890")
  350. mock_post.return_value.status_code = 200
  351. self.channel.notify(self.check)
  352. self.assertTrue(mock_post.called)