Skip to content

Commit

Permalink
Add tests for flask/lambda/starlette adapters (slackapi#49)
Browse files Browse the repository at this point in the history
* Add tests for flask/lambda/starlette adapters
* Add requests
* Update tests
* Make boto3 client lazy
  • Loading branch information
seratch authored Aug 27, 2020
1 parent d1b1204 commit e93c62f
Show file tree
Hide file tree
Showing 14 changed files with 1,166 additions and 11 deletions.
9 changes: 6 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,17 @@ install:
# https://discuss.python.org/t/announcement-pip-20-2-release/4863
- pip config set global.use-feature 2020-resolver
- pip install "pytest>=5,<6"
- pip install "pytype"
script:
# testing without aiohttp
- travis_retry pytest tests/scenario_tests/
# testing with aiohttp
- pip install "pytest-asyncio<1" "aiohttp>=3,<4"
- pip install -e ".[async]"
- pip install "pytest-asyncio<1"
- travis_retry pytest tests/async_scenario_tests/
# testing for adapters
- pip install -e ".[adapter]"
- travis_retry pytest tests/adapter_tests/
# run all tests just in case
- travis_retry python setup.py test
# Run pytype only for Python 3.8
- if [ ${TRAVIS_PYTHON_VERSION:0:3} == "3.8" ]; then pip install -e ".[adapter]" && pytype slack_bolt/; fi
- if [ ${TRAVIS_PYTHON_VERSION:0:3} == "3.8" ]; then pip install "pytype" && pytype slack_bolt/; fi
2 changes: 1 addition & 1 deletion samples/aws_lambda/requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
slack_sdk==3.0.0a3
slack_sdk
2 changes: 1 addition & 1 deletion samples/aws_lambda/requirements_oauth.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
slack_sdk==3.0.0a3
slack_sdk
2 changes: 2 additions & 0 deletions scripts/install_all_and_run_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ test_target="$1"
if [[ $test_target != "" ]]
then
pip install -e ".[testing]" && \
pip install -e ".[adapter]" && \
black slack_bolt/ tests/ && \
pytest $1
else
pip install -e ".[testing]" && \
pip install -e ".[adapter]" && \
black slack_bolt/ tests/ && \
pytest && \
pytype slack_bolt/
Expand Down
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"aiohttp>=3,<4",
# used only under src/slack_bolt/adapter
"boto3<=2",
"moto<=2", # For AWS tests
"bottle>=0.12,<1",
"chalice>=1,<2",
"click>=7,<8", # for chalice
Expand All @@ -59,6 +60,7 @@
"pyramid>=1,<2",
"sanic>=20,<21",
"starlette>=0.13,<1",
"requests>=2,<3", # For starlette's TestClient
"tornado>=6,<7",
# server
"uvicorn<1",
Expand Down
9 changes: 6 additions & 3 deletions slack_bolt/adapter/aws_lambda/chalice_lazy_listener_runner.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import json
from logging import Logger
from typing import Callable
from typing import Callable, Optional, Any

import boto3

Expand All @@ -9,11 +9,14 @@


class ChaliceLazyListenerRunner(LazyListenerRunner):
def __init__(self, logger: Logger):
self.lambda_client = boto3.client("lambda")
def __init__(self, logger: Logger, lambda_client: Optional[Any] = None):
self.lambda_client = lambda_client
self.logger = logger

def start(self, function: Callable[..., None], request: BoltRequest) -> None:
if self.lambda_client is None:
self.lambda_client = boto3.client("lambda")

chalice_request: dict = request.context["chalice_request"]
request.headers["x-slack-bolt-lazy-only"] = ["1"]
request.headers["x-slack-bolt-lazy-function-name"] = [
Expand Down
9 changes: 6 additions & 3 deletions slack_bolt/adapter/aws_lambda/lazy_listener_runner.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import json
from logging import Logger
from typing import Callable
from typing import Callable, Optional, Any

import boto3

Expand All @@ -9,11 +9,14 @@


class LambdaLazyListenerRunner(LazyListenerRunner):
def __init__(self, logger: Logger):
self.lambda_client = boto3.client("lambda")
def __init__(self, logger: Logger, lambda_client: Optional[Any] = None):
self.lambda_client = lambda_client
self.logger = logger

def start(self, function: Callable[..., None], request: BoltRequest) -> None:
if self.lambda_client is None:
self.lambda_client = boto3.client("lambda")

event: dict = request.context["lambda_request"]
headers = event["headers"]
headers["x-slack-bolt-lazy-only"] = "1" # not an array
Expand Down
Empty file added tests/adapter_tests/__init__.py
Empty file.
190 changes: 190 additions & 0 deletions tests/adapter_tests/test_async_fastapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import json
import re
from time import time

from fastapi import FastAPI
from slack_sdk.signature import SignatureVerifier
from slack_sdk.web.async_client import AsyncWebClient
from starlette.requests import Request
from starlette.testclient import TestClient

from slack_bolt.adapter.fastapi import AsyncSlackRequestHandler
from slack_bolt.app.async_app import AsyncApp
from tests.mock_web_api_server import (
setup_mock_web_api_server,
cleanup_mock_web_api_server,
)
from tests.utils import remove_os_env_temporarily, restore_os_env


class TestFastAPI:
signing_secret = "secret"
valid_token = "xoxb-valid"
mock_api_server_base_url = "http://localhost:8888"
signature_verifier = SignatureVerifier(signing_secret)
web_client = AsyncWebClient(token=valid_token, base_url=mock_api_server_base_url,)

def setup_method(self):
self.old_os_env = remove_os_env_temporarily()
setup_mock_web_api_server(self)

def teardown_method(self):
cleanup_mock_web_api_server(self)
restore_os_env(self.old_os_env)

def generate_signature(self, body: str, timestamp: str):
return self.signature_verifier.generate_signature(
body=body, timestamp=timestamp,
)

def build_headers(self, timestamp: str, body: str):
return {
"content-type": "application/x-www-form-urlencoded",
"x-slack-signature": self.generate_signature(body, timestamp),
"x-slack-request-timestamp": timestamp,
}

def test_events(self):
app = AsyncApp(client=self.web_client, signing_secret=self.signing_secret,)

async def event_handler():
pass

app.event("app_mention")(event_handler)

payload = {
"token": "verification_token",
"team_id": "T111",
"enterprise_id": "E111",
"api_app_id": "A111",
"event": {
"client_msg_id": "9cbd4c5b-7ddf-4ede-b479-ad21fca66d63",
"type": "app_mention",
"text": "<@W111> Hi there!",
"user": "W222",
"ts": "1595926230.009600",
"team": "T111",
"channel": "C111",
"event_ts": "1595926230.009600",
},
"type": "event_callback",
"event_id": "Ev111",
"event_time": 1595926230,
"authed_users": ["W111"],
}
timestamp, body = str(int(time())), json.dumps(payload)

api = FastAPI()
app_handler = AsyncSlackRequestHandler(app)

@api.post("/slack/events")
async def endpoint(req: Request):
return await app_handler.handle(req)

client = TestClient(api)
response = client.post(
"/slack/events", data=body, headers=self.build_headers(timestamp, body),
)
assert response.status_code == 200
assert self.mock_received_requests["/auth.test"] == 1

def test_shortcuts(self):
app = AsyncApp(client=self.web_client, signing_secret=self.signing_secret,)

async def shortcut_handler(ack):
await ack()

app.shortcut("test-shortcut")(shortcut_handler)

payload = {
"type": "shortcut",
"token": "verification_token",
"action_ts": "111.111",
"team": {
"id": "T111",
"domain": "workspace-domain",
"enterprise_id": "E111",
"enterprise_name": "Org Name",
},
"user": {"id": "W111", "username": "primary-owner", "team_id": "T111"},
"callback_id": "test-shortcut",
"trigger_id": "111.111.xxxxxx",
}

timestamp, body = str(int(time())), json.dumps(payload)

api = FastAPI()
app_handler = AsyncSlackRequestHandler(app)

@api.post("/slack/events")
async def endpoint(req: Request):
return await app_handler.handle(req)

client = TestClient(api)
response = client.post(
"/slack/events", data=body, headers=self.build_headers(timestamp, body),
)
assert response.status_code == 200
assert self.mock_received_requests["/auth.test"] == 1

def test_commands(self):
app = AsyncApp(client=self.web_client, signing_secret=self.signing_secret,)

async def command_handler(ack):
await ack()

app.command("/hello-world")(command_handler)

payload = (
"token=verification_token"
"&team_id=T111"
"&team_domain=test-domain"
"&channel_id=C111"
"&channel_name=random"
"&user_id=W111"
"&user_name=primary-owner"
"&command=%2Fhello-world"
"&text=Hi"
"&enterprise_id=E111"
"&enterprise_name=Org+Name"
"&response_url=https%3A%2F%2Fhooks.slack.com%2Fcommands%2FT111%2F111%2Fxxxxx"
"&trigger_id=111.111.xxx"
)
timestamp, body = str(int(time())), json.dumps(payload)

api = FastAPI()
app_handler = AsyncSlackRequestHandler(app)

@api.post("/slack/events")
async def endpoint(req: Request):
return await app_handler.handle(req)

client = TestClient(api)
response = client.post(
"/slack/events", data=body, headers=self.build_headers(timestamp, body),
)
assert response.status_code == 200
assert self.mock_received_requests["/auth.test"] == 1

def test_oauth(self):
app = AsyncApp(
client=self.web_client,
signing_secret=self.signing_secret,
client_id="111.111",
client_secret="xxx",
scopes=["chat:write", "commands"],
)
api = FastAPI()
app_handler = AsyncSlackRequestHandler(app)

@api.get("/slack/install")
async def endpoint(req: Request):
return await app_handler.handle(req)

client = TestClient(api)
response = client.get("/slack/install", allow_redirects=False)
assert response.status_code == 302
assert re.match(
"https://slack.com/oauth/v2/authorize\\?state=[^&]+&client_id=111.111&scope=chat:write,commands&user_scope=",
response.headers["Location"],
)
Loading

0 comments on commit e93c62f

Please sign in to comment.