Skip to content

Commit ae08015

Browse files
committed
Add tests
1 parent e70705a commit ae08015

File tree

12 files changed

+506
-4
lines changed

12 files changed

+506
-4
lines changed

codecov.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ coverage:
22
status:
33
project:
44
default:
5-
threshold: 0.3%
5+
threshold: 2.0%
66
patch:
77
default:
88
target: 50%

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"pytest-cov>=2,<3",
1717
"pytest-asyncio<1", # for async
1818
"aiohttp>=3,<4", # for async
19+
"Flask-Sockets>=0.2,<1",
1920
"black==20.8b1",
2021
]
2122

@@ -33,7 +34,7 @@
3334
exclude=["examples", "integration_tests", "tests", "tests.*",]
3435
),
3536
include_package_data=True, # MANIFEST.in
36-
install_requires=["slack_sdk>=3.2.0b6,<3.3",],
37+
install_requires=["slack_sdk>=3.2.0b7,<3.3",],
3738
setup_requires=["pytest-runner==5.2"],
3839
tests_require=test_dependencies,
3940
test_suite="tests",

slack_bolt/adapter/socket_mode/async_internals.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ async def send_async_response(
3737
else:
3838
await client.send_socket_mode_response(
3939
SocketModeResponse(
40-
envelope_id=req.envelope_id, payload={"text": bolt_resp.body},
40+
envelope_id=req.envelope_id,
41+
payload={"text": bolt_resp.body},
4142
)
4243
)
4344
if client.logger.level <= logging.DEBUG:

slack_bolt/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.2.0b3"
1+
__version__ = "1.2.0b4 "

tests/adapter_tests/socket_mode/__init__.py

Whitespace-only changes.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import logging
2+
from typing import List
3+
4+
socket_mode_envelopes = [
5+
"""{"envelope_id":"57d6a792-4d35-4d0b-b6aa-3361493e1caf","payload":{"type":"shortcut","token":"xxx","action_ts":"1610198080.300836","team":{"id":"T111","domain":"seratch"},"user":{"id":"U111","username":"seratch","team_id":"T111"},"is_enterprise_install":false,"enterprise":null,"callback_id":"do-something","trigger_id":"111.222.xxx"},"type":"interactive","accepts_response_payload":false}""",
6+
"""{"envelope_id":"1d3c79ab-0ffb-41f3-a080-d19e85f53649","payload":{"token":"xxx","team_id":"T111","team_domain":"xxx","channel_id":"C111","channel_name":"random","user_id":"U111","user_name":"seratch","command":"/hello-socket-mode","text":"","api_app_id":"A111","response_url":"https://hooks.slack.com/commands/T111/111/xxx","trigger_id":"111.222.xxx"},"type":"slash_commands","accepts_response_payload":true}""",
7+
"""{"envelope_id":"08cfc559-d933-402e-a5c1-79e135afaae4","payload":{"token":"xxx","team_id":"T111","api_app_id":"A111","event":{"client_msg_id":"c9b466b5-845c-49c6-a371-57ae44359bf1","type":"message","text":"<@W111>","user":"U111","ts":"1610197986.000300","team":"T111","blocks":[{"type":"rich_text","block_id":"1HBPc","elements":[{"type":"rich_text_section","elements":[{"type":"user","user_id":"U111"}]}]}],"channel":"C111","event_ts":"1610197986.000300","channel_type":"channel"},"type":"event_callback","event_id":"Ev111","event_time":1610197986,"authorizations":[{"enterprise_id":null,"team_id":"T111","user_id":"U111","is_bot":true,"is_enterprise_install":false}],"is_ext_shared_channel":false,"event_context":"1-message-T111-C111"},"type":"events_api","accepts_response_payload":false,"retry_attempt":1,"retry_reason":"timeout"}""",
8+
]
9+
10+
from flask import Flask
11+
from flask_sockets import Sockets
12+
13+
14+
def start_socket_mode_server(self, port: int):
15+
def _start_socket_mode_server():
16+
logger = logging.getLogger(__name__)
17+
app: Flask = Flask(__name__)
18+
sockets: Sockets = Sockets(app)
19+
20+
envelopes_to_consume: List[str] = list(socket_mode_envelopes)
21+
22+
@sockets.route("/link")
23+
def link(ws):
24+
while not ws.closed:
25+
message = ws.read_message()
26+
if message is not None:
27+
if len(envelopes_to_consume) > 0:
28+
e = envelopes_to_consume.pop(0)
29+
logger.debug(f"Send an envelope: {e}")
30+
ws.send(e)
31+
32+
logger.debug(f"Server received a message: {message}")
33+
ws.send(message)
34+
35+
from gevent import pywsgi
36+
from geventwebsocket.handler import WebSocketHandler
37+
38+
server = pywsgi.WSGIServer(("", port), app, handler_class=WebSocketHandler)
39+
self.server = server
40+
server.serve_forever(stop_timeout=1)
41+
42+
return _start_socket_mode_server
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import json
2+
import logging
3+
import re
4+
import threading
5+
from http import HTTPStatus
6+
from http.server import HTTPServer, SimpleHTTPRequestHandler
7+
from typing import Type
8+
from unittest import TestCase
9+
from urllib.parse import urlparse, parse_qs
10+
11+
12+
class MockHandler(SimpleHTTPRequestHandler):
13+
protocol_version = "HTTP/1.1"
14+
default_request_version = "HTTP/1.1"
15+
logger = logging.getLogger(__name__)
16+
17+
pattern_for_language = re.compile("python/(\\S+)", re.IGNORECASE)
18+
pattern_for_package_identifier = re.compile("slackclient/(\\S+)")
19+
20+
def is_valid_user_agent(self):
21+
user_agent = self.headers["User-Agent"]
22+
return self.pattern_for_language.search(
23+
user_agent
24+
) and self.pattern_for_package_identifier.search(user_agent)
25+
26+
def is_valid_token(self):
27+
if self.path.startswith("oauth"):
28+
return True
29+
return "Authorization" in self.headers and (
30+
str(self.headers["Authorization"]).startswith("Bearer xoxb-")
31+
or str(self.headers["Authorization"]).startswith("Bearer xapp-")
32+
)
33+
34+
def set_common_headers(self):
35+
self.send_header("content-type", "application/json;charset=utf-8")
36+
self.send_header("connection", "close")
37+
self.end_headers()
38+
39+
invalid_auth = {
40+
"ok": False,
41+
"error": "invalid_auth",
42+
}
43+
44+
not_found = {
45+
"ok": False,
46+
"error": "test_data_not_found",
47+
}
48+
49+
def _handle(self):
50+
try:
51+
if self.is_valid_token() and self.is_valid_user_agent():
52+
parsed_path = urlparse(self.path)
53+
54+
len_header = self.headers.get("Content-Length") or 0
55+
content_len = int(len_header)
56+
post_body = self.rfile.read(content_len)
57+
request_body = None
58+
if post_body:
59+
try:
60+
post_body = post_body.decode("utf-8")
61+
if post_body.startswith("{"):
62+
request_body = json.loads(post_body)
63+
else:
64+
request_body = {
65+
k: v[0] for k, v in parse_qs(post_body).items()
66+
}
67+
except UnicodeDecodeError:
68+
pass
69+
else:
70+
if parsed_path and parsed_path.query:
71+
request_body = {
72+
k: v[0] for k, v in parse_qs(parsed_path.query).items()
73+
}
74+
75+
body = {"ok": False, "error": "internal_error"}
76+
if self.path == "/auth.test":
77+
body = {
78+
"ok": True,
79+
"url": "https://xyz.slack.com/",
80+
"team": "Testing Workspace",
81+
"user": "bot-user",
82+
"team_id": "T111",
83+
"user_id": "W11",
84+
"bot_id": "B111",
85+
"enterprise_id": "E111",
86+
"is_enterprise_install": False,
87+
}
88+
if self.path == "/apps.connections.open":
89+
body = {
90+
"ok": True,
91+
"url": "ws://localhost:3011/link/?ticket=xxx&app_id=yyy",
92+
}
93+
if self.path == "/api.test" and request_body:
94+
body = {"ok": True, "args": request_body}
95+
else:
96+
body = self.invalid_auth
97+
98+
if not body:
99+
body = self.not_found
100+
101+
self.send_response(HTTPStatus.OK)
102+
self.set_common_headers()
103+
self.wfile.write(json.dumps(body).encode("utf-8"))
104+
self.wfile.close()
105+
106+
except Exception as e:
107+
self.logger.error(str(e), exc_info=True)
108+
raise
109+
110+
def do_GET(self):
111+
self._handle()
112+
113+
def do_POST(self):
114+
self._handle()
115+
116+
117+
class MockServerThread(threading.Thread):
118+
def __init__(
119+
self, test: TestCase, handler: Type[SimpleHTTPRequestHandler] = MockHandler
120+
):
121+
threading.Thread.__init__(self)
122+
self.handler = handler
123+
self.test = test
124+
125+
def run(self):
126+
self.server = HTTPServer(("localhost", 8888), self.handler)
127+
self.test.server_url = "http://localhost:8888"
128+
self.test.host, self.test.port = self.server.socket.getsockname()
129+
self.test.server_started.set() # threading.Event()
130+
131+
self.test = None
132+
try:
133+
self.server.serve_forever()
134+
finally:
135+
self.server.server_close()
136+
137+
def stop(self):
138+
self.server.shutdown()
139+
self.join()
140+
141+
142+
def setup_mock_web_api_server(test: TestCase):
143+
test.server_started = threading.Event()
144+
test.thread = MockServerThread(test)
145+
test.thread.start()
146+
147+
test.server_started.wait()
148+
149+
150+
def cleanup_mock_web_api_server(test: TestCase):
151+
test.thread.stop()
152+
153+
test.thread = None
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import logging
2+
import time
3+
from threading import Thread
4+
5+
from slack_sdk import WebClient
6+
7+
from slack_bolt import App
8+
from slack_bolt.adapter.socket_mode import SocketModeHandler
9+
from .mock_socket_mode_server import (
10+
start_socket_mode_server,
11+
)
12+
from .mock_web_api_server import (
13+
setup_mock_web_api_server,
14+
cleanup_mock_web_api_server,
15+
)
16+
from ...utils import remove_os_env_temporarily, restore_os_env
17+
18+
19+
class TestSocketModeBuiltin:
20+
logger = logging.getLogger(__name__)
21+
22+
def setup_method(self):
23+
self.old_os_env = remove_os_env_temporarily()
24+
setup_mock_web_api_server(self)
25+
self.web_client = WebClient(
26+
token="xoxb-api_test",
27+
base_url="http://localhost:8888",
28+
)
29+
30+
def teardown_method(self):
31+
cleanup_mock_web_api_server(self)
32+
restore_os_env(self.old_os_env)
33+
34+
def test_interactions(self):
35+
t = Thread(target=start_socket_mode_server(self, 3011))
36+
t.daemon = True
37+
t.start()
38+
time.sleep(2) # wait for the server
39+
40+
app = App(client=self.web_client)
41+
42+
result = {"shortcut": False, "command": False}
43+
44+
@app.shortcut("do-something")
45+
def shortcut_handler(ack):
46+
result["shortcut"] = True
47+
ack()
48+
49+
@app.command("/hello-socket-mode")
50+
def command_handler(ack):
51+
result["command"] = True
52+
ack()
53+
54+
handler = SocketModeHandler(
55+
app_token="xapp-A111-222-xyz",
56+
app=app,
57+
trace_enabled=True,
58+
)
59+
try:
60+
handler.client.ping_pong_trace_enabled = True
61+
handler.client.wss_uri = "ws://127.0.0.1:3011/link"
62+
63+
handler.connect()
64+
assert handler.client.is_connected() is True
65+
time.sleep(2) # wait for the message receiver
66+
67+
handler.client.send_message("foo")
68+
69+
time.sleep(2)
70+
assert result["shortcut"] is True
71+
assert result["command"] is True
72+
finally:
73+
handler.client.close()
74+
self.server.stop()
75+
self.server.close()
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import logging
2+
import time
3+
from threading import Thread
4+
5+
from slack_sdk import WebClient
6+
7+
from slack_bolt import App
8+
from slack_bolt.adapter.socket_mode.websocket_client import SocketModeHandler
9+
from .mock_socket_mode_server import (
10+
start_socket_mode_server,
11+
)
12+
from .mock_web_api_server import (
13+
setup_mock_web_api_server,
14+
cleanup_mock_web_api_server,
15+
)
16+
from ...utils import remove_os_env_temporarily, restore_os_env
17+
18+
19+
class TestSocketModeWebsocketClient:
20+
logger = logging.getLogger(__name__)
21+
22+
def setup_method(self):
23+
self.old_os_env = remove_os_env_temporarily()
24+
setup_mock_web_api_server(self)
25+
self.web_client = WebClient(
26+
token="xoxb-api_test",
27+
base_url="http://localhost:8888",
28+
)
29+
30+
def teardown_method(self):
31+
cleanup_mock_web_api_server(self)
32+
restore_os_env(self.old_os_env)
33+
34+
def test_interactions(self):
35+
t = Thread(target=start_socket_mode_server(self, 3012))
36+
t.daemon = True
37+
t.start()
38+
time.sleep(1) # wait for the server
39+
40+
app = App(client=self.web_client)
41+
42+
result = {"shortcut": False, "command": False}
43+
44+
@app.shortcut("do-something")
45+
def shortcut_handler(ack):
46+
result["shortcut"] = True
47+
ack()
48+
49+
@app.command("/hello-socket-mode")
50+
def command_handler(ack):
51+
result["command"] = True
52+
ack()
53+
54+
handler = SocketModeHandler(
55+
app_token="xapp-A111-222-xyz",
56+
app=app,
57+
trace_enabled=True,
58+
)
59+
try:
60+
handler.client.wss_uri = "ws://localhost:3012/link"
61+
62+
handler.connect()
63+
assert handler.client.is_connected() is True
64+
time.sleep(2) # wait for the message receiver
65+
66+
handler.client.send_message("foo")
67+
68+
time.sleep(2)
69+
assert result["shortcut"] is True
70+
assert result["command"] is True
71+
finally:
72+
handler.client.close()
73+
self.server.stop()
74+
self.server.close()

tests/adapter_tests_async/socket_mode/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)