Skip to content

Commit cacb00b

Browse files
Merge branch 'main' into dependabot/pip/starlette-gte-0.49.3-and-lt-1
2 parents 197d09c + 3319330 commit cacb00b

15 files changed

Lines changed: 259 additions & 32 deletions

File tree

slack_bolt/adapter/socket_mode/async_internals.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@
88
from slack_sdk.socket_mode.request import SocketModeRequest
99
from slack_sdk.socket_mode.response import SocketModeResponse
1010

11+
from slack_bolt.adapter.socket_mode.internals import build_headers
1112
from slack_bolt.app.async_app import AsyncApp
1213
from slack_bolt.request.async_request import AsyncBoltRequest
1314
from slack_bolt.response import BoltResponse
1415

1516

1617
async def run_async_bolt_app(app: AsyncApp, req: SocketModeRequest):
17-
bolt_req: AsyncBoltRequest = AsyncBoltRequest(mode="socket_mode", body=req.payload)
18+
bolt_req: AsyncBoltRequest = AsyncBoltRequest(mode="socket_mode", body=req.payload, headers=build_headers(req))
1819
bolt_resp: BoltResponse = await app.async_dispatch(bolt_req)
1920
return bolt_resp
2021

slack_bolt/adapter/socket_mode/internals.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import json
44
import logging
55
from time import time
6+
from typing import Dict, Optional, Sequence, Union
67

78
from slack_sdk.socket_mode.client import BaseSocketModeClient
89
from slack_sdk.socket_mode.request import SocketModeRequest
@@ -13,8 +14,18 @@
1314
from slack_bolt.response import BoltResponse
1415

1516

17+
def build_headers(req: SocketModeRequest) -> Optional[Dict[str, Union[str, Sequence[str]]]]:
18+
# Mirror the HTTP mode retry headers so middleware/listeners can detect Events API retries
19+
headers: Dict[str, Union[str, Sequence[str]]] = {}
20+
if req.retry_attempt is not None:
21+
headers["x-slack-retry-num"] = str(req.retry_attempt)
22+
if req.retry_reason is not None:
23+
headers["x-slack-retry-reason"] = req.retry_reason
24+
return headers or None
25+
26+
1627
def run_bolt_app(app: App, req: SocketModeRequest):
17-
bolt_req: BoltRequest = BoltRequest(mode="socket_mode", body=req.payload)
28+
bolt_req: BoltRequest = BoltRequest(mode="socket_mode", body=req.payload, headers=build_headers(req))
1829
bolt_resp: BoltResponse = app.dispatch(bolt_req)
1930
return bolt_resp
2031

