diff --git a/slack_bolt/app/app.py b/slack_bolt/app/app.py index 16fd9463f..1103cfde0 100644 --- a/slack_bolt/app/app.py +++ b/slack_bolt/app/app.py @@ -30,7 +30,6 @@ from slack_bolt.listener_matcher.listener_matcher import ListenerMatcher from slack_bolt.logger import get_bolt_app_logger, get_bolt_logger from slack_bolt.logger.messages import ( - error_signing_secret_not_found, warning_client_prioritized_and_token_skipped, warning_token_skipped, error_auth_test_failure, @@ -106,9 +105,6 @@ def __init__( signing_secret = signing_secret or os.environ.get("SLACK_SIGNING_SECRET") token = token or os.environ.get("SLACK_BOT_TOKEN") - if signing_secret is None or signing_secret == "": - raise BoltError(error_signing_secret_not_found()) - self._name: str = name or inspect.stack()[1].filename.split(os.path.sep)[-1] self._signing_secret: str = signing_secret diff --git a/slack_bolt/app/async_app.py b/slack_bolt/app/async_app.py index 62adbc40e..3f9b0e94b 100644 --- a/slack_bolt/app/async_app.py +++ b/slack_bolt/app/async_app.py @@ -23,7 +23,6 @@ ) from slack_bolt.error import BoltError from slack_bolt.logger.messages import ( - error_signing_secret_not_found, warning_client_prioritized_and_token_skipped, warning_token_skipped, error_token_required, @@ -113,9 +112,6 @@ def __init__( signing_secret = signing_secret or os.environ.get("SLACK_SIGNING_SECRET") token = token or os.environ.get("SLACK_BOT_TOKEN") - if signing_secret is None or signing_secret == "": - raise BoltError(error_signing_secret_not_found()) - self._name: str = name or inspect.stack()[1].filename.split(os.path.sep)[-1] self._signing_secret: str = signing_secret self._verification_token: Optional[str] = verification_token or os.environ.get( diff --git a/slack_bolt/logger/messages.py b/slack_bolt/logger/messages.py index 29ee7bfe0..ad19f3edb 100644 --- a/slack_bolt/logger/messages.py +++ b/slack_bolt/logger/messages.py @@ -10,14 +10,6 @@ # ------------------------------- -def error_signing_secret_not_found() -> str: - return ( - "Signing secret not found, so could not initialize the Bolt app." - "Copy your Signing Secret from the Basic Information page " - "and then store it in a new environment variable" - ) - - def error_client_invalid_type() -> str: return "`client` must be a slack_sdk.web.WebClient" diff --git a/slack_bolt/middleware/request_verification/async_request_verification.py b/slack_bolt/middleware/request_verification/async_request_verification.py index c62041b89..7fde73ddd 100644 --- a/slack_bolt/middleware/request_verification/async_request_verification.py +++ b/slack_bolt/middleware/request_verification/async_request_verification.py @@ -14,7 +14,7 @@ async def async_process( resp: BoltResponse, next: Callable[[], Awaitable[BoltResponse]], ) -> BoltResponse: - if self._can_skip(req.body): + if self._can_skip(req.mode, req.body): return await next() body = req.raw_body diff --git a/slack_bolt/middleware/request_verification/request_verification.py b/slack_bolt/middleware/request_verification/request_verification.py index 8c91e65f2..538441070 100644 --- a/slack_bolt/middleware/request_verification/request_verification.py +++ b/slack_bolt/middleware/request_verification/request_verification.py @@ -21,7 +21,7 @@ def __init__(self, signing_secret: str): def process( self, *, req: BoltRequest, resp: BoltResponse, next: Callable[[], BoltResponse], ) -> BoltResponse: - if self._can_skip(req.body): + if self._can_skip(req.mode, req.body): return next() body = req.raw_body @@ -36,8 +36,10 @@ def process( # ----------------------------------------- @staticmethod - def _can_skip(body: Dict[str, Any]) -> bool: - return body is not None and body.get("ssl_check") == "1" + def _can_skip(mode: str, body: Dict[str, Any]) -> bool: + return mode == "socket_mode" or ( + body is not None and body.get("ssl_check") == "1" + ) @staticmethod def _build_error_response() -> BoltResponse: diff --git a/slack_bolt/request/async_request.py b/slack_bolt/request/async_request.py index 032beaafd..d1c03cbe0 100644 --- a/slack_bolt/request/async_request.py +++ b/slack_bolt/request/async_request.py @@ -1,12 +1,15 @@ from typing import Dict, Optional, Union, Any, Sequence from slack_bolt.context.async_context import AsyncBoltContext +from slack_bolt.error import BoltError from slack_bolt.request.async_internals import build_async_context from slack_bolt.request.internals import ( parse_query, parse_body, build_normalized_headers, extract_content_type, + error_message_raw_body_required_in_http_mode, + error_message_unknown_request_body_type, ) @@ -19,27 +22,37 @@ class AsyncBoltRequest: context: AsyncBoltContext lazy_only: bool lazy_function_name: Optional[str] + mode: str # either "http" or "socket_mode" def __init__( self, *, - body: str, + body: Union[str, dict], query: Optional[Union[str, Dict[str, str], Dict[str, Sequence[str]]]] = None, headers: Optional[Dict[str, Union[str, Sequence[str]]]] = None, context: Optional[Dict[str, str]] = None, + mode: str = "http", # either "http" or "socket_mode" ): """Request to a Bolt app. - :param body: The raw request body (only plain text is supported) + :param body: The raw request body (only plain text is supported for "http" mode) :param query: The query string data in any data format. :param headers: The request headers. :param context: The context in this request. + :param mode: The mode used for this request. (either "http" or "socket_mode") """ - self.raw_body = body + if mode == "http" and not isinstance(body, str): + raise BoltError(error_message_raw_body_required_in_http_mode()) + self.raw_body = body if mode == "http" else "" self.query = parse_query(query) self.headers = build_normalized_headers(headers) self.content_type = extract_content_type(self.headers) - self.body = parse_body(self.raw_body, self.content_type) + if isinstance(body, str): + self.body = parse_body(self.raw_body, self.content_type) + elif isinstance(body, dict): + self.body = body + else: + raise BoltError(error_message_unknown_request_body_type()) self.context = build_async_context( AsyncBoltContext(context if context else {}), self.body ) @@ -47,3 +60,4 @@ def __init__( self.lazy_function_name = self.headers.get( "x-slack-bolt-lazy-function-name", [None] )[0] + self.mode = mode diff --git a/slack_bolt/request/internals.py b/slack_bolt/request/internals.py index 7a3ea872e..28547fdd1 100644 --- a/slack_bolt/request/internals.py +++ b/slack_bolt/request/internals.py @@ -151,3 +151,11 @@ def build_normalized_headers( f"Unsupported type ({type(value)}) of element in headers ({headers})" ) return normalized_headers # type: ignore + + +def error_message_raw_body_required_in_http_mode() -> str: + return "`body` must be a raw string data when running in the HTTP server mode" + + +def error_message_unknown_request_body_type() -> str: + return "`body` must be either str or dict" diff --git a/slack_bolt/request/request.py b/slack_bolt/request/request.py index df5b2b351..9a1129e84 100644 --- a/slack_bolt/request/request.py +++ b/slack_bolt/request/request.py @@ -1,12 +1,15 @@ from typing import Dict, Optional, Union, Any, Sequence from slack_bolt.context.context import BoltContext +from slack_bolt.error import BoltError from slack_bolt.request.internals import ( parse_query, parse_body, build_normalized_headers, build_context, extract_content_type, + error_message_raw_body_required_in_http_mode, + error_message_unknown_request_body_type, ) @@ -19,29 +22,41 @@ class BoltRequest: context: BoltContext lazy_only: bool lazy_function_name: Optional[str] + mode: str # either "http" or "socket_mode" def __init__( self, *, - body: str, + body: Union[str, dict], query: Optional[Union[str, Dict[str, str], Dict[str, Sequence[str]]]] = None, headers: Optional[Dict[str, Union[str, Sequence[str]]]] = None, context: Optional[Dict[str, str]] = None, + mode: str = "http", # either "http" or "socket_mode" ): """Request to a Bolt app. - :param body: The raw request body (only plain text is supported) + :param body: The raw request body (only plain text is supported for "http" mode) :param query: The query string data in any data format. :param headers: The request headers. :param context: The context in this request. + :param mode: The mode used for this request. (either "http" or "socket_mode") """ - self.raw_body = body + if mode == "http" and not isinstance(body, str): + raise BoltError(error_message_raw_body_required_in_http_mode()) + self.raw_body = body if mode == "http" else "" self.query = parse_query(query) self.headers = build_normalized_headers(headers) self.content_type = extract_content_type(self.headers) - self.body = parse_body(self.raw_body, self.content_type) + if isinstance(body, str): + self.body = parse_body(self.raw_body, self.content_type) + elif isinstance(body, dict): + self.body = body + else: + raise BoltError(error_message_unknown_request_body_type()) + self.context = build_context(BoltContext(context if context else {}), self.body) self.lazy_only = self.headers.get("x-slack-bolt-lazy-only", [False])[0] self.lazy_function_name = self.headers.get( "x-slack-bolt-lazy-function-name", [None] )[0] + self.mode = mode diff --git a/tests/scenario_tests/test_events_socket_mode.py b/tests/scenario_tests/test_events_socket_mode.py new file mode 100644 index 000000000..5a02f9624 --- /dev/null +++ b/tests/scenario_tests/test_events_socket_mode.py @@ -0,0 +1,358 @@ +from time import sleep + +import pytest +from slack_sdk.web import WebClient + +from slack_bolt import App, BoltRequest, Say +from slack_bolt.error import BoltError +from tests.mock_web_api_server import ( + setup_mock_web_api_server, + cleanup_mock_web_api_server, +) +from tests.utils import remove_os_env_temporarily, restore_os_env + + +class TestEventsSocketMode: + valid_token = "xoxb-valid" + mock_api_server_base_url = "http://localhost:8888" + web_client = WebClient(token=valid_token, base_url=mock_api_server_base_url,) + + def setup_method(self): + self.old_os_env = remove_os_env_temporarily() + setup_mock_web_api_server(self) + + def teardown_method(self): + cleanup_mock_web_api_server(self) + restore_os_env(self.old_os_env) + + valid_event_body = { + "token": "verification_token", + "team_id": "T111", + "enterprise_id": "E111", + "api_app_id": "A111", + "event": { + "client_msg_id": "9cbd4c5b-7ddf-4ede-b479-ad21fca66d63", + "type": "app_mention", + "text": "<@W111> Hi there!", + "user": "W222", + "ts": "1595926230.009600", + "team": "T111", + "channel": "C111", + "event_ts": "1595926230.009600", + }, + "type": "event_callback", + "event_id": "Ev111", + "event_time": 1595926230, + "authed_users": ["W111"], + } + + def test_mock_server_is_running(self): + resp = self.web_client.api_test() + assert resp != None + + def test_body_validation(self): + with pytest.raises(BoltError): + BoltRequest(body={"foo": "bar"}, mode="http") + + def test_middleware(self): + app = App(client=self.web_client) + + @app.event("app_mention") + def handle_app_mention(body, say, payload, event): + assert body == self.valid_event_body + assert body["event"] == payload + assert payload == event + say("What's up?") + + request: BoltRequest = BoltRequest( + body=self.valid_event_body, mode="socket_mode" + ) + response = app.dispatch(request) + assert response.status == 200 + assert self.mock_received_requests["/auth.test"] == 1 + sleep(1) # wait a bit after auto ack() + assert self.mock_received_requests["/chat.postMessage"] == 1 + + def test_middleware_skip(self): + app = App(client=self.web_client) + + def skip_middleware(req, resp, next): + # return next() + pass + + @app.event("app_mention", middleware=[skip_middleware]) + def handle_app_mention(body, logger, payload, event): + assert body["event"] == payload + assert payload == event + logger.info(payload) + + request: BoltRequest = BoltRequest( + body=self.valid_event_body, mode="socket_mode" + ) + response = app.dispatch(request) + assert response.status == 404 + assert self.mock_received_requests["/auth.test"] == 1 + + valid_reaction_added_body = { + "token": "verification_token", + "team_id": "T111", + "enterprise_id": "E111", + "api_app_id": "A111", + "event": { + "type": "reaction_added", + "user": "W111", + "item": {"type": "message", "channel": "C111", "ts": "1599529504.000400"}, + "reaction": "heart_eyes", + "item_user": "W111", + "event_ts": "1599616881.000800", + }, + "type": "event_callback", + "event_id": "Ev111", + "event_time": 1599616881, + "authed_users": ["W111"], + } + + def test_reaction_added(self): + app = App(client=self.web_client) + + @app.event("reaction_added") + def handle_app_mention(body, say, payload, event): + assert body == self.valid_reaction_added_body + assert body["event"] == payload + assert payload == event + say("What's up?") + + request: BoltRequest = BoltRequest( + body=self.valid_reaction_added_body, mode="socket_mode" + ) + response = app.dispatch(request) + assert response.status == 200 + assert self.mock_received_requests["/auth.test"] == 1 + sleep(1) # wait a bit after auto ack() + assert self.mock_received_requests["/chat.postMessage"] == 1 + + def test_stable_auto_ack(self): + app = App(client=self.web_client) + + @app.event("reaction_added") + def handle_app_mention(): + raise Exception("Something wrong!") + + for _ in range(10): + request: BoltRequest = BoltRequest( + body=self.valid_reaction_added_body, mode="socket_mode" + ) + response = app.dispatch(request) + assert response.status == 200 + + def test_self_events(self): + app = App(client=self.web_client) + + event_body = { + "token": "verification_token", + "team_id": "T111", + "enterprise_id": "E111", + "api_app_id": "A111", + "event": { + "type": "reaction_added", + "user": "W23456789", # bot_user_id + "item": { + "type": "message", + "channel": "C111", + "ts": "1599529504.000400", + }, + "reaction": "heart_eyes", + "item_user": "W111", + "event_ts": "1599616881.000800", + }, + "type": "event_callback", + "event_id": "Ev111", + "event_time": 1599616881, + "authed_users": ["W111"], + } + + @app.event("reaction_added") + def handle_app_mention(say): + say("What's up?") + + request: BoltRequest = BoltRequest(body=event_body, mode="socket_mode") + response = app.dispatch(request) + assert response.status == 200 + assert self.mock_received_requests["/auth.test"] == 1 + sleep(1) # wait a bit after auto ack() + # The listener should not be executed + assert self.mock_received_requests.get("/chat.postMessage") is None + + def test_self_member_join_left_events(self): + app = App(client=self.web_client) + + join_event_body = { + "token": "verification_token", + "team_id": "T111", + "enterprise_id": "E111", + "api_app_id": "A111", + "event": { + "type": "member_joined_channel", + "user": "W23456789", # bot_user_id + "channel": "C111", + "channel_type": "C", + "team": "T111", + "inviter": "U222", + }, + "type": "event_callback", + "event_id": "Ev111", + "event_time": 1599616881, + "authed_users": ["W111"], + } + + left_event_body = { + "token": "verification_token", + "team_id": "T111", + "enterprise_id": "E111", + "api_app_id": "A111", + "event": { + "type": "member_left_channel", + "user": "W23456789", # bot_user_id + "channel": "C111", + "channel_type": "C", + "team": "T111", + }, + "type": "event_callback", + "event_id": "Ev111", + "event_time": 1599616881, + "authed_users": ["W111"], + } + + @app.event("member_joined_channel") + def handle_member_joined_channel(say): + say("What's up?") + + @app.event("member_left_channel") + def handle_member_left_channel(say): + say("What's up?") + + request: BoltRequest = BoltRequest(body=join_event_body, mode="socket_mode") + response = app.dispatch(request) + assert response.status == 200 + assert self.mock_received_requests["/auth.test"] == 1 + + request: BoltRequest = BoltRequest(body=left_event_body, mode="socket_mode") + response = app.dispatch(request) + assert response.status == 200 + + sleep(1) # wait a bit after auto ack() + assert self.mock_received_requests["/chat.postMessage"] == 2 + + def test_member_join_left_events(self): + app = App(client=self.web_client) + + join_event_body = { + "token": "verification_token", + "team_id": "T111", + "enterprise_id": "E111", + "api_app_id": "A111", + "event": { + "type": "member_joined_channel", + "user": "U999", # not self + "channel": "C111", + "channel_type": "C", + "team": "T111", + "inviter": "U222", + }, + "type": "event_callback", + "event_id": "Ev111", + "event_time": 1599616881, + "authed_users": ["W111"], + } + + left_event_body = { + "token": "verification_token", + "team_id": "T111", + "enterprise_id": "E111", + "api_app_id": "A111", + "event": { + "type": "member_left_channel", + "user": "U999", # not self + "channel": "C111", + "channel_type": "C", + "team": "T111", + }, + "type": "event_callback", + "event_id": "Ev111", + "event_time": 1599616881, + "authed_users": ["W111"], + } + + @app.event("member_joined_channel") + def handle_app_mention(say): + say("What's up?") + + @app.event("member_left_channel") + def handle_app_mention(say): + say("What's up?") + + request: BoltRequest = BoltRequest(body=join_event_body, mode="socket_mode") + response = app.dispatch(request) + assert response.status == 200 + assert self.mock_received_requests["/auth.test"] == 1 + + request: BoltRequest = BoltRequest(body=left_event_body, mode="socket_mode") + response = app.dispatch(request) + assert response.status == 200 + + sleep(1) # wait a bit after auto ack() + # the listeners should not be executed + assert self.mock_received_requests["/chat.postMessage"] == 2 + + def test_uninstallation_and_revokes(self): + app = App(client=self.web_client) + app._client = WebClient( + token="uninstalled-revoked", base_url=self.mock_api_server_base_url + ) + + @app.event("app_uninstalled") + def handler1(say: Say): + say(channel="C111", text="What's up?") + + @app.event("tokens_revoked") + def handler2(say: Say): + say(channel="C111", text="What's up?") + + app_uninstalled_body = { + "token": "verification_token", + "team_id": "T111", + "enterprise_id": "E111", + "api_app_id": "A111", + "event": {"type": "app_uninstalled"}, + "type": "event_callback", + "event_id": "Ev111", + "event_time": 1599616881, + } + + request: BoltRequest = BoltRequest( + body=app_uninstalled_body, mode="socket_mode" + ) + response = app.dispatch(request) + assert response.status == 200 + + tokens_revoked_body = { + "token": "verification_token", + "team_id": "T111", + "enterprise_id": "E111", + "api_app_id": "A111", + "event": { + "type": "tokens_revoked", + "tokens": {"oauth": ["UXXXXXXXX"], "bot": ["UXXXXXXXX"]}, + }, + "type": "event_callback", + "event_id": "Ev111", + "event_time": 1599616881, + } + + request: BoltRequest = BoltRequest(body=tokens_revoked_body, mode="socket_mode") + response = app.dispatch(request) + assert response.status == 200 + + assert self.mock_received_requests["/auth.test"] == 1 + sleep(1) # wait a bit after auto ack() + assert self.mock_received_requests["/chat.postMessage"] == 2 diff --git a/tests/scenario_tests_async/test_app.py b/tests/scenario_tests_async/test_app.py index f9e38f9ab..6f9d0a8a8 100644 --- a/tests/scenario_tests_async/test_app.py +++ b/tests/scenario_tests_async/test_app.py @@ -18,12 +18,6 @@ def setup_method(self): def teardown_method(self): restore_os_env(self.old_os_env) - def test_signing_secret_absence(self): - with pytest.raises(BoltError): - AsyncApp(signing_secret=None, token="xoxb-xxx") - with pytest.raises(BoltError): - AsyncApp(signing_secret="", token="xoxb-xxx") - def non_coro_func(self, ack): ack() diff --git a/tests/scenario_tests_async/test_events_socket_mode.py b/tests/scenario_tests_async/test_events_socket_mode.py new file mode 100644 index 000000000..d668f963c --- /dev/null +++ b/tests/scenario_tests_async/test_events_socket_mode.py @@ -0,0 +1,401 @@ +import asyncio +from random import random + +import pytest +from slack_sdk.web.async_client import AsyncWebClient + +from slack_bolt.app.async_app import AsyncApp +from slack_bolt.context.say.async_say import AsyncSay +from slack_bolt.request.async_request import AsyncBoltRequest +from tests.mock_web_api_server import ( + setup_mock_web_api_server, + cleanup_mock_web_api_server, +) +from tests.utils import remove_os_env_temporarily, restore_os_env + + +class TestAsyncEvents: + valid_token = "xoxb-valid" + mock_api_server_base_url = "http://localhost:8888" + web_client = AsyncWebClient(token=valid_token, base_url=mock_api_server_base_url,) + + @pytest.fixture + def event_loop(self): + old_os_env = remove_os_env_temporarily() + try: + setup_mock_web_api_server(self) + loop = asyncio.get_event_loop() + yield loop + loop.close() + cleanup_mock_web_api_server(self) + finally: + restore_os_env(old_os_env) + + def build_valid_app_mention_request(self) -> AsyncBoltRequest: + return AsyncBoltRequest(body=app_mention_body, mode="socket_mode") + + @pytest.mark.asyncio + async def test_mock_server_is_running(self): + resp = await self.web_client.api_test() + assert resp != None + + @pytest.mark.asyncio + async def test_app_mention(self): + app = AsyncApp(client=self.web_client) + app.event("app_mention")(whats_up) + + request = self.build_valid_app_mention_request() + response = await app.async_dispatch(request) + assert response.status == 200 + assert self.mock_received_requests["/auth.test"] == 1 + await asyncio.sleep(1) # wait a bit after auto ack() + assert self.mock_received_requests["/chat.postMessage"] == 1 + + @pytest.mark.asyncio + async def test_process_before_response(self): + app = AsyncApp(client=self.web_client, process_before_response=True,) + app.event("app_mention")(whats_up) + + request = self.build_valid_app_mention_request() + response = await app.async_dispatch(request) + assert response.status == 200 + assert self.mock_received_requests["/auth.test"] == 1 + # no sleep here + assert self.mock_received_requests["/chat.postMessage"] == 1 + + @pytest.mark.asyncio + async def test_middleware_skip(self): + app = AsyncApp(client=self.web_client) + app.event("app_mention", middleware=[skip_middleware])(whats_up) + + request = self.build_valid_app_mention_request() + response = await app.async_dispatch(request) + assert response.status == 404 + assert self.mock_received_requests["/auth.test"] == 1 + + @pytest.mark.asyncio + async def test_simultaneous_requests(self): + app = AsyncApp(client=self.web_client) + app.event("app_mention")(random_sleeper) + + request = self.build_valid_app_mention_request() + + times = 10 + tasks = [] + for i in range(times): + tasks.append(asyncio.ensure_future(app.async_dispatch(request))) + + await asyncio.sleep(5) + # Verifies all the tasks have been completed with 200 OK + assert sum([t.result().status for t in tasks if t.done()]) == 200 * times + + assert self.mock_received_requests["/auth.test"] == times + assert self.mock_received_requests["/chat.postMessage"] == times + + def build_valid_reaction_added_request(self) -> AsyncBoltRequest: + return AsyncBoltRequest(body=reaction_added_body, mode="socket_mode") + + @pytest.mark.asyncio + async def test_reaction_added(self): + app = AsyncApp(client=self.web_client) + app.event("reaction_added")(whats_up) + + request = self.build_valid_reaction_added_request() + response = await app.async_dispatch(request) + assert response.status == 200 + assert self.mock_received_requests["/auth.test"] == 1 + await asyncio.sleep(1) # wait a bit after auto ack() + assert self.mock_received_requests["/chat.postMessage"] == 1 + + @pytest.mark.asyncio + async def test_stable_auto_ack(self): + app = AsyncApp(client=self.web_client) + app.event("reaction_added")(always_failing) + + for _ in range(10): + request = self.build_valid_reaction_added_request() + response = await app.async_dispatch(request) + assert response.status == 200 + + @pytest.mark.asyncio + async def test_self_events(self): + app = AsyncApp(client=self.web_client) + app.event("reaction_added")(whats_up) + + self_event = { + "token": "verification_token", + "team_id": "T111", + "enterprise_id": "E111", + "api_app_id": "A111", + "event": { + "type": "reaction_added", + "user": "W23456789", # bot_user_id + "item": { + "type": "message", + "channel": "C111", + "ts": "1599529504.000400", + }, + "reaction": "heart_eyes", + "item_user": "W111", + "event_ts": "1599616881.000800", + }, + "type": "event_callback", + "event_id": "Ev111", + "event_time": 1599616881, + "authed_users": ["W111"], + } + request = AsyncBoltRequest(body=self_event, mode="socket_mode") + response = await app.async_dispatch(request) + assert response.status == 200 + assert self.mock_received_requests["/auth.test"] == 1 + await asyncio.sleep(1) # wait a bit after auto ack() + # The listener should not be executed + assert self.mock_received_requests.get("/chat.postMessage") is None + + @pytest.mark.asyncio + async def test_self_joined_left_events(self): + app = AsyncApp(client=self.web_client) + app.event("reaction_added")(whats_up) + + join_event_body = { + "token": "verification_token", + "team_id": "T111", + "enterprise_id": "E111", + "api_app_id": "A111", + "event": { + "type": "member_joined_channel", + "user": "W23456789", # bot_user_id + "channel": "C111", + "channel_type": "C", + "team": "T111", + "inviter": "U222", + }, + "type": "event_callback", + "event_id": "Ev111", + "event_time": 1599616881, + "authed_users": ["W111"], + } + + left_event_body = { + "token": "verification_token", + "team_id": "T111", + "enterprise_id": "E111", + "api_app_id": "A111", + "event": { + "type": "member_left_channel", + "user": "W23456789", # bot_user_id + "channel": "C111", + "channel_type": "C", + "team": "T111", + }, + "type": "event_callback", + "event_id": "Ev111", + "event_time": 1599616881, + "authed_users": ["W111"], + } + + @app.event("member_joined_channel") + async def handle_member_joined_channel(say): + await say("What's up?") + + @app.event("member_left_channel") + async def handle_member_left_channel(say): + await say("What's up?") + + request = AsyncBoltRequest(body=join_event_body, mode="socket_mode") + response = await app.async_dispatch(request) + assert response.status == 200 + assert self.mock_received_requests["/auth.test"] == 1 + + request = AsyncBoltRequest(body=left_event_body, mode="socket_mode") + response = await app.async_dispatch(request) + assert response.status == 200 + + await asyncio.sleep(1) # wait a bit after auto ack() + # The listeners should be executed + assert self.mock_received_requests.get("/chat.postMessage") == 2 + + @pytest.mark.asyncio + async def test_joined_left_events(self): + app = AsyncApp(client=self.web_client) + app.event("reaction_added")(whats_up) + + join_event_body = { + "token": "verification_token", + "team_id": "T111", + "enterprise_id": "E111", + "api_app_id": "A111", + "event": { + "type": "member_joined_channel", + "user": "W111", # other user + "channel": "C111", + "channel_type": "C", + "team": "T111", + "inviter": "U222", + }, + "type": "event_callback", + "event_id": "Ev111", + "event_time": 1599616881, + "authed_users": ["W111"], + } + + left_event_body = { + "token": "verification_token", + "team_id": "T111", + "enterprise_id": "E111", + "api_app_id": "A111", + "event": { + "type": "member_left_channel", + "user": "W111", # other user + "channel": "C111", + "channel_type": "C", + "team": "T111", + }, + "type": "event_callback", + "event_id": "Ev111", + "event_time": 1599616881, + "authed_users": ["W111"], + } + + @app.event("member_joined_channel") + async def handle_member_joined_channel(say): + await say("What's up?") + + @app.event("member_left_channel") + async def handle_member_left_channel(say): + await say("What's up?") + + request = AsyncBoltRequest(body=join_event_body, mode="socket_mode") + response = await app.async_dispatch(request) + assert response.status == 200 + assert self.mock_received_requests["/auth.test"] == 1 + + request = AsyncBoltRequest(body=left_event_body, mode="socket_mode") + response = await app.async_dispatch(request) + assert response.status == 200 + + await asyncio.sleep(1) # wait a bit after auto ack() + # The listeners should be executed + assert self.mock_received_requests.get("/chat.postMessage") == 2 + + @pytest.mark.asyncio + async def test_uninstallation_and_revokes(self): + app = AsyncApp(client=self.web_client) + app._client = AsyncWebClient( + token="uninstalled-revoked", base_url=self.mock_api_server_base_url + ) + + @app.event("app_uninstalled") + async def handler1(say: AsyncSay): + await say(channel="C111", text="What's up?") + + @app.event("tokens_revoked") + async def handler2(say: AsyncSay): + await say(channel="C111", text="What's up?") + + app_uninstalled_body = { + "token": "verification_token", + "team_id": "T111", + "enterprise_id": "E111", + "api_app_id": "A111", + "event": {"type": "app_uninstalled"}, + "type": "event_callback", + "event_id": "Ev111", + "event_time": 1599616881, + } + + request: AsyncBoltRequest = AsyncBoltRequest( + body=app_uninstalled_body, mode="socket_mode" + ) + response = await app.async_dispatch(request) + assert response.status == 200 + + tokens_revoked_body = { + "token": "verification_token", + "team_id": "T111", + "enterprise_id": "E111", + "api_app_id": "A111", + "event": { + "type": "tokens_revoked", + "tokens": {"oauth": ["UXXXXXXXX"], "bot": ["UXXXXXXXX"]}, + }, + "type": "event_callback", + "event_id": "Ev111", + "event_time": 1599616881, + } + + request: AsyncBoltRequest = AsyncBoltRequest( + body=tokens_revoked_body, mode="socket_mode" + ) + response = await app.async_dispatch(request) + assert response.status == 200 + + # AsyncApp doesn't call auth.test when booting + assert self.mock_received_requests.get("/auth.test") is None + await asyncio.sleep(1) # wait a bit after auto ack() + assert self.mock_received_requests["/chat.postMessage"] == 2 + + +app_mention_body = { + "token": "verification_token", + "team_id": "T111", + "enterprise_id": "E111", + "api_app_id": "A111", + "event": { + "client_msg_id": "9cbd4c5b-7ddf-4ede-b479-ad21fca66d63", + "type": "app_mention", + "text": "<@W111> Hi there!", + "user": "W222", + "ts": "1595926230.009600", + "team": "T111", + "channel": "C111", + "event_ts": "1595926230.009600", + }, + "type": "event_callback", + "event_id": "Ev111", + "event_time": 1595926230, + "authed_users": ["W111"], +} + +reaction_added_body = { + "token": "verification_token", + "team_id": "T111", + "enterprise_id": "E111", + "api_app_id": "A111", + "event": { + "type": "reaction_added", + "user": "W111", + "item": {"type": "message", "channel": "C111", "ts": "1599529504.000400"}, + "reaction": "heart_eyes", + "item_user": "W111", + "event_ts": "1599616881.000800", + }, + "type": "event_callback", + "event_id": "Ev111", + "event_time": 1599616881, + "authed_users": ["W111"], +} + + +async def random_sleeper(body, say, payload, event): + assert body == app_mention_body + assert body["event"] == payload + assert payload == event + seconds = random() + 2 # 2-3 seconds + await asyncio.sleep(seconds) + await say(f"Sending this message after sleeping for {seconds} seconds") + + +async def whats_up(body, say, payload, event): + assert body["event"] == payload + assert payload == event + await say("What's up?") + + +async def skip_middleware(req, resp, next): + # return next() + pass + + +async def always_failing(): + raise Exception("Something wrong!")