diff --git a/zerver/data_import/slack.py b/zerver/data_import/slack.py index 9ded60a89c..32185af32d 100644 --- a/zerver/data_import/slack.py +++ b/zerver/data_import/slack.py @@ -1466,8 +1466,13 @@ def check_token_access(token: str) -> None: data = requests.get( "https://slack.com/api/team.info", headers={"Authorization": f"Bearer {token}"} ) - if data.status_code != 200 or not data.json()["ok"]: - raise ValueError(f"Invalid Slack token: {token}") + if data.status_code != 200: + raise ValueError( + f"Failed to fetch data (HTTP status {data.status_code}) for Slack token: {token}" + ) + if not data.json()["ok"]: + error = data.json()["error"] + raise ValueError(f"Invalid Slack token: {token}, {error}") has_scopes = set(data.headers.get("x-oauth-scopes", "").split(",")) required_scopes = {"emoji:read", "users:read", "users:read.email", "team:read"} missing_scopes = required_scopes - has_scopes diff --git a/zerver/tests/test_slack_importer.py b/zerver/tests/test_slack_importer.py index c0b40376cd..bb21f18c3f 100644 --- a/zerver/tests/test_slack_importer.py +++ b/zerver/tests/test_slack_importer.py @@ -28,6 +28,7 @@ from zerver.data_import.slack import ( SlackBotEmail, channel_message_to_zerver_message, channels_to_zerver_stream, + check_token_access, convert_slack_workspace_messages, do_convert_data, fetch_shared_channel_users, @@ -181,6 +182,62 @@ class SlackImporter(ZulipTestCase): self.assertEqual(test_zerver_realm_dict["name"], realm_subdomain) self.assertEqual(test_zerver_realm_dict["date_created"], time) + @responses.activate + def test_check_token_access(self) -> None: + def token_request_callback(request: PreparedRequest) -> Tuple[int, Dict[str, str], bytes]: + auth = request.headers.get("Authorization") + if auth == "Bearer xoxb-broken-request": + return (400, {}, orjson.dumps({"ok": False, "error": "invalid_auth"})) + + if auth == "Bearer xoxb-invalid-token": + return (200, {}, orjson.dumps({"ok": False, "error": "invalid_auth"})) + + if auth == "Bearer xoxb-limited-scopes": + return ( + 200, + {"x-oauth-scopes": "emoji:read,bogus:scope"}, + orjson.dumps({"ok": True}), + ) + if auth == "Bearer xoxb-valid-token": + return ( + 200, + {"x-oauth-scopes": "emoji:read,users:read,users:read.email,team:read"}, + orjson.dumps({"ok": True}), + ) + else: # nocoverage + raise Exception("Unknown token mock") + + responses.add_callback( + responses.GET, "https://slack.com/api/team.info", callback=token_request_callback + ) + + def exception_for(token: str) -> str: + with self.assertRaises(Exception) as invalid: + check_token_access(token) + return invalid.exception.args[0] + + self.assertEqual( + exception_for("xoxq-unknown"), + "Unknown token type -- must start with xoxb- or xoxp-", + ) + + self.assertEqual( + exception_for("xoxb-invalid-token"), + "Invalid Slack token: xoxb-invalid-token, invalid_auth", + ) + + self.assertEqual( + exception_for("xoxb-broken-request"), + "Failed to fetch data (HTTP status 400) for Slack token: xoxb-broken-request", + ) + + self.assertEqual( + exception_for("xoxb-limited-scopes"), + "Slack token is missing the following required scopes: ['team:read', 'users:read', 'users:read.email']", + ) + + check_token_access("xoxb-valid-token") + def test_get_owner(self) -> None: user_data = [ {"is_owner": False, "is_primary_owner": False}, @@ -1186,8 +1243,10 @@ class SlackImporter(ZulipTestCase): @mock.patch("zerver.data_import.slack.build_avatar_url") @mock.patch("zerver.data_import.slack.build_avatar") @mock.patch("zerver.data_import.slack.get_slack_api_data") + @mock.patch("zerver.data_import.slack.check_token_access") def test_slack_import_to_existing_database( self, + mock_check_token_access: mock.Mock, mock_get_slack_api_data: mock.Mock, mock_build_avatar_url: mock.Mock, mock_build_avatar: mock.Mock, @@ -1385,8 +1444,10 @@ class SlackImporter(ZulipTestCase): @mock.patch("zerver.data_import.slack.build_avatar_url") @mock.patch("zerver.data_import.slack.build_avatar") @mock.patch("zerver.data_import.slack.get_slack_api_data") + @mock.patch("zerver.data_import.slack.check_token_access") def test_slack_import_unicode_filenames( self, + mock_check_token_access: mock.Mock, mock_get_slack_api_data: mock.Mock, mock_build_avatar_url: mock.Mock, mock_build_avatar: mock.Mock,