Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions slack_bolt/app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
error_unexpected_listener_middleware,
error_client_invalid_type,
error_authorize_conflicts,
warning_bot_only_conflicts,
)
from slack_bolt.middleware import (
Middleware,
Expand Down Expand Up @@ -80,7 +81,7 @@ def __init__(
authorize: Optional[Callable[..., AuthorizeResult]] = None,
installation_store: Optional[InstallationStore] = None,
# for v1.0.x compatibility
installation_store_bot_only: bool = False,
installation_store_bot_only: Optional[bool] = None,
# for the OAuth flow
oauth_settings: Optional[OAuthSettings] = None,
oauth_flow: Optional[OAuthFlow] = None,
Expand Down Expand Up @@ -197,8 +198,13 @@ def __init__(
self._framework_logger.warning(warning_token_skipped())

# after setting bot_only here, __init__ cannot replace authorize function
if self._authorize is not None:
self._authorize.bot_only = installation_store_bot_only
if installation_store_bot_only is not None and self._oauth_flow is not None:
app_bot_only = installation_store_bot_only or False
oauth_flow_bot_only = self._oauth_flow.settings.installation_store_bot_only
if app_bot_only != oauth_flow_bot_only:
self.logger.warning(warning_bot_only_conflicts())
self._oauth_flow.settings.installation_store_bot_only = app_bot_only
self._authorize.bot_only = app_bot_only

# --------------------------------------
# Middleware Initialization
Expand Down
19 changes: 16 additions & 3 deletions slack_bolt/app/async_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
error_authorize_conflicts,
error_oauth_settings_invalid_type_async,
error_oauth_flow_invalid_type_async,
warning_bot_only_conflicts,
)
from slack_bolt.lazy_listener.asyncio_runner import AsyncioLazyListenerRunner
from slack_bolt.listener.async_listener import AsyncListener, AsyncCustomListener
Expand Down Expand Up @@ -90,7 +91,7 @@ def __init__(
client: Optional[AsyncWebClient] = None,
# for multi-workspace apps
installation_store: Optional[AsyncInstallationStore] = None,
installation_store_bot_only: bool = False,
installation_store_bot_only: Optional[bool] = None,
authorize: Optional[Callable[..., Awaitable[AuthorizeResult]]] = None,
# for the OAuth flow
oauth_settings: Optional[AsyncOAuthSettings] = None,
Expand Down Expand Up @@ -219,8 +220,20 @@ def __init__(
self._framework_logger.warning(warning_token_skipped())

# after setting bot_only here, __init__ cannot replace authorize function
if self._async_authorize is not None:
self._async_authorize.bot_only = installation_store_bot_only
if (
installation_store_bot_only is not None
and self._async_oauth_flow is not None
):
app_bot_only = installation_store_bot_only or False
oauth_flow_bot_only = (
self._async_oauth_flow.settings.installation_store_bot_only
)
if app_bot_only != oauth_flow_bot_only:
self.logger.warning(warning_bot_only_conflicts())
self._async_oauth_flow.settings.installation_store_bot_only = (
app_bot_only
)
self._async_authorize.bot_only = app_bot_only

# --------------------------------------
# Middleware Initialization
Expand Down
7 changes: 7 additions & 0 deletions slack_bolt/logger/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ def warning_did_not_call_ack(listener_name: str) -> str:
return f"{listener_name} didn't call ack()"


def warning_bot_only_conflicts() -> str:
return (
"installation_store_bot_only exists in both App and OAuthFlow.settings. "
"The one passed in App constructor is used."
)


# -------------------------------
# Info
# -------------------------------
Expand Down
12 changes: 11 additions & 1 deletion tests/scenario_tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from slack_sdk.oauth.installation_store import FileInstallationStore
from slack_sdk.oauth.state_store import FileOAuthStateStore

from slack_bolt import App
from slack_bolt import App, Say
from slack_bolt.authorization import AuthorizeResult
from slack_bolt.error import BoltError
from slack_bolt.oauth import OAuthFlow
Expand All @@ -29,6 +29,16 @@ def teardown_method(self):
cleanup_mock_web_api_server(self)
restore_os_env(self.old_os_env)

@staticmethod
def handle_app_mention(body, say: Say, payload, event):
assert body["event"] == payload
assert payload == event
say("What's up?")

# --------------------------
# basic tests
# --------------------------

def test_signing_secret_absence(self):
with pytest.raises(BoltError):
App(signing_secret=None, token="xoxb-xxx")
Expand Down
261 changes: 261 additions & 0 deletions tests/scenario_tests/test_app_bot_only.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
import datetime
import json
import logging
from time import time, sleep
from typing import Optional

from slack_sdk import WebClient
from slack_sdk.oauth import InstallationStore
from slack_sdk.oauth.installation_store import Installation, Bot
from slack_sdk.oauth.state_store import FileOAuthStateStore
from slack_sdk.signature import SignatureVerifier

from slack_bolt import App, BoltRequest, Say
from slack_bolt.oauth import OAuthFlow
from slack_bolt.oauth.oauth_settings import OAuthSettings
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 LegacyMemoryInstallationStore(InstallationStore):
@property
def logger(self) -> logging.Logger:
return logging.getLogger(__name__)

def save(self, installation: Installation):
pass

def find_bot(
self,
*,
enterprise_id: Optional[str],
team_id: Optional[str],
is_enterprise_install: Optional[bool] = False,
) -> Optional[Bot]:
return Bot(
app_id="A111",
enterprise_id="E111",
team_id="T0G9PQBBK",
bot_token="xoxb-valid",
bot_id="B",
bot_user_id="W",
bot_scopes=["commands", "chat:write"],
installed_at=datetime.datetime.now().timestamp(),
)


class MemoryInstallationStore(LegacyMemoryInstallationStore):
def find_installation(
self,
*,
enterprise_id: Optional[str],
team_id: Optional[str],
user_id: Optional[str] = None,
is_enterprise_install: Optional[bool] = False,
) -> Optional[Installation]:
return Installation(
app_id="A111",
enterprise_id="E111",
team_id="T0G9PQBBK",
bot_token="xoxb-valid-2",
bot_id="B",
bot_user_id="W",
bot_scopes=["commands", "chat:write"],
user_id="W11111",
user_token="xoxp-valid",
user_scopes=["search:read"],
installed_at=datetime.datetime.now().timestamp(),
)


class BotOnlyMemoryInstallationStore(LegacyMemoryInstallationStore):
def find_installation(
self,
*,
enterprise_id: Optional[str],
team_id: Optional[str],
user_id: Optional[str] = None,
is_enterprise_install: Optional[bool] = False,
) -> Optional[Installation]:
raise ValueError


class TestApp:
signing_secret = "secret"
valid_token = "xoxb-valid"
mock_api_server_base_url = "http://localhost:8888"
signature_verifier = SignatureVerifier(signing_secret)
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)

def generate_signature(self, body: str, timestamp: str):
return self.signature_verifier.generate_signature(
body=body, timestamp=timestamp,
)

def build_headers(self, timestamp: str, body: str):
return {
"content-type": ["application/json"],
"x-slack-signature": [self.generate_signature(body, timestamp)],
"x-slack-request-timestamp": [timestamp],
}

app_mention_request_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"],
}

@staticmethod
def handle_app_mention(body, say: Say, payload, event):
assert body["event"] == payload
assert payload == event
say("What's up?")

oauth_settings_bot_only = OAuthSettings(
client_id="111.222",
client_secret="valid",
installation_store=BotOnlyMemoryInstallationStore(),
installation_store_bot_only=True,
state_store=FileOAuthStateStore(expiration_seconds=120),
)

oauth_settings = OAuthSettings(
client_id="111.222",
client_secret="valid",
installation_store=BotOnlyMemoryInstallationStore(),
installation_store_bot_only=False,
state_store=FileOAuthStateStore(expiration_seconds=120),
)

def build_app_mention_request(self):
timestamp, body = str(int(time())), json.dumps(self.app_mention_request_body)
return BoltRequest(body=body, headers=self.build_headers(timestamp, body))

def test_installation_store_bot_only_default(self):
app = App(
client=self.web_client,
signing_secret=self.signing_secret,
installation_store=MemoryInstallationStore(),
)

app.event("app_mention")(self.handle_app_mention)
response = app.dispatch(self.build_app_mention_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_installation_store_bot_only_false(self):
app = App(
client=self.web_client,
signing_secret=self.signing_secret,
installation_store=MemoryInstallationStore(),
# the default is False
installation_store_bot_only=False,
)

app.event("app_mention")(self.handle_app_mention)
response = app.dispatch(self.build_app_mention_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_installation_store_bot_only(self):
app = App(
client=self.web_client,
signing_secret=self.signing_secret,
installation_store=BotOnlyMemoryInstallationStore(),
installation_store_bot_only=True,
)

app.event("app_mention")(self.handle_app_mention)
response = app.dispatch(self.build_app_mention_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_installation_store_bot_only_oauth_settings(self):
app = App(
client=self.web_client,
signing_secret=self.signing_secret,
oauth_settings=self.oauth_settings_bot_only,
)

app.event("app_mention")(self.handle_app_mention)
response = app.dispatch(self.build_app_mention_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_installation_store_bot_only_oauth_settings_conflicts(self):
app = App(
client=self.web_client,
signing_secret=self.signing_secret,
installation_store_bot_only=True,
oauth_settings=self.oauth_settings,
)

app.event("app_mention")(self.handle_app_mention)
response = app.dispatch(self.build_app_mention_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_installation_store_bot_only_oauth_flow(self):
app = App(
client=self.web_client,
signing_secret=self.signing_secret,
oauth_flow=OAuthFlow(settings=self.oauth_settings_bot_only),
)

app.event("app_mention")(self.handle_app_mention)
response = app.dispatch(self.build_app_mention_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_installation_store_bot_only_oauth_flow_conflicts(self):
app = App(
client=self.web_client,
signing_secret=self.signing_secret,
installation_store_bot_only=True,
oauth_flow=OAuthFlow(settings=self.oauth_settings),
)

app.event("app_mention")(self.handle_app_mention)
response = app.dispatch(self.build_app_mention_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
Loading