Skip to content

Commit 5b79f77

Browse files
committed
add tests and validation assumptions
1 parent ba08d95 commit 5b79f77

File tree

4 files changed

+344
-21
lines changed

4 files changed

+344
-21
lines changed

localstack-core/localstack/services/apigateway/next_gen/execute_api/handlers/integration_response.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from ..api import RestApiGatewayHandler, RestApiGatewayHandlerChain
88
from ..context import RestApiInvocationContext
9-
from ..gateway_response import Default5xxError
9+
from ..gateway_response import ApiConfigurationError
1010
from ..parameters_mapping import ParametersMapper, ResponseDataMapping
1111

1212
LOG = logging.getLogger(__name__)
@@ -43,7 +43,7 @@ def __call__(
4343
# maybe we could cache the result in the context? in an IntegrationDetails field?
4444
is_lambda = False
4545
if integration_type == IntegrationType.AWS and is_lambda:
46-
# fetch errorMessage
46+
# TODO: fetch errorMessage
4747
selection_value = ""
4848
else:
4949
selection_value = str(response.status_code)
@@ -113,8 +113,13 @@ def select_integration_response(
113113
if not response.get("selectionPattern")
114114
]
115115
if not default_responses:
116-
# TODO: verify exception
117-
raise Default5xxError("Internal server error")
116+
# TODO: verify log message when the selection_value is a lambda errorMessage
117+
LOG.warning(
118+
"Configuration error: No match for output mapping and no default output mapping configured. "
119+
"Endpoint Response Status Code: %s",
120+
selection_value,
121+
)
122+
raise ApiConfigurationError("Internal server error")
118123

119124
selected_response = default_responses[0]
120125
if len(default_responses) > 1:

localstack-core/localstack/services/apigateway/next_gen/execute_api/parameters_mapping.py

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from localstack.utils.strings import to_str
1616

1717
from .context import InvocationRequest
18-
from .gateway_response import Default4xxError
18+
from .gateway_response import Default4xxError, Default5xxError
1919
from .variables import ContextVariables
2020

2121
LOG = logging.getLogger(__name__)
@@ -81,13 +81,16 @@ def map_integration_response(
8181
) -> ResponseDataMapping:
8282
response_data_mapping = ResponseDataMapping(header={})
8383

84+
# storing the case-sensitive headers once, the mapping is strict
85+
case_sensitive_headers = dict(integration_response.headers.items())
86+
8487
for response_mapping, integration_mapping in response_parameters.items():
8588
header_name = response_mapping.removeprefix("method.response.header.")
8689

8790
if integration_mapping.startswith("integration.response."):
8891
method_req_expr = integration_mapping.removeprefix("integration.response.")
8992
value = self._retrieve_parameter_from_integration_response(
90-
method_req_expr, integration_response
93+
method_req_expr, integration_response, case_sensitive_headers
9194
)
9295
else:
9396
value = self._retrieve_parameter_from_variables_and_static(
@@ -131,6 +134,7 @@ def _retrieve_parameter_from_integration_response(
131134
self,
132135
expr: str,
133136
integration_response: Response,
137+
case_sensitive_headers: dict[str, str],
134138
) -> str | None:
135139
"""
136140
See https://docs.aws.amazon.com/apigateway/latest/developerguide/request-response-data-mappings.html#mapping-response-parameters
@@ -141,16 +145,16 @@ def _retrieve_parameter_from_integration_response(
141145
:return: the value to map in the ResponseDataMapping
142146
"""
143147
if expr.startswith("body"):
144-
body = integration_response.get_data(as_text=True)
148+
body = integration_response.get_data() or b"{}"
145149
body = body.strip()
146150
try:
147151
decoded_body = self._json_load(body)
148152
except ValueError:
149-
# TODO: change
150-
raise Default4xxError(message="Invalid JSON in request body")
153+
# x-amzn-errortype: InternalFailureException
154+
raise Default5xxError(message="Internal server error")
151155

152156
if expr == "body":
153-
return body
157+
return to_str(body)
154158

155159
elif expr.startswith("body."):
156160
json_path = expr.removeprefix("body.")
@@ -165,15 +169,16 @@ def _retrieve_parameter_from_integration_response(
165169
param_type, param_name = expr.split(".")
166170

167171
if param_type == "header":
168-
# TODO: validate casing! for request it was strict?
169-
multi_headers = integration_response.headers.getlist(param_name)
170-
if multi_headers:
171-
return multi_headers[-1]
172+
# casing is strict: if you don't specify the exact casing the server is returning, it does not map it to
173+
# the response
174+
return case_sensitive_headers.get(param_name)
172175

173176
elif param_type == "multivalueheader":
174-
# TODO: validate casing! for request it was strict?
177+
# validate casing first
178+
if param_name not in case_sensitive_headers:
179+
return
180+
175181
multi_headers = integration_response.headers.getlist(param_name)
176-
# TODO: format?
177182
return ",".join(multi_headers)
178183

179184
else:
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import pytest
2+
3+
from localstack.aws.api.apigateway import IntegrationResponse
4+
from localstack.services.apigateway.next_gen.execute_api.gateway_response import (
5+
ApiConfigurationError,
6+
)
7+
from localstack.services.apigateway.next_gen.execute_api.handlers import IntegrationResponseHandler
8+
9+
10+
class TestSelectionPattern:
11+
def test_selection_pattern_status_code(self):
12+
integration_responses = {
13+
"2OO": IntegrationResponse(
14+
statusCode="200",
15+
),
16+
"400": IntegrationResponse(
17+
statusCode="400",
18+
selectionPattern="400",
19+
),
20+
"500": IntegrationResponse(
21+
statusCode="500",
22+
selectionPattern=r"5\d{2}",
23+
),
24+
}
25+
26+
def select_int_response(selection_value: str) -> IntegrationResponse:
27+
return IntegrationResponseHandler.select_integration_response(
28+
selection_value=selection_value,
29+
integration_responses=integration_responses,
30+
)
31+
32+
int_response = select_int_response("200")
33+
assert int_response["statusCode"] == "200"
34+
35+
int_response = select_int_response("400")
36+
assert int_response["statusCode"] == "400"
37+
38+
int_response = select_int_response("404")
39+
# fallback to default
40+
assert int_response["statusCode"] == "200"
41+
42+
int_response = select_int_response("500")
43+
assert int_response["statusCode"] == "500"
44+
45+
int_response = select_int_response("501")
46+
assert int_response["statusCode"] == "500"
47+
48+
def test_selection_pattern_no_default(self):
49+
integration_responses = {
50+
"2OO": IntegrationResponse(
51+
statusCode="200",
52+
selectionPattern="200",
53+
),
54+
}
55+
56+
with pytest.raises(ApiConfigurationError) as e:
57+
IntegrationResponseHandler.select_integration_response(
58+
selection_value="404",
59+
integration_responses=integration_responses,
60+
)
61+
assert e.value.message == "Internal server error"
62+
63+
def test_selection_pattern_string(self):
64+
integration_responses = {
65+
"2OO": IntegrationResponse(
66+
statusCode="200",
67+
),
68+
"400": IntegrationResponse(
69+
statusCode="400",
70+
selectionPattern="Malformed.*",
71+
),
72+
"500": IntegrationResponse(
73+
statusCode="500",
74+
selectionPattern="Internal.*",
75+
),
76+
}
77+
78+
def select_int_response(selection_value: str) -> IntegrationResponse:
79+
return IntegrationResponseHandler.select_integration_response(
80+
selection_value=selection_value,
81+
integration_responses=integration_responses,
82+
)
83+
84+
# this would basically no error message from AWS lambda
85+
int_response = select_int_response("")
86+
assert int_response["statusCode"] == "200"
87+
88+
int_response = select_int_response("Malformed request")
89+
assert int_response["statusCode"] == "400"
90+
91+
int_response = select_int_response("Internal server error")
92+
assert int_response["statusCode"] == "500"
93+
94+
int_response = select_int_response("Random error")
95+
assert int_response["statusCode"] == "200"

0 commit comments

Comments
 (0)