Skip to content

Commit e396b8b

Browse files
authored
Fix #768 The client arg in a listener does not respect the singleton WebClient's retry_handlers (#769)
1 parent 48a793d commit e396b8b

File tree

4 files changed

+234
-0
lines changed

4 files changed

+234
-0
lines changed

slack_bolt/app/app.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1223,6 +1223,7 @@ def _init_context(self, req: BoltRequest):
12231223
proxy=self._client.proxy,
12241224
headers=self._client.headers,
12251225
team_id=req.context.team_id,
1226+
retry_handlers=self._client.retry_handlers.copy() if self._client.retry_handlers is not None else None,
12261227
)
12271228
req.context["client"] = client_per_request
12281229

slack_bolt/app/async_app.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1259,6 +1259,9 @@ def _init_context(self, req: AsyncBoltRequest):
12591259
trust_env_in_session=self._async_client.trust_env_in_session,
12601260
headers=self._async_client.headers,
12611261
team_id=req.context.team_id,
1262+
retry_handlers=self._async_client.retry_handlers.copy()
1263+
if self._async_client.retry_handlers is not None
1264+
else None,
12621265
)
12631266
req.context["client"] = client_per_request
12641267

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import json
2+
from time import time
3+
from urllib.parse import quote
4+
5+
from slack_sdk import WebClient
6+
from slack_sdk.http_retry import all_builtin_retry_handlers
7+
from slack_sdk.signature import SignatureVerifier
8+
9+
from slack_bolt import BoltRequest
10+
from slack_bolt.app import App
11+
from tests.mock_web_api_server import (
12+
setup_mock_web_api_server,
13+
cleanup_mock_web_api_server,
14+
assert_auth_test_count,
15+
)
16+
from tests.utils import remove_os_env_temporarily, restore_os_env
17+
18+
19+
class TestWebClientCustomization:
20+
valid_token = "xoxb-valid"
21+
signing_secret = "secret"
22+
mock_api_server_base_url = "http://localhost:8888"
23+
signature_verifier = SignatureVerifier(signing_secret)
24+
web_client = WebClient(
25+
token=valid_token,
26+
base_url=mock_api_server_base_url,
27+
)
28+
29+
def setup_method(self):
30+
self.old_os_env = remove_os_env_temporarily()
31+
setup_mock_web_api_server(self)
32+
33+
def teardown_method(self):
34+
cleanup_mock_web_api_server(self)
35+
restore_os_env(self.old_os_env)
36+
37+
def generate_signature(self, body: str, timestamp: str):
38+
return self.signature_verifier.generate_signature(
39+
body=body,
40+
timestamp=timestamp,
41+
)
42+
43+
def build_headers(self, timestamp: str, body: str):
44+
return {
45+
"content-type": ["application/x-www-form-urlencoded"],
46+
"x-slack-signature": [self.generate_signature(body, timestamp)],
47+
"x-slack-request-timestamp": [timestamp],
48+
}
49+
50+
def build_valid_request(self) -> BoltRequest:
51+
timestamp = str(int(time()))
52+
return BoltRequest(body=raw_body, headers=self.build_headers(timestamp, raw_body))
53+
54+
def test_web_client_customization(self):
55+
self.web_client.retry_handlers = all_builtin_retry_handlers()
56+
app = App(
57+
client=self.web_client,
58+
signing_secret=self.signing_secret,
59+
)
60+
61+
@app.action("a")
62+
def listener(ack, client):
63+
assert len(client.retry_handlers) == 2
64+
ack()
65+
66+
request = self.build_valid_request()
67+
response = app.dispatch(request)
68+
assert response.status == 200
69+
assert response.body == ""
70+
assert_auth_test_count(self, 1)
71+
72+
73+
block_actions_body = {
74+
"type": "block_actions",
75+
"user": {
76+
"id": "W99999",
77+
"username": "primary-owner",
78+
"name": "primary-owner",
79+
"team_id": "T111",
80+
},
81+
"api_app_id": "A111",
82+
"token": "verification_token",
83+
"container": {
84+
"type": "message",
85+
"message_ts": "111.222",
86+
"channel_id": "C111",
87+
"is_ephemeral": True,
88+
},
89+
"trigger_id": "111.222.valid",
90+
"team": {
91+
"id": "T111",
92+
"domain": "workspace-domain",
93+
"enterprise_id": "E111",
94+
"enterprise_name": "Sandbox Org",
95+
},
96+
"channel": {"id": "C111", "name": "test-channel"},
97+
"response_url": "https://hooks.slack.com/actions/T111/111/random-value",
98+
"actions": [
99+
{
100+
"action_id": "a",
101+
"block_id": "b",
102+
"text": {"type": "plain_text", "text": "Button", "emoji": True},
103+
"value": "click_me_123",
104+
"type": "button",
105+
"action_ts": "1596530385.194939",
106+
}
107+
],
108+
}
109+
110+
raw_body = f"payload={quote(json.dumps(block_actions_body))}"
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import asyncio
2+
import json
3+
from time import time
4+
from urllib.parse import quote
5+
6+
import pytest
7+
from slack_sdk.web.async_client import AsyncWebClient
8+
from slack_sdk.http_retry.builtin_async_handlers import AsyncConnectionErrorRetryHandler, AsyncRateLimitErrorRetryHandler
9+
from slack_sdk.signature import SignatureVerifier
10+
11+
from slack_bolt.request.async_request import AsyncBoltRequest
12+
from slack_bolt.async_app import AsyncApp
13+
from tests.mock_web_api_server import (
14+
setup_mock_web_api_server,
15+
cleanup_mock_web_api_server,
16+
assert_auth_test_count_async,
17+
)
18+
from tests.utils import remove_os_env_temporarily, restore_os_env
19+
20+
21+
class TestWebClientCustomization:
22+
valid_token = "xoxb-valid"
23+
signing_secret = "secret"
24+
mock_api_server_base_url = "http://localhost:8888"
25+
signature_verifier = SignatureVerifier(signing_secret)
26+
web_client = AsyncWebClient(
27+
token=valid_token,
28+
base_url=mock_api_server_base_url,
29+
)
30+
31+
@pytest.fixture
32+
def event_loop(self):
33+
old_os_env = remove_os_env_temporarily()
34+
try:
35+
setup_mock_web_api_server(self)
36+
loop = asyncio.get_event_loop()
37+
yield loop
38+
loop.close()
39+
cleanup_mock_web_api_server(self)
40+
finally:
41+
restore_os_env(old_os_env)
42+
43+
def generate_signature(self, body: str, timestamp: str):
44+
return self.signature_verifier.generate_signature(
45+
body=body,
46+
timestamp=timestamp,
47+
)
48+
49+
def build_headers(self, timestamp: str, body: str):
50+
return {
51+
"content-type": ["application/x-www-form-urlencoded"],
52+
"x-slack-signature": [self.generate_signature(body, timestamp)],
53+
"x-slack-request-timestamp": [timestamp],
54+
}
55+
56+
def build_valid_request(self) -> AsyncBoltRequest:
57+
timestamp = str(int(time()))
58+
return AsyncBoltRequest(body=raw_body, headers=self.build_headers(timestamp, raw_body))
59+
60+
@pytest.mark.asyncio
61+
async def test_web_client_customization(self):
62+
self.web_client.retry_handlers = [
63+
AsyncConnectionErrorRetryHandler,
64+
AsyncRateLimitErrorRetryHandler,
65+
]
66+
app = AsyncApp(
67+
client=self.web_client,
68+
signing_secret=self.signing_secret,
69+
)
70+
71+
@app.action("a")
72+
async def listener(ack, client):
73+
assert len(client.retry_handlers) == 2
74+
await ack()
75+
76+
request = self.build_valid_request()
77+
response = await app.async_dispatch(request)
78+
assert response.status == 200
79+
assert response.body == ""
80+
await assert_auth_test_count_async(self, 1)
81+
82+
83+
block_actions_body = {
84+
"type": "block_actions",
85+
"user": {
86+
"id": "W99999",
87+
"username": "primary-owner",
88+
"name": "primary-owner",
89+
"team_id": "T111",
90+
},
91+
"api_app_id": "A111",
92+
"token": "verification_token",
93+
"container": {
94+
"type": "message",
95+
"message_ts": "111.222",
96+
"channel_id": "C111",
97+
"is_ephemeral": True,
98+
},
99+
"trigger_id": "111.222.valid",
100+
"team": {
101+
"id": "T111",
102+
"domain": "workspace-domain",
103+
"enterprise_id": "E111",
104+
"enterprise_name": "Sandbox Org",
105+
},
106+
"channel": {"id": "C111", "name": "test-channel"},
107+
"response_url": "https://hooks.slack.com/actions/T111/111/random-value",
108+
"actions": [
109+
{
110+
"action_id": "a",
111+
"block_id": "b",
112+
"text": {"type": "plain_text", "text": "Button", "emoji": True},
113+
"value": "click_me_123",
114+
"type": "button",
115+
"action_ts": "1596530385.194939",
116+
}
117+
],
118+
}
119+
120+
raw_body = f"payload={quote(json.dumps(block_actions_body))}"

0 commit comments

Comments
 (0)