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.

379 lines
12 KiB

  1. # coding: utf-8
  2. from datetime import timedelta as td
  3. import json
  4. from unittest.mock import patch
  5. from django.utils.timezone import now
  6. from hc.api.models import Channel, Check, Notification
  7. from hc.test import BaseTestCase
  8. from requests.exceptions import ConnectionError, Timeout
  9. from django.test.utils import override_settings
  10. class NotifyWebhookTestCase(BaseTestCase):
  11. def _setup_data(self, value, status="down", email_verified=True):
  12. self.check = Check(project=self.project)
  13. self.check.status = status
  14. self.check.last_ping = now() - td(minutes=61)
  15. self.check.save()
  16. self.channel = Channel(project=self.project)
  17. self.channel.kind = "webhook"
  18. self.channel.value = value
  19. self.channel.email_verified = email_verified
  20. self.channel.save()
  21. self.channel.checks.add(self.check)
  22. @patch("hc.api.transports.requests.request")
  23. def test_webhook(self, mock_get):
  24. definition = {
  25. "method_down": "GET",
  26. "url_down": "http://example",
  27. "body_down": "",
  28. "headers_down": {},
  29. }
  30. self._setup_data(json.dumps(definition))
  31. mock_get.return_value.status_code = 200
  32. self.channel.notify(self.check)
  33. mock_get.assert_called_with(
  34. "get",
  35. "http://example",
  36. headers={"User-Agent": "healthchecks.io"},
  37. timeout=5,
  38. )
  39. @patch("hc.api.transports.requests.request", side_effect=Timeout)
  40. def test_webhooks_handle_timeouts(self, mock_get):
  41. definition = {
  42. "method_down": "GET",
  43. "url_down": "http://example",
  44. "body_down": "",
  45. "headers_down": {},
  46. }
  47. self._setup_data(json.dumps(definition))
  48. self.channel.notify(self.check)
  49. # The transport should have retried 3 times
  50. self.assertEqual(mock_get.call_count, 3)
  51. n = Notification.objects.get()
  52. self.assertEqual(n.error, "Connection timed out")
  53. self.channel.refresh_from_db()
  54. self.assertEqual(self.channel.last_error, "Connection timed out")
  55. @patch("hc.api.transports.requests.request", side_effect=ConnectionError)
  56. def test_webhooks_handle_connection_errors(self, mock_get):
  57. definition = {
  58. "method_down": "GET",
  59. "url_down": "http://example",
  60. "body_down": "",
  61. "headers_down": {},
  62. }
  63. self._setup_data(json.dumps(definition))
  64. self.channel.notify(self.check)
  65. # The transport should have retried 3 times
  66. self.assertEqual(mock_get.call_count, 3)
  67. n = Notification.objects.get()
  68. self.assertEqual(n.error, "Connection failed")
  69. @patch("hc.api.transports.requests.request")
  70. def test_webhooks_handle_500(self, mock_get):
  71. definition = {
  72. "method_down": "GET",
  73. "url_down": "http://example",
  74. "body_down": "",
  75. "headers_down": {},
  76. }
  77. self._setup_data(json.dumps(definition))
  78. mock_get.return_value.status_code = 500
  79. self.channel.notify(self.check)
  80. # The transport should have retried 3 times
  81. self.assertEqual(mock_get.call_count, 3)
  82. n = Notification.objects.get()
  83. self.assertEqual(n.error, "Received status code 500")
  84. @patch("hc.api.transports.requests.request", side_effect=Timeout)
  85. def test_webhooks_dont_retry_when_sending_test_notifications(self, mock_get):
  86. definition = {
  87. "method_down": "GET",
  88. "url_down": "http://example",
  89. "body_down": "",
  90. "headers_down": {},
  91. }
  92. self._setup_data(json.dumps(definition))
  93. self.channel.notify(self.check, is_test=True)
  94. # is_test flag is set, the transport should not retry:
  95. self.assertEqual(mock_get.call_count, 1)
  96. n = Notification.objects.get()
  97. self.assertEqual(n.error, "Connection timed out")
  98. @patch("hc.api.transports.requests.request")
  99. def test_webhooks_support_variables(self, mock_get):
  100. definition = {
  101. "method_down": "GET",
  102. "url_down": "http://host/$CODE/$STATUS/$TAG1/$TAG2/?name=$NAME",
  103. "body_down": "",
  104. "headers_down": {},
  105. }
  106. self._setup_data(json.dumps(definition))
  107. self.check.name = "Hello World"
  108. self.check.tags = "foo bar"
  109. self.check.save()
  110. self.channel.notify(self.check)
  111. url = "http://host/%s/down/foo/bar/?name=Hello%%20World" % self.check.code
  112. args, kwargs = mock_get.call_args
  113. self.assertEqual(args[0], "get")
  114. self.assertEqual(args[1], url)
  115. self.assertEqual(kwargs["headers"], {"User-Agent": "healthchecks.io"})
  116. self.assertEqual(kwargs["timeout"], 5)
  117. @patch("hc.api.transports.requests.request")
  118. def test_webhooks_handle_variable_variables(self, mock_get):
  119. definition = {
  120. "method_down": "GET",
  121. "url_down": "http://host/$$NAMETAG1",
  122. "body_down": "",
  123. "headers_down": {},
  124. }
  125. self._setup_data(json.dumps(definition))
  126. self.check.tags = "foo bar"
  127. self.check.save()
  128. self.channel.notify(self.check)
  129. # $$NAMETAG1 should *not* get transformed to "foo"
  130. args, kwargs = mock_get.call_args
  131. self.assertEqual(args[1], "http://host/$TAG1")
  132. @patch("hc.api.transports.requests.request")
  133. def test_webhooks_support_post(self, mock_request):
  134. definition = {
  135. "method_down": "POST",
  136. "url_down": "http://example.com",
  137. "body_down": "The Time Is $NOW",
  138. "headers_down": {},
  139. }
  140. self._setup_data(json.dumps(definition))
  141. self.check.save()
  142. self.channel.notify(self.check)
  143. args, kwargs = mock_request.call_args
  144. self.assertEqual(args[0], "post")
  145. self.assertEqual(args[1], "http://example.com")
  146. # spaces should not have been urlencoded:
  147. payload = kwargs["data"].decode()
  148. self.assertTrue(payload.startswith("The Time Is 2"))
  149. @patch("hc.api.transports.requests.request")
  150. def test_webhooks_dollarsign_escaping(self, mock_get):
  151. # If name or tag contains what looks like a variable reference,
  152. # that should be left alone:
  153. definition = {
  154. "method_down": "GET",
  155. "url_down": "http://host/$NAME",
  156. "body_down": "",
  157. "headers_down": {},
  158. }
  159. self._setup_data(json.dumps(definition))
  160. self.check.name = "$TAG1"
  161. self.check.tags = "foo"
  162. self.check.save()
  163. self.channel.notify(self.check)
  164. url = "http://host/%24TAG1"
  165. mock_get.assert_called_with(
  166. "get", url, headers={"User-Agent": "healthchecks.io"}, timeout=5
  167. )
  168. @patch("hc.api.transports.requests.request")
  169. def test_webhooks_handle_up_events(self, mock_get):
  170. definition = {
  171. "method_up": "GET",
  172. "url_up": "http://bar",
  173. "body_up": "",
  174. "headers_up": {},
  175. }
  176. self._setup_data(json.dumps(definition), status="up")
  177. self.channel.notify(self.check)
  178. mock_get.assert_called_with(
  179. "get", "http://bar", headers={"User-Agent": "healthchecks.io"}, timeout=5
  180. )
  181. @patch("hc.api.transports.requests.request")
  182. def test_webhooks_handle_noop_up_events(self, mock_get):
  183. definition = {
  184. "method_up": "GET",
  185. "url_up": "",
  186. "body_up": "",
  187. "headers_up": {},
  188. }
  189. self._setup_data(json.dumps(definition), status="up")
  190. self.channel.notify(self.check)
  191. self.assertFalse(mock_get.called)
  192. self.assertEqual(Notification.objects.count(), 0)
  193. @patch("hc.api.transports.requests.request")
  194. def test_webhooks_handle_unicode_post_body(self, mock_request):
  195. definition = {
  196. "method_down": "POST",
  197. "url_down": "http://foo.com",
  198. "body_down": "(╯°□°)╯︵ ┻━┻",
  199. "headers_down": {},
  200. }
  201. self._setup_data(json.dumps(definition))
  202. self.check.save()
  203. self.channel.notify(self.check)
  204. args, kwargs = mock_request.call_args
  205. # unicode should be encoded into utf-8
  206. self.assertIsInstance(kwargs["data"], bytes)
  207. @patch("hc.api.transports.requests.request")
  208. def test_webhooks_handle_post_headers(self, mock_request):
  209. definition = {
  210. "method_down": "POST",
  211. "url_down": "http://foo.com",
  212. "body_down": "data",
  213. "headers_down": {"Content-Type": "application/json"},
  214. }
  215. self._setup_data(json.dumps(definition))
  216. self.channel.notify(self.check)
  217. headers = {"User-Agent": "healthchecks.io", "Content-Type": "application/json"}
  218. mock_request.assert_called_with(
  219. "post", "http://foo.com", data=b"data", headers=headers, timeout=5
  220. )
  221. @patch("hc.api.transports.requests.request")
  222. def test_webhooks_handle_get_headers(self, mock_request):
  223. definition = {
  224. "method_down": "GET",
  225. "url_down": "http://foo.com",
  226. "body_down": "",
  227. "headers_down": {"Content-Type": "application/json"},
  228. }
  229. self._setup_data(json.dumps(definition))
  230. self.channel.notify(self.check)
  231. headers = {"User-Agent": "healthchecks.io", "Content-Type": "application/json"}
  232. mock_request.assert_called_with(
  233. "get", "http://foo.com", headers=headers, timeout=5
  234. )
  235. @patch("hc.api.transports.requests.request")
  236. def test_webhooks_allow_user_agent_override(self, mock_request):
  237. definition = {
  238. "method_down": "GET",
  239. "url_down": "http://foo.com",
  240. "body_down": "",
  241. "headers_down": {"User-Agent": "My-Agent"},
  242. }
  243. self._setup_data(json.dumps(definition))
  244. self.channel.notify(self.check)
  245. headers = {"User-Agent": "My-Agent"}
  246. mock_request.assert_called_with(
  247. "get", "http://foo.com", headers=headers, timeout=5
  248. )
  249. @patch("hc.api.transports.requests.request")
  250. def test_webhooks_support_variables_in_headers(self, mock_request):
  251. definition = {
  252. "method_down": "GET",
  253. "url_down": "http://foo.com",
  254. "body_down": "",
  255. "headers_down": {"X-Message": "$NAME is DOWN"},
  256. }
  257. self._setup_data(json.dumps(definition))
  258. self.check.name = "Foo"
  259. self.check.save()
  260. self.channel.notify(self.check)
  261. headers = {"User-Agent": "healthchecks.io", "X-Message": "Foo is DOWN"}
  262. mock_request.assert_called_with(
  263. "get", "http://foo.com", headers=headers, timeout=5
  264. )
  265. @override_settings(WEBHOOKS_ENABLED=False)
  266. def test_it_requires_webhooks_enabled(self):
  267. definition = {
  268. "method_down": "GET",
  269. "url_down": "http://example",
  270. "body_down": "",
  271. "headers_down": {},
  272. }
  273. self._setup_data(json.dumps(definition))
  274. self.channel.notify(self.check)
  275. n = Notification.objects.get()
  276. self.assertEqual(n.error, "Webhook notifications are not enabled.")
  277. @patch("hc.api.transports.requests.request")
  278. def test_webhooks_handle_non_ascii_in_headers(self, mock_request):
  279. definition = {
  280. "method_down": "GET",
  281. "url_down": "http://foo.com",
  282. "headers_down": {"X-Foo": "bār"},
  283. "body_down": "",
  284. }
  285. self._setup_data(json.dumps(definition))
  286. self.check.save()
  287. self.channel.notify(self.check)
  288. args, kwargs = mock_request.call_args
  289. self.assertEqual(kwargs["headers"]["X-Foo"], "bār")
  290. @patch("hc.api.transports.requests.request")
  291. def test_webhooks_handle_latin1_in_headers(self, mock_request):
  292. definition = {
  293. "method_down": "GET",
  294. "url_down": "http://foo.com",
  295. "headers_down": {"X-Foo": "½"},
  296. "body_down": "",
  297. }
  298. self._setup_data(json.dumps(definition))
  299. self.check.save()
  300. self.channel.notify(self.check)
  301. args, kwargs = mock_request.call_args
  302. self.assertEqual(kwargs["headers"]["X-Foo"], "½")