test_group.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. # -*- coding: utf-8 -*-
  2. from __future__ import absolute_import
  3. import six
  4. from datetime import timedelta
  5. from django.utils import timezone
  6. from mock import patch
  7. from sentry.api.serializers import serialize
  8. from sentry.api.serializers.models.group import GroupSerializerSnuba
  9. from sentry.models import (
  10. GroupLink, GroupResolution, GroupSnooze, GroupStatus,
  11. GroupSubscription, UserOption, UserOptionValue
  12. )
  13. from sentry.testutils import APITestCase, SnubaTestCase
  14. class GroupSerializerSnubaTest(APITestCase, SnubaTestCase):
  15. def setUp(self):
  16. super(GroupSerializerSnubaTest, self).setUp()
  17. self.min_ago = timezone.now() - timedelta(minutes=1)
  18. self.day_ago = timezone.now() - timedelta(days=1)
  19. self.week_ago = timezone.now() - timedelta(days=7)
  20. def test_is_ignored_with_expired_snooze(self):
  21. now = timezone.now().replace(microsecond=0)
  22. user = self.create_user()
  23. group = self.create_group(
  24. status=GroupStatus.IGNORED,
  25. )
  26. GroupSnooze.objects.create(
  27. group=group,
  28. until=now - timedelta(minutes=1),
  29. )
  30. result = serialize(group, user, serializer=GroupSerializerSnuba())
  31. assert result['status'] == 'unresolved'
  32. assert result['statusDetails'] == {}
  33. def test_is_ignored_with_valid_snooze(self):
  34. now = timezone.now().replace(microsecond=0)
  35. user = self.create_user()
  36. group = self.create_group(
  37. status=GroupStatus.IGNORED,
  38. )
  39. snooze = GroupSnooze.objects.create(
  40. group=group,
  41. until=now + timedelta(minutes=1),
  42. )
  43. result = serialize(group, user, serializer=GroupSerializerSnuba())
  44. assert result['status'] == 'ignored'
  45. assert result['statusDetails']['ignoreCount'] == snooze.count
  46. assert result['statusDetails']['ignoreWindow'] == snooze.window
  47. assert result['statusDetails']['ignoreUserCount'] == snooze.user_count
  48. assert result['statusDetails']['ignoreUserWindow'] == snooze.user_window
  49. assert result['statusDetails']['ignoreUntil'] == snooze.until
  50. assert result['statusDetails']['actor'] is None
  51. def test_is_ignored_with_valid_snooze_and_actor(self):
  52. now = timezone.now().replace(microsecond=0)
  53. user = self.create_user()
  54. group = self.create_group(
  55. status=GroupStatus.IGNORED,
  56. )
  57. GroupSnooze.objects.create(
  58. group=group,
  59. until=now + timedelta(minutes=1),
  60. actor_id=user.id,
  61. )
  62. result = serialize(group, user, serializer=GroupSerializerSnuba())
  63. assert result['status'] == 'ignored'
  64. assert result['statusDetails']['actor']['id'] == six.text_type(user.id)
  65. def test_resolved_in_next_release(self):
  66. release = self.create_release(project=self.project, version='a')
  67. user = self.create_user()
  68. group = self.create_group(
  69. status=GroupStatus.RESOLVED,
  70. )
  71. GroupResolution.objects.create(
  72. group=group,
  73. release=release,
  74. type=GroupResolution.Type.in_next_release,
  75. )
  76. result = serialize(group, user, serializer=GroupSerializerSnuba())
  77. assert result['status'] == 'resolved'
  78. assert result['statusDetails'] == {'inNextRelease': True, 'actor': None}
  79. def test_resolved_in_release(self):
  80. release = self.create_release(project=self.project, version='a')
  81. user = self.create_user()
  82. group = self.create_group(
  83. status=GroupStatus.RESOLVED,
  84. )
  85. GroupResolution.objects.create(
  86. group=group,
  87. release=release,
  88. type=GroupResolution.Type.in_release,
  89. )
  90. result = serialize(group, user, serializer=GroupSerializerSnuba())
  91. assert result['status'] == 'resolved'
  92. assert result['statusDetails'] == {'inRelease': 'a', 'actor': None}
  93. def test_resolved_with_actor(self):
  94. release = self.create_release(project=self.project, version='a')
  95. user = self.create_user()
  96. group = self.create_group(
  97. status=GroupStatus.RESOLVED,
  98. )
  99. GroupResolution.objects.create(
  100. group=group,
  101. release=release,
  102. type=GroupResolution.Type.in_release,
  103. actor_id=user.id,
  104. )
  105. result = serialize(group, user, serializer=GroupSerializerSnuba())
  106. assert result['status'] == 'resolved'
  107. assert result['statusDetails']['actor']['id'] == six.text_type(user.id)
  108. def test_resolved_in_commit(self):
  109. repo = self.create_repo(project=self.project)
  110. commit = self.create_commit(repo=repo)
  111. user = self.create_user()
  112. group = self.create_group(
  113. status=GroupStatus.RESOLVED,
  114. )
  115. GroupLink.objects.create(
  116. group_id=group.id,
  117. project_id=group.project_id,
  118. linked_id=commit.id,
  119. linked_type=GroupLink.LinkedType.commit,
  120. relationship=GroupLink.Relationship.resolves,
  121. )
  122. result = serialize(group, user, serializer=GroupSerializerSnuba())
  123. assert result['status'] == 'resolved'
  124. assert result['statusDetails']['inCommit']['id'] == commit.key
  125. @patch('sentry.models.Group.is_over_resolve_age')
  126. def test_auto_resolved(self, mock_is_over_resolve_age):
  127. mock_is_over_resolve_age.return_value = True
  128. user = self.create_user()
  129. group = self.create_group(
  130. status=GroupStatus.UNRESOLVED,
  131. )
  132. result = serialize(group, user, serializer=GroupSerializerSnuba())
  133. assert result['status'] == 'resolved'
  134. assert result['statusDetails'] == {'autoResolved': True}
  135. def test_subscribed(self):
  136. user = self.create_user()
  137. group = self.create_group()
  138. GroupSubscription.objects.create(
  139. user=user,
  140. group=group,
  141. project=group.project,
  142. is_active=True,
  143. )
  144. result = serialize(group, user, serializer=GroupSerializerSnuba())
  145. assert result['isSubscribed']
  146. assert result['subscriptionDetails'] == {
  147. 'reason': 'unknown',
  148. }
  149. def test_explicit_unsubscribed(self):
  150. user = self.create_user()
  151. group = self.create_group()
  152. GroupSubscription.objects.create(
  153. user=user,
  154. group=group,
  155. project=group.project,
  156. is_active=False,
  157. )
  158. result = serialize(group, user, serializer=GroupSerializerSnuba())
  159. assert not result['isSubscribed']
  160. assert not result['subscriptionDetails']
  161. def test_implicit_subscribed(self):
  162. user = self.create_user()
  163. group = self.create_group()
  164. combinations = (
  165. # ((default, project), (subscribed, details))
  166. ((None, None), (True, None)),
  167. ((UserOptionValue.all_conversations, None), (True, None)),
  168. ((UserOptionValue.all_conversations, UserOptionValue.all_conversations), (True, None)),
  169. ((UserOptionValue.all_conversations, UserOptionValue.participating_only), (False, None)),
  170. ((UserOptionValue.all_conversations, UserOptionValue.no_conversations),
  171. (False, {'disabled': True})),
  172. ((UserOptionValue.participating_only, None), (False, None)),
  173. ((UserOptionValue.participating_only, UserOptionValue.all_conversations), (True, None)),
  174. ((UserOptionValue.participating_only, UserOptionValue.participating_only), (False, None)),
  175. ((UserOptionValue.participating_only, UserOptionValue.no_conversations),
  176. (False, {'disabled': True})),
  177. ((UserOptionValue.no_conversations, None), (False, {'disabled': True})),
  178. ((UserOptionValue.no_conversations, UserOptionValue.all_conversations), (True, None)),
  179. ((UserOptionValue.no_conversations, UserOptionValue.participating_only), (False, None)),
  180. ((UserOptionValue.no_conversations, UserOptionValue.no_conversations),
  181. (False, {'disabled': True})),
  182. )
  183. def maybe_set_value(project, value):
  184. if value is not None:
  185. UserOption.objects.set_value(
  186. user=user,
  187. project=project,
  188. key='workflow:notifications',
  189. value=value,
  190. )
  191. else:
  192. UserOption.objects.unset_value(
  193. user=user,
  194. project=project,
  195. key='workflow:notifications',
  196. )
  197. for options, (is_subscribed, subscription_details) in combinations:
  198. default_value, project_value = options
  199. UserOption.objects.clear_local_cache()
  200. maybe_set_value(None, default_value)
  201. maybe_set_value(group.project, project_value)
  202. result = serialize(group, user, serializer=GroupSerializerSnuba())
  203. assert result['isSubscribed'] is is_subscribed
  204. assert result.get('subscriptionDetails') == subscription_details
  205. def test_global_no_conversations_overrides_group_subscription(self):
  206. user = self.create_user()
  207. group = self.create_group()
  208. GroupSubscription.objects.create(
  209. user=user,
  210. group=group,
  211. project=group.project,
  212. is_active=True,
  213. )
  214. UserOption.objects.set_value(
  215. user=user,
  216. project=None,
  217. key='workflow:notifications',
  218. value=UserOptionValue.no_conversations,
  219. )
  220. result = serialize(group, user, serializer=GroupSerializerSnuba())
  221. assert not result['isSubscribed']
  222. assert result['subscriptionDetails'] == {
  223. 'disabled': True,
  224. }
  225. def test_project_no_conversations_overrides_group_subscription(self):
  226. user = self.create_user()
  227. group = self.create_group()
  228. GroupSubscription.objects.create(
  229. user=user,
  230. group=group,
  231. project=group.project,
  232. is_active=True,
  233. )
  234. UserOption.objects.set_value(
  235. user=user,
  236. project=group.project,
  237. key='workflow:notifications',
  238. value=UserOptionValue.no_conversations,
  239. )
  240. result = serialize(group, user, serializer=GroupSerializerSnuba())
  241. assert not result['isSubscribed']
  242. assert result['subscriptionDetails'] == {
  243. 'disabled': True,
  244. }
  245. def test_no_user_unsubscribed(self):
  246. group = self.create_group()
  247. result = serialize(group, serializer=GroupSerializerSnuba())
  248. assert not result['isSubscribed']
  249. def test_seen_stats(self):
  250. group = self.create_group(first_seen=self.week_ago, times_seen=5)
  251. # should use group columns when no environments arg passed
  252. result = serialize(group, serializer=GroupSerializerSnuba())
  253. assert result['count'] == '5'
  254. assert result['lastSeen'] == group.last_seen
  255. assert result['firstSeen'] == group.first_seen
  256. environment = self.create_environment(project=group.project)
  257. environment2 = self.create_environment(project=group.project)
  258. self.create_event(
  259. 'a' * 32, group=group, datetime=self.day_ago, tags={'environment': environment.name}
  260. )
  261. self.create_event(
  262. 'b' * 32, group=group, datetime=self.min_ago, tags={'environment': environment.name}
  263. )
  264. self.create_event(
  265. 'c' * 32, group=group, datetime=self.min_ago, tags={'environment': environment2.name}
  266. )
  267. result = serialize(
  268. group, serializer=GroupSerializerSnuba(
  269. environment_ids=[environment.id, environment2.id])
  270. )
  271. assert result['count'] == '3'
  272. # result is rounded down to nearest second
  273. assert result['lastSeen'] == self.min_ago - timedelta(microseconds=self.min_ago.microsecond)
  274. assert result['firstSeen'] == self.day_ago - \
  275. timedelta(microseconds=self.day_ago.microsecond)