From 887411fe4ece31d8fd9f0e36dc4ad85cf07c716f Mon Sep 17 00:00:00 2001 From: hnnasit <84355507+hnnasit@users.noreply.github.com> Date: Wed, 21 Jun 2023 12:32:07 -0400 Subject: [PATCH] test: Integration tests for remote invoke on regular lambda functions (#5382) * Created base integ glass for remote invoke tests * Add integration tests for invoking lambda functions * make black * Moved tearDownClass to base class * Removed tearDown class from inherited classes and updated lambda fn timeout * Remove the check to skip appveyor tests on master branch --- tests/integration/remote/__init__.py | 0 tests/integration/remote/invoke/__init__.py | 0 .../remote/invoke/remote_invoke_integ_base.py | 104 +++++++ .../remote/invoke/test_remote_invoke.py | 279 ++++++++++++++++++ .../testdata/remote_invoke/__init__.py | 0 .../remote_invoke/events/default_event.json | 5 + .../remote_invoke/lambda-fns/__init__.py | 0 .../testdata/remote_invoke/lambda-fns/main.py | 31 ++ .../childstack/function/__init__.py | 0 .../childstack/function/app.py | 4 + .../childstack/function/requirements.txt | 0 .../nested_templates/childstack/template.yaml | 11 + .../nested_templates/template.yaml | 8 + .../template-multiple-resources.yaml | 55 ++++ .../remote_invoke/template-single-lambda.yaml | 11 + 15 files changed, 508 insertions(+) create mode 100644 tests/integration/remote/__init__.py create mode 100644 tests/integration/remote/invoke/__init__.py create mode 100644 tests/integration/remote/invoke/remote_invoke_integ_base.py create mode 100644 tests/integration/remote/invoke/test_remote_invoke.py create mode 100644 tests/integration/testdata/remote_invoke/__init__.py create mode 100644 tests/integration/testdata/remote_invoke/events/default_event.json create mode 100644 tests/integration/testdata/remote_invoke/lambda-fns/__init__.py create mode 100644 tests/integration/testdata/remote_invoke/lambda-fns/main.py create mode 100644 tests/integration/testdata/remote_invoke/nested_templates/childstack/function/__init__.py create mode 100644 tests/integration/testdata/remote_invoke/nested_templates/childstack/function/app.py create mode 100644 tests/integration/testdata/remote_invoke/nested_templates/childstack/function/requirements.txt create mode 100644 tests/integration/testdata/remote_invoke/nested_templates/childstack/template.yaml create mode 100644 tests/integration/testdata/remote_invoke/nested_templates/template.yaml create mode 100644 tests/integration/testdata/remote_invoke/template-multiple-resources.yaml create mode 100644 tests/integration/testdata/remote_invoke/template-single-lambda.yaml diff --git a/tests/integration/remote/__init__.py b/tests/integration/remote/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/remote/invoke/__init__.py b/tests/integration/remote/invoke/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/remote/invoke/remote_invoke_integ_base.py b/tests/integration/remote/invoke/remote_invoke_integ_base.py new file mode 100644 index 0000000000..b0bd0cdaf8 --- /dev/null +++ b/tests/integration/remote/invoke/remote_invoke_integ_base.py @@ -0,0 +1,104 @@ +from unittest import TestCase, skipIf +from pathlib import Path +from typing import Optional + +from tests.testing_utils import ( + get_sam_command, + run_command, +) +from tests.integration.deploy.deploy_integ_base import DeployIntegBase + +from samcli.lib.utils.boto_utils import get_boto_resource_provider_with_config, get_boto_client_provider_with_config +from samcli.lib.utils.cloudformation import get_resource_summaries + + +class RemoteInvokeIntegBase(TestCase): + template: Optional[Path] = None + + @classmethod + def setUpClass(cls): + cls.cmd = get_sam_command() + cls.test_data_path = cls.get_integ_dir().joinpath("testdata") + if cls.template: + cls.template_path = str(cls.test_data_path.joinpath("remote_invoke", cls.template)) + cls.events_folder_path = cls.test_data_path.joinpath("remote_invoke", "events") + + @classmethod + def tearDownClass(cls): + # Delete the deployed stack + cls.cfn_client.delete_stack(StackName=cls.stack_name) + + @staticmethod + def get_integ_dir(): + return Path(__file__).resolve().parents[2] + + @staticmethod + def remote_invoke_deploy_stack(stack_name, template_path): + + deploy_cmd = DeployIntegBase.get_deploy_command_list( + stack_name=stack_name, + template_file=template_path, + resolve_s3=True, + capabilities_list=["CAPABILITY_IAM", "CAPABILITY_AUTO_EXPAND"], + ) + + run_command(deploy_cmd) + + @classmethod + def create_resources_and_boto_clients(cls): + cls.remote_invoke_deploy_stack(cls.stack_name, cls.template_path) + stack_resource_summaries = get_resource_summaries( + get_boto_resource_provider_with_config(), + get_boto_client_provider_with_config(), + cls.stack_name, + ) + cls.stack_resources = { + resource_full_path: stack_resource_summary.physical_resource_id + for resource_full_path, stack_resource_summary in stack_resource_summaries.items() + } + cls.cfn_client = get_boto_client_provider_with_config()("cloudformation") + cls.lambda_client = get_boto_client_provider_with_config()("lambda") + + @staticmethod + def get_command_list( + stack_name=None, + resource_id=None, + event=None, + event_file=None, + parameter_list=None, + output=None, + region=None, + profile=None, + beta_features=None, + ): + command_list = [get_sam_command(), "remote", "invoke"] + + if stack_name: + command_list = command_list + ["--stack-name", stack_name] + + if event: + command_list = command_list + ["-e", event] + + if event_file: + command_list = command_list + ["--event-file", event_file] + + if profile: + command_list = command_list + ["--parameter", parameter] + + if output: + command_list = command_list + ["--output", output] + + if parameter_list: + for (parameter, value) in parameter_list: + command_list = command_list + ["--parameter", f"{parameter}={value}"] + + if region: + command_list = command_list + ["--region", region] + + if beta_features is not None: + command_list = command_list + ["--beta-features" if beta_features else "--no-beta-features"] + + if resource_id: + command_list = command_list + [resource_id] + + return command_list diff --git a/tests/integration/remote/invoke/test_remote_invoke.py b/tests/integration/remote/invoke/test_remote_invoke.py new file mode 100644 index 0000000000..e3eb6aa2de --- /dev/null +++ b/tests/integration/remote/invoke/test_remote_invoke.py @@ -0,0 +1,279 @@ +import json +import uuid +import base64 + +from parameterized import parameterized + +from tests.integration.remote.invoke.remote_invoke_integ_base import RemoteInvokeIntegBase +from tests.testing_utils import run_command + +from pathlib import Path +import pytest + + +@pytest.mark.xdist_group(name="sam_remote_invoke_single_lambda_resource") +class TestSingleResourceInvoke(RemoteInvokeIntegBase): + template = Path("template-single-lambda.yaml") + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.stack_name = f"{TestSingleResourceInvoke.__name__}-{uuid.uuid4().hex}" + cls.create_resources_and_boto_clients() + + def test_invoke_empty_event_provided(self): + command_list = self.get_command_list(stack_name=self.stack_name) + + remote_invoke_result = run_command(command_list) + self.assertEqual(0, remote_invoke_result.process.returncode) + remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) + self.assertEqual(remote_invoke_result_stdout["errorType"], "KeyError") + + def test_invoke_with_only_event_provided(self): + command_list = self.get_command_list( + stack_name=self.stack_name, + event='{"key1": "Hello", "key2": "serverless", "key3": "world"}', + ) + + remote_invoke_result = run_command(command_list) + + self.assertEqual(0, remote_invoke_result.process.returncode) + remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) + self.assertEqual(remote_invoke_result_stdout, {"message": "Hello world"}) + + def test_invoke_with_only_event_file_provided(self): + event_file_path = str(self.events_folder_path.joinpath("default_event.json")) + command_list = self.get_command_list( + stack_name=self.stack_name, resource_id="HelloWorldFunction", event_file=event_file_path + ) + + remote_invoke_result = run_command(command_list) + + self.assertEqual(0, remote_invoke_result.process.returncode) + remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) + self.assertEqual(remote_invoke_result_stdout, {"message": "Hello world"}) + + def test_invoke_with_resource_id_provided_as_arn(self): + resource_id = "HelloWorldFunction" + lambda_name = self.stack_resources[resource_id] + lambda_arn = self.lambda_client.get_function(FunctionName=lambda_name)["Configuration"]["FunctionArn"] + + command_list = self.get_command_list( + resource_id=lambda_arn, + event='{"key1": "Hello", "key2": "serverless", "key3": "world"}', + ) + + remote_invoke_result = run_command(command_list) + + self.assertEqual(0, remote_invoke_result.process.returncode) + remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) + self.assertEqual(remote_invoke_result_stdout, {"message": "Hello world"}) + + def test_invoke_asynchronous_using_boto_parameter(self): + command_list = self.get_command_list( + stack_name=self.stack_name, + event='{"key1": "Hello", "key2": "serverless", "key3": "world"}', + parameter_list=[("InvocationType", "Event"), ("LogType", "None")], + output="json", + ) + + remote_invoke_result = run_command(command_list) + + self.assertEqual(0, remote_invoke_result.process.returncode) + remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) + self.assertEqual(remote_invoke_result_stdout["Payload"], "") + self.assertEqual(remote_invoke_result_stdout["StatusCode"], 202) + + def test_invoke_dryrun_using_boto_parameter(self): + command_list = self.get_command_list( + stack_name=self.stack_name, + event='{"key1": "Hello", "key2": "serverless", "key3": "world"}', + parameter_list=[("InvocationType", "DryRun"), ("Qualifier", "$LATEST")], + output="json", + ) + + remote_invoke_result = run_command(command_list) + + self.assertEqual(0, remote_invoke_result.process.returncode) + remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) + self.assertEqual(remote_invoke_result_stdout["Payload"], "") + self.assertEqual(remote_invoke_result_stdout["StatusCode"], 204) + + def test_invoke_response_json_output_format(self): + command_list = self.get_command_list( + stack_name=self.stack_name, + event='{"key1": "Hello", "key2": "serverless", "key3": "world"}', + output="json", + ) + + remote_invoke_result = run_command(command_list) + + self.assertEqual(0, remote_invoke_result.process.returncode) + remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) + + response_payload = json.loads(remote_invoke_result_stdout["Payload"]) + self.assertEqual(response_payload, {"message": "Hello world"}) + self.assertEqual(remote_invoke_result_stdout["StatusCode"], 200) + + +@pytest.mark.xdist_group(name="sam_remote_invoke_multiple_resources") +class TestMultipleResourcesInvoke(RemoteInvokeIntegBase): + template = Path("template-multiple-resources.yaml") + + @classmethod + def tearDownClass(cls): + # Delete the deployed stack + cls.cfn_client.delete_stack(StackName=cls.stack_name) + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.stack_name = f"{TestMultipleResourcesInvoke.__name__}-{uuid.uuid4().hex}" + cls.create_resources_and_boto_clients() + + def test_invoke_empty_event_provided(self): + command_list = self.get_command_list(stack_name=self.stack_name, resource_id="EchoEventFunction") + + remote_invoke_result = run_command(command_list) + self.assertEqual(0, remote_invoke_result.process.returncode) + remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) + self.assertEqual(remote_invoke_result_stdout, {}) + + @parameterized.expand( + [ + ("HelloWorldServerlessFunction", {"message": "Hello world"}), + ("EchoCustomEnvVarFunction", "MyOtherVar"), + ("EchoEventFunction", {"key1": "Hello", "key2": "serverless", "key3": "world"}), + ] + ) + def test_invoke_with_only_event_provided(self, resource_id, expected_response): + command_list = self.get_command_list( + stack_name=self.stack_name, + resource_id=resource_id, + event='{"key1": "Hello", "key2": "serverless", "key3": "world"}', + ) + + remote_invoke_result = run_command(command_list) + + self.assertEqual(0, remote_invoke_result.process.returncode) + remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) + self.assertEqual(remote_invoke_result_stdout, expected_response) + + @parameterized.expand( + [ + ("HelloWorldServerlessFunction", {"message": "Hello world"}), + ("EchoCustomEnvVarFunction", "MyOtherVar"), + ("EchoEventFunction", {"key1": "Hello", "key2": "serverless", "key3": "world"}), + ] + ) + def test_invoke_with_resource_id_provided_as_arn(self, resource_id, expected_response): + lambda_name = self.stack_resources[resource_id] + lambda_arn = self.lambda_client.get_function(FunctionName=lambda_name)["Configuration"]["FunctionArn"] + + command_list = self.get_command_list( + resource_id=lambda_arn, + event='{"key1": "Hello", "key2": "serverless", "key3": "world"}', + ) + + remote_invoke_result = run_command(command_list) + + self.assertEqual(0, remote_invoke_result.process.returncode) + remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) + self.assertEqual(remote_invoke_result_stdout, expected_response) + + def test_lambda_writes_to_stderr_invoke(self): + command_list = RemoteInvokeIntegBase.get_command_list( + stack_name=self.stack_name, + resource_id="WriteToStderrFunction", + event='{"key1": "Hello", "key2": "serverless", "key3": "world"}', + ) + + remote_invoke_result = run_command(command_list) + + self.assertEqual(0, remote_invoke_result.process.returncode) + remote_invoke_result_stdout = remote_invoke_result.stdout.strip().decode() + remote_invoke_result_stderr = remote_invoke_result.stderr.strip().decode() + self.assertIn("Lambda Function is writing to stderr", remote_invoke_result_stderr) + self.assertEqual('"wrote to stderr"', remote_invoke_result_stdout) + + def test_lambda_raises_exception_invoke(self): + command_list = self.get_command_list( + stack_name=self.stack_name, + resource_id="RaiseExceptionFunction", + event='{"key1": "Hello", "key2": "serverless", "key3": "world"}', + ) + + remote_invoke_result = run_command(command_list) + + self.assertEqual(0, remote_invoke_result.process.returncode) + remote_invoke_result_stderr = remote_invoke_result.stderr.strip().decode() + remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) + + self.assertIn("Lambda is raising an exception", remote_invoke_result_stderr) + self.assertEqual("Lambda is raising an exception", remote_invoke_result_stdout["errorMessage"]) + + def test_lambda_invoke_client_context_boto_parameter(self): + custom_json_str = {"custom": {"foo": "bar", "baz": "quzz"}} + client_context_base64_str = base64.b64encode(json.dumps(custom_json_str).encode()).decode("utf-8") + + command_list = self.get_command_list( + stack_name=self.stack_name, + resource_id="EchoClientContextData", + event='{"key1": "Hello", "key2": "serverless", "key3": "world"}', + parameter_list=[("ClientContext", client_context_base64_str)], + ) + + remote_invoke_result = run_command(command_list) + + self.assertEqual(0, remote_invoke_result.process.returncode) + remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) + self.assertEqual(remote_invoke_result_stdout, custom_json_str["custom"]) + + +@pytest.mark.xdist_group(name="sam_remote_invoke_nested_resources") +class TestNestedTemplateResourcesInvoke(RemoteInvokeIntegBase): + template = Path("nested_templates/template.yaml") + + @classmethod + def tearDownClass(cls): + # Delete the deployed stack + cls.cfn_client.delete_stack(StackName=cls.stack_name) + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.stack_name = f"{TestNestedTemplateResourcesInvoke.__name__}-{uuid.uuid4().hex}" + cls.create_resources_and_boto_clients() + + def test_invoke_empty_event_provided(self): + command_list = self.get_command_list( + stack_name=self.stack_name, + ) + + remote_invoke_result = run_command(command_list) + self.assertEqual(0, remote_invoke_result.process.returncode) + remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) + self.assertEqual(remote_invoke_result_stdout, {"message": "Hello world"}) + + def test_invoke_with_only_event_provided(self): + command_list = self.get_command_list( + stack_name=self.stack_name, + resource_id="ChildStack/HelloWorldFunction", + event='{"key1": "Hello", "key2": "serverless", "key3": "world"}', + ) + + remote_invoke_result = run_command(command_list) + + self.assertEqual(0, remote_invoke_result.process.returncode) + remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) + self.assertEqual(remote_invoke_result_stdout, {"message": "Hello world"}) + + def test_invoke_default_lambda_function(self): + event_file_path = str(self.events_folder_path.joinpath("default_event.json")) + command_list = self.get_command_list(stack_name=self.stack_name, event_file=event_file_path) + + remote_invoke_result = run_command(command_list) + + self.assertEqual(0, remote_invoke_result.process.returncode) + remote_invoke_result_stdout = json.loads(remote_invoke_result.stdout.strip().decode()) + self.assertEqual(remote_invoke_result_stdout, {"message": "Hello world"}) diff --git a/tests/integration/testdata/remote_invoke/__init__.py b/tests/integration/testdata/remote_invoke/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/testdata/remote_invoke/events/default_event.json b/tests/integration/testdata/remote_invoke/events/default_event.json new file mode 100644 index 0000000000..b842029ae7 --- /dev/null +++ b/tests/integration/testdata/remote_invoke/events/default_event.json @@ -0,0 +1,5 @@ +{ + "key1": "Hello", + "key2": "serverless", + "key3": "world" +} \ No newline at end of file diff --git a/tests/integration/testdata/remote_invoke/lambda-fns/__init__.py b/tests/integration/testdata/remote_invoke/lambda-fns/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/testdata/remote_invoke/lambda-fns/main.py b/tests/integration/testdata/remote_invoke/lambda-fns/main.py new file mode 100644 index 0000000000..b3424d1b70 --- /dev/null +++ b/tests/integration/testdata/remote_invoke/lambda-fns/main.py @@ -0,0 +1,31 @@ +import os +import logging + +LOG = logging.getLogger(__name__) + +def default_handler(event, context): + print("value1 = " + event["key1"]) + print("value2 = " + event["key2"]) + print("value3 = " + event["key3"]) + + return { + "message": f'{event["key1"]} {event["key3"]}' + } + +def custom_env_var_echo_handler(event, context): + return os.environ.get("CustomEnvVar") + +def echo_client_context_data(event, context): + custom_dict = context.client_context.custom + return custom_dict + +def write_to_stderr(event, context): + LOG.error("Lambda Function is writing to stderr") + + return "wrote to stderr" + +def echo_event(event, context): + return event + +def raise_exception(event, context): + raise Exception("Lambda is raising an exception") \ No newline at end of file diff --git a/tests/integration/testdata/remote_invoke/nested_templates/childstack/function/__init__.py b/tests/integration/testdata/remote_invoke/nested_templates/childstack/function/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/testdata/remote_invoke/nested_templates/childstack/function/app.py b/tests/integration/testdata/remote_invoke/nested_templates/childstack/function/app.py new file mode 100644 index 0000000000..cce4a03dca --- /dev/null +++ b/tests/integration/testdata/remote_invoke/nested_templates/childstack/function/app.py @@ -0,0 +1,4 @@ +def handler(event, context): + return { + "message": "Hello world", + } \ No newline at end of file diff --git a/tests/integration/testdata/remote_invoke/nested_templates/childstack/function/requirements.txt b/tests/integration/testdata/remote_invoke/nested_templates/childstack/function/requirements.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/testdata/remote_invoke/nested_templates/childstack/template.yaml b/tests/integration/testdata/remote_invoke/nested_templates/childstack/template.yaml new file mode 100644 index 0000000000..c082eb0fe4 --- /dev/null +++ b/tests/integration/testdata/remote_invoke/nested_templates/childstack/template.yaml @@ -0,0 +1,11 @@ +AWSTemplateFormatVersion : '2010-09-09' +Transform: AWS::Serverless-2016-10-31 + +Resources: + HelloWorldFunction: + Type: AWS::Serverless::Function + Properties: + Handler: app.handler + Runtime: python3.9 + CodeUri: function/ + Timeout: 30 \ No newline at end of file diff --git a/tests/integration/testdata/remote_invoke/nested_templates/template.yaml b/tests/integration/testdata/remote_invoke/nested_templates/template.yaml new file mode 100644 index 0000000000..66cd5f3433 --- /dev/null +++ b/tests/integration/testdata/remote_invoke/nested_templates/template.yaml @@ -0,0 +1,8 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 + +Resources: + ChildStack: + Properties: + TemplateURL: childstack/template.yaml + Type: AWS::CloudFormation::Stack \ No newline at end of file diff --git a/tests/integration/testdata/remote_invoke/template-multiple-resources.yaml b/tests/integration/testdata/remote_invoke/template-multiple-resources.yaml new file mode 100644 index 0000000000..ffebe530c1 --- /dev/null +++ b/tests/integration/testdata/remote_invoke/template-multiple-resources.yaml @@ -0,0 +1,55 @@ +AWSTemplateFormatVersion : '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: A hello world application that creates multiple resources used for inteting remote invoke command. + +Resources: + HelloWorldServerlessFunction: + Type: AWS::Serverless::Function + Properties: + Handler: main.default_handler + Runtime: python3.9 + CodeUri: ./lambda-fns + Timeout: 5 + + EchoEventFunction: + Type: AWS::Serverless::Function + Properties: + Handler: main.echo_event + Runtime: python3.9 + CodeUri: ./lambda-fns + Timeout: 5 + + EchoClientContextData: + Type: AWS::Serverless::Function + Properties: + Handler: main.echo_client_context_data + Runtime: python3.9 + CodeUri: ./lambda-fns + Timeout: 5 + + EchoCustomEnvVarFunction: + Type: AWS::Serverless::Function + Properties: + Handler: main.custom_env_var_echo_handler + Runtime: python3.9 + CodeUri: ./lambda-fns + Environment: + Variables: + CustomEnvVar: "MyOtherVar" + Timeout: 5 + + WriteToStderrFunction: + Type: AWS::Serverless::Function + Properties: + Handler: main.write_to_stderr + Runtime: python3.9 + CodeUri: ./lambda-fns + Timeout: 5 + + RaiseExceptionFunction: + Type: AWS::Serverless::Function + Properties: + Handler: main.raise_exception + Runtime: python3.9 + CodeUri: ./lambda-fns + Timeout: 5 \ No newline at end of file diff --git a/tests/integration/testdata/remote_invoke/template-single-lambda.yaml b/tests/integration/testdata/remote_invoke/template-single-lambda.yaml new file mode 100644 index 0000000000..7ca9b259db --- /dev/null +++ b/tests/integration/testdata/remote_invoke/template-single-lambda.yaml @@ -0,0 +1,11 @@ +AWSTemplateFormatVersion : '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: A hello world application with single lambda function. + +Resources: + HelloWorldFunction: + Type: AWS::Serverless::Function + Properties: + Handler: main.default_handler + Runtime: python3.9 + CodeUri: ./lambda-fns \ No newline at end of file