mirror of
https://github.com/apache/superset.git
synced 2026-05-24 01:05:21 +00:00
804 lines
28 KiB
Python
804 lines
28 KiB
Python
# Licensed to the Apache Software Foundation (ASF) under one
|
|
# or more contributor license agreements. See the NOTICE file
|
|
# distributed with this work for additional information
|
|
# regarding copyright ownership. The ASF licenses this file
|
|
# to you under the Apache License, Version 2.0 (the
|
|
# "License"); you may not use this file except in compliance
|
|
# with the License. You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing,
|
|
# software distributed under the License is distributed on an
|
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
# KIND, either express or implied. See the License for the
|
|
# specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import pytest
|
|
|
|
from superset.utils.slack import get_channels_with_search, SlackChannelTypes
|
|
|
|
|
|
class MockResponse:
|
|
def __init__(self, data):
|
|
self._data = data
|
|
|
|
@property
|
|
def data(self):
|
|
return self._data
|
|
|
|
|
|
class TestGetChannelsWithSearch:
|
|
# Fetch all channels when no search string is provided
|
|
def test_fetch_all_channels_no_search_string(self, mocker):
|
|
# Mock data
|
|
mock_data = {
|
|
"channels": [
|
|
{
|
|
"name": "general",
|
|
"id": "C12345",
|
|
"is_private": False,
|
|
"is_member": True,
|
|
}
|
|
],
|
|
"response_metadata": {"next_cursor": None},
|
|
}
|
|
|
|
# Mock class instance with data property
|
|
mock_response_instance = MockResponse(mock_data)
|
|
|
|
mock_client = mocker.Mock()
|
|
mock_client.conversations_list.return_value = mock_response_instance
|
|
mocker.patch("superset.utils.slack.get_slack_client", return_value=mock_client)
|
|
|
|
result = get_channels_with_search()
|
|
assert result == {
|
|
"result": [
|
|
{
|
|
"name": "general",
|
|
"id": "C12345",
|
|
"is_private": False,
|
|
"is_member": True,
|
|
}
|
|
],
|
|
"next_cursor": None,
|
|
"has_more": False,
|
|
}
|
|
|
|
# Handle an empty search string gracefully
|
|
def test_handle_empty_search_string(self, mocker):
|
|
mock_data = {
|
|
"channels": [
|
|
{
|
|
"name": "general",
|
|
"id": "C12345",
|
|
"is_private": False,
|
|
"is_member": True,
|
|
}
|
|
],
|
|
"response_metadata": {"next_cursor": None},
|
|
}
|
|
|
|
mock_response_instance = MockResponse(mock_data)
|
|
mock_client = mocker.Mock()
|
|
mock_client.conversations_list.return_value = mock_response_instance
|
|
mocker.patch("superset.utils.slack.get_slack_client", return_value=mock_client)
|
|
|
|
result = get_channels_with_search(search_string="")
|
|
assert result == {
|
|
"result": [
|
|
{
|
|
"name": "general",
|
|
"id": "C12345",
|
|
"is_private": False,
|
|
"is_member": True,
|
|
}
|
|
],
|
|
"next_cursor": None,
|
|
"has_more": False,
|
|
}
|
|
|
|
def test_handle_exact_match_search_string_single_channel(self, mocker):
|
|
# Mock data with multiple channels
|
|
mock_data = {
|
|
"channels": [
|
|
{
|
|
"name": "general",
|
|
"id": "C12345",
|
|
"is_private": False,
|
|
"is_member": True,
|
|
},
|
|
{
|
|
"name": "general2",
|
|
"id": "C13454",
|
|
"is_private": False,
|
|
"is_member": True,
|
|
},
|
|
{
|
|
"name": "random",
|
|
"id": "C67890",
|
|
"is_private": False,
|
|
"is_member": True,
|
|
},
|
|
],
|
|
"response_metadata": {"next_cursor": None},
|
|
}
|
|
|
|
# Mock response and client setup
|
|
mock_response_instance = MockResponse(mock_data)
|
|
mock_client = mocker.Mock()
|
|
mock_client.conversations_list.return_value = mock_response_instance
|
|
mocker.patch("superset.utils.slack.get_slack_client", return_value=mock_client)
|
|
|
|
# Call the function with a search string that matches a single channel
|
|
result = get_channels_with_search(
|
|
search_string="general", exact_match=True, limit=100
|
|
)
|
|
|
|
# Assert that the result is a dict with proper structure
|
|
assert result == {
|
|
"result": [
|
|
{
|
|
"name": "general",
|
|
"id": "C12345",
|
|
"is_private": False,
|
|
"is_member": True,
|
|
}
|
|
],
|
|
"next_cursor": None,
|
|
"has_more": False,
|
|
}
|
|
|
|
def test_handle_exact_match_search_string_multiple_channels(self, mocker):
|
|
mock_data = {
|
|
"channels": [
|
|
{
|
|
"name": "general",
|
|
"id": "C12345",
|
|
"is_private": False,
|
|
"is_member": True,
|
|
},
|
|
{
|
|
"name": "general2",
|
|
"id": "C13454",
|
|
"is_private": False,
|
|
"is_member": True,
|
|
},
|
|
{
|
|
"name": "random",
|
|
"id": "C67890",
|
|
"is_private": False,
|
|
"is_member": True,
|
|
},
|
|
],
|
|
"response_metadata": {"next_cursor": None},
|
|
}
|
|
|
|
mock_response_instance = MockResponse(mock_data)
|
|
mock_client = mocker.Mock()
|
|
mock_client.conversations_list.return_value = mock_response_instance
|
|
mocker.patch("superset.utils.slack.get_slack_client", return_value=mock_client)
|
|
|
|
result = get_channels_with_search(
|
|
search_string="general", exact_match=True, limit=100
|
|
)
|
|
assert result == {
|
|
"result": [
|
|
{
|
|
"name": "general",
|
|
"id": "C12345",
|
|
"is_private": False,
|
|
"is_member": True,
|
|
},
|
|
],
|
|
"next_cursor": None,
|
|
"has_more": False,
|
|
}
|
|
|
|
def test_handle_loose_match_search_string_multiple_channels(self, mocker):
|
|
mock_data = {
|
|
"channels": [
|
|
{
|
|
"name": "general",
|
|
"id": "C12345",
|
|
"is_private": False,
|
|
"is_member": True,
|
|
},
|
|
{
|
|
"name": "general2",
|
|
"id": "C13454",
|
|
"is_private": False,
|
|
"is_member": True,
|
|
},
|
|
{
|
|
"name": "random",
|
|
"id": "C67890",
|
|
"is_private": False,
|
|
"is_member": True,
|
|
},
|
|
],
|
|
"response_metadata": {"next_cursor": None},
|
|
}
|
|
|
|
mock_response_instance = MockResponse(mock_data)
|
|
mock_client = mocker.Mock()
|
|
mock_client.conversations_list.return_value = mock_response_instance
|
|
mocker.patch("superset.utils.slack.get_slack_client", return_value=mock_client)
|
|
|
|
result = get_channels_with_search(search_string="general", limit=100)
|
|
assert result == {
|
|
"result": [
|
|
{
|
|
"name": "general",
|
|
"id": "C12345",
|
|
"is_private": False,
|
|
"is_member": True,
|
|
},
|
|
{
|
|
"name": "general2",
|
|
"id": "C13454",
|
|
"is_private": False,
|
|
"is_member": True,
|
|
},
|
|
],
|
|
"next_cursor": None,
|
|
"has_more": False,
|
|
}
|
|
|
|
def test_handle_slack_client_error_listing_channels(self, mocker):
|
|
from slack_sdk.errors import SlackApiError
|
|
|
|
from superset.exceptions import SupersetException
|
|
|
|
mock_client = mocker.Mock()
|
|
mock_client.conversations_list.side_effect = SlackApiError(
|
|
"foo", "missing scope: channels:read"
|
|
)
|
|
mocker.patch("superset.utils.slack.get_slack_client", return_value=mock_client)
|
|
|
|
with pytest.raises(SupersetException) as ex:
|
|
get_channels_with_search()
|
|
|
|
assert str(ex.value) == (
|
|
"""Failed to list channels: foo
|
|
The server responded with: missing scope: channels:read"""
|
|
)
|
|
|
|
@pytest.mark.parametrize(
|
|
"types, expected_channel_ids",
|
|
[
|
|
([SlackChannelTypes.PUBLIC], {"public_channel_id"}),
|
|
([SlackChannelTypes.PRIVATE], {"private_channel_id"}),
|
|
(
|
|
[SlackChannelTypes.PUBLIC, SlackChannelTypes.PRIVATE],
|
|
{"public_channel_id", "private_channel_id"},
|
|
),
|
|
([], {"public_channel_id", "private_channel_id"}),
|
|
],
|
|
)
|
|
def test_filter_channels_by_specified_types(
|
|
self, types: list[SlackChannelTypes], expected_channel_ids: set[str], mocker
|
|
):
|
|
# Determine which channels to return based on types parameter
|
|
public_channel = {
|
|
"id": "public_channel_id",
|
|
"name": "open",
|
|
"is_member": False,
|
|
"is_private": False,
|
|
}
|
|
private_channel = {
|
|
"id": "private_channel_id",
|
|
"name": "secret",
|
|
"is_member": False,
|
|
"is_private": True,
|
|
}
|
|
|
|
# Mock should return channels matching the requested types
|
|
# (simulating Slack API's type filtering)
|
|
channels = []
|
|
if not types or SlackChannelTypes.PUBLIC in types:
|
|
channels.append(public_channel)
|
|
if not types or SlackChannelTypes.PRIVATE in types:
|
|
channels.append(private_channel)
|
|
|
|
mock_data = {
|
|
"channels": channels,
|
|
"response_metadata": {"next_cursor": None},
|
|
}
|
|
|
|
mock_response_instance = MockResponse(mock_data)
|
|
mock_client = mocker.Mock()
|
|
mock_client.conversations_list.return_value = mock_response_instance
|
|
mocker.patch("superset.utils.slack.get_slack_client", return_value=mock_client)
|
|
|
|
result = get_channels_with_search(types=types)
|
|
assert {channel["id"] for channel in result["result"]} == expected_channel_ids
|
|
|
|
def test_handle_pagination_without_search(self, mocker):
|
|
"""Test pagination returns single page with cursor"""
|
|
mock_data_page1 = {
|
|
"channels": [
|
|
{
|
|
"name": "general",
|
|
"id": "C12345",
|
|
"is_private": False,
|
|
"is_member": True,
|
|
}
|
|
],
|
|
"response_metadata": {"next_cursor": "page2_cursor"},
|
|
}
|
|
|
|
mock_response_instance_page1 = MockResponse(mock_data_page1)
|
|
mock_client = mocker.Mock()
|
|
mock_client.conversations_list.return_value = mock_response_instance_page1
|
|
mocker.patch("superset.utils.slack.get_slack_client", return_value=mock_client)
|
|
|
|
result = get_channels_with_search(limit=100)
|
|
assert result == {
|
|
"result": [
|
|
{
|
|
"name": "general",
|
|
"id": "C12345",
|
|
"is_private": False,
|
|
"is_member": True,
|
|
}
|
|
],
|
|
"next_cursor": "page2_cursor",
|
|
"has_more": True,
|
|
}
|
|
|
|
def test_handle_pagination_with_cursor(self, mocker):
|
|
"""Test pagination with cursor fetches next page"""
|
|
mock_data_page2 = {
|
|
"channels": [
|
|
{
|
|
"name": "random",
|
|
"id": "C67890",
|
|
"is_private": False,
|
|
"is_member": True,
|
|
}
|
|
],
|
|
"response_metadata": {"next_cursor": None},
|
|
}
|
|
|
|
mock_response_instance_page2 = MockResponse(mock_data_page2)
|
|
mock_client = mocker.Mock()
|
|
mock_client.conversations_list.return_value = mock_response_instance_page2
|
|
mocker.patch("superset.utils.slack.get_slack_client", return_value=mock_client)
|
|
|
|
result = get_channels_with_search(cursor="page2_cursor", limit=100)
|
|
assert result == {
|
|
"result": [
|
|
{
|
|
"name": "random",
|
|
"id": "C67890",
|
|
"is_private": False,
|
|
"is_member": True,
|
|
}
|
|
],
|
|
"next_cursor": None,
|
|
"has_more": False,
|
|
}
|
|
|
|
def test_streaming_search_pagination(self, mocker):
|
|
"""Test search mode streams through pages until limit is reached"""
|
|
mock_data_page1 = {
|
|
"channels": [
|
|
{"name": "general", "id": "C1", "is_private": False, "is_member": True},
|
|
{"name": "random", "id": "C2", "is_private": False, "is_member": True},
|
|
],
|
|
"response_metadata": {"next_cursor": "page2"},
|
|
}
|
|
mock_data_page2 = {
|
|
"channels": [
|
|
{
|
|
"name": "general-2",
|
|
"id": "C3",
|
|
"is_private": False,
|
|
"is_member": True,
|
|
},
|
|
{"name": "other", "id": "C4", "is_private": False, "is_member": True},
|
|
],
|
|
"response_metadata": {"next_cursor": None},
|
|
}
|
|
|
|
mock_response_instance_page1 = MockResponse(mock_data_page1)
|
|
mock_response_instance_page2 = MockResponse(mock_data_page2)
|
|
|
|
mock_client = mocker.Mock()
|
|
mock_client.conversations_list.side_effect = [
|
|
mock_response_instance_page1,
|
|
mock_response_instance_page2,
|
|
]
|
|
mocker.patch("superset.utils.slack.get_slack_client", return_value=mock_client)
|
|
|
|
# Search for "general" - should find 2 channels across 2 pages
|
|
result = get_channels_with_search(search_string="general", limit=100)
|
|
assert result == {
|
|
"result": [
|
|
{"name": "general", "id": "C1", "is_private": False, "is_member": True},
|
|
{
|
|
"name": "general-2",
|
|
"id": "C3",
|
|
"is_private": False,
|
|
"is_member": True,
|
|
},
|
|
],
|
|
"next_cursor": None,
|
|
"has_more": False,
|
|
}
|
|
|
|
def test_streaming_search_max_pages_safety_limit(self, mocker):
|
|
"""Test streaming search stops after 50 pages to prevent runaway requests"""
|
|
|
|
# Create a response that always has a next cursor (infinite pagination)
|
|
mock_data = {
|
|
"channels": [
|
|
{"name": "channel", "id": "C1", "is_private": False, "is_member": True},
|
|
],
|
|
"response_metadata": {"next_cursor": "next_page"},
|
|
}
|
|
mock_response = MockResponse(mock_data)
|
|
|
|
mock_client = mocker.Mock()
|
|
mock_client.conversations_list.return_value = mock_response
|
|
mocker.patch("superset.utils.slack.get_slack_client", return_value=mock_client)
|
|
|
|
# Search that matches the channel - should stop at 50 pages
|
|
result = get_channels_with_search(search_string="channel", limit=100)
|
|
|
|
# Should have called conversations_list exactly 50 times (max pages)
|
|
assert mock_client.conversations_list.call_count == 50
|
|
# Should return the matches found (50 channels, one per page)
|
|
assert len(result["result"]) == 50
|
|
|
|
def test_search_with_no_matches(self, mocker):
|
|
"""Test search that finds no matching channels"""
|
|
|
|
mock_data = {
|
|
"channels": [
|
|
{"name": "general", "id": "C1", "is_private": False, "is_member": True},
|
|
{"name": "random", "id": "C2", "is_private": False, "is_member": True},
|
|
],
|
|
"response_metadata": {"next_cursor": None},
|
|
}
|
|
mock_response = MockResponse(mock_data)
|
|
|
|
mock_client = mocker.Mock()
|
|
mock_client.conversations_list.return_value = mock_response
|
|
mocker.patch("superset.utils.slack.get_slack_client", return_value=mock_client)
|
|
|
|
# Search for non-existent channel
|
|
result = get_channels_with_search(search_string="nonexistent", limit=100)
|
|
|
|
assert result == {
|
|
"result": [],
|
|
"next_cursor": None,
|
|
"has_more": False,
|
|
}
|
|
|
|
def test_search_returns_exactly_limit(self, mocker):
|
|
"""Test search that returns exactly the requested limit"""
|
|
|
|
# Create 100 matching channels
|
|
channels = [
|
|
{"name": f"test-{i}", "id": f"C{i}", "is_private": False, "is_member": True}
|
|
for i in range(100)
|
|
]
|
|
|
|
mock_data = {
|
|
"channels": channels,
|
|
"response_metadata": {"next_cursor": "next_page"},
|
|
}
|
|
mock_response = MockResponse(mock_data)
|
|
|
|
mock_client = mocker.Mock()
|
|
mock_client.conversations_list.return_value = mock_response
|
|
mocker.patch("superset.utils.slack.get_slack_client", return_value=mock_client)
|
|
|
|
# Search that matches all channels
|
|
result = get_channels_with_search(search_string="test", limit=100)
|
|
|
|
# Should return exactly 100 channels
|
|
assert len(result["result"]) == 100
|
|
# Should indicate more results available
|
|
assert result["has_more"] is True
|
|
assert result["next_cursor"] == "next_page"
|
|
|
|
def test_partial_page_results(self, mocker):
|
|
"""Test pagination with partial page (less than limit)"""
|
|
|
|
# Only 50 channels returned (less than default 100 limit)
|
|
channels = [
|
|
{
|
|
"name": f"channel-{i}",
|
|
"id": f"C{i}",
|
|
"is_private": False,
|
|
"is_member": True,
|
|
}
|
|
for i in range(50)
|
|
]
|
|
|
|
mock_data = {
|
|
"channels": channels,
|
|
"response_metadata": {"next_cursor": None},
|
|
}
|
|
mock_response = MockResponse(mock_data)
|
|
|
|
mock_client = mocker.Mock()
|
|
mock_client.conversations_list.return_value = mock_response
|
|
mocker.patch("superset.utils.slack.get_slack_client", return_value=mock_client)
|
|
|
|
result = get_channels_with_search(limit=100)
|
|
|
|
# Should return all 50 channels
|
|
assert len(result["result"]) == 50
|
|
# Should indicate no more results
|
|
assert result["has_more"] is False
|
|
assert result["next_cursor"] is None
|
|
|
|
def test_streaming_search_stops_when_limit_reached(self, mocker):
|
|
"""Test that streaming search stops immediately when limit is reached"""
|
|
|
|
# First page with 60 matching channels
|
|
page1_channels = [
|
|
{"name": f"test-{i}", "id": f"C{i}", "is_private": False, "is_member": True}
|
|
for i in range(60)
|
|
]
|
|
# Second page with 60 more matching channels (should not be fully processed)
|
|
page2_channels = [
|
|
{
|
|
"name": f"test-{i}",
|
|
"id": f"C{i + 60}",
|
|
"is_private": False,
|
|
"is_member": True,
|
|
}
|
|
for i in range(60)
|
|
]
|
|
|
|
mock_data_page1 = {
|
|
"channels": page1_channels,
|
|
"response_metadata": {"next_cursor": "page2"},
|
|
}
|
|
mock_data_page2 = {
|
|
"channels": page2_channels,
|
|
"response_metadata": {"next_cursor": "page3"},
|
|
}
|
|
|
|
mock_response_page1 = MockResponse(mock_data_page1)
|
|
mock_response_page2 = MockResponse(mock_data_page2)
|
|
|
|
mock_client = mocker.Mock()
|
|
mock_client.conversations_list.side_effect = [
|
|
mock_response_page1,
|
|
mock_response_page2,
|
|
]
|
|
mocker.patch("superset.utils.slack.get_slack_client", return_value=mock_client)
|
|
|
|
# Request limit of 100
|
|
result = get_channels_with_search(search_string="test", limit=100)
|
|
|
|
# Should return exactly 100 channels (60 from page1 + 40 from page2)
|
|
assert len(result["result"]) == 100
|
|
# Should indicate more results available (next cursor points to page3)
|
|
assert result["has_more"] is True
|
|
assert result["next_cursor"] == "page3"
|
|
|
|
def test_cursor_format_with_special_characters(self, mocker):
|
|
"""Test that cursor with special characters is handled correctly"""
|
|
|
|
# Slack cursors are base64 encoded strings that might contain special chars
|
|
special_cursor = "dGVhbTpDMDYxRkE1UEw="
|
|
|
|
mock_data = {
|
|
"channels": [
|
|
{"name": "test", "id": "C123", "is_private": False, "is_member": True},
|
|
],
|
|
"response_metadata": {"next_cursor": None},
|
|
}
|
|
mock_response = MockResponse(mock_data)
|
|
|
|
mock_client = mocker.Mock()
|
|
mock_client.conversations_list.return_value = mock_response
|
|
mocker.patch("superset.utils.slack.get_slack_client", return_value=mock_client)
|
|
|
|
# Call with special cursor
|
|
get_channels_with_search(cursor=special_cursor, limit=100)
|
|
|
|
# Verify cursor was passed to Slack API
|
|
mock_client.conversations_list.assert_called_once()
|
|
call_kwargs = mock_client.conversations_list.call_args[1]
|
|
assert call_kwargs["cursor"] == special_cursor
|
|
|
|
def test_empty_channel_list_response(self, mocker):
|
|
"""Test handling of empty channels list from Slack API"""
|
|
|
|
mock_data = {
|
|
"channels": [],
|
|
"response_metadata": {"next_cursor": None},
|
|
}
|
|
mock_response = MockResponse(mock_data)
|
|
|
|
mock_client = mocker.Mock()
|
|
mock_client.conversations_list.return_value = mock_response
|
|
mocker.patch("superset.utils.slack.get_slack_client", return_value=mock_client)
|
|
|
|
result = get_channels_with_search()
|
|
|
|
assert result == {
|
|
"result": [],
|
|
"next_cursor": None,
|
|
"has_more": False,
|
|
}
|
|
|
|
def test_custom_limit_parameter(self, mocker):
|
|
"""Test that custom limit parameter is respected"""
|
|
|
|
all_channels = [
|
|
{
|
|
"name": f"channel-{i}",
|
|
"id": f"C{i}",
|
|
"is_private": False,
|
|
"is_member": True,
|
|
}
|
|
for i in range(200)
|
|
]
|
|
|
|
# Mock should respect the limit parameter (simulating Slack API behavior)
|
|
def mock_conversations_list(**kwargs):
|
|
limit = kwargs.get("limit", 100)
|
|
return MockResponse(
|
|
{
|
|
"channels": all_channels[:limit],
|
|
"response_metadata": {"next_cursor": "next_page"},
|
|
}
|
|
)
|
|
|
|
mock_client = mocker.Mock()
|
|
mock_client.conversations_list.side_effect = mock_conversations_list
|
|
mocker.patch("superset.utils.slack.get_slack_client", return_value=mock_client)
|
|
|
|
# Request custom limit of 50
|
|
result = get_channels_with_search(limit=50)
|
|
|
|
# Should return exactly 50 channels
|
|
assert len(result["result"]) == 50
|
|
assert result["has_more"] is True
|
|
|
|
def test_non_search_pagination_over_200_limit(self, mocker):
|
|
"""Test non-search queries paginate correctly for limits > 200"""
|
|
# Create 500 channels
|
|
all_channels = [
|
|
{
|
|
"name": f"channel-{i}",
|
|
"id": f"C{i}",
|
|
"is_private": False,
|
|
"is_member": True,
|
|
}
|
|
for i in range(500)
|
|
]
|
|
|
|
call_count = 0
|
|
|
|
def mock_conversations_list(**kwargs):
|
|
nonlocal call_count
|
|
limit = kwargs.get("limit", 100)
|
|
cursor = kwargs.get("cursor")
|
|
|
|
# Simulate Slack API pagination (max 200 per page)
|
|
if cursor is None:
|
|
start = 0
|
|
elif cursor == "cursor_200":
|
|
start = 200
|
|
elif cursor == "cursor_400":
|
|
start = 400
|
|
else:
|
|
start = 600
|
|
|
|
end = min(start + limit, 500)
|
|
next_cursor = f"cursor_{end}" if end < 500 else None
|
|
|
|
call_count += 1
|
|
return MockResponse(
|
|
{
|
|
"channels": all_channels[start:end],
|
|
"response_metadata": {"next_cursor": next_cursor},
|
|
}
|
|
)
|
|
|
|
mock_client = mocker.Mock()
|
|
mock_client.conversations_list.side_effect = mock_conversations_list
|
|
mocker.patch("superset.utils.slack.get_slack_client", return_value=mock_client)
|
|
|
|
# Request 300 channels (requires 2 pages of 200 each)
|
|
result = get_channels_with_search(limit=300)
|
|
|
|
# Should return exactly 300 channels
|
|
assert len(result["result"]) == 300
|
|
assert result["has_more"] is True
|
|
assert result["next_cursor"] == "cursor_400"
|
|
# Should have made 2 API calls
|
|
assert call_count == 2
|
|
|
|
def test_search_with_exact_match_optimization(self, mocker):
|
|
"""Test exact match search uses optimized string comparison"""
|
|
channels = [
|
|
{"name": "test", "id": "C1", "is_private": False, "is_member": True},
|
|
{"name": "test-dev", "id": "C2", "is_private": False, "is_member": True},
|
|
{"name": "testing", "id": "C3", "is_private": False, "is_member": True},
|
|
]
|
|
mock_data = {"channels": channels, "response_metadata": {"next_cursor": None}}
|
|
|
|
mock_response = MockResponse(mock_data)
|
|
mock_client = mocker.Mock()
|
|
mock_client.conversations_list.return_value = mock_response
|
|
mocker.patch("superset.utils.slack.get_slack_client", return_value=mock_client)
|
|
|
|
result = get_channels_with_search(
|
|
search_string="TEST", exact_match=True, limit=100
|
|
)
|
|
|
|
# Only "test" should match (case-insensitive exact match)
|
|
assert len(result["result"]) == 1
|
|
assert result["result"][0]["name"] == "test"
|
|
|
|
def test_search_substring_match_optimization(self, mocker):
|
|
"""Test substring search uses optimized string comparison"""
|
|
channels = [
|
|
{"name": "prod-api", "id": "C1", "is_private": False, "is_member": True},
|
|
{"name": "dev-api", "id": "C2", "is_private": False, "is_member": True},
|
|
{"name": "staging", "id": "C3", "is_private": False, "is_member": True},
|
|
]
|
|
mock_data = {"channels": channels, "response_metadata": {"next_cursor": None}}
|
|
|
|
mock_response = MockResponse(mock_data)
|
|
mock_client = mocker.Mock()
|
|
mock_client.conversations_list.return_value = mock_response
|
|
mocker.patch("superset.utils.slack.get_slack_client", return_value=mock_client)
|
|
|
|
result = get_channels_with_search(search_string="API", limit=100)
|
|
|
|
# Both "prod-api" and "dev-api" should match (case-insensitive)
|
|
assert len(result["result"]) == 2
|
|
assert {ch["name"] for ch in result["result"]} == {"prod-api", "dev-api"}
|
|
|
|
def test_search_by_channel_id(self, mocker):
|
|
"""Test search can match by channel ID"""
|
|
channels = [
|
|
{"name": "general", "id": "C12345", "is_private": False, "is_member": True},
|
|
{"name": "random", "id": "C67890", "is_private": False, "is_member": True},
|
|
]
|
|
mock_data = {"channels": channels, "response_metadata": {"next_cursor": None}}
|
|
|
|
mock_response = MockResponse(mock_data)
|
|
mock_client = mocker.Mock()
|
|
mock_client.conversations_list.return_value = mock_response
|
|
mocker.patch("superset.utils.slack.get_slack_client", return_value=mock_client)
|
|
|
|
result = get_channels_with_search(
|
|
search_string="c12345", exact_match=True, limit=100
|
|
)
|
|
|
|
# Should match by ID (case-insensitive)
|
|
assert len(result["result"]) == 1
|
|
assert result["result"][0]["id"] == "C12345"
|
|
|
|
def test_non_search_empty_result_handling(self, mocker):
|
|
"""Test non-search query handles empty channel list"""
|
|
mock_data = {
|
|
"channels": [],
|
|
"response_metadata": {"next_cursor": None},
|
|
}
|
|
|
|
mock_response = MockResponse(mock_data)
|
|
mock_client = mocker.Mock()
|
|
mock_client.conversations_list.return_value = mock_response
|
|
mocker.patch("superset.utils.slack.get_slack_client", return_value=mock_client)
|
|
|
|
result = get_channels_with_search(limit=100)
|
|
|
|
assert len(result["result"]) == 0
|
|
assert result["has_more"] is False
|
|
assert result["next_cursor"] is None
|