test_adapter.py 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994
  1. from datetime import datetime
  2. import pytz
  3. from django.contrib.auth.models import AnonymousUser
  4. from django.core import mail
  5. from django.db.models import F
  6. from django.utils import timezone
  7. from exam import fixture
  8. from sentry.api.serializers import UserReportWithGroupSerializer, serialize
  9. from sentry.digests.notifications import build_digest, event_to_record
  10. from sentry.event_manager import EventManager, get_event_type
  11. from sentry.mail import mail_adapter
  12. from sentry.mail.adapter import ActionTargetType
  13. from sentry.models import (
  14. Activity,
  15. NotificationSetting,
  16. Organization,
  17. OrganizationMember,
  18. OrganizationMemberTeam,
  19. ProjectOption,
  20. ProjectOwnership,
  21. Repository,
  22. Rule,
  23. User,
  24. UserReport,
  25. )
  26. from sentry.notifications.types import NotificationSettingOptionValues, NotificationSettingTypes
  27. from sentry.ownership import grammar
  28. from sentry.ownership.grammar import Matcher, Owner, dump_schema
  29. from sentry.plugins.base import Notification
  30. from sentry.rules.processor import RuleFuture
  31. from sentry.testutils import TestCase
  32. from sentry.testutils.helpers.datetime import before_now, iso_format
  33. from sentry.types.integrations import ExternalProviders
  34. from sentry.utils.compat import mock
  35. from sentry.utils.email import MessageBuilder
  36. class BaseMailAdapterTest:
  37. @fixture
  38. def adapter(self):
  39. return mail_adapter
  40. def make_event_data(self, filename, url="http://example.com"):
  41. mgr = EventManager(
  42. {
  43. "tags": [("level", "error")],
  44. "stacktrace": {"frames": [{"lineno": 1, "filename": filename}]},
  45. "request": {"url": url},
  46. }
  47. )
  48. mgr.normalize()
  49. data = mgr.get_data()
  50. event_type = get_event_type(data)
  51. data["type"] = event_type.key
  52. data["metadata"] = event_type.get_metadata(data)
  53. return data
  54. class MailAdapterGetSendToTest(BaseMailAdapterTest, TestCase):
  55. def setUp(self):
  56. self.user2 = self.create_user(email="baz@example.com", is_active=True)
  57. self.create_member(user=self.user2, organization=self.organization, teams=[self.team])
  58. ProjectOwnership.objects.create(
  59. project_id=self.project.id,
  60. schema=dump_schema(
  61. [
  62. grammar.Rule(Matcher("path", "*.py"), [Owner("team", self.team.slug)]),
  63. grammar.Rule(Matcher("path", "*.jx"), [Owner("user", self.user2.email)]),
  64. grammar.Rule(
  65. Matcher("path", "*.cbl"),
  66. [Owner("user", self.user.email), Owner("user", self.user2.email)],
  67. ),
  68. ]
  69. ),
  70. fallthrough=True,
  71. )
  72. def test_get_send_to_with_team_owners(self):
  73. event = self.store_event(data=self.make_event_data("foo.py"), project_id=self.project.id)
  74. assert sorted({self.user.pk, self.user2.pk}) == sorted(
  75. self.adapter.get_send_to(self.project, ActionTargetType.ISSUE_OWNERS, event=event.data)[
  76. ExternalProviders.EMAIL
  77. ]
  78. )
  79. # Make sure that disabling mail alerts works as expected
  80. NotificationSetting.objects.update_settings(
  81. ExternalProviders.EMAIL,
  82. NotificationSettingTypes.ISSUE_ALERTS,
  83. NotificationSettingOptionValues.NEVER,
  84. user=self.user2,
  85. project=self.project,
  86. )
  87. assert {self.user.pk} == self.adapter.get_send_to(
  88. self.project, ActionTargetType.ISSUE_OWNERS, event=event.data
  89. )[ExternalProviders.EMAIL]
  90. def test_get_send_to_with_user_owners(self):
  91. event = self.store_event(data=self.make_event_data("foo.cbl"), project_id=self.project.id)
  92. assert sorted({self.user.pk, self.user2.pk}) == sorted(
  93. self.adapter.get_send_to(self.project, ActionTargetType.ISSUE_OWNERS, event=event.data)[
  94. ExternalProviders.EMAIL
  95. ]
  96. )
  97. # Make sure that disabling mail alerts works as expected
  98. NotificationSetting.objects.update_settings(
  99. ExternalProviders.EMAIL,
  100. NotificationSettingTypes.ISSUE_ALERTS,
  101. NotificationSettingOptionValues.NEVER,
  102. user=self.user2,
  103. project=self.project,
  104. )
  105. assert {self.user.pk} == self.adapter.get_send_to(
  106. self.project, ActionTargetType.ISSUE_OWNERS, event=event.data
  107. )[ExternalProviders.EMAIL]
  108. def test_get_send_to_with_user_owner(self):
  109. event = self.store_event(data=self.make_event_data("foo.jx"), project_id=self.project.id)
  110. assert {self.user2.pk} == self.adapter.get_send_to(
  111. self.project, ActionTargetType.ISSUE_OWNERS, event=event.data
  112. )[ExternalProviders.EMAIL]
  113. def test_get_send_to_with_fallthrough(self):
  114. event = self.store_event(data=self.make_event_data("foo.cpp"), project_id=self.project.id)
  115. assert {self.user.pk, self.user2.pk} == self.adapter.get_send_to(
  116. self.project, ActionTargetType.ISSUE_OWNERS, event=event.data
  117. )[ExternalProviders.EMAIL]
  118. def test_get_send_to_without_fallthrough(self):
  119. ProjectOwnership.objects.get(project_id=self.project.id).update(fallthrough=False)
  120. event = self.store_event(data=self.make_event_data("foo.cpp"), project_id=self.project.id)
  121. assert set() == set(
  122. self.adapter.get_send_to(self.project, ActionTargetType.ISSUE_OWNERS, event=event.data)
  123. )
  124. class MailAdapterGetSendableUsersTest(BaseMailAdapterTest, TestCase):
  125. def test_get_sendable_user_objects(self):
  126. user = self.create_user(email="foo@example.com", is_active=True)
  127. user2 = self.create_user(email="baz@example.com", is_active=True)
  128. self.create_user(email="baz2@example.com", is_active=True)
  129. # user with inactive account
  130. self.create_user(email="bar@example.com", is_active=False)
  131. # user not in any groups
  132. self.create_user(email="bar2@example.com", is_active=True)
  133. organization = self.create_organization(owner=user)
  134. team = self.create_team(organization=organization)
  135. project = self.create_project(name="Test", teams=[team])
  136. OrganizationMemberTeam.objects.create(
  137. organizationmember=OrganizationMember.objects.get(user=user, organization=organization),
  138. team=team,
  139. )
  140. self.create_member(user=user2, organization=organization, teams=[team])
  141. # all members
  142. users = self.adapter.get_sendable_user_objects(project)
  143. assert sorted({user.id, user2.id}) == sorted([user.id for user in users])
  144. # disabled user2
  145. NotificationSetting.objects.update_settings(
  146. ExternalProviders.EMAIL,
  147. NotificationSettingTypes.ISSUE_ALERTS,
  148. NotificationSettingOptionValues.NEVER,
  149. user=user2,
  150. project=project,
  151. )
  152. assert user2 not in self.adapter.get_sendable_user_objects(project)
  153. user4 = User.objects.create(username="baz4", email="bar@example.com", is_active=True)
  154. self.create_member(user=user4, organization=organization, teams=[team])
  155. assert user4 in self.adapter.get_sendable_user_objects(project)
  156. # disabled by default user4
  157. NotificationSetting.objects.update_settings(
  158. ExternalProviders.EMAIL,
  159. NotificationSettingTypes.ISSUE_ALERTS,
  160. NotificationSettingOptionValues.NEVER,
  161. user=user4,
  162. )
  163. assert user4 not in self.adapter.get_sendable_user_objects(project)
  164. NotificationSetting.objects.remove_settings(
  165. ExternalProviders.EMAIL,
  166. NotificationSettingTypes.ISSUE_ALERTS,
  167. user=user4,
  168. )
  169. NotificationSetting.objects.update_settings(
  170. ExternalProviders.EMAIL,
  171. NotificationSettingTypes.ISSUE_ALERTS,
  172. NotificationSettingOptionValues.NEVER,
  173. user=user4,
  174. )
  175. assert user4 not in self.adapter.get_sendable_user_objects(project)
  176. class MailAdapterBuildSubjectPrefixTest(BaseMailAdapterTest, TestCase):
  177. def test_default_prefix(self):
  178. assert self.adapter._build_subject_prefix(self.project) == "[Sentry] "
  179. def test_project_level_prefix(self):
  180. prefix = "[Example prefix] "
  181. ProjectOption.objects.set_value(
  182. project=self.project, key="mail:subject_prefix", value=prefix
  183. )
  184. assert self.adapter._build_subject_prefix(self.project) == prefix
  185. class MailAdapterBuildMessageTest(BaseMailAdapterTest, TestCase):
  186. def test(self):
  187. subject = "hello"
  188. assert self.adapter._build_message(self.project, subject) is None
  189. def test_specify_send_to(self):
  190. subject = "hello"
  191. send_to_user = self.create_user("hello@timecube.com")
  192. msg = self.adapter._build_message(self.project, subject, send_to=[send_to_user.id])
  193. assert msg._send_to == {send_to_user.email}
  194. assert msg.subject.endswith(subject)
  195. class MailAdapterSendMailTest(BaseMailAdapterTest, TestCase):
  196. def test(self):
  197. subject = "hello"
  198. with self.tasks():
  199. self.adapter._send_mail(self.project, subject, body="hi", send_to=[self.user.id])
  200. msg = mail.outbox[0]
  201. assert msg.subject.endswith(subject)
  202. assert msg.recipients() == [self.user.email]
  203. class MailAdapterNotifyTest(BaseMailAdapterTest, TestCase):
  204. def test_simple_notification(self):
  205. event = self.store_event(
  206. data={"message": "Hello world", "level": "error"}, project_id=self.project.id
  207. )
  208. rule = Rule.objects.create(project=self.project, label="my rule")
  209. notification = Notification(event=event, rule=rule)
  210. with self.options({"system.url-prefix": "http://example.com"}), self.tasks():
  211. self.adapter.notify(notification, ActionTargetType.ISSUE_OWNERS)
  212. msg = mail.outbox[0]
  213. assert msg.subject == "[Sentry] BAR-1 - Hello world"
  214. assert "my rule" in msg.alternatives[0][0]
  215. @mock.patch("sentry.interfaces.stacktrace.Stacktrace.get_title")
  216. @mock.patch("sentry.interfaces.stacktrace.Stacktrace.to_email_html")
  217. def test_notify_users_renders_interfaces_with_utf8(self, _to_email_html, _get_title):
  218. _to_email_html.return_value = "רונית מגן"
  219. _get_title.return_value = "Stacktrace"
  220. event = self.store_event(
  221. data={"message": "Soubor ji\xc5\xbe existuje", "stacktrace": {"frames": [{}]}},
  222. project_id=self.project.id,
  223. )
  224. notification = Notification(event=event)
  225. with self.options({"system.url-prefix": "http://example.com"}):
  226. self.adapter.notify(notification, ActionTargetType.ISSUE_OWNERS)
  227. _get_title.assert_called_once_with()
  228. _to_email_html.assert_called_once_with(event)
  229. @mock.patch("sentry.mail.mail_adapter._send_mail")
  230. def test_notify_users_does_email(self, _send_mail):
  231. event_manager = EventManager({"message": "hello world", "level": "error"})
  232. event_manager.normalize()
  233. event_data = event_manager.get_data()
  234. event_type = get_event_type(event_data)
  235. event_data["type"] = event_type.key
  236. event_data["metadata"] = event_type.get_metadata(event_data)
  237. event = event_manager.save(self.project.id)
  238. group = event.group
  239. notification = Notification(event=event)
  240. with self.options({"system.url-prefix": "http://example.com"}):
  241. self.adapter.notify(notification, ActionTargetType.ISSUE_OWNERS)
  242. assert _send_mail.call_count == 1
  243. args, kwargs = _send_mail.call_args
  244. self.assertEquals(kwargs.get("project"), self.project)
  245. self.assertEquals(kwargs.get("reference"), group)
  246. assert kwargs.get("subject") == "BAR-1 - hello world"
  247. @mock.patch("sentry.mail.mail_adapter._send_mail")
  248. def test_multiline_error(self, _send_mail):
  249. event_manager = EventManager({"message": "hello world\nfoo bar", "level": "error"})
  250. event_manager.normalize()
  251. event_data = event_manager.get_data()
  252. event_type = get_event_type(event_data)
  253. event_data["type"] = event_type.key
  254. event_data["metadata"] = event_type.get_metadata(event_data)
  255. event = event_manager.save(self.project.id)
  256. notification = Notification(event=event)
  257. with self.options({"system.url-prefix": "http://example.com"}):
  258. self.adapter.notify(notification, ActionTargetType.ISSUE_OWNERS)
  259. assert _send_mail.call_count == 1
  260. args, kwargs = _send_mail.call_args
  261. assert kwargs.get("subject") == "BAR-1 - hello world"
  262. def test_notify_users_with_utf8_subject(self):
  263. event = self.store_event(
  264. data={"message": "רונית מגן", "level": "error"}, project_id=self.project.id
  265. )
  266. notification = Notification(event=event)
  267. with self.options({"system.url-prefix": "http://example.com"}), self.tasks():
  268. self.adapter.notify(notification, ActionTargetType.ISSUE_OWNERS)
  269. assert len(mail.outbox) == 1
  270. msg = mail.outbox[0]
  271. assert msg.subject == "[Sentry] BAR-1 - רונית מגן"
  272. def test_notify_with_suspect_commits(self):
  273. repo = Repository.objects.create(
  274. organization_id=self.organization.id, name=self.organization.id
  275. )
  276. release = self.create_release(project=self.project, version="v12")
  277. release.set_commits(
  278. [
  279. {
  280. "id": "a" * 40,
  281. "repository": repo.name,
  282. "author_email": "bob@example.com",
  283. "author_name": "Bob",
  284. "message": "i fixed a bug",
  285. "patch_set": [{"path": "src/sentry/models/release.py", "type": "M"}],
  286. }
  287. ]
  288. )
  289. event = self.store_event(
  290. data={
  291. "message": "Kaboom!",
  292. "platform": "python",
  293. "timestamp": iso_format(before_now(seconds=1)),
  294. "stacktrace": {
  295. "frames": [
  296. {
  297. "function": "handle_set_commits",
  298. "abs_path": "/usr/src/sentry/src/sentry/tasks.py",
  299. "module": "sentry.tasks",
  300. "in_app": True,
  301. "lineno": 30,
  302. "filename": "sentry/tasks.py",
  303. },
  304. {
  305. "function": "set_commits",
  306. "abs_path": "/usr/src/sentry/src/sentry/models/release.py",
  307. "module": "sentry.models.release",
  308. "in_app": True,
  309. "lineno": 39,
  310. "filename": "sentry/models/release.py",
  311. },
  312. ]
  313. },
  314. "tags": {"sentry:release": release.version},
  315. },
  316. project_id=self.project.id,
  317. )
  318. with self.tasks():
  319. notification = Notification(event=event)
  320. self.adapter.notify(notification, ActionTargetType.ISSUE_OWNERS)
  321. assert len(mail.outbox) >= 1
  322. msg = mail.outbox[-1]
  323. assert "Suspect Commits" in msg.body
  324. def assert_notify(
  325. self,
  326. event,
  327. emails_sent_to,
  328. target_type=ActionTargetType.ISSUE_OWNERS,
  329. target_identifier=None,
  330. ):
  331. mail.outbox = []
  332. with self.options({"system.url-prefix": "http://example.com"}), self.tasks():
  333. self.adapter.notify(Notification(event=event), target_type, target_identifier)
  334. assert sorted(email.to[0] for email in mail.outbox) == sorted(emails_sent_to)
  335. def test_notify_users_with_owners(self):
  336. user = self.create_user(email="foo@example.com", is_active=True)
  337. user2 = self.create_user(email="baz@example.com", is_active=True)
  338. organization = self.create_organization(owner=user)
  339. team = self.create_team(organization=organization)
  340. project = self.create_project(name="Test", teams=[team])
  341. OrganizationMemberTeam.objects.create(
  342. organizationmember=OrganizationMember.objects.get(user=user, organization=organization),
  343. team=team,
  344. )
  345. self.create_member(user=user2, organization=organization, teams=[team])
  346. self.group = self.create_group(
  347. first_seen=timezone.now(),
  348. last_seen=timezone.now(),
  349. project=project,
  350. message="hello world",
  351. logger="root",
  352. )
  353. ProjectOwnership.objects.create(
  354. project_id=project.id,
  355. schema=dump_schema(
  356. [
  357. grammar.Rule(Matcher("path", "*.py"), [Owner("team", team.slug)]),
  358. grammar.Rule(Matcher("path", "*.jx"), [Owner("user", user2.email)]),
  359. grammar.Rule(
  360. Matcher("path", "*.cbl"),
  361. [Owner("user", user.email), Owner("user", user2.email)],
  362. ),
  363. ]
  364. ),
  365. fallthrough=True,
  366. )
  367. event_all_users = self.store_event(
  368. data=self.make_event_data("foo.cbl"), project_id=project.id
  369. )
  370. self.assert_notify(event_all_users, [user.email, user2.email])
  371. event_team = self.store_event(data=self.make_event_data("foo.py"), project_id=project.id)
  372. self.assert_notify(event_team, [user.email, user2.email])
  373. event_single_user = self.store_event(
  374. data=self.make_event_data("foo.jx"), project_id=project.id
  375. )
  376. self.assert_notify(event_single_user, [user2.email])
  377. # Make sure that disabling mail alerts works as expected
  378. NotificationSetting.objects.update_settings(
  379. ExternalProviders.EMAIL,
  380. NotificationSettingTypes.ISSUE_ALERTS,
  381. NotificationSettingOptionValues.NEVER,
  382. user=user2,
  383. project=project,
  384. )
  385. event_all_users = self.store_event(
  386. data=self.make_event_data("foo.cbl"), project_id=project.id
  387. )
  388. self.assert_notify(event_all_users, [user.email])
  389. def test_notify_team(self):
  390. user = self.create_user(email="foo@example.com", is_active=True)
  391. user2 = self.create_user(email="baz@example.com", is_active=True)
  392. team = self.create_team(organization=self.organization, members=[user, user2])
  393. project = self.create_project(teams=[team])
  394. event = self.store_event(data=self.make_event_data("foo.py"), project_id=project.id)
  395. self.assert_notify(event, [user.email, user2.email], ActionTargetType.TEAM, str(team.id))
  396. def test_notify_user(self):
  397. user = self.create_user(email="foo@example.com", is_active=True)
  398. self.create_team(organization=self.organization, members=[user])
  399. event = self.store_event(data=self.make_event_data("foo.py"), project_id=self.project.id)
  400. self.assert_notify(event, [user.email], ActionTargetType.MEMBER, str(user.id))
  401. class MailAdapterGetDigestSubjectTest(BaseMailAdapterTest, TestCase):
  402. def test_get_digest_subject(self):
  403. assert (
  404. self.adapter.get_digest_subject(
  405. mock.Mock(qualified_short_id="BAR-1"),
  406. {mock.sentinel.group: 3},
  407. datetime(2016, 9, 19, 1, 2, 3, tzinfo=pytz.utc),
  408. )
  409. == "BAR-1 - 1 new alert since Sept. 19, 2016, 1:02 a.m. UTC"
  410. )
  411. class MailAdapterNotifyDigestTest(BaseMailAdapterTest, TestCase):
  412. @mock.patch.object(mail_adapter, "notify", side_effect=mail_adapter.notify, autospec=True)
  413. def test_notify_digest(self, notify):
  414. project = self.project
  415. event = self.store_event(
  416. data={"timestamp": iso_format(before_now(minutes=1)), "fingerprint": ["group-1"]},
  417. project_id=project.id,
  418. )
  419. event2 = self.store_event(
  420. data={"timestamp": iso_format(before_now(minutes=1)), "fingerprint": ["group-2"]},
  421. project_id=project.id,
  422. )
  423. rule = project.rule_set.all()[0]
  424. digest = build_digest(
  425. project, (event_to_record(event, (rule,)), event_to_record(event2, (rule,)))
  426. )
  427. with self.tasks():
  428. self.adapter.notify_digest(project, digest, ActionTargetType.ISSUE_OWNERS)
  429. assert notify.call_count == 0
  430. assert len(mail.outbox) == 1
  431. message = mail.outbox[0]
  432. assert "List-ID" in message.message()
  433. @mock.patch.object(mail_adapter, "notify", side_effect=mail_adapter.notify, autospec=True)
  434. @mock.patch.object(MessageBuilder, "send_async", autospec=True)
  435. def test_notify_digest_single_record(self, send_async, notify):
  436. event = self.store_event(data={}, project_id=self.project.id)
  437. rule = self.project.rule_set.all()[0]
  438. digest = build_digest(self.project, (event_to_record(event, (rule,)),))
  439. self.adapter.notify_digest(self.project, digest, ActionTargetType.ISSUE_OWNERS)
  440. assert send_async.call_count == 1
  441. assert notify.call_count == 1
  442. def test_notify_digest_subject_prefix(self):
  443. ProjectOption.objects.set_value(
  444. project=self.project, key="mail:subject_prefix", value="[Example prefix] "
  445. )
  446. event = self.store_event(
  447. data={"timestamp": iso_format(before_now(minutes=1)), "fingerprint": ["group-1"]},
  448. project_id=self.project.id,
  449. )
  450. event2 = self.store_event(
  451. data={"timestamp": iso_format(before_now(minutes=1)), "fingerprint": ["group-2"]},
  452. project_id=self.project.id,
  453. )
  454. rule = self.project.rule_set.all()[0]
  455. digest = build_digest(
  456. self.project, (event_to_record(event, (rule,)), event_to_record(event2, (rule,)))
  457. )
  458. with self.tasks():
  459. self.adapter.notify_digest(self.project, digest, ActionTargetType.ISSUE_OWNERS)
  460. assert len(mail.outbox) == 1
  461. msg = mail.outbox[0]
  462. assert msg.subject.startswith("[Example prefix]")
  463. @mock.patch.object(mail_adapter, "notify", side_effect=mail_adapter.notify, autospec=True)
  464. def test_notify_digest_user_does_not_exist(self, notify):
  465. """Test that in the event a rule has been created with an action to send to a user who
  466. no longer exists, we don't blow up when getting users in get_send_to
  467. """
  468. project = self.project
  469. event = self.store_event(
  470. data={"timestamp": iso_format(before_now(minutes=1)), "fingerprint": ["group-1"]},
  471. project_id=project.id,
  472. )
  473. event2 = self.store_event(
  474. data={"timestamp": iso_format(before_now(minutes=1)), "fingerprint": ["group-2"]},
  475. project_id=project.id,
  476. )
  477. action_data = {
  478. "id": "sentry.mail.actions.NotifyEmailAction",
  479. "targetType": "Member",
  480. "targetIdentifier": str(444),
  481. }
  482. rule = Rule.objects.create(
  483. project=self.project,
  484. label="a rule",
  485. data={
  486. "match": "all",
  487. "actions": [action_data],
  488. },
  489. )
  490. digest = build_digest(
  491. project, (event_to_record(event, (rule,)), event_to_record(event2, (rule,)))
  492. )
  493. with self.tasks():
  494. self.adapter.notify_digest(project, digest, ActionTargetType.MEMBER, 444)
  495. assert notify.call_count == 0
  496. assert len(mail.outbox) == 0
  497. class MailAdapterRuleNotifyTest(BaseMailAdapterTest, TestCase):
  498. def test_normal(self):
  499. event = self.store_event(data={}, project_id=self.project.id)
  500. rule = Rule.objects.create(project=self.project, label="my rule")
  501. futures = [RuleFuture(rule, {})]
  502. with mock.patch.object(self.adapter, "notify") as notify:
  503. self.adapter.rule_notify(event, futures, ActionTargetType.ISSUE_OWNERS)
  504. notify.call_count == 1
  505. @mock.patch("sentry.mail.adapter.digests")
  506. def test_digest(self, digests):
  507. digests.enabled.return_value = True
  508. event = self.store_event(data={}, project_id=self.project.id)
  509. rule = Rule.objects.create(project=self.project, label="my rule")
  510. futures = [RuleFuture(rule, {})]
  511. self.adapter.rule_notify(event, futures, ActionTargetType.ISSUE_OWNERS)
  512. digests.add.call_count == 1
  513. class MailAdapterShouldNotifyTest(BaseMailAdapterTest, TestCase):
  514. def test_should_notify(self):
  515. assert self.adapter.should_notify(ActionTargetType.ISSUE_OWNERS, self.group)
  516. assert self.adapter.should_notify(ActionTargetType.MEMBER, self.group)
  517. def test_should_not_notify_no_users(self):
  518. NotificationSetting.objects.update_settings(
  519. ExternalProviders.EMAIL,
  520. NotificationSettingTypes.ISSUE_ALERTS,
  521. NotificationSettingOptionValues.NEVER,
  522. user=self.user,
  523. project=self.project,
  524. )
  525. assert not self.adapter.should_notify(ActionTargetType.ISSUE_OWNERS, self.group)
  526. def test_should_always_notify_target_member(self):
  527. NotificationSetting.objects.update_settings(
  528. ExternalProviders.EMAIL,
  529. NotificationSettingTypes.ISSUE_ALERTS,
  530. NotificationSettingOptionValues.NEVER,
  531. user=self.user,
  532. project=self.project,
  533. )
  534. assert self.adapter.should_notify(ActionTargetType.MEMBER, self.group)
  535. class MailAdapterGetSendToOwnersTest(BaseMailAdapterTest, TestCase):
  536. def setUp(self):
  537. self.user = self.create_user(email="foo@example.com", is_active=True)
  538. self.user2 = self.create_user(email="baz@example.com", is_active=True)
  539. self.user3 = self.create_user(email="bar@example.com", is_active=True)
  540. self.organization = self.create_organization(owner=self.user)
  541. self.team = self.create_team(
  542. organization=self.organization, members=[self.user2, self.user3]
  543. )
  544. self.team2 = self.create_team(organization=self.organization, members=[self.user])
  545. self.project = self.create_project(name="Test", teams=[self.team, self.team2])
  546. self.group = self.create_group(
  547. first_seen=timezone.now(),
  548. last_seen=timezone.now(),
  549. project=self.project,
  550. message="hello world",
  551. logger="root",
  552. )
  553. ProjectOwnership.objects.create(
  554. project_id=self.project.id,
  555. schema=dump_schema(
  556. [
  557. grammar.Rule(Matcher("path", "*.py"), [Owner("team", self.team.slug)]),
  558. grammar.Rule(Matcher("path", "*.jx"), [Owner("user", self.user2.email)]),
  559. grammar.Rule(
  560. Matcher("path", "*.cbl"),
  561. [
  562. Owner("user", self.user.email),
  563. Owner("user", self.user2.email),
  564. Owner("user", self.user3.email),
  565. ],
  566. ),
  567. ]
  568. ),
  569. fallthrough=True,
  570. )
  571. def test_all_users(self):
  572. event_all_users = self.store_event(
  573. data=self.make_event_data("foo.cbl"), project_id=self.project.id
  574. )
  575. assert self.adapter.get_send_to_owners(event_all_users, self.project)[
  576. ExternalProviders.EMAIL
  577. ] == {
  578. self.user.id,
  579. self.user2.id,
  580. self.user3.id,
  581. }
  582. def test_team(self):
  583. event_team = self.store_event(
  584. data=self.make_event_data("foo.py"), project_id=self.project.id
  585. )
  586. assert self.adapter.get_send_to_owners(event_team, self.project)[
  587. ExternalProviders.EMAIL
  588. ] == {
  589. self.user2.id,
  590. self.user3.id,
  591. }
  592. def test_single_user(self):
  593. event_single_user = self.store_event(
  594. data=self.make_event_data("foo.jx"), project_id=self.project.id
  595. )
  596. assert self.adapter.get_send_to_owners(event_single_user, self.project)[
  597. ExternalProviders.EMAIL
  598. ] == {self.user2.id}
  599. def test_disable_alerts_user_scope(self):
  600. event_all_users = self.store_event(
  601. data=self.make_event_data("foo.cbl"), project_id=self.project.id
  602. )
  603. NotificationSetting.objects.update_settings(
  604. ExternalProviders.EMAIL,
  605. NotificationSettingTypes.ISSUE_ALERTS,
  606. NotificationSettingOptionValues.NEVER,
  607. user=self.user2,
  608. )
  609. assert self.user2.id not in self.adapter.get_send_to_owners(event_all_users, self.project)
  610. def test_disable_alerts_project_scope(self):
  611. event_all_users = self.store_event(
  612. data=self.make_event_data("foo.cbl"), project_id=self.project.id
  613. )
  614. NotificationSetting.objects.update_settings(
  615. ExternalProviders.EMAIL,
  616. NotificationSettingTypes.ISSUE_ALERTS,
  617. NotificationSettingOptionValues.NEVER,
  618. user=self.user2,
  619. project=self.project,
  620. )
  621. assert self.user2.id not in self.adapter.get_send_to_owners(event_all_users, self.project)
  622. def test_disable_alerts_multiple_scopes(self):
  623. event_all_users = self.store_event(
  624. data=self.make_event_data("foo.cbl"), project_id=self.project.id
  625. )
  626. # Project-independent setting.
  627. NotificationSetting.objects.update_settings(
  628. ExternalProviders.EMAIL,
  629. NotificationSettingTypes.ISSUE_ALERTS,
  630. NotificationSettingOptionValues.ALWAYS,
  631. user=self.user2,
  632. )
  633. # Per-project setting.
  634. NotificationSetting.objects.update_settings(
  635. ExternalProviders.EMAIL,
  636. NotificationSettingTypes.ISSUE_ALERTS,
  637. NotificationSettingOptionValues.NEVER,
  638. user=self.user2,
  639. project=self.project,
  640. )
  641. assert self.user2.id not in self.adapter.get_send_to_owners(event_all_users, self.project)
  642. class MailAdapterGetSendToTeamTest(BaseMailAdapterTest, TestCase):
  643. def test_send_to_team(self):
  644. assert {self.user.id} == self.adapter.get_send_to_team(self.project, str(self.team.id))
  645. def test_send_disabled(self):
  646. NotificationSetting.objects.update_settings(
  647. ExternalProviders.EMAIL,
  648. NotificationSettingTypes.ISSUE_ALERTS,
  649. NotificationSettingOptionValues.NEVER,
  650. user=self.user,
  651. project=self.project,
  652. )
  653. assert set() == self.adapter.get_send_to_team(self.project, str(self.team.id))
  654. def test_invalid_team(self):
  655. assert set() == self.adapter.get_send_to_team(self.project, "900001")
  656. def test_other_project_team(self):
  657. user_2 = self.create_user()
  658. team_2 = self.create_team(self.organization, members=[user_2])
  659. project_2 = self.create_project(organization=self.organization, teams=[team_2])
  660. assert {user_2.id} == self.adapter.get_send_to_team(project_2, str(team_2.id))
  661. assert set() == self.adapter.get_send_to_team(self.project, str(team_2.id))
  662. def test_other_org_team(self):
  663. org_2 = self.create_organization()
  664. user_2 = self.create_user()
  665. team_2 = self.create_team(org_2, members=[user_2])
  666. project_2 = self.create_project(organization=org_2, teams=[team_2])
  667. assert {user_2.id} == self.adapter.get_send_to_team(project_2, str(team_2.id))
  668. assert set() == self.adapter.get_send_to_team(self.project, str(team_2.id))
  669. class MailAdapterGetSendToMemberTest(BaseMailAdapterTest, TestCase):
  670. def test_send_to_user(self):
  671. assert {self.user.id} == self.adapter.get_send_to_member(self.project, str(self.user.id))
  672. def test_send_disabled_still_sends(self):
  673. NotificationSetting.objects.update_settings(
  674. ExternalProviders.EMAIL,
  675. NotificationSettingTypes.ISSUE_ALERTS,
  676. NotificationSettingOptionValues.NEVER,
  677. user=self.user,
  678. project=self.project,
  679. )
  680. assert {self.user.id} == self.adapter.get_send_to_member(self.project, str(self.user.id))
  681. def test_invalid_user(self):
  682. assert set() == self.adapter.get_send_to_member(self.project, "900001")
  683. def test_other_org_user(self):
  684. org_2 = self.create_organization()
  685. user_2 = self.create_user()
  686. team_2 = self.create_team(org_2, members=[user_2])
  687. team_3 = self.create_team(org_2, members=[user_2])
  688. project_2 = self.create_project(organization=org_2, teams=[team_2, team_3])
  689. assert {user_2.id} == self.adapter.get_send_to_member(project_2, str(user_2.id))
  690. assert set() == self.adapter.get_send_to_member(self.project, str(user_2.id))
  691. def test_no_project_access(self):
  692. org_2 = self.create_organization()
  693. user_2 = self.create_user()
  694. team_2 = self.create_team(org_2, members=[user_2])
  695. user_3 = self.create_user()
  696. self.create_team(org_2, members=[user_3])
  697. project_2 = self.create_project(organization=org_2, teams=[team_2])
  698. assert {user_2.id} == self.adapter.get_send_to_member(project_2, str(user_2.id))
  699. assert set() == self.adapter.get_send_to_member(self.project, str(user_3.id))
  700. class MailAdapterNotifyAboutActivityTest(BaseMailAdapterTest, TestCase):
  701. def test_assignment(self):
  702. NotificationSetting.objects.update_settings(
  703. ExternalProviders.EMAIL,
  704. NotificationSettingTypes.WORKFLOW,
  705. NotificationSettingOptionValues.ALWAYS,
  706. user=self.user,
  707. )
  708. activity = Activity.objects.create(
  709. project=self.project,
  710. group=self.group,
  711. type=Activity.ASSIGNED,
  712. user=self.create_user("foo@example.com"),
  713. data={"assignee": str(self.user.id), "assigneeType": "user"},
  714. )
  715. with self.tasks():
  716. self.adapter.notify_about_activity(activity)
  717. assert len(mail.outbox) == 1
  718. msg = mail.outbox[0]
  719. assert msg.subject == "Re: [Sentry] BAR-1 - こんにちは"
  720. assert msg.to == [self.user.email]
  721. def test_assignment_team(self):
  722. NotificationSetting.objects.update_settings(
  723. ExternalProviders.EMAIL,
  724. NotificationSettingTypes.WORKFLOW,
  725. NotificationSettingOptionValues.ALWAYS,
  726. user=self.user,
  727. )
  728. activity = Activity.objects.create(
  729. project=self.project,
  730. group=self.group,
  731. type=Activity.ASSIGNED,
  732. user=self.create_user("foo@example.com"),
  733. data={"assignee": str(self.project.teams.first().id), "assigneeType": "team"},
  734. )
  735. with self.tasks():
  736. self.adapter.notify_about_activity(activity)
  737. assert len(mail.outbox) == 1
  738. msg = mail.outbox[0]
  739. assert msg.subject == "Re: [Sentry] BAR-1 - こんにちは"
  740. assert msg.to == [self.user.email]
  741. def test_note(self):
  742. user_foo = self.create_user("foo@example.com")
  743. NotificationSetting.objects.update_settings(
  744. ExternalProviders.EMAIL,
  745. NotificationSettingTypes.WORKFLOW,
  746. NotificationSettingOptionValues.ALWAYS,
  747. user=self.user,
  748. )
  749. activity = Activity.objects.create(
  750. project=self.project,
  751. group=self.group,
  752. type=Activity.NOTE,
  753. user=user_foo,
  754. data={"text": "sup guise"},
  755. )
  756. self.project.teams.first().organization.member_set.create(user=user_foo)
  757. with self.tasks():
  758. self.adapter.notify_about_activity(activity)
  759. assert len(mail.outbox) >= 1
  760. msg = mail.outbox[-1]
  761. assert msg.subject == "Re: [Sentry] BAR-1 - こんにちは"
  762. assert msg.to == [self.user.email]
  763. class MailAdapterHandleSignalTest(BaseMailAdapterTest, TestCase):
  764. def create_report(self):
  765. user_foo = self.create_user("foo@example.com")
  766. self.project.teams.first().organization.member_set.create(user=user_foo)
  767. return UserReport.objects.create(
  768. project_id=self.project.id,
  769. group_id=self.group.id,
  770. name="Homer Simpson",
  771. email="homer.simpson@example.com",
  772. )
  773. def test_user_feedback(self):
  774. report = self.create_report()
  775. NotificationSetting.objects.update_settings(
  776. ExternalProviders.EMAIL,
  777. NotificationSettingTypes.WORKFLOW,
  778. NotificationSettingOptionValues.ALWAYS,
  779. user=self.user,
  780. )
  781. with self.tasks():
  782. self.adapter.handle_signal(
  783. name="user-reports.created",
  784. project=self.project,
  785. payload={
  786. "report": serialize(report, AnonymousUser(), UserReportWithGroupSerializer())
  787. },
  788. )
  789. assert len(mail.outbox) == 1
  790. msg = mail.outbox[0]
  791. # email includes issue metadata
  792. assert "group-header" in msg.alternatives[0][0]
  793. assert "enhanced privacy" not in msg.body
  794. assert (
  795. msg.subject
  796. == f"[Sentry] {self.group.qualified_short_id} - New Feedback from Homer Simpson"
  797. )
  798. assert msg.to == [self.user.email]
  799. def test_user_feedback__enhanced_privacy(self):
  800. self.organization.update(flags=F("flags").bitor(Organization.flags.enhanced_privacy))
  801. assert self.organization.flags.enhanced_privacy.is_set is True
  802. NotificationSetting.objects.update_settings(
  803. ExternalProviders.EMAIL,
  804. NotificationSettingTypes.WORKFLOW,
  805. NotificationSettingOptionValues.ALWAYS,
  806. user=self.user,
  807. )
  808. report = self.create_report()
  809. with self.tasks():
  810. self.adapter.handle_signal(
  811. name="user-reports.created",
  812. project=self.project,
  813. payload={
  814. "report": serialize(report, AnonymousUser(), UserReportWithGroupSerializer())
  815. },
  816. )
  817. assert len(mail.outbox) == 1
  818. msg = mail.outbox[0]
  819. # email does not include issue metadata
  820. assert "group-header" not in msg.alternatives[0][0]
  821. assert "enhanced privacy" in msg.body
  822. assert (
  823. msg.subject
  824. == f"[Sentry] {self.group.qualified_short_id} - New Feedback from Homer Simpson"
  825. )
  826. assert msg.to == [self.user.email]