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.

1003 lines
35 KiB

6 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
6 years ago
6 years ago
6 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
6 years ago
6 years ago
6 years ago
5 years ago
5 years ago
  1. # coding: utf-8
  2. from datetime import timedelta as td
  3. import json
  4. from unittest.mock import patch, Mock
  5. from django.core import mail
  6. from django.utils.timezone import now
  7. from hc.api.models import Channel, Check, Notification, TokenBucket
  8. from hc.test import BaseTestCase
  9. from requests.exceptions import ConnectionError, Timeout
  10. from django.test.utils import override_settings
  11. class NotifyTestCase(BaseTestCase):
  12. def _setup_data(self, kind, value, status="down", email_verified=True):
  13. self.check = Check(project=self.project)
  14. self.check.status = status
  15. self.check.last_ping = now() - td(minutes=61)
  16. self.check.save()
  17. self.channel = Channel(project=self.project)
  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. definition = {
  26. "method_down": "GET",
  27. "url_down": "http://example",
  28. "body_down": "",
  29. "headers_down": {},
  30. }
  31. self._setup_data("webhook", json.dumps(definition))
  32. mock_get.return_value.status_code = 200
  33. self.channel.notify(self.check)
  34. mock_get.assert_called_with(
  35. "get",
  36. "http://example",
  37. headers={"User-Agent": "healthchecks.io"},
  38. timeout=5,
  39. )
  40. @patch("hc.api.transports.requests.request", side_effect=Timeout)
  41. def test_webhooks_handle_timeouts(self, mock_get):
  42. definition = {
  43. "method_down": "GET",
  44. "url_down": "http://example",
  45. "body_down": "",
  46. "headers_down": {},
  47. }
  48. self._setup_data("webhook", json.dumps(definition))
  49. self.channel.notify(self.check)
  50. # The transport should have retried 3 times
  51. self.assertEqual(mock_get.call_count, 3)
  52. n = Notification.objects.get()
  53. self.assertEqual(n.error, "Connection timed out")
  54. self.channel.refresh_from_db()
  55. self.assertEqual(self.channel.last_error, "Connection timed out")
  56. @patch("hc.api.transports.requests.request", side_effect=ConnectionError)
  57. def test_webhooks_handle_connection_errors(self, mock_get):
  58. definition = {
  59. "method_down": "GET",
  60. "url_down": "http://example",
  61. "body_down": "",
  62. "headers_down": {},
  63. }
  64. self._setup_data("webhook", json.dumps(definition))
  65. self.channel.notify(self.check)
  66. # The transport should have retried 3 times
  67. self.assertEqual(mock_get.call_count, 3)
  68. n = Notification.objects.get()
  69. self.assertEqual(n.error, "Connection failed")
  70. @patch("hc.api.transports.requests.request")
  71. def test_webhooks_handle_500(self, mock_get):
  72. definition = {
  73. "method_down": "GET",
  74. "url_down": "http://example",
  75. "body_down": "",
  76. "headers_down": {},
  77. }
  78. self._setup_data("webhook", json.dumps(definition))
  79. mock_get.return_value.status_code = 500
  80. self.channel.notify(self.check)
  81. # The transport should have retried 3 times
  82. self.assertEqual(mock_get.call_count, 3)
  83. n = Notification.objects.get()
  84. self.assertEqual(n.error, "Received status code 500")
  85. @patch("hc.api.transports.requests.request", side_effect=Timeout)
  86. def test_webhooks_dont_retry_when_sending_test_notifications(self, mock_get):
  87. definition = {
  88. "method_down": "GET",
  89. "url_down": "http://example",
  90. "body_down": "",
  91. "headers_down": {},
  92. }
  93. self._setup_data("webhook", json.dumps(definition))
  94. self.channel.notify(self.check, is_test=True)
  95. # is_test flag is set, the transport should not retry:
  96. self.assertEqual(mock_get.call_count, 1)
  97. n = Notification.objects.get()
  98. self.assertEqual(n.error, "Connection timed out")
  99. @patch("hc.api.transports.requests.request")
  100. def test_webhooks_support_variables(self, mock_get):
  101. definition = {
  102. "method_down": "GET",
  103. "url_down": "http://host/$CODE/$STATUS/$TAG1/$TAG2/?name=$NAME",
  104. "body_down": "",
  105. "headers_down": {},
  106. }
  107. self._setup_data("webhook", json.dumps(definition))
  108. self.check.name = "Hello World"
  109. self.check.tags = "foo bar"
  110. self.check.save()
  111. self.channel.notify(self.check)
  112. url = "http://host/%s/down/foo/bar/?name=Hello%%20World" % self.check.code
  113. args, kwargs = mock_get.call_args
  114. self.assertEqual(args[0], "get")
  115. self.assertEqual(args[1], url)
  116. self.assertEqual(kwargs["headers"], {"User-Agent": "healthchecks.io"})
  117. self.assertEqual(kwargs["timeout"], 5)
  118. @patch("hc.api.transports.requests.request")
  119. def test_webhooks_handle_variable_variables(self, mock_get):
  120. definition = {
  121. "method_down": "GET",
  122. "url_down": "http://host/$$NAMETAG1",
  123. "body_down": "",
  124. "headers_down": {},
  125. }
  126. self._setup_data("webhook", json.dumps(definition))
  127. self.check.tags = "foo bar"
  128. self.check.save()
  129. self.channel.notify(self.check)
  130. # $$NAMETAG1 should *not* get transformed to "foo"
  131. args, kwargs = mock_get.call_args
  132. self.assertEqual(args[1], "http://host/$TAG1")
  133. @patch("hc.api.transports.requests.request")
  134. def test_webhooks_support_post(self, mock_request):
  135. definition = {
  136. "method_down": "POST",
  137. "url_down": "http://example.com",
  138. "body_down": "The Time Is $NOW",
  139. "headers_down": {},
  140. }
  141. self._setup_data("webhook", json.dumps(definition))
  142. self.check.save()
  143. self.channel.notify(self.check)
  144. args, kwargs = mock_request.call_args
  145. self.assertEqual(args[0], "post")
  146. self.assertEqual(args[1], "http://example.com")
  147. # spaces should not have been urlencoded:
  148. payload = kwargs["data"].decode()
  149. self.assertTrue(payload.startswith("The Time Is 2"))
  150. @patch("hc.api.transports.requests.request")
  151. def test_webhooks_dollarsign_escaping(self, mock_get):
  152. # If name or tag contains what looks like a variable reference,
  153. # that should be left alone:
  154. definition = {
  155. "method_down": "GET",
  156. "url_down": "http://host/$NAME",
  157. "body_down": "",
  158. "headers_down": {},
  159. }
  160. self._setup_data("webhook", json.dumps(definition))
  161. self.check.name = "$TAG1"
  162. self.check.tags = "foo"
  163. self.check.save()
  164. self.channel.notify(self.check)
  165. url = "http://host/%24TAG1"
  166. mock_get.assert_called_with(
  167. "get", url, headers={"User-Agent": "healthchecks.io"}, timeout=5
  168. )
  169. @patch("hc.api.transports.requests.request")
  170. def test_webhooks_handle_up_events(self, mock_get):
  171. definition = {
  172. "method_up": "GET",
  173. "url_up": "http://bar",
  174. "body_up": "",
  175. "headers_up": {},
  176. }
  177. self._setup_data("webhook", json.dumps(definition), status="up")
  178. self.channel.notify(self.check)
  179. mock_get.assert_called_with(
  180. "get", "http://bar", headers={"User-Agent": "healthchecks.io"}, timeout=5
  181. )
  182. @patch("hc.api.transports.requests.request")
  183. def test_webhooks_handle_noop_up_events(self, mock_get):
  184. definition = {
  185. "method_up": "GET",
  186. "url_up": "",
  187. "body_up": "",
  188. "headers_up": {},
  189. }
  190. self._setup_data("webhook", json.dumps(definition), status="up")
  191. self.channel.notify(self.check)
  192. self.assertFalse(mock_get.called)
  193. self.assertEqual(Notification.objects.count(), 0)
  194. @patch("hc.api.transports.requests.request")
  195. def test_webhooks_handle_unicode_post_body(self, mock_request):
  196. definition = {
  197. "method_down": "POST",
  198. "url_down": "http://foo.com",
  199. "body_down": "(╯°□°)╯︵ ┻━┻",
  200. "headers_down": {},
  201. }
  202. self._setup_data("webhook", json.dumps(definition))
  203. self.check.save()
  204. self.channel.notify(self.check)
  205. args, kwargs = mock_request.call_args
  206. # unicode should be encoded into utf-8
  207. self.assertIsInstance(kwargs["data"], bytes)
  208. @patch("hc.api.transports.requests.request")
  209. def test_webhooks_handle_post_headers(self, mock_request):
  210. definition = {
  211. "method_down": "POST",
  212. "url_down": "http://foo.com",
  213. "body_down": "data",
  214. "headers_down": {"Content-Type": "application/json"},
  215. }
  216. self._setup_data("webhook", json.dumps(definition))
  217. self.channel.notify(self.check)
  218. headers = {"User-Agent": "healthchecks.io", "Content-Type": "application/json"}
  219. mock_request.assert_called_with(
  220. "post", "http://foo.com", data=b"data", headers=headers, timeout=5
  221. )
  222. @patch("hc.api.transports.requests.request")
  223. def test_webhooks_handle_get_headers(self, mock_request):
  224. definition = {
  225. "method_down": "GET",
  226. "url_down": "http://foo.com",
  227. "body_down": "",
  228. "headers_down": {"Content-Type": "application/json"},
  229. }
  230. self._setup_data("webhook", json.dumps(definition))
  231. self.channel.notify(self.check)
  232. headers = {"User-Agent": "healthchecks.io", "Content-Type": "application/json"}
  233. mock_request.assert_called_with(
  234. "get", "http://foo.com", headers=headers, timeout=5
  235. )
  236. @patch("hc.api.transports.requests.request")
  237. def test_webhooks_allow_user_agent_override(self, mock_request):
  238. definition = {
  239. "method_down": "GET",
  240. "url_down": "http://foo.com",
  241. "body_down": "",
  242. "headers_down": {"User-Agent": "My-Agent"},
  243. }
  244. self._setup_data("webhook", json.dumps(definition))
  245. self.channel.notify(self.check)
  246. headers = {"User-Agent": "My-Agent"}
  247. mock_request.assert_called_with(
  248. "get", "http://foo.com", headers=headers, timeout=5
  249. )
  250. @patch("hc.api.transports.requests.request")
  251. def test_webhooks_support_variables_in_headers(self, mock_request):
  252. definition = {
  253. "method_down": "GET",
  254. "url_down": "http://foo.com",
  255. "body_down": "",
  256. "headers_down": {"X-Message": "$NAME is DOWN"},
  257. }
  258. self._setup_data("webhook", json.dumps(definition))
  259. self.check.name = "Foo"
  260. self.check.save()
  261. self.channel.notify(self.check)
  262. headers = {"User-Agent": "healthchecks.io", "X-Message": "Foo is DOWN"}
  263. mock_request.assert_called_with(
  264. "get", "http://foo.com", headers=headers, timeout=5
  265. )
  266. def test_email(self):
  267. self._setup_data("email", "[email protected]")
  268. self.channel.notify(self.check)
  269. n = Notification.objects.get()
  270. self.assertEqual(n.error, "")
  271. # And email should have been sent
  272. self.assertEqual(len(mail.outbox), 1)
  273. email = mail.outbox[0]
  274. self.assertEqual(email.to[0], "[email protected]")
  275. self.assertTrue("X-Status-Url" in email.extra_headers)
  276. self.assertTrue("List-Unsubscribe" in email.extra_headers)
  277. self.assertTrue("List-Unsubscribe-Post" in email.extra_headers)
  278. def test_email_transport_handles_json_value(self):
  279. payload = {"value": "[email protected]", "up": True, "down": True}
  280. self._setup_data("email", json.dumps(payload))
  281. self.channel.notify(self.check)
  282. # And email should have been sent
  283. self.assertEqual(len(mail.outbox), 1)
  284. email = mail.outbox[0]
  285. self.assertEqual(email.to[0], "[email protected]")
  286. def test_it_reports_unverified_email(self):
  287. self._setup_data("email", "[email protected]", email_verified=False)
  288. self.channel.notify(self.check)
  289. # If an email is not verified, it should say so in the notification:
  290. n = Notification.objects.get()
  291. self.assertEqual(n.error, "Email not verified")
  292. def test_email_checks_up_down_flags(self):
  293. payload = {"value": "[email protected]", "up": True, "down": False}
  294. self._setup_data("email", json.dumps(payload))
  295. self.channel.notify(self.check)
  296. # This channel should not notify on "down" events:
  297. self.assertEqual(Notification.objects.count(), 0)
  298. self.assertEqual(len(mail.outbox), 0)
  299. def test_email_handles_amperstand(self):
  300. self._setup_data("email", "[email protected]")
  301. self.check.name = "Foo & Bar"
  302. self.channel.notify(self.check)
  303. email = mail.outbox[0]
  304. self.assertEqual(email.subject, "DOWN | Foo & Bar")
  305. @patch("hc.api.transports.requests.request")
  306. def test_pd(self, mock_post):
  307. self._setup_data("pd", "123")
  308. mock_post.return_value.status_code = 200
  309. self.channel.notify(self.check)
  310. assert Notification.objects.count() == 1
  311. args, kwargs = mock_post.call_args
  312. payload = kwargs["json"]
  313. self.assertEqual(payload["event_type"], "trigger")
  314. self.assertEqual(payload["service_key"], "123")
  315. @patch("hc.api.transports.requests.request")
  316. def test_pd_complex(self, mock_post):
  317. self._setup_data("pd", json.dumps({"service_key": "456"}))
  318. mock_post.return_value.status_code = 200
  319. self.channel.notify(self.check)
  320. assert Notification.objects.count() == 1
  321. args, kwargs = mock_post.call_args
  322. payload = kwargs["json"]
  323. self.assertEqual(payload["event_type"], "trigger")
  324. self.assertEqual(payload["service_key"], "456")
  325. @patch("hc.api.transports.requests.request")
  326. def test_pagertree(self, mock_post):
  327. self._setup_data("pagertree", "123")
  328. mock_post.return_value.status_code = 200
  329. self.channel.notify(self.check)
  330. assert Notification.objects.count() == 1
  331. args, kwargs = mock_post.call_args
  332. payload = kwargs["json"]
  333. self.assertEqual(payload["event_type"], "trigger")
  334. @patch("hc.api.transports.requests.request")
  335. def test_pagerteam(self, mock_post):
  336. self._setup_data("pagerteam", "123")
  337. self.channel.notify(self.check)
  338. self.assertFalse(mock_post.called)
  339. self.assertEqual(Notification.objects.count(), 0)
  340. @patch("hc.api.transports.requests.request")
  341. def test_slack(self, mock_post):
  342. self._setup_data("slack", "123")
  343. mock_post.return_value.status_code = 200
  344. self.channel.notify(self.check)
  345. assert Notification.objects.count() == 1
  346. args, kwargs = mock_post.call_args
  347. payload = kwargs["json"]
  348. attachment = payload["attachments"][0]
  349. fields = {f["title"]: f["value"] for f in attachment["fields"]}
  350. self.assertEqual(fields["Last Ping"], "an hour ago")
  351. @patch("hc.api.transports.requests.request")
  352. def test_slack_with_complex_value(self, mock_post):
  353. v = json.dumps({"incoming_webhook": {"url": "123"}})
  354. self._setup_data("slack", v)
  355. mock_post.return_value.status_code = 200
  356. self.channel.notify(self.check)
  357. assert Notification.objects.count() == 1
  358. args, kwargs = mock_post.call_args
  359. self.assertEqual(args[1], "123")
  360. @patch("hc.api.transports.requests.request")
  361. def test_slack_handles_500(self, mock_post):
  362. self._setup_data("slack", "123")
  363. mock_post.return_value.status_code = 500
  364. self.channel.notify(self.check)
  365. n = Notification.objects.get()
  366. self.assertEqual(n.error, "Received status code 500")
  367. @patch("hc.api.transports.requests.request", side_effect=Timeout)
  368. def test_slack_handles_timeout(self, mock_post):
  369. self._setup_data("slack", "123")
  370. self.channel.notify(self.check)
  371. n = Notification.objects.get()
  372. self.assertEqual(n.error, "Connection timed out")
  373. @patch("hc.api.transports.requests.request")
  374. def test_slack_with_tabs_in_schedule(self, mock_post):
  375. self._setup_data("slack", "123")
  376. self.check.kind = "cron"
  377. self.check.schedule = "*\t* * * *"
  378. self.check.save()
  379. mock_post.return_value.status_code = 200
  380. self.channel.notify(self.check)
  381. self.assertEqual(Notification.objects.count(), 1)
  382. self.assertTrue(mock_post.called)
  383. @patch("hc.api.transports.requests.request")
  384. def test_hipchat(self, mock_post):
  385. self._setup_data("hipchat", "123")
  386. self.channel.notify(self.check)
  387. self.assertFalse(mock_post.called)
  388. self.assertEqual(Notification.objects.count(), 0)
  389. @patch("hc.api.transports.requests.request")
  390. def test_opsgenie_with_legacy_value(self, mock_post):
  391. self._setup_data("opsgenie", "123")
  392. mock_post.return_value.status_code = 202
  393. self.channel.notify(self.check)
  394. n = Notification.objects.first()
  395. self.assertEqual(n.error, "")
  396. self.assertEqual(mock_post.call_count, 1)
  397. args, kwargs = mock_post.call_args
  398. self.assertIn("api.opsgenie.com", args[1])
  399. payload = kwargs["json"]
  400. self.assertIn("DOWN", payload["message"])
  401. @patch("hc.api.transports.requests.request")
  402. def test_opsgenie_up(self, mock_post):
  403. self._setup_data("opsgenie", "123", status="up")
  404. mock_post.return_value.status_code = 202
  405. self.channel.notify(self.check)
  406. n = Notification.objects.first()
  407. self.assertEqual(n.error, "")
  408. self.assertEqual(mock_post.call_count, 1)
  409. args, kwargs = mock_post.call_args
  410. method, url = args
  411. self.assertTrue(str(self.check.code) in url)
  412. @patch("hc.api.transports.requests.request")
  413. def test_opsgenie_with_json_value(self, mock_post):
  414. self._setup_data("opsgenie", json.dumps({"key": "456", "region": "eu"}))
  415. mock_post.return_value.status_code = 202
  416. self.channel.notify(self.check)
  417. n = Notification.objects.first()
  418. self.assertEqual(n.error, "")
  419. self.assertEqual(mock_post.call_count, 1)
  420. args, kwargs = mock_post.call_args
  421. self.assertIn("api.eu.opsgenie.com", args[1])
  422. @patch("hc.api.transports.requests.request")
  423. def test_opsgenie_returns_error(self, mock_post):
  424. self._setup_data("opsgenie", "123")
  425. mock_post.return_value.status_code = 403
  426. mock_post.return_value.json.return_value = {"message": "Nice try"}
  427. self.channel.notify(self.check)
  428. n = Notification.objects.first()
  429. self.assertEqual(n.error, 'Received status code 403 with a message: "Nice try"')
  430. @patch("hc.api.transports.requests.request")
  431. def test_pushover(self, mock_post):
  432. self._setup_data("po", "123|0")
  433. mock_post.return_value.status_code = 200
  434. self.channel.notify(self.check)
  435. assert Notification.objects.count() == 1
  436. args, kwargs = mock_post.call_args
  437. payload = kwargs["data"]
  438. self.assertIn("DOWN", payload["title"])
  439. @patch("hc.api.transports.requests.request")
  440. def test_pushover_up_priority(self, mock_post):
  441. self._setup_data("po", "123|0|2", status="up")
  442. mock_post.return_value.status_code = 200
  443. self.channel.notify(self.check)
  444. assert Notification.objects.count() == 1
  445. args, kwargs = mock_post.call_args
  446. payload = kwargs["data"]
  447. self.assertIn("UP", payload["title"])
  448. self.assertEqual(payload["priority"], 2)
  449. self.assertIn("retry", payload)
  450. self.assertIn("expire", payload)
  451. @patch("hc.api.transports.requests.request")
  452. def test_victorops(self, mock_post):
  453. self._setup_data("victorops", "123")
  454. mock_post.return_value.status_code = 200
  455. self.channel.notify(self.check)
  456. assert Notification.objects.count() == 1
  457. args, kwargs = mock_post.call_args
  458. payload = kwargs["json"]
  459. self.assertEqual(payload["message_type"], "CRITICAL")
  460. @patch("hc.api.transports.requests.request")
  461. def test_discord(self, mock_post):
  462. v = json.dumps({"webhook": {"url": "123"}})
  463. self._setup_data("discord", v)
  464. mock_post.return_value.status_code = 200
  465. self.channel.notify(self.check)
  466. assert Notification.objects.count() == 1
  467. args, kwargs = mock_post.call_args
  468. payload = kwargs["json"]
  469. attachment = payload["attachments"][0]
  470. fields = {f["title"]: f["value"] for f in attachment["fields"]}
  471. self.assertEqual(fields["Last Ping"], "an hour ago")
  472. @patch("hc.api.transports.requests.request")
  473. def test_discord_rewrites_discordapp_com(self, mock_post):
  474. v = json.dumps({"webhook": {"url": "https://discordapp.com/foo"}})
  475. self._setup_data("discord", v)
  476. mock_post.return_value.status_code = 200
  477. self.channel.notify(self.check)
  478. assert Notification.objects.count() == 1
  479. args, kwargs = mock_post.call_args
  480. url = args[1]
  481. # discordapp.com is deprecated. For existing webhook URLs, wwe should
  482. # rewrite discordapp.com to discord.com:
  483. self.assertEqual(url, "https://discord.com/foo/slack")
  484. @patch("hc.api.transports.requests.request")
  485. def test_pushbullet(self, mock_post):
  486. self._setup_data("pushbullet", "fake-token")
  487. mock_post.return_value.status_code = 200
  488. self.channel.notify(self.check)
  489. assert Notification.objects.count() == 1
  490. _, kwargs = mock_post.call_args
  491. self.assertEqual(kwargs["json"]["type"], "note")
  492. self.assertEqual(kwargs["headers"]["Access-Token"], "fake-token")
  493. @patch("hc.api.transports.requests.request")
  494. def test_telegram(self, mock_post):
  495. v = json.dumps({"id": 123})
  496. self._setup_data("telegram", v)
  497. mock_post.return_value.status_code = 200
  498. self.channel.notify(self.check)
  499. assert Notification.objects.count() == 1
  500. args, kwargs = mock_post.call_args
  501. payload = kwargs["json"]
  502. self.assertEqual(payload["chat_id"], 123)
  503. self.assertTrue("The check" in payload["text"])
  504. @patch("hc.api.transports.requests.request")
  505. def test_telegram_returns_error(self, mock_post):
  506. self._setup_data("telegram", json.dumps({"id": 123}))
  507. mock_post.return_value.status_code = 400
  508. mock_post.return_value.json.return_value = {"description": "Hi"}
  509. self.channel.notify(self.check)
  510. n = Notification.objects.first()
  511. self.assertEqual(n.error, 'Received status code 400 with a message: "Hi"')
  512. def test_telegram_obeys_rate_limit(self):
  513. self._setup_data("telegram", json.dumps({"id": 123}))
  514. TokenBucket.objects.create(value="tg-123", tokens=0)
  515. self.channel.notify(self.check)
  516. n = Notification.objects.first()
  517. self.assertEqual(n.error, "Rate limit exceeded")
  518. @patch("hc.api.transports.requests.request")
  519. def test_sms(self, mock_post):
  520. self._setup_data("sms", "+1234567890")
  521. self.check.last_ping = now() - td(hours=2)
  522. mock_post.return_value.status_code = 200
  523. self.channel.notify(self.check)
  524. n = Notification.objects.get()
  525. args, kwargs = mock_post.call_args
  526. payload = kwargs["data"]
  527. self.assertEqual(payload["To"], "+1234567890")
  528. self.assertFalse("\xa0" in payload["Body"])
  529. callback_path = f"/api/v1/notifications/{n.code}/status"
  530. self.assertTrue(payload["StatusCallback"].endswith(callback_path))
  531. # sent SMS counter should go up
  532. self.profile.refresh_from_db()
  533. self.assertEqual(self.profile.sms_sent, 1)
  534. @patch("hc.api.transports.requests.request")
  535. def test_sms_handles_json_value(self, mock_post):
  536. value = {"label": "foo", "value": "+1234567890"}
  537. self._setup_data("sms", json.dumps(value))
  538. self.check.last_ping = now() - td(hours=2)
  539. mock_post.return_value.status_code = 200
  540. self.channel.notify(self.check)
  541. assert Notification.objects.count() == 1
  542. args, kwargs = mock_post.call_args
  543. payload = kwargs["data"]
  544. self.assertEqual(payload["To"], "+1234567890")
  545. @patch("hc.api.transports.requests.request")
  546. def test_sms_limit(self, mock_post):
  547. # At limit already:
  548. self.profile.last_sms_date = now()
  549. self.profile.sms_sent = 50
  550. self.profile.save()
  551. self._setup_data("sms", "+1234567890")
  552. self.channel.notify(self.check)
  553. self.assertFalse(mock_post.called)
  554. n = Notification.objects.get()
  555. self.assertTrue("Monthly SMS limit exceeded" in n.error)
  556. # And email should have been sent
  557. self.assertEqual(len(mail.outbox), 1)
  558. email = mail.outbox[0]
  559. self.assertEqual(email.to[0], "[email protected]")
  560. self.assertEqual(email.subject, "Monthly SMS Limit Reached")
  561. @patch("hc.api.transports.requests.request")
  562. def test_sms_limit_reset(self, mock_post):
  563. # At limit, but also into a new month
  564. self.profile.sms_sent = 50
  565. self.profile.last_sms_date = now() - td(days=100)
  566. self.profile.save()
  567. self._setup_data("sms", "+1234567890")
  568. mock_post.return_value.status_code = 200
  569. self.channel.notify(self.check)
  570. self.assertTrue(mock_post.called)
  571. @patch("hc.api.transports.requests.request")
  572. def test_whatsapp(self, mock_post):
  573. definition = {"value": "+1234567890", "up": True, "down": True}
  574. self._setup_data("whatsapp", json.dumps(definition))
  575. self.check.last_ping = now() - td(hours=2)
  576. mock_post.return_value.status_code = 200
  577. self.channel.notify(self.check)
  578. args, kwargs = mock_post.call_args
  579. payload = kwargs["data"]
  580. self.assertEqual(payload["To"], "whatsapp:+1234567890")
  581. n = Notification.objects.get()
  582. callback_path = f"/api/v1/notifications/{n.code}/status"
  583. self.assertTrue(payload["StatusCallback"].endswith(callback_path))
  584. # sent SMS counter should go up
  585. self.profile.refresh_from_db()
  586. self.assertEqual(self.profile.sms_sent, 1)
  587. @patch("hc.api.transports.requests.request")
  588. def test_whatsapp_obeys_up_down_flags(self, mock_post):
  589. definition = {"value": "+1234567890", "up": True, "down": False}
  590. self._setup_data("whatsapp", json.dumps(definition))
  591. self.check.last_ping = now() - td(hours=2)
  592. self.channel.notify(self.check)
  593. self.assertEqual(Notification.objects.count(), 0)
  594. self.assertFalse(mock_post.called)
  595. @patch("hc.api.transports.requests.request")
  596. def test_whatsapp_limit(self, mock_post):
  597. # At limit already:
  598. self.profile.last_sms_date = now()
  599. self.profile.sms_sent = 50
  600. self.profile.save()
  601. definition = {"value": "+1234567890", "up": True, "down": True}
  602. self._setup_data("whatsapp", json.dumps(definition))
  603. self.channel.notify(self.check)
  604. self.assertFalse(mock_post.called)
  605. n = Notification.objects.get()
  606. self.assertTrue("Monthly message limit exceeded" in n.error)
  607. # And email should have been sent
  608. self.assertEqual(len(mail.outbox), 1)
  609. email = mail.outbox[0]
  610. self.assertEqual(email.to[0], "[email protected]")
  611. self.assertEqual(email.subject, "Monthly WhatsApp Limit Reached")
  612. @patch("hc.api.transports.requests.request")
  613. def test_call(self, mock_post):
  614. self.profile.call_limit = 1
  615. self.profile.save()
  616. value = {"label": "foo", "value": "+1234567890"}
  617. self._setup_data("call", json.dumps(value))
  618. self.check.last_ping = now() - td(hours=2)
  619. mock_post.return_value.status_code = 200
  620. self.channel.notify(self.check)
  621. args, kwargs = mock_post.call_args
  622. payload = kwargs["data"]
  623. self.assertEqual(payload["To"], "+1234567890")
  624. n = Notification.objects.get()
  625. callback_path = f"/api/v1/notifications/{n.code}/status"
  626. self.assertTrue(payload["StatusCallback"].endswith(callback_path))
  627. @patch("hc.api.transports.requests.request")
  628. def test_call_limit(self, mock_post):
  629. # At limit already:
  630. self.profile.last_call_date = now()
  631. self.profile.calls_sent = 50
  632. self.profile.save()
  633. definition = {"value": "+1234567890"}
  634. self._setup_data("call", json.dumps(definition))
  635. self.channel.notify(self.check)
  636. self.assertFalse(mock_post.called)
  637. n = Notification.objects.get()
  638. self.assertTrue("Monthly phone call limit exceeded" in n.error)
  639. # And email should have been sent
  640. self.assertEqual(len(mail.outbox), 1)
  641. email = mail.outbox[0]
  642. self.assertEqual(email.to[0], "[email protected]")
  643. self.assertEqual(email.subject, "Monthly Phone Call Limit Reached")
  644. @patch("hc.api.transports.requests.request")
  645. def test_call_limit_reset(self, mock_post):
  646. # At limit, but also into a new month
  647. self.profile.calls_sent = 50
  648. self.profile.last_call_date = now() - td(days=100)
  649. self.profile.save()
  650. self._setup_data("sms", "+1234567890")
  651. mock_post.return_value.status_code = 200
  652. self.channel.notify(self.check)
  653. self.assertTrue(mock_post.called)
  654. @patch("apprise.Apprise")
  655. @override_settings(APPRISE_ENABLED=True)
  656. def test_apprise_enabled(self, mock_apprise):
  657. self._setup_data("apprise", "123")
  658. mock_aobj = Mock()
  659. mock_aobj.add.return_value = True
  660. mock_aobj.notify.return_value = True
  661. mock_apprise.return_value = mock_aobj
  662. self.channel.notify(self.check)
  663. self.assertEqual(Notification.objects.count(), 1)
  664. self.check.status = "up"
  665. self.assertEqual(Notification.objects.count(), 1)
  666. @patch("apprise.Apprise")
  667. @override_settings(APPRISE_ENABLED=False)
  668. def test_apprise_disabled(self, mock_apprise):
  669. self._setup_data("apprise", "123")
  670. mock_aobj = Mock()
  671. mock_aobj.add.return_value = True
  672. mock_aobj.notify.return_value = True
  673. mock_apprise.return_value = mock_aobj
  674. self.channel.notify(self.check)
  675. self.assertEqual(Notification.objects.count(), 1)
  676. def test_not_implimented(self):
  677. self._setup_data("webhook", "http://example")
  678. self.channel.kind = "invalid"
  679. with self.assertRaises(NotImplementedError):
  680. self.channel.notify(self.check)
  681. @patch("hc.api.transports.requests.request")
  682. def test_msteams(self, mock_post):
  683. self._setup_data("msteams", "http://example.com/webhook")
  684. mock_post.return_value.status_code = 200
  685. self.check.name = "_underscores_ & more"
  686. self.channel.notify(self.check)
  687. assert Notification.objects.count() == 1
  688. args, kwargs = mock_post.call_args
  689. payload = kwargs["json"]
  690. self.assertEqual(payload["@type"], "MessageCard")
  691. # summary and title should be the same, except
  692. # title should have any special HTML characters escaped
  693. self.assertEqual(payload["summary"], "“_underscores_ & more” is DOWN.")
  694. self.assertEqual(payload["title"], "“_underscores_ & more” is DOWN.")
  695. @patch("hc.api.transports.requests.request")
  696. def test_msteams_escapes_html_and_markdown_in_desc(self, mock_post):
  697. self._setup_data("msteams", "http://example.com/webhook")
  698. mock_post.return_value.status_code = 200
  699. self.check.desc = """
  700. TEST _underscore_ `backticks` <u>underline</u> \\backslash\\ "quoted"
  701. """
  702. self.channel.notify(self.check)
  703. args, kwargs = mock_post.call_args
  704. text = kwargs["json"]["sections"][0]["text"]
  705. self.assertIn(r"\_underscore\_", text)
  706. self.assertIn(r"\`backticks\`", text)
  707. self.assertIn("&lt;u&gt;underline&lt;/u&gt;", text)
  708. self.assertIn(r"\\backslash\\ ", text)
  709. self.assertIn("&quot;quoted&quot;", text)
  710. @patch("hc.api.transports.os.system")
  711. @override_settings(SHELL_ENABLED=True)
  712. def test_shell(self, mock_system):
  713. definition = {"cmd_down": "logger hello", "cmd_up": ""}
  714. self._setup_data("shell", json.dumps(definition))
  715. mock_system.return_value = 0
  716. self.channel.notify(self.check)
  717. mock_system.assert_called_with("logger hello")
  718. @patch("hc.api.transports.os.system")
  719. @override_settings(SHELL_ENABLED=True)
  720. def test_shell_handles_nonzero_exit_code(self, mock_system):
  721. definition = {"cmd_down": "logger hello", "cmd_up": ""}
  722. self._setup_data("shell", json.dumps(definition))
  723. mock_system.return_value = 123
  724. self.channel.notify(self.check)
  725. n = Notification.objects.get()
  726. self.assertEqual(n.error, "Command returned exit code 123")
  727. @patch("hc.api.transports.os.system")
  728. @override_settings(SHELL_ENABLED=True)
  729. def test_shell_supports_variables(self, mock_system):
  730. definition = {"cmd_down": "logger $NAME is $STATUS ($TAG1)", "cmd_up": ""}
  731. self._setup_data("shell", json.dumps(definition))
  732. mock_system.return_value = 0
  733. self.check.name = "Database"
  734. self.check.tags = "foo bar"
  735. self.check.save()
  736. self.channel.notify(self.check)
  737. mock_system.assert_called_with("logger Database is down (foo)")
  738. @patch("hc.api.transports.os.system")
  739. @override_settings(SHELL_ENABLED=False)
  740. def test_shell_disabled(self, mock_system):
  741. definition = {"cmd_down": "logger hello", "cmd_up": ""}
  742. self._setup_data("shell", json.dumps(definition))
  743. self.channel.notify(self.check)
  744. self.assertFalse(mock_system.called)
  745. n = Notification.objects.get()
  746. self.assertEqual(n.error, "Shell commands are not enabled")
  747. @patch("hc.api.transports.requests.request")
  748. def test_zulip(self, mock_post):
  749. definition = {
  750. "bot_email": "[email protected]",
  751. "api_key": "fake-key",
  752. "mtype": "stream",
  753. "to": "general",
  754. }
  755. self._setup_data("zulip", json.dumps(definition))
  756. mock_post.return_value.status_code = 200
  757. self.channel.notify(self.check)
  758. assert Notification.objects.count() == 1
  759. args, kwargs = mock_post.call_args
  760. payload = kwargs["data"]
  761. self.assertIn("DOWN", payload["topic"])
  762. @patch("hc.api.transports.requests.request")
  763. def test_zulip_returns_error(self, mock_post):
  764. definition = {
  765. "bot_email": "[email protected]",
  766. "api_key": "fake-key",
  767. "mtype": "stream",
  768. "to": "general",
  769. }
  770. self._setup_data("zulip", json.dumps(definition))
  771. mock_post.return_value.status_code = 403
  772. mock_post.return_value.json.return_value = {"msg": "Nice try"}
  773. self.channel.notify(self.check)
  774. n = Notification.objects.first()
  775. self.assertEqual(n.error, 'Received status code 403 with a message: "Nice try"')