diff --git a/README.md b/README.md index 18bb14f99..7300a6431 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,17 @@ The AWS Serverless Application Model (SAM) is an open-source framework for build It provides shorthand syntax to express functions, APIs, databases, and event source mappings. With just a few lines of configuration, you can define the application you want and model it. -[![Getting Started with AWS SAM](./docs/get-started-youtube.png)](https://www.youtube.com/watch?v=1dzihtC5LJ0) +[![Getting Started with AWS SAM](./docs/get-started-youtube.png)](https://www.youtube.com/watch?v=QBBewrKR1qg) + +## Recent blogposts and workshops + +* **Develop Node projects with SAM CLI using esbuild (Beta)** - and use SAM Accelerate on Typescript projects. [Read blogpost here](https://s12d.com/5Aa6u0o7) + +* **Speed up development with SAM Accelerate (Beta)** - quickly test your changes in the cloud. [Read blogpost here](https://s12d.com/WeMib4nf) + +* **Getting started with CI/CD? SAM pipelines can help you get started** - [This workshop](https://s12d.com/_JQ48d5T) walks you through the basics + +* **Get started with Serverless Application development using SAM CLI** - [This workshop](https://s12d.com/Tq9ZE-Br) walks you through the basics. ## Get Started diff --git a/integration/combination/test_api_settings.py b/integration/combination/test_api_settings.py index 5752ba9ae..5e1f11c0d 100644 --- a/integration/combination/test_api_settings.py +++ b/integration/combination/test_api_settings.py @@ -5,7 +5,6 @@ except ImportError: from pathlib2 import Path -import requests from parameterized import parameterized from integration.helpers.base_test import BaseTest @@ -158,7 +157,7 @@ def test_implicit_api_settings(self): def verify_binary_media_request(self, url, expected_status_code): headers = {"accept": "image/png"} - response = requests.get(url, headers=headers) + response = BaseTest.do_get_request_with_logging(url, headers) status = response.status_code expected_file_path = str(Path(self.code_dir, "AWS_logo_RGB.png")) diff --git a/integration/combination/test_api_with_authorizer_apikey.py b/integration/combination/test_api_with_authorizer_apikey.py index c0a11cbbc..436945f5f 100644 --- a/integration/combination/test_api_with_authorizer_apikey.py +++ b/integration/combination/test_api_with_authorizer_apikey.py @@ -1,12 +1,6 @@ -from unittest.case import skipIf - -import requests - from integration.helpers.base_test import BaseTest from integration.helpers.deployer.utils.retry import retry from integration.helpers.exception import StatusCodeError -from integration.helpers.resource import current_region_does_not_support -from integration.config.service_names import COGNITO class TestApiWithAuthorizerApiKey(BaseTest): @@ -82,10 +76,10 @@ def verify_authorized_request( header_value=None, ): if not header_key or not header_value: - response = requests.get(url) + response = BaseTest.do_get_request_with_logging(url) else: headers = {header_key: header_value} - response = requests.get(url, headers=headers) + response = BaseTest.do_get_request_with_logging(url, headers) status = response.status_code if status != expected_status_code: raise StatusCodeError( diff --git a/integration/combination/test_api_with_authorizers.py b/integration/combination/test_api_with_authorizers.py index 4d6975d64..3d0ce02dc 100644 --- a/integration/combination/test_api_with_authorizers.py +++ b/integration/combination/test_api_with_authorizers.py @@ -1,7 +1,5 @@ from unittest.case import skipIf -import requests - from integration.helpers.base_test import BaseTest from integration.helpers.deployer.utils.retry import retry from integration.helpers.exception import StatusCodeError @@ -436,11 +434,12 @@ def verify_authorized_request( header_value=None, ): if not header_key or not header_value: - response = requests.get(url) + response = BaseTest.do_get_request_with_logging(url) else: headers = {header_key: header_value} - response = requests.get(url, headers=headers) + response = BaseTest.do_get_request_with_logging(url, headers) status = response.status_code + if status != expected_status_code: raise StatusCodeError( "Request to {} failed with status: {}, expected status: {}".format(url, status, expected_status_code) diff --git a/integration/config/logger_configurations.py b/integration/config/logger_configurations.py new file mode 100644 index 000000000..d51f98885 --- /dev/null +++ b/integration/config/logger_configurations.py @@ -0,0 +1,14 @@ +# Logger configurations +import logging + + +class LoggingConfiguration: + @staticmethod + def configure_request_logger(logger): + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.INFO) + console_handler.setFormatter( + logging.Formatter("%(asctime)s %(message)s | Status: %(status)s | Headers: %(headers)s ") + ) + logger.addHandler(console_handler) + logger.propagate = False diff --git a/integration/helpers/base_test.py b/integration/helpers/base_test.py index 785b2af54..2dfd2a99d 100644 --- a/integration/helpers/base_test.py +++ b/integration/helpers/base_test.py @@ -1,14 +1,16 @@ import logging import os +import requests import shutil import botocore import pytest -import requests +from integration.config.logger_configurations import LoggingConfiguration from integration.helpers.client_provider import ClientProvider from integration.helpers.deployer.exceptions.exceptions import ThrottlingError from integration.helpers.deployer.utils.retry import retry_with_exponential_backoff_and_jitter +from integration.helpers.request_utils import RequestUtils from integration.helpers.resource import generate_suffix, create_bucket, verify_stack_resources from integration.helpers.s3_uploader import S3Uploader from integration.helpers.yaml_utils import dump_yaml, load_yaml @@ -28,6 +30,10 @@ from integration.helpers.file_resources import FILE_TO_S3_URI_MAP, CODE_KEY_TO_FILE_MAP LOG = logging.getLogger(__name__) + +REQUEST_LOGGER = logging.getLogger(f"{__name__}.requests") +LoggingConfiguration.configure_request_logger(REQUEST_LOGGER) + STACK_NAME_PREFIX = "sam-integ-stack-" S3_BUCKET_PREFIX = "sam-integ-bucket-" @@ -496,7 +502,7 @@ def verify_stack(self, end_state="CREATE_COMPLETE"): if error: self.fail(error) - def verify_get_request_response(self, url, expected_status_code): + def verify_get_request_response(self, url, expected_status_code, headers=None): """ Verify if the get request to a certain url return the expected status code @@ -506,9 +512,10 @@ def verify_get_request_response(self, url, expected_status_code): the url for the get request expected_status_code : string the expected status code + headers : dict + headers to use in request """ - print("Making request to " + url) - response = requests.get(url) + response = BaseTest.do_get_request_with_logging(url, headers) self.assertEqual(response.status_code, expected_status_code, " must return HTTP " + str(expected_status_code)) return response @@ -547,3 +554,19 @@ def generate_parameter(key, value, previous_value=False, resolved_value="string" "ResolvedValue": resolved_value, } return parameter + + @staticmethod + def do_get_request_with_logging(url, headers=None): + """ + Perform a get request to an APIGW endpoint and log relevant info + Parameters + ---------- + url : string + the url for the get request + headers : dict + headers to use in request + """ + response = requests.get(url, headers=headers) if headers else requests.get(url) + amazon_headers = RequestUtils(response).get_amazon_headers() + REQUEST_LOGGER.info("Request made to " + url, extra={"status": response.status_code, "headers": amazon_headers}) + return response diff --git a/integration/helpers/request_utils.py b/integration/helpers/request_utils.py new file mode 100644 index 000000000..655d7ae8a --- /dev/null +++ b/integration/helpers/request_utils.py @@ -0,0 +1,37 @@ +# Utils for requests + +# Relevant headers that should be captured for debugging +AMAZON_HEADERS = [ + "x-amzn-requestid", + "x-amz-apigw-id", + "x-amz-cf-id", + "x-amzn-errortype", + "apigw-requestid", +] + + +class RequestUtils: + def __init__(self, response): + self.response = response + self.headers = self._normalize_response_headers() + + def get_amazon_headers(self): + """ + Get a list of relevant amazon headers that could be useful for debugging + """ + amazon_headers = {} + for header, header_val in self.headers.items(): + if header in AMAZON_HEADERS: + amazon_headers[header] = header_val + return amazon_headers + + def _normalize_response_headers(self): + """ + API gateway can return headers with letters in different cases i.e. x-amzn-requestid or x-amzn-requestId + We make them all lowercase here to more easily match them up + """ + if self.response is None or not self.response.headers: + # Need to check for response is None here since the __bool__ method checks 200 <= status < 400 + return {} + + return dict((k.lower(), v) for k, v in self.response.headers.items()) diff --git a/integration/resources/templates/combination/api_with_authorizers_invokefunction_set_none.yaml b/integration/resources/templates/combination/api_with_authorizers_invokefunction_set_none.yaml index b7e9e55fc..591724471 100644 --- a/integration/resources/templates/combination/api_with_authorizers_invokefunction_set_none.yaml +++ b/integration/resources/templates/combination/api_with_authorizers_invokefunction_set_none.yaml @@ -12,7 +12,7 @@ Resources: InlineCode: | print("hello") Handler: index.handler - Runtime: python3.6 + Runtime: python3.8 Events: API3: Type: Api diff --git a/integration/resources/templates/single/basic_api_with_mode.yaml b/integration/resources/templates/single/basic_api_with_mode.yaml index 0aa4556c0..ffad6b15d 100644 --- a/integration/resources/templates/single/basic_api_with_mode.yaml +++ b/integration/resources/templates/single/basic_api_with_mode.yaml @@ -9,7 +9,7 @@ Resources: Type: 'AWS::Serverless::Function' Properties: Handler: index.handler - Runtime: python3.6 + Runtime: python3.8 AutoPublishAlias: live InlineCode: | import json diff --git a/integration/resources/templates/single/basic_api_with_mode_update.yaml b/integration/resources/templates/single/basic_api_with_mode_update.yaml index 33991486a..3f34693b9 100644 --- a/integration/resources/templates/single/basic_api_with_mode_update.yaml +++ b/integration/resources/templates/single/basic_api_with_mode_update.yaml @@ -9,7 +9,7 @@ Resources: Type: 'AWS::Serverless::Function' Properties: Handler: index.handler - Runtime: python3.6 + Runtime: python3.8 AutoPublishAlias: live InlineCode: | def handler(event, context): diff --git a/integration/single/test_basic_api.py b/integration/single/test_basic_api.py index 9eb00ecb8..402fd054f 100644 --- a/integration/single/test_basic_api.py +++ b/integration/single/test_basic_api.py @@ -2,7 +2,6 @@ from unittest.case import skipIf from integration.helpers.base_test import BaseTest -import requests from integration.helpers.resource import current_region_does_not_support from integration.config.service_names import MODE @@ -40,17 +39,17 @@ def test_basic_api_with_mode(self): stack_output = self.get_stack_outputs() api_endpoint = stack_output.get("ApiEndpoint") - response = requests.get(f"{api_endpoint}/get") + response = BaseTest.do_get_request_with_logging(f"{api_endpoint}/get") self.assertEqual(response.status_code, 200) # Removes get from the API self.update_and_verify_stack(file_path="single/basic_api_with_mode_update") - response = requests.get(f"{api_endpoint}/get") + response = BaseTest.do_get_request_with_logging(f"{api_endpoint}/get") # API Gateway by default returns 403 if a path do not exist retries = 20 while retries > 0: retries -= 1 - response = requests.get(f"{api_endpoint}/get") + response = BaseTest.do_get_request_with_logging(f"{api_endpoint}/get") if response.status_code != 500: break time.sleep(5) diff --git a/integration/single/test_basic_function.py b/integration/single/test_basic_function.py index 951d47826..0c4ea07d2 100644 --- a/integration/single/test_basic_function.py +++ b/integration/single/test_basic_function.py @@ -1,7 +1,5 @@ from unittest.case import skipIf -import requests - from integration.config.service_names import KMS, XRAY, ARM from integration.helpers.resource import current_region_does_not_support from parameterized import parameterized @@ -42,7 +40,7 @@ def test_function_with_http_api_events(self, file_name): endpoint = self.get_api_v2_endpoint("MyHttpApi") - self.assertEqual(requests.get(endpoint).text, self.FUNCTION_OUTPUT) + self.assertEqual(BaseTest.do_get_request_with_logging(endpoint).text, self.FUNCTION_OUTPUT) @parameterized.expand( [ diff --git a/integration/single/test_function_with_http_api_and_auth.py b/integration/single/test_function_with_http_api_and_auth.py index c1e5546d2..02cd368bb 100644 --- a/integration/single/test_function_with_http_api_and_auth.py +++ b/integration/single/test_function_with_http_api_and_auth.py @@ -1,5 +1,3 @@ -import requests -from parameterized import parameterized from integration.helpers.base_test import BaseTest @@ -16,14 +14,22 @@ def test_function_with_http_api_and_auth(self): self.create_and_verify_stack("function_with_http_api_events_and_auth") implicitEndpoint = self.get_api_v2_endpoint("ServerlessHttpApi") - self.assertEqual(requests.get(implicitEndpoint + "/default-auth").text, self.FUNCTION_OUTPUT) - self.assertEqual(requests.get(implicitEndpoint + "/iam-auth").text, IAM_AUTH_OUTPUT) + self.assertEqual( + BaseTest.do_get_request_with_logging(implicitEndpoint + "/default-auth").text, self.FUNCTION_OUTPUT + ) + self.assertEqual(BaseTest.do_get_request_with_logging(implicitEndpoint + "/iam-auth").text, IAM_AUTH_OUTPUT) defaultIamEndpoint = self.get_api_v2_endpoint("MyDefaultIamAuthHttpApi") - self.assertEqual(requests.get(defaultIamEndpoint + "/no-auth").text, self.FUNCTION_OUTPUT) - self.assertEqual(requests.get(defaultIamEndpoint + "/default-auth").text, IAM_AUTH_OUTPUT) - self.assertEqual(requests.get(defaultIamEndpoint + "/iam-auth").text, IAM_AUTH_OUTPUT) + self.assertEqual( + BaseTest.do_get_request_with_logging(defaultIamEndpoint + "/no-auth").text, self.FUNCTION_OUTPUT + ) + self.assertEqual( + BaseTest.do_get_request_with_logging(defaultIamEndpoint + "/default-auth").text, IAM_AUTH_OUTPUT + ) + self.assertEqual(BaseTest.do_get_request_with_logging(defaultIamEndpoint + "/iam-auth").text, IAM_AUTH_OUTPUT) iamEnabledEndpoint = self.get_api_v2_endpoint("MyIamAuthEnabledHttpApi") - self.assertEqual(requests.get(iamEnabledEndpoint + "/default-auth").text, self.FUNCTION_OUTPUT) - self.assertEqual(requests.get(iamEnabledEndpoint + "/iam-auth").text, IAM_AUTH_OUTPUT) + self.assertEqual( + BaseTest.do_get_request_with_logging(iamEnabledEndpoint + "/default-auth").text, self.FUNCTION_OUTPUT + ) + self.assertEqual(BaseTest.do_get_request_with_logging(iamEnabledEndpoint + "/iam-auth").text, IAM_AUTH_OUTPUT) diff --git a/pytest.ini b/pytest.ini index 257206172..d567f28ac 100644 --- a/pytest.ini +++ b/pytest.ini @@ -7,4 +7,5 @@ env = AWS_DEFAULT_REGION = ap-southeast-1 markers = slow: marks tests as slow (deselect with '-m "not slow"') - +log_cli = 1 +log_cli_level = INFO diff --git a/samtranslator/__init__.py b/samtranslator/__init__.py index a29c76c70..9a9a7e961 100644 --- a/samtranslator/__init__.py +++ b/samtranslator/__init__.py @@ -1 +1 @@ -__version__ = "1.46.0" +__version__ = "1.47.0" diff --git a/samtranslator/model/apigatewayv2.py b/samtranslator/model/apigatewayv2.py index 14f00b63d..ece8166d3 100644 --- a/samtranslator/model/apigatewayv2.py +++ b/samtranslator/model/apigatewayv2.py @@ -172,10 +172,18 @@ def _validate_lambda_authorizer(self): self.api_logical_id, f"{self.name} Lambda Authorizer must define 'AuthorizerPayloadFormatVersion'." ) - if self.identity and not isinstance(self.identity, dict): - raise InvalidResourceException( - self.api_logical_id, f"{self.name} Lambda Authorizer property 'identity' is of invalid type." - ) + if self.identity: + if not isinstance(self.identity, dict): + raise InvalidResourceException( + self.api_logical_id, self.name + " Lambda Authorizer property 'identity' is of invalid type." + ) + headers = self.identity.get("Headers") + if headers: + if not isinstance(headers, list) or any([not isinstance(header, str) for header in headers]): + raise InvalidResourceException( + self.api_logical_id, + self.name + " Lambda Authorizer property identity's 'Headers' is of invalid type.", + ) def generate_openapi(self): """ diff --git a/samtranslator/model/eventsources/push.py b/samtranslator/model/eventsources/push.py index 751e0e40b..93ed5592f 100644 --- a/samtranslator/model/eventsources/push.py +++ b/samtranslator/model/eventsources/push.py @@ -1121,7 +1121,7 @@ def _get_permission(self, resources_to_link, stage): editor = OpenApiEditor(resources_to_link["explicit_api"].get("DefinitionBody")) except InvalidDocumentException as e: api_logical_id = self.ApiId.get("Ref") if isinstance(self.ApiId, dict) else self.ApiId - raise InvalidResourceException(api_logical_id, e) + raise InvalidResourceException(api_logical_id, " ".join(ex.message for ex in e.causes)) # If this is using the new $default path, keep path blank and add a * permission if path == OpenApiEditor._DEFAULT_PATH: diff --git a/samtranslator/model/sam_resources.py b/samtranslator/model/sam_resources.py index d098c23d3..ce4ffa3e9 100644 --- a/samtranslator/model/sam_resources.py +++ b/samtranslator/model/sam_resources.py @@ -878,7 +878,7 @@ def _validate_deployment_preference_and_add_update_policy( ) if deployment_preference_collection.get(self.logical_id).enabled: - if self.AutoPublishAlias is None: + if not self.AutoPublishAlias: raise InvalidResourceException( self.logical_id, "'DeploymentPreference' requires AutoPublishAlias property to be specified." ) diff --git a/samtranslator/open_api/open_api.py b/samtranslator/open_api/open_api.py index 2226e27ea..d19eecc5d 100644 --- a/samtranslator/open_api/open_api.py +++ b/samtranslator/open_api/open_api.py @@ -41,8 +41,11 @@ def __init__(self, doc): """ if not OpenApiEditor.is_valid(doc): raise InvalidDocumentException( - "Invalid OpenApi document. " - "Invalid values or missing keys for 'openapi' or 'paths' in 'DefinitionBody'." + [ + InvalidTemplateException( + "Invalid OpenApi document. Invalid values or missing keys for 'openapi' or 'paths' in 'DefinitionBody'." + ) + ] ) self._doc = copy.deepcopy(doc) @@ -219,6 +222,8 @@ def add_lambda_integration( for path_item in self.get_conditional_contents(self.paths.get(path)): # create as Py27Dict and insert key one by one to preserve input order + if path_item[method] is None: + path_item[method] = Py27Dict() path_item[method][self._X_APIGW_INTEGRATION] = Py27Dict() path_item[method][self._X_APIGW_INTEGRATION]["type"] = "aws_proxy" path_item[method][self._X_APIGW_INTEGRATION]["httpMethod"] = "POST" @@ -397,6 +402,15 @@ def set_path_default_authorizer(self, path, default_authorizer, authorizers, api ] ) for method_definition in self.get_conditional_contents(method): + # check if there is any method_definition given by customer + if not method_definition: + raise InvalidDocumentException( + [ + InvalidTemplateException( + f"Invalid method definition ({normalized_method_name}) for path: {path}" + ) + ] + ) # If no integration given, then we don't need to process this definition (could be AWS::NoValue) if not self.method_definition_has_integration(method_definition): continue diff --git a/samtranslator/swagger/swagger.py b/samtranslator/swagger/swagger.py index 4c0ec3e34..28cf84a4f 100644 --- a/samtranslator/swagger/swagger.py +++ b/samtranslator/swagger/swagger.py @@ -48,7 +48,7 @@ def __init__(self, doc): """ if not SwaggerEditor.is_valid(doc): - raise InvalidDocumentException("Invalid Swagger document") + raise InvalidDocumentException([InvalidTemplateException("Invalid Swagger document")]) self._doc = copy.deepcopy(doc) self.paths = self._doc["paths"] @@ -187,7 +187,11 @@ def add_lambda_integration( method = self._normalize_method_name(method) if self.has_integration(path, method): raise InvalidDocumentException( - "Lambda integration already exists on Path={}, Method={}".format(path, method) + [ + InvalidTemplateException( + "Lambda integration already exists on Path={}, Method={}".format(path, method) + ) + ] ) self.add_path(path, method) @@ -252,7 +256,9 @@ def add_state_machine_integration( method = self._normalize_method_name(method) if self.has_integration(path, method): - raise InvalidDocumentException("Integration already exists on Path={}, Method={}".format(path, method)) + raise InvalidDocumentException( + [InvalidTemplateException("Integration already exists on Path={}, Method={}".format(path, method))] + ) self.add_path(path, method) @@ -1029,7 +1035,9 @@ def _add_iam_resource_policy_for_method(self, policy_list, effect, resource_list return if effect not in ["Allow", "Deny"]: - raise InvalidDocumentException("Effect must be one of {}".format(["Allow", "Deny"])) + raise InvalidDocumentException( + [InvalidTemplateException("Effect must be one of {}".format(["Allow", "Deny"]))] + ) if not isinstance(policy_list, (dict, list)): raise InvalidDocumentException( @@ -1091,7 +1099,9 @@ def _add_ip_resource_policy_for_method(self, ip_list, conditional, resource_list ip_list = [ip_list] if conditional not in ["IpAddress", "NotIpAddress"]: - raise InvalidDocumentException("Conditional must be one of {}".format(["IpAddress", "NotIpAddress"])) + raise InvalidDocumentException( + [InvalidTemplateException("Conditional must be one of {}".format(["IpAddress", "NotIpAddress"]))] + ) self.resource_policy["Version"] = "2012-10-17" allow_statement = Py27Dict() @@ -1127,7 +1137,9 @@ def _add_vpc_resource_policy_for_method(self, endpoint_dict, conditional, resour """ if conditional not in ["StringNotEquals", "StringEquals"]: - raise InvalidDocumentException("Conditional must be one of {}".format(["StringNotEquals", "StringEquals"])) + raise InvalidDocumentException( + [InvalidTemplateException("Conditional must be one of {}".format(["StringNotEquals", "StringEquals"]))] + ) condition = Py27Dict() string_endpoint_list = endpoint_dict.get("StringEndpointList") diff --git a/tests/openapi/test_openapi.py b/tests/openapi/test_openapi.py index 6e12e8031..23675e031 100644 --- a/tests/openapi/test_openapi.py +++ b/tests/openapi/test_openapi.py @@ -203,11 +203,32 @@ def setUp(self): "paths": { "/foo": {"post": {"a": [1, 2, "b"], "responses": {"something": "is already here"}}}, "/bar": {"get": {_X_INTEGRATION: {"a": "b"}}}, + "/nullmethod": {"get": None}, }, } self.editor = OpenApiEditor(self.original_openapi) + def test_must_override_null_path(self): + path = "/nullmethod" + method = "get" + integration_uri = "something" + expected = { + "responses": {}, + _X_INTEGRATION: { + "type": "aws_proxy", + "httpMethod": "POST", + "payloadFormatVersion": "2.0", + "uri": integration_uri, + }, + } + + self.editor.add_lambda_integration(path, method, integration_uri) + + self.assertTrue(self.editor.has_path(path, method)) + actual = self.editor.openapi["paths"][path][method] + self.assertEqual(expected, actual) + def test_must_add_new_integration_to_new_path(self): path = "/newpath" method = "get" diff --git a/tests/translator/input/definition_body_intrinsics_support.yaml b/tests/translator/input/definition_body_intrinsics_support.yaml new file mode 100644 index 000000000..d082c4f65 --- /dev/null +++ b/tests/translator/input/definition_body_intrinsics_support.yaml @@ -0,0 +1,81 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + DefinitionBody currently only supports intrinsics when SwaggerEditor/OpenApiEditor is not used (as of 2022-05-30). + Let's add tests to make sure we keep this support, because we've had a regression where we accidentally + removed this intrinsics support by using the SwaggerEditor. + Note that the only supported intrinsic function for DefinitionBody is Fn::If. Other intrinsics are not supported + because they don't resolve to dictionary. + +Conditions: + FalseCondition: + Fn::Equals: + - true + - false + +Resources: + # Rest Api with DefinitionBody under If intrinsic, SwaggerEditor not used + RestApiIfIntrinsicAndNoSwaggerEditor: + Type: AWS::Serverless::Api + Properties: + StageName: prod + DefinitionBody: + Fn::If: + - FalseCondition + - + swagger: '2.0' + info: + title: !Sub ${AWS::StackName}-Api + paths: + /post: + post: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: https://www.alphavantage.co/ + payloadFormatVersion: '1.0' + - + swagger: '2.0' + info: + title: !Sub ${AWS::StackName}-Api + paths: + /post: + post: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: https://www.otheruri.co/ + payloadFormatVersion: '1.0' + + # HttpApi with DefinitionBody under If intrinsic, OpenApiEditor not used + HttpApiIfIntrinsicAndNoOpenApiEditor: + Type: AWS::Serverless::HttpApi + Properties: + StageName: prod + DefinitionBody: + Fn::If: + - FalseCondition + - + openapi: '3.0' + info: + title: !Sub ${AWS::StackName}-Api + paths: + /post: + post: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: https://www.alphavantage.co/ + payloadFormatVersion: '1.0' + - + openapi: '3.0' + info: + title: !Sub ${AWS::StackName}-Api + paths: + /post: + post: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: https://www.otheruri.co/ + payloadFormatVersion: '1.0' diff --git a/tests/translator/input/error_api_authorizer_property_indentity_header_with_invalid_type.yaml b/tests/translator/input/error_api_authorizer_property_indentity_header_with_invalid_type.yaml new file mode 100644 index 000000000..06b3238ca --- /dev/null +++ b/tests/translator/input/error_api_authorizer_property_indentity_header_with_invalid_type.yaml @@ -0,0 +1,58 @@ +Parameters: + AuthKeyName: + Type: String + Default: Auth_name + +Resources: + MyLambdaFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: python3.7 + InlineCode: | + def handler(event, context): + return {'body': 'Hello World!', 'statusCode': 200} + MemorySize: 128 + Events: + PostApi: + Type: HttpApi + Properties: + Auth: + Authorizer: MyLambdaAuthUpdated + ApiId: + Ref: MyApi + Method: POST + Path: /post + + MyAuthFn: + Type: AWS::Serverless::Function + Properties: + InlineCode: | + print("hello") + Handler: index.handler + Runtime: nodejs12.x + + MyApi: + Type: AWS::Serverless::HttpApi + Properties: + Tags: + Tag1: value1 + Tag2: value2 + Auth: + Authorizers: + MyLambdaAuthUpdated: + FunctionArn: + Fn::GetAtt: + - MyAuthFn + - Arn + FunctionInvokeRole: + Fn::GetAtt: + - MyAuthFnRole + - Arn + Identity: + Headers: + - Ref: AuthKeyName + AuthorizerPayloadFormatVersion: 1.0 + DefaultAuthorizer: MyLambdaAuthUpdated + + \ No newline at end of file diff --git a/tests/translator/input/error_auto_publish_alias_empty_string.yaml b/tests/translator/input/error_auto_publish_alias_empty_string.yaml new file mode 100644 index 000000000..a67ea4084 --- /dev/null +++ b/tests/translator/input/error_auto_publish_alias_empty_string.yaml @@ -0,0 +1,10 @@ +Resources: + MinimalFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + AutoPublishAlias: '' + DeploymentPreference: + Type: AllAtOnce \ No newline at end of file diff --git a/tests/translator/input/error_http_api_null_method.yaml b/tests/translator/input/error_http_api_null_method.yaml new file mode 100644 index 000000000..874f77dca --- /dev/null +++ b/tests/translator/input/error_http_api_null_method.yaml @@ -0,0 +1,18 @@ +AWSTemplateFormatVersion: '2010-09-09' +Resources: + MyHttpApiGateway: + Type: AWS::Serverless::HttpApi + Properties: + Auth: + Authorizers: + MyAuthorizer: + IdentitySource: $request.header.Authorization + JwtConfiguration: + issuer: + Ref: MyOAuth2Issuer + DefaultAuthorizer: MyAuthorizer + DefinitionBody: + openapi: 3.0.1 + paths: + /test: + post: null diff --git a/tests/translator/input/http_api_with_null_path.yaml b/tests/translator/input/http_api_with_null_path.yaml new file mode 100644 index 000000000..ce2018446 --- /dev/null +++ b/tests/translator/input/http_api_with_null_path.yaml @@ -0,0 +1,27 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: 'AWS::Serverless-2016-10-31' +Description: A template to test for API condition handling with a mix of explicit and implicit api events. + +Resources: + EXAMPLEPARTIALGateway: + Type: AWS::Serverless::HttpApi + Properties: + DefinitionBody: + openapi: 3.0.1 + paths: + /my/route: + post: null + EXAMPLEPARTIALLambda: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Runtime: python3.9 + Handler: hello.handler + Events: + PostRequest: + Type: HttpApi + Properties: + Path: /my/route + ApiId: + Ref: EXAMPLEPARTIALGateway + Method: POST \ No newline at end of file diff --git a/tests/translator/output/aws-cn/definition_body_intrinsics_support.json b/tests/translator/output/aws-cn/definition_body_intrinsics_support.json new file mode 100644 index 000000000..e8693dc49 --- /dev/null +++ b/tests/translator/output/aws-cn/definition_body_intrinsics_support.json @@ -0,0 +1,154 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "DefinitionBody currently only supports intrinsics when SwaggerEditor/OpenApiEditor is not used (as of 2022-05-30). Let's add tests to make sure we keep this support, because we've had a regression where we accidentally removed this intrinsics support by using the SwaggerEditor. Note that the only supported intrinsic function for DefinitionBody is Fn::If. Other intrinsics are not supported because they don't resolve to dictionary. \n", + "Conditions": { + "FalseCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Resources": { + "RestApiIfIntrinsicAndNoSwaggerEditorDeployment2f0411dccd": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "Description": "RestApi deployment id: 2f0411dccde517dcb5c82d3848dbc431e7479ebc", + "RestApiId": { + "Ref": "RestApiIfIntrinsicAndNoSwaggerEditor" + }, + "StageName": "Stage" + } + }, + "RestApiIfIntrinsicAndNoSwaggerEditorprodStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "RestApiIfIntrinsicAndNoSwaggerEditorDeployment2f0411dccd" + }, + "RestApiId": { + "Ref": "RestApiIfIntrinsicAndNoSwaggerEditor" + }, + "StageName": "prod" + } + }, + "HttpApiIfIntrinsicAndNoOpenApiEditorprodStage": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "HttpApiIfIntrinsicAndNoOpenApiEditor" + }, + "StageName": "prod", + "AutoDeploy": true + } + }, + "RestApiIfIntrinsicAndNoSwaggerEditor": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "Fn::If": [ + "FalseCondition", + { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.alphavantage.co/", + "payloadFormatVersion": "1.0" + } + } + } + }, + "swagger": "2.0" + }, + { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.otheruri.co/", + "payloadFormatVersion": "1.0" + } + } + } + }, + "swagger": "2.0" + } + ] + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + } + } + }, + "HttpApiIfIntrinsicAndNoOpenApiEditor": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Body": { + "Fn::If": [ + "FalseCondition", + { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.alphavantage.co/", + "payloadFormatVersion": "1.0" + } + } + } + }, + "openapi": "3.0" + }, + { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.otheruri.co/", + "payloadFormatVersion": "1.0" + } + } + } + }, + "openapi": "3.0" + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-cn/function_with_alias_and_code_sha256.json b/tests/translator/output/aws-cn/function_with_alias_and_code_sha256.json new file mode 100644 index 000000000..b92a1b908 --- /dev/null +++ b/tests/translator/output/aws-cn/function_with_alias_and_code_sha256.json @@ -0,0 +1,89 @@ +{ + "Parameters": { + "AutoPublishCodeSha256": { + "Type": "String", + "Description": "Sha256 to uniquely identify creation of the lambda", + "Default": "6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b" + } + }, + "Resources": { + "MinimalFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "MinimalFunctionRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MinimalFunctionVersion6b86b273ff": { + "Type": "AWS::Lambda::Version", + "DeletionPolicy": "Retain", + "Properties": { + "Description": "sam-testing", + "FunctionName": { + "Ref": "MinimalFunction" + } + } + }, + "MinimalFunctionAliaslive": { + "Type": "AWS::Lambda::Alias", + "Properties": { + "Name": "live", + "FunctionName": { + "Ref": "MinimalFunction" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "MinimalFunctionVersion6b86b273ff", + "Version" + ] + } + } + }, + "MinimalFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + } + } + } \ No newline at end of file diff --git a/tests/translator/output/aws-cn/http_api_with_null_path.json b/tests/translator/output/aws-cn/http_api_with_null_path.json new file mode 100644 index 000000000..dc55dbed7 --- /dev/null +++ b/tests/translator/output/aws-cn/http_api_with_null_path.json @@ -0,0 +1,122 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "A template to test for API condition handling with a mix of explicit and implicit api events.", + "Resources": { + "EXAMPLEPARTIALLambda": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "EXAMPLEPARTIALLambdaRole", + "Arn" + ] + }, + "Runtime": "python3.9", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "EXAMPLEPARTIALLambdaRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "EXAMPLEPARTIALLambdaPostRequestPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "EXAMPLEPARTIALLambda" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/POST/my/route", + { + "__ApiId__": { + "Ref": "EXAMPLEPARTIALGateway" + }, + "__Stage__": "*" + } + ] + } + } + }, + "EXAMPLEPARTIALGateway": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Body": { + "openapi": "3.0.1", + "paths": { + "/my/route": { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${EXAMPLEPARTIALLambda.Arn}/invocations" + }, + "payloadFormatVersion": "2.0" + }, + "responses": {} + } + } + }, + "tags": [ + { + "name": "httpapi:createdBy", + "x-amazon-apigateway-tag-value": "SAM" + } + ] + } + } + }, + "EXAMPLEPARTIALGatewayApiGatewayDefaultStage": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "EXAMPLEPARTIALGateway" + }, + "StageName": "$default", + "Tags": { + "httpapi:createdBy": "SAM" + }, + "AutoDeploy": true + } + } + } + } \ No newline at end of file diff --git a/tests/translator/output/aws-us-gov/definition_body_intrinsics_support.json b/tests/translator/output/aws-us-gov/definition_body_intrinsics_support.json new file mode 100644 index 000000000..e8693dc49 --- /dev/null +++ b/tests/translator/output/aws-us-gov/definition_body_intrinsics_support.json @@ -0,0 +1,154 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "DefinitionBody currently only supports intrinsics when SwaggerEditor/OpenApiEditor is not used (as of 2022-05-30). Let's add tests to make sure we keep this support, because we've had a regression where we accidentally removed this intrinsics support by using the SwaggerEditor. Note that the only supported intrinsic function for DefinitionBody is Fn::If. Other intrinsics are not supported because they don't resolve to dictionary. \n", + "Conditions": { + "FalseCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Resources": { + "RestApiIfIntrinsicAndNoSwaggerEditorDeployment2f0411dccd": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "Description": "RestApi deployment id: 2f0411dccde517dcb5c82d3848dbc431e7479ebc", + "RestApiId": { + "Ref": "RestApiIfIntrinsicAndNoSwaggerEditor" + }, + "StageName": "Stage" + } + }, + "RestApiIfIntrinsicAndNoSwaggerEditorprodStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "RestApiIfIntrinsicAndNoSwaggerEditorDeployment2f0411dccd" + }, + "RestApiId": { + "Ref": "RestApiIfIntrinsicAndNoSwaggerEditor" + }, + "StageName": "prod" + } + }, + "HttpApiIfIntrinsicAndNoOpenApiEditorprodStage": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "HttpApiIfIntrinsicAndNoOpenApiEditor" + }, + "StageName": "prod", + "AutoDeploy": true + } + }, + "RestApiIfIntrinsicAndNoSwaggerEditor": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "Fn::If": [ + "FalseCondition", + { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.alphavantage.co/", + "payloadFormatVersion": "1.0" + } + } + } + }, + "swagger": "2.0" + }, + { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.otheruri.co/", + "payloadFormatVersion": "1.0" + } + } + } + }, + "swagger": "2.0" + } + ] + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + } + } + }, + "HttpApiIfIntrinsicAndNoOpenApiEditor": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Body": { + "Fn::If": [ + "FalseCondition", + { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.alphavantage.co/", + "payloadFormatVersion": "1.0" + } + } + } + }, + "openapi": "3.0" + }, + { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.otheruri.co/", + "payloadFormatVersion": "1.0" + } + } + } + }, + "openapi": "3.0" + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-us-gov/function_with_alias_and_code_sha256.json b/tests/translator/output/aws-us-gov/function_with_alias_and_code_sha256.json new file mode 100644 index 000000000..94c25d971 --- /dev/null +++ b/tests/translator/output/aws-us-gov/function_with_alias_and_code_sha256.json @@ -0,0 +1,89 @@ +{ + "Parameters": { + "AutoPublishCodeSha256": { + "Type": "String", + "Description": "Sha256 to uniquely identify creation of the lambda", + "Default": "6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b" + } + }, + "Resources": { + "MinimalFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "MinimalFunctionRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MinimalFunctionVersion6b86b273ff": { + "Type": "AWS::Lambda::Version", + "DeletionPolicy": "Retain", + "Properties": { + "Description": "sam-testing", + "FunctionName": { + "Ref": "MinimalFunction" + } + } + }, + "MinimalFunctionAliaslive": { + "Type": "AWS::Lambda::Alias", + "Properties": { + "Name": "live", + "FunctionName": { + "Ref": "MinimalFunction" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "MinimalFunctionVersion6b86b273ff", + "Version" + ] + } + } + }, + "MinimalFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + } + } + } \ No newline at end of file diff --git a/tests/translator/output/aws-us-gov/http_api_with_null_path.json b/tests/translator/output/aws-us-gov/http_api_with_null_path.json new file mode 100644 index 000000000..db435477a --- /dev/null +++ b/tests/translator/output/aws-us-gov/http_api_with_null_path.json @@ -0,0 +1,122 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "A template to test for API condition handling with a mix of explicit and implicit api events.", + "Resources": { + "EXAMPLEPARTIALLambda": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "EXAMPLEPARTIALLambdaRole", + "Arn" + ] + }, + "Runtime": "python3.9", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "EXAMPLEPARTIALLambdaRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "EXAMPLEPARTIALLambdaPostRequestPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "EXAMPLEPARTIALLambda" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/POST/my/route", + { + "__ApiId__": { + "Ref": "EXAMPLEPARTIALGateway" + }, + "__Stage__": "*" + } + ] + } + } + }, + "EXAMPLEPARTIALGateway": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Body": { + "openapi": "3.0.1", + "paths": { + "/my/route": { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${EXAMPLEPARTIALLambda.Arn}/invocations" + }, + "payloadFormatVersion": "2.0" + }, + "responses": {} + } + } + }, + "tags": [ + { + "name": "httpapi:createdBy", + "x-amazon-apigateway-tag-value": "SAM" + } + ] + } + } + }, + "EXAMPLEPARTIALGatewayApiGatewayDefaultStage": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "EXAMPLEPARTIALGateway" + }, + "StageName": "$default", + "Tags": { + "httpapi:createdBy": "SAM" + }, + "AutoDeploy": true + } + } + } + } \ No newline at end of file diff --git a/tests/translator/output/definition_body_intrinsics_support.json b/tests/translator/output/definition_body_intrinsics_support.json new file mode 100644 index 000000000..3d19754b8 --- /dev/null +++ b/tests/translator/output/definition_body_intrinsics_support.json @@ -0,0 +1,146 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "DefinitionBody currently only supports intrinsics when SwaggerEditor/OpenApiEditor is not used (as of 2022-05-30). Let's add tests to make sure we keep this support, because we've had a regression where we accidentally removed this intrinsics support by using the SwaggerEditor. Note that the only supported intrinsic function for DefinitionBody is Fn::If. Other intrinsics are not supported because they don't resolve to dictionary. \n", + "Conditions": { + "FalseCondition": { + "Fn::Equals": [ + true, + false + ] + } + }, + "Resources": { + "RestApiIfIntrinsicAndNoSwaggerEditorDeployment2f0411dccd": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "Description": "RestApi deployment id: 2f0411dccde517dcb5c82d3848dbc431e7479ebc", + "RestApiId": { + "Ref": "RestApiIfIntrinsicAndNoSwaggerEditor" + }, + "StageName": "Stage" + } + }, + "RestApiIfIntrinsicAndNoSwaggerEditorprodStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "RestApiIfIntrinsicAndNoSwaggerEditorDeployment2f0411dccd" + }, + "RestApiId": { + "Ref": "RestApiIfIntrinsicAndNoSwaggerEditor" + }, + "StageName": "prod" + } + }, + "HttpApiIfIntrinsicAndNoOpenApiEditorprodStage": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "HttpApiIfIntrinsicAndNoOpenApiEditor" + }, + "StageName": "prod", + "AutoDeploy": true + } + }, + "RestApiIfIntrinsicAndNoSwaggerEditor": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "Fn::If": [ + "FalseCondition", + { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.alphavantage.co/", + "payloadFormatVersion": "1.0" + } + } + } + }, + "swagger": "2.0" + }, + { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.otheruri.co/", + "payloadFormatVersion": "1.0" + } + } + } + }, + "swagger": "2.0" + } + ] + } + } + }, + "HttpApiIfIntrinsicAndNoOpenApiEditor": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Body": { + "Fn::If": [ + "FalseCondition", + { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.alphavantage.co/", + "payloadFormatVersion": "1.0" + } + } + } + }, + "openapi": "3.0" + }, + { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-Api" + } + }, + "paths": { + "/post": { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": "https://www.otheruri.co/", + "payloadFormatVersion": "1.0" + } + } + } + }, + "openapi": "3.0" + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/error_api_authorizer_property_indentity_header_with_invalid_type.json b/tests/translator/output/error_api_authorizer_property_indentity_header_with_invalid_type.json new file mode 100644 index 000000000..a8cc1d127 --- /dev/null +++ b/tests/translator/output/error_api_authorizer_property_indentity_header_with_invalid_type.json @@ -0,0 +1,8 @@ +{ + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [MyApi] is invalid. MyLambdaAuthUpdated Lambda Authorizer property identity's 'Headers' is of invalid type.", + "errors": [ + { + "errorMessage": "Resource with id [MyApi] is invalid. MyLambdaAuthUpdated Lambda Authorizer property identity's 'Headers' is of invalid type." + } + ] +} \ No newline at end of file diff --git a/tests/translator/output/error_auto_publish_alias_empty_string.json b/tests/translator/output/error_auto_publish_alias_empty_string.json new file mode 100644 index 000000000..46183aa60 --- /dev/null +++ b/tests/translator/output/error_auto_publish_alias_empty_string.json @@ -0,0 +1,3 @@ +{ + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [MinimalFunction] is invalid. 'DeploymentPreference' requires AutoPublishAlias property to be specified." +} \ No newline at end of file diff --git a/tests/translator/output/error_http_api_invalid_openapi.json b/tests/translator/output/error_http_api_invalid_openapi.json index 05782e3c6..990d3880a 100644 --- a/tests/translator/output/error_http_api_invalid_openapi.json +++ b/tests/translator/output/error_http_api_invalid_openapi.json @@ -4,5 +4,5 @@ "errorMessage": "Resource with id [Api] is invalid. Invalid OpenApi document. Invalid values or missing keys for 'openapi' or 'paths' in 'DefinitionBody'." } ], - "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [Api] is invalid. Invalid OpenApi document. Invalid values or missing keys for 'openapi' or 'paths' in 'DefinitionBody'." + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Resource with id [Api] is invalid. Structure of the SAM template is invalid. Invalid OpenApi document. Invalid values or missing keys for 'openapi' or 'paths' in 'DefinitionBody'." } \ No newline at end of file diff --git a/tests/translator/output/error_http_api_null_method.json b/tests/translator/output/error_http_api_null_method.json new file mode 100644 index 000000000..8df1a9ddf --- /dev/null +++ b/tests/translator/output/error_http_api_null_method.json @@ -0,0 +1,3 @@ +{ + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Structure of the SAM template is invalid. Invalid method definition (post) for path: /test" +} \ No newline at end of file diff --git a/tests/translator/output/function_with_alias_and_code_sha256.json b/tests/translator/output/function_with_alias_and_code_sha256.json index 5f89a1632..00b2c40f2 100644 --- a/tests/translator/output/function_with_alias_and_code_sha256.json +++ b/tests/translator/output/function_with_alias_and_code_sha256.json @@ -1,73 +1,71 @@ { + "Parameters": { + "AutoPublishCodeSha256": { + "Type": "String", + "Description": "Sha256 to uniquely identify creation of the lambda", + "Default": "6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b" + } + }, "Resources": { + "MinimalFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "MinimalFunctionRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, "MinimalFunctionVersion6b86b273ff": { - "DeletionPolicy": "Retain", - "Type": "AWS::Lambda::Version", + "Type": "AWS::Lambda::Version", + "DeletionPolicy": "Retain", "Properties": { "Description": "sam-testing", "FunctionName": { "Ref": "MinimalFunction" } } - }, + }, "MinimalFunctionAliaslive": { - "Type": "AWS::Lambda::Alias", + "Type": "AWS::Lambda::Alias", "Properties": { + "Name": "live", + "FunctionName": { + "Ref": "MinimalFunction" + }, "FunctionVersion": { "Fn::GetAtt": [ "MinimalFunctionVersion6b86b273ff", "Version" ] - }, - "FunctionName": { - "Ref": "MinimalFunction" - }, - "Name": "live" - } - }, - "MinimalFunction": { - "Type": "AWS::Lambda::Function", - "Properties": { - "Code": { - "S3Bucket": "sam-demo-bucket", - "S3Key": "hello.zip" - }, - "Handler": "hello.handler", - "Role": { - "Fn::GetAtt": [ - "MinimalFunctionRole", - "Arn" - ] - }, - "Runtime": "python2.7", - "Tags": [ - { - "Value": "SAM", - "Key": "lambda:createdBy" - } - ] + } } - }, + }, "MinimalFunctionRole": { - "Type": "AWS::IAM::Role", + "Type": "AWS::IAM::Role", "Properties": { - "ManagedPolicyArns": [ - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" - ], - "Tags": [ - { - "Value": "SAM", - "Key": "lambda:createdBy" - } - ], "AssumeRolePolicyDocument": { - "Version": "2012-10-17", + "Version": "2012-10-17", "Statement": [ { "Action": [ "sts:AssumeRole" - ], - "Effect": "Allow", + ], + "Effect": "Allow", "Principal": { "Service": [ "lambda.amazonaws.com" @@ -75,7 +73,16 @@ } } ] - } + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] } } } diff --git a/tests/translator/output/http_api_with_null_path.json b/tests/translator/output/http_api_with_null_path.json new file mode 100644 index 000000000..db515b15b --- /dev/null +++ b/tests/translator/output/http_api_with_null_path.json @@ -0,0 +1,122 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "A template to test for API condition handling with a mix of explicit and implicit api events.", + "Resources": { + "EXAMPLEPARTIALLambda": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "EXAMPLEPARTIALLambdaRole", + "Arn" + ] + }, + "Runtime": "python3.9", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "EXAMPLEPARTIALLambdaRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "EXAMPLEPARTIALLambdaPostRequestPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "EXAMPLEPARTIALLambda" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/POST/my/route", + { + "__ApiId__": { + "Ref": "EXAMPLEPARTIALGateway" + }, + "__Stage__": "*" + } + ] + } + } + }, + "EXAMPLEPARTIALGateway": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Body": { + "openapi": "3.0.1", + "paths": { + "/my/route": { + "post": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${EXAMPLEPARTIALLambda.Arn}/invocations" + }, + "payloadFormatVersion": "2.0" + }, + "responses": {} + } + } + }, + "tags": [ + { + "name": "httpapi:createdBy", + "x-amazon-apigateway-tag-value": "SAM" + } + ] + } + } + }, + "EXAMPLEPARTIALGatewayApiGatewayDefaultStage": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "EXAMPLEPARTIALGateway" + }, + "StageName": "$default", + "Tags": { + "httpapi:createdBy": "SAM" + }, + "AutoDeploy": true + } + } + } + } \ No newline at end of file diff --git a/tests/translator/test_translator.py b/tests/translator/test_translator.py index e2942b707..b7230f4dd 100644 --- a/tests/translator/test_translator.py +++ b/tests/translator/test_translator.py @@ -33,6 +33,11 @@ BASE_PATH = os.path.dirname(__file__) INPUT_FOLDER = os.path.join(BASE_PATH, "input") +SUCCESS_FILES_NAMES_FOR_TESTING = [ + os.path.splitext(f)[0] + for f in os.listdir(INPUT_FOLDER) + if not (f.startswith("error_") or f.startswith("translate_")) +] ERROR_FILES_NAMES_FOR_TESTING = [os.path.splitext(f)[0] for f in os.listdir(INPUT_FOLDER) if f.startswith("error_")] OUTPUT_FOLDER = os.path.join(BASE_PATH, "output") @@ -253,238 +258,7 @@ def _generate_new_deployment_hash(self, logical_id, dict_to_hash, rest_api_to_sw class TestTranslatorEndToEnd(AbstractTestTranslator): @parameterized.expand( itertools.product( - [ - "congito_userpool_with_sms_configuration", - "cognito_userpool_with_event", - "s3_with_condition", - "function_with_condition", - "basic_function", - "basic_function_withimageuri", - "basic_application", - "application_preparing_state", - "application_with_intrinsics", - "basic_layer", - "cloudwatchevent", - "cloudwatchevent_intrinsics", - "eventbridgerule", - "eventbridgerule_with_dlq", - "eventbridgerule_with_retry_policy", - "eventbridgerule_schedule_properties", - "cloudwatch_logs_with_ref", - "cloudwatchlog", - "streams", - "sqs", - "function_with_amq", - "function_with_amq_kms", - "function_with_mq_virtual_host", - "simpletable", - "simpletable_with_sse", - "resource_with_invalid_type", - "implicit_api", - "explicit_api", - "api_description", - "api_endpoint_configuration", - "api_endpoint_configuration_with_vpcendpoint", - "api_with_auth_all_maximum", - "api_with_auth_all_minimum", - "api_with_auth_no_default", - "api_with_auth_with_default_scopes", - "api_with_auth_with_default_scopes_openapi", - "api_with_default_aws_iam_auth", - "api_with_default_aws_iam_auth_and_no_auth_route", - "api_with_method_aws_iam_auth", - "api_with_aws_iam_auth_overrides", - "api_with_swagger_authorizer_none", - "api_with_method_settings", - "api_with_binary_media_types", - "api_with_binary_media_types_definition_body", - "api_with_minimum_compression_size", - "api_with_resource_refs", - "api_with_cors", - "api_with_cors_and_auth_no_preflight_auth", - "api_with_cors_and_auth_preflight_auth", - "api_with_cors_and_only_methods", - "api_with_cors_and_only_headers", - "api_with_cors_and_only_origins", - "api_with_cors_and_only_maxage", - "api_with_cors_and_only_credentials_false", - "api_with_cors_no_definitionbody", - "api_with_incompatible_stage_name", - "api_with_gateway_responses", - "api_with_gateway_responses_all", - "api_with_gateway_responses_minimal", - "api_with_gateway_responses_implicit", - "api_with_gateway_responses_string_status_code", - "api_with_identity_intrinsic", - "api_cache", - "api_with_access_log_setting", - "api_with_any_method_in_swagger", - "api_with_canary_setting", - "api_with_xray_tracing", - "api_request_model", - "api_request_model_with_validator", - "api_with_stage_tags", - "api_with_mode", - "api_with_no_properties", - "api_with_disable_api_execute_endpoint", - "s3", - "s3_create_remove", - "s3_existing_lambda_notification_configuration", - "s3_existing_other_notification_configuration", - "s3_filter", - "s3_intrinsics", - "s3_multiple_events_same_bucket", - "s3_multiple_functions", - "s3_with_dependsOn", - "sns", - "sns_sqs", - "sns_existing_sqs", - "sns_intrinsics", - "sns_outside_sqs", - "sns_existing_other_subscription", - "sns_topic_outside_template", - "alexa_skill", - "alexa_skill_with_skill_id", - "iot_rule", - "kinesis_intrinsics", - "layers_with_intrinsics", - "layers_all_properties", - "layer_deletion_policy_precedence", - "function_managed_inline_policy", - "unsupported_resources", - "intrinsic_functions", - "basic_function_with_tags", - "depends_on", - "function_with_ephemeral_storage", - "function_event_conditions", - "function_with_dlq", - "function_with_kmskeyarn", - "function_with_alias", - "function_with_alias_intrinsics", - "function_with_custom_codedeploy_deployment_preference", - "function_with_custom_conditional_codedeploy_deployment_preference", - "function_with_disabled_deployment_preference", - "function_with_disabled_traffic_hook", - "function_with_deployment_preference", - "function_with_deployment_preference_condition_with_passthrough", - "function_with_deployment_preference_condition_without_passthrough", - "function_with_deployment_preference_all_parameters", - "function_with_deployment_preference_from_parameters", - "function_with_deployment_preference_multiple_combinations", - "function_with_deployment_preference_alarms_intrinsic_if", - "function_with_deployment_preference_multiple_combinations_conditions_with_passthrough", - "function_with_deployment_preference_multiple_combinations_conditions_without_passthrough", - "function_with_deployment_preference_passthrough_condition_with_supported_intrinsics", - "function_with_alias_and_event_sources", - "function_with_resource_refs", - "function_with_deployment_and_custom_role", - "function_with_deployment_no_service_role_with_passthrough", - "function_with_deployment_no_service_role_without_passthrough", - "function_with_global_layers", - "function_with_layers", - "function_with_many_layers", - "function_with_null_events", - "function_with_permissions_boundary", - "function_with_policy_templates", - "function_with_sns_event_source_all_parameters", - "function_with_conditional_managed_policy", - "function_with_conditional_managed_policy_and_ref_no_value", - "function_with_conditional_policy_template", - "function_with_conditional_policy_template_and_ref_no_value", - "function_with_request_parameters", - "function_with_signing_profile", - "global_handle_path_level_parameter", - "globals_for_function", - "globals_for_api", - "globals_for_simpletable", - "all_policy_templates", - "simple_table_ref_parameter_intrinsic", - "simple_table_with_table_name", - "function_concurrency", - "simple_table_with_extra_tags", - "explicit_api_with_invalid_events_config", - "no_implicit_api_with_serverless_rest_api_resource", - "implicit_api_deletion_policy_precedence", - "implicit_api_with_serverless_rest_api_resource", - "implicit_api_with_auth_and_conditions_max", - "implicit_api_with_many_conditions", - "implicit_and_explicit_api_with_conditions", - "inline_precedence", - "api_with_cors_and_conditions_no_definitionbody", - "api_with_auth_and_conditions_all_max", - "api_with_apikey_default_override", - "api_with_apikey_required", - "api_with_apikey_source", - "api_with_path_parameters", - "function_with_event_source_mapping", - "function_with_event_dest", - "function_with_event_dest_basic", - "function_with_event_dest_conditional", - "api_with_usageplans", - "api_with_usageplans_shared_attributes_two", - "api_with_usageplans_shared_attributes_three", - "api_with_usageplans_intrinsics", - "state_machine_with_inline_definition", - "state_machine_with_tags", - "state_machine_with_inline_definition_intrinsics", - "state_machine_with_role", - "state_machine_with_inline_policies", - "state_machine_with_sam_policy_templates", - "state_machine_with_definition_S3_string", - "state_machine_with_definition_S3_object", - "state_machine_with_definition_substitutions", - "state_machine_with_standard_logging", - "state_machine_with_express_logging", - "state_machine_with_managed_policy", - "state_machine_with_condition", - "state_machine_with_schedule", - "state_machine_with_schedule_dlq_retry_policy", - "state_machine_with_cwe", - "state_machine_with_eb_retry_policy", - "state_machine_with_eb_dlq", - "state_machine_with_eb_dlq_generated", - "state_machine_with_explicit_api", - "state_machine_with_implicit_api", - "state_machine_with_implicit_api_globals", - "state_machine_with_api_authorizer", - "state_machine_with_api_authorizer_maximum", - "state_machine_with_api_resource_policy", - "state_machine_with_api_auth_default_scopes", - "state_machine_with_condition_and_events", - "state_machine_with_xray_policies", - "state_machine_with_xray_role", - "state_machine_with_null_events", - "function_with_file_system_config", - "state_machine_with_permissions_boundary", - "version_deletion_policy_precedence", - "api_swagger_integration_with_string_api_id", - "api_swagger_integration_with_ref_intrinsic_api_id", - "function_with_architectures", - "function_with_intrinsic_architecture", - "self_managed_kafka_with_intrinsics", - "function_with_self_managed_kafka", - "function_with_auth_mechanism_for_self_managed_kafka", - "function_with_vpc_permission_for_self_managed_kafka", - "function_with_event_filtering", - "api_with_security_definition_and_components", - "api_with_security_definition_and_none_components", - "api_with_security_definition_and_no_components", - "api_http_paths_with_if_condition", - "api_http_paths_with_if_condition_no_value_then_case", - "api_http_paths_with_if_condition_no_value_else_case", - "api_rest_paths_with_if_condition_swagger", - "api_rest_paths_with_if_condition_swagger_no_value_then_case", - "api_rest_paths_with_if_condition_swagger_no_value_else_case", - "api_rest_paths_with_if_condition_openapi", - "api_rest_paths_with_if_condition_openapi_no_value_then_case", - "api_rest_paths_with_if_condition_openapi_no_value_else_case", - "function_with_function_url_config", - "function_with_function_url_config_conditions", - "function_with_function_url_config_with_intrinsics", - "function_with_function_url_config_with_iam_authorization_type", - "function_with_function_url_config_without_cors_config", - "function_with_function_url_config_and_autopublishalias", - ], + SUCCESS_FILES_NAMES_FOR_TESTING, [ ("aws", "ap-southeast-1"), ("aws-cn", "cn-north-1"),