diff --git a/samcli/commands/_utils/options.py b/samcli/commands/_utils/options.py index 07c46d41fb..188b1705b4 100644 --- a/samcli/commands/_utils/options.py +++ b/samcli/commands/_utils/options.py @@ -594,7 +594,13 @@ def remote_invoke_parameter_click_option(): type=RemoteInvokeBotoApiParameterType(), callback=remote_invoke_boto_parameter_callback, required=False, - help="Additional parameters for the boto API call.\n" "Lambda APIs: invoke and invoke_with_response_stream", + help="Additional parameters that can be passed to invoke the resource.\n" + "The following additional parameters can be used to invoke a lambda resource and get a buffered response: " + "InvocationType='Event'|'RequestResponse'|'DryRun', LogType='None'|'Tail', " + "ClientContext='base64-encoded string' Qualifier='string'. " + "The following additional parameters can be used to invoke a lambda resource with response streaming: " + "InvocationType='RequestResponse'|'DryRun', LogType='None'|'Tail', " + "ClientContext='base64-encoded string', Qualifier='string'.", ) diff --git a/samcli/commands/remote/invoke/cli.py b/samcli/commands/remote/invoke/cli.py index 4f7ed1b081..3f3a771ea1 100644 --- a/samcli/commands/remote/invoke/cli.py +++ b/samcli/commands/remote/invoke/cli.py @@ -8,7 +8,9 @@ from samcli.cli.context import Context from samcli.cli.main import aws_creds_options, common_options, pass_context, print_cmdline_args from samcli.cli.types import RemoteInvokeOutputFormatType +from samcli.commands._utils.command_exception_handler import command_exception_handler from samcli.commands._utils.options import remote_invoke_parameter_option +from samcli.commands.remote.invoke.core.command import RemoteInvokeCommand from samcli.lib.cli_validation.remote_invoke_options_validations import ( event_and_event_file_options_validation, stack_name_or_resource_id_atleast_one_option_validation, @@ -20,30 +22,45 @@ LOG = logging.getLogger(__name__) HELP_TEXT = """ -Invoke or send an event to cloud resources in your CFN stack +Invoke or send an event to resources in the cloud. """ SHORT_HELP = "Invoke a deployed resource in the cloud" +DESCRIPTION = """ + Invoke or send an event to resources in the cloud. + An event body can be passed using either -e (--event) or --event-file parameter. + Returned response will be written to stdout. Lambda logs will be written to stderr. +""" + -@click.command("invoke", help=HELP_TEXT, short_help=SHORT_HELP) +@click.command( + "invoke", + cls=RemoteInvokeCommand, + help=HELP_TEXT, + description=DESCRIPTION, + short_help=SHORT_HELP, + requires_credentials=True, + context_settings={"max_content_width": 120}, +) @configuration_option(provider=TomlProvider(section="parameters")) @click.option("--stack-name", required=False, help="Name of the stack to get the resource information from") -@click.option("--resource-id", required=False, help="Name of the resource that will be invoked") +@click.argument("resource-id", required=False) @click.option( "--event", "-e", help="The event that will be sent to the resource. The target parameter will depend on the resource type. " - "For instance: 'Payload' for Lambda", + "For instance: 'Payload' for Lambda which can be passed as a JSON string", ) @click.option( "--event-file", type=click.File("r", encoding="utf-8"), - help="The file that contains the event that will be sent to the resource", + help="The file that contains the event that will be sent to the resource.", ) @click.option( - "--output-format", - help="Output format for the boto API response", - default=RemoteInvokeOutputFormat.DEFAULT.name.lower(), + "--output", + help="Output the results from the command in a given output format. " + "The text format prints a readable AWS API response. The json format prints the full AWS API response.", + default=RemoteInvokeOutputFormat.TEXT.name.lower(), type=RemoteInvokeOutputFormatType(RemoteInvokeOutputFormat), ) @remote_invoke_parameter_option @@ -55,13 +72,14 @@ @track_command @check_newer_version @print_cmdline_args +@command_exception_handler def cli( ctx: Context, stack_name: str, resource_id: str, event: str, event_file: TextIOWrapper, - output_format: RemoteInvokeOutputFormat, + output: RemoteInvokeOutputFormat, parameter: dict, config_file: str, config_env: str, @@ -75,7 +93,7 @@ def cli( resource_id, event, event_file, - output_format, + output, parameter, ctx.region, ctx.profile, @@ -89,7 +107,7 @@ def do_cli( resource_id: str, event: str, event_file: TextIOWrapper, - output_format: RemoteInvokeOutputFormat, + output: RemoteInvokeOutputFormat, parameter: dict, region: str, profile: str, @@ -120,7 +138,7 @@ def do_cli( ) as remote_invoke_context: remote_invoke_input = RemoteInvokeExecutionInfo( - payload=event, payload_file=event_file, parameters=parameter, output_format=output_format + payload=event, payload_file=event_file, parameters=parameter, output_format=output ) remote_invoke_context.run(remote_invoke_input=remote_invoke_input) diff --git a/samcli/commands/remote/invoke/core/__init__.py b/samcli/commands/remote/invoke/core/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/samcli/commands/remote/invoke/core/command.py b/samcli/commands/remote/invoke/core/command.py new file mode 100644 index 0000000000..37ec181d86 --- /dev/null +++ b/samcli/commands/remote/invoke/core/command.py @@ -0,0 +1,158 @@ +""" +Invoke Command Class. +""" +import json + +from click import Context, style + +from samcli.cli.core.command import CoreCommand +from samcli.cli.row_modifiers import RowDefinition, ShowcaseRowModifier +from samcli.commands.remote.invoke.core.formatters import RemoteInvokeCommandHelpTextFormatter +from samcli.commands.remote.invoke.core.options import OPTIONS_INFO + + +class RemoteInvokeCommand(CoreCommand): + class CustomFormatterContext(Context): + formatter_class = RemoteInvokeCommandHelpTextFormatter + + context_class = CustomFormatterContext + + @staticmethod + def format_examples(ctx: Context, formatter: RemoteInvokeCommandHelpTextFormatter): + with formatter.indented_section(name="Examples", extra_indents=1): + with formatter.indented_section(name="Invoke default lambda function with empty event", extra_indents=1): + formatter.write_rd( + [ + RowDefinition( + text="\n", + ), + RowDefinition( + name=style(f"${ctx.command_path} --stack-name hello-world"), + extra_row_modifiers=[ShowcaseRowModifier()], + ), + ] + ) + with formatter.indented_section( + name="Invoke default lambda function with event passed as text input", extra_indents=1 + ): + formatter.write_rd( + [ + RowDefinition( + text="\n", + ), + RowDefinition( + name=style( + f"${ctx.command_path} --stack-name hello-world -e '{json.dumps({'message':'hello!'})}'" + ), + extra_row_modifiers=[ShowcaseRowModifier()], + ), + ] + ) + with formatter.indented_section(name="Invoke named lambda function with an event file", extra_indents=1): + formatter.write_rd( + [ + RowDefinition( + text="\n", + ), + RowDefinition( + name=style( + f"${ctx.command_path} --stack-name " + f"hello-world HelloWorldFunction --event-file event.json" + ), + extra_row_modifiers=[ShowcaseRowModifier()], + ), + ] + ) + with formatter.indented_section(name="Invoke lambda function with event as stdin input", extra_indents=1): + formatter.write_rd( + [ + RowDefinition( + text="\n", + ), + RowDefinition( + name=style( + f"$ echo '{json.dumps({'message':'hello!'})}' | " + f"{ctx.command_path} HelloWorldFunction --event-file -" + ), + extra_row_modifiers=[ShowcaseRowModifier()], + ), + ] + ) + with formatter.indented_section( + name="Invoke lambda function using lambda ARN and get the full AWS API response", extra_indents=1 + ): + formatter.write_rd( + [ + RowDefinition( + text="\n", + ), + RowDefinition( + name=style( + f"${ctx.command_path} arn:aws:lambda:us-west-2:123456789012:function:my-function -e <>" + f" --output json" + ), + extra_row_modifiers=[ShowcaseRowModifier()], + ), + ] + ) + with formatter.indented_section( + name="Asynchronously invoke lambda function with additional boto parameters", extra_indents=1 + ): + formatter.write_rd( + [ + RowDefinition( + text="\n", + ), + RowDefinition( + name=style( + f"${ctx.command_path} HelloWorldFunction -e <> " + f"--parameter InvocationType=Event --parameter Qualifier=MyQualifier" + ), + extra_row_modifiers=[ShowcaseRowModifier()], + ), + ] + ) + with formatter.indented_section( + name="Dry invoke a lambda function to validate parameter values and user/role permissions", + extra_indents=1, + ): + formatter.write_rd( + [ + RowDefinition( + text="\n", + ), + RowDefinition( + name=style( + f"${ctx.command_path} HelloWorldFunction -e <> --output json " + f"--parameter InvocationType=DryRun" + ), + extra_row_modifiers=[ShowcaseRowModifier()], + ), + ] + ) + + @staticmethod + def format_acronyms(formatter: RemoteInvokeCommandHelpTextFormatter): + with formatter.indented_section(name="Acronyms", extra_indents=1): + formatter.write_rd( + [ + RowDefinition( + name="ARN", + text="Amazon Resource Name", + extra_row_modifiers=[ShowcaseRowModifier()], + ), + ] + ) + + def format_options(self, ctx: Context, formatter: RemoteInvokeCommandHelpTextFormatter) -> None: # type:ignore + # NOTE: `ignore` is put in place here for mypy even though it is the correct behavior, + # as the `formatter_class` can be set in subclass of Command. If ignore is not set, + # mypy raises argument needs to be HelpFormatter as super class defines it. + + self.format_description(formatter) + RemoteInvokeCommand.format_examples(ctx, formatter) + RemoteInvokeCommand.format_acronyms(formatter) + + CoreCommand._format_options( + ctx=ctx, params=self.get_params(ctx), formatter=formatter, formatting_options=OPTIONS_INFO + ) diff --git a/samcli/commands/remote/invoke/core/formatters.py b/samcli/commands/remote/invoke/core/formatters.py new file mode 100644 index 0000000000..ee8cee01aa --- /dev/null +++ b/samcli/commands/remote/invoke/core/formatters.py @@ -0,0 +1,21 @@ +""" +Remote Invoke Command Formatter. +""" +from samcli.cli.formatters import RootCommandHelpTextFormatter +from samcli.cli.row_modifiers import BaseLineRowModifier +from samcli.commands.remote.invoke.core.options import ALL_OPTIONS + + +class RemoteInvokeCommandHelpTextFormatter(RootCommandHelpTextFormatter): + ADDITIVE_JUSTIFICATION = 17 + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # NOTE: Add Additional space after determining the longest option. + # However, do not justify with padding for more than half the width of + # the terminal to retain aesthetics. + self.left_justification_length = min( + max([len(option) for option in ALL_OPTIONS]) + self.ADDITIVE_JUSTIFICATION, + self.width // 2 - self.indent_increment, + ) + self.modifiers = [BaseLineRowModifier()] diff --git a/samcli/commands/remote/invoke/core/options.py b/samcli/commands/remote/invoke/core/options.py new file mode 100644 index 0000000000..87f5394fee --- /dev/null +++ b/samcli/commands/remote/invoke/core/options.py @@ -0,0 +1,54 @@ +""" +Remote Invoke Command Options related Datastructures for formatting. +""" +from typing import Dict, List + +from samcli.cli.core.options import ALL_COMMON_OPTIONS, add_common_options_info +from samcli.cli.row_modifiers import RowDefinition + +# NOTE: The ordering of the option lists matter, they are the order +# in which options will be displayed. + +INFRASTRUCTURE_OPTION_NAMES: List[str] = ["stack_name"] + +INPUT_EVENT_OPTIONS: List[str] = ["event", "event_file"] + +ADDITIONAL_OPTIONS: List[str] = ["parameter", "output"] + +AWS_CREDENTIAL_OPTION_NAMES: List[str] = ["region", "profile"] + +CONFIGURATION_OPTION_NAMES: List[str] = ["config_env", "config_file"] + +OTHER_OPTIONS: List[str] = ["debug"] + +ALL_OPTIONS: List[str] = ( + INFRASTRUCTURE_OPTION_NAMES + + INPUT_EVENT_OPTIONS + + ADDITIONAL_OPTIONS + + AWS_CREDENTIAL_OPTION_NAMES + + CONFIGURATION_OPTION_NAMES + + ALL_COMMON_OPTIONS +) + +OPTIONS_INFO: Dict[str, Dict] = { + "Infrastructure Options": { + "option_names": {opt: {"rank": idx} for idx, opt in enumerate(INFRASTRUCTURE_OPTION_NAMES)} + }, + "Input Event Options": {"option_names": {opt: {"rank": idx} for idx, opt in enumerate(INPUT_EVENT_OPTIONS)}}, + "Additional Options": {"option_names": {opt: {"rank": idx} for idx, opt in enumerate(ADDITIONAL_OPTIONS)}}, + "AWS Credential Options": { + "option_names": {opt: {"rank": idx} for idx, opt in enumerate(AWS_CREDENTIAL_OPTION_NAMES)} + }, + "Configuration Options": { + "option_names": {opt: {"rank": idx} for idx, opt in enumerate(CONFIGURATION_OPTION_NAMES)}, + "extras": [ + RowDefinition(name="Learn more about configuration files at:"), + RowDefinition( + name="https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli" + "-config.html. " + ), + ], + }, +} + +add_common_options_info(OPTIONS_INFO) diff --git a/samcli/lib/cli_validation/remote_invoke_options_validations.py b/samcli/lib/cli_validation/remote_invoke_options_validations.py index dbb0cde6ef..58345cd807 100644 --- a/samcli/lib/cli_validation/remote_invoke_options_validations.py +++ b/samcli/lib/cli_validation/remote_invoke_options_validations.py @@ -4,8 +4,6 @@ import logging import sys from functools import wraps -from io import TextIOWrapper -from typing import cast import click @@ -17,7 +15,7 @@ def event_and_event_file_options_validation(func): """ This function validates the cases when both --event and --event-file are provided and - neither option is provided + logs if "-" is provided for --event-file and event is read from stdin. Parameters ---------- @@ -48,10 +46,9 @@ def wrapped(*args, **kwargs): validator.validate() - # if no event nor event_file arguments are given, read from stdin - if not event and not event_file: - LOG.debug("Neither --event nor --event-file options have been provided, reading from stdin") - kwargs["event_file"] = cast(TextIOWrapper, sys.stdin) + # If "-" is provided for --event-file, click uses it as a special file to refer to stdin. + if event_file and event_file.fileno() == sys.stdin.fileno(): + LOG.info("Reading event from stdin (you can also pass it from file with --event-file)") return func(*args, **kwargs) return wrapped @@ -83,7 +80,7 @@ def wrapped(*args, **kwargs): exception=click.BadOptionUsage( option_name="--resource-id", ctx=ctx, - message="Atleast 1 of --stack-name or --resource-id parameters should be provided.", + message="At least 1 of --stack-name or --resource-id parameters should be provided.", ), ) diff --git a/samcli/lib/remote_invoke/lambda_invoke_executors.py b/samcli/lib/remote_invoke/lambda_invoke_executors.py index 936cd89289..4c47683008 100644 --- a/samcli/lib/remote_invoke/lambda_invoke_executors.py +++ b/samcli/lib/remote_invoke/lambda_invoke_executors.py @@ -111,9 +111,9 @@ def _execute_lambda_invoke(self, payload: str) -> RemoteInvokeIterableResponseTy self.request_parameters, ) lambda_response = self._execute_boto_call(self._lambda_client.invoke) - if self._remote_output_format == RemoteInvokeOutputFormat.RAW: + if self._remote_output_format == RemoteInvokeOutputFormat.JSON: yield RemoteInvokeResponse(lambda_response) - if self._remote_output_format == RemoteInvokeOutputFormat.DEFAULT: + if self._remote_output_format == RemoteInvokeOutputFormat.TEXT: log_result = lambda_response.get(LOG_RESULT) if log_result: yield RemoteInvokeLogOutput(base64.b64decode(log_result).decode("utf-8")) @@ -133,9 +133,9 @@ def _execute_lambda_invoke(self, payload: str) -> RemoteInvokeIterableResponseTy self.request_parameters, ) lambda_response = self._execute_boto_call(self._lambda_client.invoke_with_response_stream) - if self._remote_output_format == RemoteInvokeOutputFormat.RAW: + if self._remote_output_format == RemoteInvokeOutputFormat.JSON: yield RemoteInvokeResponse(lambda_response) - if self._remote_output_format == RemoteInvokeOutputFormat.DEFAULT: + if self._remote_output_format == RemoteInvokeOutputFormat.TEXT: event_stream: EventStream = lambda_response.get(EVENT_STREAM, []) for event in event_stream: if PAYLOAD_CHUNK in event: @@ -154,6 +154,9 @@ class DefaultConvertToJSON(RemoteInvokeRequestResponseMapper[RemoteInvokeExecuti def map(self, test_input: RemoteInvokeExecutionInfo) -> RemoteInvokeExecutionInfo: if not test_input.is_file_provided(): + if not test_input.payload: + LOG.debug("Input event not found, invoking Lambda Function with an empty event") + test_input.payload = "{}" LOG.debug("Mapping input Payload to JSON string object") try: _ = json.loads(cast(str, test_input.payload)) diff --git a/samcli/lib/remote_invoke/remote_invoke_executor_factory.py b/samcli/lib/remote_invoke/remote_invoke_executor_factory.py index 129f9302d9..19bf7ff106 100644 --- a/samcli/lib/remote_invoke/remote_invoke_executor_factory.py +++ b/samcli/lib/remote_invoke/remote_invoke_executor_factory.py @@ -89,12 +89,13 @@ def _create_lambda_boto_executor( :return: Returns the created remote invoke Executor """ + LOG.info(f"Invoking Lambda Function {cfn_resource_summary.logical_resource_id}") lambda_client = self._boto_client_provider("lambda") mappers = [] if _is_function_invoke_mode_response_stream(lambda_client, cfn_resource_summary.physical_resource_id): LOG.debug("Creating response stream invocator for function %s", cfn_resource_summary.physical_resource_id) - if remote_invoke_output_format == RemoteInvokeOutputFormat.RAW: + if remote_invoke_output_format == RemoteInvokeOutputFormat.JSON: mappers = [ LambdaStreamResponseConverter(), ResponseObjectToJsonStringMapper(), @@ -110,7 +111,7 @@ def _create_lambda_boto_executor( log_consumer=log_consumer, ) - if remote_invoke_output_format == RemoteInvokeOutputFormat.RAW: + if remote_invoke_output_format == RemoteInvokeOutputFormat.JSON: mappers = [ LambdaResponseConverter(), ResponseObjectToJsonStringMapper(), diff --git a/samcli/lib/remote_invoke/remote_invoke_executors.py b/samcli/lib/remote_invoke/remote_invoke_executors.py index 0c69a9d5bf..76713bfe98 100644 --- a/samcli/lib/remote_invoke/remote_invoke_executors.py +++ b/samcli/lib/remote_invoke/remote_invoke_executors.py @@ -43,8 +43,8 @@ class RemoteInvokeOutputFormat(Enum): Types of output formats used to by remote invoke """ - DEFAULT = "default" - RAW = "raw" + TEXT = "text" + JSON = "json" class RemoteInvokeExecutionInfo: diff --git a/tests/unit/cli/test_types.py b/tests/unit/cli/test_types.py index eed37f0f58..68a285b2ac 100644 --- a/tests/unit/cli/test_types.py +++ b/tests/unit/cli/test_types.py @@ -504,12 +504,12 @@ def test_must_fail_on_invalid_values(self, input): @parameterized.expand( [ ( - "default", - RemoteInvokeOutputFormat.DEFAULT, + "text", + RemoteInvokeOutputFormat.TEXT, ), ( - "raw", - RemoteInvokeOutputFormat.RAW, + "json", + RemoteInvokeOutputFormat.JSON, ), ] ) diff --git a/tests/unit/commands/remote/invoke/core/__init__.py b/tests/unit/commands/remote/invoke/core/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/commands/remote/invoke/core/test_command.py b/tests/unit/commands/remote/invoke/core/test_command.py new file mode 100644 index 0000000000..aa5604156d --- /dev/null +++ b/tests/unit/commands/remote/invoke/core/test_command.py @@ -0,0 +1,87 @@ +import unittest +from unittest.mock import Mock, patch +from samcli.commands.remote.invoke.cli import RemoteInvokeCommand +from samcli.commands.remote.invoke.cli import DESCRIPTION +from tests.unit.cli.test_command import MockFormatter + + +class MockParams: + def __init__(self, rv, name): + self.rv = rv + self.name = name + + def get_help_record(self, ctx): + return self.rv + + +class TestRemoteInvokeCommand(unittest.TestCase): + @patch.object(RemoteInvokeCommand, "get_params") + def test_get_options_remote_invoke_command_text(self, mock_get_params): + ctx = Mock() + ctx.command_path = "sam remote invoke" + ctx.parent.command_path = "sam" + formatter = MockFormatter(scrub_text=True) + # NOTE: One option per option section. + mock_get_params.return_value = [ + MockParams(rv=("--region", "Region"), name="region"), + MockParams(rv=("--stack-name", ""), name="stack_name"), + MockParams(rv=("--parameter", ""), name="parameter"), + MockParams(rv=("--event", ""), name="event"), + MockParams(rv=("--config-file", ""), name="config_file"), + MockParams(rv=("--beta-features", ""), name="beta_features"), + MockParams(rv=("--debug", ""), name="debug"), + ] + + cmd = RemoteInvokeCommand(name="remote invoke", requires_credentials=True, description=DESCRIPTION) + expected_output = { + "Description": [(cmd.description + cmd.description_addendum, "")], + "Examples": [], + "Invoke default lambda function with empty event": [ + ("", ""), + ("$sam remote invoke --stack-name hello-world\x1b[0m", ""), + ], + "Invoke default lambda function with event passed as text input": [ + ("", ""), + ('$sam remote invoke --stack-name hello-world -e \'{"message": "hello!"}\'\x1b[0m', ""), + ], + "Invoke named lambda function with an event file": [ + ("", ""), + ("$sam remote invoke --stack-name hello-world HelloWorldFunction --event-file event.json\x1b[0m", ""), + ], + "Invoke lambda function with event as stdin input": [ + ("", ""), + ('$ echo \'{"message": "hello!"}\' | sam remote invoke HelloWorldFunction --event-file -\x1b[0m', ""), + ], + "Invoke lambda function using lambda ARN and get the full AWS API response": [ + ("", ""), + ( + "$sam remote invoke arn:aws:lambda:us-west-2:123456789012:function:my-function -e <> --output json\x1b[0m", + "", + ), + ], + "Asynchronously invoke lambda function with additional boto parameters": [ + ("", ""), + ( + "$sam remote invoke HelloWorldFunction -e <> --parameter InvocationType=Event --parameter Qualifier=MyQualifier\x1b[0m", + "", + ), + ], + "Dry invoke a lambda function to validate parameter values and user/role permissions": [ + ("", ""), + ( + "$sam remote invoke HelloWorldFunction -e <> --output json --parameter InvocationType=DryRun\x1b[0m", + "", + ), + ], + "Acronyms": [("ARN", "")], + "Infrastructure Options": [("", ""), ("--stack-name", ""), ("", "")], + "Input Event Options": [("", ""), ("--event", ""), ("", "")], + "Additional Options": [("", ""), ("--parameter", ""), ("", "")], + "AWS Credential Options": [("", ""), ("--region", ""), ("", "")], + "Configuration Options": [("", ""), ("--config-file", ""), ("", "")], + "Beta Options": [("", ""), ("--beta-features", ""), ("", "")], + "Other Options": [("", ""), ("--debug", ""), ("", "")], + } + + cmd.format_options(ctx, formatter) + self.assertEqual(formatter.data, expected_output) diff --git a/tests/unit/commands/remote/invoke/core/test_formatter.py b/tests/unit/commands/remote/invoke/core/test_formatter.py new file mode 100644 index 0000000000..b21652f4ac --- /dev/null +++ b/tests/unit/commands/remote/invoke/core/test_formatter.py @@ -0,0 +1,12 @@ +from shutil import get_terminal_size +from unittest import TestCase + +from samcli.cli.row_modifiers import BaseLineRowModifier +from samcli.commands.remote.invoke.core.formatters import RemoteInvokeCommandHelpTextFormatter + + +class TestRemoteInvokeCommandHelpTextFormatter(TestCase): + def test_remote_invoke_formatter(self): + self.formatter = RemoteInvokeCommandHelpTextFormatter() + self.assertTrue(self.formatter.left_justification_length <= get_terminal_size().columns // 2) + self.assertIsInstance(self.formatter.modifiers[0], BaseLineRowModifier) diff --git a/tests/unit/commands/remote/invoke/core/test_options.py b/tests/unit/commands/remote/invoke/core/test_options.py new file mode 100644 index 0000000000..bdbe2649bc --- /dev/null +++ b/tests/unit/commands/remote/invoke/core/test_options.py @@ -0,0 +1,12 @@ +from unittest import TestCase + +from click import Option + +from samcli.commands.remote.invoke.cli import cli +from samcli.commands.remote.invoke.core.options import ALL_OPTIONS + + +class TestOptions(TestCase): + def test_all_options_formatted(self): + command_options = [param.human_readable_name if isinstance(param, Option) else None for param in cli.params] + self.assertEqual(sorted(ALL_OPTIONS), sorted(filter(lambda item: item is not None, command_options + ["help"]))) diff --git a/tests/unit/commands/remote/invoke/test_cli.py b/tests/unit/commands/remote/invoke/test_cli.py index 97aecfc721..fe9179a891 100644 --- a/tests/unit/commands/remote/invoke/test_cli.py +++ b/tests/unit/commands/remote/invoke/test_cli.py @@ -24,14 +24,14 @@ def setUp(self) -> None: @parameterized.expand( [ - ("event", None, RemoteInvokeOutputFormat.DEFAULT, {}, "log-output"), - ("event", None, RemoteInvokeOutputFormat.DEFAULT, {}, None), - ("event", None, RemoteInvokeOutputFormat.DEFAULT, {"Param1": "ParamValue1"}, "log-output"), - ("event", None, RemoteInvokeOutputFormat.RAW, {}, None), - ("event", None, RemoteInvokeOutputFormat.RAW, {"Param1": "ParamValue1"}, "log-output"), - ("event", None, RemoteInvokeOutputFormat.RAW, {"Param1": "ParamValue1"}, None), - (None, "event_file", RemoteInvokeOutputFormat.DEFAULT, {"Param1": "ParamValue1"}, None), - (None, "event_file", RemoteInvokeOutputFormat.RAW, {"Param1": "ParamValue1"}, "log-output"), + ("event", None, RemoteInvokeOutputFormat.TEXT, {}, "log-output"), + ("event", None, RemoteInvokeOutputFormat.TEXT, {}, None), + ("event", None, RemoteInvokeOutputFormat.TEXT, {"Param1": "ParamValue1"}, "log-output"), + ("event", None, RemoteInvokeOutputFormat.JSON, {}, None), + ("event", None, RemoteInvokeOutputFormat.JSON, {"Param1": "ParamValue1"}, "log-output"), + ("event", None, RemoteInvokeOutputFormat.JSON, {"Param1": "ParamValue1"}, None), + (None, "event_file", RemoteInvokeOutputFormat.TEXT, {"Param1": "ParamValue1"}, None), + (None, "event_file", RemoteInvokeOutputFormat.JSON, {"Param1": "ParamValue1"}, "log-output"), ] ) @patch("samcli.lib.remote_invoke.remote_invoke_executors.RemoteInvokeExecutionInfo") @@ -42,7 +42,7 @@ def test_remote_invoke_command( self, event, event_file, - output_format, + output, parameter, log_output, mock_remote_invoke_context, @@ -78,7 +78,7 @@ def test_remote_invoke_command( event=event, event_file=event_file, parameter=parameter, - output_format=output_format, + output=output, region=self.region, profile=self.profile, config_file=self.config_file, @@ -96,7 +96,7 @@ def test_remote_invoke_command( ) patched_remote_invoke_execution_info.assert_called_with( - payload=event, payload_file=event_file, parameters=parameter, output_format=output_format + payload=event, payload_file=event_file, parameters=parameter, output_format=output ) context_mock.run.assert_called_with(remote_invoke_input=given_remote_invoke_execution_info) @@ -121,7 +121,7 @@ def test_raise_user_exception_invoke_not_successfull(self, exeception_to_raise, event="event", event_file=None, parameter={}, - output_format=RemoteInvokeOutputFormat.DEFAULT, + output=RemoteInvokeOutputFormat.TEXT, region=self.region, profile=self.profile, config_file=self.config_file, diff --git a/tests/unit/lib/cli_validation/test_remote_invoke_options_validations.py b/tests/unit/lib/cli_validation/test_remote_invoke_options_validations.py index 17448fc856..5dca536b54 100644 --- a/tests/unit/lib/cli_validation/test_remote_invoke_options_validations.py +++ b/tests/unit/lib/cli_validation/test_remote_invoke_options_validations.py @@ -10,18 +10,22 @@ class TestEventFileValidation(TestCase): + @patch("samcli.lib.cli_validation.remote_invoke_options_validations.sys") @patch("samcli.lib.cli_validation.remote_invoke_options_validations.LOG") @patch("samcli.lib.cli_validation.remote_invoke_options_validations.click.get_current_context") - def test_both_not_provided_params(self, patched_click_context, patched_log): + def test_event_file_provided_as_stdin(self, patched_click_context, patched_log, patched_sys): mock_func = Mock() mocked_context = Mock() patched_click_context.return_value = mocked_context + mock_event_file = Mock() + mock_event_file.fileno.return_value = 0 + patched_sys.stdin.fileno.return_value = 0 - mocked_context.params.get.return_value = {} + mocked_context.params.get.side_effect = lambda key: mock_event_file if key == "event_file" else None event_and_event_file_options_validation(mock_func)() - patched_log.debug.assert_called_with( - "Neither --event nor --event-file options have been provided, reading from stdin" + patched_log.info.assert_called_with( + "Reading event from stdin (you can also pass it from file with --event-file)" ) mock_func.assert_called_once() @@ -39,14 +43,18 @@ def test_only_event_param(self, patched_click_context): mock_func.assert_called_once() + @patch("samcli.lib.cli_validation.remote_invoke_options_validations.sys") @patch("samcli.lib.cli_validation.remote_invoke_options_validations.click.get_current_context") - def test_only_event_file_param(self, patched_click_context): + def test_only_event_file_param(self, patched_click_context, patched_sys): mock_func = Mock() mocked_context = Mock() patched_click_context.return_value = mocked_context + mock_event_file = Mock() + mock_event_file.fileno.return_value = 4 + patched_sys.stdin.fileno.return_value = 0 - mocked_context.params.get.side_effect = lambda key: "event_file" if key == "event_file" else None + mocked_context.params.get.side_effect = lambda key: mock_event_file if key == "event_file" else None event_and_event_file_options_validation(mock_func)() @@ -106,6 +114,8 @@ def test_no_params_provided(self, patched_click_context): with self.assertRaises(BadOptionUsage) as ex: stack_name_or_resource_id_atleast_one_option_validation(mock_func)() - self.assertIn("Atleast 1 of --stack-name or --resource-id parameters should be provided.", ex.exception.message) + self.assertIn( + "At least 1 of --stack-name or --resource-id parameters should be provided.", ex.exception.message + ) mock_func.assert_not_called() diff --git a/tests/unit/lib/remote_invoke/test_lambda_invoke_executors.py b/tests/unit/lib/remote_invoke/test_lambda_invoke_executors.py index dca00cafae..e1a5093a2f 100644 --- a/tests/unit/lib/remote_invoke/test_lambda_invoke_executors.py +++ b/tests/unit/lib/remote_invoke/test_lambda_invoke_executors.py @@ -96,7 +96,7 @@ def setUp(self) -> None: self.lambda_client = Mock() self.function_name = Mock() self.lambda_invoke_executor = LambdaInvokeExecutor( - self.lambda_client, self.function_name, RemoteInvokeOutputFormat.RAW + self.lambda_client, self.function_name, RemoteInvokeOutputFormat.JSON ) def test_execute_action(self): @@ -120,7 +120,7 @@ def setUp(self) -> None: self.lambda_client = Mock() self.function_name = Mock() self.lambda_invoke_executor = LambdaInvokeWithResponseStreamExecutor( - self.lambda_client, self.function_name, RemoteInvokeOutputFormat.RAW + self.lambda_client, self.function_name, RemoteInvokeOutputFormat.JSON ) def test_execute_action(self): @@ -142,10 +142,11 @@ def _get_boto3_method(self): class TestDefaultConvertToJSON(TestCase): def setUp(self) -> None: self.lambda_convert_to_default_json = DefaultConvertToJSON() - self.output_format = RemoteInvokeOutputFormat.DEFAULT + self.output_format = RemoteInvokeOutputFormat.TEXT @parameterized.expand( [ + (None, "{}"), ("Hello World", '"Hello World"'), ('{"message": "hello world"}', '{"message": "hello world"}'), ] @@ -170,7 +171,7 @@ def setUp(self) -> None: self.lambda_response_converter = LambdaResponseConverter() def test_lambda_streaming_body_response_conversion(self): - output_format = RemoteInvokeOutputFormat.DEFAULT + output_format = RemoteInvokeOutputFormat.TEXT given_streaming_body = Mock() given_decoded_string = "decoded string" given_streaming_body.read().decode.return_value = given_decoded_string @@ -185,7 +186,7 @@ def test_lambda_streaming_body_response_conversion(self): self.assertEqual(result.response, expected_result) def test_lambda_streaming_body_invalid_response_exception(self): - output_format = RemoteInvokeOutputFormat.DEFAULT + output_format = RemoteInvokeOutputFormat.TEXT given_streaming_body = Mock() given_decoded_string = "decoded string" given_streaming_body.read().decode.return_value = given_decoded_string @@ -205,7 +206,7 @@ def setUp(self) -> None: [({LOG_RESULT: base64.b64encode(b"log output")}, {LOG_RESULT: base64.b64encode(b"log output")}), ({}, {})] ) def test_lambda_streaming_body_response_conversion(self, invoke_complete_response, mapped_log_response): - output_format = RemoteInvokeOutputFormat.DEFAULT + output_format = RemoteInvokeOutputFormat.TEXT given_test_result = { EVENT_STREAM: [ {PAYLOAD_CHUNK: {PAYLOAD: b"stream1"}}, @@ -229,7 +230,7 @@ def test_lambda_streaming_body_response_conversion(self, invoke_complete_respons self.assertEqual(result.response, expected_result) def test_lambda_streaming_body_invalid_response_exception(self): - output_format = RemoteInvokeOutputFormat.DEFAULT + output_format = RemoteInvokeOutputFormat.TEXT remote_invoke_execution_info = RemoteInvokeExecutionInfo(None, None, {}, output_format) remote_invoke_execution_info.response = Mock() diff --git a/tests/unit/lib/remote_invoke/test_remote_invoke_executor_factory.py b/tests/unit/lib/remote_invoke/test_remote_invoke_executor_factory.py index bbb8c1bac9..57b5e7988c 100644 --- a/tests/unit/lib/remote_invoke/test_remote_invoke_executor_factory.py +++ b/tests/unit/lib/remote_invoke/test_remote_invoke_executor_factory.py @@ -51,7 +51,7 @@ def test_failed_create_test_executor(self): self.assertIsNone(executor) @parameterized.expand( - itertools.product([True, False], [RemoteInvokeOutputFormat.RAW, RemoteInvokeOutputFormat.DEFAULT]) + itertools.product([True, False], [RemoteInvokeOutputFormat.JSON, RemoteInvokeOutputFormat.TEXT]) ) @patch("samcli.lib.remote_invoke.remote_invoke_executor_factory.LambdaInvokeExecutor") @patch("samcli.lib.remote_invoke.remote_invoke_executor_factory.LambdaInvokeWithResponseStreamExecutor") @@ -96,7 +96,7 @@ def test_create_lambda_test_executor( if is_function_invoke_mode_response_stream: expected_mappers = [] - if remote_invoke_output_format == RemoteInvokeOutputFormat.RAW: + if remote_invoke_output_format == RemoteInvokeOutputFormat.JSON: patched_object_to_json_converter.assert_called_once() patched_stream_response_converter.assert_called_once() patched_lambda_invoke_with_response_stream_executor.assert_called_with( @@ -115,7 +115,7 @@ def test_create_lambda_test_executor( ) else: expected_mappers = [] - if remote_invoke_output_format == RemoteInvokeOutputFormat.RAW: + if remote_invoke_output_format == RemoteInvokeOutputFormat.JSON: patched_object_to_json_converter.assert_called_once() patched_response_converter.assert_called_once() patched_lambda_invoke_executor.assert_called_with( diff --git a/tests/unit/lib/remote_invoke/test_remote_invoke_executors.py b/tests/unit/lib/remote_invoke/test_remote_invoke_executors.py index 8f3ce96e46..19589fc2b9 100644 --- a/tests/unit/lib/remote_invoke/test_remote_invoke_executors.py +++ b/tests/unit/lib/remote_invoke/test_remote_invoke_executors.py @@ -17,7 +17,7 @@ class TestRemoteInvokeExecutionInfo(TestCase): def setUp(self) -> None: - self.output_format = RemoteInvokeOutputFormat.DEFAULT + self.output_format = RemoteInvokeOutputFormat.TEXT def test_execution_info_payload(self): given_payload = Mock() @@ -76,7 +76,7 @@ def setUp(self) -> None: def test_execute_with_payload(self): given_payload = Mock() given_parameters = {"ExampleParameter": "ExampleValue"} - given_output_format = "default" + given_output_format = "text" test_execution_info = RemoteInvokeExecutionInfo(given_payload, None, given_parameters, given_output_format) with patch.object(self.boto_action_executor, "_execute_action") as patched_execute_action, patch.object( @@ -93,7 +93,7 @@ def test_execute_with_payload(self): def test_execute_with_payload_file(self): given_payload_file = Mock() given_parameters = {"ExampleParameter": "ExampleValue"} - given_output_format = "original-boto-response" + given_output_format = "json" test_execution_info = RemoteInvokeExecutionInfo(None, given_payload_file, given_parameters, given_output_format) with patch.object(self.boto_action_executor, "_execute_action") as patched_execute_action, patch.object( @@ -110,7 +110,7 @@ def test_execute_with_payload_file(self): def test_execute_error(self): given_payload = Mock() given_parameters = {"ExampleParameter": "ExampleValue"} - given_output_format = "original-boto-response" + given_output_format = "json" test_execution_info = RemoteInvokeExecutionInfo(given_payload, None, given_parameters, given_output_format) with patch.object(self.boto_action_executor, "_execute_action") as patched_execute_action: @@ -143,7 +143,7 @@ def setUp(self) -> None: def test_execution(self): given_payload = Mock() given_parameters = {"ExampleParameter": "ExampleValue"} - given_output_format = RemoteInvokeOutputFormat.RAW + given_output_format = RemoteInvokeOutputFormat.JSON test_execution_info = RemoteInvokeExecutionInfo(given_payload, None, given_parameters, given_output_format) validate_action_parameters_function = Mock() self.mock_boto_action_executor.validate_action_parameters = validate_action_parameters_function @@ -162,7 +162,7 @@ def test_execution(self): def test_execution_failure(self): given_payload = Mock() given_parameters = {"ExampleParameter": "ExampleValue"} - given_output_format = RemoteInvokeOutputFormat.RAW + given_output_format = RemoteInvokeOutputFormat.JSON test_execution_info = RemoteInvokeExecutionInfo(given_payload, None, given_parameters, given_output_format) validate_action_parameters_function = Mock() self.mock_boto_action_executor.validate_action_parameters = validate_action_parameters_function @@ -186,7 +186,7 @@ def test_execution_failure(self): class TestResponseObjectToJsonStringMapper(TestCase): def test_mapper(self): - output_format = RemoteInvokeOutputFormat.DEFAULT + output_format = RemoteInvokeOutputFormat.TEXT given_object = [{"key": "value", "key2": 123}] test_execution_info = RemoteInvokeExecutionInfo(None, None, {}, output_format) test_execution_info.response = given_object