8000 Improve support for test-invoke-method · codeperl/localstack@3480ef3 · GitHub
[go: up one dir, main page]

Skip to content

Commit 3480ef3

Browse files
authored
Improve support for test-invoke-method
1 parent f3a06d8 commit 3480ef3

File tree

5 files changed

+368
-43
lines changed

5 files changed

+368
-43
lines changed

localstack/services/apigateway/helpers.py

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import logging
55
import re
66
import time
7+
from collections import defaultdict
78
from datetime import datetime, timezone
89
from typing import Any, Callable, Dict, List, Optional, Tuple, TypedDict, Union
910
from urllib import parse as urlparse
@@ -43,7 +44,7 @@
4344
from localstack.utils.aws.arns import parse_arn
4445
from localstack.utils.aws.aws_responses import requests_error_response_json, requests_response
4546
from localstack.utils.aws.request_context import MARKER_APIGW_REQUEST_REGION, THREAD_LOCAL
46-
from localstack.utils.strings import long_uid, short_uid
47+
from localstack.utils.strings import long_uid, short_uid, to_str
4748
from localstack.utils.time import TIMESTAMP_FORMAT_TZ, timestamp
4849
from localstack.utils.urls import localstack_host
4950

@@ -71,7 +72,18 @@
7172
PATH_REGEX_CLIENT_CERTS = r"/clientcertificates/?([^/]+)?$"
7273
PATH_REGEX_VPC_LINKS = r"/vpclinks/([^/]+)?(.*)"
7374
PATH_REGEX_TEST_INVOKE_API = r"^\/restapis\/([A-Za-z0-9_\-]+)\/resources\/([A-Za-z0-9_\-]+)\/methods\/([A-Za-z0-9_\-]+)/?(\?.*)?"
74-
75+
INVOKE_TEST_LOG_TEMPLATE = """Execution log for request {request_id}
76+
{formatted_date} : Starting execution for request: {request_id}
77+
{formatted_date} : HTTP Method: {http_method}, Resource Path: {resource_path}
78+
{formatted_date} : Method request path: {request_path}
79+
{formatted_date} : Method request query string: {query_string}
80+
{formatted_date} : Method request headers: {request_headers}
81+
{formatted_date} : Method request body before transformations: {request_body}
82+
{formatted_date} : Method response body after transformations: {response_body}
83+
{formatted_date} : Method response headers: {response_headers}
84+
{formatted_date} : Successfully completed execution
85+
{formatted_date} : Method completed with status: {status_code}
86+
"""
7587
# template for SQS inbound data
7688
APIGATEWAY_SQS_DATA_INBOUND_TEMPLATE = (
7789
"Action=SendMessage&MessageBody=$util.base64Encode($input.json('$'))"
@@ -1332,18 +1344,9 @@ def set_api_id_stage_invocation_path(
13321344
stage = path.strip("/").split("/")[0]
13331345
relative_path_w_query_params = "/%s" % path.lstrip("/").partition("/")[2]
13341346
elif test_invoke_match:
1335-
# special case: fetch the resource details for TestInvokeApi invocations
1336-
stage = None
1337-
region_name = invocation_context.region_name
1338-
api_id = test_invoke_match.group(1)
1339-
resource_id = test_invoke_match.group(2)
1340-
query_string = test_invoke_match.group(4) or ""
1341-
apigateway = aws_stack.connect_to_service(
1342-
service_name="apigateway", region_name=region_name
1343-
)
1344-
resource = apigateway.get_resource(restApiId=api_id, resourceId=resource_id)
1345-
resource_path = resource.get("path")
1346-
relative_path_w_query_params = f"{resource_path}{query_string}"
1347+
stage = invocation_context.stage
1348+
api_id = invocation_context.api_id
1349+
relative_path_w_query_params = invocation_context.path_with_query_string
13471350
else:
13481351
raise Exception(
13491352
f"Unable to extract API Gateway details from request: {path} {dict(headers)}"
@@ -1479,3 +1482,45 @@ def is_greedy_path(path_part: str) -> bool:
14791482

14801483
def is_variable_path(path_part: str) -> bool:
14811484
return path_part.startswith("{") and path_part.endswith("}")
1485+
1486+
1487+
def multi_value_dict_for_list(elements: Union[List, Dict]) -> Dict:
1488+
temp_mv_dict = defaultdict(list)
1489+
for key in elements:
1490+
if isinstance(key, (list, tuple)):
1491+
key, value = key
1492+
else:
1493+
value = elements[key]
1494+
1495+
key = to_str(key)
1496+
temp_mv_dict[key].append(value)
1497+
return {k: tuple(v) for k, v in temp_mv_dict.items()}
1498+
1499+
1500+
def log_template(
1501+
request_id: str,
1502+
date: datetime,
1503+
http_method: str,
1504+
resource_path: str,
1505+
request_path: str,
1506+
query_string: str,
1507+
request_headers: str,
1508+
request_body: str,
1509+
response_body: str,
1510+
response_headers: str,
1511+
status_code: str,
1512+
):
1513+
formatted_date = date.strftime("%a %b %d %H:%M:%S %Z %Y")
1514+
return INVOKE_TEST_LOG_TEMPLATE.format(
1515+
request_id=request_id,
1516+
formatted_date=formatted_date,
1517+
http_method=http_method,
1518+
resource_path=resource_path,
1519+
request_path=request_path,
1520+
query_string=query_string,
1521+
request_headers=request_headers,
1522+
request_body=request_body,
1523+
response_body=response_body,
1524+
response_headers=response_headers,
1525+
status_code=status_code,
1526+
)

localstack/services/apigateway/integration.py

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@
33
import logging
44
import re
55
from abc import ABC, abstractmethod
6-
from collections import defaultdict
76
from functools import lru_cache
87
from http import HTTPStatus
9-
from typing import Any, Dict, List, Union
8+
from typing import Any, Dict
109
from urllib.parse import urljoin
1110

1211
import requests
@@ -33,6 +32,7 @@
3332
extract_query_string_params,
3433
get_event_request_context,
3534
make_error_response,
35+
multi_value_dict_for_list,
3636
)
3737
from localstack.services.apigateway.templates import (
3838
MappingTemplates,
@@ -223,19 +223,6 @@ def lambda_result_to_response(cls, result) -> LambdaResponse:
223223
response.multi_value_headers = parsed_result.get("multiValueHeaders") or {}
224224
return response
225225

226-
@staticmethod
227-
def multi_value_dict_for_list(elements: Union[List, Dict]) -> Dict:
228-
temp_mv_dict = defaultdict(list)
229-
for key in elements:
230-
if isinstance(key, (list, tuple)):
231-
key, value = key
232-
else:
233-
value = elements[key]
234-
key = to_str(key)
235-
temp_mv_dict[key].append(value)
236-
237-
return dict((k, tuple(v)) for k, v in temp_mv_dict.items())
238-
239226
@staticmethod
240227
def fix_proxy_path_params(path_params):
241228
proxy_path_param_value = path_params.get("proxy+")
@@ -258,7 +245,7 @@ def construct_invocation_event(
258245
return {
259246
"path": path,
260247
"headers": dict(headers),
261-
"multiValueHeaders": cls.multi_value_dict_for_list(headers),
248+
"multiValueHeaders": multi_value_dict_for_list(headers),
262249
"body": data,
263250
"isBase64Encoded": is_base64_encoded,
264251
"httpMethod": method,

localstack/services/apigateway/provider.py

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import json
44
import logging
55
from copy import deepcopy
6+
from datetime import datetime
67
from typing import IO, Any
78

89
from moto.apigateway import models as apigw_models
@@ -75,6 +76,8 @@
7576
import_api_from_openapi_spec,
7677
is_greedy_path,
7778
is_variable_path,
79+
log_template,
80+
multi_value_dict_for_list,
7881
)
7982
from localstack.services.apigateway.invocations import invoke_rest_api_from_request
8083
from localstack.services.apigateway.models import RestApiContainer
@@ -110,28 +113,49 @@ def on_after_init(self):
110113
def test_invoke_method(
111114
self, context: RequestContext, request: TestInvokeMethodRequest
112115
) -> TestInvokeMethodResponse:
113-
114116
invocation_context = to_invocation_context(context.request)
115-
invocation_context.method = request["httpMethod"]
117+
invocation_context.method = request.get("httpMethod")
118+
invocation_context.api_id = request.get("restApiId")
119+
invocation_context.path_with_query_string = request.get("pathWithQueryString")
120+
121+
moto_rest_api = get_moto_rest_api(context=context, rest_api_id=invocation_context.api_id)
122+
resource = moto_rest_api.resources.get(request["resourceId"])
123+
if not resource:
124+
raise NotFoundException("Invalid Resource identifier specified")
125+
126+
invocation_context.resource = {"id": resource.id}
127+
invocation_context.resource_path = resource.path_part
116128

117129
if data := parse_json_or_yaml(to_str(invocation_context.data or b"")):
118-
orig_data = data
119-
if path_with_query_string := orig_data.get("pathWithQueryString"):
120-
invocation_context.path_with_query_string = path_with_query_string
121130
invocation_context.data = data.get("body")
122-
invocation_context.headers = orig_data.get("headers", {})
131+
invocation_context.headers = data.get("headers", {})
123132

133+
req_start_time = datetime.now()
124134
result = invoke_rest_api_from_request(invocation_context)
125-
126-
# TODO: implement the other TestInvokeMethodResponse parameters
127-
# * multiValueHeaders: Optional[MapOfStringToList]
128-
# * log: Optional[String]
129-
# * latency: Optional[Long]
130-
135+
req_end_time = datetime.now()
136+
137+
# TODO: add the missing fields to the log. Next iteration will add helpers to extract the missing fields
138+
# from the apicontext
139+
log = log_template(
140+
request_id=invocation_context.context["requestId"],
141+
date=req_start_time,
142+
http_method=invocation_context.method,
143+
resource_path=invocation_context.invocation_path,
144+
request_path="",
145+
query_string="",
146+
request_headers="",
147+
request_body="",
148+
response_body="",
149+
response_headers=result.headers,
150+
status_code=result.status_code,
151+
)
131152
return TestInvokeMethodResponse(
132153
status=result.status_code,
133154
headers=dict(result.headers),
134155
body=to_str(result.content),
156+
log=log,
157+
latency=int((req_end_time - req_start_time).total_seconds()),
158+
multiValueHeaders=multi_value_dict_for_list(result.headers),
135159
)
136160

137161
@handler("CreateRestApi", expand=False)

0 commit comments

Comments
 (0)
0