diff --git a/README.md b/README.md index 0c3df5d0d3..98b191cfc1 100644 --- a/README.md +++ b/README.md @@ -92,4 +92,4 @@ Read the [SAM Documentation Contribution Guide](https://github.com/awsdocs/aws-s started. ### Join the SAM Community on Slack -[Join the SAM developers channel (#samdev)](https://join.slack.com/t/awsdevelopers/shared_invite/zt-idww18e8-Z1kXhI7GNuDewkweCF3YjA) on Slack to collaborate with fellow community members and the AWS SAM team. +[Join the SAM developers channel (#samdev)](https://join.slack.com/t/awsdevelopers/shared_invite/zt-yryddays-C9fkWrmguDv0h2EEDzCqvw) on Slack to collaborate with fellow community members and the AWS SAM team. diff --git a/appveyor-ubuntu.yml b/appveyor-ubuntu.yml index c834ceb6ee..819bfb8f52 100644 --- a/appveyor-ubuntu.yml +++ b/appveyor-ubuntu.yml @@ -54,7 +54,7 @@ install: # AppVeyor's apt-get cache might be outdated, and the package could potentially be 404. - sh: "sudo apt-get update" - - sh: "gvm use go1.13" + - sh: "gvm use go1.15" - sh: "echo $PATH" - sh: "ls /usr/" - sh: "JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64" diff --git a/appveyor.yml b/appveyor.yml index 7400229f05..16de910e42 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -123,7 +123,7 @@ for: # AppVeyor's apt-get cache might be outdated, and the package could potentially be 404. - sh: "sudo apt-get update" - - sh: "gvm use go1.13" + - sh: "gvm use go1.15" - sh: "echo $PATH" - sh: "ls /usr/" - sh: "JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64" diff --git a/samcli/__init__.py b/samcli/__init__.py index 65e71d76b4..ec115dc234 100644 --- a/samcli/__init__.py +++ b/samcli/__init__.py @@ -2,4 +2,4 @@ SAM CLI version """ -__version__ = "1.56.0" +__version__ = "1.56.1" diff --git a/samcli/lib/samlib/wrapper.py b/samcli/lib/samlib/wrapper.py index 08a52a7523..e215d77687 100644 --- a/samcli/lib/samlib/wrapper.py +++ b/samcli/lib/samlib/wrapper.py @@ -20,7 +20,9 @@ InvalidResourceException, InvalidEventException, ) +from samtranslator.model.types import is_str from samtranslator.plugins import LifeCycleEvents +from samtranslator.sdk.resource import SamResource, SamResourceType from samtranslator.translator.translator import prepare_plugins from samtranslator.validator.validator import SamTemplateValidator @@ -64,6 +66,9 @@ def run_plugins(self, convert_local_uris=True): additional_plugins, parameters=self.parameter_values if self.parameter_values else {} ) + # Temporarily disabling validation for DeletionPolicy and UpdateReplacePolicy when language extensions are set + self._patch_language_extensions() + try: parser.parse(template_copy, all_plugins) # parse() will run all configured plugins except InvalidDocumentException as e: @@ -77,6 +82,43 @@ def run_plugins(self, convert_local_uris=True): def template(self): return copy.deepcopy(self._sam_template) + def _patch_language_extensions(self) -> None: + """ + Monkey patch SamResource.valid function to exclude checking DeletionPolicy + and UpdateReplacePolicy when language extensions are set + """ + template_copy = self.template + if self._check_using_language_extension(template_copy): + + def patched_func(self): + if self.condition: + if not is_str()(self.condition, should_raise=False): + raise InvalidDocumentException( + [InvalidTemplateException("Every Condition member must be a string.")] + ) + return SamResourceType.has_value(self.type) + + SamResource.valid = patched_func + + @staticmethod + def _check_using_language_extension(template: Dict) -> bool: + """ + Check if language extensions are set in the template's Transform + :param template: template to check + :return: True if language extensions are set in the template, False otherwise + """ + transform = template.get("Transform") + if transform: + if isinstance(transform, str) and transform.startswith("AWS::LanguageExtensions"): + return True + if isinstance(transform, list): + for transform_instance in transform: + if not isinstance(transform_instance, str): + continue + if transform_instance.startswith("AWS::LanguageExtensions"): + return True + return False + class _SamParserReimplemented: """ diff --git a/samcli/lib/sync/watch_manager.py b/samcli/lib/sync/watch_manager.py index b75a18137f..0171c4bbc2 100644 --- a/samcli/lib/sync/watch_manager.py +++ b/samcli/lib/sync/watch_manager.py @@ -182,7 +182,7 @@ def _start(self) -> None: time.sleep(1) def _execute_infra_sync(self) -> None: - LOG.info(self._color.cyan("Queued infra sync. Wating for in progress code syncs to complete...")) + LOG.info(self._color.cyan("Queued infra sync. Waiting for in progress code syncs to complete...")) self._waiting_infra_sync = False self._stop_code_sync() try: diff --git a/samcli/lib/telemetry/metric.py b/samcli/lib/telemetry/metric.py index 470882f41b..efa2c427e6 100644 --- a/samcli/lib/telemetry/metric.py +++ b/samcli/lib/telemetry/metric.py @@ -162,10 +162,12 @@ def wrapped(*args, **kwargs): metric.add_data("debugFlagProvided", bool(ctx.debug)) metric.add_data("region", ctx.region or "") metric.add_data("commandName", ctx.command_path) # Full command path. ex: sam local start-api - # Project metadata metrics - metric_specific_attributes["gitOrigin"] = get_git_remote_origin_url() - metric_specific_attributes["projectName"] = get_project_name() - metric_specific_attributes["initialCommit"] = get_initial_commit_hash() + if not ctx.command_path.endswith("init") or ctx.command_path.endswith("pipeline init"): + # Project metadata + # We don't capture below usage attributes for sam init as the command is not run inside a project + metric_specific_attributes["gitOrigin"] = get_git_remote_origin_url() + metric_specific_attributes["projectName"] = get_project_name() + metric_specific_attributes["initialCommit"] = get_initial_commit_hash() metric.add_data("metricSpecificAttributes", metric_specific_attributes) # Metric about command's execution characteristics metric.add_data("duration", duration_fn()) diff --git a/samcli/lib/telemetry/project_metadata.py b/samcli/lib/telemetry/project_metadata.py index c825db9022..9d266e0206 100644 --- a/samcli/lib/telemetry/project_metadata.py +++ b/samcli/lib/telemetry/project_metadata.py @@ -1,25 +1,27 @@ """ -Creates and encrypts metadata regarding SAM CLI projects. +Creates and hashes metadata regarding SAM CLI projects. """ import hashlib -from os import getcwd import re import subprocess -from typing import List, Optional +from os import getcwd +from os.path import basename +from typing import Optional +from urllib.parse import urlparse from samcli.cli.global_config import GlobalConfig def get_git_remote_origin_url() -> Optional[str]: """ - Retrieve an encrypted version of the project's git remote origin url, if it exists. + Retrieve an hashed version of the project's git remote origin url, if it exists. Returns ------- str | None A SHA256 hexdigest string of the git remote origin url, formatted such that the - encrypted value follows the pattern //.git. + hashed value follows the pattern //.git. If telemetry is opted out of by the user, or the `.git` folder is not found (the directory is not a git repository), returns None """ @@ -31,17 +33,17 @@ def get_git_remote_origin_url() -> Optional[str]: runcmd = subprocess.run( ["git", "config", "--get", "remote.origin.url"], capture_output=True, shell=True, check=True, text=True ) - metadata = _parse_remote_origin_url(str(runcmd.stdout)) - git_url = "/".join(metadata) + ".git" # Format to //.git + git_url = _parse_remote_origin_url(str(runcmd.stdout)) except subprocess.CalledProcessError: - return None # Not a git repo + # Ignoring, None git_url will be handled later + pass - return _encrypt_value(git_url) + return _hash_value(git_url) if git_url else None def get_project_name() -> Optional[str]: """ - Retrieve an encrypted version of the project's name, as defined by the .git folder (or directory name if no .git). + Retrieve an hashed version of the project's name, as defined by the .git folder (or directory name if no .git). Returns ------- @@ -53,21 +55,27 @@ def get_project_name() -> Optional[str]: if not bool(GlobalConfig().telemetry_enabled): return None - project_name = "" + project_name = None try: runcmd = subprocess.run( ["git", "config", "--get", "remote.origin.url"], capture_output=True, shell=True, check=True, text=True ) - project_name = _parse_remote_origin_url(str(runcmd.stdout))[2] # dir is git repo, get project name from URL + git_url = _parse_remote_origin_url(str(runcmd.stdout)) + if git_url: + project_name = git_url.split("/")[-1] # dir is git repo, get project name from URL except subprocess.CalledProcessError: - project_name = getcwd().replace("\\", "/") # dir is not a git repo, get directory name + # Ignoring, None project_name will be handled at the end before returning + pass + + if not project_name: + project_name = basename(getcwd().replace("\\", "/")) # dir is not a git repo, get directory name - return _encrypt_value(project_name) + return _hash_value(project_name) def get_initial_commit_hash() -> Optional[str]: """ - Retrieve an encrypted version of the project's initial commit hash, if it exists. + Retrieve an hashed version of the project's initial commit hash, if it exists. Returns ------- @@ -88,24 +96,34 @@ def get_initial_commit_hash() -> Optional[str]: except subprocess.CalledProcessError: return None # Not a git repo - return _encrypt_value(metadata) + return _hash_value(metadata) -def _parse_remote_origin_url(url: str) -> List[str]: +def _parse_remote_origin_url(url: str) -> Optional[str]: """ - Parse a `git remote origin url` into its hostname, owner, and project name. + Parse a `git remote origin url` into a formatted "hostname/project" string Returns ------- - List[str] - A list of 3 strings, with indeces corresponding to 0:hostname, 1:owner, 2:project_name + str + formatted project origin url """ - pattern = re.compile(r"(?:https?://|git@)(?P\S*)(?:/|:)(?P\S*)/(?P\S*)\.git") - return [str(item) for item in pattern.findall(url)[0]] + parsed = urlparse(url) + if not parsed.path: + return None + + formatted = (parsed.hostname or "") + parsed.path + formatted = re.sub(r"\n", "", formatted) + formatted = re.sub("/$", "", formatted) + formatted = re.sub(".git$", "", formatted) + formatted = re.sub("^(.+)@", "", formatted) + formatted = formatted.replace(":", "/") + + return formatted -def _encrypt_value(value: str) -> str: - """Encrypt a string, and then return the encrypted value as a byte string.""" +def _hash_value(value: str) -> str: + """Hash a string, and then return the hashed value as a byte string.""" h = hashlib.sha256() h.update(value.encode("utf-8")) return h.hexdigest() diff --git a/samcli/local/apigw/local_apigw_service.py b/samcli/local/apigw/local_apigw_service.py index 0d1fa0e38a..400c91858c 100644 --- a/samcli/local/apigw/local_apigw_service.py +++ b/samcli/local/apigw/local_apigw_service.py @@ -11,6 +11,7 @@ from flask import Flask, request from werkzeug.datastructures import Headers from werkzeug.routing import BaseConverter +from werkzeug.serving import WSGIRequestHandler from samcli.lib.providers.provider import Cors from samcli.local.services.base_local_service import BaseLocalService, LambdaOutputParser @@ -155,6 +156,8 @@ def create(self): """ Creates a Flask Application that can be started. """ + # Setting sam local start-api to respond using HTTP/1.1 instead of the default HTTP/1.0 + WSGIRequestHandler.protocol_version = "HTTP/1.1" self._app = Flask( __name__, diff --git a/tests/integration/buildcmd/test_build_cmd.py b/tests/integration/buildcmd/test_build_cmd.py index 8e43c27ada..c5afc184e8 100644 --- a/tests/integration/buildcmd/test_build_cmd.py +++ b/tests/integration/buildcmd/test_build_cmd.py @@ -2536,3 +2536,19 @@ def test_sar_application_with_location_resolved_from_map(self, use_container, re # will fail the build as there is no mapping self.assertEqual(process_execute.process.returncode, 1) self.assertIn("Property \\'ApplicationId\\' cannot be resolved.", str(process_execute.stderr)) + + +@skipIf( + ((IS_WINDOWS and RUNNING_ON_CI) and not CI_OVERRIDE), + "Skip build tests on windows when running in CI unless overridden", +) +class TestBuildWithLanguageExtensions(BuildIntegBase): + template = "language-extensions.yaml" + + def test_validation_does_not_error_out(self): + cmdlist = self.get_command_list() + LOG.info("Running Command: %s", cmdlist) + LOG.info(self.working_dir) + process_execute = run_command(cmdlist, cwd=self.working_dir) + self.assertEqual(process_execute.process.returncode, 0) + self.assertIn("template.yaml", os.listdir(self.default_build_dir)) diff --git a/tests/integration/deploy/test_deploy_command.py b/tests/integration/deploy/test_deploy_command.py index e35a1baa4f..9c910e89ea 100644 --- a/tests/integration/deploy/test_deploy_command.py +++ b/tests/integration/deploy/test_deploy_command.py @@ -1582,3 +1582,14 @@ def test_update_stack_correct_stack_outputs(self, template): process_stdout = deploy_process_execute.stdout.decode() self.assertNotRegex(process_stdout, r"CREATE_COMPLETE.+HelloWorldFunction") self.assertRegex(process_stdout, r"UPDATE_COMPLETE.+HelloWorldFunction") + + def test_deploy_with_language_extensions(self): + template = Path(__file__).resolve().parents[1].joinpath("testdata", "buildcmd", "language-extensions.yaml") + stack_name = self._method_to_stack_name(self.id()) + self.stacks.append({"name": stack_name}) + + deploy_command_list = self.get_deploy_command_list( + template_file=template, stack_name=stack_name, capabilities="CAPABILITY_IAM" + ) + deploy_process_execute = run_command(deploy_command_list) + self.assertEqual(deploy_process_execute.process.returncode, 0) diff --git a/tests/integration/local/start_api/test_start_api.py b/tests/integration/local/start_api/test_start_api.py index d0e7558deb..36970dc018 100644 --- a/tests/integration/local/start_api/test_start_api.py +++ b/tests/integration/local/start_api/test_start_api.py @@ -6,6 +6,7 @@ from typing import Dict import requests +from http.client import HTTPConnection from concurrent.futures import ThreadPoolExecutor, as_completed from time import time, sleep @@ -18,6 +19,131 @@ from ..invoke.layer_utils import LayerUtils +@parameterized_class( + ("template_path",), + [ + ("/testdata/start_api/template.yaml",), + ("/testdata/start_api/nested-templates/template-parent.yaml",), + ("/testdata/start_api/cdk/template_cdk.yaml",), + ], +) +class TestServiceHTTP10(StartApiIntegBaseClass): + """ + Testing general requirements around the Service that powers `sam local start-api` + """ + + def setUp(self): + self.url = "http://127.0.0.1:{}".format(self.port) + HTTPConnection._http_vsn_str = "HTTP/1.0" + + def test_static_directory(self): + pass + + @pytest.mark.flaky(reruns=3) + @pytest.mark.timeout(timeout=600, method="thread") + def test_calling_proxy_endpoint_http10(self): + response = requests.get(self.url + "/proxypath/this/is/some/path", timeout=300) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), {"hello": "world"}) + self.assertEqual(response.raw.version, 11) # Checks if the response is HTTP/1.1 version + + @pytest.mark.flaky(reruns=3) + @pytest.mark.timeout(timeout=600, method="thread") + def test_get_call_with_path_setup_with_any_implicit_api_http10(self): + """ + Get Request to a path that was defined as ANY in SAM through AWS::Serverless::Function Events + """ + response = requests.get(self.url + "/anyandall", timeout=300) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), {"hello": "world"}) + self.assertEqual(response.raw.version, 11) + + @pytest.mark.flaky(reruns=3) + @pytest.mark.timeout(timeout=600, method="thread") + def test_post_call_with_path_setup_with_any_implicit_api_http10(self): + """ + Post Request to a path that was defined as ANY in SAM through AWS::Serverless::Function Events + """ + response = requests.post(self.url + "/anyandall", json={}, timeout=300) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), {"hello": "world"}) + self.assertEqual(response.raw.version, 11) + + @pytest.mark.flaky(reruns=3) + @pytest.mark.timeout(timeout=600, method="thread") + def test_put_call_with_path_setup_with_any_implicit_api_http10(self): + """ + Put Request to a path that was defined as ANY in SAM through AWS::Serverless::Function Events + """ + response = requests.put(self.url + "/anyandall", json={}, timeout=300) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), {"hello": "world"}) + self.assertEqual(response.raw.version, 11) + + @pytest.mark.flaky(reruns=3) + @pytest.mark.timeout(timeout=600, method="thread") + def test_head_call_with_path_setup_with_any_implicit_api_http10(self): + """ + Head Request to a path that was defined as ANY in SAM through AWS::Serverless::Function Events + """ + response = requests.head(self.url + "/anyandall", timeout=300) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.raw.version, 11) + + @pytest.mark.flaky(reruns=3) + @pytest.mark.timeout(timeout=600, method="thread") + def test_delete_call_with_path_setup_with_any_implicit_api_http10(self): + """ + Delete Request to a path that was defined as ANY in SAM through AWS::Serverless::Function Events + """ + response = requests.delete(self.url + "/anyandall", timeout=300) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), {"hello": "world"}) + self.assertEqual(response.raw.version, 11) + + @pytest.mark.flaky(reruns=3) + @pytest.mark.timeout(timeout=600, method="thread") + def test_options_call_with_path_setup_with_any_implicit_api_http10(self): + """ + Options Request to a path that was defined as ANY in SAM through AWS::Serverless::Function Events + """ + response = requests.options(self.url + "/anyandall", timeout=300) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.raw.version, 11) + + @pytest.mark.flaky(reruns=3) + @pytest.mark.timeout(timeout=600, method="thread") + def test_patch_call_with_path_setup_with_any_implicit_api_http10(self): + """ + Patch Request to a path that was defined as ANY in SAM through AWS::Serverless::Function Events + """ + response = requests.patch(self.url + "/anyandall", timeout=300) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), {"hello": "world"}) + self.assertEqual(response.raw.version, 11) + + @pytest.mark.flaky(reruns=3) + @pytest.mark.timeout(timeout=600, method="thread") + def test_large_input_request_http10(self): + # not exact 6 mega, as local start-api sends extra data with the input data + around_six_mega = 6 * 1024 * 1024 - 2 * 1024 + data = "a" * around_six_mega + response = requests.post(self.url + "/echoeventbody", data=data, timeout=300) + + self.assertEqual(response.status_code, 200) + response_data = response.json() + self.assertEqual(response_data.get("body"), data) + self.assertEqual(response.raw.version, 11) + + @parameterized_class( ("template_path",), [ @@ -32,6 +158,7 @@ class TestParallelRequests(StartApiIntegBaseClass): def setUp(self): self.url = "http://127.0.0.1:{}".format(self.port) + HTTPConnection._http_vsn_str = "HTTP/1.1" @pytest.mark.flaky(reruns=3) @pytest.mark.timeout(timeout=600, method="thread") @@ -57,6 +184,7 @@ def test_same_endpoint(self): for result in results: self.assertEqual(result.status_code, 200) self.assertEqual(result.json(), {"message": "HelloWorld! I just slept and waking up."}) + self.assertEqual(result.raw.version, 11) # Checks if the response is HTTP/1.1 version @pytest.mark.flaky(reruns=3) @pytest.mark.timeout(timeout=600, method="thread") @@ -86,6 +214,7 @@ def test_different_endpoints(self): for result in results: self.assertEqual(result.status_code, 200) self.assertEqual(result.json(), {"message": "HelloWorld! I just slept and waking up."}) + self.assertEqual(result.raw.version, 11) @parameterized_class( @@ -110,6 +239,7 @@ def test_invalid_http_verb_for_endpoint(self): self.assertEqual(response.status_code, 403) self.assertEqual(response.json(), {"message": "Missing Authentication Token"}) + self.assertEqual(response.raw.version, 11) # Checks if the response is HTTP/1.1 version @pytest.mark.flaky(reruns=3) @pytest.mark.timeout(timeout=600, method="thread") @@ -118,6 +248,7 @@ def test_invalid_response_from_lambda(self): self.assertEqual(response.status_code, 502) self.assertEqual(response.json(), {"message": "Internal server error"}) + self.assertEqual(response.raw.version, 11) @pytest.mark.flaky(reruns=3) @pytest.mark.timeout(timeout=600, method="thread") @@ -126,6 +257,7 @@ def test_invalid_json_response_from_lambda(self): self.assertEqual(response.status_code, 502) self.assertEqual(response.json(), {"message": "Internal server error"}) + self.assertEqual(response.raw.version, 11) @pytest.mark.flaky(reruns=3) @pytest.mark.timeout(timeout=600, method="thread") @@ -159,6 +291,7 @@ def test_calling_proxy_endpoint(self): self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), {"hello": "world"}) + self.assertEqual(response.raw.version, 11) # Checks if the response is HTTP/1.1 version @pytest.mark.flaky(reruns=3) @pytest.mark.timeout(timeout=600, method="thread") @@ -170,6 +303,7 @@ def test_get_call_with_path_setup_with_any_implicit_api(self): self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), {"hello": "world"}) + self.assertEqual(response.raw.version, 11) @pytest.mark.flaky(reruns=3) @pytest.mark.timeout(timeout=600, method="thread") @@ -181,6 +315,7 @@ def test_post_call_with_path_setup_with_any_implicit_api(self): self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), {"hello": "world"}) + self.assertEqual(response.raw.version, 11) @pytest.mark.flaky(reruns=3) @pytest.mark.timeout(timeout=600, method="thread") @@ -192,6 +327,7 @@ def test_put_call_with_path_setup_with_any_implicit_api(self): self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), {"hello": "world"}) + self.assertEqual(response.raw.version, 11) @pytest.mark.flaky(reruns=3) @pytest.mark.timeout(timeout=600, method="thread") @@ -202,6 +338,7 @@ def test_head_call_with_path_setup_with_any_implicit_api(self): response = requests.head(self.url + "/anyandall", timeout=300) self.assertEqual(response.status_code, 200) + self.assertEqual(response.raw.version, 11) @pytest.mark.flaky(reruns=3) @pytest.mark.timeout(timeout=600, method="thread") @@ -213,6 +350,7 @@ def test_delete_call_with_path_setup_with_any_implicit_api(self): self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), {"hello": "world"}) + self.assertEqual(response.raw.version, 11) @pytest.mark.flaky(reruns=3) @pytest.mark.timeout(timeout=600, method="thread") @@ -223,6 +361,7 @@ def test_options_call_with_path_setup_with_any_implicit_api(self): response = requests.options(self.url + "/anyandall", timeout=300) self.assertEqual(response.status_code, 200) + self.assertEqual(response.raw.version, 11) @pytest.mark.flaky(reruns=3) @pytest.mark.timeout(timeout=600, method="thread") @@ -234,6 +373,7 @@ def test_patch_call_with_path_setup_with_any_implicit_api(self): self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), {"hello": "world"}) + self.assertEqual(response.raw.version, 11) @pytest.mark.flaky(reruns=3) @pytest.mark.timeout(timeout=600, method="thread") @@ -246,6 +386,7 @@ def test_large_input_request(self): self.assertEqual(response.status_code, 200) response_data = response.json() self.assertEqual(response_data.get("body"), data) + self.assertEqual(response.raw.version, 11) @parameterized_class( @@ -273,6 +414,7 @@ def test_calling_proxy_endpoint(self): self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), {"hello": "world"}) + self.assertEqual(response.raw.version, 11) @pytest.mark.flaky(reruns=3) @pytest.mark.timeout(timeout=600, method="thread") @@ -284,6 +426,7 @@ def test_get_call_with_path_setup_with_any_implicit_api(self): self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), {"hello": "world"}) + self.assertEqual(response.raw.version, 11) @pytest.mark.flaky(reruns=3) @pytest.mark.timeout(timeout=600, method="thread") @@ -295,6 +438,7 @@ def test_post_call_with_path_setup_with_any_implicit_api(self): self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), {"hello": "world"}) + self.assertEqual(response.raw.version, 11) @pytest.mark.flaky(reruns=3) @pytest.mark.timeout(timeout=600, method="thread") @@ -306,6 +450,7 @@ def test_put_call_with_path_setup_with_any_implicit_api(self): self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), {"hello": "world"}) + self.assertEqual(response.raw.version, 11) @pytest.mark.flaky(reruns=3) @pytest.mark.timeout(timeout=600, method="thread") @@ -316,6 +461,7 @@ def test_head_call_with_path_setup_with_any_implicit_api(self): response = requests.head(self.url + "/anyandall", timeout=300) self.assertEqual(response.status_code, 200) + self.assertEqual(response.raw.version, 11) @pytest.mark.flaky(reruns=3) @pytest.mark.timeout(timeout=600, method="thread") @@ -327,6 +473,7 @@ def test_delete_call_with_path_setup_with_any_implicit_api(self): self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), {"hello": "world"}) + self.assertEqual(response.raw.version, 11) @pytest.mark.flaky(reruns=3) @pytest.mark.timeout(timeout=600, method="thread") @@ -337,6 +484,7 @@ def test_options_call_with_path_setup_with_any_implicit_api(self): response = requests.options(self.url + "/anyandall", timeout=300) self.assertEqual(response.status_code, 200) + self.assertEqual(response.raw.version, 11) @pytest.mark.flaky(reruns=3) @pytest.mark.timeout(timeout=600, method="thread") @@ -348,6 +496,7 @@ def test_patch_call_with_path_setup_with_any_implicit_api(self): self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), {"hello": "world"}) + self.assertEqual(response.raw.version, 11) @pytest.mark.flaky(reruns=3) @pytest.mark.timeout(timeout=600, method="thread") @@ -359,6 +508,7 @@ def test_valid_v2_lambda_json_response(self): self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), {"foo": "bar"}) + self.assertEqual(response.raw.version, 11) @pytest.mark.flaky(reruns=3) @pytest.mark.timeout(timeout=600, method="thread") @@ -370,6 +520,7 @@ def test_invalid_v1_lambda_json_response(self): self.assertEqual(response.status_code, 502) self.assertEqual(response.json(), {"message": "Internal server error"}) + self.assertEqual(response.raw.version, 11) @pytest.mark.flaky(reruns=3) @pytest.mark.timeout(timeout=600, method="thread") @@ -381,6 +532,7 @@ def test_valid_v2_lambda_string_response(self): self.assertEqual(response.status_code, 200) self.assertEqual(response.text, "This is invalid") + self.assertEqual(response.raw.version, 11) @pytest.mark.flaky(reruns=3) @pytest.mark.timeout(timeout=600, method="thread") @@ -392,6 +544,7 @@ def test_valid_v2_lambda_integer_response(self): self.assertEqual(response.status_code, 200) self.assertEqual(response.text, "2") + self.assertEqual(response.raw.version, 11) @pytest.mark.flaky(reruns=3) @pytest.mark.timeout(timeout=600, method="thread") @@ -403,6 +556,7 @@ def test_v2_lambda_response_skip_unexpected_fields(self): self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), {"hello": "world"}) + self.assertEqual(response.raw.version, 11) @pytest.mark.flaky(reruns=3) @pytest.mark.timeout(timeout=600, method="thread") @@ -414,6 +568,7 @@ def test_invalid_v1_lambda_string_response(self): self.assertEqual(response.status_code, 502) self.assertEqual(response.json(), {"message": "Internal server error"}) + self.assertEqual(response.raw.version, 11) class TestStartApiWithSwaggerApis(StartApiIntegBaseClass): diff --git a/tests/integration/testdata/buildcmd/language-extensions.yaml b/tests/integration/testdata/buildcmd/language-extensions.yaml new file mode 100644 index 0000000000..db3b791f6e --- /dev/null +++ b/tests/integration/testdata/buildcmd/language-extensions.yaml @@ -0,0 +1,26 @@ +AWSTemplateFormatVersion : '2010-09-09' +Transform: + - AWS::LanguageExtensions + - AWS::Serverless-2016-10-31 + +Parameters: + Environment: + Type: String + Default: dev + AllowedValues: + - dev + - prod + +Conditions: + IsProd: !Equals [!Ref Environment, prod] + +Globals: + Function: + Timeout: 20 + MemorySize: 512 + +Resources: + Bucket: + Type: AWS::S3::Bucket + DeletionPolicy: !If [ IsProd, Retain, Delete ] + UpdateReplacePolicy: !If [ IsProd, Retain, Delete ] diff --git a/tests/unit/lib/samlib/test_wrapper.py b/tests/unit/lib/samlib/test_wrapper.py new file mode 100644 index 0000000000..3f91ff5c4e --- /dev/null +++ b/tests/unit/lib/samlib/test_wrapper.py @@ -0,0 +1,28 @@ +from unittest import TestCase +from unittest.mock import patch, Mock + +from parameterized import parameterized + +from samcli.lib.samlib.wrapper import SamTranslatorWrapper + + +class TestLanguageExtensionsPatching(TestCase): + @parameterized.expand( + [ + ({"Transform": ["AWS::LanguageExtensions", "AWS::Serverless-2016-10-31"]}, True), + ({"Transform": ["AWS::LanguageExtensions"]}, True), + ({"Transform": ["AWS::LanguageExtensions-extension"]}, True), + ({"Transform": "AWS::LanguageExtensions"}, True), + ({"Transform": "AWS::LanguageExtensions-extension"}, True), + ({"Transform": "AWS::Serverless-2016-10-31"}, False), + ({}, False), + ] + ) + def test_check_using_langauge_extension(self, template, expected): + self.assertEqual(SamTranslatorWrapper._check_using_language_extension(template), expected) + + @patch("samcli.lib.samlib.wrapper.SamResource") + def test_patch_language_extensions(self, patched_sam_resource): + wrapper = SamTranslatorWrapper({"Transform": "AWS::LanguageExtensions"}) + wrapper._patch_language_extensions() + self.assertEqual(patched_sam_resource.valid.__name__, "patched_func") diff --git a/tests/unit/lib/telemetry/test_project_metadata.py b/tests/unit/lib/telemetry/test_project_metadata.py index b165e912fa..acb62b81f8 100644 --- a/tests/unit/lib/telemetry/test_project_metadata.py +++ b/tests/unit/lib/telemetry/test_project_metadata.py @@ -36,12 +36,14 @@ def test_return_none_when_telemetry_disabled(self): @parameterized.expand( [ - ("https://github.com/aws/aws-sam-cli.git\n", "github.com/aws/aws-sam-cli.git"), - ("http://github.com/aws/aws-sam-cli.git\n", "github.com/aws/aws-sam-cli.git"), - ("git@github.com:aws/aws-sam-cli.git\n", "github.com/aws/aws-sam-cli.git"), - ("https://github.com/aws/aws-cli.git\n", "github.com/aws/aws-cli.git"), - ("http://not.a.real.site.com/somebody/my-project.git", "not.a.real.site.com/somebody/my-project.git"), - ("git@not.github:person/my-project.git", "not.github/person/my-project.git"), + ("https://github.com/aws/aws-sam-cli.git\n", "github.com/aws/aws-sam-cli"), + ("http://github.com/aws/aws-sam-cli.git/\n", "github.com/aws/aws-sam-cli"), + ("http://example.com:8080/aws-sam-cli.git\n", "example.com/aws-sam-cli"), + ("http://my_user@example.com/aws-sam-cli.git/\n", "example.com/aws-sam-cli"), + ("git@github.com:aws/aws-sam-cli.git\n", "github.com/aws/aws-sam-cli"), + ("https://github.com/aws/aws-cli.git\n", "github.com/aws/aws-cli"), + ("http://not.a.real.site.com/somebody/my-project.git", "not.a.real.site.com/somebody/my-project"), + ("git@not.github:person/my-project.git", "not.github/person/my-project"), ] ) @patch("samcli.lib.telemetry.project_metadata.subprocess.run") @@ -63,16 +65,21 @@ def test_retrieve_git_origin_when_not_a_repo(self, sp_mock): @parameterized.expand( [ ("https://github.com/aws/aws-sam-cli.git\n", "aws-sam-cli"), - ("https://github.com/aws/aws-sam-cli.git\n", "aws-sam-cli"), + ("http://github.com/aws/aws-sam-cli.git\n", "aws-sam-cli"), + ("http://example.com:8080/aws-sam-cli.git\n", "aws-sam-cli"), + ("http://my_user@example.com/aws-sam-cli\n", "aws-sam-cli"), ("git@github.com:aws/aws-sam-cli.git\n", "aws-sam-cli"), - ("https://github.com/aws/aws-cli.git\n", "aws-cli"), + ("https://github.com/aws/aws-cli/\n", "aws-cli"), ("http://not.a.real.site.com/somebody/my-project.git", "my-project"), ("git@not.github:person/my-project.git", "my-project"), + ("user@example.com/some_project.git", "some_project"), ] ) + @patch("samcli.lib.telemetry.project_metadata.getcwd") @patch("samcli.lib.telemetry.project_metadata.subprocess.run") - def test_retrieve_project_name_from_git(self, origin, expected, sp_mock): + def test_retrieve_project_name_from_git(self, origin, expected, sp_mock, cwd_mock): sp_mock.return_value = CompletedProcess(["git", "config", "--get", "remote.origin.url"], 0, stdout=origin) + cwd_mock.return_value = expected project_name = get_project_name() expected_hash = hashlib.sha256() @@ -81,25 +88,25 @@ def test_retrieve_project_name_from_git(self, origin, expected, sp_mock): @parameterized.expand( [ - ("C:/Users/aws/path/to/library/aws-sam-cli"), - ("C:\\Users\\aws\\Windows\\path\\aws-sam-cli"), - ("C:/"), - ("C:\\"), - ("E:/path/to/another/dir"), - ("This/one/doesn't/start/with/a/letter"), - ("/banana"), - ("D:/one/more/just/to/be/safe"), + ("C:/Users/aws/path/to/library/aws-sam-cli", "aws-sam-cli"), + ("C:\\Users\\aws\\Windows\\path\\aws-sam-cli", "aws-sam-cli"), + ("C:/", ""), + ("C:\\", ""), + ("E:/path/to/another/dir", "dir"), + ("This/one/doesn't/start/with/a/letter", "letter"), + ("/banana", "banana"), + ("D:/one/more/just/to/be/safe", "safe"), ] ) @patch("samcli.lib.telemetry.project_metadata.getcwd") @patch("samcli.lib.telemetry.project_metadata.subprocess.run") - def test_retrieve_project_name_from_dir(self, cwd, sp_mock, cwd_mock): + def test_retrieve_project_name_from_dir(self, cwd, expected, sp_mock, cwd_mock): sp_mock.side_effect = CalledProcessError(128, ["git", "config", "--get", "remote.origin.url"]) cwd_mock.return_value = cwd project_name = get_project_name() expected_hash = hashlib.sha256() - expected_hash.update(cwd.replace("\\", "/").encode("utf-8")) + expected_hash.update(expected.encode("utf-8")) self.assertEqual(project_name, expected_hash.hexdigest()) @parameterized.expand(