Skip to content

Commit 41f9e5a

Browse files
test(integrations): Mock Slack, Discord, Jira, and Sentry App calls in tests (#127946)
The outbound network guard in report mode (#127944) found 14 integration tests that make real requests to `slack.com`, `discord.com`, `example.atlassian.net`, or `example.com`. Some of them only pass because of what the real service sends back. For example, `test_post_message_failure_sdk` asserts the `ok=False, status=200` metric that Slack's real `invalid_auth` reply produces. Each test now uses a mock pattern that already exists in the same file: - **Slack SDK** (it sends with urllib, so `responses` never matches): - `tasks/test_tasks.py::test_post_message_failure_sdk` and `service/test_slack_service.py::test_send_notification_to_slack_channel_error` patch `_perform_urllib_http_request` to return the `invalid_auth` reply the tests depended on, like their success-path neighbors do. - `test_notify_action.py::test_invalid_channel_selected` had `responses` mocks that could never match. It now uses the file's own `mock_msg_schedule_response("channel_not_found")` and `mock_list("users", ...)` helpers, which return the same data. - `utils/test_channel.py::test_invalid_channel_selected_sdk_client` also patches `users_list` with an empty member list. - `webhooks/events/test_app_mention.py`: an autouse `set_thread_status` patch, the same one `test_direct_message.py` uses. `test_app_mention_only_mentions_halts` also patches `send_threaded_ephemeral_message`, like its sibling halt tests. - **Discord** `test_issue_alert.py::test_no_channel_id`: `responses` returns a 400 for `GET /channels/`, and the test now asserts that the mocked call happened. - **Jira** `test_integration.py::JiraApiPipelineTest::test_install`: when the install response serializes the org config, it fetches the project list. `responses` now returns a 401 for that request, which leads to the `ApiError` handling the real request hit. - **Sentry Apps** `test_disabled_sentry_app.py::test_delete_installation_of_disabled_app_allowed`: the uninstall webhook is mocked the way `test_organization_sentry_app_installation_details.py::test_delete_install` does it. No assertions were removed or weakened. This PR only changes tests and doesn't depend on #127944. I ran every touched file locally with `SENTRY_NETWORK_GUARD=strict`: they pass, and the guard records no outbound attempts. <!-- junior-request-attribution:start --> via @joshuarli. <!-- junior-request-attribution:end --> <!-- junior-session-footer:start --> <!-- junior-conversation-id:agent%3A35c0cbe32045897a494787cce713d692 --> -- [View Junior Session](https://junior-prod.sentry.dev/conversations/agent%3A35c0cbe32045897a494787cce713d692) [[Sentry]](https://sentry.sentry.io/explore/conversations/agent%3A35c0cbe32045897a494787cce713d692/?project=4510944073809921) <!-- junior-session-footer:end --> Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> Co-authored-by: Joshua Li <14209781+joshuarli@users.noreply.github.com>
1 parent ee03089 commit 41f9e5a

8 files changed

Lines changed: 50 additions & 26 deletions

File tree

‎tests/sentry/integrations/discord/test_issue_alert.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from sentry.integrations.discord.actions.issue_alert.form import DiscordNotifyServiceForm
1010
from sentry.integrations.discord.actions.issue_alert.notification import DiscordNotifyServiceAction
1111
from sentry.integrations.discord.analytics import DiscordIntegrationNotificationSent
12-
from sentry.integrations.discord.client import DISCORD_BASE_URL, MESSAGE_URL
12+
from sentry.integrations.discord.client import CHANNEL_URL, DISCORD_BASE_URL, MESSAGE_URL
1313
from sentry.integrations.discord.message_builder import LEVEL_TO_COLOR
1414
from sentry.integrations.discord.message_builder.base.component import DiscordComponentCustomIds
1515
from sentry.integrations.messaging.message_builder import (
@@ -328,14 +328,21 @@ def test_valid(self, mock_validate_channel_id: mock.MagicMock) -> None:
328328
assert self.form.is_valid()
329329
assert mock_validate_channel_id.call_count == 1
330330

331+
@responses.activate
331332
@mock.patch(
332333
"sentry.integrations.discord.actions.issue_alert.form.get_channel_id_from_url",
333334
return_value="",
334335
)
335336
def test_no_channel_id(self, mock_get_channel_id_from_url: mock.MagicMock) -> None:
337+
responses.add(
338+
responses.GET,
339+
f"{DISCORD_BASE_URL}{CHANNEL_URL.format(channel_id='')}",
340+
status=400,
341+
)
336342
self.form.full_clean()
337343
assert not self.form.is_valid()
338344
assert mock_get_channel_id_from_url.call_count == 1
345+
assert len(responses.calls) == 1
339346

340347
def test_no_server(self) -> None:
341348
form = DiscordNotifyServiceForm(integrations=self.integrations)

‎tests/sentry/integrations/jira/test_integration.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2456,7 +2456,10 @@ def test_advance_with_invalid_state_errors(self) -> None:
24562456
assert resp.status_code == 400
24572457
assert not Integration.objects.filter(provider=self.provider.key).exists()
24582458

2459+
@responses.activate
24592460
def test_install(self) -> None:
2461+
# The install response serializes the org config, which fetches Jira projects.
2462+
responses.add(responses.GET, "https://example.atlassian.net/rest/api/2/project", status=401)
24602463
resp = self._initialize_pipeline(initial_data={"signedParams": self._signed_params()})
24612464
pipeline_signature = resp.data["data"]["state"]
24622465

‎tests/sentry/integrations/slack/service/test_slack_service.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -538,7 +538,13 @@ def test_send_notification_to_slack_channel(self, mock_api_call: mock.MagicMock)
538538
client=SlackSdkClient(integration_id=self.integration.id),
539539
)
540540

541-
def test_send_notification_to_slack_channel_error(self) -> None:
541+
@mock.patch("slack_sdk.web.client.WebClient._perform_urllib_http_request")
542+
def test_send_notification_to_slack_channel_error(self, mock_api_call: mock.MagicMock) -> None:
543+
mock_api_call.return_value = {
544+
"body": orjson.dumps({"ok": False, "error": "invalid_auth"}).decode(),
545+
"headers": {},
546+
"status": 200,
547+
}
542548
with pytest.raises(SlackApiError) as err:
543549
self.service._send_notification_to_slack_channel(
544550
channel_id=self.channel_id,

‎tests/sentry/integrations/slack/tasks/test_tasks.py‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,17 @@ def test_post_message_success(self, mock_api_call: MagicMock, mock_metrics: Magi
6060
)
6161

6262
@patch("sentry.integrations.slack.sdk_client.metrics")
63+
@patch("slack_sdk.web.client.WebClient._perform_urllib_http_request")
6364
@responses.activate
64-
def test_post_message_failure_sdk(self, mock_metrics: MagicMock) -> None:
65+
def test_post_message_failure_sdk(
66+
self, mock_api_call: MagicMock, mock_metrics: MagicMock
67+
) -> None:
68+
mock_api_call.return_value = {
69+
"body": orjson.dumps({"ok": False, "error": "invalid_auth"}).decode(),
70+
"headers": {},
71+
"status": 200,
72+
}
73+
6574
with self.tasks():
6675
post_message.apply_async(
6776
kwargs={

‎tests/sentry/integrations/slack/test_notify_action.py‎

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -243,34 +243,19 @@ def test_valid_member_selected(self) -> None:
243243
assert form.is_valid()
244244
self.assert_form_valid(form, "morty-id", "@morty")
245245

246-
@responses.activate
247246
def test_invalid_channel_selected(self) -> None:
248247
rule = self.get_rule(
249248
data={"workspace": self.integration.id, "channel": "#my-channel", "tags": ""}
250249
)
251250

252-
responses.add(
253-
method=responses.POST,
254-
url="https://slack.com/api/chat.scheduleMessage",
255-
status=200,
256-
content_type="application/json",
257-
body=orjson.dumps({"ok": False, "error": "channel_not_found"}),
258-
)
259-
260-
members = {"ok": "true", "members": [{"name": "other-member", "id": "member-id"}]}
261-
262-
responses.add(
263-
method=responses.GET,
264-
url="https://slack.com/api/users.list",
265-
status=200,
266-
content_type="application/json",
267-
body=orjson.dumps(members),
268-
)
251+
members = [{"name": "other-member", "id": "member-id"}]
269252

270-
form = SlackNotifyServiceForm(rule.data, integrations=rule.get_integrations())
253+
with self.mock_msg_schedule_response("channel_not_found"):
254+
with self.mock_list("users", members, "members"):
255+
form = SlackNotifyServiceForm(rule.data, integrations=rule.get_integrations())
271256

272-
assert not form.is_valid()
273-
assert len(form.errors) == 1
257+
assert not form.is_valid()
258+
assert len(form.errors) == 1
274259

275260
@responses.activate
276261
@patch("slack_sdk.web.client.WebClient.users_list")

‎tests/sentry/integrations/slack/utils/test_channel.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ def test_invalid_member_selected_display_name_sdk_client(self) -> None:
170170
get_channel_id(self.integration, "@Morty")
171171

172172
def test_invalid_channel_selected_sdk_client(self) -> None:
173-
with self.patch_msg_response_not_found():
173+
with self.patch_msg_response_not_found(), self.patch_mock_list("users", [], "members"):
174174
assert get_channel_id(self.integration, "#fake-channel").channel_id is None
175175
assert get_channel_id(self.integration, "@fake-user").channel_id is None
176176

‎tests/sentry/integrations/slack/webhooks/events/test_app_mention.py‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from unittest.mock import patch
22

3+
import pytest
4+
35
from sentry.integrations.messaging.metrics import SeerSlackHaltReason
46
from sentry.testutils.asserts import assert_halt_metric
57

@@ -23,6 +25,13 @@ def setUp(self):
2325
# Tests default to having the mention come from a linked user.
2426
self.link_identity(user=self.user, slack_user_id=APP_MENTION_EVENT["user"])
2527

28+
@pytest.fixture(autouse=True)
29+
def mock_set_thread_status(self):
30+
with patch(
31+
"sentry.integrations.slack.integration.SlackIntegration.set_thread_status",
32+
) as self.mock_status:
33+
yield
34+
2635
@patch("sentry.seer.entrypoints.slack.tasks.process_mention_for_slack.apply_async")
2736
def test_app_mention_dispatches_task(self, mock_apply_async):
2837
with self.feature(SEER_EXPLORER_FEATURES):
@@ -137,9 +146,10 @@ def test_app_mention_only_the_bot_mention_halts_without_authorizations(
137146
assert_halt_metric(mock_record, SeerSlackHaltReason.EMPTY_PROMPT)
138147
mock_send.assert_called_once()
139148

149+
@patch("sentry.integrations.slack.workspace.send_threaded_ephemeral_message")
140150
@patch("sentry.integrations.utils.metrics.EventLifecycle.record_event")
141151
@patch("sentry.seer.entrypoints.slack.tasks.process_mention_for_slack.apply_async")
142-
def test_app_mention_only_mentions_halts(self, mock_apply_async, mock_record):
152+
def test_app_mention_only_mentions_halts(self, mock_apply_async, mock_record, mock_send):
143153
"""Mentioning us alongside other users, with no question, is still empty."""
144154
with self.feature(SEER_EXPLORER_FEATURES):
145155
resp = self.post_webhook(

‎tests/sentry/sentry_apps/api/bases/test_disabled_sentry_app.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import responses
2+
13
from sentry.sentry_apps.models.sentry_app import SentryApp
24
from sentry.silo.base import SiloMode
35
from sentry.testutils.cases import APITestCase
@@ -61,8 +63,10 @@ def test_get_installation_of_disabled_app_returns_403(self) -> None:
6163
disable_app(self.app)
6264
self.get_error_response(self.install.uuid, status_code=403)
6365

66+
@responses.activate
6467
@override_options(OPTION)
6568
def test_delete_installation_of_disabled_app_allowed(self) -> None:
69+
responses.add(url="https://example.com/webhook", method=responses.POST, body=b"")
6670
disable_app(self.app)
6771
self.get_success_response(self.install.uuid, method="delete", status_code=204)
6872

0 commit comments

Comments
 (0)