Skip to content

Commit e6ebdbb

Browse files
Merge branch 'main' into codex/fix-ssl-check-request-verification-bypass
2 parents a54bf56 + b0eda44 commit e6ebdbb

9 files changed

Lines changed: 129 additions & 31 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

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_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(

tests/scenario_tests_async/test_function.py

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from slack_sdk.signature import SignatureVerifier
99
from slack_sdk.web.async_client import AsyncWebClient
1010

11+
import slack_bolt.listener.asyncio_runner as async_runner_module
1112
from slack_bolt.app.async_app import AsyncApp
1213
from slack_bolt.request.async_request import AsyncBoltRequest
1314
from tests.mock_web_api_server import (
@@ -19,10 +20,6 @@
1920
from tests.utils import remove_os_env_temporarily, restore_os_env
2021

2122

22-
async def fake_sleep(seconds):
23-
pass
24-
25-
2623
class TestAsyncFunction:
2724
signing_secret = "secret"
2825
valid_token = "xoxb-valid"
@@ -60,9 +57,9 @@ def build_request_from_body(self, message_body: dict) -> AsyncBoltRequest:
6057
timestamp, body = str(int(time.time())), json.dumps(message_body)
6158
return AsyncBoltRequest(body=body, headers=self.build_headers(timestamp, body))
6259

63-
def setup_time_mocks(self, *, monkeypatch: pytest.MonkeyPatch, time_mock: Mock, sleep_mock: MagicMock):
64-
monkeypatch.setattr(time, "time", time_mock)
65-
monkeypatch.setattr(asyncio, "sleep", sleep_mock)
60+
def setup_time_mocks(self, *, monkeypatch: pytest.MonkeyPatch, time_mock, sleep_mock):
61+
monkeypatch.setattr(async_runner_module.time, "time", time_mock)
62+
monkeypatch.setattr(async_runner_module.asyncio, "sleep", sleep_mock)
6663

6764
@pytest.mark.asyncio
6865
async def test_mock_server_is_running(self):
@@ -146,9 +143,18 @@ async def test_auto_acknowledge_false_without_acknowledging(self, caplog, monkey
146143
app.function("reverse", auto_acknowledge=False)(just_no_ack)
147144
request = self.build_request_from_body(function_body)
148145

146+
elapsed_seconds = 0
147+
148+
def fake_time():
149+
return float(elapsed_seconds)
150+
151+
async def fake_sleep(duration):
152+
nonlocal elapsed_seconds
153+
elapsed_seconds += 1
154+
149155
self.setup_time_mocks(
150156
monkeypatch=monkeypatch,
151-
time_mock=Mock(side_effect=[current_time for current_time in range(100)]),
157+
time_mock=fake_time,
152158
sleep_mock=MagicMock(side_effect=fake_sleep),
153159
)
154160

@@ -167,20 +173,28 @@ async def test_function_handler_timeout(self, monkeypatch):
167173
app.function("reverse", auto_acknowledge=False, ack_timeout=timeout)(just_no_ack)
168174
request = self.build_request_from_body(function_body)
169175

170-
sleep_mock = MagicMock(side_effect=fake_sleep)
176+
elapsed_seconds = 0
177+
178+
def fake_time():
179+
return float(elapsed_seconds)
180+
181+
async def fake_sleep(duration):
182+
nonlocal elapsed_seconds
183+
elapsed_seconds += 1
184+
171185
self.setup_time_mocks(
172186
monkeypatch=monkeypatch,
173-
time_mock=Mock(side_effect=[current_time for current_time in range(100)]),
174-
sleep_mock=sleep_mock,
187+
time_mock=fake_time,
188+
sleep_mock=MagicMock(side_effect=fake_sleep),
175189
)
176190

177191
response = await app.async_dispatch(request)
178192

179193
assert response.status == 404
180194
await assert_auth_test_count_async(self, 1)
181-
assert (
182-
sleep_mock.call_count == timeout
183-
), f"Expected handler to time out after calling time.sleep 5 times, but it was called {sleep_mock.call_count} times"
195+
assert elapsed_seconds == timeout + 1, (
196+
f"Expected handler to time out after {timeout + 1} sleep calls, " f"but it was called {elapsed_seconds} times"
197+
)
184198

185199
@pytest.mark.asyncio
186200
async def test_warning_when_timeout_improperly_set(self, caplog):

0 commit comments

Comments
 (0)