|
@@ -1,9 +1,13 @@
|
|
|
+from collections.abc import Sequence
|
|
|
from unittest.mock import patch
|
|
|
|
|
|
+import responses
|
|
|
+
|
|
|
from sentry.api.serializers.base import serialize
|
|
|
from sentry.constants import SentryAppInstallationStatus
|
|
|
from sentry.coreapi import APIError
|
|
|
from sentry.sentry_apps.models.sentry_app import SentryApp
|
|
|
+from sentry.sentry_apps.models.sentry_app_component import SentryAppComponent
|
|
|
from sentry.sentry_apps.utils.errors import SentryAppIntegratorError, SentryAppSentryError
|
|
|
from sentry.testutils.cases import APITestCase
|
|
|
from sentry.testutils.silo import control_silo_test
|
|
@@ -40,7 +44,7 @@ class SentryAppComponentsTest(APITestCase):
|
|
|
"uuid": str(self.component.uuid),
|
|
|
"type": "issue-link",
|
|
|
"schema": self.component.schema,
|
|
|
- "error": False,
|
|
|
+ "error": "",
|
|
|
"sentryApp": {
|
|
|
"uuid": self.sentry_app.uuid,
|
|
|
"slug": self.sentry_app.slug,
|
|
@@ -104,7 +108,7 @@ class OrganizationSentryAppComponentsTest(APITestCase):
|
|
|
"uuid": str(self.component1.uuid),
|
|
|
"type": "issue-link",
|
|
|
"schema": self.component1.schema,
|
|
|
- "error": False,
|
|
|
+ "error": "",
|
|
|
"sentryApp": {
|
|
|
"uuid": self.sentry_app1.uuid,
|
|
|
"slug": self.sentry_app1.slug,
|
|
@@ -117,7 +121,7 @@ class OrganizationSentryAppComponentsTest(APITestCase):
|
|
|
"uuid": str(self.component2.uuid),
|
|
|
"type": "issue-link",
|
|
|
"schema": self.component2.schema,
|
|
|
- "error": False,
|
|
|
+ "error": "",
|
|
|
"sentryApp": {
|
|
|
"uuid": self.sentry_app2.uuid,
|
|
|
"slug": self.sentry_app2.slug,
|
|
@@ -143,7 +147,7 @@ class OrganizationSentryAppComponentsTest(APITestCase):
|
|
|
"uuid": str(component.uuid),
|
|
|
"type": "alert-rule",
|
|
|
"schema": component.schema,
|
|
|
- "error": False,
|
|
|
+ "error": "",
|
|
|
"sentryApp": {
|
|
|
"uuid": sentry_app.uuid,
|
|
|
"slug": sentry_app.slug,
|
|
@@ -161,7 +165,10 @@ class OrganizationSentryAppComponentsTest(APITestCase):
|
|
|
|
|
|
@patch("sentry.sentry_apps.components.SentryAppComponentPreparer.run")
|
|
|
def test_component_prep_errors_are_isolated(self, run):
|
|
|
- run.side_effect = [SentryAppIntegratorError(message="whoops"), self.component2]
|
|
|
+ run.side_effect = [
|
|
|
+ SentryAppIntegratorError(message="zoinks!", public_context={"foo": "bar"}),
|
|
|
+ self.component2,
|
|
|
+ ]
|
|
|
|
|
|
response = self.get_success_response(
|
|
|
self.org.slug, qs_params={"projectId": self.project.id}
|
|
@@ -174,7 +181,7 @@ class OrganizationSentryAppComponentsTest(APITestCase):
|
|
|
"uuid": str(self.component1.uuid),
|
|
|
"type": self.component1.type,
|
|
|
"schema": self.component1.schema,
|
|
|
- "error": True,
|
|
|
+ "error": {"detail": "zoinks!", "context": {"foo": "bar"}},
|
|
|
"sentryApp": {
|
|
|
"uuid": self.sentry_app1.uuid,
|
|
|
"slug": self.sentry_app1.slug,
|
|
@@ -186,7 +193,7 @@ class OrganizationSentryAppComponentsTest(APITestCase):
|
|
|
"uuid": str(self.component2.uuid),
|
|
|
"type": self.component2.type,
|
|
|
"schema": self.component2.schema,
|
|
|
- "error": False,
|
|
|
+ "error": "",
|
|
|
"sentryApp": {
|
|
|
"uuid": self.sentry_app2.uuid,
|
|
|
"slug": self.sentry_app2.slug,
|
|
@@ -198,9 +205,169 @@ class OrganizationSentryAppComponentsTest(APITestCase):
|
|
|
|
|
|
assert response.data == expected
|
|
|
|
|
|
+ @responses.activate
|
|
|
+ def test_component_prep_api_error(self):
|
|
|
+ responses.add(
|
|
|
+ method=responses.GET,
|
|
|
+ url="https://example.com/",
|
|
|
+ json={"error": "the dumpsters on fire!!!"},
|
|
|
+ status=500,
|
|
|
+ content_type="application/json",
|
|
|
+ )
|
|
|
+
|
|
|
+ responses.add(
|
|
|
+ method=responses.GET,
|
|
|
+ url="https://example.com/",
|
|
|
+ json={"error": "couldnt find the dumpsters :C"},
|
|
|
+ status=404,
|
|
|
+ content_type="application/json",
|
|
|
+ )
|
|
|
+
|
|
|
+ response = self.get_success_response(
|
|
|
+ self.org.slug, qs_params={"projectId": self.project.id}
|
|
|
+ )
|
|
|
+ expected = [
|
|
|
+ {
|
|
|
+ "uuid": str(self.component1.uuid),
|
|
|
+ "type": self.component1.type,
|
|
|
+ "schema": self.component1.schema,
|
|
|
+ "error": {
|
|
|
+ "detail": f"Something went wrong while getting options for Select FormField from {self.sentry_app1.slug}"
|
|
|
+ },
|
|
|
+ "sentryApp": {
|
|
|
+ "uuid": self.sentry_app1.uuid,
|
|
|
+ "slug": self.sentry_app1.slug,
|
|
|
+ "name": self.sentry_app1.name,
|
|
|
+ "avatars": get_sentry_app_avatars(self.sentry_app1),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "uuid": str(self.component2.uuid),
|
|
|
+ "type": self.component2.type,
|
|
|
+ "schema": self.component2.schema,
|
|
|
+ "error": {
|
|
|
+ "detail": f"Something went wrong while getting options for Select FormField from {self.sentry_app2.slug}"
|
|
|
+ },
|
|
|
+ "sentryApp": {
|
|
|
+ "uuid": self.sentry_app2.uuid,
|
|
|
+ "slug": self.sentry_app2.slug,
|
|
|
+ "name": self.sentry_app2.name,
|
|
|
+ "avatars": get_sentry_app_avatars(self.sentry_app2),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ ]
|
|
|
+
|
|
|
+ assert response.data == expected
|
|
|
+
|
|
|
+ @responses.activate
|
|
|
+ def test_component_prep_validation_error(self):
|
|
|
+ component1_uris = self._get_component_uris(
|
|
|
+ component_field="link", component=self.component1
|
|
|
+ )
|
|
|
+
|
|
|
+ component2_uris = self._get_component_uris(
|
|
|
+ component_field="link", component=self.component2
|
|
|
+ )
|
|
|
+
|
|
|
+ # We only get the first uri since the SentryAppComponentPreparer will short circuit after getting the first error
|
|
|
+ responses.add(
|
|
|
+ method=responses.GET,
|
|
|
+ url=f"https://example.com{component1_uris[0]}?installationId={self.install1.uuid}",
|
|
|
+ json=[{"bruh": "the dumpsters on fire!!!"}],
|
|
|
+ status=200,
|
|
|
+ content_type="application/json",
|
|
|
+ )
|
|
|
+
|
|
|
+ responses.add(
|
|
|
+ method=responses.GET,
|
|
|
+ url=f"https://example.com{component2_uris[0]}?installationId={self.install2.uuid}",
|
|
|
+ json={},
|
|
|
+ status=200,
|
|
|
+ content_type="application/json",
|
|
|
+ )
|
|
|
+
|
|
|
+ response = self.get_success_response(
|
|
|
+ self.org.slug, qs_params={"projectId": self.project.id}
|
|
|
+ )
|
|
|
+ expected = [
|
|
|
+ {
|
|
|
+ "uuid": str(self.component1.uuid),
|
|
|
+ "type": self.component1.type,
|
|
|
+ "schema": self.component1.schema,
|
|
|
+ "error": {
|
|
|
+ "detail": "Missing `value` or `label` in option data for Select FormField"
|
|
|
+ },
|
|
|
+ "sentryApp": {
|
|
|
+ "uuid": self.sentry_app1.uuid,
|
|
|
+ "slug": self.sentry_app1.slug,
|
|
|
+ "name": self.sentry_app1.name,
|
|
|
+ "avatars": get_sentry_app_avatars(self.sentry_app1),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "uuid": str(self.component2.uuid),
|
|
|
+ "type": self.component2.type,
|
|
|
+ "schema": self.component2.schema,
|
|
|
+ "error": {
|
|
|
+ "detail": f"Invalid response format for Select FormField in {self.sentry_app2.slug} from uri: {component2_uris[0]}"
|
|
|
+ },
|
|
|
+ "sentryApp": {
|
|
|
+ "uuid": self.sentry_app2.uuid,
|
|
|
+ "slug": self.sentry_app2.slug,
|
|
|
+ "name": self.sentry_app2.name,
|
|
|
+ "avatars": get_sentry_app_avatars(self.sentry_app2),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ ]
|
|
|
+
|
|
|
+ assert response.data == expected
|
|
|
+
|
|
|
+ @patch("sentry_sdk.capture_exception")
|
|
|
+ @patch("sentry.sentry_apps.components.SentryAppComponentPreparer.run")
|
|
|
+ def test_component_prep_general_error(self, run, capture_exception):
|
|
|
+ run.side_effect = [Exception(":dead:"), SentryAppSentryError("government secrets here")]
|
|
|
+ capture_exception.return_value = 1
|
|
|
+ response = self.get_success_response(
|
|
|
+ self.org.slug, qs_params={"projectId": self.project.id}
|
|
|
+ )
|
|
|
+ expected = [
|
|
|
+ {
|
|
|
+ "uuid": str(self.component1.uuid),
|
|
|
+ "type": self.component1.type,
|
|
|
+ "schema": self.component1.schema,
|
|
|
+ "error": {
|
|
|
+ "detail": f"Something went wrong while trying to link issue for component: {str(self.component1.uuid)}. Sentry error ID: {capture_exception.return_value}"
|
|
|
+ },
|
|
|
+ "sentryApp": {
|
|
|
+ "uuid": self.sentry_app1.uuid,
|
|
|
+ "slug": self.sentry_app1.slug,
|
|
|
+ "name": self.sentry_app1.name,
|
|
|
+ "avatars": get_sentry_app_avatars(self.sentry_app1),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "uuid": str(self.component2.uuid),
|
|
|
+ "type": self.component2.type,
|
|
|
+ "schema": self.component2.schema,
|
|
|
+ "error": {
|
|
|
+ "detail": f"Something went wrong while trying to link issue for component: {str(self.component2.uuid)}. Sentry error ID: {capture_exception.return_value}"
|
|
|
+ },
|
|
|
+ "sentryApp": {
|
|
|
+ "uuid": self.sentry_app2.uuid,
|
|
|
+ "slug": self.sentry_app2.slug,
|
|
|
+ "name": self.sentry_app2.name,
|
|
|
+ "avatars": get_sentry_app_avatars(self.sentry_app2),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ ]
|
|
|
+
|
|
|
+ assert response.data == expected
|
|
|
+
|
|
|
+ @patch("sentry_sdk.capture_exception")
|
|
|
@patch("sentry.sentry_apps.components.SentryAppComponentPreparer.run")
|
|
|
- def test_component_prep_errors_dont_bring_down_everything(self, run):
|
|
|
+ def test_component_prep_errors_dont_bring_down_everything(self, run, capture_exception):
|
|
|
run.side_effect = [APIError(), SentryAppSentryError(message="kewl")]
|
|
|
+ capture_exception.return_value = 1
|
|
|
|
|
|
response = self.get_success_response(
|
|
|
self.org.slug, qs_params={"projectId": self.project.id}
|
|
@@ -213,7 +380,9 @@ class OrganizationSentryAppComponentsTest(APITestCase):
|
|
|
"uuid": str(self.component1.uuid),
|
|
|
"type": self.component1.type,
|
|
|
"schema": self.component1.schema,
|
|
|
- "error": True,
|
|
|
+ "error": {
|
|
|
+ "detail": f"Something went wrong while trying to link issue for component: {self.component1.uuid}. Sentry error ID: {capture_exception.return_value}"
|
|
|
+ },
|
|
|
"sentryApp": {
|
|
|
"uuid": self.sentry_app1.uuid,
|
|
|
"slug": self.sentry_app1.slug,
|
|
@@ -225,7 +394,9 @@ class OrganizationSentryAppComponentsTest(APITestCase):
|
|
|
"uuid": str(self.component2.uuid),
|
|
|
"type": self.component2.type,
|
|
|
"schema": self.component2.schema,
|
|
|
- "error": True,
|
|
|
+ "error": {
|
|
|
+ "detail": f"Something went wrong while trying to link issue for component: {self.component2.uuid}. Sentry error ID: {capture_exception.return_value}"
|
|
|
+ },
|
|
|
"sentryApp": {
|
|
|
"uuid": self.sentry_app2.uuid,
|
|
|
"slug": self.sentry_app2.slug,
|
|
@@ -236,3 +407,20 @@ class OrganizationSentryAppComponentsTest(APITestCase):
|
|
|
]
|
|
|
|
|
|
assert response.data == expected
|
|
|
+
|
|
|
+ def _get_component_uris(
|
|
|
+ self, component_field: str, component: SentryAppComponent
|
|
|
+ ) -> Sequence[str]:
|
|
|
+ fields = dict(**component.app_schema).get(component_field)
|
|
|
+ assert fields, "component field was not found in the schema"
|
|
|
+ uris = []
|
|
|
+
|
|
|
+ for field in fields.get("required_fields", []):
|
|
|
+ if "uri" in field:
|
|
|
+ uris.append(field.get("uri"))
|
|
|
+
|
|
|
+ for field in fields.get("optional_fields", []):
|
|
|
+ if "uri" in field:
|
|
|
+ uris.append(field.get("uri"))
|
|
|
+
|
|
|
+ return uris
|