This is an automated email from the ASF dual-hosted git repository.

vavila pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new d2e0e2b79c fix(Slack): Fix Slack recipients migration to V2 (#32336)
d2e0e2b79c is described below

commit d2e0e2b79caa25562d139032655141732b08a24c
Author: Vitor Avila <[email protected]>
AuthorDate: Thu Mar 6 08:52:15 2025 -0300

    fix(Slack): Fix Slack recipients migration to V2 (#32336)
---
 superset/commands/report/execute.py               |  78 +++++++-----
 superset/reports/notifications/slack.py           |   4 +-
 superset/reports/notifications/slackv2.py         |   4 +-
 superset/reports/schemas.py                       |  16 ++-
 superset/utils/core.py                            |  14 ++-
 superset/utils/slack.py                           |  25 ++--
 tests/integration_tests/reports/commands_tests.py | 140 +++++++++++++++++++---
 tests/integration_tests/reports/utils.py          |   6 +-
 tests/integration_tests/utils_tests.py            |  14 +--
 tests/unit_tests/commands/report/execute_test.py  |  77 ++++++++++++
 tests/unit_tests/utils/slack_test.py              |  16 ++-
 11 files changed, 317 insertions(+), 77 deletions(-)

diff --git a/superset/commands/report/execute.py 
b/superset/commands/report/execute.py
index 0c57d55bb5..2e94b8c4b8 100644
--- a/superset/commands/report/execute.py
+++ b/superset/commands/report/execute.py
@@ -72,7 +72,7 @@ from superset.reports.notifications.exceptions import (
 )
 from superset.tasks.utils import get_executor
 from superset.utils import json
-from superset.utils.core import HeaderDataType, override_user
+from superset.utils.core import HeaderDataType, override_user, 
recipients_string_to_list
 from superset.utils.csv import get_chart_csv_data, get_chart_dataframe
 from superset.utils.decorators import logs_context, transaction
 from superset.utils.pdf import build_pdf_from_screenshots
@@ -137,24 +137,40 @@ class BaseReportState:
                 if recipient.type == ReportRecipientType.SLACK:
                     recipient.type = ReportRecipientType.SLACKV2
                     slack_recipients = 
json.loads(recipient.recipient_config_json)
+                    # V1 method allowed to use leading `#` in the channel name
+                    channel_names = (slack_recipients["target"] or 
"").replace("#", "")
                     # we need to ensure that existing reports can also fetch
                     # ids from private channels
+                    channels = get_channels_with_search(
+                        search_string=channel_names,
+                        types=[
+                            SlackChannelTypes.PRIVATE,
+                            SlackChannelTypes.PUBLIC,
+                        ],
+                        exact_match=True,
+                    )
+                    channels_list = recipients_string_to_list(channel_names)
+                    if len(channels_list) != len(channels):
+                        missing_channels = set(channels_list) - {
+                            channel["name"] for channel in channels
+                        }
+                        msg = (
+                            "Could not find the following channels: "
+                            f"{', '.join(missing_channels)}"
+                        )
+                        raise UpdateFailedError(msg)
+                    channel_ids = ",".join(channel["id"] for channel in 
channels)
                     recipient.recipient_config_json = json.dumps(
                         {
-                            "target": get_channels_with_search(
-                                slack_recipients["target"],
-                                types=[
-                                    SlackChannelTypes.PRIVATE,
-                                    SlackChannelTypes.PUBLIC,
-                                ],
-                            )
+                            "target": channel_ids,
                         }
                     )
         except Exception as ex:
-            logger.warning(
-                "Failed to update slack recipients to v2: %s", str(ex), 
exc_info=True
-            )
-            raise UpdateFailedError from ex
+            # Revert to v1 to preserve configuration (requires manual fix)
+            recipient.type = ReportRecipientType.SLACK
+            msg = f"Failed to update slack recipients to v2: {str(ex)}"
+            logger.exception(msg)
+            raise UpdateFailedError(msg) from ex
 
     def create_log(self, error_message: Optional[str] = None) -> None:
         """
@@ -553,30 +569,32 @@ class BaseReportState:
         for recipient in recipients:
             notification = create_notification(recipient, notification_content)
             try:
-                if app.config["ALERT_REPORTS_NOTIFICATION_DRY_RUN"]:
+                try:
+                    if app.config["ALERT_REPORTS_NOTIFICATION_DRY_RUN"]:
+                        logger.info(
+                            "Would send notification for alert %s, to %s. "
+                            "ALERT_REPORTS_NOTIFICATION_DRY_RUN is enabled, "
+                            "set it to False to send notifications.",
+                            self._report_schedule.name,
+                            recipient.recipient_config_json,
+                        )
+                    else:
+                        notification.send()
+                except SlackV1NotificationError as ex:
+                    # The slack notification should be sent with the v2 api
                     logger.info(
-                        "Would send notification for alert %s, to %s. "
-                        "ALERT_REPORTS_NOTIFICATION_DRY_RUN is enabled, "
-                        "set it to False to send notifications.",
-                        self._report_schedule.name,
-                        recipient.recipient_config_json,
+                        "Attempting to upgrade the report to Slackv2: %s", 
str(ex)
                     )
-                else:
-                    notification.send()
-            except SlackV1NotificationError as ex:
-                # The slack notification should be sent with the v2 api
-                logger.info("Attempting to upgrade the report to Slackv2: %s", 
str(ex))
-                try:
                     self.update_report_schedule_slack_v2()
                     recipient.type = ReportRecipientType.SLACKV2
                     notification = create_notification(recipient, 
notification_content)
                     notification.send()
-                except (UpdateFailedError, NotificationParamException) as err:
-                    # log the error but keep processing the report with SlackV1
-                    logger.warning(
-                        "Failed to update slack recipients to v2: %s", str(err)
-                    )
-            except (NotificationError, SupersetException) as ex:
+            except (
+                UpdateFailedError,
+                NotificationParamException,
+                NotificationError,
+                SupersetException,
+            ) as ex:
                 # collect errors but keep processing them
                 notification_errors.append(
                     SupersetError(
diff --git a/superset/reports/notifications/slack.py 
b/superset/reports/notifications/slack.py
index 4622a45606..589fddb9aa 100644
--- a/superset/reports/notifications/slack.py
+++ b/superset/reports/notifications/slack.py
@@ -43,7 +43,7 @@ from superset.reports.notifications.exceptions import (
 )
 from superset.reports.notifications.slack_mixin import SlackMixin
 from superset.utils import json
-from superset.utils.core import get_email_address_list
+from superset.utils.core import recipients_string_to_list
 from superset.utils.decorators import statsd_gauge
 from superset.utils.slack import (
     get_slack_client,
@@ -70,7 +70,7 @@ class SlackNotification(SlackMixin, BaseNotification):  # 
pylint: disable=too-fe
         """
         recipient_str = 
json.loads(self._recipient.recipient_config_json)["target"]
 
-        return ",".join(get_email_address_list(recipient_str))
+        return ",".join(recipients_string_to_list(recipient_str))
 
     def _get_inline_files(
         self,
diff --git a/superset/reports/notifications/slackv2.py 
b/superset/reports/notifications/slackv2.py
index 824d4bd326..19b5d98e00 100644
--- a/superset/reports/notifications/slackv2.py
+++ b/superset/reports/notifications/slackv2.py
@@ -42,7 +42,7 @@ from superset.reports.notifications.exceptions import (
 )
 from superset.reports.notifications.slack_mixin import SlackMixin
 from superset.utils import json
-from superset.utils.core import get_email_address_list
+from superset.utils.core import recipients_string_to_list
 from superset.utils.decorators import statsd_gauge
 from superset.utils.slack import get_slack_client
 
@@ -64,7 +64,7 @@ class SlackV2Notification(SlackMixin, BaseNotification):  # 
pylint: disable=too-
         """  # noqa: E501
         recipient_str = 
json.loads(self._recipient.recipient_config_json)["target"]
 
-        return get_email_address_list(recipient_str)
+        return recipients_string_to_list(recipient_str)
 
     def _get_inline_files(
         self,
diff --git a/superset/reports/schemas.py b/superset/reports/schemas.py
index 7078970b38..cfccc579bc 100644
--- a/superset/reports/schemas.py
+++ b/superset/reports/schemas.py
@@ -19,7 +19,7 @@ from typing import Any, Optional, Union
 from croniter import croniter
 from flask import current_app
 from flask_babel import gettext as _
-from marshmallow import fields, Schema, validate, validates, validates_schema
+from marshmallow import EXCLUDE, fields, Schema, validate, validates, 
validates_schema
 from marshmallow.validate import Length, Range, ValidationError
 from pytz import all_timezones
 
@@ -399,3 +399,17 @@ class ReportSchedulePutSchema(Schema):
                     max=max_width,
                 )
             )
+
+
+class SlackChannelSchema(Schema):
+    """
+    Schema to load Slack channels, set to ignore any fields not used by 
Superset.
+    """
+
+    class Meta:
+        unknown = EXCLUDE
+
+    id = fields.String()
+    name = fields.String()
+    is_member = fields.Boolean()
+    is_private = fields.Boolean()
diff --git a/superset/utils/core.py b/superset/utils/core.py
index 2aaf912229..9e1bee5ecb 100644
--- a/superset/utils/core.py
+++ b/superset/utils/core.py
@@ -702,7 +702,7 @@ def send_email_smtp(  # pylint: 
disable=invalid-name,too-many-arguments,too-many
         '[email protected]', 'foo', '<b>Foo</b> bar',['/dev/null'], dryrun=True)
     """
     smtp_mail_from = config["SMTP_MAIL_FROM"]
-    smtp_mail_to = get_email_address_list(to)
+    smtp_mail_to = recipients_string_to_list(to)
 
     msg = MIMEMultipart(mime_subtype)
     msg["Subject"] = subject
@@ -713,14 +713,14 @@ def send_email_smtp(  # pylint: 
disable=invalid-name,too-many-arguments,too-many
 
     recipients = smtp_mail_to
     if cc:
-        smtp_mail_cc = get_email_address_list(cc)
+        smtp_mail_cc = recipients_string_to_list(cc)
         msg["Cc"] = ", ".join(smtp_mail_cc)
         recipients = recipients + smtp_mail_cc
 
     smtp_mail_bcc = []
     if bcc:
         # don't add bcc in header
-        smtp_mail_bcc = get_email_address_list(bcc)
+        smtp_mail_bcc = recipients_string_to_list(bcc)
         recipients = recipients + smtp_mail_bcc
 
     msg["Date"] = formatdate(localtime=True)
@@ -813,7 +813,13 @@ def send_mime_email(
     smtp.quit()
 
 
-def get_email_address_list(address_string: str) -> list[str]:
+def recipients_string_to_list(address_string: str | None) -> list[str]:
+    """
+    Returns the list of target recipients for alerts and reports.
+
+    Strips values and converts a comma/semicolon separated
+    string into a list.
+    """
     address_string_list: list[str] = []
     if isinstance(address_string, str):
         address_string_list = re.split(r",|\s|;", address_string)
diff --git a/superset/utils/slack.py b/superset/utils/slack.py
index b4c8018ffc..6d51f2765e 100644
--- a/superset/utils/slack.py
+++ b/superset/utils/slack.py
@@ -26,7 +26,9 @@ from slack_sdk.http_retry.builtin_handlers import 
RateLimitErrorRetryHandler
 
 from superset import feature_flag_manager
 from superset.exceptions import SupersetException
+from superset.reports.schemas import SlackChannelSchema
 from superset.utils.backports import StrEnum
+from superset.utils.core import recipients_string_to_list
 
 logger = logging.getLogger(__name__)
 
@@ -57,7 +59,7 @@ def get_channels_with_search(
     limit: int = 999,
     types: Optional[list[SlackChannelTypes]] = None,
     exact_match: bool = False,
-) -> list[str]:
+) -> list[SlackChannelSchema]:
     """
     The slack api is paginated but does not include search, so we need to fetch
     all channels and filter them ourselves
@@ -66,7 +68,8 @@ def get_channels_with_search(
 
     try:
         client = get_slack_client()
-        channels = []
+        channel_schema = SlackChannelSchema()
+        channels: list[SlackChannelSchema] = []
         cursor = None
         extra_params = {}
         extra_params["types"] = ",".join(types) if types else None
@@ -75,29 +78,27 @@ def get_channels_with_search(
             response = client.conversations_list(
                 limit=limit, cursor=cursor, exclude_archived=True, 
**extra_params
             )
-            channels.extend(response.data["channels"])
+            channels.extend(
+                channel_schema.load(channel) for channel in 
response.data["channels"]
+            )
             cursor = response.data.get("response_metadata", 
{}).get("next_cursor")
             if not cursor:
                 break
 
         # The search string can be multiple channels separated by commas
         if search_string:
-            search_array = [
-                search.lower()
-                for search in (search_string.split(",") if search_string else 
[])
-            ]
-
+            search_array = recipients_string_to_list(search_string)
             channels = [
                 channel
                 for channel in channels
                 if any(
                     (
-                        search == channel["name"].lower()
-                        or search == channel["id"].lower()
+                        search.lower() == channel["name"].lower()
+                        or search.lower() == channel["id"].lower()
                         if exact_match
                         else (
-                            search in channel["name"].lower()
-                            or search in channel["id"].lower()
+                            search.lower() in channel["name"].lower()
+                            or search.lower() in channel["id"].lower()
                         )
                     )
                     for search in search_array
diff --git a/tests/integration_tests/reports/commands_tests.py 
b/tests/integration_tests/reports/commands_tests.py
index 7b22b38fc0..d3798d05ec 100644
--- a/tests/integration_tests/reports/commands_tests.py
+++ b/tests/integration_tests/reports/commands_tests.py
@@ -308,7 +308,10 @@ def create_report_slack_chart():
 def create_report_slack_chartv2():
     chart = db.session.query(Slice).first()
     report_schedule = create_report_notification(
-        slack_channel="slack_channel_id", chart=chart, 
name="report_slack_chartv2"
+        slack_channel="slack_channel_id",
+        chart=chart,
+        name="report_slack_chartv2",
+        use_slack_v2=True,
     )
     yield report_schedule
 
@@ -1300,9 +1303,7 @@ def test_email_dashboard_report_schedule_force_screenshot(
         assert_log(ReportState.SUCCESS)
 
 
[email protected](
-    "load_birth_names_dashboard_with_slices", "create_report_slack_chart"
-)
[email protected]("create_report_slack_chart")
 @patch("superset.commands.report.execute.get_channels_with_search")
 @patch("superset.reports.notifications.slack.should_use_v2_api", 
return_value=True)
 @patch("superset.reports.notifications.slackv2.get_slack_client")
@@ -1316,13 +1317,19 @@ def test_slack_chart_report_schedule_converts_to_v2(
 ):
     """
     ExecuteReport Command: Test chart slack report schedule
+    while converting the recipients list to SlackV2.
     """
     # setup screenshot mock
     screenshot_mock.return_value = SCREENSHOT_FILE
-
     channel_id = "slack_channel_id"
-
-    get_channels_with_search_mock.return_value = channel_id
+    get_channels_with_search_mock.return_value = [
+        {
+            "id": channel_id,
+            "name": "slack_channel",
+            "is_member": True,
+            "is_private": False,
+        },
+    ]
 
     with freeze_time("2020-01-01T00:00:00Z"):
         with patch.object(current_app.config["STATS_LOGGER"], "gauge") as 
statsd_mock:
@@ -1357,33 +1364,40 @@ def test_slack_chart_report_schedule_converts_to_v2(
             assert statsd_mock.call_args_list[1] == 
call("reports.slack.send.ok", 1)
 
 
[email protected](
-    "load_birth_names_dashboard_with_slices", "create_report_slack_chartv2"
-)
 @patch("superset.commands.report.execute.get_channels_with_search")
 @patch("superset.reports.notifications.slack.should_use_v2_api", 
return_value=True)
 @patch("superset.reports.notifications.slackv2.get_slack_client")
 @patch("superset.utils.screenshots.ChartScreenshot.get_screenshot")
-def test_slack_chart_report_schedule_v2(
+def test_slack_chart_report_schedule_converts_to_v2_channel_with_hash(
     screenshot_mock,
     slack_client_mock,
     slack_should_use_v2_api_mock,
     get_channels_with_search_mock,
-    create_report_slack_chart,
 ):
     """
-    ExecuteReport Command: Test chart slack report schedule
+    ExecuteReport Command: Test converting a Slack report to v2 when
+    the channel name includes the leading hash (supported in v1).
     """
     # setup screenshot mock
     screenshot_mock.return_value = SCREENSHOT_FILE
     channel_id = "slack_channel_id"
-
-    get_channels_with_search_mock.return_value = channel_id
+    chart = db.session.query(Slice).first()
+    report_schedule = create_report_notification(
+        slack_channel="#slack_channel", chart=chart
+    )
+    get_channels_with_search_mock.return_value = [
+        {
+            "id": channel_id,
+            "name": "slack_channel",
+            "is_member": True,
+            "is_private": False,
+        },
+    ]
 
     with freeze_time("2020-01-01T00:00:00Z"):
         with patch.object(current_app.config["STATS_LOGGER"], "gauge") as 
statsd_mock:
             AsyncExecuteReportScheduleCommand(
-                TEST_ID, create_report_slack_chart.id, datetime.utcnow()
+                TEST_ID, report_schedule.id, datetime.utcnow()
             ).run()
 
             assert (
@@ -1395,6 +1409,12 @@ def test_slack_chart_report_schedule_v2(
                 == SCREENSHOT_FILE
             )
 
+            # Assert that the report recipients were updated
+            assert report_schedule.recipients[0].recipient_config_json == 
json.dumps(
+                {"target": channel_id}
+            )
+            assert report_schedule.recipients[0].type == 
ReportRecipientType.SLACKV2
+
             # Assert logs are correct
             assert_log(ReportState.SUCCESS)
             # this will send a warning
@@ -1403,6 +1423,94 @@ def test_slack_chart_report_schedule_v2(
             )
             assert statsd_mock.call_args_list[1] == 
call("reports.slack.send.ok", 1)
 
+    cleanup_report_schedule(report_schedule)
+
+
+@patch("superset.commands.report.execute.get_channels_with_search")
+@patch("superset.reports.notifications.slack.should_use_v2_api", 
return_value=True)
+@patch("superset.reports.notifications.slackv2.get_slack_client")
+@patch("superset.utils.screenshots.ChartScreenshot.get_screenshot")
+def test_slack_chart_report_schedule_fails_to_converts_to_v2(
+    screenshot_mock,
+    slack_client_mock,
+    slack_should_use_v2_api_mock,
+    get_channels_with_search_mock,
+):
+    """
+    ExecuteReport Command: Test converting a Slack report to v2 fails.
+    """
+    # setup screenshot mock
+    screenshot_mock.return_value = SCREENSHOT_FILE
+    channel_id = "slack_channel_id"
+    chart = db.session.query(Slice).first()
+    report_schedule = create_report_notification(
+        slack_channel="#slack_channel,my_member_ID", chart=chart
+    )
+    get_channels_with_search_mock.return_value = [
+        {
+            "id": channel_id,
+            "name": "slack_channel",
+            "is_member": True,
+            "is_private": False,
+        },
+    ]
+
+    with pytest.raises(ReportScheduleSystemErrorsException):
+        AsyncExecuteReportScheduleCommand(
+            TEST_ID, report_schedule.id, datetime.utcnow()
+        ).run()
+
+    # Assert failuer with proper log
+    expected_message = (
+        "Failed to update slack recipients to v2: "
+        "Could not find the following channels: my_member_ID"
+    )
+    assert_log(ReportState.ERROR, error_message=expected_message)
+
+    # Assert that previous configuration was kept for manual correction
+    assert report_schedule.recipients[0].recipient_config_json == json.dumps(
+        {"target": "#slack_channel,my_member_ID"}
+    )
+    assert report_schedule.recipients[0].type == ReportRecipientType.SLACK
+
+    cleanup_report_schedule(report_schedule)
+
+
[email protected]("create_report_slack_chartv2")
+@patch("superset.reports.notifications.slack.should_use_v2_api", 
return_value=True)
+@patch("superset.reports.notifications.slackv2.get_slack_client")
+@patch("superset.utils.screenshots.ChartScreenshot.get_screenshot")
+def test_slack_chart_report_schedule_v2(
+    screenshot_mock,
+    slack_client_mock,
+    slack_should_use_v2_api_mock,
+    create_report_slack_chartv2,
+):
+    """
+    ExecuteReport Command: Test chart slack report schedule using Slack v2.
+    """
+    # setup screenshot mock
+    screenshot_mock.return_value = SCREENSHOT_FILE
+
+    with freeze_time("2020-01-01T00:00:00Z"):
+        with patch.object(current_app.config["STATS_LOGGER"], "gauge") as 
statsd_mock:
+            AsyncExecuteReportScheduleCommand(
+                TEST_ID, create_report_slack_chartv2.id, datetime.utcnow()
+            ).run()
+
+            assert (
+                
slack_client_mock.return_value.files_upload_v2.call_args[1]["channel"]
+                == "slack_channel_id"
+            )
+            assert (
+                
slack_client_mock.return_value.files_upload_v2.call_args[1]["file"]
+                == SCREENSHOT_FILE
+            )
+
+            # Assert logs are correct
+            assert_log(ReportState.SUCCESS)
+            assert statsd_mock.call_args_list[0] == 
call("reports.slack.send.ok", 1)
+
 
 @pytest.mark.usefixtures(
     "load_birth_names_dashboard_with_slices", "create_report_slack_chart"
diff --git a/tests/integration_tests/reports/utils.py 
b/tests/integration_tests/reports/utils.py
index 3ab5c46c4e..e11ecc653b 100644
--- a/tests/integration_tests/reports/utils.py
+++ b/tests/integration_tests/reports/utils.py
@@ -119,6 +119,7 @@ def create_report_notification(
     owners: Optional[list[User]] = None,
     ccTarget: Optional[str] = None,  # noqa: N803
     bccTarget: Optional[str] = None,  # noqa: N803
+    use_slack_v2: bool = False,
 ) -> ReportSchedule:
     if not owners:
         owners = [
@@ -130,8 +131,11 @@ def create_report_notification(
         ]
 
     if slack_channel:
+        type = (
+            ReportRecipientType.SLACKV2 if use_slack_v2 else 
ReportRecipientType.SLACK
+        )
         recipient = ReportRecipients(
-            type=ReportRecipientType.SLACK,
+            type=type,
             recipient_config_json=json.dumps(
                 {
                     "target": slack_channel,
diff --git a/tests/integration_tests/utils_tests.py 
b/tests/integration_tests/utils_tests.py
index ca49d65fc0..8a09a23f9f 100644
--- a/tests/integration_tests/utils_tests.py
+++ b/tests/integration_tests/utils_tests.py
@@ -52,7 +52,7 @@ from superset.utils.core import (
     GenericDataType,
     get_form_data_token,
     as_list,
-    get_email_address_list,
+    recipients_string_to_list,
     get_stacktrace,
     merge_extra_filters,
     merge_extra_form_data,
@@ -809,12 +809,12 @@ class TestUtils(SupersetTestCase):
         assert expected_filename in path
         assert os.path.exists(path)
 
-    def test_get_email_address_list(self):
-        assert get_email_address_list("a@a") == ["a@a"]
-        assert get_email_address_list(" a@a ") == ["a@a"]
-        assert get_email_address_list("a@a\n") == ["a@a"]
-        assert get_email_address_list(",a@a;") == ["a@a"]
-        assert get_email_address_list(",a@a; b@b c@c a-c@c; d@d, f@f") == [
+    def test_recipients_string_to_list(self):
+        assert recipients_string_to_list("a@a") == ["a@a"]
+        assert recipients_string_to_list(" a@a ") == ["a@a"]
+        assert recipients_string_to_list("a@a\n") == ["a@a"]
+        assert recipients_string_to_list(",a@a;") == ["a@a"]
+        assert recipients_string_to_list(",a@a; b@b c@c a-c@c; d@d, f@f") == [
             "a@a",
             "b@b",
             "c@c",
diff --git a/tests/unit_tests/commands/report/execute_test.py 
b/tests/unit_tests/commands/report/execute_test.py
index 7c04f2f8ab..0f24577729 100644
--- a/tests/unit_tests/commands/report/execute_test.py
+++ b/tests/unit_tests/commands/report/execute_test.py
@@ -24,9 +24,11 @@ import pytest
 from pytest_mock import MockerFixture
 
 from superset.app import SupersetApp
+from superset.commands.exceptions import UpdateFailedError
 from superset.commands.report.execute import BaseReportState
 from superset.dashboards.permalink.types import DashboardPermalinkState
 from superset.reports.models import (
+    ReportRecipients,
     ReportRecipientType,
     ReportSchedule,
     ReportScheduleType,
@@ -480,3 +482,78 @@ def test_screenshot_width_calculation(
                     f"Test {test_id}: Expected width {expected_width}, "
                     f"but got {kwargs['window_size'][0]}"
                 )
+
+
+def test_update_recipient_to_slack_v2(mocker: MockerFixture):
+    """
+    Test converting a Slack recipient to Slack v2 format.
+    """
+    mocker.patch(
+        "superset.commands.report.execute.get_channels_with_search",
+        return_value=[
+            {
+                "id": "abc124f",
+                "name": "channel-1",
+                "is_member": True,
+                "is_private": False,
+            },
+            {
+                "id": "blah_!channel_2",
+                "name": "Channel_2",
+                "is_member": True,
+                "is_private": False,
+            },
+        ],
+    )
+    mock_report_schedule = ReportSchedule(
+        recipients=[
+            ReportRecipients(
+                type=ReportRecipientType.SLACK,
+                recipient_config_json=json.dumps({"target": "Channel-1, 
Channel_2"}),
+            ),
+        ],
+    )
+
+    mock_cmmd: BaseReportState = BaseReportState(
+        mock_report_schedule, "January 1, 2021", "execution_id_example"
+    )
+    mock_cmmd.update_report_schedule_slack_v2()
+
+    assert (
+        mock_cmmd._report_schedule.recipients[0].recipient_config_json
+        == '{"target": "abc124f,blah_!channel_2"}'
+    )
+    assert mock_cmmd._report_schedule.recipients[0].type == 
ReportRecipientType.SLACKV2
+
+
+def test_update_recipient_to_slack_v2_missing_channels(mocker: MockerFixture):
+    """
+    Test converting a Slack recipient to Slack v2 format raises an error
+    in case it can't find all channels.
+    """
+    mocker.patch(
+        "superset.commands.report.execute.get_channels_with_search",
+        return_value=[
+            {
+                "id": "blah_!channel_2",
+                "name": "Channel 2",
+                "is_member": True,
+                "is_private": False,
+            },
+        ],
+    )
+    mock_report_schedule = ReportSchedule(
+        name="Test Report",
+        recipients=[
+            ReportRecipients(
+                type=ReportRecipientType.SLACK,
+                recipient_config_json=json.dumps({"target": "Channel 1, 
Channel 2"}),
+            ),
+        ],
+    )
+
+    mock_cmmd: BaseReportState = BaseReportState(
+        mock_report_schedule, "January 1, 2021", "execution_id_example"
+    )
+    with pytest.raises(UpdateFailedError):
+        mock_cmmd.update_report_schedule_slack_v2()
diff --git a/tests/unit_tests/utils/slack_test.py 
b/tests/unit_tests/utils/slack_test.py
index 42c3d3d4ce..ed7a82c220 100644
--- a/tests/unit_tests/utils/slack_test.py
+++ b/tests/unit_tests/utils/slack_test.py
@@ -153,7 +153,12 @@ The server responded with: missing scope: channels:read"""
     def test_filter_channels_by_specified_types(self, mocker):
         mock_data = {
             "channels": [
-                {"name": "general", "id": "C12345", "type": "public"},
+                {
+                    "id": "C12345",
+                    "name": "general",
+                    "is_member": False,
+                    "is_private": False,
+                },
             ],
             "response_metadata": {"next_cursor": None},
         }
@@ -164,7 +169,14 @@ The server responded with: missing scope: channels:read"""
         mocker.patch("superset.utils.slack.get_slack_client", 
return_value=mock_client)
 
         result = get_channels_with_search(types=["public"])
-        assert result == [{"name": "general", "id": "C12345", "type": 
"public"}]
+        assert result == [
+            {
+                "id": "C12345",
+                "name": "general",
+                "is_member": False,
+                "is_private": False,
+            }
+        ]
 
     def test_handle_pagination_multiple_pages(self, mocker):
         mock_data_page1 = {

Reply via email to