|
@@ -1,37 +1,18 @@
|
|
|
from urllib.parse import parse_qs
|
|
|
|
|
|
import responses
|
|
|
-from django.utils import timezone
|
|
|
-from exam import fixture
|
|
|
|
|
|
import sentry
|
|
|
from sentry.digests.backends.redis import RedisBackend
|
|
|
from sentry.digests.notifications import event_to_record
|
|
|
-from sentry.integrations.slack.notifications import send_notification_as_slack
|
|
|
-from sentry.mail import mail_adapter
|
|
|
from sentry.models import (
|
|
|
- Activity,
|
|
|
- Deploy,
|
|
|
ExternalActor,
|
|
|
Identity,
|
|
|
IdentityProvider,
|
|
|
IdentityStatus,
|
|
|
- Integration,
|
|
|
NotificationSetting,
|
|
|
ProjectOwnership,
|
|
|
- Release,
|
|
|
Rule,
|
|
|
- UserOption,
|
|
|
-)
|
|
|
-from sentry.notifications.notifications.activity import (
|
|
|
- AssignedActivityNotification,
|
|
|
- NewProcessingIssuesActivityNotification,
|
|
|
- NoteActivityNotification,
|
|
|
- RegressionActivityNotification,
|
|
|
- ReleaseActivityNotification,
|
|
|
- ResolvedActivityNotification,
|
|
|
- ResolvedInReleaseActivityNotification,
|
|
|
- UnassignedActivityNotification,
|
|
|
)
|
|
|
from sentry.notifications.notifications.rules import AlertRuleNotification
|
|
|
from sentry.notifications.types import (
|
|
@@ -44,461 +25,15 @@ from sentry.ownership.grammar import Rule as GrammarRule
|
|
|
from sentry.ownership.grammar import dump_schema
|
|
|
from sentry.plugins.base import Notification
|
|
|
from sentry.tasks.digests import deliver_digest
|
|
|
-from sentry.testutils import TestCase
|
|
|
-from sentry.types.activity import ActivityType
|
|
|
from sentry.types.integrations import ExternalProviders
|
|
|
from sentry.utils import json
|
|
|
from sentry.utils.compat import mock
|
|
|
from sentry.utils.compat.mock import patch
|
|
|
-from tests.sentry.mail.activity import ActivityTestCase
|
|
|
-
|
|
|
-
|
|
|
-def send_notification(*args):
|
|
|
- args_list = list(args)[1:]
|
|
|
- send_notification_as_slack(*args_list, {})
|
|
|
-
|
|
|
-
|
|
|
-def get_attachment():
|
|
|
- assert len(responses.calls) >= 1
|
|
|
- data = parse_qs(responses.calls[0].request.body)
|
|
|
- assert "text" in data
|
|
|
- assert "attachments" in data
|
|
|
- attachments = json.loads(data["attachments"][0])
|
|
|
-
|
|
|
- assert len(attachments) == 1
|
|
|
- return attachments[0], data["text"][0]
|
|
|
-
|
|
|
-
|
|
|
-class SlackActivityNotificationTest(ActivityTestCase, TestCase):
|
|
|
- @fixture
|
|
|
- def adapter(self):
|
|
|
- return mail_adapter
|
|
|
-
|
|
|
- def setUp(self):
|
|
|
- NotificationSetting.objects.update_settings(
|
|
|
- ExternalProviders.SLACK,
|
|
|
- NotificationSettingTypes.WORKFLOW,
|
|
|
- NotificationSettingOptionValues.ALWAYS,
|
|
|
- user=self.user,
|
|
|
- )
|
|
|
- NotificationSetting.objects.update_settings(
|
|
|
- ExternalProviders.SLACK,
|
|
|
- NotificationSettingTypes.DEPLOY,
|
|
|
- NotificationSettingOptionValues.ALWAYS,
|
|
|
- user=self.user,
|
|
|
- )
|
|
|
- NotificationSetting.objects.update_settings(
|
|
|
- ExternalProviders.SLACK,
|
|
|
- NotificationSettingTypes.ISSUE_ALERTS,
|
|
|
- NotificationSettingOptionValues.ALWAYS,
|
|
|
- user=self.user,
|
|
|
- )
|
|
|
- UserOption.objects.create(user=self.user, key="self_notifications", value="1")
|
|
|
- self.integration = Integration.objects.create(
|
|
|
- provider="slack",
|
|
|
- name="Team A",
|
|
|
- external_id="TXXXXXXX1",
|
|
|
- metadata={
|
|
|
- "access_token": "xoxp-xxxxxxxxx-xxxxxxxxxx-xxxxxxxxxxxx",
|
|
|
- "installation_type": "born_as_bot",
|
|
|
- },
|
|
|
- )
|
|
|
- self.integration.add_organization(self.organization, self.user)
|
|
|
- self.idp = IdentityProvider.objects.create(type="slack", external_id="TXXXXXXX1", config={})
|
|
|
- self.identity = Identity.objects.create(
|
|
|
- external_id="UXXXXXXX1",
|
|
|
- idp=self.idp,
|
|
|
- user=self.user,
|
|
|
- status=IdentityStatus.VALID,
|
|
|
- scopes=[],
|
|
|
- )
|
|
|
- responses.add(
|
|
|
- method=responses.POST,
|
|
|
- url="https://slack.com/api/chat.postMessage",
|
|
|
- body='{"ok": true}',
|
|
|
- status=200,
|
|
|
- content_type="application/json",
|
|
|
- )
|
|
|
- self.name = self.user.get_display_name()
|
|
|
- self.short_id = self.group.qualified_short_id
|
|
|
-
|
|
|
- @responses.activate
|
|
|
- @mock.patch("sentry.notifications.notify.notify", side_effect=send_notification)
|
|
|
- def test_multiple_identities(self, mock_func):
|
|
|
- """
|
|
|
- Test that we notify a user with multiple Identities in each place
|
|
|
- """
|
|
|
- integration2 = Integration.objects.create(
|
|
|
- provider="slack",
|
|
|
- name="Team B",
|
|
|
- external_id="TXXXXXXX2",
|
|
|
- metadata={
|
|
|
- "access_token": "xoxp-xxxxxxxxx-xxxxxxxxxx-xxxxxxxxxxxx",
|
|
|
- "installation_type": "born_as_bot",
|
|
|
- },
|
|
|
- )
|
|
|
- integration2.add_organization(self.organization, self.user)
|
|
|
- idp2 = IdentityProvider.objects.create(type="slack", external_id="TXXXXXXX2", config={})
|
|
|
- identity2 = Identity.objects.create(
|
|
|
- external_id="UXXXXXXX2",
|
|
|
- idp=idp2,
|
|
|
- user=self.user,
|
|
|
- status=IdentityStatus.VALID,
|
|
|
- scopes=[],
|
|
|
- )
|
|
|
- # create a second response
|
|
|
- responses.add(
|
|
|
- method=responses.POST,
|
|
|
- url="https://slack.com/api/chat.postMessage",
|
|
|
- body='{"ok": true}',
|
|
|
- status=200,
|
|
|
- content_type="application/json",
|
|
|
- )
|
|
|
-
|
|
|
- notification = AssignedActivityNotification(
|
|
|
- Activity(
|
|
|
- project=self.project,
|
|
|
- group=self.group,
|
|
|
- user=self.user,
|
|
|
- type=ActivityType.ASSIGNED,
|
|
|
- data={"assignee": self.user.id},
|
|
|
- )
|
|
|
- )
|
|
|
- with self.tasks():
|
|
|
- notification.send()
|
|
|
-
|
|
|
- assert len(responses.calls) >= 2
|
|
|
- data = parse_qs(responses.calls[0].request.body)
|
|
|
- assert "channel" in data
|
|
|
- channel = data["channel"][0]
|
|
|
- assert channel == self.identity.external_id
|
|
|
|
|
|
- data = parse_qs(responses.calls[1].request.body)
|
|
|
- assert "channel" in data
|
|
|
- channel = data["channel"][0]
|
|
|
- assert channel == identity2.external_id
|
|
|
+from . import SlackActivityNotificationTest, get_attachment, send_notification
|
|
|
|
|
|
- @responses.activate
|
|
|
- @mock.patch("sentry.notifications.notify.notify", side_effect=send_notification)
|
|
|
- def test_multiple_orgs(self, mock_func):
|
|
|
- """
|
|
|
- Test that if a user is in 2 orgs with Slack and has an Identity linked in each,
|
|
|
- we're only going to notify them for the relevant org
|
|
|
- """
|
|
|
- org2 = self.create_organization(owner=self.user)
|
|
|
- integration2 = Integration.objects.create(
|
|
|
- provider="slack",
|
|
|
- name="Team B",
|
|
|
- external_id="TXXXXXXX2",
|
|
|
- metadata={
|
|
|
- "access_token": "xoxp-xxxxxxxxx-xxxxxxxxxx-xxxxxxxxxxxx",
|
|
|
- "installation_type": "born_as_bot",
|
|
|
- },
|
|
|
- )
|
|
|
- integration2.add_organization(org2, self.user)
|
|
|
- idp2 = IdentityProvider.objects.create(type="slack", external_id="TXXXXXXX2", config={})
|
|
|
- Identity.objects.create(
|
|
|
- external_id="UXXXXXXX2",
|
|
|
- idp=idp2,
|
|
|
- user=self.user,
|
|
|
- status=IdentityStatus.VALID,
|
|
|
- scopes=[],
|
|
|
- )
|
|
|
- # create a second response that won't actually be used, but here to make sure it's not a false positive
|
|
|
- responses.add(
|
|
|
- method=responses.POST,
|
|
|
- url="https://slack.com/api/chat.postMessage",
|
|
|
- body='{"ok": true}',
|
|
|
- status=200,
|
|
|
- content_type="application/json",
|
|
|
- )
|
|
|
-
|
|
|
- notification = AssignedActivityNotification(
|
|
|
- Activity(
|
|
|
- project=self.project,
|
|
|
- group=self.group,
|
|
|
- user=self.user,
|
|
|
- type=ActivityType.ASSIGNED,
|
|
|
- data={"assignee": self.user.id},
|
|
|
- )
|
|
|
- )
|
|
|
- with self.tasks():
|
|
|
- notification.send()
|
|
|
-
|
|
|
- assert len(responses.calls) == 1
|
|
|
- data = parse_qs(responses.calls[0].request.body)
|
|
|
- assert "channel" in data
|
|
|
- channel = data["channel"][0]
|
|
|
- assert channel == self.identity.external_id
|
|
|
-
|
|
|
- @responses.activate
|
|
|
- @mock.patch("sentry.notifications.notify.notify", side_effect=send_notification)
|
|
|
- def test_assignment(self, mock_func):
|
|
|
- """
|
|
|
- Test that a Slack message is sent with the expected payload when an issue is assigned
|
|
|
- """
|
|
|
- notification = AssignedActivityNotification(
|
|
|
- Activity(
|
|
|
- project=self.project,
|
|
|
- group=self.group,
|
|
|
- user=self.user,
|
|
|
- type=ActivityType.ASSIGNED,
|
|
|
- data={"assignee": self.user.id},
|
|
|
- )
|
|
|
- )
|
|
|
- with self.tasks():
|
|
|
- notification.send()
|
|
|
- attachment, text = get_attachment()
|
|
|
- assert text == f"Issue assigned to {self.name} by themselves"
|
|
|
- assert attachment["title"] == self.group.title
|
|
|
- assert (
|
|
|
- attachment["footer"]
|
|
|
- == f"{self.project.slug} | <http://testserver/settings/account/notifications/workflow/?referrer=AssignedActivitySlack|Notification Settings>"
|
|
|
- )
|
|
|
-
|
|
|
- @responses.activate
|
|
|
- @mock.patch("sentry.notifications.notify.notify", side_effect=send_notification)
|
|
|
- def test_unassignment(self, mock_func):
|
|
|
- """
|
|
|
- Test that a Slack message is sent with the expected payload when an issue is unassigned
|
|
|
- """
|
|
|
- notification = UnassignedActivityNotification(
|
|
|
- Activity(
|
|
|
- project=self.project,
|
|
|
- group=self.group,
|
|
|
- user=self.user,
|
|
|
- type=ActivityType.ASSIGNED,
|
|
|
- data={"assignee": ""},
|
|
|
- )
|
|
|
- )
|
|
|
- with self.tasks():
|
|
|
- notification.send()
|
|
|
-
|
|
|
- attachment, text = get_attachment()
|
|
|
- assert text == f"Issue unassigned by {self.name}"
|
|
|
- assert attachment["title"] == self.group.title
|
|
|
- assert (
|
|
|
- attachment["footer"]
|
|
|
- == f"{self.project.slug} | <http://testserver/settings/account/notifications/workflow/?referrer=UnassignedActivitySlack|Notification Settings>"
|
|
|
- )
|
|
|
-
|
|
|
- @responses.activate
|
|
|
- @mock.patch("sentry.notifications.notify.notify", side_effect=send_notification)
|
|
|
- def test_resolved(self, mock_func):
|
|
|
- """
|
|
|
- Test that a Slack message is sent with the expected payload when an issue is resolved
|
|
|
- """
|
|
|
- notification = ResolvedActivityNotification(
|
|
|
- Activity(
|
|
|
- project=self.project,
|
|
|
- group=self.group,
|
|
|
- user=self.user,
|
|
|
- type=ActivityType.SET_RESOLVED,
|
|
|
- data={"assignee": ""},
|
|
|
- )
|
|
|
- )
|
|
|
- with self.tasks():
|
|
|
- notification.send()
|
|
|
-
|
|
|
- attachment, text = get_attachment()
|
|
|
- assert (
|
|
|
- text
|
|
|
- == f"{self.name} marked <http://testserver/organizations/{self.organization.slug}/issues/{self.group.id}/?referrer=activity_notification|{self.short_id}> as resolved"
|
|
|
- )
|
|
|
- assert (
|
|
|
- attachment["footer"]
|
|
|
- == f"{self.project.slug} | <http://testserver/settings/account/notifications/workflow/?referrer=ResolvedActivitySlack|Notification Settings>"
|
|
|
- )
|
|
|
-
|
|
|
- @responses.activate
|
|
|
- @mock.patch("sentry.notifications.notify.notify", side_effect=send_notification)
|
|
|
- def test_regression(self, mock_func):
|
|
|
- """
|
|
|
- Test that a Slack message is sent with the expected payload when an issue regresses
|
|
|
- """
|
|
|
- notification = RegressionActivityNotification(
|
|
|
- Activity(
|
|
|
- project=self.project,
|
|
|
- group=self.group,
|
|
|
- user=self.user,
|
|
|
- type=ActivityType.SET_REGRESSION,
|
|
|
- data={},
|
|
|
- )
|
|
|
- )
|
|
|
- with self.tasks():
|
|
|
- notification.send()
|
|
|
-
|
|
|
- attachment, text = get_attachment()
|
|
|
- assert text == "Issue marked as regression"
|
|
|
- assert (
|
|
|
- attachment["footer"]
|
|
|
- == f"{self.project.slug} | <http://testserver/settings/account/notifications/workflow/?referrer=RegressionActivitySlack|Notification Settings>"
|
|
|
- )
|
|
|
-
|
|
|
- @responses.activate
|
|
|
- @mock.patch("sentry.notifications.notify.notify", side_effect=send_notification)
|
|
|
- def test_new_processing_issue(self, mock_func):
|
|
|
- """
|
|
|
- Test that a Slack message is sent with the expected payload when an issue is held back in reprocessing
|
|
|
- """
|
|
|
- data = [
|
|
|
- {
|
|
|
- "data": {
|
|
|
- "image_arch": "arm64",
|
|
|
- "image_path": "/var/containers/Bundle/Application/FB14D416-DE4E-4224-9789-6B88E9C42601/CrashProbeiOS.app/CrashProbeiOS",
|
|
|
- "image_uuid": "a2df1794-e0c7-371c-baa4-93eac340a78a",
|
|
|
- },
|
|
|
- "object": "dsym:a2df1794-e0c7-371c-baa4-93eac340a78a",
|
|
|
- "scope": "native",
|
|
|
- "type": "native_missing_dsym",
|
|
|
- },
|
|
|
- {
|
|
|
- "data": {
|
|
|
- "image_arch": "arm64",
|
|
|
- "image_path": "/var/containers/Bundle/Application/FB14D416-DE4E-4224-9789-6B88E9C42601/CrashProbeiOS.app/libCrashProbeiOS",
|
|
|
- "image_uuid": "12dc1b4c-a01b-463f-ae88-5cf0c31ae680",
|
|
|
- },
|
|
|
- "object": "dsym:12dc1b4c-a01b-463f-ae88-5cf0c31ae680",
|
|
|
- "scope": "native",
|
|
|
- "type": "native_bad_dsym",
|
|
|
- },
|
|
|
- ]
|
|
|
- notification = NewProcessingIssuesActivityNotification(
|
|
|
- Activity(
|
|
|
- project=self.project,
|
|
|
- user=self.user,
|
|
|
- type=ActivityType.NEW_PROCESSING_ISSUES,
|
|
|
- data={
|
|
|
- "issues": data,
|
|
|
- "reprocessing_active": True,
|
|
|
- },
|
|
|
- )
|
|
|
- )
|
|
|
- with self.tasks():
|
|
|
- notification.send()
|
|
|
-
|
|
|
- attachment, text = get_attachment()
|
|
|
- assert (
|
|
|
- text
|
|
|
- == f"Processing issues on <{self.project.slug}|http://testserver/settings/{self.organization.slug}/projects/{self.project.slug}/processing-issues/"
|
|
|
- )
|
|
|
- assert (
|
|
|
- attachment["text"]
|
|
|
- == f"Some events failed to process in your project {self.project.slug}"
|
|
|
- )
|
|
|
- assert (
|
|
|
- attachment["footer"]
|
|
|
- == f"{self.project.slug} | <http://testserver/settings/account/notifications/workflow/?referrer=NewProcessingIssuesActivitySlack|Notification Settings>"
|
|
|
- )
|
|
|
-
|
|
|
- @responses.activate
|
|
|
- @mock.patch("sentry.notifications.notify.notify", side_effect=send_notification)
|
|
|
- def test_resolved_in_release(self, mock_func):
|
|
|
- """
|
|
|
- Test that a Slack message is sent with the expected payload when an issue is resolved in a release
|
|
|
- """
|
|
|
- notification = ResolvedInReleaseActivityNotification(
|
|
|
- Activity(
|
|
|
- project=self.project,
|
|
|
- group=self.group,
|
|
|
- user=self.user,
|
|
|
- type=ActivityType.SET_RESOLVED_IN_RELEASE,
|
|
|
- data={"version": "meow"},
|
|
|
- )
|
|
|
- )
|
|
|
- with self.tasks():
|
|
|
- notification.send()
|
|
|
-
|
|
|
- attachment, text = get_attachment()
|
|
|
- release_name = notification.activity.data["version"]
|
|
|
- assert text == f"Issue marked as resolved in {release_name} by {self.name}"
|
|
|
- assert (
|
|
|
- attachment["footer"]
|
|
|
- == f"{self.project.slug} | <http://testserver/settings/account/notifications/workflow/?referrer=ResolvedInReleaseActivitySlack|Notification Settings>"
|
|
|
- )
|
|
|
-
|
|
|
- @responses.activate
|
|
|
- @mock.patch("sentry.notifications.notify.notify", side_effect=send_notification)
|
|
|
- def test_note(self, mock_func):
|
|
|
- """
|
|
|
- Test that a Slack message is sent with the expected payload when a comment is made on an issue
|
|
|
- """
|
|
|
- notification = NoteActivityNotification(
|
|
|
- Activity(
|
|
|
- project=self.project,
|
|
|
- group=self.group,
|
|
|
- user=self.user,
|
|
|
- type=ActivityType.NOTE,
|
|
|
- data={"text": "text", "mentions": []},
|
|
|
- )
|
|
|
- )
|
|
|
- with self.tasks():
|
|
|
- notification.send()
|
|
|
-
|
|
|
- attachment, text = get_attachment()
|
|
|
-
|
|
|
- assert text == f"New comment by {self.name}"
|
|
|
- assert attachment["title"] == f"{self.group.title}"
|
|
|
- assert (
|
|
|
- attachment["title_link"]
|
|
|
- == f"http://testserver/organizations/{self.organization.slug}/issues/{self.group.id}/?referrer=NoteActivitySlack"
|
|
|
- )
|
|
|
- assert attachment["text"] == notification.activity.data["text"]
|
|
|
- assert (
|
|
|
- attachment["footer"]
|
|
|
- == f"{self.project.slug} | <http://testserver/settings/account/notifications/workflow/?referrer=NoteActivitySlack|Notification Settings>"
|
|
|
- )
|
|
|
-
|
|
|
- @responses.activate
|
|
|
- @mock.patch("sentry.notifications.notify.notify", side_effect=send_notification)
|
|
|
- def test_deploy(self, mock_func):
|
|
|
- """
|
|
|
- Test that a Slack message is sent with the expected payload when a deploy happens
|
|
|
- """
|
|
|
- release = Release.objects.create(
|
|
|
- version="meow" * 10,
|
|
|
- organization_id=self.project.organization_id,
|
|
|
- date_released=timezone.now(),
|
|
|
- )
|
|
|
- project2 = self.create_project(name="battlesnake")
|
|
|
- release.add_project(self.project)
|
|
|
- release.add_project(project2)
|
|
|
- deploy = Deploy.objects.create(
|
|
|
- release=release,
|
|
|
- organization_id=self.organization.id,
|
|
|
- environment_id=self.environment.id,
|
|
|
- )
|
|
|
- notification = ReleaseActivityNotification(
|
|
|
- Activity(
|
|
|
- project=self.project,
|
|
|
- user=self.user,
|
|
|
- type=Activity.RELEASE,
|
|
|
- data={"version": release.version, "deploy_id": deploy.id},
|
|
|
- )
|
|
|
- )
|
|
|
- with self.tasks():
|
|
|
- notification.send()
|
|
|
-
|
|
|
- attachment, text = get_attachment()
|
|
|
- assert (
|
|
|
- text
|
|
|
- == f"Release {release.version} was deployed to {self.environment.name} for these projects"
|
|
|
- )
|
|
|
- assert attachment["actions"][0]["text"] == self.project.slug
|
|
|
- assert (
|
|
|
- attachment["actions"][0]["url"]
|
|
|
- == f"http://testserver/organizations/{self.organization.slug}/releases/{release.version}/?project={self.project.id}&unselectedSeries=Healthy/"
|
|
|
- )
|
|
|
- assert attachment["actions"][1]["text"] == project2.slug
|
|
|
- assert (
|
|
|
- attachment["actions"][1]["url"]
|
|
|
- == f"http://testserver/organizations/{self.organization.slug}/releases/{release.version}/?project={project2.id}&unselectedSeries=Healthy/"
|
|
|
- )
|
|
|
- assert (
|
|
|
- attachment["footer"]
|
|
|
- == f"{self.project.slug} | <http://testserver/settings/account/notifications/deploy/?referrer=ReleaseActivitySlack|Notification Settings>"
|
|
|
- )
|
|
|
|
|
|
+class SlackUnassignedNotificationTest(SlackActivityNotificationTest):
|
|
|
@responses.activate
|
|
|
@mock.patch("sentry.notifications.notify.notify", side_effect=send_notification)
|
|
|
def test_issue_alert_user(self, mock_func):
|