|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +from typing import Any |
1 | 4 | from unittest.mock import MagicMock, patch |
2 | 5 | from urllib.parse import parse_qs, urlencode, urlparse |
3 | 6 |
|
4 | 7 | import orjson |
5 | 8 | import pytest |
6 | 9 | import responses |
| 10 | +from django.urls import reverse |
7 | 11 | from responses.matchers import query_string_matcher |
8 | 12 | from slack_sdk.errors import SlackApiError |
9 | 13 | from slack_sdk.web import SlackResponse |
10 | 14 |
|
11 | 15 | from sentry import audit_log |
12 | 16 | from sentry.integrations.models.integration import Integration |
13 | 17 | from sentry.integrations.models.organization_integration import OrganizationIntegration |
| 18 | +from sentry.integrations.pipeline import IntegrationPipeline |
14 | 19 | from sentry.integrations.slack import SlackIntegration, SlackIntegrationProvider |
15 | 20 | from sentry.integrations.slack.utils.constants import SlackScope |
16 | 21 | from sentry.integrations.slack.utils.users import SLACK_GET_USERS_PAGE_SIZE |
@@ -653,3 +658,107 @@ def test_get_thread_history_error_returns_empty_list( |
653 | 658 | channel_id=self.channel_id, thread_ts=self.thread_ts |
654 | 659 | ) |
655 | 660 | assert result == [] |
| 661 | + |
| 662 | + |
| 663 | +@control_silo_test |
| 664 | +class SlackApiPipelineTest(APITestCase): |
| 665 | + endpoint = "sentry-api-0-organization-pipeline" |
| 666 | + method = "post" |
| 667 | + |
| 668 | + def setUp(self) -> None: |
| 669 | + super().setUp() |
| 670 | + self.login_as(self.user) |
| 671 | + |
| 672 | + def tearDown(self) -> None: |
| 673 | + responses.reset() |
| 674 | + super().tearDown() |
| 675 | + |
| 676 | + def _get_pipeline_url(self) -> str: |
| 677 | + return reverse( |
| 678 | + self.endpoint, |
| 679 | + args=[self.organization.slug, IntegrationPipeline.pipeline_name], |
| 680 | + ) |
| 681 | + |
| 682 | + def _initialize_pipeline(self) -> Any: |
| 683 | + return self.client.post( |
| 684 | + self._get_pipeline_url(), |
| 685 | + data={"action": "initialize", "provider": "slack"}, |
| 686 | + format="json", |
| 687 | + ) |
| 688 | + |
| 689 | + def _advance_step(self, data: dict[str, Any]) -> Any: |
| 690 | + return self.client.post(self._get_pipeline_url(), data=data, format="json") |
| 691 | + |
| 692 | + def _get_pipeline_signature(self, resp: Any) -> str: |
| 693 | + return resp.data["data"]["oauthUrl"].split("state=")[1].split("&")[0] |
| 694 | + |
| 695 | + @responses.activate |
| 696 | + def test_initialize_pipeline(self) -> None: |
| 697 | + resp = self._initialize_pipeline() |
| 698 | + assert resp.status_code == 200 |
| 699 | + assert resp.data["step"] == "oauth_login" |
| 700 | + assert resp.data["stepIndex"] == 0 |
| 701 | + assert resp.data["totalSteps"] == 1 |
| 702 | + assert resp.data["provider"] == "slack" |
| 703 | + oauth_url = resp.data["data"]["oauthUrl"] |
| 704 | + assert "slack.com/oauth/v2/authorize" in oauth_url |
| 705 | + assert "user_scope=" in oauth_url |
| 706 | + |
| 707 | + @responses.activate |
| 708 | + def test_oauth_step_missing_code(self) -> None: |
| 709 | + self._initialize_pipeline() |
| 710 | + resp = self._advance_step({}) |
| 711 | + assert resp.status_code == 400 |
| 712 | + |
| 713 | + @responses.activate |
| 714 | + @patch("sentry.integrations.slack.integration.WebClient") |
| 715 | + def test_full_pipeline_flow(self, mock_web_client_cls: MagicMock) -> None: |
| 716 | + mock_client = MagicMock() |
| 717 | + mock_web_client_cls.return_value = mock_client |
| 718 | + mock_client.team_info.return_value = SlackResponse( |
| 719 | + client=mock_client, |
| 720 | + http_verb="GET", |
| 721 | + api_url="https://slack.com/api/team.info", |
| 722 | + req_args={}, |
| 723 | + data={ |
| 724 | + "ok": True, |
| 725 | + "team": { |
| 726 | + "name": "Test Team", |
| 727 | + "id": "T1234", |
| 728 | + "domain": "test-team", |
| 729 | + "icon": {"image_132": "https://example.com/icon.png"}, |
| 730 | + }, |
| 731 | + }, |
| 732 | + headers={}, |
| 733 | + status_code=200, |
| 734 | + ) |
| 735 | + |
| 736 | + responses.add( |
| 737 | + responses.POST, |
| 738 | + "https://slack.com/api/oauth.v2.access", |
| 739 | + json={ |
| 740 | + "ok": True, |
| 741 | + "access_token": "xoxb-test-token", |
| 742 | + "scope": "channels:read,chat:write", |
| 743 | + "team": {"name": "Test Team", "id": "T1234"}, |
| 744 | + "authed_user": {"id": "U1234"}, |
| 745 | + }, |
| 746 | + ) |
| 747 | + |
| 748 | + resp = self._initialize_pipeline() |
| 749 | + assert resp.data["step"] == "oauth_login" |
| 750 | + pipeline_signature = self._get_pipeline_signature(resp) |
| 751 | + |
| 752 | + resp = self._advance_step({"code": "slack-auth-code", "state": pipeline_signature}) |
| 753 | + assert resp.status_code == 200 |
| 754 | + assert resp.data["status"] == "complete" |
| 755 | + assert "data" in resp.data |
| 756 | + |
| 757 | + integration = Integration.objects.get(provider="slack") |
| 758 | + assert integration.external_id == "T1234" |
| 759 | + assert integration.name == "Test Team" |
| 760 | + |
| 761 | + assert OrganizationIntegration.objects.filter( |
| 762 | + organization_id=self.organization.id, |
| 763 | + integration=integration, |
| 764 | + ).exists() |
0 commit comments