mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-21 16:05:19 +08:00
201 lines
8.2 KiB
Python
201 lines
8.2 KiB
Python
import asyncio
|
|
import json
|
|
import time
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock
|
|
from urllib.parse import urlencode
|
|
|
|
import pytest
|
|
from slack_sdk.signature import SignatureVerifier
|
|
|
|
from langbot.libs.slack_api.api import SlackClient
|
|
from langbot.libs.slack_api.slackevent import SlackEvent
|
|
from langbot.pkg.platform.adapters.slack.event_converter import SlackEventConverter
|
|
from langbot.pkg.api.http.service.slack_setup import manifest
|
|
from langbot.libs.slack_api.media import send_chain
|
|
from langbot_plugin.api.entities.builtin.platform import message as pm
|
|
|
|
|
|
def client():
|
|
return SlackClient('test', 'secret', SimpleNamespace(error=AsyncMock(), warning=AsyncMock()), True)
|
|
|
|
|
|
async def test_thread_text_and_file_use_the_same_channel_and_thread():
|
|
bot = client()
|
|
bot.client = SimpleNamespace(
|
|
chat_postMessage=AsyncMock(return_value={'ok': True, 'ts': '2.1'}),
|
|
files_upload_v2=AsyncMock(return_value={'ok': True, 'files': [{'id': 'F1'}]}),
|
|
)
|
|
await send_chain(
|
|
bot,
|
|
'channel',
|
|
'D1',
|
|
pm.MessageChain(
|
|
[
|
|
pm.Plain(text='reply'),
|
|
pm.File(name='test.txt', base64='dGVzdA=='),
|
|
]
|
|
),
|
|
thread_ts='1.1',
|
|
)
|
|
bot.client.chat_postMessage.assert_awaited_once_with(channel='D1', text='reply', thread_ts='1.1')
|
|
assert bot.client.files_upload_v2.call_args.kwargs['thread_ts'] == '1.1'
|
|
assert bot.client.files_upload_v2.call_args.kwargs['channel'] == 'D1'
|
|
|
|
|
|
async def test_http_backpressure_does_not_mark_event_seen():
|
|
bot = client()
|
|
for index in range(100):
|
|
assert bot._enqueue({'index': index}, str(index))
|
|
assert not bot._enqueue({'event_id': 'overflow'}, 'overflow')
|
|
assert 'overflow' not in bot.socket_seen
|
|
await bot.close_socket()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
('raw', 'expected'),
|
|
[
|
|
(
|
|
{'type': 'message', 'subtype': 'message_changed', 'message': {'ts': '1.1', 'text': 'new', 'user': 'U1'}},
|
|
'message.edited',
|
|
),
|
|
({'type': 'message', 'subtype': 'message_deleted', 'deleted_ts': '1.1'}, 'message.deleted'),
|
|
(
|
|
{'type': 'reaction_added', 'user': 'U1', 'reaction': 'thumbsup', 'item': {'type': 'message', 'ts': '1.1'}},
|
|
'message.reaction',
|
|
),
|
|
(
|
|
{
|
|
'type': 'reaction_removed',
|
|
'user': 'U1',
|
|
'reaction': 'thumbsup',
|
|
'item': {'type': 'message', 'ts': '1.1'},
|
|
},
|
|
'message.reaction',
|
|
),
|
|
({'type': 'member_joined_channel', 'user': 'U1'}, 'group.member_joined'),
|
|
({'type': 'member_left_channel', 'user': 'U1'}, 'group.member_left'),
|
|
({'type': 'member_joined_channel', 'user': 'UBOT', 'inviter': 'U1'}, 'bot.invited_to_group'),
|
|
({'type': 'member_left_channel', 'user': 'UBOT'}, 'bot.removed_from_group'),
|
|
({'type': 'channel_rename', 'channel': {'id': 'C1', 'name': 'new'}}, 'group.info_updated'),
|
|
({'type': 'group_archive'}, 'group.info_updated'),
|
|
({'type': 'message', 'subtype': 'channel_topic', 'topic': 'new'}, 'group.info_updated'),
|
|
({'type': 'app_home_opened', 'user': 'U1', 'tab': 'home'}, 'platform.specific'),
|
|
({'type': 'app_uninstalled'}, 'platform.specific'),
|
|
({'type': 'tokens_revoked', 'tokens': {'bot': ['UBOT']}}, 'platform.specific'),
|
|
],
|
|
)
|
|
async def test_standard_events(raw, expected):
|
|
event = await SlackEventConverter(bot_user_id='UBOT').target2yiri(
|
|
SlackEvent({'event_id': 'delivery', 'event': {'channel': 'C1', 'channel_type': 'channel', **raw}})
|
|
)
|
|
assert event.type == expected
|
|
if expected.startswith('message.'):
|
|
assert event.message_id == '1.1'
|
|
assert event.chat_id == 'C1'
|
|
if expected == 'message.reaction':
|
|
assert event.is_add == (raw['type'] == 'reaction_added')
|
|
if expected == 'message.deleted':
|
|
assert event.operator is None
|
|
if expected == 'message.edited':
|
|
assert 'new' in str(event.new_content)
|
|
if raw['type'] == 'channel_rename':
|
|
assert event.group.name == 'new'
|
|
assert event.changed_fields == ['name']
|
|
|
|
|
|
async def test_channel_messages_are_not_duplicate_mentions_or_new_messages_on_edit():
|
|
bot = client()
|
|
bot._handle_message = AsyncMock()
|
|
await bot.handle_payload({'event': {'type': 'message', 'channel_type': 'channel', 'text': '<@BOT> hello'}})
|
|
bot._handle_message.assert_not_awaited()
|
|
await bot.handle_payload({'event': {'type': 'app_mention', 'channel': 'C1', 'text': '<@BOT> hello'}})
|
|
assert bot._handle_message.await_count == 1
|
|
await bot.handle_payload({'event': {'type': 'message', 'channel_type': 'im', 'subtype': 'message_changed'}})
|
|
assert bot._handle_message.call_args.args[1] == 'event'
|
|
|
|
|
|
@pytest.mark.parametrize('kind', ['block_actions', 'view_submission', 'view_closed', 'shortcut', 'message_action'])
|
|
async def test_interactions_dispatched_and_sanitized(kind):
|
|
bot = client()
|
|
handler = AsyncMock()
|
|
bot.on_message('event')(handler)
|
|
payload = {'type': kind, 'token': 'secret', 'response_url': 'secret-url', 'user': {'id': 'U1'}}
|
|
await bot.handle_payload(payload)
|
|
event = await SlackEventConverter().target2yiri(handler.call_args.args[0])
|
|
assert event.action == f'slack.{kind}'
|
|
assert 'token' not in event.data and 'response_url' not in event.data
|
|
|
|
|
|
async def test_http_signature_fast_ack_form_and_retry_dedup():
|
|
bot = client()
|
|
gate = asyncio.Event()
|
|
|
|
async def process(_):
|
|
await gate.wait()
|
|
|
|
bot.handle_payload = AsyncMock(side_effect=process)
|
|
|
|
def request(body, mime):
|
|
stamp = str(int(time.time()))
|
|
return SimpleNamespace(
|
|
get_data=AsyncMock(return_value=body),
|
|
mimetype=mime,
|
|
headers={
|
|
'X-Slack-Request-Timestamp': stamp,
|
|
'X-Slack-Signature': SignatureVerifier('secret').generate_signature(timestamp=stamp, body=body),
|
|
},
|
|
)
|
|
|
|
body = urlencode({'payload': json.dumps({'type': 'block_actions'})}).encode()
|
|
try:
|
|
async with bot.app.app_context():
|
|
assert await bot._handle_callback_internal(request(body, 'application/x-www-form-urlencoded')) == ('', 200)
|
|
invalid = request(body, 'application/x-www-form-urlencoded')
|
|
invalid.headers['X-Slack-Signature'] = 'invalid'
|
|
assert (await bot._handle_callback_internal(invalid))[1] == 401
|
|
event_body = json.dumps({'event_id': 'E1', 'event': {'type': 'app_home_opened'}}).encode()
|
|
await bot._handle_callback_internal(request(event_body, 'application/json'))
|
|
await bot._handle_callback_internal(request(event_body, 'application/json'))
|
|
assert bot.socket_queue.qsize() == 2
|
|
finally:
|
|
gate.set()
|
|
await bot.close_socket()
|
|
|
|
|
|
async def test_socket_interactivity_ack_and_delivery():
|
|
bot = client()
|
|
socket = SimpleNamespace(send_socket_mode_response=AsyncMock())
|
|
payload = {'type': 'view_submission'}
|
|
request = SimpleNamespace(type='interactive', envelope_id='E1', payload=payload)
|
|
await bot._socket_request(socket, request)
|
|
await bot._socket_request(socket, request)
|
|
assert bot.socket_queue.qsize() == 1
|
|
socket.send_socket_mode_response.assert_awaited()
|
|
assert await bot.socket_queue.get() == payload
|
|
|
|
|
|
@pytest.mark.parametrize('socket', [True, False])
|
|
def test_setup_complete_manifest(socket):
|
|
result = manifest(
|
|
'LangBot', 'https://example.com/bots/1', 'https://example.com/callback', events=True, socket_mode=socket
|
|
)
|
|
settings = result['settings']
|
|
assert {
|
|
'message.channels',
|
|
'message.groups',
|
|
'message.mpim',
|
|
'reaction_added',
|
|
'reaction_removed',
|
|
'member_joined_channel',
|
|
'member_left_channel',
|
|
'app_home_opened',
|
|
'app_uninstalled',
|
|
'tokens_revoked',
|
|
} <= set(settings['event_subscriptions']['bot_events'])
|
|
assert 'reactions:read' in result['oauth_config']['scopes']['bot']
|
|
assert settings['interactivity']['is_enabled']
|
|
assert ('request_url' in settings['interactivity']) is not socket
|
|
assert ('request_url' in settings['event_subscriptions']) is not socket
|
|
assert 'interactivity' not in manifest('LangBot', '', '', events=False, socket_mode=socket)['settings']
|