8000 improves response template handling (#7531) · localstack/localstack@8b9caab · GitHub
[go: up one dir, main page]

Skip to content

Commit 8b9caab

Browse files
authored
improves response template handling (#7531)
1 parent a247bae commit 8b9caab

File tree

6 files changed

+202
-10
lines changed

6 files changed

+202
-10
lines changed

localstack/services/apigateway/invocations.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ def invoke_rest_api_integration_backend(invocation_context: ApiInvocationContext
366366
headers["X-Amz-Target"] = target
367367

368368
result = common.make_http_request(
369-
url=config.service_url("kineses"), data=payload, headers=headers, method="POST"
369+
url=config.service_url("kinesis"), data=payload, headers=headers, method="POST"
370370
)
371371

372372
# apply response template

localstack/services/apigateway/templates.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,9 @@ def render(self, api_context: ApiInvocationContext) -> Union[bytes, str]:
216216

217217
class ResponseTemplates(Templates):
218218
"""
219-
Handles response template rendering
219+
Handles response template rendering. The integration response status code is used to select
220+
the correct template to render, if there is no template for the status code, the default
221+
template is used.
220222
"""
221223

222224
def render(self, api_context: ApiInvocationContext, **kwargs) -> Union[bytes, str]:
@@ -230,21 +232,42 @@ def render(self, api_context: ApiInvocationContext, **kwargs) -> Union[bytes, st
230232
# depending on the type of templates.
231233
api_context.data = response._content
232234

233-
integration_responses = integration.get("integrationResponses") or {}
235+
# status code returned by the integration
236+
status_code = str(response.status_code)
237+
238+
# get the integration responses configuration from the integration object
239+
integration_responses = integration.get("integrationResponses")
234240
if not integration_responses:
235241
return response._content
236-
entries = list(integration_responses.keys())
237-
return_code = str(response.status_code)
238-
D7AF if return_code not in entries and len(entries) > 1:
239-
LOG.info("Found multiple integration response status codes: %s", entries)
242+
243+
# get the configured integration response status codes,
244+
# e.g. ["200", "400", "500"]
245+
integration_status_codes = [str(code) for code in list(integration_responses.keys())]
246+
247+
# we return the response as is if the integration response status code is not on
248+
# the list of configured integration response status codes
249+
if status_code not in integration_status_codes or not integration_status_codes:
240250
return response._content
241-
return_code = entries[0]
242251

243-
response_templates = integration_responses[return_code].get("responseTemplates", {})
252+
# if there is integration response for the status code returned
253+
# by the integration we use the template configured for that status code
254+
if status_code in integration_responses:
255+
response_templates = integration_responses[status_code].get("responseTemplates", {})
256+
else:
257+
# if there is no integration response for the status code returned
258+
# by the integration we use the first integration response status code
259+
LOG.info(
260+
f"Found multiple integration response status codes: {integration_status_codes}"
261+
)
262+
response_templates = integration_responses[0].get("responseTemplates", {})
263+
264+
# we only support JSON templates for now - if there is no template we return
265+
# the response as is
244266
template = response_templates.get(APPLICATION_JSON, {})
245267
if not template:
246268
return response._content
247269

270+
# we render the template with the context data and the response content
248271
variables = self.build_variables_mapping(api_context)
249272
response._content = self.render_vtl(template, variables=variables)
250273
LOG.info("Endpoint response body after transformations:\n%s", response._content)

localstack/testing/snapshots/transformer_utility.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,8 +267,10 @@ def kinesis_api():
267267
replacement="sequence_number",
268268
replace_reference=True,
269269
),
270+
TransformerUtility.key_value("SequenceNumber", "sequence_number"),
270271
TransformerUtility.key_value("StartingSequenceNumber", "starting_sequence_number"),
271272
TransformerUtility.key_value("ShardId", "shard_id"),
273+
TransformerUtility.key_value("NextShardIterator", "next_shard_iterator"),
272274
TransformerUtility.key_value(
273275
"EndingHashKey", "ending_hash", reference_replacement=False
274276
),
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import json
2+
3+
import pytest
4+
5+
from localstack.utils.http import safe_requests as requests
6+
from localstack.utils.strings import short_uid
7+
from localstack.utils.sync import retry
8+
from tests.integration.apigateway_fixtures import (
9+
api_invoke_url,
10+
create_rest_api_deployment,
11+
create_rest_api_integration,
12+
create_rest_api_integration_response,
13+
create_rest_api_method_response,
14+
create_rest_api_stage,
15+
create_rest_resource,
16+
create_rest_resource_method,
17+
)
18+
from tests.integration.test_apigateway import (
19+
APIGATEWAY_ASSUME_ROLE_POLICY,
20+
APIGATEWAY_KINESIS_POLICY,
21+
)
22+
23+
24+
# PutRecord does not return EncryptionType, but it's documented as such.
25+
# xxx requires further investigation
26+
@pytest.mark.skip_snapshot_verify(paths=["$..EncryptionType", "$..ChildShards"])
27+
def test_apigateway_to_kinesis(
28+
create_rest_apigw,
29+
apigateway_client,
30+
sts_client,
31+
kinesis_create_stream,
32+
kinesis_client,
33+
create_lambda_function,
34+
lambda_client,
35+
lambda_su_role,
36+
cleanups,
37+
wait_for_stream_ready,
38+
create_iam_role_with_policy,
39+
snapshot,
40+
):
41+
snapshot.add_transformer(snapshot.transform.apigateway_api())
42+
snapshot.add_transformer(snapshot.transform.kinesis_api())
43+
44+
stream_name = f"kinesis-stream-{short_uid()}"
45+
region_name = apigateway_client.meta.region_name
46+
47+
api_id, name, root_id = create_rest_apigw(
48+
name="test-apigateway-to-kinesis",
49+
description="test apigateway to kinesis",
50+
endpointConfiguration={"types": ["REGIONAL"]},
51+
)
52+
53+
resource_id, _ = create_rest_resource(
54+
apigateway_client, restApiId=api_id, parentId=root_id, pathPart="test"
55+
)
56+
57+
method, _ = create_rest_resource_method(
58+
apigateway_client,
59+
restApiId=api_id,
60+
resourceId=resource_id,
61+
httpMethod="POST",
62+
authorizationType="NONE",
63+
)
64+
65+
assume_role_arn = create_iam_role_with_policy(
66+
RoleName=f"role-apigw-{short_uid()}",
67+
PolicyName=f"policy-apigw-{short_uid()}",
68+
RoleDefinition=APIGATEWAY_ASSUME_ROLE_POLICY,
69+
PolicyDefinition=APIGATEWAY_KINESIS_POLICY,
70+
)
71+
72+
create_rest_api_integration(
73+
apigateway_client,
74+
restApiId=api_id,
75+
resourceId=resource_id,
76+
httpMethod=method,
77+
integrationHttpMethod="POST",
78+
type="AWS",
79+
credentials=assume_role_arn,
80+
uri=f"arn:aws:apigateway:{region_name}:kinesis:action/PutRecord",
81+
requestTemplates={
82+
"application/json": json.dumps(
83+
{
84+
"StreamName": stream_name,
85+
"Data": "$util.base64Encode($input.body)",
86+
"PartitionKey": "test",
87+
}
88+
)
89+
},
90+
)
91+
92+
create_rest_api_method_response(
93+
apigateway_client,
94+
restApiId=api_id,
95+
resourceId=resource_id,
96+
httpMethod="POST",
97+
statusCode="200",
98+
)
99+
100+
create_rest_api_integration_response(
101+
apigateway_client,
102+
restApiId=api_id,
103+
resourceId=resource_id,
104+
httpMethod="POST",
105+
statusCode="200",
106+
)
107+
108+
deployment_id, _ = create_rest_api_deployment(apigateway_client, restApiId=api_id)
109+
stage = create_rest_api_stage(
110+
apigateway_client, restApiId=api_id, stageName="dev", deploymentId=deployment_id
111+
)
112+
113+
kinesis_create_stream(StreamName=stream_name, ShardCount=1)
114+
wait_for_stream_ready(stream_name=stream_name)
115+
stream_summary = kinesis_client.describe_stream_summary(StreamName=stream_name)
116+
assert stream_summary["StreamDescriptionSummary"]["OpenShardCount"] == 1
117+
first_stream_shard_data = kinesis_client.describe_stream(StreamName=stream_name)[
118+
"StreamDescription"
119+
]["Shards"][0]
120+
shard_id = first_stream_shard_data["ShardId"]
121+
122+
shard_iterator = kinesis_client.get_shard_iterator(
123+
StreamName=stream_name, ShardIteratorType="LATEST", ShardId=shard_id
124+
)["ShardIterator"]
125+
126+
# asserts
127+
def _invoke_apigw_to_kinesis():
128+
url = api_invoke_url(api_id, stage=stage, path="/test")
129+
response = requests.post(url, json={"kinesis": "snapshot"})
130+
assert response.status_code == 200
131+
snapshot.match("apigateway_response", response.json())
132+
133+
retry(_invoke_apigw_to_kinesis, retries=15, sleep=1)
134+
135+
get_records_response = kinesis_client.get_records(ShardIterator=shard_iterator)
136+
snapshot.match("kinesis_records", get_records_response)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"tests/integration/apigateway/test_apigateway_kinesis.py::test_apigateway_to_kinesis": {
3+
"recorded-date": "26-01-2023, 09:45:49",
4+
"recorded-content": {
5+
"apigateway_response": {
6+
"SequenceNumber": "<sequence_number:1>",
7+
"ShardId": "<shard_id:1>"
8+
},
9+
"kinesis_records": {
10+
"MillisBehindLatest": 0,
11+
"NextShardIterator": "<next_shard_iterator:1>",
12+
"Records": [
13+
{
14+
"ApproximateArrivalTimestamp": "timestamp",
15+
"Data": "b'{\"kinesis\": \"snapshot\"}'",
16+
"PartitionKey": "test",
17+
"SequenceNumber": "<sequence_number:1>"
18+
}
19+
],
20+
"ResponseMetadata": {
21+
"HTTPHeaders": {},
22+
"HTTPStatusCode": 200
23+
}
24+
}
25+
}
26+
}
27+
}

tests/integration/test_apigateway.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@
9494
"Statement": [{"Effect": "Allow", "Action": "states:*", "Resource": "*"}],
9595
}
9696

97+
APIGATEWAY_KINESIS_POLICY = {
98+
"Version": "2012-10-17",
99+
"Statement": [{"Effect": "Allow", "Action": "kinesis:*", "Resource": "*"}],
100+
}
101+
97102
APIGATEWAY_ASSUME_ROLE_POLICY = {
98103
"Statement": {
99104
"Sid": "",
@@ -237,7 +242,6 @@ def test_create_rest_api_with_custom_id(
237242
assert response.ok
238243
assert response._content == b'{"echo": "foobar", "response": "mocked"}'
239244

240-
@pytest.mark.skip
241245
def test_api_gateway_kinesis_integration(self):
242246
# create target Kinesis stream
243247
stream = resource_util.create_kinesis_stream(self.TEST_STREAM_KINESIS_API_GW)

0 commit comments

Comments
 (0)
0