test_group_details.py 11 KB

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