Skip to content

Commit d82a7d9

Browse files
committed
Fix #177 by updating App/AsyncApp bot_only flag initialization
1 parent 890cf3f commit d82a7d9

File tree

6 files changed

+566
-7
lines changed

6 files changed

+566
-7
lines changed

slack_bolt/app/app.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
error_unexpected_listener_middleware,
4242
error_client_invalid_type,
4343
error_authorize_conflicts,
44+
warning_bot_only_conflicts,
4445
)
4546
from slack_bolt.middleware import (
4647
Middleware,
@@ -80,7 +81,7 @@ def __init__(
8081
authorize: Optional[Callable[..., AuthorizeResult]] = None,
8182
installation_store: Optional[InstallationStore] = None,
8283
# for v1.0.x compatibility
83-
installation_store_bot_only: bool = False,
84+
installation_store_bot_only: Optional[bool] = None,
8485
# for the OAuth flow
8586
oauth_settings: Optional[OAuthSettings] = None,
8687
oauth_flow: Optional[OAuthFlow] = None,
@@ -197,8 +198,13 @@ def __init__(
197198
self._framework_logger.warning(warning_token_skipped())
198199

199200
# after setting bot_only here, __init__ cannot replace authorize function
200-
if self._authorize is not None:
201-
self._authorize.bot_only = installation_store_bot_only
201+
if installation_store_bot_only is not None and self._oauth_flow is not None:
202+
app_bot_only = installation_store_bot_only or False
203+
oauth_flow_bot_only = self._oauth_flow.settings.installation_store_bot_only
204+
if app_bot_only != oauth_flow_bot_only:
205+
self.logger.warning(warning_bot_only_conflicts())
206+
self._oauth_flow.settings.installation_store_bot_only = app_bot_only
207+
self._authorize.bot_only = app_bot_only
202208

203209
# --------------------------------------
204210
# Middleware Initialization

slack_bolt/app/async_app.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
error_authorize_conflicts,
3939
error_oauth_settings_invalid_type_async,
4040
error_oauth_flow_invalid_type_async,
41+
warning_bot_only_conflicts,
4142
)
4243
from slack_bolt.lazy_listener.asyncio_runner import AsyncioLazyListenerRunner
4344
from slack_bolt.listener.async_listener import AsyncListener, AsyncCustomListener
@@ -90,7 +91,7 @@ def __init__(
9091
client: Optional[AsyncWebClient] = None,
9192
# for multi-workspace apps
9293
installation_store: Optional[AsyncInstallationStore] = None,
93-
installation_store_bot_only: bool = False,
94+
installation_store_bot_only: Optional[bool] = None,
9495
authorize: Optional[Callable[..., Awaitable[AuthorizeResult]]] = None,
9596
# for the OAuth flow
9697
oauth_settings: Optional[AsyncOAuthSettings] = None,
@@ -219,8 +220,20 @@ def __init__(
219220
self._framework_logger.warning(warning_token_skipped())
220221

221222
# after setting bot_only here, __init__ cannot replace authorize function
222-
if self._async_authorize is not None:
223-
self._async_authorize.bot_only = installation_store_bot_only
223+
if (
224+
installation_store_bot_only is not None
225+
and self._async_oauth_flow is not None
226+
):
227+
app_bot_only = installation_store_bot_only or False
228+
oauth_flow_bot_only = (
229+
self._async_oauth_flow.settings.installation_store_bot_only
230+
)
231+
if app_bot_only != oauth_flow_bot_only:
232+
self.logger.warning(warning_bot_only_conflicts())
233+
self._async_oauth_flow.settings.installation_store_bot_only = (
234+
app_bot_only
235+
)
236+
self._async_authorize.bot_only = app_bot_only
224237

225238
# --------------------------------------
226239
# Middleware Initialization

slack_bolt/logger/messages.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@ def warning_did_not_call_ack(listener_name: str) -> str:
7676
return f"{listener_name} didn't call ack()"
7777

7878

79+
def warning_bot_only_conflicts() -> str:
80+
return (
81+
"installation_store_bot_only exists in both App and OAuthFlow.settings. "
82+
"The one passed in App constructor is used."
83+
)
84+
85+
7986
# -------------------------------
8087
# Info
8188
# -------------------------------

tests/scenario_tests/test_app.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from slack_sdk.oauth.installation_store import FileInstallationStore
44
from slack_sdk.oauth.state_store import FileOAuthStateStore
55

6-
from slack_bolt import App
6+
from slack_bolt import App, Say
77
from slack_bolt.authorization import AuthorizeResult
88
from slack_bolt.error import BoltError
99
from slack_bolt.oauth import OAuthFlow
@@ -29,6 +29,16 @@ def teardown_method(self):
2929
cleanup_mock_web_api_server(self)
3030
restore_os_env(self.old_os_env)
3131

32+
@staticmethod
33+
def handle_app_mention(body, say: Say, payload, event):
34+
assert body["event"] == payload
35+
assert payload == event
36+
say("What's up?")
37+
38+
# --------------------------
39+
# basic tests
40+
# --------------------------
41+
3242
def test_signing_secret_absence(self):
3343
with pytest.raises(BoltError):
3444
App(signing_secret=None, token="xoxb-xxx")
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
import datetime
2+
import json
3+
import logging
4+
from time import time, sleep
5+
from typing import Optional
6+
7+
from slack_sdk import WebClient
8+
from slack_sdk.oauth import InstallationStore
9+
from slack_sdk.oauth.installation_store import Installation, Bot
10+
from slack_sdk.oauth.state_store import FileOAuthStateStore
11+
from slack_sdk.signature import SignatureVerifier
12+
13+
from slack_bolt import App, BoltRequest, Say
14+
from slack_bolt.oauth import OAuthFlow
15+
from slack_bolt.oauth.oauth_settings import OAuthSettings
16+
from tests.mock_web_api_server import (
17+
setup_mock_web_api_server,
18+
cleanup_mock_web_api_server,
19+
)
20+
from tests.utils import remove_os_env_temporarily, restore_os_env
21+
22+
23+
class LegacyMemoryInstallationStore(InstallationStore):
24+
@property
25+
def logger(self) -> logging.Logger:
26+
return logging.getLogger(__name__)
27+
28+
def save(self, installation: Installation):
29+
pass
30+
31+
def find_bot(
32+
self,
33+
*,
34+
enterprise_id: Optional[str],
35+
team_id: Optional[str],
36+
is_enterprise_install: Optional[bool] = False,
37+
) -> Optional[Bot]:
38+
return Bot(
39+
app_id="A111",
40+
enterprise_id="E111",
41+
team_id="T0G9PQBBK",
42+
bot_token="xoxb-valid",
43+
bot_id="B",
44+
bot_user_id="W",
45+
bot_scopes=["commands", "chat:write"],
46+
installed_at=datetime.datetime.now().timestamp(),
47+
)
48+
49+
50+
class MemoryInstallationStore(LegacyMemoryInstallationStore):
51+
def find_installation(
52+
self,
53+
*,
54+
enterprise_id: Optional[str],
55+
team_id: Optional[str],
56+
user_id: Optional[str] = None,
57+
is_enterprise_install: Optional[bool] = False,
58+
) -> Optional[Installation]:
59+
return Installation(
60+
app_id="A111",
61+
enterprise_id="E111",
62+
team_id="T0G9PQBBK",
63+
bot_token="xoxb-valid-2",
64+
bot_id="B",
65+
bot_user_id="W",
66+
bot_scopes=["commands", "chat:write"],
67+
user_id="W11111",
68+
user_token="xoxp-valid",
69+
user_scopes=["search:read"],
70+
installed_at=datetime.datetime.now().timestamp(),
71+
)
72+
73+
74+
class BotOnlyMemoryInstallationStore(LegacyMemoryInstallationStore):
75+
def find_installation(
76+
self,
77+
*,
78+
enterprise_id: Optional[str],
79+
team_id: Optional[str],
80+
user_id: Optional[str] = None,
81+
is_enterprise_install: Optional[bool] = False,
82+
) -> Optional[Installation]:
83+
raise ValueError
84+
85+
86+
class TestApp:
87+
signing_secret = "secret"
88+
valid_token = "xoxb-valid"
89+
mock_api_server_base_url = "http://localhost:8888"
90+
signature_verifier = SignatureVerifier(signing_secret)
91+
web_client = WebClient(token=valid_token, base_url=mock_api_server_base_url,)
92+
93+
def setup_method(self):
94+
self.old_os_env = remove_os_env_temporarily()
95+
setup_mock_web_api_server(self)
96+
97+
def teardown_method(self):
98+
cleanup_mock_web_api_server(self)
99+
restore_os_env(self.old_os_env)
100+
101+
def generate_signature(self, body: str, timestamp: str):
102+
return self.signature_verifier.generate_signature(
103+
body=body, timestamp=timestamp,
104+
)
105+
106+
def build_headers(self, timestamp: str, body: str):
107+
return {
108+
"content-type": ["application/json"],
109+
"x-slack-signature": [self.generate_signature(body, timestamp)],
110+
"x-slack-request-timestamp": [timestamp],
111+
}
112+
113+
app_mention_request_body = {
114+
"token": "verification_token",
115+
"team_id": "T111",
116+
"enterprise_id": "E111",
117+
"api_app_id": "A111",
118+
"event": {
119+
"client_msg_id": "9cbd4c5b-7ddf-4ede-b479-ad21fca66d63",
120+
"type": "app_mention",
121+
"text": "<@W111> Hi there!",
122+
"user": "W222",
123+
"ts": "1595926230.009600",
124+
"team": "T111",
125+
"channel": "C111",
126+
"event_ts": "1595926230.009600",
127+
},
128+
"type": "event_callback",
129+
"event_id": "Ev111",
130+
"event_time": 1595926230,
131+
"authed_users": ["W111"],
132+
}
133+
134+
@staticmethod
135+
def handle_app_mention(body, say: Say, payload, event):
136+
assert body["event"] == payload
137+
assert payload == event
138+
say("What's up?")
139+
140+
oauth_settings_bot_only = OAuthSettings(
141+
client_id="111.222",
142+
client_secret="valid",
143+
installation_store=BotOnlyMemoryInstallationStore(),
144+
installation_store_bot_only=True,
145+
state_store=FileOAuthStateStore(expiration_seconds=120),
146+
)
147+
148+
oauth_settings = OAuthSettings(
149+
client_id="111.222",
150+
client_secret="valid",
151+
installation_store=BotOnlyMemoryInstallationStore(),
152+
installation_store_bot_only=False,
153+
state_store=FileOAuthStateStore(expiration_seconds=120),
154+
)
155+
156+
def build_app_mention_request(self):
157+
timestamp, body = str(int(time())), json.dumps(self.app_mention_request_body)
158+
return BoltRequest(body=body, headers=self.build_headers(timestamp, body))
159+
160+
def test_installation_store_bot_only_default(self):
161+
app = App(
162+
client=self.web_client,
163+
signing_secret=self.signing_secret,
164+
installation_store=MemoryInstallationStore(),
165+
)
166+
167+
app.event("app_mention")(self.handle_app_mention)
168+
response = app.dispatch(self.build_app_mention_request())
169+
assert response.status == 200
170+
assert self.mock_received_requests["/auth.test"] == 1
171+
sleep(1) # wait a bit after auto ack()
172+
assert self.mock_received_requests["/chat.postMessage"] == 1
173+
174+
def test_installation_store_bot_only_false(self):
175+
app = App(
176+
client=self.web_client,
177+
signing_secret=self.signing_secret,
178+
installation_store=MemoryInstallationStore(),
179+
# the default is False
180+
installation_store_bot_only=False,
181+
)
182+
183+
app.event("app_mention")(self.handle_app_mention)
184+
response = app.dispatch(self.build_app_mention_request())
185+
assert response.status == 200
186+
assert self.mock_received_requests["/auth.test"] == 1
187+
sleep(1) # wait a bit after auto ack()
188+
assert self.mock_received_requests["/chat.postMessage"] == 1
189+
190+
def test_installation_store_bot_only(self):
191+
app = App(
192+
client=self.web_client,
193+
signing_secret=self.signing_secret,
194+
installation_store=BotOnlyMemoryInstallationStore(),
195+
installation_store_bot_only=True,
196+
)
197+
198+
app.event("app_mention")(self.handle_app_mention)
199+
response = app.dispatch(self.build_app_mention_request())
200+
assert response.status == 200
201+
assert self.mock_received_requests["/auth.test"] == 1
202+
sleep(1) # wait a bit after auto ack()
203+
assert self.mock_received_requests["/chat.postMessage"] == 1
204+
205+
def test_installation_store_bot_only_oauth_settings(self):
206+
app = App(
207+
client=self.web_client,
208+
signing_secret=self.signing_secret,
209+
oauth_settings=self.oauth_settings_bot_only,
210+
)
211+
212+
app.event("app_mention")(self.handle_app_mention)
213+
response = app.dispatch(self.build_app_mention_request())
214+
assert response.status == 200
215+
assert self.mock_received_requests["/auth.test"] == 1
216+
sleep(1) # wait a bit after auto ack()
217+
assert self.mock_received_requests["/chat.postMessage"] == 1
218+
219+
def test_installation_store_bot_only_oauth_settings_conflicts(self):
220+
app = App(
221+
client=self.web_client,
222+
signing_secret=self.signing_secret,
223+
installation_store_bot_only=True,
224+
oauth_settings=self.oauth_settings,
225+
)
226+
227+
app.event("app_mention")(self.handle_app_mention)
228+
response = app.dispatch(self.build_app_mention_request())
229+
assert response.status == 200
230+
assert self.mock_received_requests["/auth.test"] == 1
231+
sleep(1) # wait a bit after auto ack()
232+
assert self.mock_received_requests["/chat.postMessage"] == 1
233+
234+
def test_installation_store_bot_only_oauth_flow(self):
235+
app = App(
236+
client=self.web_client,
237+
signing_secret=self.signing_secret,
238+
oauth_flow=OAuthFlow(settings=self.oauth_settings_bot_only),
239+
)
240+
241+
app.event("app_mention")(self.handle_app_mention)
242+
response = app.dispatch(self.build_app_mention_request())
243+
assert response.status == 200
244+
assert self.mock_received_requests["/auth.test"] == 1
245+
sleep(1) # wait a bit after auto ack()
246+
assert self.mock_received_requests["/chat.postMessage"] == 1
247+
248+
def test_installation_store_bot_only_oauth_flow_conflicts(self):
249+
app = App(
250+
client=self.web_client,
251+
signing_secret=self.signing_secret,
252+
installation_store_bot_only=True,
253+
oauth_flow=OAuthFlow(settings=self.oauth_settings),
254+
)
255+
256+
app.event("app_mention")(self.handle_app_mention)
257+
response = app.dispatch(self.build_app_mention_request())
258+
assert response.status == 200
259+
assert self.mock_received_requests["/auth.test"] == 1
260+
sleep(1) # wait a bit after auto ack()
261+
assert self.mock_received_requests["/chat.postMessage"] == 1

0 commit comments

Comments
 (0)