Skip to content

Commit e93c62f

Browse files
authored
Add tests for flask/lambda/starlette adapters (slackapi#49)
* Add tests for flask/lambda/starlette adapters * Add requests * Update tests * Make boto3 client lazy
1 parent d1b1204 commit e93c62f

File tree

14 files changed

+1166
-11
lines changed

14 files changed

+1166
-11
lines changed

.travis.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,17 @@ install:
1010
# https://discuss.python.org/t/announcement-pip-20-2-release/4863
1111
- pip config set global.use-feature 2020-resolver
1212
- pip install "pytest>=5,<6"
13-
- pip install "pytype"
1413
script:
1514
# testing without aiohttp
1615
- travis_retry pytest tests/scenario_tests/
1716
# testing with aiohttp
18-
- pip install "pytest-asyncio<1" "aiohttp>=3,<4"
17+
- pip install -e ".[async]"
18+
- pip install "pytest-asyncio<1"
1919
- travis_retry pytest tests/async_scenario_tests/
20+
# testing for adapters
21+
- pip install -e ".[adapter]"
22+
- travis_retry pytest tests/adapter_tests/
2023
# run all tests just in case
2124
- travis_retry python setup.py test
2225
# Run pytype only for Python 3.8
23-
- if [ ${TRAVIS_PYTHON_VERSION:0:3} == "3.8" ]; then pip install -e ".[adapter]" && pytype slack_bolt/; fi
26+
- if [ ${TRAVIS_PYTHON_VERSION:0:3} == "3.8" ]; then pip install "pytype" && pytype slack_bolt/; fi
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
slack_sdk==3.0.0a3
1+
slack_sdk
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
slack_sdk==3.0.0a3
1+
slack_sdk

scripts/install_all_and_run_tests.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ test_target="$1"
1111
if [[ $test_target != "" ]]
1212
then
1313
pip install -e ".[testing]" && \
14+
pip install -e ".[adapter]" && \
1415
black slack_bolt/ tests/ && \
1516
pytest $1
1617
else
1718
pip install -e ".[testing]" && \
19+
pip install -e ".[adapter]" && \
1820
black slack_bolt/ tests/ && \
1921
pytest && \
2022
pytype slack_bolt/

setup.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"aiohttp>=3,<4",
5050
# used only under src/slack_bolt/adapter
5151
"boto3<=2",
52+
"moto<=2", # For AWS tests
5253
"bottle>=0.12,<1",
5354
"chalice>=1,<2",
5455
"click>=7,<8", # for chalice
@@ -59,6 +60,7 @@
5960
"pyramid>=1,<2",
6061
"sanic>=20,<21",
6162
"starlette>=0.13,<1",
63+
"requests>=2,<3", # For starlette's TestClient
6264
"tornado>=6,<7",
6365
# server
6466
"uvicorn<1",

slack_bolt/adapter/aws_lambda/chalice_lazy_listener_runner.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import json
22
from logging import Logger
3-
from typing import Callable
3+
from typing import Callable, Optional, Any
44

55
import boto3
66

@@ -9,11 +9,14 @@
99

1010

1111
class ChaliceLazyListenerRunner(LazyListenerRunner):
12-
def __init__(self, logger: Logger):
13-
self.lambda_client = boto3.client("lambda")
12+
def __init__(self, logger: Logger, lambda_client: Optional[Any] = None):
13+
self.lambda_client = lambda_client
1414
self.logger = logger
1515

1616
def start(self, function: Callable[..., None], request: BoltRequest) -> None:
17+
if self.lambda_client is None:
18+
self.lambda_client = boto3.client("lambda")
19+
1720
chalice_request: dict = request.context["chalice_request"]
1821
request.headers["x-slack-bolt-lazy-only"] = ["1"]
1922
request.headers["x-slack-bolt-lazy-function-name"] = [

slack_bolt/adapter/aws_lambda/lazy_listener_runner.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import json
22
from logging import Logger
3-
from typing import Callable
3+
from typing import Callable, Optional, Any
44

55
import boto3
66

@@ -9,11 +9,14 @@
99

1010

1111
class LambdaLazyListenerRunner(LazyListenerRunner):
12-
def __init__(self, logger: Logger):
13-
self.lambda_client = boto3.client("lambda")
12+
def __init__(self, logger: Logger, lambda_client: Optional[Any] = None):
13+
self.lambda_client = lambda_client
1414
self.logger = logger
1515

1616
def start(self, function: Callable[..., None], request: BoltRequest) -> None:
17+
if self.lambda_client is None:
18+
self.lambda_client = boto3.client("lambda")
19+
1720
event: dict = request.context["lambda_request"]
1821
headers = event["headers"]
1922
headers["x-slack-bolt-lazy-only"] = "1" # not an array

tests/adapter_tests/__init__.py

Whitespace-only changes.
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import json
2+
import re
3+
from time import time
4+
5+
from fastapi import FastAPI
6+
from slack_sdk.signature import SignatureVerifier
7+
from slack_sdk.web.async_client import AsyncWebClient
8+
from starlette.requests import Request
9+
from starlette.testclient import TestClient
10+
11+
from slack_bolt.adapter.fastapi import AsyncSlackRequestHandler
12+
from slack_bolt.app.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+
)
17+
from tests.utils import remove_os_env_temporarily, restore_os_env
18+
19+
20+
class TestFastAPI:
21+
signing_secret = "secret"
22+
valid_token = "xoxb-valid"
23+
mock_api_server_base_url = "http://localhost:8888"
24+
signature_verifier = SignatureVerifier(signing_secret)
25+
web_client = AsyncWebClient(token=valid_token, base_url=mock_api_server_base_url,)
26+
27+
def setup_method(self):
28+
self.old_os_env = remove_os_env_temporarily()
29+
setup_mock_web_api_server(self)
30+
31+
def teardown_method(self):
32+
cleanup_mock_web_api_server(self)
33+
restore_os_env(self.old_os_env)
34+
35+
def generate_signature(self, body: str, timestamp: str):
36+
return self.signature_verifier.generate_signature(
37+
body=body, timestamp=timestamp,
38+
)
39+
40+
def build_headers(self, timestamp: str, body: str):
41+
return {
42+
"content-type": "application/x-www-form-urlencoded",
43+
"x-slack-signature": self.generate_signature(body, timestamp),
44+
"x-slack-request-timestamp": timestamp,
45+
}
46+
47+
def test_events(self):
48+
app = AsyncApp(client=self.web_client, signing_secret=self.signing_secret,)
49+
50+
async def event_handler():
51+
pass
52+
53+
app.event("app_mention")(event_handler)
54+
55+
payload = {
56+
"token": "verification_token",
57+
"team_id": "T111",
58+
"enterprise_id": "E111",
59+
"api_app_id": "A111",
60+
"event": {
61+
"client_msg_id": "9cbd4c5b-7ddf-4ede-b479-ad21fca66d63",
62+
"type": "app_mention",
63+
"text": "<@W111> Hi there!",
64+
"user": "W222",
65+
"ts": "1595926230.009600",
66+
"team": "T111",
67+
"channel": "C111",
68+
"event_ts": "1595926230.009600",
69+
},
70+
"type": "event_callback",
71+
"event_id": "Ev111",
72+
"event_time": 1595926230,
73+
"authed_users": ["W111"],
74+
}
75+
timestamp, body = str(int(time())), json.dumps(payload)
76+
77+
api = FastAPI()
78+
app_handler = AsyncSlackRequestHandler(app)
79+
80+
@api.post("/slack/events")
81+
async def endpoint(req: Request):
82+
return await app_handler.handle(req)
83+
84+
client = TestClient(api)
85+
response = client.post(
86+
"/slack/events", data=body, headers=self.build_headers(timestamp, body),
87+
)
88+
assert response.status_code == 200
89+
assert self.mock_received_requests["/auth.test"] == 1
90+
91+
def test_shortcuts(self):
92+
app = AsyncApp(client=self.web_client, signing_secret=self.signing_secret,)
93+
94+
async def shortcut_handler(ack):
95+
await ack()
96+
97+
app.shortcut("test-shortcut")(shortcut_handler)
98+
99+
payload = {
100+
"type": "shortcut",
101+
"token": "verification_token",
102+
"action_ts": "111.111",
103+
"team": {
104+
"id": "T111",
105+
"domain": "workspace-domain",
106+
"enterprise_id": "E111",
107+
"enterprise_name": "Org Name",
108+
},
109+
"user": {"id": "W111", "username": "primary-owner", "team_id": "T111"},
110+
"callback_id": "test-shortcut",
111+
"trigger_id": "111.111.xxxxxx",
112+
}
113+
114+
timestamp, body = str(int(time())), json.dumps(payload)
115+
116+
api = FastAPI()
117+
app_handler = AsyncSlackRequestHandler(app)
118+
119+
@api.post("/slack/events")
120+
async def endpoint(req: Request):
121+
return await app_handler.handle(req)
122+
123+
client = TestClient(api)
124+
response = client.post(
125+
"/slack/events", data=body, headers=self.build_headers(timestamp, body),
126+
)
127+
assert response.status_code == 200
128+
assert self.mock_received_requests["/auth.test"] == 1
129+
130+
def test_commands(self):
131+
app = AsyncApp(client=self.web_client, signing_secret=self.signing_secret,)
132+
133+
async def command_handler(ack):
134+
await ack()
135+
136+
app.command("/hello-world")(command_handler)
137+
138+
payload = (
139+
"token=verification_token"
140+
"&team_id=T111"
141+
"&team_domain=test-domain"
142+
"&channel_id=C111"
143+
"&channel_name=random"
144+
"&user_id=W111"
145+
"&user_name=primary-owner"
146+
"&command=%2Fhello-world"
147+
"&text=Hi"
148+
"&enterprise_id=E111"
149+
"&enterprise_name=Org+Name"
150+
"&response_url=https%3A%2F%2Fhooks.slack.com%2Fcommands%2FT111%2F111%2Fxxxxx"
151+
"&trigger_id=111.111.xxx"
152+
)
153+
timestamp, body = str(int(time())), json.dumps(payload)
154+
155+
api = FastAPI()
156+
app_handler = AsyncSlackRequestHandler(app)
157+
158+
@api.post("/slack/events")
159+
async def endpoint(req: Request):
160+
return await app_handler.handle(req)
161+
162+
client = TestClient(api)
163+
response = client.post(
164+
"/slack/events", data=body, headers=self.build_headers(timestamp, body),
165+
)
166+
assert response.status_code == 200
167+
assert self.mock_received_requests["/auth.test"] == 1
168+
169+
def test_oauth(self):
170+
app = AsyncApp(
171+
client=self.web_client,
172+
signing_secret=self.signing_secret,
173+
client_id="111.111",
174+
client_secret="xxx",
175+
scopes=["chat:write", "commands"],
176+
)
177+
api = FastAPI()
178+
app_handler = AsyncSlackRequestHandler(app)
179+
180+
@api.get("/slack/install")
181+
async def endpoint(req: Request):
182+
return await app_handler.handle(req)
183+
184+
client = TestClient(api)
185+
response = client.get("/slack/install", allow_redirects=False)
186+
assert response.status_code == 302
187+
assert re.match(
188+
"https://slack.com/oauth/v2/authorize\\?state=[^&]+&client_id=111.111&scope=chat:write,commands&user_scope=",
189+
response.headers["Location"],
190+
)

0 commit comments

Comments
 (0)