test_commit_context.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804
  1. from datetime import datetime, timedelta
  2. from datetime import timezone as datetime_timezone
  3. from unittest.mock import Mock, patch
  4. import pytest
  5. import responses
  6. from celery.exceptions import MaxRetriesExceededError
  7. from django.utils import timezone
  8. from sentry.integrations.github.integration import GitHubIntegrationProvider
  9. from sentry.models import PullRequest, PullRequestComment, Repository
  10. from sentry.models.commit import Commit
  11. from sentry.models.groupowner import GroupOwner, GroupOwnerType
  12. from sentry.models.options.organization_option import OrganizationOption
  13. from sentry.models.pullrequest import PullRequestCommit
  14. from sentry.shared_integrations.exceptions.base import ApiError
  15. from sentry.snuba.sessions_v2 import isoformat_z
  16. from sentry.tasks.commit_context import PR_COMMENT_WINDOW, process_commit_context
  17. from sentry.testutils.cases import IntegrationTestCase, TestCase
  18. from sentry.testutils.helpers.datetime import before_now, iso_format
  19. from sentry.testutils.silo import region_silo_test
  20. from sentry.utils.committers import get_frame_paths
  21. class TestCommitContextMixin(TestCase):
  22. def setUp(self):
  23. self.project = self.create_project()
  24. self.repo = Repository.objects.create(
  25. organization_id=self.organization.id,
  26. name="example",
  27. integration_id=self.integration.id,
  28. )
  29. self.code_mapping = self.create_code_mapping(
  30. repo=self.repo,
  31. project=self.project,
  32. )
  33. self.commit_author = self.create_commit_author(project=self.project, user=self.user)
  34. self.commit = self.create_commit(
  35. project=self.project,
  36. repo=self.repo,
  37. author=self.commit_author,
  38. key="asdfwreqr",
  39. message="placeholder commit message",
  40. )
  41. self.group = self.create_group(
  42. project=self.project, message="Kaboom!", first_release=self.release
  43. )
  44. self.event = self.store_event(
  45. data={
  46. "message": "Kaboom!",
  47. "platform": "python",
  48. "timestamp": iso_format(before_now(seconds=10)),
  49. "stacktrace": {
  50. "frames": [
  51. {
  52. "function": "handle_set_commits",
  53. "abs_path": "/usr/src/sentry/src/sentry/tasks.py",
  54. "module": "sentry.tasks",
  55. "in_app": False,
  56. "lineno": 30,
  57. "filename": "sentry/tasks.py",
  58. },
  59. {
  60. "function": "set_commits",
  61. "abs_path": "/usr/src/sentry/src/sentry/models/release.py",
  62. "module": "sentry.models.release",
  63. "in_app": True,
  64. "lineno": 39,
  65. "filename": "sentry/models/release.py",
  66. },
  67. ]
  68. },
  69. "tags": {"sentry:release": self.release.version},
  70. "fingerprint": ["put-me-in-the-control-group"],
  71. },
  72. project_id=self.project.id,
  73. )
  74. @region_silo_test(stable=True)
  75. class TestCommitContext(TestCommitContextMixin):
  76. @patch(
  77. "sentry.integrations.github.GitHubIntegration.get_commit_context",
  78. return_value={
  79. "commitId": "asdfwreqr",
  80. "committedDate": (datetime.now(tz=datetime_timezone.utc) - timedelta(days=7)),
  81. "commitMessage": "placeholder commit message",
  82. "commitAuthorName": "",
  83. "commitAuthorEmail": "admin@localhost",
  84. },
  85. )
  86. def test_simple(self, mock_get_commit_context):
  87. with self.tasks():
  88. assert not GroupOwner.objects.filter(group=self.event.group).exists()
  89. event_frames = get_frame_paths(self.event)
  90. process_commit_context(
  91. event_id=self.event.event_id,
  92. event_platform=self.event.platform,
  93. event_frames=event_frames,
  94. group_id=self.event.group_id,
  95. project_id=self.event.project_id,
  96. )
  97. assert GroupOwner.objects.get(
  98. group=self.event.group,
  99. project=self.event.project,
  100. organization=self.event.project.organization,
  101. type=GroupOwnerType.SUSPECT_COMMIT.value,
  102. )
  103. assert GroupOwner.objects.get(
  104. group=self.event.group,
  105. project=self.event.project,
  106. organization=self.event.project.organization,
  107. type=GroupOwnerType.SUSPECT_COMMIT.value,
  108. ).context == {"commitId": self.commit.id}
  109. @patch("sentry.analytics.record")
  110. @patch(
  111. "sentry.integrations.github.GitHubIntegration.get_commit_context",
  112. side_effect=ApiError(text="integration_failed"),
  113. )
  114. def test_failed_to_fetch_commit_context_record(self, mock_get_commit_context, mock_record):
  115. with self.tasks():
  116. assert not GroupOwner.objects.filter(group=self.event.group).exists()
  117. event_frames = get_frame_paths(self.event)
  118. process_commit_context(
  119. event_id=self.event.event_id,
  120. event_platform=self.event.platform,
  121. event_frames=event_frames,
  122. group_id=self.event.group_id,
  123. project_id=self.event.project_id,
  124. )
  125. mock_record.assert_called_with(
  126. "integrations.failed_to_fetch_commit_context",
  127. organization_id=self.organization.id,
  128. project_id=self.project.id,
  129. code_mapping_id=self.code_mapping.id,
  130. group_id=self.event.group_id,
  131. provider="github",
  132. error_message="integration_failed",
  133. )
  134. @patch("sentry.tasks.commit_context.logger")
  135. @patch(
  136. "sentry.integrations.github.GitHubIntegration.get_commit_context",
  137. return_value={
  138. "commitId": "asdfasdf",
  139. "committedDate": (datetime.now(tz=datetime_timezone.utc) - timedelta(days=370)),
  140. "commitMessage": "placeholder commit message",
  141. "commitAuthorName": "",
  142. "commitAuthorEmail": "admin@localhost",
  143. },
  144. )
  145. def test_found_commit_is_too_old(self, mock_get_commit_context, mock_logger):
  146. with self.tasks():
  147. assert not GroupOwner.objects.filter(group=self.event.group).exists()
  148. event_frames = get_frame_paths(self.event)
  149. process_commit_context(
  150. event_id=self.event.event_id,
  151. event_platform=self.event.platform,
  152. event_frames=event_frames,
  153. group_id=self.event.group_id,
  154. project_id=self.event.project_id,
  155. )
  156. assert mock_logger.info.call_count == 1
  157. mock_logger.info.assert_called_with(
  158. "process_commit_context.find_commit_context",
  159. extra={
  160. "event": self.event.event_id,
  161. "group": self.event.group_id,
  162. "organization": self.event.group.project.organization_id,
  163. "reason": "could_not_fetch_commit_context",
  164. "code_mappings_count": 1,
  165. "fallback": True,
  166. },
  167. )
  168. @patch(
  169. "sentry.integrations.github.GitHubIntegration.get_commit_context",
  170. return_value={
  171. "commitId": "asdfasdf",
  172. "committedDate": (datetime.now(tz=datetime_timezone.utc) - timedelta(days=7)),
  173. "commitMessage": "placeholder commit message",
  174. "commitAuthorName": "",
  175. "commitAuthorEmail": "admin@localhost",
  176. },
  177. )
  178. def test_no_matching_commit_in_db(self, mock_get_commit_context):
  179. with self.tasks():
  180. assert not GroupOwner.objects.filter(group=self.event.group).exists()
  181. assert not Commit.objects.filter(key="asdfasdf").exists()
  182. event_frames = get_frame_paths(self.event)
  183. process_commit_context(
  184. event_id=self.event.event_id,
  185. event_platform=self.event.platform,
  186. event_frames=event_frames,
  187. group_id=self.event.group_id,
  188. project_id=self.event.project_id,
  189. )
  190. assert Commit.objects.filter(key="asdfasdf").exists()
  191. assert GroupOwner.objects.filter(group=self.event.group).exists()
  192. @patch(
  193. "sentry.integrations.github.GitHubIntegration.get_commit_context",
  194. return_value={
  195. "commitId": "asdfwreqr",
  196. "committedDate": (datetime.now(tz=datetime_timezone.utc) - timedelta(days=7)),
  197. "commitMessage": "placeholder commit message",
  198. "commitAuthorName": "",
  199. "commitAuthorEmail": "admin@localhost",
  200. },
  201. )
  202. def test_delete_old_entries(self, mock_get_commit_context):
  203. # As new events come in associated with new owners, we should delete old ones.
  204. user_2 = self.create_user("another@user.com", is_superuser=True)
  205. self.create_member(teams=[self.team], user=user_2, organization=self.organization)
  206. owner = GroupOwner.objects.create(
  207. group=self.event.group,
  208. user_id=user_2.id,
  209. project=self.project,
  210. organization=self.organization,
  211. type=GroupOwnerType.SUSPECT_COMMIT.value,
  212. date_added=timezone.now() - timedelta(days=8),
  213. )
  214. with self.tasks():
  215. event_frames = get_frame_paths(self.event)
  216. process_commit_context(
  217. event_id=self.event.event_id,
  218. event_platform=self.event.platform,
  219. event_frames=event_frames,
  220. group_id=self.event.group_id,
  221. project_id=self.event.project_id,
  222. )
  223. assert not GroupOwner.objects.filter(id=owner.id).exists()
  224. assert GroupOwner.objects.filter(group=self.event.group).count() == 1
  225. assert GroupOwner.objects.filter(group=self.event.group, user_id=self.user.id).exists()
  226. @patch("sentry.tasks.groupowner.process_suspect_commits.delay")
  227. def test_no_inapp_frame_in_stacktrace(self, mock_process_suspect_commits):
  228. with self.tasks():
  229. assert not GroupOwner.objects.filter(group=self.event.group).exists()
  230. self.event_2 = self.store_event(
  231. data={
  232. "message": "Kaboom!",
  233. "platform": "python",
  234. "timestamp": iso_format(before_now(seconds=10)),
  235. "stacktrace": {
  236. "frames": [
  237. {
  238. "function": "handle_set_commits",
  239. "abs_path": "/usr/src/sentry/src/sentry/tasks.py",
  240. "module": "sentry.tasks",
  241. "in_app": False,
  242. "lineno": 30,
  243. "filename": "sentry/tasks.py",
  244. },
  245. {
  246. "function": "set_commits",
  247. "abs_path": "/usr/src/sentry/src/sentry/models/release.py",
  248. "module": "sentry.models.release",
  249. "in_app": False,
  250. "lineno": 39,
  251. "filename": "sentry/models/release.py",
  252. },
  253. ]
  254. },
  255. "tags": {"sentry:release": self.release.version},
  256. "fingerprint": ["put-me-in-the-control-group"],
  257. },
  258. project_id=self.project.id,
  259. )
  260. event_frames = get_frame_paths(self.event_2)
  261. process_commit_context(
  262. event_id=self.event.event_id,
  263. event_platform=self.event.platform,
  264. event_frames=event_frames,
  265. group_id=self.event.group_id,
  266. project_id=self.event.project_id,
  267. )
  268. assert mock_process_suspect_commits.call_count == 1
  269. assert not GroupOwner.objects.filter(
  270. group=self.event.group,
  271. project=self.event.project,
  272. organization=self.event.project.organization,
  273. type=GroupOwnerType.SUSPECT_COMMIT.value,
  274. ).exists()
  275. @patch(
  276. "sentry.integrations.github.GitHubIntegration.get_commit_context",
  277. return_value={
  278. "commitId": "somekey",
  279. "committedDate": (datetime.now(tz=datetime_timezone.utc) - timedelta(days=7)),
  280. "commitMessage": "placeholder commit message",
  281. "commitAuthorName": "",
  282. "commitAuthorEmail": "randomuser@sentry.io",
  283. },
  284. )
  285. def test_commit_author_not_in_sentry(self, mock_get_commit_context):
  286. self.commit_author_2 = self.create_commit_author(
  287. project=self.project,
  288. )
  289. self.commit_2 = self.create_commit(
  290. project=self.project,
  291. repo=self.repo,
  292. author=self.commit_author_2,
  293. key="somekey",
  294. message="placeholder commit message",
  295. )
  296. with self.tasks():
  297. assert not GroupOwner.objects.filter(group=self.event.group).exists()
  298. event_frames = get_frame_paths(self.event)
  299. process_commit_context(
  300. event_id=self.event.event_id,
  301. event_platform=self.event.platform,
  302. event_frames=event_frames,
  303. group_id=self.event.group_id,
  304. project_id=self.event.project_id,
  305. )
  306. assert GroupOwner.objects.filter(group=self.event.group).exists()
  307. assert len(GroupOwner.objects.filter(group=self.event.group)) == 1
  308. owner = GroupOwner.objects.get(group=self.event.group)
  309. assert owner.type == GroupOwnerType.SUSPECT_COMMIT.value
  310. assert owner.user_id is None
  311. assert owner.team is None
  312. assert owner.context == {"commitId": self.commit_2.id}
  313. @patch("sentry.tasks.commit_context.get_users_for_authors", return_value={})
  314. @patch(
  315. "sentry.integrations.github.GitHubIntegration.get_commit_context",
  316. return_value={
  317. "commitId": "somekey",
  318. "committedDate": (datetime.now(tz=datetime_timezone.utc) - timedelta(days=7)),
  319. "commitMessage": "placeholder commit message",
  320. "commitAuthorName": "",
  321. "commitAuthorEmail": "randomuser@sentry.io",
  322. },
  323. )
  324. def test_commit_author_no_user(self, mock_get_commit_context, mock_get_users_for_author):
  325. self.commit_author_2 = self.create_commit_author(
  326. project=self.project,
  327. )
  328. self.commit_2 = self.create_commit(
  329. project=self.project,
  330. repo=self.repo,
  331. author=self.commit_author_2,
  332. key="somekey",
  333. message="placeholder commit message",
  334. )
  335. with self.tasks(), patch(
  336. "sentry.tasks.commit_context.get_users_for_authors", return_value={}
  337. ):
  338. event_frames = get_frame_paths(self.event)
  339. process_commit_context(
  340. event_id=self.event.event_id,
  341. event_platform=self.event.platform,
  342. event_frames=event_frames,
  343. group_id=self.event.group_id,
  344. project_id=self.event.project_id,
  345. )
  346. assert GroupOwner.objects.filter(group=self.event.group).exists()
  347. assert len(GroupOwner.objects.filter(group=self.event.group)) == 1
  348. owner = GroupOwner.objects.get(group=self.event.group)
  349. assert owner.type == GroupOwnerType.SUSPECT_COMMIT.value
  350. assert owner.user_id is None
  351. assert owner.team is None
  352. assert owner.context == {"commitId": self.commit_2.id}
  353. @patch(
  354. "sentry.integrations.github.GitHubIntegration.get_commit_context",
  355. return_value={
  356. "commitId": "somekey",
  357. "committedDate": (datetime.now(tz=datetime_timezone.utc) - timedelta(days=7)),
  358. "commitMessage": "placeholder commit message",
  359. "commitAuthorName": "",
  360. "commitAuthorEmail": "randomuser@sentry.io",
  361. },
  362. )
  363. def test_multiple_matching_code_mappings_but_only_1_repository_has_the_commit_in_db(
  364. self, mock_get_commit_context
  365. ):
  366. self.integration_2 = self.create_integration(
  367. organization=self.organization,
  368. provider="github",
  369. name="GitHub",
  370. external_id="github:2",
  371. )
  372. self.repo_2 = Repository.objects.create(
  373. organization_id=self.organization.id,
  374. name="another/example",
  375. integration_id=self.integration_2.id,
  376. )
  377. self.code_mapping_2 = self.create_code_mapping(
  378. repo=self.repo_2, project=self.project, stack_root="src", source_root="src"
  379. )
  380. self.commit_author_2 = self.create_commit_author(
  381. project=self.project,
  382. )
  383. self.commit_2 = self.create_commit(
  384. project=self.project,
  385. repo=self.repo_2,
  386. author=self.commit_author_2,
  387. key="somekey",
  388. message="placeholder commit message",
  389. )
  390. with self.tasks():
  391. assert not GroupOwner.objects.filter(group=self.event.group).exists()
  392. event_frames = get_frame_paths(self.event)
  393. process_commit_context(
  394. event_id=self.event.event_id,
  395. event_platform=self.event.platform,
  396. event_frames=event_frames,
  397. group_id=self.event.group_id,
  398. project_id=self.event.project_id,
  399. )
  400. assert GroupOwner.objects.filter(group=self.event.group).exists()
  401. assert len(GroupOwner.objects.filter(group=self.event.group)) == 1
  402. owner = GroupOwner.objects.get(group=self.event.group)
  403. assert owner.type == GroupOwnerType.SUSPECT_COMMIT.value
  404. assert owner.user_id is None
  405. assert owner.team is None
  406. assert owner.context == {"commitId": self.commit_2.id}
  407. @patch(
  408. "sentry.integrations.github.GitHubIntegration.get_commit_context",
  409. side_effect=ApiError(text="integration_failed"),
  410. )
  411. @patch("sentry.tasks.groupowner.process_suspect_commits.delay")
  412. def test_fallback_if_max_retries_exceeded(self, mock_suspect_commits, mock_get_commit_context):
  413. def after_return(self, status, retval, task_id, args, kwargs, einfo):
  414. raise MaxRetriesExceededError()
  415. with self.tasks() and pytest.raises(MaxRetriesExceededError):
  416. with patch("celery.app.task.Task.after_return", after_return):
  417. process_commit_context.apply(
  418. kwargs={
  419. "event_id": self.event.event_id,
  420. "event_platform": self.event.platform,
  421. "event_frames": get_frame_paths(self.event),
  422. "group_id": self.event.group_id,
  423. "project_id": self.event.project_id,
  424. },
  425. retries=1,
  426. )
  427. assert mock_suspect_commits.called
  428. @region_silo_test(stable=True)
  429. @patch(
  430. "sentry.integrations.github.GitHubIntegration.get_commit_context",
  431. Mock(
  432. return_value={
  433. "commitId": "asdfwreqr",
  434. "committedDate": (datetime.now(tz=datetime_timezone.utc) - timedelta(days=7)),
  435. "commitMessage": "placeholder commit message",
  436. "commitAuthorName": "",
  437. "commitAuthorEmail": "admin@localhost",
  438. }
  439. ),
  440. )
  441. @patch("sentry.tasks.integrations.github.pr_comment.github_comment_workflow.delay")
  442. class TestGHCommentQueuing(IntegrationTestCase, TestCommitContextMixin):
  443. provider = GitHubIntegrationProvider
  444. base_url = "https://api.github.com"
  445. def setUp(self):
  446. super().setUp()
  447. self.pull_request = PullRequest.objects.create(
  448. organization_id=self.commit.organization_id,
  449. repository_id=self.repo.id,
  450. key="99",
  451. author=self.commit.author,
  452. message="foo",
  453. title="bar",
  454. merge_commit_sha=self.commit.key,
  455. date_added=iso_format(before_now(days=1)),
  456. )
  457. self.repo.provider = "integrations:github"
  458. self.repo.save()
  459. self.pull_request_comment = PullRequestComment.objects.create(
  460. pull_request=self.pull_request,
  461. external_id=1,
  462. created_at=iso_format(before_now(days=1)),
  463. updated_at=iso_format(before_now(days=1)),
  464. group_ids=[],
  465. )
  466. self.installation_id = "github:1"
  467. self.user_id = "user_1"
  468. self.app_id = "app_1"
  469. self.access_token = "xxxxx-xxxxxxxxx-xxxxxxxxxx-xxxxxxxxxxxx"
  470. self.expires_at = isoformat_z(timezone.now() + timedelta(days=365))
  471. def add_responses(self):
  472. responses.add(
  473. responses.POST,
  474. self.base_url + f"/app/installations/{self.installation_id}/access_tokens",
  475. json={"token": self.access_token, "expires_at": self.expires_at},
  476. )
  477. responses.add(
  478. responses.GET,
  479. self.base_url + f"/repos/example/commits/{self.commit.key}/pulls",
  480. status=200,
  481. json=[{"merge_commit_sha": self.pull_request.merge_commit_sha}],
  482. )
  483. def test_gh_comment_not_github(self, mock_comment_workflow):
  484. """Non github repos shouldn't be commented on"""
  485. self.repo.provider = "integrations:gitlab"
  486. self.repo.save()
  487. with self.tasks():
  488. event_frames = get_frame_paths(self.event)
  489. process_commit_context(
  490. event_id=self.event.event_id,
  491. event_platform=self.event.platform,
  492. event_frames=event_frames,
  493. group_id=self.event.group_id,
  494. project_id=self.event.project_id,
  495. )
  496. assert not mock_comment_workflow.called
  497. def test_gh_comment_org_option(self, mock_comment_workflow):
  498. """No comments on org with organization option disabled"""
  499. OrganizationOption.objects.set_value(
  500. organization=self.project.organization, key="sentry:github_pr_bot", value=False
  501. )
  502. with self.tasks():
  503. event_frames = get_frame_paths(self.event)
  504. process_commit_context(
  505. event_id=self.event.event_id,
  506. event_platform=self.event.platform,
  507. event_frames=event_frames,
  508. group_id=self.event.group_id,
  509. project_id=self.event.project_id,
  510. )
  511. assert not mock_comment_workflow.called
  512. @patch("sentry.integrations.github.client.get_jwt", return_value=b"jwt_token_1")
  513. @responses.activate
  514. def test_gh_comment_no_pr_from_api(self, get_jwt, mock_comment_workflow):
  515. """No comments on suspect commit with no pr returned from API response"""
  516. self.pull_request.delete()
  517. responses.add(
  518. responses.POST,
  519. self.base_url + f"/app/installations/{self.installation_id}/access_tokens",
  520. json={"token": self.access_token, "expires_at": self.expires_at},
  521. )
  522. responses.add(
  523. responses.GET,
  524. self.base_url + f"/repos/example/commits/{self.commit.key}/pulls",
  525. status=200,
  526. json={"message": "No commit found for SHA"},
  527. )
  528. with self.tasks():
  529. event_frames = get_frame_paths(self.event)
  530. process_commit_context(
  531. event_id=self.event.event_id,
  532. event_platform=self.event.platform,
  533. event_frames=event_frames,
  534. group_id=self.event.group_id,
  535. project_id=self.event.project_id,
  536. )
  537. assert not mock_comment_workflow.called
  538. @patch("sentry.integrations.github.client.get_jwt", return_value=b"jwt_token_1")
  539. @patch("sentry_sdk.capture_exception")
  540. @responses.activate
  541. def test_gh_comment_api_error(self, mock_capture_exception, get_jwt, mock_comment_workflow):
  542. """Captures exception if Github API call errors"""
  543. responses.add(
  544. responses.POST,
  545. self.base_url + f"/app/installations/{self.installation_id}/access_tokens",
  546. json={"token": self.access_token, "expires_at": self.expires_at},
  547. )
  548. responses.add(
  549. responses.GET,
  550. self.base_url + f"/repos/example/commits/{self.commit.key}/pulls",
  551. status=400,
  552. json={"message": "error"},
  553. )
  554. with self.tasks():
  555. event_frames = get_frame_paths(self.event)
  556. process_commit_context(
  557. event_id=self.event.event_id,
  558. event_platform=self.event.platform,
  559. event_frames=event_frames,
  560. group_id=self.event.group_id,
  561. project_id=self.event.project_id,
  562. )
  563. assert mock_capture_exception.called
  564. assert not mock_comment_workflow.called
  565. @patch("sentry.integrations.github.client.get_jwt", return_value=b"jwt_token_1")
  566. @responses.activate
  567. def test_gh_comment_commit_not_in_default_branch(self, get_jwt, mock_comment_workflow):
  568. """No comments on commit not in default branch"""
  569. responses.add(
  570. responses.POST,
  571. self.base_url + f"/app/installations/{self.installation_id}/access_tokens",
  572. json={"token": self.access_token, "expires_at": self.expires_at},
  573. )
  574. responses.add(
  575. responses.GET,
  576. self.base_url + f"/repos/example/commits/{self.commit.key}/pulls",
  577. status=200,
  578. json=[{"merge_commit_sha": "abcd"}, {"merge_commit_sha": "efgh"}],
  579. )
  580. with self.tasks():
  581. event_frames = get_frame_paths(self.event)
  582. process_commit_context(
  583. event_id=self.event.event_id,
  584. event_platform=self.event.platform,
  585. event_frames=event_frames,
  586. group_id=self.event.group_id,
  587. project_id=self.event.project_id,
  588. )
  589. assert not mock_comment_workflow.called
  590. @patch("sentry.integrations.github.client.get_jwt", return_value=b"jwt_token_1")
  591. @responses.activate
  592. def test_gh_comment_no_pr_from_query(self, get_jwt, mock_comment_workflow):
  593. """No comments on suspect commit with no pr row in table"""
  594. self.pull_request.delete()
  595. self.add_responses()
  596. with self.tasks():
  597. event_frames = get_frame_paths(self.event)
  598. process_commit_context(
  599. event_id=self.event.event_id,
  600. event_platform=self.event.platform,
  601. event_frames=event_frames,
  602. group_id=self.event.group_id,
  603. project_id=self.event.project_id,
  604. )
  605. assert not mock_comment_workflow.called
  606. @patch("sentry.integrations.github.client.get_jwt", return_value=b"jwt_token_1")
  607. @responses.activate
  608. def test_gh_comment_pr_too_old(self, get_jwt, mock_comment_workflow):
  609. """No comment on pr that's older than PR_COMMENT_WINDOW"""
  610. self.pull_request.date_added = iso_format(before_now(days=PR_COMMENT_WINDOW + 1))
  611. self.pull_request.save()
  612. self.add_responses()
  613. with self.tasks():
  614. event_frames = get_frame_paths(self.event)
  615. process_commit_context(
  616. event_id=self.event.event_id,
  617. event_platform=self.event.platform,
  618. event_frames=event_frames,
  619. group_id=self.event.group_id,
  620. project_id=self.event.project_id,
  621. )
  622. assert not mock_comment_workflow.called
  623. assert len(PullRequestCommit.objects.all()) == 0
  624. @patch("sentry.integrations.github.client.get_jwt", return_value=b"jwt_token_1")
  625. @responses.activate
  626. def test_gh_comment_repeat_issue(self, get_jwt, mock_comment_workflow):
  627. """No comment on a pr that has a comment with the issue in the same pr list"""
  628. self.pull_request_comment.group_ids.append(self.event.group_id)
  629. self.pull_request_comment.save()
  630. self.add_responses()
  631. with self.tasks():
  632. event_frames = get_frame_paths(self.event)
  633. process_commit_context(
  634. event_id=self.event.event_id,
  635. event_platform=self.event.platform,
  636. event_frames=event_frames,
  637. group_id=self.event.group_id,
  638. project_id=self.event.project_id,
  639. )
  640. assert not mock_comment_workflow.called
  641. assert len(PullRequestCommit.objects.all()) == 0
  642. @patch("sentry.integrations.github.client.get_jwt", return_value=b"jwt_token_1")
  643. @responses.activate
  644. def test_gh_comment_create_queued(self, get_jwt, mock_comment_workflow):
  645. """Task queued if no prior comment exists"""
  646. self.pull_request_comment.delete()
  647. self.add_responses()
  648. with self.tasks():
  649. event_frames = get_frame_paths(self.event)
  650. process_commit_context(
  651. event_id=self.event.event_id,
  652. event_platform=self.event.platform,
  653. event_frames=event_frames,
  654. group_id=self.event.group_id,
  655. project_id=self.event.project_id,
  656. )
  657. assert mock_comment_workflow.called
  658. pr_commits = PullRequestCommit.objects.all()
  659. assert len(pr_commits) == 1
  660. assert pr_commits[0].commit == self.commit
  661. @patch("sentry.integrations.github.client.get_jwt", return_value=b"jwt_token_1")
  662. @responses.activate
  663. def test_gh_comment_create_queued_existing_pr_commit(self, get_jwt, mock_comment_workflow):
  664. """Task queued if no prior comment exists"""
  665. pr_commit = PullRequestCommit.objects.create(
  666. commit=self.commit, pull_request=self.pull_request
  667. )
  668. self.pull_request_comment.delete()
  669. self.add_responses()
  670. with self.tasks():
  671. event_frames = get_frame_paths(self.event)
  672. process_commit_context(
  673. event_id=self.event.event_id,
  674. event_platform=self.event.platform,
  675. event_frames=event_frames,
  676. group_id=self.event.group_id,
  677. project_id=self.event.project_id,
  678. )
  679. assert mock_comment_workflow.called
  680. pr_commits = PullRequestCommit.objects.all()
  681. assert len(pr_commits) == 1
  682. assert pr_commits[0] == pr_commit
  683. @patch("sentry.integrations.github.client.get_jwt", return_value=b"jwt_token_1")
  684. @responses.activate
  685. def test_gh_comment_update_queue(self, get_jwt, mock_comment_workflow):
  686. """Task queued if new issue for prior comment"""
  687. self.add_responses()
  688. with self.tasks():
  689. assert not GroupOwner.objects.filter(group=self.event.group).exists()
  690. event_frames = get_frame_paths(self.event)
  691. process_commit_context(
  692. event_id=self.event.event_id,
  693. event_platform=self.event.platform,
  694. event_frames=event_frames,
  695. group_id=self.event.group_id,
  696. project_id=self.event.project_id,
  697. )
  698. assert mock_comment_workflow.called
  699. pr_commits = PullRequestCommit.objects.all()
  700. assert len(pr_commits) == 1
  701. assert pr_commits[0].commit == self.commit
  702. def test_gh_comment_no_repo(self, mock_comment_workflow):
  703. """No comments on suspect commit if no repo row exists"""
  704. self.repo.delete()
  705. with self.tasks():
  706. event_frames = get_frame_paths(self.event)
  707. process_commit_context(
  708. event_id=self.event.event_id,
  709. event_platform=self.event.platform,
  710. event_frames=event_frames,
  711. group_id=self.event.group_id,
  712. project_id=self.event.project_id,
  713. )
  714. assert not mock_comment_workflow.called
  715. assert len(PullRequestCommit.objects.all()) == 0
  716. @patch("sentry.integrations.github.client.get_jwt", return_value=b"jwt_token_1")
  717. @responses.activate
  718. def test_gh_comment_debounces(self, get_jwt, mock_comment_workflow):
  719. self.add_responses()
  720. with self.tasks():
  721. assert not GroupOwner.objects.filter(group=self.event.group).exists()
  722. event_frames = get_frame_paths(self.event)
  723. process_commit_context(
  724. event_id=self.event.event_id,
  725. event_platform=self.event.platform,
  726. event_frames=event_frames,
  727. group_id=self.event.group_id,
  728. project_id=self.event.project_id,
  729. )
  730. process_commit_context(
  731. event_id=self.event.event_id,
  732. event_platform=self.event.platform,
  733. event_frames=event_frames,
  734. group_id=self.event.group_id,
  735. project_id=self.event.project_id,
  736. )
  737. assert mock_comment_workflow.call_count == 1