test_utils.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. import responses
  2. from django.core.urlresolvers import reverse
  3. import pytest
  4. from sentry.incidents.logic import CRITICAL_TRIGGER_LABEL
  5. from sentry.integrations.slack.utils import (
  6. build_group_attachment,
  7. build_incident_attachment,
  8. CHANNEL_PREFIX,
  9. get_channel_id,
  10. MEMBER_PREFIX,
  11. RESOLVED_COLOR,
  12. LEVEL_TO_COLOR,
  13. parse_link,
  14. )
  15. from sentry.models import Integration
  16. from sentry.testutils import TestCase
  17. from sentry.utils import json
  18. from sentry.utils.assets import get_asset_url
  19. from sentry.utils.dates import to_timestamp
  20. from sentry.utils.http import absolute_uri
  21. from sentry.shared_integrations.exceptions import DuplicateDisplayNameError
  22. class GetChannelIdBotTest(TestCase):
  23. def setUp(self):
  24. self.resp = responses.mock
  25. self.resp.__enter__()
  26. self.integration = Integration.objects.create(
  27. provider="slack",
  28. name="Awesome Team",
  29. external_id="TXXXXXXX1",
  30. metadata={
  31. "access_token": "xoxb-xxxxxxxxx-xxxxxxxxxx-xxxxxxxxxxxx",
  32. "installation_type": "born_as_bot",
  33. },
  34. )
  35. self.integration.add_organization(self.event.project.organization, self.user)
  36. self.add_list_response(
  37. "conversations",
  38. [
  39. {"name": "my-channel", "id": "m-c"},
  40. {"name": "other-chann", "id": "o-c"},
  41. {"name": "my-private-channel", "id": "m-p-c", "is_private": True},
  42. ],
  43. result_name="channels",
  44. )
  45. self.add_list_response(
  46. "users",
  47. [
  48. {"name": "first-morty", "id": "m", "profile": {"display_name": "Morty"}},
  49. {"name": "other-user", "id": "o-u", "profile": {"display_name": "Jimbob"}},
  50. {"name": "better_morty", "id": "bm", "profile": {"display_name": "Morty"}},
  51. ],
  52. result_name="members",
  53. )
  54. def tearDown(self):
  55. self.resp.__exit__(None, None, None)
  56. def add_list_response(self, list_type, channels, result_name="channels"):
  57. self.resp.add(
  58. method=responses.GET,
  59. url="https://slack.com/api/%s.list" % list_type,
  60. status=200,
  61. content_type="application/json",
  62. body=json.dumps({"ok": "true", result_name: channels}),
  63. )
  64. def run_valid_test(self, channel, expected_prefix, expected_id, timed_out):
  65. assert (expected_prefix, expected_id, timed_out) == get_channel_id(
  66. self.organization, self.integration, channel
  67. )
  68. def test_valid_channel_selected(self):
  69. self.run_valid_test("#My-Channel", CHANNEL_PREFIX, "m-c", False)
  70. def test_valid_private_channel_selected(self):
  71. self.run_valid_test("#my-private-channel", CHANNEL_PREFIX, "m-p-c", False)
  72. def test_valid_member_selected(self):
  73. self.run_valid_test("@first-morty", MEMBER_PREFIX, "m", False)
  74. def test_valid_member_selected_display_name(self):
  75. self.run_valid_test("@Jimbob", MEMBER_PREFIX, "o-u", False)
  76. def test_invalid_member_selected_display_name(self):
  77. with pytest.raises(DuplicateDisplayNameError):
  78. get_channel_id(self.organization, self.integration, "@Morty")
  79. def test_invalid_channel_selected(self):
  80. assert get_channel_id(self.organization, self.integration, "#fake-channel")[1] is None
  81. assert get_channel_id(self.organization, self.integration, "@fake-user")[1] is None
  82. class BuildIncidentAttachmentTest(TestCase):
  83. def test_simple(self):
  84. logo_url = absolute_uri(get_asset_url("sentry", "images/sentry-email-avatar.png"))
  85. alert_rule = self.create_alert_rule()
  86. incident = self.create_incident(alert_rule=alert_rule, status=2)
  87. trigger = self.create_alert_rule_trigger(alert_rule, CRITICAL_TRIGGER_LABEL, 100)
  88. action = self.create_alert_rule_trigger_action(
  89. alert_rule_trigger=trigger, triggered_for_incident=incident
  90. )
  91. title = f"Resolved: {alert_rule.name}"
  92. incident_footer_ts = (
  93. "<!date^{:.0f}^Sentry Incident - Started {} at {} | Sentry Incident>".format(
  94. to_timestamp(incident.date_started), "{date_pretty}", "{time}"
  95. )
  96. )
  97. assert build_incident_attachment(action, incident) == {
  98. "fallback": title,
  99. "title": title,
  100. "title_link": absolute_uri(
  101. reverse(
  102. "sentry-metric-alert",
  103. kwargs={
  104. "organization_slug": incident.organization.slug,
  105. "incident_id": incident.identifier,
  106. },
  107. )
  108. ),
  109. "text": "0 events in the last 10 minutes\nFilter: level:error",
  110. "fields": [],
  111. "mrkdwn_in": ["text"],
  112. "footer_icon": logo_url,
  113. "footer": incident_footer_ts,
  114. "color": RESOLVED_COLOR,
  115. "actions": [],
  116. }
  117. def test_metric_value(self):
  118. logo_url = absolute_uri(get_asset_url("sentry", "images/sentry-email-avatar.png"))
  119. alert_rule = self.create_alert_rule()
  120. incident = self.create_incident(alert_rule=alert_rule, status=2)
  121. title = f"Critical: {alert_rule.name}" # This test will use the action/method and not the incident to build status
  122. metric_value = 5000
  123. trigger = self.create_alert_rule_trigger(alert_rule, CRITICAL_TRIGGER_LABEL, 100)
  124. action = self.create_alert_rule_trigger_action(
  125. alert_rule_trigger=trigger, triggered_for_incident=incident
  126. )
  127. incident_footer_ts = (
  128. "<!date^{:.0f}^Sentry Incident - Started {} at {} | Sentry Incident>".format(
  129. to_timestamp(incident.date_started), "{date_pretty}", "{time}"
  130. )
  131. )
  132. # This should fail because it pulls status from `action` instead of `incident`
  133. assert build_incident_attachment(
  134. action, incident, metric_value=metric_value, method="fire"
  135. ) == {
  136. "fallback": title,
  137. "title": title,
  138. "title_link": absolute_uri(
  139. reverse(
  140. "sentry-metric-alert",
  141. kwargs={
  142. "organization_slug": incident.organization.slug,
  143. "incident_id": incident.identifier,
  144. },
  145. )
  146. ),
  147. "text": f"{metric_value} events in the last 10 minutes\nFilter: level:error",
  148. "fields": [],
  149. "mrkdwn_in": ["text"],
  150. "footer_icon": logo_url,
  151. "footer": incident_footer_ts,
  152. "color": LEVEL_TO_COLOR["fatal"],
  153. "actions": [],
  154. }
  155. def test_build_group_attachment(self):
  156. self.user = self.create_user("foo@example.com")
  157. self.org = self.create_organization(name="Rowdy Tiger", owner=None)
  158. self.team = self.create_team(organization=self.org, name="Mariachi Band")
  159. self.project = self.create_project(
  160. organization=self.org, teams=[self.team], name="Bengal-Elephant-Giraffe-Tree-House"
  161. )
  162. self.create_member(user=self.user, organization=self.org, role="owner", teams=[self.team])
  163. group = self.create_group(project=self.project)
  164. ts = group.last_seen
  165. assert build_group_attachment(group) == {
  166. "color": "#E03E2F",
  167. "text": "",
  168. "actions": [
  169. {"name": "status", "text": "Resolve", "type": "button", "value": "resolved"},
  170. {"text": "Ignore", "type": "button", "name": "status", "value": "ignored"},
  171. {
  172. "option_groups": [
  173. {
  174. "text": "Teams",
  175. "options": [
  176. {
  177. "text": "#mariachi-band",
  178. "value": "team:" + str(self.team.id),
  179. }
  180. ],
  181. },
  182. {
  183. "text": "People",
  184. "options": [
  185. {
  186. "text": "foo@example.com",
  187. "value": "user:" + str(self.user.id),
  188. }
  189. ],
  190. },
  191. ],
  192. "text": "Select Assignee...",
  193. "selected_options": [None],
  194. "type": "select",
  195. "name": "assign",
  196. },
  197. ],
  198. "mrkdwn_in": ["text"],
  199. "title": group.title,
  200. "fields": [],
  201. "footer": "BENGAL-ELEPHANT-GIRAFFE-TREE-HOUSE-1",
  202. "ts": to_timestamp(ts),
  203. "title_link": "http://testserver/organizations/rowdy-tiger/issues/"
  204. + str(group.id)
  205. + "/?referrer=slack",
  206. "callback_id": '{"issue":' + str(group.id) + "}",
  207. "fallback": f"[{self.project.slug}] {group.title}",
  208. "footer_icon": "http://testserver/_static/{version}/sentry/images/sentry-email-avatar.png",
  209. }
  210. event = self.store_event(data={}, project_id=self.project.id)
  211. ts = event.datetime
  212. assert build_group_attachment(group, event) == {
  213. "color": "#E03E2F",
  214. "text": "",
  215. "actions": [
  216. {"name": "status", "text": "Resolve", "type": "button", "value": "resolved"},
  217. {"text": "Ignore", "type": "button", "name": "status", "value": "ignored"},
  218. {
  219. "option_groups": [
  220. {
  221. "text": "Teams",
  222. "options": [
  223. {
  224. "text": "#mariachi-band",
  225. "value": "team:" + str(self.team.id),
  226. }
  227. ],
  228. },
  229. {
  230. "text": "People",
  231. "options": [
  232. {
  233. "text": "foo@example.com",
  234. "value": "user:" + str(self.user.id),
  235. }
  236. ],
  237. },
  238. ],
  239. "text": "Select Assignee...",
  240. "selected_options": [None],
  241. "type": "select",
  242. "name": "assign",
  243. },
  244. ],
  245. "mrkdwn_in": ["text"],
  246. "title": event.title,
  247. "fields": [],
  248. "footer": "BENGAL-ELEPHANT-GIRAFFE-TREE-HOUSE-1",
  249. "ts": to_timestamp(ts),
  250. "title_link": "http://testserver/organizations/rowdy-tiger/issues/"
  251. + str(group.id)
  252. + "/?referrer=slack",
  253. "callback_id": '{"issue":' + str(group.id) + "}",
  254. "fallback": f"[{self.project.slug}] {event.title}",
  255. "footer_icon": "http://testserver/_static/{version}/sentry/images/sentry-email-avatar.png",
  256. }
  257. assert build_group_attachment(group, event, link_to_event=True) == {
  258. "color": "#E03E2F",
  259. "text": "",
  260. "actions": [
  261. {"name": "status", "text": "Resolve", "type": "button", "value": "resolved"},
  262. {"text": "Ignore", "type": "button", "name": "status", "value": "ignored"},
  263. {
  264. "option_groups": [
  265. {
  266. "text": "Teams",
  267. "options": [
  268. {
  269. "text": "#mariachi-band",
  270. "value": "team:" + str(self.team.id),
  271. }
  272. ],
  273. },
  274. {
  275. "text": "People",
  276. "options": [
  277. {
  278. "text": "foo@example.com",
  279. "value": "user:" + str(self.user.id),
  280. }
  281. ],
  282. },
  283. ],
  284. "text": "Select Assignee...",
  285. "selected_options": [None],
  286. "type": "select",
  287. "name": "assign",
  288. },
  289. ],
  290. "mrkdwn_in": ["text"],
  291. "title": event.title,
  292. "fields": [],
  293. "footer": "BENGAL-ELEPHANT-GIRAFFE-TREE-HOUSE-1",
  294. "ts": to_timestamp(ts),
  295. "title_link": f"http://testserver/organizations/rowdy-tiger/issues/{group.id}/events/{event.event_id}/"
  296. + "?referrer=slack",
  297. "callback_id": '{"issue":' + str(group.id) + "}",
  298. "fallback": f"[{self.project.slug}] {event.title}",
  299. "footer_icon": "http://testserver/_static/{version}/sentry/images/sentry-email-avatar.png",
  300. }
  301. def test_build_group_attachment_color_no_event_error_fallback(self):
  302. group_with_no_events = self.create_group(project=self.project)
  303. assert build_group_attachment(group_with_no_events)["color"] == "#E03E2F"
  304. def test_build_group_attachment_color_unxpected_level_error_fallback(self):
  305. unexpected_level_event = self.store_event(
  306. data={"level": "trace"}, project_id=self.project.id, assert_no_errors=False
  307. )
  308. assert build_group_attachment(unexpected_level_event.group)["color"] == "#E03E2F"
  309. def test_build_group_attachment_color_warning(self):
  310. warning_event = self.store_event(data={"level": "warning"}, project_id=self.project.id)
  311. assert build_group_attachment(warning_event.group)["color"] == "#FFC227"
  312. assert build_group_attachment(warning_event.group, warning_event)["color"] == "#FFC227"
  313. def test_parse_link(self):
  314. link = "https://meowlificent.ngrok.io/organizations/sentry/issues/167/?project=2&query=is%3Aunresolved"
  315. link2 = "https://meowlificent.ngrok.io/organizations/sentry/issues/1/events/2d113519854c4f7a85bae8b69c7404ad/?project=2"
  316. link3 = "https://meowlificent.ngrok.io/organizations/sentry/issues/9998089891/events/198e93sfa99d41b993ac8ae5dc384642/events/"
  317. assert (
  318. parse_link(link)
  319. == "organizations/{organization}/issues/{issue_id}/project=%7Bproject%7D&query=%5B%27is%3Aunresolved%27%5D"
  320. )
  321. assert (
  322. parse_link(link2)
  323. == "organizations/{organization}/issues/{issue_id}/events/{event_id}/project=%7Bproject%7D"
  324. )
  325. assert (
  326. parse_link(link3)
  327. == "organizations/{organization}/issues/{issue_id}/events/{event_id}/events/"
  328. )