# Backwards-compatible refactoring of the Slack toolkit Several performance improvements, streamlined tool set, and easier to understand tool interfaces. All "old" tools were kept for backwards compatibility, with the same interfaces and response structure (but using the new and more performant tools under the hood). Full revision of unit-tests and evals. ## Streamlined tool set Multiple groups of tools were streamlined into a single one: * "get conversation metadata" from 5 tools to one; * "send message" from 2 tools to one; * "get users in conversation" from 3 tools to one; * "get messages" from 4 tools to one ## New capabilities * Messages retrieved are now populated with the users' names, apart from ID: makes it easier for LLMs to reference who sent a message, were mentioned, or reacted to a message * Retrieve users by username, email, and/or ID (before we only supported ID) * Retrieve multiple users in a single tool call ## Concurrency controls All operations issuing multiple requests concurrently now have a `Semaphore` to limit the concurrency level. The limit can be controlled through the `SLACK_MAX_CONCURRENT_REQUESTS` env var (defaults to 3). ## Networking performance improvement Various operations that used to make multiple API calls are now executed more efficiently: ### Find users by username * Before: a full scan of `users_list` was required (potentially multiple pages for large workspaces); * Now it stops as soon as we have all users needed (yes, it was dumb before) ### Get multiple users by their IDs * Before: for each user ID, we made one API call to the `users_info` endpoint * Now: we call `list_users` and scan the results to match the user IDs (an estimate of 99.5% of Slack workspaces have < 200 users; for large workspaces, we may need to paginate `list_users`) ### Get a conversation by its users * Before: * Call to `list_conversations` (potentially paginating) * For each conversation, one call to `conversations_members` (potentially paginating) * Then loop and find which conversation matches the users' IDs * Now: * A single call to `conversations_open`
689 lines
21 KiB
Python
689 lines
21 KiB
Python
import asyncio
|
|
import copy
|
|
import json
|
|
from unittest.mock import AsyncMock, call, patch
|
|
|
|
import pytest
|
|
from arcade_tdk.errors import RetryableToolError
|
|
from slack_sdk.errors import SlackApiError
|
|
from slack_sdk.web.async_client import AsyncWebClient
|
|
|
|
from arcade_slack.exceptions import PaginationTimeoutError
|
|
from arcade_slack.models import (
|
|
ConcurrencySafeCoroutineCaller,
|
|
FindMultipleUsersByUsernameSentinel,
|
|
FindUserByUsernameSentinel,
|
|
)
|
|
from arcade_slack.utils import (
|
|
async_paginate,
|
|
build_multiple_users_retrieval_response,
|
|
filter_conversations_by_user_ids,
|
|
gather_with_concurrency_limit,
|
|
is_valid_email,
|
|
populate_users_in_messages,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_paginate():
|
|
mock_slack_client = AsyncMock()
|
|
mock_slack_client.conversations_list.return_value = {
|
|
"ok": True,
|
|
"channels": [{"id": "123"}],
|
|
"response_metadata": {"next_cursor": None},
|
|
}
|
|
|
|
results, next_cursor = await async_paginate(
|
|
func=mock_slack_client.conversations_list,
|
|
response_key="channels",
|
|
)
|
|
|
|
assert results == [{"id": "123"}]
|
|
assert next_cursor is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_paginate_with_find_user_sentinel():
|
|
mock_slack_client = AsyncMock()
|
|
mock_slack_client.users_list.side_effect = [
|
|
{
|
|
"ok": True,
|
|
"members": [
|
|
{"id": "123", "name": "Jack"},
|
|
{"id": "456", "name": "John"},
|
|
],
|
|
"response_metadata": {"next_cursor": "cursor1"},
|
|
},
|
|
{
|
|
"ok": True,
|
|
"members": [{"id": "789", "name": "Jenifer"}],
|
|
"response_metadata": {"next_cursor": "cursor2"},
|
|
},
|
|
{
|
|
"ok": True,
|
|
"members": [{"id": "007", "name": "James"}],
|
|
"response_metadata": {"next_cursor": None},
|
|
},
|
|
]
|
|
|
|
results, next_cursor = await async_paginate(
|
|
func=mock_slack_client.users_list,
|
|
response_key="members",
|
|
sentinel=FindUserByUsernameSentinel(username="jenifer"),
|
|
)
|
|
|
|
assert results == [
|
|
{"id": "123", "name": "Jack"},
|
|
{"id": "456", "name": "John"},
|
|
{"id": "789", "name": "Jenifer"},
|
|
]
|
|
assert next_cursor == "cursor2"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_paginate_with_find_user_sentinel_not_found():
|
|
mock_slack_client = AsyncMock()
|
|
mock_slack_client.users_list.side_effect = [
|
|
{
|
|
"ok": True,
|
|
"members": [
|
|
{"id": "123", "name": "Jack"},
|
|
{"id": "456", "name": "John"},
|
|
],
|
|
"response_metadata": {"next_cursor": "cursor1"},
|
|
},
|
|
{
|
|
"ok": True,
|
|
"members": [{"id": "789", "name": "Jenifer"}],
|
|
"response_metadata": {"next_cursor": "cursor2"},
|
|
},
|
|
{
|
|
"ok": True,
|
|
"members": [{"id": "007", "name": "James"}],
|
|
"response_metadata": {"next_cursor": None},
|
|
},
|
|
]
|
|
|
|
results, next_cursor = await async_paginate(
|
|
func=mock_slack_client.users_list,
|
|
response_key="members",
|
|
sentinel=FindUserByUsernameSentinel(username="Do not find me"),
|
|
)
|
|
|
|
assert results == [
|
|
{"id": "123", "name": "Jack"},
|
|
{"id": "456", "name": "John"},
|
|
{"id": "789", "name": "Jenifer"},
|
|
{"id": "007", "name": "James"},
|
|
]
|
|
assert next_cursor is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_paginate_with_find_multiple_users_sentinel():
|
|
mock_slack_client = AsyncMock()
|
|
mock_slack_client.users_list.side_effect = [
|
|
{
|
|
"ok": True,
|
|
"members": [
|
|
{"id": "123", "name": "Jack"},
|
|
{"id": "456", "name": "John"},
|
|
],
|
|
"response_metadata": {"next_cursor": "cursor1"},
|
|
},
|
|
{
|
|
"ok": True,
|
|
"members": [
|
|
{"id": "789", "name": "Jenifer"},
|
|
{"id": "101", "name": "Janis"},
|
|
],
|
|
"response_metadata": {"next_cursor": "cursor2"},
|
|
},
|
|
{
|
|
"ok": True,
|
|
"members": [{"id": "007", "name": "James"}],
|
|
"response_metadata": {"next_cursor": None},
|
|
},
|
|
]
|
|
|
|
results, next_cursor = await async_paginate(
|
|
func=mock_slack_client.users_list,
|
|
response_key="members",
|
|
sentinel=FindMultipleUsersByUsernameSentinel(usernames=["jenifer", "jack"]),
|
|
)
|
|
|
|
assert results == [
|
|
{"id": "123", "name": "Jack"},
|
|
{"id": "456", "name": "John"},
|
|
{"id": "789", "name": "Jenifer"},
|
|
{"id": "101", "name": "Janis"},
|
|
]
|
|
assert next_cursor == "cursor2"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_paginate_with_find_multiple_users_sentinel_not_found():
|
|
mock_slack_client = AsyncMock()
|
|
mock_slack_client.users_list.side_effect = [
|
|
{
|
|
"ok": True,
|
|
"members": [
|
|
{"id": "123", "name": "Jack"},
|
|
{"id": "456", "name": "John"},
|
|
],
|
|
"response_metadata": {"next_cursor": "cursor1"},
|
|
},
|
|
{
|
|
"ok": True,
|
|
"members": [
|
|
{"id": "789", "name": "Jenifer"},
|
|
{"id": "101", "name": "Janis"},
|
|
],
|
|
"response_metadata": {"next_cursor": "cursor2"},
|
|
},
|
|
{
|
|
"ok": True,
|
|
"members": [{"id": "007", "name": "James"}],
|
|
"response_metadata": {"next_cursor": None},
|
|
},
|
|
]
|
|
|
|
results, next_cursor = await async_paginate(
|
|
func=mock_slack_client.users_list,
|
|
response_key="members",
|
|
sentinel=FindMultipleUsersByUsernameSentinel(
|
|
usernames=["jenifer", "jack", "do not find me"]
|
|
),
|
|
)
|
|
|
|
assert results == [
|
|
{"id": "123", "name": "Jack"},
|
|
{"id": "456", "name": "John"},
|
|
{"id": "789", "name": "Jenifer"},
|
|
{"id": "101", "name": "Janis"},
|
|
{"id": "007", "name": "James"},
|
|
]
|
|
assert next_cursor is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_paginate_with_response_error():
|
|
mock_slack_client = AsyncMock()
|
|
mock_slack_client.conversations_list.side_effect = SlackApiError(
|
|
message="slack_error",
|
|
response={"ok": False, "error": "slack_error"},
|
|
)
|
|
|
|
with pytest.raises(SlackApiError) as e:
|
|
await async_paginate(
|
|
func=mock_slack_client.conversations_list,
|
|
response_key="channels",
|
|
)
|
|
assert str(e.value) == "slack_error"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_paginate_with_custom_pagination_args():
|
|
mock_slack_client = AsyncMock()
|
|
mock_slack_client.conversations_list.return_value = {
|
|
"ok": True,
|
|
"channels": [{"id": "123"}],
|
|
"response_metadata": {"next_cursor": "456"},
|
|
}
|
|
|
|
results, next_cursor = await async_paginate(
|
|
func=mock_slack_client.conversations_list,
|
|
response_key="channels",
|
|
limit=1,
|
|
next_cursor="123",
|
|
hello="world",
|
|
)
|
|
|
|
assert results == [{"id": "123"}]
|
|
assert next_cursor == "456"
|
|
|
|
mock_slack_client.conversations_list.assert_called_once_with(
|
|
hello="world",
|
|
limit=1,
|
|
cursor="123",
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"test_limit, last_next_cursor, last_expected_limit",
|
|
[(5, "cursor3", 1), (None, None, 2)],
|
|
)
|
|
async def test_async_paginate_large_limit(test_limit, last_next_cursor, last_expected_limit):
|
|
mock_slack_client = AsyncMock(spec=AsyncWebClient)
|
|
mock_slack_client.conversations_list.side_effect = [
|
|
{
|
|
"ok": True,
|
|
"channels": [{"id": "channel1"}, {"id": "channel2"}],
|
|
"response_metadata": {"next_cursor": "cursor1"},
|
|
},
|
|
{
|
|
"ok": True,
|
|
"channels": [{"id": "channel3"}, {"id": "channel4"}],
|
|
"response_metadata": {"next_cursor": "cursor2"},
|
|
},
|
|
{
|
|
"ok": True,
|
|
"channels": [{"id": "channel5"}],
|
|
"response_metadata": {"next_cursor": last_next_cursor},
|
|
},
|
|
]
|
|
|
|
with patch("arcade_slack.utils.MAX_PAGINATION_SIZE_LIMIT", 2):
|
|
results, next_cursor = await async_paginate(
|
|
func=mock_slack_client.conversations_list,
|
|
response_key="channels",
|
|
limit=test_limit,
|
|
hello="world",
|
|
)
|
|
|
|
assert results == [
|
|
{"id": "channel1"},
|
|
{"id": "channel2"},
|
|
{"id": "channel3"},
|
|
{"id": "channel4"},
|
|
{"id": "channel5"},
|
|
]
|
|
assert next_cursor == last_next_cursor
|
|
assert mock_slack_client.conversations_list.call_count == 3
|
|
mock_slack_client.conversations_list.assert_has_calls([
|
|
call(hello="world", limit=2, cursor=None),
|
|
call(hello="world", limit=2, cursor="cursor1"),
|
|
call(hello="world", limit=last_expected_limit, cursor="cursor2"),
|
|
])
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_paginate_large_limit_with_response_error():
|
|
mock_slack_client = AsyncMock()
|
|
mock_slack_client.conversations_list.side_effect = [
|
|
{
|
|
"ok": True,
|
|
"channels": [{"id": "channel1"}, {"id": "channel2"}],
|
|
"response_metadata": {"next_cursor": "cursor1"},
|
|
},
|
|
SlackApiError(message="slack_error", response={"ok": False, "error": "slack_error"}),
|
|
{
|
|
"ok": True,
|
|
"channels": [{"id": "channel5"}],
|
|
"response_metadata": {"next_cursor": "cursor3"},
|
|
},
|
|
]
|
|
|
|
with (
|
|
patch("arcade_slack.utils.MAX_PAGINATION_SIZE_LIMIT", 2),
|
|
pytest.raises(SlackApiError) as e,
|
|
):
|
|
await async_paginate(
|
|
func=mock_slack_client.conversations_list,
|
|
response_key="channels",
|
|
limit=5,
|
|
hello="world",
|
|
)
|
|
assert str(e.value) == "slack_error"
|
|
|
|
assert mock_slack_client.conversations_list.call_count == 2
|
|
mock_slack_client.conversations_list.assert_has_calls([
|
|
call(hello="world", limit=2, cursor=None),
|
|
call(hello="world", limit=2, cursor="cursor1"),
|
|
])
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_async_paginate_with_timeout():
|
|
# Mock Slack client
|
|
mock_slack_client = AsyncMock()
|
|
|
|
# Simulate a network delay by making the mock function sleep
|
|
async def mock_conversations_list(*args, **kwargs):
|
|
await asyncio.sleep(1) # Sleep for 1 second to simulate delay
|
|
return {
|
|
"ok": True,
|
|
"channels": [{"id": "123"}],
|
|
"response_metadata": {"next_cursor": None},
|
|
}
|
|
|
|
mock_slack_client.conversations_list.side_effect = mock_conversations_list
|
|
|
|
# Set a low timeout to trigger the timeout error quickly during the test
|
|
max_pagination_timeout_seconds = 0.1 # 100 milliseconds
|
|
|
|
with pytest.raises(PaginationTimeoutError) as exc_info:
|
|
await async_paginate(
|
|
func=mock_slack_client.conversations_list,
|
|
response_key="channels",
|
|
max_pagination_timeout_seconds=max_pagination_timeout_seconds,
|
|
)
|
|
|
|
assert (
|
|
str(exc_info.value)
|
|
== f"The pagination process timed out after {max_pagination_timeout_seconds} seconds."
|
|
)
|
|
|
|
|
|
def test_filter_conversations_by_user_ids():
|
|
conversations = [
|
|
{"id": "123", "members": [{"id": "user1"}, {"id": "user2"}, {"id": "user3"}]},
|
|
{"id": "456", "members": [{"id": "user2"}, {"id": "user3"}]},
|
|
]
|
|
response = filter_conversations_by_user_ids(
|
|
conversations=conversations,
|
|
user_ids=["user1", "user2"],
|
|
exact_match=False,
|
|
)
|
|
assert response == [
|
|
{"id": "123", "members": [{"id": "user1"}, {"id": "user2"}, {"id": "user3"}]},
|
|
]
|
|
|
|
|
|
def test_filter_conversations_by_user_ids_empty_response():
|
|
conversations = [
|
|
{"id": "123", "members": [{"id": "user1"}, {"id": "user3"}, {"id": "user4"}]},
|
|
{"id": "456", "members": [{"id": "user2"}, {"id": "user3"}, {"id": "user4"}]},
|
|
]
|
|
response = filter_conversations_by_user_ids(
|
|
conversations=conversations,
|
|
user_ids=["user1", "user2"],
|
|
exact_match=False,
|
|
)
|
|
assert response == []
|
|
|
|
|
|
def test_filter_conversations_by_user_ids_multiple_matches():
|
|
conversations = [
|
|
{"id": "123", "members": [{"id": "user1"}, {"id": "user2"}, {"id": "user3"}]},
|
|
{"id": "456", "members": [{"id": "user2"}, {"id": "user3"}]},
|
|
{
|
|
"id": "789",
|
|
"members": [{"id": "user4"}, {"id": "user1"}, {"id": "user2"}, {"id": "user3"}],
|
|
},
|
|
]
|
|
response = filter_conversations_by_user_ids(
|
|
conversations=conversations,
|
|
user_ids=["user1", "user2"],
|
|
exact_match=False,
|
|
)
|
|
assert response == [
|
|
{"id": "123", "members": [{"id": "user1"}, {"id": "user2"}, {"id": "user3"}]},
|
|
{
|
|
"id": "789",
|
|
"members": [{"id": "user4"}, {"id": "user1"}, {"id": "user2"}, {"id": "user3"}],
|
|
},
|
|
]
|
|
|
|
|
|
def test_filter_conversations_by_user_ids_exact_match():
|
|
conversations = [
|
|
{"id": "123", "members": [{"id": "user1"}, {"id": "user2"}]},
|
|
{"id": "456", "members": [{"id": "user2"}, {"id": "user3"}]},
|
|
]
|
|
response = filter_conversations_by_user_ids(
|
|
conversations=conversations,
|
|
user_ids=["user1", "user2"],
|
|
exact_match=True,
|
|
)
|
|
assert response == [{"id": "123", "members": [{"id": "user1"}, {"id": "user2"}]}]
|
|
|
|
|
|
def test_filter_conversations_by_user_ids_exact_match_empty_response():
|
|
conversations = [
|
|
{"id": "123", "members": [{"id": "user1"}, {"id": "user2"}, {"id": "user3"}]},
|
|
{"id": "456", "members": [{"id": "user2"}, {"id": "user3"}]},
|
|
]
|
|
response = filter_conversations_by_user_ids(
|
|
conversations=conversations,
|
|
user_ids=["user1", "user2"],
|
|
exact_match=True,
|
|
)
|
|
assert response == []
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"users_by_email, users_by_username, expected_response",
|
|
[
|
|
(
|
|
{"users": [{"id": "U1", "name": "user1"}]},
|
|
{"users": [{"id": "U2", "name": "user2"}]},
|
|
[{"id": "U1", "name": "user1"}, {"id": "U2", "name": "user2"}],
|
|
),
|
|
(
|
|
{"users": [{"id": "U1", "name": "user1"}]},
|
|
{"users": []},
|
|
[{"id": "U1", "name": "user1"}],
|
|
),
|
|
(
|
|
{"users": []},
|
|
{"users": [{"id": "U2", "name": "user2"}]},
|
|
[{"id": "U2", "name": "user2"}],
|
|
),
|
|
(
|
|
{"users": []},
|
|
{"users": []},
|
|
[],
|
|
),
|
|
],
|
|
)
|
|
@pytest.mark.asyncio
|
|
async def test_build_multiple_users_retrieval_response_success(
|
|
users_by_email,
|
|
users_by_username,
|
|
expected_response,
|
|
mock_context,
|
|
):
|
|
response = await build_multiple_users_retrieval_response(
|
|
context=mock_context,
|
|
users_responses=[users_by_email, users_by_username],
|
|
)
|
|
assert response == expected_response
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"users_by_email, users_by_username",
|
|
[
|
|
# Both emails and usernames not found
|
|
(
|
|
{
|
|
"users": [{"id": "U1", "name": "user1"}],
|
|
"not_found": ["email_not_found"],
|
|
},
|
|
{
|
|
"users": [{"id": "U2", "name": "user2"}],
|
|
"not_found": ["username_not_found"],
|
|
"available_users": [{"id": "U3", "name": "user3"}],
|
|
},
|
|
),
|
|
# Email not found, usernames found
|
|
(
|
|
{
|
|
"users": [{"id": "U1", "name": "user1"}],
|
|
"not_found": ["email_not_found"],
|
|
},
|
|
{
|
|
"users": [{"id": "U2", "name": "user2"}],
|
|
"not_found": [],
|
|
},
|
|
),
|
|
# Email found, username not found
|
|
(
|
|
{
|
|
"users": [{"id": "U1", "name": "user1"}],
|
|
"not_found": [],
|
|
},
|
|
{
|
|
"users": [{"id": "U2", "name": "user2"}],
|
|
"not_found": ["username_not_found"],
|
|
"available_users": [{"id": "U3", "name": "user3"}],
|
|
},
|
|
),
|
|
],
|
|
)
|
|
@pytest.mark.asyncio
|
|
async def test_build_multiple_users_retrieval_response_not_found(
|
|
users_by_email,
|
|
users_by_username,
|
|
mock_context,
|
|
):
|
|
with pytest.raises(RetryableToolError) as error:
|
|
await build_multiple_users_retrieval_response(
|
|
context=mock_context,
|
|
users_responses=[users_by_email, users_by_username],
|
|
)
|
|
|
|
emails_not_found = users_by_email.get("not_found", [])
|
|
usernames_not_found = users_by_username.get("not_found", [])
|
|
available_users = users_by_username.get("available_users", [])
|
|
|
|
for email in emails_not_found:
|
|
assert email in error.value.message
|
|
for username in usernames_not_found:
|
|
assert username in error.value.message
|
|
for user in available_users:
|
|
assert json.dumps(user) in error.value.additional_prompt_content
|
|
|
|
|
|
def test_is_valid_email():
|
|
assert is_valid_email("test@example.com")
|
|
assert is_valid_email("test+123@example.com")
|
|
assert is_valid_email("test-123@example.com")
|
|
assert is_valid_email("test_123@example.com")
|
|
assert is_valid_email("test.123@example.com")
|
|
assert is_valid_email("test123@example.com")
|
|
assert is_valid_email("test@example.co")
|
|
assert is_valid_email("test@example.com.co")
|
|
assert not is_valid_email("test123@example")
|
|
assert not is_valid_email("test@example")
|
|
assert not is_valid_email("test@example.")
|
|
assert not is_valid_email("test@.com")
|
|
assert not is_valid_email("test@example.c")
|
|
assert not is_valid_email("test@example.com.")
|
|
assert not is_valid_email("test@example.com.c")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_gather_with_concurrency_limit():
|
|
mock_func1 = AsyncMock()
|
|
mock_func2 = AsyncMock()
|
|
|
|
caller1 = ConcurrencySafeCoroutineCaller(mock_func1, "arg1", "arg2", kwarg1="kwarg1")
|
|
caller2 = ConcurrencySafeCoroutineCaller(mock_func2, "arg1", "arg2", kwarg1="kwarg1")
|
|
|
|
mock_semaphore = AsyncMock(spec=asyncio.Semaphore)
|
|
|
|
response = await gather_with_concurrency_limit(
|
|
coroutine_callers=[caller1, caller2],
|
|
semaphore=mock_semaphore,
|
|
)
|
|
|
|
response = tuple(response)
|
|
|
|
assert len(response) == 2
|
|
assert response[0] == mock_func1.return_value
|
|
assert response[1] == mock_func2.return_value
|
|
|
|
mock_func1.assert_awaited_once_with("arg1", "arg2", kwarg1="kwarg1")
|
|
mock_func2.assert_awaited_once_with("arg1", "arg2", kwarg1="kwarg1")
|
|
|
|
assert mock_semaphore.__aenter__.await_count == 2
|
|
assert mock_semaphore.__aexit__.await_count == 2
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_populate_users_in_messages(
|
|
mock_context,
|
|
mock_user_retrieval_slack_client,
|
|
dummy_message_factory,
|
|
dummy_reaction_factory,
|
|
dummy_user_factory,
|
|
):
|
|
user1 = dummy_user_factory(id_="U1", name="user1")
|
|
user2 = dummy_user_factory(id_="U2", name="user2")
|
|
user3 = dummy_user_factory(id_="U3", name="user3")
|
|
user4 = dummy_user_factory(id_="U4", name="user4")
|
|
user5 = dummy_user_factory(id_="U5", name="user5")
|
|
|
|
user1_short = {"id": user1["id"], "name": user1["name"]}
|
|
user2_short = {"id": user2["id"], "name": user2["name"]}
|
|
user3_short = {"id": user3["id"], "name": user3["name"]}
|
|
user4_short = {"id": user4["id"], "name": user4["name"]}
|
|
|
|
user2_mention = f"<@{user2['name']} (id:{user2['id']})>"
|
|
user5_mention = f"<@{user5['name']} (id:{user5['id']})>"
|
|
|
|
reactions = [
|
|
dummy_reaction_factory(name="thumbsup", user_ids=[user1["id"], user2["id"]]),
|
|
dummy_reaction_factory(name="partyparrot", user_ids=[user3["id"], user4["id"]]),
|
|
]
|
|
|
|
messages = [
|
|
dummy_message_factory(
|
|
user_id=user1["id"],
|
|
text=f"Hello <@{user2['id']}>",
|
|
),
|
|
dummy_message_factory(
|
|
user_id=user2["id"],
|
|
text="foobar",
|
|
reactions=copy.deepcopy(reactions[:1]),
|
|
),
|
|
dummy_message_factory(
|
|
user_id=user3["id"],
|
|
text=f"Is this @{user5['id']} a user mention?",
|
|
),
|
|
dummy_message_factory(
|
|
user_id=user4["id"],
|
|
text="hello",
|
|
reactions=copy.deepcopy(reactions),
|
|
),
|
|
]
|
|
|
|
mock_user_retrieval_slack_client.users_list.side_effect = [
|
|
{
|
|
"ok": True,
|
|
"members": [user1, user2, user3],
|
|
"response_metadata": {"next_cursor": "cursor1"},
|
|
},
|
|
{
|
|
"ok": True,
|
|
"members": [user4, user5],
|
|
"response_metadata": {"next_cursor": None},
|
|
},
|
|
]
|
|
|
|
response = await populate_users_in_messages(
|
|
auth_token=mock_context.get_auth_token_or_empty(),
|
|
messages=messages,
|
|
)
|
|
|
|
msg1 = response[0]
|
|
msg2 = response[1]
|
|
msg3 = response[2]
|
|
msg4 = response[3]
|
|
|
|
assert msg1["user"] == user1_short
|
|
assert msg1["text"] == f"Hello {user2_mention}"
|
|
assert "reactions" not in msg1
|
|
|
|
assert msg2["user"] == user2_short
|
|
assert msg2["text"] == "foobar"
|
|
assert "reactions" in msg2
|
|
assert len(msg2["reactions"]) == 1
|
|
assert msg2["reactions"][0]["name"] == "thumbsup"
|
|
assert msg2["reactions"][0]["users"] == [user1_short, user2_short]
|
|
|
|
assert msg3["user"] == user3_short
|
|
assert msg3["text"] == f"Is this @{user5['id']} a user mention?"
|
|
assert "reactions" not in msg3
|
|
assert user5_mention not in msg3["text"]
|
|
|
|
assert msg4["user"] == user4_short
|
|
assert msg4["text"] == "hello"
|
|
assert "reactions" in msg4
|
|
assert len(msg4["reactions"]) == 2
|
|
assert msg4["reactions"][0]["name"] == "thumbsup"
|
|
assert msg4["reactions"][0]["users"] == [user1_short, user2_short]
|
|
assert msg4["reactions"][1]["name"] == "partyparrot"
|
|
assert msg4["reactions"][1]["users"] == [user3_short, user4_short]
|