diff --git a/appveyor.yml b/appveyor.yml index 606fe62d5f..ed730e0a24 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -159,7 +159,7 @@ for: # Pre-dev Tests - "pip install -e \".[pre-dev]\"" - "pylint --rcfile .pylintrc samcli" - + # Dev Tests - "pip install -e \".[dev]\"" - "pytest --cov samcli --cov-report term-missing --cov-fail-under 94 tests/unit" @@ -170,7 +170,11 @@ for: # Runs only in Linux, logging docker hub when running canary and docker cred is available - sh: " if [[ -n $BY_CANARY ]] && [[ -n $DOCKER_USER ]] && [[ -n $DOCKER_PASS ]]; - then echo Logging in Docker Hub; echo $DOCKER_PASS | docker login --username $DOCKER_USER --password-stdin; + then echo Logging in Docker Hub; echo $DOCKER_PASS | docker login --username $DOCKER_USER --password-stdin registry-1.docker.io; + fi" + - sh: " + if [[ -n $BY_CANARY ]]; + then echo Logging in Public ECR; aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws; fi" - sh: "pytest -vv tests/integration" - sh: "pytest -vv tests/regression" diff --git a/requirements/base.txt b/requirements/base.txt index ab432ff159..25efa93b05 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -6,7 +6,7 @@ boto3~=1.14 jmespath~=0.10.0 PyYAML~=5.3 cookiecutter~=1.7.2 -aws-sam-translator==1.36.0 +aws-sam-translator==1.37.0 #docker minor version updates can include breaking changes. Auto update micro version only. docker~=4.2.0 dateparser~=0.7 diff --git a/requirements/reproducible-linux.txt b/requirements/reproducible-linux.txt index a7a92e25a4..a2f725e5fd 100644 --- a/requirements/reproducible-linux.txt +++ b/requirements/reproducible-linux.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile +# This file is autogenerated by pip-compile with python 3.7 # To update, run: # # pip-compile --allow-unsafe --generate-hashes --output-file=requirements/reproducible-linux.txt @@ -17,10 +17,10 @@ aws-lambda-builders==1.4.0 \ --hash=sha256:5d4e4ecb3d3290f0eec1f62b7b0d9d6b91160ae71447d95899eede392d05f75f \ --hash=sha256:d32f79cf67b189a7598793f69797f284b2eb9a9fada562175b1e854187f95aed # via aws-sam-cli (setup.py) -aws-sam-translator==1.36.0 \ - --hash=sha256:4195ae8196f04803e7f0384a2b5ccd8c2b06ce0d8dc408aa1f1ce96c23bcf39d \ - --hash=sha256:f7d51b661fe1f5613a882f4733d1c92eff4dac36a076eafd18031d209b178695 \ - --hash=sha256:fa1b990d9329d19052e7b91cf0b19371ed9d31a529054b616005884cd662b584 +aws-sam-translator==1.37.0 \ + --hash=sha256:12cbf4af9e95acf73dabfbc44af990dc1e880f35697bb8c04f31b3bb90ab5526 \ + --hash=sha256:26e4866627e4284afc367bee2bd04d3cf23cecc8ff879b419457715a738395a9 \ + --hash=sha256:6884d942a815450637bac48e297996df2dacc27077d25ced09d8e9ce1f6a585c # via aws-sam-cli (setup.py) binaryornot==0.4.4 \ --hash=sha256:359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061 \ @@ -88,10 +88,6 @@ itsdangerous==1.1.0 \ --hash=sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19 \ --hash=sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749 # via flask -jinja2-time==0.2.0 \ - --hash=sha256:d14eaa4d315e7688daa4969f616f226614350c48730bfa1692d2caebd8c90d40 \ - --hash=sha256:d3eab6605e3ec8b7a0863df09cc1d23714908fa61aa6986a845c20ba488b4efa - # via cookiecutter jinja2==2.11.3 \ --hash=sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419 \ --hash=sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6 @@ -99,6 +95,10 @@ jinja2==2.11.3 \ # cookiecutter # flask # jinja2-time +jinja2-time==0.2.0 \ + --hash=sha256:d14eaa4d315e7688daa4969f616f226614350c48730bfa1692d2caebd8c90d40 \ + --hash=sha256:d3eab6605e3ec8b7a0863df09cc1d23714908fa61aa6986a845c20ba488b4efa + # via cookiecutter jmespath==0.10.0 \ --hash=sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9 \ --hash=sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f diff --git a/samcli/__init__.py b/samcli/__init__.py index 3fe00ac134..1c484dccfd 100644 --- a/samcli/__init__.py +++ b/samcli/__init__.py @@ -2,4 +2,4 @@ SAM CLI version """ -__version__ = "1.24.1" +__version__ = "1.26.0" diff --git a/samcli/commands/deploy/command.py b/samcli/commands/deploy/command.py index 371dc61c4d..5b7744b89d 100644 --- a/samcli/commands/deploy/command.py +++ b/samcli/commands/deploy/command.py @@ -153,7 +153,9 @@ "--resolve-s3", required=False, is_flag=True, - help="Automatically resolve s3 bucket for non-guided deployments." + help="Automatically resolve s3 bucket for non-guided deployments. " + "Enabling this option will also create a managed default s3 bucket for you. " + "If you do not provide a --s3-bucket value, the managed bucket will be used. " "Do not use --s3-guided parameter with this option.", ) @metadata_override_option diff --git a/samcli/commands/deploy/guided_context.py b/samcli/commands/deploy/guided_context.py index b4bb65a0d6..dafdf0a331 100644 --- a/samcli/commands/deploy/guided_context.py +++ b/samcli/commands/deploy/guided_context.py @@ -316,7 +316,7 @@ def prompt_image_repository(self, stacks: List[Stack]): if isinstance(self.image_repositories, dict) else "" or self.image_repository, ) - if not is_ecr_url(image_repositories.get(resource_id)): + if resource_id not in image_repositories or not is_ecr_url(str(image_repositories[resource_id])): raise GuidedDeployFailedError( f"Invalid Image Repository ECR URI: {image_repositories.get(resource_id)}" ) diff --git a/samcli/commands/package/command.py b/samcli/commands/package/command.py index cab68b6d88..cc0dc35c5d 100644 --- a/samcli/commands/package/command.py +++ b/samcli/commands/package/command.py @@ -121,7 +121,9 @@ def resources_and_properties_help_string(): exc_set=PackageResolveS3AndS3SetError, exc_not_set=PackageResolveS3AndS3NotSetError, ), - help="Automatically resolve s3 bucket for non-guided deployments." + help="Automatically resolve s3 bucket for non-guided deployments. " + "Enabling this option will also create a managed default s3 bucket for you. " + "If you do not provide a --s3-bucket value, the managed bucket will be used. " "Do not use --s3-guided parameter with this option.", ) @metadata_override_option diff --git a/samcli/commands/package/exceptions.py b/samcli/commands/package/exceptions.py index a650f62843..af549058e9 100644 --- a/samcli/commands/package/exceptions.py +++ b/samcli/commands/package/exceptions.py @@ -124,7 +124,8 @@ class BucketNotSpecifiedError(UserException): def __init__(self, **kwargs): self.kwargs = kwargs - message_fmt = "\nS3 Bucket not specified, use --s3-bucket to specify a bucket name or run sam deploy --guided" + message_fmt = "\nS3 Bucket not specified, use --s3-bucket to specify a bucket name, or use --resolve-s3 \ +to create a managed default bucket, or run sam deploy --guided" super().__init__(message=message_fmt.format(**self.kwargs)) diff --git a/samcli/lib/bootstrap/bootstrap.py b/samcli/lib/bootstrap/bootstrap.py index 81c30c7748..eaed58d630 100644 --- a/samcli/lib/bootstrap/bootstrap.py +++ b/samcli/lib/bootstrap/bootstrap.py @@ -73,6 +73,9 @@ def _get_stack_template(): - "/*" Principal: Service: serverlessrepo.amazonaws.com + Condition: + StringEquals: + aws:SourceAccount: !Ref AWS::AccountId Outputs: SourceBucket: diff --git a/samcli/lib/build/build_strategy.py b/samcli/lib/build/build_strategy.py index ecded3a743..258101ba2d 100644 --- a/samcli/lib/build/build_strategy.py +++ b/samcli/lib/build/build_strategy.py @@ -5,6 +5,7 @@ import pathlib import shutil from abc import abstractmethod, ABC +from copy import deepcopy from typing import Callable, Dict, List, Any, Optional, cast from samcli.commands.build.exceptions import MissingBuildMethodException @@ -114,6 +115,10 @@ def build_single_function_definition(self, build_definition: FunctionBuildDefini LOG.debug("Building to following folder %s", single_build_dir) + # we should create a copy and pass it down, otherwise additional env vars like LAMBDA_BUILDERS_LOG_LEVEL + # will make cache invalid all the time + container_env_vars = deepcopy(build_definition.env_vars) + # when a function is passed here, it is ZIP function, codeuri and runtime are not None result = self._build_function( build_definition.get_function_name(), @@ -123,7 +128,7 @@ def build_single_function_definition(self, build_definition: FunctionBuildDefini build_definition.get_handler_name(), single_build_dir, build_definition.metadata, - build_definition.env_vars, + container_env_vars, ) function_build_results[single_full_path] = result diff --git a/samcli/lib/package/ecr_utils.py b/samcli/lib/package/ecr_utils.py index 6186d24099..f4bedc4a27 100644 --- a/samcli/lib/package/ecr_utils.py +++ b/samcli/lib/package/ecr_utils.py @@ -6,5 +6,5 @@ from samcli.lib.package.regexpr import ECR_URL -def is_ecr_url(url): +def is_ecr_url(url: str) -> bool: return bool(re.match(ECR_URL, url)) if url else False diff --git a/samcli/lib/package/s3_uploader.py b/samcli/lib/package/s3_uploader.py index 4a64a983d0..34ac666b86 100644 --- a/samcli/lib/package/s3_uploader.py +++ b/samcli/lib/package/s3_uploader.py @@ -85,7 +85,7 @@ def upload(self, file_name: str, remote_path: str) -> str: # Check if a file with same data exists if not self.force_upload and self.file_exists(remote_path): - LOG.debug("File with same data is already exists at %s. " "Skipping upload", remote_path) + LOG.info("File with same data already exists at %s, skipping upload", remote_path) return self.make_url(remote_path) try: diff --git a/samcli/lib/providers/sam_base_provider.py b/samcli/lib/providers/sam_base_provider.py index c059284eb8..7a75c70cc8 100644 --- a/samcli/lib/providers/sam_base_provider.py +++ b/samcli/lib/providers/sam_base_provider.py @@ -10,6 +10,8 @@ from samcli.lib.intrinsic_resolver.intrinsics_symbol_table import IntrinsicsSymbolTable from samcli.lib.samlib.resource_metadata_normalizer import ResourceMetadataNormalizer from samcli.lib.samlib.wrapper import SamTranslatorWrapper +from samcli.lib.package.ecr_utils import is_ecr_url + LOG = logging.getLogger(__name__) @@ -34,6 +36,11 @@ class SamBaseProvider: SERVERLESS_LAYER: "ContentUri", } + IMAGE_PROPERTY_KEYS = { + LAMBDA_FUNCTION: "Code", + SERVERLESS_FUNCTION: "ImageUri", + } + def get(self, name: str) -> Optional[Any]: """ Given name of the function, this method must return the Function object @@ -88,6 +95,17 @@ def _is_s3_location(location: Optional[Union[str, Dict]]) -> bool: isinstance(location, str) and location.startswith("s3://") ) + @staticmethod + def _is_ecr_uri(location: Optional[Union[str, Dict]]) -> bool: + """ + the input could be: + - ImageUri of Serverless::Function + - Code of Lambda::Function + """ + return location is not None and is_ecr_url( + str(location.get("ImageUri", "")) if isinstance(location, dict) else location + ) + @staticmethod def _warn_code_extraction(resource_type: str, resource_name: str, code_property: str) -> None: LOG.warning( @@ -98,6 +116,16 @@ def _warn_code_extraction(resource_type: str, resource_name: str, code_property: code_property, ) + @staticmethod + def _warn_imageuri_extraction(resource_type: str, resource_name: str, image_property: str) -> None: + LOG.warning( + "The resource %s '%s' has specified ECR registry image for %s. " + "It will not be built and SAM CLI does not support invoking it locally.", + resource_type, + resource_name, + image_property, + ) + @staticmethod def _extract_lambda_function_imageuri(resource_properties: Dict, code_property_key: str) -> Optional[str]: """ diff --git a/samcli/lib/providers/sam_function_provider.py b/samcli/lib/providers/sam_function_provider.py index 7bc231f929..6bffc4bf75 100644 --- a/samcli/lib/providers/sam_function_provider.py +++ b/samcli/lib/providers/sam_function_provider.py @@ -130,13 +130,28 @@ def _extract_functions( resource_properties["Metadata"] = resource_metadata if resource_type in [SamFunctionProvider.SERVERLESS_FUNCTION, SamFunctionProvider.LAMBDA_FUNCTION]: + resource_package_type = resource_properties.get("PackageType", ZIP) + code_property_key = SamBaseProvider.CODE_PROPERTY_KEYS[resource_type] - if SamBaseProvider._is_s3_location(resource_properties.get(code_property_key)): + image_property_key = SamBaseProvider.IMAGE_PROPERTY_KEYS[resource_type] + + if resource_package_type == ZIP and SamBaseProvider._is_s3_location( + resource_properties.get(code_property_key) + ): + # CodeUri can be a dictionary of S3 Bucket/Key or a S3 URI, neither of which are supported if not ignore_code_extraction_warnings: SamFunctionProvider._warn_code_extraction(resource_type, name, code_property_key) continue + if resource_package_type == IMAGE and SamBaseProvider._is_ecr_uri( + resource_properties.get(image_property_key) + ): + # ImageUri can be an ECR uri, which is not supported + if not ignore_code_extraction_warnings: + SamFunctionProvider._warn_imageuri_extraction(resource_type, name, image_property_key) + continue + if resource_type == SamFunctionProvider.SERVERLESS_FUNCTION: layers = SamFunctionProvider._parse_layer_info( stack, diff --git a/samcli/local/apigw/local_apigw_service.py b/samcli/local/apigw/local_apigw_service.py index cc2684c200..5a6d397d54 100644 --- a/samcli/local/apigw/local_apigw_service.py +++ b/samcli/local/apigw/local_apigw_service.py @@ -333,7 +333,7 @@ def _request_handler(self, **kwargs): ) else: (status_code, headers, body) = self._parse_v1_payload_format_lambda_output( - lambda_response, self.api.binary_media_types, request + lambda_response, self.api.binary_media_types, request, route.event_type ) except LambdaResponseParseException as ex: LOG.error("Invalid lambda response received: %s", ex) @@ -379,13 +379,14 @@ def get_request_methods_endpoints(flask_request): # Consider moving this out to its own class. Logic is started to get dense and looks messy @jfuss @staticmethod - def _parse_v1_payload_format_lambda_output(lambda_output: str, binary_types, flask_request): + def _parse_v1_payload_format_lambda_output(lambda_output: str, binary_types, flask_request, event_type): """ Parses the output from the Lambda Container :param str lambda_output: Output from Lambda Invoke :param binary_types: list of binary types :param flask_request: flash request object + :param event_type: determines the route event type :return: Tuple(int, dict, str, bool) """ # pylint: disable-msg=too-many-statements @@ -397,6 +398,9 @@ def _parse_v1_payload_format_lambda_output(lambda_output: str, binary_types, fla if not isinstance(json_output, dict): raise LambdaResponseParseException(f"Lambda returned {type(json_output)} instead of dict") + if event_type == Route.HTTP and json_output.get("statusCode") is None: + raise LambdaResponseParseException(f"Invalid API Gateway Response Key: statusCode is not in {json_output}") + status_code = json_output.get("statusCode") or 200 headers = LocalApigwService._merge_response_headers( json_output.get("headers") or {}, json_output.get("multiValueHeaders") or {} @@ -405,7 +409,8 @@ def _parse_v1_payload_format_lambda_output(lambda_output: str, binary_types, fla body = json_output.get("body") if body is None: LOG.warning("Lambda returned empty body!") - is_base_64_encoded = json_output.get("isBase64Encoded") or False + + is_base_64_encoded = LocalApigwService.get_base_64_encoded(event_type, json_output) try: status_code = int(status_code) @@ -422,8 +427,10 @@ def _parse_v1_payload_format_lambda_output(lambda_output: str, binary_types, fla f"Non null response bodies should be able to convert to string: {body}" ) from ex - invalid_keys = LocalApigwService._invalid_apig_response_keys(json_output) - if invalid_keys: + invalid_keys = LocalApigwService._invalid_apig_response_keys(json_output, event_type) + # HTTP API Gateway just skip the non allowed lambda response fields, but Rest API gateway fail on + # the non allowed fields + if event_type == Route.API and invalid_keys: raise LambdaResponseParseException(f"Invalid API Gateway Response Keys: {invalid_keys} in {json_output}") # If the customer doesn't define Content-Type default to application/json @@ -432,17 +439,51 @@ def _parse_v1_payload_format_lambda_output(lambda_output: str, binary_types, fla headers["Content-Type"] = "application/json" try: - if LocalApigwService._should_base64_decode_body(binary_types, flask_request, headers, is_base_64_encoded): + # HTTP API Gateway always decode the lambda response only if isBase64Encoded field in response is True + # regardless the response content-type + # Rest API Gateway depends on the response content-type and the API configured BinaryMediaTypes to decide + # if it will decode the response or not + if (event_type == Route.HTTP and is_base_64_encoded) or ( + event_type == Route.API + and LocalApigwService._should_base64_decode_body( + binary_types, flask_request, headers, is_base_64_encoded + ) + ): body = base64.b64decode(body) except ValueError as ex: LambdaResponseParseException(str(ex)) return status_code, headers, body + @staticmethod + def get_base_64_encoded(event_type, json_output): + # The following behaviour is undocumented behaviour, and based on some trials + # Http API gateway checks lambda response for isBase64Encoded field, and ignore base64Encoded + # Rest API gateway checks first the field base64Encoded field, if not exist, it checks isBase64Encoded field + + if event_type == Route.API and json_output.get("base64Encoded") is not None: + is_base_64_encoded = json_output.get("base64Encoded") + field_name = "base64Encoded" + elif json_output.get("isBase64Encoded") is not None: + is_base_64_encoded = json_output.get("isBase64Encoded") + field_name = "isBase64Encoded" + else: + is_base_64_encoded = False + field_name = "isBase64Encoded" + + if isinstance(is_base_64_encoded, str) and is_base_64_encoded in ["true", "True", "false", "False"]: + is_base_64_encoded = is_base_64_encoded in ["true", "True"] + elif not isinstance(is_base_64_encoded, bool): + raise LambdaResponseParseException( + f"Invalid API Gateway Response Key: {is_base_64_encoded} is not a valid" f"{field_name}" + ) + + return is_base_64_encoded + @staticmethod def _parse_v2_payload_format_lambda_output(lambda_output: str, binary_types, flask_request): """ - Parses the output from the Lambda Container + Parses the output from the Lambda Container. V2 Payload Format means that the event_type is only HTTP :param str lambda_output: Output from Lambda Invoke :param binary_types: list of binary types @@ -487,21 +528,15 @@ def _parse_v2_payload_format_lambda_output(lambda_output: str, binary_types, fla f"Non null response bodies should be able to convert to string: {body}" ) from ex - # API Gateway only accepts statusCode, body, headers, and isBase64Encoded in - # a response shape. - # Don't check the response keys when inferring a response, see - # https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html#http-api-develop-integrations-lambda.v2. - invalid_keys = LocalApigwService._invalid_apig_response_keys(json_output) - if "statusCode" in json_output and invalid_keys: - raise LambdaResponseParseException(f"Invalid API Gateway Response Keys: {invalid_keys} in {json_output}") - # If the customer doesn't define Content-Type default to application/json if "Content-Type" not in headers: LOG.info("No Content-Type given. Defaulting to 'application/json'.") headers["Content-Type"] = "application/json" try: - if LocalApigwService._should_base64_decode_body(binary_types, flask_request, headers, is_base_64_encoded): + # HTTP API Gateway always decode the lambda response only if isBase64Encoded field in response is True + # regardless the response content-type + if is_base_64_encoded: # Note(xinhol): here in this method we change the type of the variable body multiple times # and confused mypy, we might want to avoid this and use multiple variables here. body = base64.b64decode(body) # type: ignore @@ -511,8 +546,10 @@ def _parse_v2_payload_format_lambda_output(lambda_output: str, binary_types, fla return status_code, headers, body @staticmethod - def _invalid_apig_response_keys(output): + def _invalid_apig_response_keys(output, event_type): allowable = {"statusCode", "body", "headers", "multiValueHeaders", "isBase64Encoded", "cookies"} + if event_type == Route.API: + allowable.add("base64Encoded") invalid_keys = output.keys() - allowable return invalid_keys diff --git a/tests/functional/commands/validate/lib/models/function_with_mq_virtual_host.yaml b/tests/functional/commands/validate/lib/models/function_with_mq_virtual_host.yaml new file mode 100644 index 0000000000..b5d2c62085 --- /dev/null +++ b/tests/functional/commands/validate/lib/models/function_with_mq_virtual_host.yaml @@ -0,0 +1,19 @@ +Resources: + MQFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/queues.zip + Handler: queue.mq_handler + Runtime: python2.7 + Events: + MyMQQueue: + Type: MQ + Properties: + Broker: arn:aws:mq:us-east-2:123456789012:broker:MyBroker:b-1234a5b6-78cd-901e-2fgh-3i45j6k178l9 + Queues: + - "Queue1" + SourceAccessConfigurations: + - Type: BASIC_AUTH + URI: arn:aws:secretsmanager:us-west-2:123456789012:secret:my-path/my-secret-name-1a2b3c + - Type: VIRTUAL_HOST + URI: vhost_name \ No newline at end of file diff --git a/tests/integration/buildcmd/test_build_cmd.py b/tests/integration/buildcmd/test_build_cmd.py index ba25849672..4681a4f2eb 100644 --- a/tests/integration/buildcmd/test_build_cmd.py +++ b/tests/integration/buildcmd/test_build_cmd.py @@ -1346,6 +1346,36 @@ def test_cache_build(self, use_container, code_uri, function1_handler, function2 expected_messages, command_result, self._make_parameter_override_arg(overrides) ) + @skipIf(SKIP_DOCKER_TESTS, SKIP_DOCKER_MESSAGE) + def test_cached_build_with_env_vars(self): + """ + Build 2 times to verify that second time hits the cached build + """ + overrides = { + "FunctionCodeUri": "Python", + "Function1Handler": "main.first_function_handler", + "Function2Handler": "main.second_function_handler", + "FunctionRuntime": "python3.8", + } + cmdlist = self.get_command_list( + use_container=True, parameter_overrides=overrides, cached=True, container_env_var="FOO=BAR" + ) + + LOG.info("Running Command (cache should be invalid): %s", cmdlist) + command_result = run_command(cmdlist, cwd=self.working_dir) + self.assertTrue( + "Cache is invalid, running build and copying resources to function build definition" + in command_result.stderr.decode("utf-8") + ) + + LOG.info("Re-Running Command (valid cache should exist): %s", cmdlist) + command_result_with_cache = run_command(cmdlist, cwd=self.working_dir) + + self.assertTrue( + "Valid cache found, copying previously built resources from function build definition" + in command_result_with_cache.stderr.decode("utf-8") + ) + @skipIf( ((IS_WINDOWS and RUNNING_ON_CI) and not CI_OVERRIDE), diff --git a/tests/integration/deploy/test_deploy_command.py b/tests/integration/deploy/test_deploy_command.py index 3e4bd53f87..893799e157 100644 --- a/tests/integration/deploy/test_deploy_command.py +++ b/tests/integration/deploy/test_deploy_command.py @@ -313,7 +313,8 @@ def test_deploy_without_s3_bucket(self, template_file): self.assertEqual(deploy_process_execute.process.returncode, 1) self.assertIn( bytes( - f"S3 Bucket not specified, use --s3-bucket to specify a bucket name or run sam deploy --guided", + f"S3 Bucket not specified, use --s3-bucket to specify a bucket name, or use --resolve-s3 \ +to create a managed default bucket, or run sam deploy --guided", encoding="utf-8", ), deploy_process_execute.stderr, diff --git a/tests/integration/local/start_api/test_start_api.py b/tests/integration/local/start_api/test_start_api.py index e7e5ad59a1..0ddb8d5a31 100644 --- a/tests/integration/local/start_api/test_start_api.py +++ b/tests/integration/local/start_api/test_start_api.py @@ -1,3 +1,4 @@ +import base64 import uuid import random @@ -382,14 +383,14 @@ def test_valid_v2_lambda_integer_response(self): @pytest.mark.flaky(reruns=3) @pytest.mark.timeout(timeout=600, method="thread") - def test_invalid_v2_lambda_response(self): + def test_v2_lambda_response_skip_unexpected_fields(self): """ Patch Request to a path that was defined as ANY in SAM through AWS::Serverless::Function Events """ response = requests.get(self.url + "/invalidv2response", timeout=300) - self.assertEqual(response.status_code, 502) - self.assertEqual(response.json(), {"message": "Internal server error"}) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), {"hello": "world"}) @pytest.mark.flaky(reruns=3) @pytest.mark.timeout(timeout=600, method="thread") @@ -538,6 +539,48 @@ def test_binary_response(self): self.assertEqual(response.headers.get("Content-Type"), "image/gif") self.assertEqual(response.content, expected) + @pytest.mark.flaky(reruns=3) + @pytest.mark.timeout(timeout=600, method="thread") + def test_non_decoded_binary_response(self): + """ + Binary data is returned correctly + """ + expected = base64.b64encode(self.get_binary_data(self.binary_data_file)) + + response = requests.get(self.url + "/nondecodedbase64response", timeout=300) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.headers.get("Content-Type"), "image/gif") + self.assertEqual(response.content, expected) + + @pytest.mark.flaky(reruns=3) + @pytest.mark.timeout(timeout=600, method="thread") + def test_decoded_binary_response_base64encoded_field(self): + """ + Binary data is returned correctly + """ + expected = self.get_binary_data(self.binary_data_file) + + response = requests.get(self.url + "/decodedbase64responsebas64encoded", timeout=300) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.headers.get("Content-Type"), "image/gif") + self.assertEqual(response.content, expected) + + @pytest.mark.flaky(reruns=3) + @pytest.mark.timeout(timeout=600, method="thread") + def test_decoded_binary_response_base64encoded_field_is_priority(self): + """ + Binary data is returned correctly + """ + expected = base64.b64encode(self.get_binary_data(self.binary_data_file)) + + response = requests.get(self.url + "/decodedbase64responsebas64encodedpriority", timeout=300) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.headers.get("Content-Type"), "image/gif") + self.assertEqual(response.content, expected) + class TestStartApiWithSwaggerHttpApis(StartApiIntegBaseClass): template_path = "/testdata/start_api/swagger-template-http-api.yaml" diff --git a/tests/integration/testdata/start_api/binarydata.gif b/tests/integration/testdata/start_api/binarydata.gif index 855b404179..3f40c2073d 100644 Binary files a/tests/integration/testdata/start_api/binarydata.gif and b/tests/integration/testdata/start_api/binarydata.gif differ diff --git a/tests/integration/testdata/start_api/image_package_type/main.py b/tests/integration/testdata/start_api/image_package_type/main.py index 452d91fd12..eba4c12408 100644 --- a/tests/integration/testdata/start_api/image_package_type/main.py +++ b/tests/integration/testdata/start_api/image_package_type/main.py @@ -70,7 +70,7 @@ def invalid_hash_response(event, context): def base64_response(event, context): - gifImageBase64 = "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" # NOQA + gifImageBase64 = "R0lGODdhAQABAJEAAAAAAP///wAAAAAAACH5BAkAAAIALAAAAAABAAEAAAICRAEAOw==" # NOQA return { "statusCode": 200, diff --git a/tests/integration/testdata/start_api/main.py b/tests/integration/testdata/start_api/main.py index 452d91fd12..19dc6f51c2 100644 --- a/tests/integration/testdata/start_api/main.py +++ b/tests/integration/testdata/start_api/main.py @@ -2,6 +2,8 @@ import sys import time +GIF_IMAGE_BASE64 = "R0lGODdhAQABAJEAAAAAAP///wAAAAAAACH5BAkAAAIALAAAAAABAAEAAAICRAEAOw==" + def handler(event, context): return {"statusCode": 200, "body": json.dumps({"hello": "world"})} @@ -70,11 +72,41 @@ def invalid_hash_response(event, context): def base64_response(event, context): - gifImageBase64 = "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" # NOQA return { "statusCode": 200, - "body": gifImageBase64, + "body": GIF_IMAGE_BASE64, + "isBase64Encoded": True, + "headers": {"Content-Type": "image/gif"}, + } + + +def base64_with_False_isBase64Encoded_response(event, context): + + return { + "statusCode": 200, + "body": GIF_IMAGE_BASE64, + "isBase64Encoded": False, + "headers": {"Content-Type": "image/gif"}, + } + + +def base64_with_True_Base64Encoded_response(event, context): + + return { + "statusCode": 200, + "body": GIF_IMAGE_BASE64, + "base64Encoded": True, + "headers": {"Content-Type": "image/gif"}, + } + + +def base64_with_Base64Encoded_priority_response(event, context): + + return { + "statusCode": 200, + "body": GIF_IMAGE_BASE64, + "base64Encoded": False, "isBase64Encoded": True, "headers": {"Content-Type": "image/gif"}, } diff --git a/tests/integration/testdata/start_api/swagger-template.yaml b/tests/integration/testdata/start_api/swagger-template.yaml index 880115d2d7..3b29656f11 100644 --- a/tests/integration/testdata/start_api/swagger-template.yaml +++ b/tests/integration/testdata/start_api/swagger-template.yaml @@ -67,6 +67,30 @@ Resources: uri: Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Base64ResponseFunction.Arn}/invocations + "/nondecodedbase64response": + get: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Base64ResponseFunctionWithFalseIsBase64EncodedField.Arn}/invocations + + "/decodedbase64responsebas64encoded": + get: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Base64ResponseFunctionWithTrueBase64EncodedField.Arn}/invocations + + "/decodedbase64responsebas64encodedpriority": + get: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Base64ResponseFunctionWithBase64EncodedFieldPriority.Arn}/invocations + "/echobase64eventbody": post: x-amazon-apigateway-integration: @@ -124,6 +148,30 @@ Resources: CodeUri: . Timeout: 600 + Base64ResponseFunctionWithFalseIsBase64EncodedField: + Type: AWS::Serverless::Function + Properties: + Handler: main.base64_with_False_isBase64Encoded_response + Runtime: python3.6 + CodeUri: . + Timeout: 600 + + Base64ResponseFunctionWithTrueBase64EncodedField: + Type: AWS::Serverless::Function + Properties: + Handler: main.base64_with_True_Base64Encoded_response + Runtime: python3.6 + CodeUri: . + Timeout: 600 + + Base64ResponseFunctionWithBase64EncodedFieldPriority: + Type: AWS::Serverless::Function + Properties: + Handler: main.base64_with_Base64Encoded_priority_response + Runtime: python3.6 + CodeUri: . + Timeout: 600 + EchoBase64EventBodyFunction: Type: AWS::Serverless::Function Properties: diff --git a/tests/unit/commands/local/lib/test_sam_function_provider.py b/tests/unit/commands/local/lib/test_sam_function_provider.py index 3d33f1a312..9daf92abc0 100644 --- a/tests/unit/commands/local/lib/test_sam_function_provider.py +++ b/tests/unit/commands/local/lib/test_sam_function_provider.py @@ -63,10 +63,6 @@ class TestSamFunctionProviderEndToEnd(TestCase): "Handler": "index.handler", }, }, - "SamFunc4": { - "Type": "AWS::Serverless::Function", - "Properties": {"ImageUri": "123456789012.dkr.ecr.us-east-1.amazonaws.com/myrepo", "PackageType": IMAGE}, - }, "SamFuncWithFunctionNameOverride": { "Type": "AWS::Serverless::Function", "Properties": { @@ -76,6 +72,29 @@ class TestSamFunctionProviderEndToEnd(TestCase): "Handler": "index.handler", }, }, + "SamFuncWithImage1": { + "Type": "AWS::Serverless::Function", + "Properties": { + "PackageType": IMAGE, + }, + "Metadata": {"DockerTag": "tag", "DockerContext": "./image", "Dockerfile": "Dockerfile"}, + }, + "SamFuncWithImage2": { + "Type": "AWS::Serverless::Function", + "Properties": { + "ImageUri": "image:tag", + "PackageType": IMAGE, + }, + "Metadata": {"DockerTag": "tag", "DockerContext": "./image", "Dockerfile": "Dockerfile"}, + }, + "SamFuncWithImage3": { + # ImageUri is unsupported ECR location + "Type": "AWS::Serverless::Function", + "Properties": { + "ImageUri": "123456789012.dkr.ecr.us-east-1.amazonaws.com/myrepo:myimage", + "PackageType": IMAGE, + }, + }, "LambdaFunc1": { "Type": "AWS::Lambda::Function", "Properties": { @@ -84,21 +103,37 @@ class TestSamFunctionProviderEndToEnd(TestCase): "Handler": "index.handler", }, }, - "LambdaFuncWithInlineCode": { + "LambdaFuncWithImage1": { "Type": "AWS::Lambda::Function", "Properties": { - "Code": {"ZipFile": "testcode"}, - "Runtime": "nodejs4.3", - "Handler": "index.handler", + "PackageType": IMAGE, + }, + "Metadata": {"DockerTag": "tag", "DockerContext": "./image", "Dockerfile": "Dockerfile"}, + }, + "LambdaFuncWithImage2": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": {"ImageUri": "image:tag"}, + "PackageType": IMAGE, }, + "Metadata": {"DockerTag": "tag", "DockerContext": "./image", "Dockerfile": "Dockerfile"}, }, - "LambdaFunc2": { + "LambdaFuncWithImage3": { + # ImageUri is unsupported ECR location "Type": "AWS::Lambda::Function", "Properties": { "Code": {"ImageUri": "123456789012.dkr.ecr.us-east-1.amazonaws.com/myrepo"}, "PackageType": IMAGE, }, }, + "LambdaFuncWithInlineCode": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": {"ZipFile": "testcode"}, + "Runtime": "nodejs4.3", + "Handler": "index.handler", + }, + }, "LambdaFuncWithLocalPath": { "Type": "AWS::Lambda::Function", "Properties": {"Code": "./some/path/to/code", "Runtime": "nodejs4.3", "Handler": "index.handler"}, @@ -248,10 +283,10 @@ def setUp(self): ("SamFunc2", None), # codeuri is a s3 location, ignored ("SamFunc3", None), # codeuri is a s3 location, ignored ( - "SamFunc4", + "SamFuncWithImage1", Function( - name="SamFunc4", - functionname="SamFunc4", + name="SamFuncWithImage1", + functionname="SamFuncWithImage1", runtime=None, handler=None, codeuri=".", @@ -262,14 +297,46 @@ def setUp(self): layers=[], events=None, inlinecode=None, - imageuri="123456789012.dkr.ecr.us-east-1.amazonaws.com/myrepo", + imageuri=None, imageconfig=None, packagetype=IMAGE, - metadata=None, + metadata={ + "DockerTag": "tag", + "DockerContext": os.path.join("image"), + "Dockerfile": "Dockerfile", + }, codesign_config_arn=None, stack_path="", ), ), + ( + "SamFuncWithImage2", + Function( + name="SamFuncWithImage2", + functionname="SamFuncWithImage2", + runtime=None, + handler=None, + codeuri=".", + memory=None, + timeout=None, + environment=None, + rolearn=None, + layers=[], + events=None, + inlinecode=None, + imageuri="image:tag", + imageconfig=None, + packagetype=IMAGE, + metadata={ + "DockerTag": "tag", + "DockerContext": os.path.join("image"), + "Dockerfile": "Dockerfile", + }, + codesign_config_arn=None, + stack_path="", + ), + ), + ("SamFuncWithImage3", None), # imageuri is ecr location, ignored ( "SamFuncWithFunctionNameOverride-x", Function( @@ -295,33 +362,37 @@ def setUp(self): ), ("LambdaFunc1", None), # codeuri is a s3 location, ignored ( - "LambdaFuncWithInlineCode", + "LambdaFuncWithImage1", Function( - name="LambdaFuncWithInlineCode", - functionname="LambdaFuncWithInlineCode", - runtime="nodejs4.3", - handler="index.handler", - codeuri=None, + name="LambdaFuncWithImage1", + functionname="LambdaFuncWithImage1", + runtime=None, + handler=None, + codeuri=".", memory=None, timeout=None, environment=None, rolearn=None, layers=[], events=None, - metadata=None, - inlinecode="testcode", - codesign_config_arn=None, + metadata={ + "DockerTag": "tag", + "DockerContext": os.path.join("image"), + "Dockerfile": "Dockerfile", + }, + inlinecode=None, imageuri=None, imageconfig=None, - packagetype=ZIP, + packagetype=IMAGE, + codesign_config_arn=None, stack_path="", ), ), ( - "LambdaFunc2", + "LambdaFuncWithImage2", Function( - name="LambdaFunc2", - functionname="LambdaFunc2", + name="LambdaFuncWithImage2", + functionname="LambdaFuncWithImage2", runtime=None, handler=None, codeuri=".", @@ -331,15 +402,43 @@ def setUp(self): rolearn=None, layers=[], events=None, - metadata=None, + metadata={ + "DockerTag": "tag", + "DockerContext": os.path.join("image"), + "Dockerfile": "Dockerfile", + }, inlinecode=None, - imageuri="123456789012.dkr.ecr.us-east-1.amazonaws.com/myrepo", + imageuri="image:tag", imageconfig=None, packagetype=IMAGE, codesign_config_arn=None, stack_path="", ), ), + ("LambdaFuncWithImage3", None), # imageuri is a ecr location, ignored + ( + "LambdaFuncWithInlineCode", + Function( + name="LambdaFuncWithInlineCode", + functionname="LambdaFuncWithInlineCode", + runtime="nodejs4.3", + handler="index.handler", + codeuri=None, + memory=None, + timeout=None, + environment=None, + rolearn=None, + layers=[], + events=None, + metadata=None, + inlinecode="testcode", + codesign_config_arn=None, + imageuri=None, + imageconfig=None, + packagetype=ZIP, + stack_path="", + ), + ), ( "LambdaFuncWithLocalPath", Function( @@ -494,11 +593,13 @@ def test_get_all_must_return_all_functions(self): result = {posixpath.join(f.stack_path, f.name) for f in self.provider.get_all()} expected = { "SamFunctions", + "SamFuncWithImage1", + "SamFuncWithImage2", "SamFuncWithInlineCode", - "SamFunc4", "SamFuncWithFunctionNameOverride", + "LambdaFuncWithImage1", + "LambdaFuncWithImage2", "LambdaFuncWithInlineCode", - "LambdaFunc2", "LambdaFuncWithLocalPath", "LambdaFuncWithFunctionNameOverride", "LambdaFuncWithCodeSignConfig", diff --git a/tests/unit/lib/build_module/test_build_strategy.py b/tests/unit/lib/build_module/test_build_strategy.py index 7e9902a172..1fae5b7962 100644 --- a/tests/unit/lib/build_module/test_build_strategy.py +++ b/tests/unit/lib/build_module/test_build_strategy.py @@ -1,3 +1,4 @@ +from copy import deepcopy from unittest import TestCase from unittest.mock import Mock, patch, MagicMock, call, ANY @@ -218,11 +219,15 @@ def test_build_single_function_definition_image_functions_with_same_metadata(sel function2.name = "Function2" function2.full_path = "Function2" function2.packagetype = IMAGE - build_definition = FunctionBuildDefinition("3.7", "codeuri", IMAGE, {}) + build_definition = FunctionBuildDefinition("3.7", "codeuri", IMAGE, {}, env_vars={"FOO": "BAR"}) # since they have the same metadata, they are put into the same build_definition. build_definition.functions = [function1, function2] - result = default_build_strategy.build_single_function_definition(build_definition) + with patch("samcli.lib.build.build_strategy.deepcopy", wraps=deepcopy) as patched_deepcopy: + result = default_build_strategy.build_single_function_definition(build_definition) + + patched_deepcopy.assert_called_with(build_definition.env_vars) + # both of the function name should show up in results self.assertEqual(result, {"Function": built_image, "Function2": built_image}) diff --git a/tests/unit/local/apigw/test_local_apigw_service.py b/tests/unit/local/apigw/test_local_apigw_service.py index dc785936a5..a6ab380f7d 100644 --- a/tests/unit/local/apigw/test_local_apigw_service.py +++ b/tests/unit/local/apigw/test_local_apigw_service.py @@ -226,8 +226,11 @@ def test_request_handler_returns_process_stdout_when_making_response(self, lambd make_response_mock = Mock() request_mock.return_value = ("test", "test") self.api_service.service_response = make_response_mock + current_route = Mock() self.api_service._get_current_route = MagicMock() - self.api_service._get_current_route.methods = [] + self.api_service._get_current_route.return_value = current_route + current_route.methods = [] + current_route.event_type = Route.API self.api_service._construct_v_1_0_event = Mock() @@ -249,7 +252,7 @@ def test_request_handler_returns_process_stdout_when_making_response(self, lambd lambda_output_parser_mock.get_lambda_output.assert_called_with(ANY) # Make sure the parse method is called only on the returned response and not on the raw data from stdout - parse_output_mock.assert_called_with(lambda_response, ANY, ANY) + parse_output_mock.assert_called_with(lambda_response, ANY, ANY, Route.API) # Make sure the logs are written to stderr self.stderr.write.assert_called_with(lambda_logs) @@ -507,69 +510,105 @@ def test_merge_does_not_duplicate_values(self): class TestServiceParsingV1PayloadFormatLambdaOutput(TestCase): - def test_default_content_type_header_added_with_no_headers(self): + @parameterized.expand( + [ + param(Route.API), + param(Route.HTTP), + ] + ) + def test_default_content_type_header_added_with_no_headers(self, event_type): lambda_output = ( '{"statusCode": 200, "body": "{\\"message\\":\\"Hello from Lambda\\"}", ' '"isBase64Encoded": false}' ) (_, headers, _) = LocalApigwService._parse_v1_payload_format_lambda_output( - lambda_output, binary_types=[], flask_request=Mock() + lambda_output, binary_types=[], flask_request=Mock(), event_type=event_type ) self.assertIn("Content-Type", headers) self.assertEqual(headers["Content-Type"], "application/json") - def test_default_content_type_header_added_with_empty_headers(self): + @parameterized.expand( + [ + param(Route.API), + param(Route.HTTP), + ] + ) + def test_default_content_type_header_added_with_empty_headers(self, event_type): lambda_output = ( '{"statusCode": 200, "headers":{}, "body": "{\\"message\\":\\"Hello from Lambda\\"}", ' '"isBase64Encoded": false}' ) (_, headers, _) = LocalApigwService._parse_v1_payload_format_lambda_output( - lambda_output, binary_types=[], flask_request=Mock() + lambda_output, binary_types=[], flask_request=Mock(), event_type=event_type ) self.assertIn("Content-Type", headers) self.assertEqual(headers["Content-Type"], "application/json") - def test_custom_content_type_header_is_not_modified(self): + @parameterized.expand( + [ + param(Route.API), + param(Route.HTTP), + ] + ) + def test_custom_content_type_header_is_not_modified(self, event_type): lambda_output = ( '{"statusCode": 200, "headers":{"Content-Type": "text/xml"}, "body": "{}", ' '"isBase64Encoded": false}' ) (_, headers, _) = LocalApigwService._parse_v1_payload_format_lambda_output( - lambda_output, binary_types=[], flask_request=Mock() + lambda_output, binary_types=[], flask_request=Mock(), event_type=event_type ) self.assertIn("Content-Type", headers) self.assertEqual(headers["Content-Type"], "text/xml") - def test_custom_content_type_multivalue_header_is_not_modified(self): + @parameterized.expand( + [ + param(Route.API), + param(Route.HTTP), + ] + ) + def test_custom_content_type_multivalue_header_is_not_modified(self, event_type): lambda_output = ( '{"statusCode": 200, "multiValueHeaders":{"Content-Type": ["text/xml"]}, "body": "{}", ' '"isBase64Encoded": false}' ) (_, headers, _) = LocalApigwService._parse_v1_payload_format_lambda_output( - lambda_output, binary_types=[], flask_request=Mock() + lambda_output, binary_types=[], flask_request=Mock(), event_type=event_type ) self.assertIn("Content-Type", headers) self.assertEqual(headers["Content-Type"], "text/xml") - def test_multivalue_headers(self): + @parameterized.expand( + [ + param(Route.API), + param(Route.HTTP), + ] + ) + def test_multivalue_headers(self, event_type): lambda_output = ( '{"statusCode": 200, "multiValueHeaders":{"X-Foo": ["bar", "42"]}, ' '"body": "{\\"message\\":\\"Hello from Lambda\\"}", "isBase64Encoded": false}' ) (_, headers, _) = LocalApigwService._parse_v1_payload_format_lambda_output( - lambda_output, binary_types=[], flask_request=Mock() + lambda_output, binary_types=[], flask_request=Mock(), event_type=event_type ) self.assertEqual(headers, Headers({"Content-Type": "application/json", "X-Foo": ["bar", "42"]})) - def test_single_and_multivalue_headers(self): + @parameterized.expand( + [ + param(Route.API), + param(Route.HTTP), + ] + ) + def test_single_and_multivalue_headers(self, event_type): lambda_output = ( '{"statusCode": 200, "headers":{"X-Foo": "foo", "X-Bar": "bar"}, ' '"multiValueHeaders":{"X-Foo": ["bar", "42"]}, ' @@ -577,7 +616,7 @@ def test_single_and_multivalue_headers(self): ) (_, headers, _) = LocalApigwService._parse_v1_payload_format_lambda_output( - lambda_output, binary_types=[], flask_request=Mock() + lambda_output, binary_types=[], flask_request=Mock(), event_type=event_type ) self.assertEqual( @@ -587,29 +626,54 @@ def test_single_and_multivalue_headers(self): def test_extra_values_raise(self): lambda_output = ( '{"statusCode": 200, "headers": {}, "body": "{\\"message\\":\\"Hello from Lambda\\"}", ' - '"isBase64Encoded": false, "another_key": "some value"}' + '"isBase64Encoded": false, "base64Encoded": false, "another_key": "some value"}' ) with self.assertRaises(LambdaResponseParseException): LocalApigwService._parse_v1_payload_format_lambda_output( - lambda_output, binary_types=[], flask_request=Mock() + lambda_output, binary_types=[], flask_request=Mock(), event_type=Route.API ) - def test_parse_returns_correct_tuple(self): + def test_extra_values_skipped_http_api(self): + lambda_output = ( + '{"statusCode": 200, "headers": {}, "body": "{\\"message\\":\\"Hello from Lambda\\"}", ' + '"isBase64Encoded": false, "another_key": "some value"}' + ) + + (status_code, headers, body) = LocalApigwService._parse_v1_payload_format_lambda_output( + lambda_output, binary_types=[], flask_request=Mock(), event_type=Route.HTTP + ) + self.assertEqual(status_code, 200) + self.assertEqual(headers, Headers({"Content-Type": "application/json"})) + self.assertEqual(body, '{"message":"Hello from Lambda"}') + + @parameterized.expand( + [ + param(Route.API), + param(Route.HTTP), + ] + ) + def test_parse_returns_correct_tuple(self, event_type): lambda_output = ( '{"statusCode": 200, "headers": {}, "body": "{\\"message\\":\\"Hello from Lambda\\"}", ' '"isBase64Encoded": false}' ) (status_code, headers, body) = LocalApigwService._parse_v1_payload_format_lambda_output( - lambda_output, binary_types=[], flask_request=Mock() + lambda_output, binary_types=[], flask_request=Mock(), event_type=event_type ) self.assertEqual(status_code, 200) self.assertEqual(headers, Headers({"Content-Type": "application/json"})) self.assertEqual(body, '{"message":"Hello from Lambda"}') - def test_parse_raises_when_invalid_mimetype(self): + @parameterized.expand( + [ + param(Route.API), + param(Route.HTTP), + ] + ) + def test_parse_raises_when_invalid_mimetype(self, event_type): lambda_output = ( '{"statusCode": 200, "headers": {\\"Content-Type\\": \\"text\\"}, "body": "{\\"message\\":\\"Hello from Lambda\\"}", ' '"isBase64Encoded": false}' @@ -617,11 +681,92 @@ def test_parse_raises_when_invalid_mimetype(self): with self.assertRaises(LambdaResponseParseException): LocalApigwService._parse_v1_payload_format_lambda_output( - lambda_output, binary_types=[], flask_request=Mock() + lambda_output, binary_types=[], flask_request=Mock(), event_type=event_type ) + @parameterized.expand( + [ + param("isBase64Encoded", True, True), + param("base64Encoded", True, True), + param("isBase64Encoded", False, False), + param("base64Encoded", False, False), + param("isBase64Encoded", "True", True), + param("base64Encoded", "True", True), + param("isBase64Encoded", "true", True), + param("base64Encoded", "true", True), + param("isBase64Encoded", "False", False), + param("base64Encoded", "False", False), + param("isBase64Encoded", "false", False), + param("base64Encoded", "false", False), + ] + ) @patch("samcli.local.apigw.local_apigw_service.LocalApigwService._should_base64_decode_body") - def test_parse_returns_decodes_base64_to_binary(self, should_decode_body_patch): + def test_parse_returns_decodes_base64_to_binary_for_rest_api( + self, encoded_field_name, encoded_response_value, encoded_parsed_value, should_decode_body_patch + ): + should_decode_body_patch.return_value = True + + binary_body = b"011000100110100101101110011000010111001001111001" # binary in binary + base64_body = base64.b64encode(binary_body).decode("utf-8") + lambda_output = { + "statusCode": 200, + "headers": {"Content-Type": "application/octet-stream"}, + "body": base64_body, + encoded_field_name: encoded_response_value, + } + + flask_request_mock = Mock() + (status_code, headers, body) = LocalApigwService._parse_v1_payload_format_lambda_output( + json.dumps(lambda_output), binary_types=["*/*"], flask_request=flask_request_mock, event_type=Route.API + ) + + should_decode_body_patch.assert_called_with( + ["*/*"], flask_request_mock, Headers({"Content-Type": "application/octet-stream"}), encoded_parsed_value + ) + + self.assertEqual(status_code, 200) + self.assertEqual(headers, Headers({"Content-Type": "application/octet-stream"})) + self.assertEqual(body, binary_body) + + @parameterized.expand( + [ + param("isBase64Encoded", 0), + param("base64Encoded", 0), + param("isBase64Encoded", 1), + param("base64Encoded", 1), + param("isBase64Encoded", -1), + param("base64Encoded", -1), + param("isBase64Encoded", 10), + param("base64Encoded", 10), + param("isBase64Encoded", "TRue"), + param("base64Encoded", "TRue"), + param("isBase64Encoded", "Any Value"), + param("base64Encoded", "Any Value"), + ] + ) + @patch("samcli.local.apigw.local_apigw_service.LocalApigwService._should_base64_decode_body") + def test_parse_raise_exception_invalide_base64_encoded( + self, encoded_field_name, encoded_response_value, should_decode_body_patch + ): + should_decode_body_patch.return_value = True + + binary_body = b"011000100110100101101110011000010111001001111001" # binary in binary + base64_body = base64.b64encode(binary_body).decode("utf-8") + lambda_output = { + "statusCode": 200, + "headers": {"Content-Type": "application/octet-stream"}, + "body": base64_body, + encoded_field_name: encoded_response_value, + } + + flask_request_mock = Mock() + with self.assertRaises(LambdaResponseParseException): + LocalApigwService._parse_v1_payload_format_lambda_output( + json.dumps(lambda_output), binary_types=["*/*"], flask_request=flask_request_mock, event_type=Route.API + ) + + @patch("samcli.local.apigw.local_apigw_service.LocalApigwService._should_base64_decode_body") + def test_parse_base64Encoded_field_is_priority(self, should_decode_body_patch): should_decode_body_patch.return_value = True binary_body = b"011000100110100101101110011000010111001001111001" # binary in binary @@ -631,17 +776,136 @@ def test_parse_returns_decodes_base64_to_binary(self, should_decode_body_patch): "headers": {"Content-Type": "application/octet-stream"}, "body": base64_body, "isBase64Encoded": False, + "base64Encoded": True, } + flask_request_mock = Mock() (status_code, headers, body) = LocalApigwService._parse_v1_payload_format_lambda_output( - json.dumps(lambda_output), binary_types=["*/*"], flask_request=Mock() + json.dumps(lambda_output), binary_types=["*/*"], flask_request=flask_request_mock, event_type=Route.API + ) + + should_decode_body_patch.assert_called_with( + ["*/*"], flask_request_mock, Headers({"Content-Type": "application/octet-stream"}), True ) self.assertEqual(status_code, 200) self.assertEqual(headers, Headers({"Content-Type": "application/octet-stream"})) self.assertEqual(body, binary_body) - def test_status_code_not_int(self): + @parameterized.expand( + [ + param(True, True), + param(False, False), + param("True", True), + param("true", True), + param("False", False), + param("false", False), + ] + ) + def test_parse_returns_decodes_base64_to_binary_for_http_api(self, encoded_response_value, encoded_parsed_value): + binary_body = b"011000100110100101101110011000010111001001111001" # binary in binary + base64_body = base64.b64encode(binary_body).decode("utf-8") + lambda_output = { + "statusCode": 200, + "headers": {"Content-Type": "application/octet-stream"}, + "body": base64_body, + "isBase64Encoded": encoded_response_value, + } + + (status_code, headers, body) = LocalApigwService._parse_v1_payload_format_lambda_output( + json.dumps(lambda_output), binary_types=["*/*"], flask_request=Mock(), event_type=Route.HTTP + ) + + self.assertEqual(status_code, 200) + self.assertEqual(headers, Headers({"Content-Type": "application/octet-stream"})) + self.assertEqual(body, binary_body if encoded_parsed_value else base64_body) + + @parameterized.expand( + [ + param(0), + param(1), + param(-1), + param(10), + param("TRue"), + param("Any Value"), + ] + ) + def test_parse_raise_exception_invalide_base64_encoded_for_http_api(self, encoded_response_value): + + binary_body = b"011000100110100101101110011000010111001001111001" # binary in binary + base64_body = base64.b64encode(binary_body).decode("utf-8") + lambda_output = { + "statusCode": 200, + "headers": {"Content-Type": "application/octet-stream"}, + "body": base64_body, + "isBase64Encoded": encoded_response_value, + } + + flask_request_mock = Mock() + with self.assertRaises(LambdaResponseParseException): + LocalApigwService._parse_v1_payload_format_lambda_output( + json.dumps(lambda_output), binary_types=["*/*"], flask_request=flask_request_mock, event_type=Route.API + ) + + @parameterized.expand( + [ + param(True), + param(False), + param("True"), + param("true"), + param("False"), + param("false"), + param(0), + param(1), + param(-1), + param(10), + param("TRue"), + param("Any Value"), + ] + ) + def test_parse_skip_base_64_encoded_field_http_api(self, encoded_response_value): + binary_body = b"011000100110100101101110011000010111001001111001" # binary in binary + base64_body = base64.b64encode(binary_body).decode("utf-8") + lambda_output = { + "statusCode": 200, + "headers": {"Content-Type": "application/octet-stream"}, + "body": base64_body, + "base64Encoded": encoded_response_value, + } + + (status_code, headers, body) = LocalApigwService._parse_v1_payload_format_lambda_output( + json.dumps(lambda_output), binary_types=["*/*"], flask_request=Mock(), event_type=Route.HTTP + ) + + self.assertEqual(status_code, 200) + self.assertEqual(headers, Headers({"Content-Type": "application/octet-stream"})) + self.assertEqual(body, base64_body) + + def test_parse_returns_does_not_decodes_base64_to_binary_for_http_api(self): + binary_body = b"011000100110100101101110011000010111001001111001" # binary in binary + base64_body = base64.b64encode(binary_body).decode("utf-8") + lambda_output = { + "statusCode": 200, + "headers": {"Content-Type": "application/octet-stream"}, + "body": base64_body, + "isBase64Encoded": False, + } + + (status_code, headers, body) = LocalApigwService._parse_v1_payload_format_lambda_output( + json.dumps(lambda_output), binary_types=["*/*"], flask_request=Mock(), event_type=Route.HTTP + ) + + self.assertEqual(status_code, 200) + self.assertEqual(headers, Headers({"Content-Type": "application/octet-stream"})) + self.assertEqual(body, base64_body) + + @parameterized.expand( + [ + param(Route.API), + param(Route.HTTP), + ] + ) + def test_status_code_not_int(self, event_type): lambda_output = ( '{"statusCode": "str", "headers": {}, "body": "{\\"message\\":\\"Hello from Lambda\\"}", ' '"isBase64Encoded": false}' @@ -649,21 +913,33 @@ def test_status_code_not_int(self): with self.assertRaises(LambdaResponseParseException): LocalApigwService._parse_v1_payload_format_lambda_output( - lambda_output, binary_types=[], flask_request=Mock() + lambda_output, binary_types=[], flask_request=Mock(), event_type=event_type ) - def test_status_code_int_str(self): + @parameterized.expand( + [ + param(Route.API), + param(Route.HTTP), + ] + ) + def test_status_code_int_str(self, event_type): lambda_output = ( '{"statusCode": "200", "headers": {}, "body": "{\\"message\\":\\"Hello from Lambda\\"}", ' '"isBase64Encoded": false}' ) (status_code, _, _) = LocalApigwService._parse_v1_payload_format_lambda_output( - lambda_output, binary_types=[], flask_request=Mock() + lambda_output, binary_types=[], flask_request=Mock(), event_type=event_type ) self.assertEqual(status_code, 200) - def test_status_code_negative_int(self): + @parameterized.expand( + [ + param(Route.API), + param(Route.HTTP), + ] + ) + def test_status_code_negative_int(self, event_type): lambda_output = ( '{"statusCode": -1, "headers": {}, "body": "{\\"message\\":\\"Hello from Lambda\\"}", ' '"isBase64Encoded": false}' @@ -671,10 +947,39 @@ def test_status_code_negative_int(self): with self.assertRaises(LambdaResponseParseException): LocalApigwService._parse_v1_payload_format_lambda_output( - lambda_output, binary_types=[], flask_request=Mock() + lambda_output, binary_types=[], flask_request=Mock(), event_type=event_type ) - def test_status_code_negative_int_str(self): + def test_status_code_is_none_http_api(self): + lambda_output = ( + '{"headers": {}, "body": "{\\"message\\":\\"Hello from Lambda\\"}", ' '"isBase64Encoded": false}' + ) + + with self.assertRaises(LambdaResponseParseException): + LocalApigwService._parse_v1_payload_format_lambda_output( + lambda_output, binary_types=[], flask_request=Mock(), event_type=Route.HTTP + ) + + def test_status_code_is_none_rest_api(self): + lambda_output = ( + '{"headers": {}, "body": "{\\"message\\":\\"Hello from Lambda\\"}", ' '"isBase64Encoded": false}' + ) + + (status_code, headers, body) = LocalApigwService._parse_v1_payload_format_lambda_output( + lambda_output, binary_types=[], flask_request=Mock(), event_type=Route.API + ) + + self.assertEqual(status_code, 200) + self.assertEqual(headers, Headers({"Content-Type": "application/json"})) + self.assertEqual(body, '{"message":"Hello from Lambda"}') + + @parameterized.expand( + [ + param(Route.API), + param(Route.HTTP), + ] + ) + def test_status_code_negative_int_str(self, event_type): lambda_output = ( '{"statusCode": "-1", "headers": {}, "body": "{\\"message\\":\\"Hello from Lambda\\"}", ' '"isBase64Encoded": false}' @@ -682,44 +987,68 @@ def test_status_code_negative_int_str(self): with self.assertRaises(LambdaResponseParseException): LocalApigwService._parse_v1_payload_format_lambda_output( - lambda_output, binary_types=[], flask_request=Mock() + lambda_output, binary_types=[], flask_request=Mock(), event_type=event_type ) - def test_lambda_output_list_not_dict(self): + @parameterized.expand( + [ + param(Route.API), + param(Route.HTTP), + ] + ) + def test_lambda_output_list_not_dict(self, event_type): lambda_output = "[]" with self.assertRaises(LambdaResponseParseException): LocalApigwService._parse_v1_payload_format_lambda_output( - lambda_output, binary_types=[], flask_request=Mock() + lambda_output, binary_types=[], flask_request=Mock(), event_type=event_type ) - def test_lambda_output_not_json_serializable(self): + @parameterized.expand( + [ + param(Route.API), + param(Route.HTTP), + ] + ) + def test_lambda_output_not_json_serializable(self, event_type): lambda_output = "some str" with self.assertRaises(LambdaResponseParseException): LocalApigwService._parse_v1_payload_format_lambda_output( - lambda_output, binary_types=[], flask_request=Mock() + lambda_output, binary_types=[], flask_request=Mock(), event_type=event_type ) - def test_properties_are_null(self): + @parameterized.expand( + [ + param(Route.API), + param(Route.HTTP), + ] + ) + def test_properties_are_null(self, event_type): lambda_output = '{"statusCode": 0, "headers": null, "body": null, ' '"isBase64Encoded": null}' (status_code, headers, body) = LocalApigwService._parse_v1_payload_format_lambda_output( - lambda_output, binary_types=[], flask_request=Mock() + lambda_output, binary_types=[], flask_request=Mock(), event_type=event_type ) self.assertEqual(status_code, 200) self.assertEqual(headers, Headers({"Content-Type": "application/json"})) self.assertEqual(body, None) - def test_cookies_is_not_raise(self): + @parameterized.expand( + [ + param(Route.API), + param(Route.HTTP), + ] + ) + def test_cookies_is_not_raise(self, event_type): lambda_output = ( '{"statusCode": 200, "headers":{}, "body": "{\\"message\\":\\"Hello from Lambda\\"}", ' '"isBase64Encoded": false, "cookies":{}}' ) (_, headers, _) = LocalApigwService._parse_v1_payload_format_lambda_output( - lambda_output, binary_types=[], flask_request=Mock() + lambda_output, binary_types=[], flask_request=Mock(), event_type=event_type ) @@ -761,16 +1090,19 @@ def test_custom_content_type_header_is_not_modified(self): self.assertIn("Content-Type", headers) self.assertEqual(headers["Content-Type"], "text/xml") - def test_extra_values_raise(self): + def test_extra_values_skipped(self): lambda_output = ( '{"statusCode": 200, "headers": {}, "body": "{\\"message\\":\\"Hello from Lambda\\"}", ' '"isBase64Encoded": false, "another_key": "some value"}' ) - with self.assertRaises(LambdaResponseParseException): - LocalApigwService._parse_v2_payload_format_lambda_output( - lambda_output, binary_types=[], flask_request=Mock() - ) + (status_code, headers, body) = LocalApigwService._parse_v2_payload_format_lambda_output( + lambda_output, binary_types=[], flask_request=Mock() + ) + + self.assertEqual(status_code, 200) + self.assertEqual(headers, Headers({"Content-Type": "application/json"})) + self.assertEqual(body, '{"message":"Hello from Lambda"}') def test_parse_returns_correct_tuple(self): lambda_output = ( @@ -797,10 +1129,7 @@ def test_parse_raises_when_invalid_mimetype(self): lambda_output, binary_types=[], flask_request=Mock() ) - @patch("samcli.local.apigw.local_apigw_service.LocalApigwService._should_base64_decode_body") - def test_parse_returns_decodes_base64_to_binary(self, should_decode_body_patch): - should_decode_body_patch.return_value = True - + def test_parse_returns_does_not_decodes_base64_to_binary(self): binary_body = b"011000100110100101101110011000010111001001111001" # binary in binary base64_body = base64.b64encode(binary_body).decode("utf-8") lambda_output = { @@ -814,6 +1143,24 @@ def test_parse_returns_decodes_base64_to_binary(self, should_decode_body_patch): json.dumps(lambda_output), binary_types=["*/*"], flask_request=Mock() ) + self.assertEqual(status_code, 200) + self.assertEqual(headers, Headers({"Content-Type": "application/octet-stream"})) + self.assertEqual(body, base64_body) + + def test_parse_returns_decodes_base64_to_binary(self): + binary_body = b"011000100110100101101110011000010111001001111001" # binary in binary + base64_body = base64.b64encode(binary_body).decode("utf-8") + lambda_output = { + "statusCode": 200, + "headers": {"Content-Type": "application/octet-stream"}, + "body": base64_body, + "isBase64Encoded": True, + } + + (status_code, headers, body) = LocalApigwService._parse_v2_payload_format_lambda_output( + json.dumps(lambda_output), binary_types=["*/*"], flask_request=Mock() + ) + self.assertEqual(status_code, 200) self.assertEqual(headers, Headers({"Content-Type": "application/octet-stream"})) self.assertEqual(body, binary_body)