Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion samcli/local/apigw/local_apigw_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -779,7 +779,7 @@ def _construct_v_2_0_event_http(
# Flask does not parse/decode the request data. We should do it ourselves
request_data = request_data.decode("utf-8")

query_string_dict, _ = LocalApigwService._query_string_params(flask_request)
query_string_dict = LocalApigwService._query_string_params_v_2_0(flask_request)

cookies = LocalApigwService._event_http_cookies(flask_request)
headers = LocalApigwService._event_http_headers(flask_request, port)
Expand Down Expand Up @@ -843,6 +843,33 @@ def _query_string_params(flask_request):

return query_string_dict, multi_value_query_string_dict

@staticmethod
def _query_string_params_v_2_0(flask_request):
"""
Constructs an APIGW equivalent query string dictionary using the 2.0 format
https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html#2.0

Parameters
----------
flask_request request
Request from Flask

Returns dict (str: str)
-------
Empty dict if no query params where in the request otherwise returns a dictionary of key to value

"""
query_string_dict = {}

# Flask returns an ImmutableMultiDict so convert to a dictionary that becomes
# a dict(str: list) then iterate over
query_string_dict = {
query_string_key: ",".join(query_string_list)
for query_string_key, query_string_list in flask_request.args.lists()
}

return query_string_dict

@staticmethod
def _event_headers(flask_request, port):
"""
Expand Down
12 changes: 11 additions & 1 deletion samcli/local/events/api_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,13 +404,23 @@ def __init__(
if not isinstance(stage_variables, dict) and stage_variables is not None:
raise TypeError("'stage_variables' must be of type dict or None")

# convert mutlivalue queries into a comma separated list per API GW documentation for format v2
converted_query_string_params = None
if query_string_params is not None:
converted_query_string_params = {}
for k, v in query_string_params.items():
if isinstance(v, str):
converted_query_string_params[k] = v
else:
converted_query_string_params[k] = ",".join(v)

self.version = "2.0"
self.route_key = route_key
self.raw_path = raw_path
self.raw_query_string = raw_query_string
self.cookies = cookies
self.headers = headers
self.query_string_params = query_string_params
self.query_string_params = converted_query_string_params
self.request_context = request_context
self.body = body
self.path_parameters = path_parameters
Expand Down
45 changes: 45 additions & 0 deletions tests/integration/local/start_api/test_start_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1126,6 +1126,51 @@ def test_forward_headers_are_added_to_event(self):
self.assertEqual(response_data.get("multiValueHeaders").get("X-Forwarded-Port"), [self.port])


class TestServiceRequestsWithHttpApi(StartApiIntegBaseClass):
"""
Test Class centered around the different requests that can happen; specifically testing the change
in format for mulivalue query parameters in payload format v2 (HTTP APIs)
"""

template_path = "/testdata/start_api/swagger-template-http-api.yaml"

def setUp(self):
self.url = "http://127.0.0.1:{}".format(self.port)

@pytest.mark.flaky(reruns=3)
@pytest.mark.timeout(timeout=600, method="thread")
def test_request_with_multi_value_headers(self):
response = requests.get(
self.url + "/echoeventbody",
headers={"Content-Type": "application/x-www-form-urlencoded, image/gif"},
timeout=300,
)

self.assertEqual(response.status_code, 200)
response_data = response.json()
self.assertEqual(response_data.get("version"), "2.0")
self.assertIsNone(response_data.get("multiValueHeaders"))
self.assertEqual(
response_data.get("headers").get("Content-Type"), "application/x-www-form-urlencoded, image/gif"
)

@pytest.mark.flaky(reruns=3)
@pytest.mark.timeout(timeout=600, method="thread")
def test_request_with_list_of_query_params(self):
"""
Query params given should be put into the Event to Lambda
"""
response = requests.get(self.url + "/echoeventbody", params={"key": ["value", "value2"]}, timeout=300)

self.assertEqual(response.status_code, 200)

response_data = response.json()

self.assertEqual(response_data.get("version"), "2.0")
self.assertEqual(response_data.get("queryStringParameters"), {"key": "value,value2"})
self.assertIsNone(response_data.get("multiValueQueryStringParameters"))


class TestStartApiWithStage(StartApiIntegBaseClass):
"""
Test Class centered around the different responses that can happen in Lambda and pass through start-api
Expand Down
13 changes: 11 additions & 2 deletions tests/unit/local/apigw/test_local_apigw_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -1473,6 +1473,15 @@ def test_query_string_params_with_param_value_being_non_empty_list(self):
actual_query_string = LocalApigwService._query_string_params(request_mock)
self.assertEqual(actual_query_string, ({"param": "b"}, {"param": ["a", "b"]}))

def test_query_string_params_v_2_0_with_param_value_being_non_empty_list(self):
request_mock = Mock()
query_param_args_mock = Mock()
query_param_args_mock.lists.return_value = {"param": ["a", "b"]}.items()
request_mock.args = query_param_args_mock

actual_query_string = LocalApigwService._query_string_params_v_2_0(request_mock)
self.assertEqual(actual_query_string, {"param": "a,b"})


class TestService_construct_event_http(TestCase):
def setUp(self):
Expand All @@ -1483,7 +1492,7 @@ def setUp(self):
self.request_mock.get_data.return_value = b"DATA!!!!"
self.request_mock.mimetype = "application/json"
query_param_args_mock = Mock()
query_param_args_mock.lists.return_value = {"query": ["params"]}.items()
query_param_args_mock.lists.return_value = {"query": ["param1", "param2"]}.items()
self.request_mock.args = query_param_args_mock
self.request_mock.query_string = b"query=params"
headers_mock = Mock()
Expand Down Expand Up @@ -1514,7 +1523,7 @@ def setUp(self):
"X-Forwarded-Proto": "http",
"X-Forwarded-Port": "3000"
}},
"queryStringParameters": {{"query": "params"}},
"queryStringParameters": {{"query": "param1,param2"}},
"requestContext": {{
"accountId": "123456789012",
"apiId": "1234567890",
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/local/events/test_api_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ def test_to_dict(self):
"raw_query_string",
["cookie1=value1"],
{"header_key": "value"},
{"query_string": "some query"},
{"query_string": "some query", "multi": ["first", "second"]},
request_context_mock,
"body",
{"param": "some param"},
Expand All @@ -531,7 +531,7 @@ def test_to_dict(self):
"rawQueryString": "raw_query_string",
"cookies": ["cookie1=value1"],
"headers": {"header_key": "value"},
"queryStringParameters": {"query_string": "some query"},
"queryStringParameters": {"query_string": "some query", "multi": "first,second"},
"requestContext": request_context_mock.to_dict(),
"body": "body",
"pathParameters": {"param": "some param"},
Expand Down