slack_bolt/middleware/request_verification/request_verification.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def process(
4949

5050
@staticmethod
5151
def _can_skip(mode: str, body: Dict[str, Any]) -> bool:
52-
return mode == "socket_mode" or (body is not None and body.get("ssl_check") == "1")
52+
return mode == "socket_mode"
5353

5454
@staticmethod
5555
def _build_error_response() -> BoltResponse:

tests/adapter_tests/socket_mode/test_interactions_builtin.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def teardown_method(self):
3636
def test_interactions(self):
3737
app = App(client=self.web_client)
3838

39-
result = {"shortcut": False, "command": False}
39+
result = {"shortcut": False, "command": False, "message": False}
4040

4141
@app.shortcut("do-something")
4242
def shortcut_handler(ack):
@@ -48,6 +48,13 @@ def command_handler(ack):
4848
result["command"] = True
4949
ack()
5050

51+
@app.message("<@W111>")
52+
def message_handler(ack, req):
53+
result["message"] = req.headers.get("x-slack-retry-num") == ["1"] and req.headers.get(
54+
"x-slack-retry-reason"
55+
) == ["timeout"]
56+
ack()
57+
5158
handler = SocketModeHandler(
5259
app_token="xapp-A111-222-xyz",
5360
app=app,
@@ -66,5 +73,6 @@ def command_handler(ack):
6673
time.sleep(2)
6774
assert result["shortcut"] is True
6875
assert result["command"] is True
76+
assert result["message"] is True
6977
finally:
7078
handler.client.close()

tests/adapter_tests/socket_mode/test_interactions_websocket_client.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def test_interactions(self):
3737

3838
app = App(client=self.web_client)
3939

40-
result = {"shortcut": False, "command": False}
40+
result = {"shortcut": False, "command": False, "message": False}
4141

4242
@app.shortcut("do-something")
4343
def shortcut_handler(ack):
@@ -49,6 +49,13 @@ def command_handler(ack):
4949
result["command"] = True
5050
ack()
5151

52+
@app.message("<@W111>")
53+
def message_handler(ack, req):
54+
result["message"] = req.headers.get("x-slack-retry-num") == ["1"] and req.headers.get(
55+
"x-slack-retry-reason"
56+
) == ["timeout"]
57+
ack()
58+
5259
handler = SocketModeHandler(
5360
app_token="xapp-A111-222-xyz",
5461
app=app,
@@ -67,5 +74,6 @@ def command_handler(ack):
6774
time.sleep(2)
6875
assert result["shortcut"] is True
6976
assert result["command"] is True
77+
assert result["message"] is True
7078
finally:
7179
handler.client.close()
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from slack_sdk.socket_mode.request import SocketModeRequest
2+
3+
from slack_bolt.adapter.socket_mode.internals import build_headers, run_bolt_app
4+
5+
6+
class TestSocketModeInternals:
7+
def test_build_retry_headers_without_retry(self):
8+
req = SocketModeRequest(type="events_api", envelope_id="e1", payload={"type": "event_callback"})
9+
assert build_headers(req) is None
10+
11+
def test_build_retry_headers_with_retry(self):
12+
req = SocketModeRequest(
13+
type="events_api",
14+
envelope_id="e1",
15+
payload={"type": "event_callback"},
16+
retry_attempt=2,
17+
retry_reason="http_timeout",
18+
)
19+
headers = build_headers(req)
20+
assert headers == {"x-slack-retry-num": "2", "x-slack-retry-reason": "http_timeout"}

tests/adapter_tests/wsgi/test_wsgi_http.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,47 @@ def command_handler(ack):
8989
assert response.headers.get("content-type") == "text/plain;charset=utf-8"
9090
assert_auth_test_count(self, 1)
9191

92+
def test_ssl_check_param_does_not_bypass_request_verification(self):
93+
app = App(
94+
client=self.web_client,
95+
signing_secret=self.signing_secret,
96+
ssl_check_enabled=False,
97+
)
98+
command_called = False
99+
100+
def command_handler(ack):
101+
nonlocal command_called
102+
command_called = True
103+
ack()
104+
105+
app.command("/hello-world")(command_handler)
106+
107+
body = (
108+
"token=verification_token"
109+
"&team_id=T111"
110+
"&team_domain=test-domain"
111+
"&channel_id=C111"
112+
"&channel_name=random"
113+
"&user_id=W111"
114+
"&user_name=primary-owner"
115+
"&command=%2Fhello-world"
116+
"&text=Hi"
117+
"&enterprise_id=E111"
118+
"&enterprise_name=Org+Name"
119+
"&response_url=https%3A%2F%2Fhooks.slack.com%2Fcommands%2FT111%2F111%2Fxxxxx"
120+
"&trigger_id=111.111.xxx"
121+
"&ssl_check=1"
122+
)
123+
headers = self.build_raw_headers("0", body)
124+
headers["x-slack-signature"] = "v0=invalid"
125+
126+
wsgi_server = WsgiTestServer(SlackRequestHandler(app))
127+
response = wsgi_server.http(method="POST", headers=headers, body=body)
128+
129+
assert response.status == "401 Unauthorized"
130+
assert response.body == """{"error": "invalid request"}"""
131+
assert command_called is False
132+
92133
def test_events(self):
93134
app = App(
94135
client=self.web_client,

tests/adapter_tests_async/socket_mode/test_async_aiohttp.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ async def test_events(self):
4040

4141
app = AsyncApp(client=self.web_client)
4242

43-
result = {"shortcut": False, "command": False}
43+
result = {"shortcut": False, "command": False, "message": False}
4444

4545
@app.shortcut("do-something")
4646
async def shortcut_handler(ack):
@@ -52,6 +52,13 @@ async def command_handler(ack):
5252
result["command"] = True
5353
await ack()
5454

55+
@app.message("<@W111>")
56+
async def message_handler(ack, req):
57+
result["message"] = req.headers.get("x-slack-retry-num") == ["1"] and req.headers.get(
58+
"x-slack-retry-reason"
59+
) == ["timeout"]
60+
await ack()
61+
5562
handler = AsyncSocketModeHandler(
5663
app_token="xapp-A111-222-xyz",
5764
app=app,
@@ -67,6 +74,7 @@ async def command_handler(ack):
6774
await asyncio.sleep(2)
6875
assert result["shortcut"] is True
6976
assert result["command"] is True
77+
assert result["message"] is True
7078
finally:
7179
await handler.client.close()
7280
stop_socket_mode_server(self)

tests/adapter_tests_async/socket_mode/test_async_websockets.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ async def test_events(self):
4040

4141
app = AsyncApp(client=self.web_client)
4242

43-
result = {"shortcut": False, "command": False}
43+
result = {"shortcut": False, "command": False, "message": False}
4444

4545
@app.shortcut("do-something")
4646
async def shortcut_handler(ack):
@@ -52,6 +52,13 @@ async def command_handler(ack):
5252
result["command"] = True
5353
await ack()
5454

55+
@app.message("<@W111>")
56+
async def message_handler(ack, req):
57+
result["message"] = req.headers.get("x-slack-retry-num") == ["1"] and req.headers.get(
58+
"x-slack-retry-reason"
59+
) == ["timeout"]
60+
await ack()
61+
5562
handler = AsyncSocketModeHandler(
5663
app_token="xapp-A111-222-xyz",
5764
app=app,
@@ -67,6 +74,7 @@ async def command_handler(ack):
6774
await asyncio.sleep(2)
6875
assert result["shortcut"] is True
6976
assert result["command"] is True
77+
assert result["message"] is True
7078
finally:
7179
await handler.client.close()
7280
stop_socket_mode_server(self)

tests/scenario_tests/test_function.py

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from slack_sdk.signature import SignatureVerifier
88
from slack_sdk.web import WebClient
99

10+
import slack_bolt.listener.thread_runner as runner_module
1011
from slack_bolt.app import App
1112
from slack_bolt.request import BoltRequest
1213
from tests.mock_web_api_server import (
@@ -53,9 +54,9 @@ def build_request_from_body(self, message_body: dict) -> BoltRequest:
5354
timestamp, body = str(int(time.time())), json.dumps(message_body)
5455
return BoltRequest(body=body, headers=self.build_headers(timestamp, body))
5556

56-
def setup_time_mocks(self, *, monkeypatch: pytest.MonkeyPatch, time_mock: Mock, sleep_mock: Mock):
57-
monkeypatch.setattr(time, "time", time_mock)
58-
monkeypatch.setattr(time, "sleep", sleep_mock)
57+
def setup_time_mocks(self, *, monkeypatch: pytest.MonkeyPatch, time_mock, sleep_mock):
58+
monkeypatch.setattr(runner_module.time, "time", time_mock)
59+
monkeypatch.setattr(runner_module.time, "sleep", sleep_mock)
5960

6061
def test_valid_callback_id_success(self):
6162
app = App(
@@ -138,10 +139,20 @@ def test_auto_acknowledge_false_without_acknowledging(self, caplog, monkeypatch)
138139
app.function("reverse", auto_acknowledge=False)(just_no_ack)
139140

140141
request = self.build_request_from_body(function_body)
142+
143+
elapsed_seconds = 0
144+
145+
def fake_time():
146+
return float(elapsed_seconds)
147+
148+
def fake_sleep(duration):
149+
nonlocal elapsed_seconds
150+
elapsed_seconds += 1
151+
141152
self.setup_time_mocks(
142153
monkeypatch=monkeypatch,
143-
time_mock=Mock(side_effect=[current_time for current_time in range(100)]),
144-
sleep_mock=Mock(),
154+
time_mock=fake_time,
155+
sleep_mock=Mock(side_effect=fake_sleep),
145156
)
146157
response = app.dispatch(request)
147158

@@ -158,20 +169,29 @@ def test_function_handler_timeout(self, monkeypatch):
158169
app.function("reverse", auto_acknowledge=False, ack_timeout=timeout)(just_no_ack)
159170
request = self.build_request_from_body(function_body)
160171

161-
sleep_mock = Mock()
172+
elapsed_seconds = 0
173+
174+
def fake_time():
175+
return float(elapsed_seconds)
176+
177+
def fake_sleep(duration):
178+
nonlocal elapsed_seconds
179+
elapsed_seconds += 1
180+
162181
self.setup_time_mocks(
163182
monkeypatch=monkeypatch,
164-
time_mock=Mock(side_effect=[current_time for current_time in range(100)]),
165-
sleep_mock=sleep_mock,
183+
time_mock=fake_time,
184+
sleep_mock=Mock(side_effect=fake_sleep),
166185
)
167186

168187
response = app.dispatch(request)
169188

170189
assert response.status == 404
171190
assert_auth_test_count(self, 1)
172-
assert (
173-
sleep_mock.call_count == timeout
174-
), f"Expected handler to time out after calling time.sleep 5 times, but it was called {sleep_mock.call_count} times"
191+
assert elapsed_seconds == timeout + 1, (
192+
f"Expected handler to time out after {timeout + 1} time.sleep calls, "
193+
f"but it was called {elapsed_seconds} times"
194+
)
175195

176196
def test_warning_when_timeout_improperly_set(self, caplog):
177197
app = App(

0 commit comments

Comments
 (0)