diff --git a/samcli/local/apigw/local_apigw_service.py b/samcli/local/apigw/local_apigw_service.py index 088f34b17e..43c9260251 100644 --- a/samcli/local/apigw/local_apigw_service.py +++ b/samcli/local/apigw/local_apigw_service.py @@ -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) @@ -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): """ diff --git a/samcli/local/events/api_event.py b/samcli/local/events/api_event.py index beaf5e71b5..7f6bc74149 100644 --- a/samcli/local/events/api_event.py +++ b/samcli/local/events/api_event.py @@ -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 diff --git a/tests/integration/local/start_api/test_start_api.py b/tests/integration/local/start_api/test_start_api.py index 947139a224..d0e7558deb 100644 --- a/tests/integration/local/start_api/test_start_api.py +++ b/tests/integration/local/start_api/test_start_api.py @@ -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 diff --git a/tests/unit/local/apigw/test_local_apigw_service.py b/tests/unit/local/apigw/test_local_apigw_service.py index 454f1eec7f..a416b84d15 100644 --- a/tests/unit/local/apigw/test_local_apigw_service.py +++ b/tests/unit/local/apigw/test_local_apigw_service.py @@ -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): @@ -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() @@ -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", diff --git a/tests/unit/local/events/test_api_event.py b/tests/unit/local/events/test_api_event.py index c5c629d583..8b0aa9d8b9 100644 --- a/tests/unit/local/events/test_api_event.py +++ b/tests/unit/local/events/test_api_event.py @@ -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"}, @@ -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"},