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.

584 lines
20 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. class NotifyTestCase(BaseTestCase):
  11. def _setup_data(self, kind, value, status="down", email_verified=True):
  12. self.check = Check()
  13. self.check.status = status
  14. self.check.user = self.alice
  15. self.check.last_ping = now() - td(minutes=61)
  16. self.check.save()
  17. self.channel = Channel(user=self.alice)
  18. self.channel.kind = kind
  19. self.channel.value = value
  20. self.channel.email_verified = email_verified
  21. self.channel.save()
  22. self.channel.checks.add(self.check)
  23. @patch("hc.api.transports.requests.request")
  24. def test_webhook(self, mock_get):
  25. self._setup_data("webhook", "http://example")
  26. mock_get.return_value.status_code = 200
  27. self.channel.notify(self.check)
  28. mock_get.assert_called_with(
  29. "get", u"http://example",
  30. headers={"User-Agent": "healthchecks.io"}, timeout=5)
  31. @patch("hc.api.transports.requests.request", side_effect=Timeout)
  32. def test_webhooks_handle_timeouts(self, mock_get):
  33. self._setup_data("webhook", "http://example")
  34. self.channel.notify(self.check)
  35. n = Notification.objects.get()
  36. self.assertEqual(n.error, "Connection timed out")
  37. @patch("hc.api.transports.requests.request", side_effect=ConnectionError)
  38. def test_webhooks_handle_connection_errors(self, mock_get):
  39. self._setup_data("webhook", "http://example")
  40. self.channel.notify(self.check)
  41. n = Notification.objects.get()
  42. self.assertEqual(n.error, "Connection failed")
  43. @patch("hc.api.transports.requests.request")
  44. def test_webhooks_ignore_up_events(self, mock_get):
  45. self._setup_data("webhook", "http://example", status="up")
  46. self.channel.notify(self.check)
  47. self.assertFalse(mock_get.called)
  48. self.assertEqual(Notification.objects.count(), 0)
  49. @patch("hc.api.transports.requests.request")
  50. def test_webhooks_handle_500(self, mock_get):
  51. self._setup_data("webhook", "http://example")
  52. mock_get.return_value.status_code = 500
  53. self.channel.notify(self.check)
  54. n = Notification.objects.get()
  55. self.assertEqual(n.error, "Received status code 500")
  56. @patch("hc.api.transports.requests.request")
  57. def test_webhooks_support_variables(self, mock_get):
  58. template = "http://host/$CODE/$STATUS/$TAG1/$TAG2/?name=$NAME"
  59. self._setup_data("webhook", template)
  60. self.check.name = "Hello World"
  61. self.check.tags = "foo bar"
  62. self.check.save()
  63. self.channel.notify(self.check)
  64. url = u"http://host/%s/down/foo/bar/?name=Hello%%20World" \
  65. % self.check.code
  66. args, kwargs = mock_get.call_args
  67. self.assertEqual(args[0], "get")
  68. self.assertEqual(args[1], url)
  69. self.assertEqual(kwargs["headers"], {"User-Agent": "healthchecks.io"})
  70. self.assertEqual(kwargs["timeout"], 5)
  71. @patch("hc.api.transports.requests.request")
  72. def test_webhooks_support_post(self, mock_request):
  73. template = "http://example.com\n\nThe Time Is $NOW"
  74. self._setup_data("webhook", template)
  75. self.check.save()
  76. self.channel.notify(self.check)
  77. args, kwargs = mock_request.call_args
  78. self.assertEqual(args[0], "post")
  79. self.assertEqual(args[1], "http://example.com")
  80. # spaces should not have been urlencoded:
  81. payload = kwargs["data"].decode()
  82. self.assertTrue(payload.startswith("The Time Is 2"))
  83. @patch("hc.api.transports.requests.request")
  84. def test_webhooks_dollarsign_escaping(self, mock_get):
  85. # If name or tag contains what looks like a variable reference,
  86. # that should be left alone:
  87. template = "http://host/$NAME"
  88. self._setup_data("webhook", template)
  89. self.check.name = "$TAG1"
  90. self.check.tags = "foo"
  91. self.check.save()
  92. self.channel.notify(self.check)
  93. url = u"http://host/%24TAG1"
  94. mock_get.assert_called_with(
  95. "get", url, headers={"User-Agent": "healthchecks.io"}, timeout=5)
  96. @patch("hc.api.transports.requests.request")
  97. def test_webhook_fires_on_up_event(self, mock_get):
  98. self._setup_data("webhook", "http://foo\nhttp://bar", status="up")
  99. self.channel.notify(self.check)
  100. mock_get.assert_called_with(
  101. "get", "http://bar", headers={"User-Agent": "healthchecks.io"},
  102. timeout=5)
  103. @patch("hc.api.transports.requests.request")
  104. def test_webhooks_handle_unicode_post_body(self, mock_request):
  105. template = u"http://example.com\n\n(╯°□°)╯︵ ┻━┻"
  106. self._setup_data("webhook", template)
  107. self.check.save()
  108. self.channel.notify(self.check)
  109. args, kwargs = mock_request.call_args
  110. # unicode should be encoded into utf-8
  111. self.assertIsInstance(kwargs["data"], bytes)
  112. @patch("hc.api.transports.requests.request")
  113. def test_webhooks_handle_json_value(self, mock_request):
  114. definition = {"url_down": "http://foo.com"}
  115. self._setup_data("webhook", json.dumps(definition))
  116. self.channel.notify(self.check)
  117. headers = {"User-Agent": "healthchecks.io"}
  118. mock_request.assert_called_with(
  119. "get", "http://foo.com", headers=headers, timeout=5)
  120. @patch("hc.api.transports.requests.request")
  121. def test_webhooks_handle_json_up_event(self, mock_request):
  122. definition = {"url_up": "http://bar"}
  123. self._setup_data("webhook", json.dumps(definition), status="up")
  124. self.channel.notify(self.check)
  125. headers = {"User-Agent": "healthchecks.io"}
  126. mock_request.assert_called_with(
  127. "get", "http://bar", headers=headers, timeout=5)
  128. @patch("hc.api.transports.requests.request")
  129. def test_webhooks_handle_post_headers(self, mock_request):
  130. definition = {
  131. "url_down": "http://foo.com",
  132. "post_data": "data",
  133. "headers": {"Content-Type": "application/json"}
  134. }
  135. self._setup_data("webhook", json.dumps(definition))
  136. self.channel.notify(self.check)
  137. headers = {
  138. "User-Agent": "healthchecks.io",
  139. "Content-Type": "application/json"
  140. }
  141. mock_request.assert_called_with(
  142. "post", "http://foo.com", data=b"data", headers=headers, timeout=5)
  143. @patch("hc.api.transports.requests.request")
  144. def test_webhooks_handle_get_headers(self, mock_request):
  145. definition = {
  146. "url_down": "http://foo.com",
  147. "headers": {"Content-Type": "application/json"}
  148. }
  149. self._setup_data("webhook", json.dumps(definition))
  150. self.channel.notify(self.check)
  151. headers = {
  152. "User-Agent": "healthchecks.io",
  153. "Content-Type": "application/json"
  154. }
  155. mock_request.assert_called_with(
  156. "get", "http://foo.com", headers=headers, timeout=5)
  157. @patch("hc.api.transports.requests.request")
  158. def test_webhooks_allow_user_agent_override(self, mock_request):
  159. definition = {
  160. "url_down": "http://foo.com",
  161. "headers": {"User-Agent": "My-Agent"}
  162. }
  163. self._setup_data("webhook", json.dumps(definition))
  164. self.channel.notify(self.check)
  165. headers = {"User-Agent": "My-Agent"}
  166. mock_request.assert_called_with(
  167. "get", "http://foo.com", headers=headers, timeout=5)
  168. @patch("hc.api.transports.requests.request")
  169. def test_webhooks_support_variables_in_headers(self, mock_request):
  170. definition = {
  171. "url_down": "http://foo.com",
  172. "headers": {"X-Message": "$NAME is DOWN"}
  173. }
  174. self._setup_data("webhook", json.dumps(definition))
  175. self.check.name = "Foo"
  176. self.check.save()
  177. self.channel.notify(self.check)
  178. headers = {
  179. "User-Agent": "healthchecks.io",
  180. "X-Message": "Foo is DOWN"
  181. }
  182. mock_request.assert_called_with(
  183. "get", "http://foo.com", headers=headers, timeout=5)
  184. def test_email(self):
  185. self._setup_data("email", "[email protected]")
  186. self.channel.notify(self.check)
  187. n = Notification.objects.get()
  188. self.assertEqual(n.error, "")
  189. # And email should have been sent
  190. self.assertEqual(len(mail.outbox), 1)
  191. email = mail.outbox[0]
  192. self.assertTrue("X-Bounce-Url" in email.extra_headers)
  193. def test_it_skips_unverified_email(self):
  194. self._setup_data("email", "[email protected]", email_verified=False)
  195. self.channel.notify(self.check)
  196. # If an email is not verified, it should be skipped over
  197. # without logging a notification:
  198. self.assertEqual(Notification.objects.count(), 0)
  199. self.assertEqual(len(mail.outbox), 0)
  200. @patch("hc.api.transports.requests.request")
  201. def test_pd(self, mock_post):
  202. self._setup_data("pd", "123")
  203. mock_post.return_value.status_code = 200
  204. self.channel.notify(self.check)
  205. assert Notification.objects.count() == 1
  206. args, kwargs = mock_post.call_args
  207. payload = kwargs["json"]
  208. self.assertEqual(payload["event_type"], "trigger")
  209. self.assertEqual(payload["service_key"], "123")
  210. @patch("hc.api.transports.requests.request")
  211. def test_pd_complex(self, mock_post):
  212. self._setup_data("pd", json.dumps({"service_key": "456"}))
  213. mock_post.return_value.status_code = 200
  214. self.channel.notify(self.check)
  215. assert Notification.objects.count() == 1
  216. args, kwargs = mock_post.call_args
  217. payload = kwargs["json"]
  218. self.assertEqual(payload["event_type"], "trigger")
  219. self.assertEqual(payload["service_key"], "456")
  220. @patch("hc.api.transports.requests.request")
  221. def test_pagertree(self, mock_post):
  222. self._setup_data("pagertree", "123")
  223. mock_post.return_value.status_code = 200
  224. self.channel.notify(self.check)
  225. assert Notification.objects.count() == 1
  226. args, kwargs = mock_post.call_args
  227. payload = kwargs["json"]
  228. self.assertEqual(payload["event_type"], "trigger")
  229. @patch("hc.api.transports.requests.request")
  230. def test_slack(self, mock_post):
  231. self._setup_data("slack", "123")
  232. mock_post.return_value.status_code = 200
  233. self.channel.notify(self.check)
  234. assert Notification.objects.count() == 1
  235. args, kwargs = mock_post.call_args
  236. payload = kwargs["json"]
  237. attachment = payload["attachments"][0]
  238. fields = {f["title"]: f["value"] for f in attachment["fields"]}
  239. self.assertEqual(fields["Last Ping"], "an hour ago")
  240. @patch("hc.api.transports.requests.request")
  241. def test_slack_with_complex_value(self, mock_post):
  242. v = json.dumps({"incoming_webhook": {"url": "123"}})
  243. self._setup_data("slack", v)
  244. mock_post.return_value.status_code = 200
  245. self.channel.notify(self.check)
  246. assert Notification.objects.count() == 1
  247. args, kwargs = mock_post.call_args
  248. self.assertEqual(args[1], "123")
  249. @patch("hc.api.transports.requests.request")
  250. def test_slack_handles_500(self, mock_post):
  251. self._setup_data("slack", "123")
  252. mock_post.return_value.status_code = 500
  253. self.channel.notify(self.check)
  254. n = Notification.objects.get()
  255. self.assertEqual(n.error, "Received status code 500")
  256. @patch("hc.api.transports.requests.request", side_effect=Timeout)
  257. def test_slack_handles_timeout(self, mock_post):
  258. self._setup_data("slack", "123")
  259. self.channel.notify(self.check)
  260. n = Notification.objects.get()
  261. self.assertEqual(n.error, "Connection timed out")
  262. @patch("hc.api.transports.requests.request")
  263. def test_slack_with_tabs_in_schedule(self, mock_post):
  264. self._setup_data("slack", "123")
  265. self.check.kind = "cron"
  266. self.check.schedule = "*\t* * * *"
  267. self.check.save()
  268. mock_post.return_value.status_code = 200
  269. self.channel.notify(self.check)
  270. self.assertEqual(Notification.objects.count(), 1)
  271. self.assertTrue(mock_post.called)
  272. @patch("hc.api.transports.requests.request")
  273. def test_hipchat(self, mock_post):
  274. self._setup_data("hipchat", "123")
  275. mock_post.return_value.status_code = 204
  276. self.channel.notify(self.check)
  277. n = Notification.objects.first()
  278. self.assertEqual(n.error, "")
  279. args, kwargs = mock_post.call_args
  280. payload = kwargs["json"]
  281. self.assertIn("DOWN", payload["message"])
  282. @patch("hc.api.transports.requests.request")
  283. def test_opsgenie(self, mock_post):
  284. self._setup_data("opsgenie", "123")
  285. mock_post.return_value.status_code = 202
  286. self.channel.notify(self.check)
  287. n = Notification.objects.first()
  288. self.assertEqual(n.error, "")
  289. self.assertEqual(mock_post.call_count, 1)
  290. args, kwargs = mock_post.call_args
  291. payload = kwargs["json"]
  292. self.assertIn("DOWN", payload["message"])
  293. @patch("hc.api.transports.requests.request")
  294. def test_opsgenie_up(self, mock_post):
  295. self._setup_data("opsgenie", "123", status="up")
  296. mock_post.return_value.status_code = 202
  297. self.channel.notify(self.check)
  298. n = Notification.objects.first()
  299. self.assertEqual(n.error, "")
  300. self.assertEqual(mock_post.call_count, 1)
  301. args, kwargs = mock_post.call_args
  302. method, url = args
  303. self.assertTrue(str(self.check.code) in url)
  304. @patch("hc.api.transports.requests.request")
  305. def test_pushover(self, mock_post):
  306. self._setup_data("po", "123|0")
  307. mock_post.return_value.status_code = 200
  308. self.channel.notify(self.check)
  309. assert Notification.objects.count() == 1
  310. args, kwargs = mock_post.call_args
  311. payload = kwargs["data"]
  312. self.assertIn("DOWN", payload["title"])
  313. @patch("hc.api.transports.requests.request")
  314. def test_victorops(self, mock_post):
  315. self._setup_data("victorops", "123")
  316. mock_post.return_value.status_code = 200
  317. self.channel.notify(self.check)
  318. assert Notification.objects.count() == 1
  319. args, kwargs = mock_post.call_args
  320. payload = kwargs["json"]
  321. self.assertEqual(payload["message_type"], "CRITICAL")
  322. @patch("hc.api.transports.requests.request")
  323. def test_discord(self, mock_post):
  324. v = json.dumps({"webhook": {"url": "123"}})
  325. self._setup_data("discord", v)
  326. mock_post.return_value.status_code = 200
  327. self.channel.notify(self.check)
  328. assert Notification.objects.count() == 1
  329. args, kwargs = mock_post.call_args
  330. payload = kwargs["json"]
  331. attachment = payload["attachments"][0]
  332. fields = {f["title"]: f["value"] for f in attachment["fields"]}
  333. self.assertEqual(fields["Last Ping"], "an hour ago")
  334. @patch("hc.api.transports.requests.request")
  335. def test_pushbullet(self, mock_post):
  336. self._setup_data("pushbullet", "fake-token")
  337. mock_post.return_value.status_code = 200
  338. self.channel.notify(self.check)
  339. assert Notification.objects.count() == 1
  340. _, kwargs = mock_post.call_args
  341. self.assertEqual(kwargs["json"]["type"], "note")
  342. self.assertEqual(kwargs["headers"]["Access-Token"], "fake-token")
  343. @patch("hc.api.transports.requests.request")
  344. def test_telegram(self, mock_post):
  345. v = json.dumps({"id": 123})
  346. self._setup_data("telegram", v)
  347. mock_post.return_value.status_code = 200
  348. self.channel.notify(self.check)
  349. assert Notification.objects.count() == 1
  350. args, kwargs = mock_post.call_args
  351. payload = kwargs["json"]
  352. self.assertEqual(payload["chat_id"], 123)
  353. self.assertTrue("The check" in payload["text"])
  354. @patch("hc.api.transports.requests.request")
  355. def test_sms(self, mock_post):
  356. self._setup_data("sms", "+1234567890")
  357. self.check.last_ping = now() - td(hours=2)
  358. mock_post.return_value.status_code = 200
  359. self.channel.notify(self.check)
  360. assert Notification.objects.count() == 1
  361. args, kwargs = mock_post.call_args
  362. payload = kwargs["data"]
  363. self.assertEqual(payload["To"], "+1234567890")
  364. self.assertFalse(u"\xa0" in payload["Body"])
  365. # sent SMS counter should go up
  366. self.profile.refresh_from_db()
  367. self.assertEqual(self.profile.sms_sent, 1)
  368. @patch("hc.api.transports.requests.request")
  369. def test_sms_handles_json_value(self, mock_post):
  370. value = {"label": "foo", "value": "+1234567890"}
  371. self._setup_data("sms", json.dumps(value))
  372. self.check.last_ping = now() - td(hours=2)
  373. mock_post.return_value.status_code = 200
  374. self.channel.notify(self.check)
  375. assert Notification.objects.count() == 1
  376. args, kwargs = mock_post.call_args
  377. payload = kwargs["data"]
  378. self.assertEqual(payload["To"], "+1234567890")
  379. @patch("hc.api.transports.requests.request")
  380. def test_sms_limit(self, mock_post):
  381. # At limit already:
  382. self.profile.last_sms_date = now()
  383. self.profile.sms_sent = 50
  384. self.profile.save()
  385. self._setup_data("sms", "+1234567890")
  386. self.channel.notify(self.check)
  387. self.assertFalse(mock_post.called)
  388. n = Notification.objects.get()
  389. self.assertTrue("Monthly SMS limit exceeded" in n.error)
  390. @patch("hc.api.transports.requests.request")
  391. def test_sms_limit_reset(self, mock_post):
  392. # At limit, but also into a new month
  393. self.profile.sms_sent = 50
  394. self.profile.last_sms_date = now() - td(days=100)
  395. self.profile.save()
  396. self._setup_data("sms", "+1234567890")
  397. mock_post.return_value.status_code = 200
  398. self.channel.notify(self.check)
  399. self.assertTrue(mock_post.called)
  400. @patch("hc.api.transports.requests.request")
  401. def test_zendesk_down(self, mock_post):
  402. v = json.dumps({"access_token": "fake-token", "subdomain": "foo"})
  403. self._setup_data("zendesk", v)
  404. mock_post.return_value.status_code = 200
  405. self.channel.notify(self.check)
  406. assert Notification.objects.count() == 1
  407. args, kwargs = mock_post.call_args
  408. method, url = args
  409. self.assertEqual(method, "post")
  410. self.assertTrue("foo.zendesk.com" in url)
  411. payload = kwargs["json"]
  412. self.assertEqual(payload["request"]["type"], "incident")
  413. self.assertTrue("down" in payload["request"]["subject"])
  414. headers = kwargs["headers"]
  415. self.assertEqual(headers["Authorization"], "Bearer fake-token")
  416. @patch("hc.api.transports.requests.request")
  417. @patch("hc.api.transports.requests.get")
  418. def test_zendesk_up(self, mock_get, mock_post):
  419. v = json.dumps({"access_token": "fake-token", "subdomain": "foo"})
  420. self._setup_data("zendesk", v, status="up")
  421. mock_post.return_value.status_code = 200
  422. mock_get.return_value.status_code = 200
  423. mock_get.return_value.json.return_value = {
  424. "requests": [{
  425. "url": "https://foo.example.org/comment",
  426. "description": "code is %s" % self.check.code
  427. }]
  428. }
  429. self.channel.notify(self.check)
  430. assert Notification.objects.count() == 1
  431. args, kwargs = mock_post.call_args
  432. self.assertTrue("foo.example.org" in args[1])
  433. payload = kwargs["json"]
  434. self.assertEqual(payload["request"]["type"], "incident")
  435. self.assertTrue("UP" in payload["request"]["subject"])
  436. headers = kwargs["headers"]
  437. self.assertEqual(headers["Authorization"], "Bearer fake-token")
  438. @patch("hc.api.transports.requests.request")
  439. @patch("hc.api.transports.requests.get")
  440. def test_zendesk_up_with_no_existing_ticket(self, mock_get, mock_post):
  441. v = json.dumps({"access_token": "fake-token", "subdomain": "foo"})
  442. self._setup_data("zendesk", v, status="up")
  443. mock_get.return_value.status_code = 200
  444. mock_get.return_value.json.return_value = {"requests": []}
  445. self.channel.notify(self.check)
  446. n = Notification.objects.get()
  447. self.assertEqual(n.error, "Could not find a ticket to update")
  448. self.assertFalse(mock_post.called)