test_group_details.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. from unittest import mock
  2. from rest_framework.exceptions import ErrorDetail
  3. from sentry import tsdb
  4. from sentry.issues.forecasts import generate_and_save_forecasts
  5. from sentry.models.activity import Activity
  6. from sentry.models.environment import Environment
  7. from sentry.models.group import GroupStatus
  8. from sentry.models.groupinbox import GroupInboxReason, add_group_to_inbox, remove_group_from_inbox
  9. from sentry.models.groupowner import GROUP_OWNER_TYPE, GroupOwner, GroupOwnerType
  10. from sentry.models.release import Release
  11. from sentry.testutils.cases import APITestCase, SnubaTestCase
  12. from sentry.testutils.helpers.datetime import before_now, iso_format
  13. from sentry.testutils.helpers.features import with_feature
  14. from sentry.types.activity import ActivityType
  15. from sentry.types.group import PriorityLevel
  16. class GroupDetailsTest(APITestCase, SnubaTestCase):
  17. def test_multiple_environments(self):
  18. group = self.create_group()
  19. self.login_as(user=self.user)
  20. environment = Environment.get_or_create(group.project, "production")
  21. environment2 = Environment.get_or_create(group.project, "staging")
  22. url = f"/api/0/issues/{group.id}/"
  23. with mock.patch(
  24. "sentry.api.endpoints.group_details.tsdb.backend.get_range",
  25. side_effect=tsdb.backend.get_range,
  26. ) as get_range:
  27. response = self.client.get(
  28. f"{url}?environment=production&environment=staging", format="json"
  29. )
  30. assert response.status_code == 200
  31. assert get_range.call_count == 2
  32. for args, kwargs in get_range.call_args_list:
  33. assert kwargs["environment_ids"] == [environment.id, environment2.id]
  34. response = self.client.get(f"{url}?environment=invalid", format="json")
  35. assert response.status_code == 404
  36. def test_with_first_last_release(self):
  37. self.login_as(user=self.user)
  38. first_release = {
  39. "firstEvent": before_now(minutes=3),
  40. "lastEvent": before_now(minutes=2, seconds=30),
  41. }
  42. last_release = {
  43. "firstEvent": before_now(minutes=1, seconds=30),
  44. "lastEvent": before_now(minutes=1),
  45. }
  46. for timestamp in first_release.values():
  47. self.store_event(
  48. data={"release": "1.0", "timestamp": iso_format(timestamp)},
  49. project_id=self.project.id,
  50. )
  51. self.store_event(
  52. data={"release": "1.1", "timestamp": iso_format(before_now(minutes=2))},
  53. project_id=self.project.id,
  54. )
  55. event = [
  56. self.store_event(
  57. data={"release": "1.0a", "timestamp": iso_format(timestamp)},
  58. project_id=self.project.id,
  59. )
  60. for timestamp in last_release.values()
  61. ][-1]
  62. group = event.group
  63. url = f"/api/0/issues/{group.id}/"
  64. response = self.client.get(url, format="json")
  65. assert response.status_code == 200, response.content
  66. assert response.data["id"] == str(group.id)
  67. release = response.data["firstRelease"]
  68. assert release["version"] == "1.0"
  69. for event, timestamp in first_release.items():
  70. assert release[event].ctime() == timestamp.ctime()
  71. release = response.data["lastRelease"]
  72. assert release["version"] == "1.0a"
  73. for event, timestamp in last_release.items():
  74. assert release[event].ctime() == timestamp.ctime()
  75. def test_first_last_only_one_tagstore(self):
  76. self.login_as(user=self.user)
  77. event = self.store_event(
  78. data={"release": "1.0", "timestamp": iso_format(before_now(days=3))},
  79. project_id=self.project.id,
  80. )
  81. self.store_event(
  82. data={"release": "1.1", "timestamp": iso_format(before_now(minutes=3))},
  83. project_id=self.project.id,
  84. )
  85. group = event.group
  86. url = f"/api/0/issues/{group.id}/"
  87. with mock.patch("sentry.tagstore.backend.get_release_tags") as get_release_tags:
  88. response = self.client.get(url, format="json")
  89. assert response.status_code == 200
  90. assert get_release_tags.call_count == 1
  91. def test_first_release_only(self):
  92. self.login_as(user=self.user)
  93. first_event = before_now(days=3)
  94. self.store_event(
  95. data={"release": "1.0", "timestamp": iso_format(first_event)},
  96. project_id=self.project.id,
  97. )
  98. event = self.store_event(
  99. data={"release": "1.1", "timestamp": iso_format(before_now(days=1))},
  100. project_id=self.project.id,
  101. )
  102. # Forcibly remove one of the releases
  103. Release.objects.get(version="1.1").delete()
  104. group = event.group
  105. url = f"/api/0/issues/{group.id}/"
  106. response = self.client.get(url, format="json")
  107. assert response.status_code == 200, response.content
  108. assert response.data["firstRelease"]["version"] == "1.0"
  109. # only one event
  110. assert (
  111. response.data["firstRelease"]["firstEvent"]
  112. == response.data["firstRelease"]["lastEvent"]
  113. )
  114. assert response.data["firstRelease"]["firstEvent"].ctime() == first_event.ctime()
  115. assert response.data["lastRelease"] is None
  116. def test_group_expand_inbox(self):
  117. self.login_as(user=self.user)
  118. event = self.store_event(
  119. data={"timestamp": iso_format(before_now(minutes=3))},
  120. project_id=self.project.id,
  121. )
  122. group = event.group
  123. add_group_to_inbox(group, GroupInboxReason.NEW)
  124. url = f"/api/0/issues/{group.id}/?expand=inbox"
  125. response = self.client.get(url, format="json")
  126. assert response.status_code == 200, response.content
  127. assert response.data["inbox"] is not None
  128. assert response.data["inbox"]["reason"] == GroupInboxReason.NEW.value
  129. assert response.data["inbox"]["reason_details"] is None
  130. remove_group_from_inbox(event.group)
  131. response = self.client.get(url, format="json")
  132. assert response.status_code == 200, response.content
  133. assert response.data["inbox"] is None
  134. def test_group_expand_owners(self):
  135. self.login_as(user=self.user)
  136. event = self.store_event(
  137. data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]},
  138. project_id=self.project.id,
  139. )
  140. group = event.group
  141. url = f"/api/0/issues/{group.id}/?expand=owners"
  142. self.login_as(user=self.user)
  143. # Test with no owner
  144. response = self.client.get(url, format="json")
  145. assert response.status_code == 200
  146. assert response.data["owners"] is None
  147. # Test with owners
  148. GroupOwner.objects.create(
  149. group=event.group,
  150. project=event.project,
  151. organization=event.project.organization,
  152. type=GroupOwnerType.SUSPECT_COMMIT.value,
  153. user_id=self.user.id,
  154. )
  155. response = self.client.get(url, format="json")
  156. assert response.status_code == 200, response.content
  157. assert response.data["owners"] is not None
  158. assert len(response.data["owners"]) == 1
  159. assert response.data["owners"][0]["owner"] == f"user:{self.user.id}"
  160. assert response.data["owners"][0]["type"] == GROUP_OWNER_TYPE[GroupOwnerType.SUSPECT_COMMIT]
  161. def test_group_expand_forecasts(self):
  162. self.login_as(user=self.user)
  163. event = self.store_event(
  164. data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]},
  165. project_id=self.project.id,
  166. )
  167. group = event.group
  168. generate_and_save_forecasts([group])
  169. url = f"/api/0/issues/{group.id}/?expand=forecast"
  170. response = self.client.get(url, format="json")
  171. assert response.status_code == 200, response.content
  172. assert response.data["forecast"] is not None
  173. assert response.data["forecast"]["data"] is not None
  174. assert response.data["forecast"]["date_added"] is not None
  175. @with_feature("projects:issue-priority")
  176. def test_group_get_priority(self):
  177. self.login_as(user=self.user)
  178. group = self.create_group(
  179. project=self.project,
  180. status=GroupStatus.IGNORED,
  181. priority=PriorityLevel.LOW,
  182. )
  183. url = f"/api/0/issues/{group.id}/"
  184. response = self.client.get(url, format="json")
  185. assert response.status_code == 200, response.content
  186. assert response.data["priority"] == "low"
  187. assert response.data["priorityLockedAt"] is None
  188. @with_feature("projects:issue-priority")
  189. def test_group_post_priority(self):
  190. self.login_as(user=self.user)
  191. group = self.create_group(
  192. project=self.project,
  193. status=GroupStatus.IGNORED,
  194. priority=PriorityLevel.LOW,
  195. )
  196. url = f"/api/0/issues/{group.id}/"
  197. get_response_before = self.client.get(url, format="json")
  198. assert get_response_before.status_code == 200, get_response_before.content
  199. assert get_response_before.data["priority"] == "low"
  200. response = self.client.put(url, {"priority": "high"}, format="json")
  201. assert response.status_code == 200, response.content
  202. assert response.data["priority"] == "high"
  203. act_for_group = Activity.objects.get_activities_for_group(group=group, num=100)
  204. assert len(act_for_group) == 2
  205. assert act_for_group[0].type == ActivityType.SET_PRIORITY.value
  206. assert act_for_group[-1].type == ActivityType.FIRST_SEEN.value
  207. assert act_for_group[0].user_id == self.user.id
  208. assert act_for_group[0].data["priority"] == "high"
  209. get_response_after = self.client.get(url, format="json")
  210. assert get_response_after.status_code == 200, get_response_after.content
  211. assert get_response_after.data["priority"] == "high"
  212. assert get_response_after.data["priorityLockedAt"] is not None
  213. def test_assigned_to_unknown(self):
  214. self.login_as(user=self.user)
  215. event = self.store_event(
  216. data={"timestamp": iso_format(before_now(minutes=3))},
  217. project_id=self.project.id,
  218. )
  219. group = event.group
  220. url = f"/api/0/issues/{group.id}/"
  221. response = self.client.put(
  222. url, {"assignedTo": "admin@localhost", "status": "unresolved"}, format="json"
  223. )
  224. assert response.status_code == 200
  225. response = self.client.put(
  226. url, {"assignedTo": "user@doesnotexist.com", "status": "unresolved"}, format="json"
  227. )
  228. assert response.status_code == 400
  229. assert response.data == {
  230. "assignedTo": [
  231. ErrorDetail(
  232. string="Could not parse actor. Format should be `type:id` where type is `team` or `user`.",
  233. code="invalid",
  234. )
  235. ]
  236. }
  237. def test_collapse_stats_does_not_work(self):
  238. """
  239. 'collapse' param should hide the stats data and not return anything in the response, but the impl
  240. doesn't seem to respect this param.
  241. include this test here in-case the endpoint behavior changes in the future.
  242. """
  243. self.login_as(user=self.user)
  244. event = self.store_event(
  245. data={"timestamp": iso_format(before_now(minutes=3))},
  246. project_id=self.project.id,
  247. )
  248. group = event.group
  249. url = f"/api/0/issues/{group.id}/"
  250. response = self.client.get(url, {"collapse": ["stats"]}, format="json")
  251. assert response.status_code == 200
  252. assert int(response.data["id"]) == event.group.id
  253. assert response.data["stats"] # key shouldn't be present
  254. assert response.data["count"] is not None # key shouldn't be present
  255. assert response.data["userCount"] is not None # key shouldn't be present
  256. assert response.data["firstSeen"] is not None # key shouldn't be present
  257. assert response.data["lastSeen"] is not None # key shouldn't be present
  258. def test_issue_type_category(self):
  259. """Test that the issue's type and category is returned in the results"""
  260. self.login_as(user=self.user)
  261. event = self.store_event(
  262. data={"timestamp": iso_format(before_now(minutes=3))},
  263. project_id=self.project.id,
  264. )
  265. url = f"/api/0/issues/{event.group.id}/"
  266. response = self.client.get(url, format="json")
  267. assert response.status_code == 200
  268. assert int(response.data["id"]) == event.group.id
  269. assert response.data["issueType"] == "error"
  270. assert response.data["issueCategory"] == "error"