Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ init:
fi

test:
# Run unit tests
# Fail if coverage falls below 95%
# Run unit tests and fail if coverage falls below 94%
pytest --cov samcli --cov schema --cov-report term-missing --cov-fail-under 94 tests/unit

test-cov-report:
Expand Down
9 changes: 7 additions & 2 deletions samcli/commands/remote/invoke/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,15 @@ def do_cli(
and remote_invoke_context.resource_summary
and remote_invoke_context.resource_summary.resource_type == AWS_LAMBDA_FUNCTION
):
lambda_test_event = remote_invoke_context.get_lambda_shared_test_event_provider()
LOG.debug("Retrieving remote event %s", test_event_name)
event = lambda_test_event.get_event(test_event_name, remote_invoke_context.resource_summary)
lambda_test_event = remote_invoke_context.get_lambda_shared_test_event_provider().get_event(
test_event_name, remote_invoke_context.resource_summary
)
event = lambda_test_event["json"]
LOG.debug("Remote event contents: %s", event)
metadata = lambda_test_event["metadata"]
if "invocationType" in metadata:
parameter.setdefault("InvocationType", metadata["invocationType"])
elif test_event_name:
LOG.info("Note: remote event is only supported for AWS Lambda Function resource.")
test_event_name = ""
Expand Down
2 changes: 1 addition & 1 deletion samcli/commands/remote/test_event/get/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ def do_cli(
raise UserException(str(ex), wrapped_from=ex.__class__.__name__) from ex

LOG.debug("Getting remote event '%s' for resource: %s", name, function_resource)
event = lambda_test_event.get_event(name, function_resource)
event = lambda_test_event.get_event(name, function_resource)["json"]

try:
output_file.write(event)
Expand Down
15 changes: 14 additions & 1 deletion samcli/commands/remote/test_event/put/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from samcli.cli.cli_config_file import ConfigProvider, configuration_option
from samcli.cli.context import Context
from samcli.cli.main import aws_creds_options, common_options, pass_context, print_cmdline_args
from samcli.commands._utils.click_mutex import ClickMutex
from samcli.commands._utils.command_exception_handler import command_exception_handler
from samcli.commands.remote.exceptions import IllFormedEventData
from samcli.commands.remote.test_event.put.core.command import RemoteTestEventPutCommand
Expand All @@ -19,6 +20,7 @@
stack_name_or_resource_id_atleast_one_option_validation,
)
from samcli.lib.telemetry.metric import track_command
from samcli.lib.utils.invocation_type import EVENT, REQUEST_RESPONSE
from samcli.lib.utils.version_checker import check_newer_version
from samcli.yamlhelper import yaml_parse

Expand Down Expand Up @@ -56,6 +58,14 @@
"The option '--file' is required (You can provide '-' to read from stdin)"
),
)
@click.option(
"--invocation-type",
type=click.Choice([EVENT, REQUEST_RESPONSE]),
replace_help_option="--invocation-type INVOCATION_TYPE",
help="Lambda function invocation type."
+ click.style(f"\n\nInvocation types: {[EVENT, REQUEST_RESPONSE]}", bold=True),
cls=ClickMutex,
)
@click.option("--force", "-f", is_flag=True, help="Force saving the event even if it already exists")
@click.argument("resource-id", required=False)
@aws_creds_options
Expand All @@ -72,6 +82,7 @@ def cli(
resource_id: Optional[str],
name: str,
file: TextIOWrapper,
invocation_type: str,
force: bool,
config_file: str,
config_env: str,
Expand All @@ -84,6 +95,7 @@ def cli(
resource_id,
name,
file,
invocation_type,
force,
ctx.region,
ctx.profile,
Expand All @@ -97,6 +109,7 @@ def do_cli(
resource_id: Optional[str],
name: str,
file: TextIOWrapper,
invocation_type: str,
force: bool,
region: str,
profile: str,
Expand Down Expand Up @@ -147,7 +160,7 @@ def do_cli(
data = yaml_parse(contents)
event_data = json.dumps(data)
LOG.debug("Creating remote event %s from resource: %s", name, function_resource)
lambda_test_event.create_event(name, function_resource, event_data, force=force)
lambda_test_event.create_event(name, function_resource, event_data, invocation_type, force=force)
click.echo(f"Put remote event '{name}' completed successfully")
except (ValueError, YAMLError, JSONDecodeError) as ex:
raise IllFormedEventData(f"File {file.name} doesn't contain a valid JSON:\n {ex}") from ex
2 changes: 1 addition & 1 deletion samcli/commands/remote/test_event/put/core/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
# NOTE: The ordering of the option lists matter, they are the order
# in which options will be displayed.

EVENT_OPTIONS: List[str] = ["name", "file", "force"]
EVENT_OPTIONS: List[str] = ["name", "file", "invocation_type", "force"]

ALL_OPTIONS: List[str] = (
INFRASTRUCTURE_OPTION_NAMES
Expand Down
34 changes: 25 additions & 9 deletions samcli/lib/shared_test_events/lambda_shared_test_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import json
import logging
from json import JSONDecodeError
from typing import Any
from typing import Any, Optional

import botocore

Expand All @@ -17,6 +17,7 @@
)
from samcli.lib.schemas.schemas_api_caller import SchemasApiCaller
from samcli.lib.utils.cloudformation import CloudFormationResourceSummary
from samcli.lib.utils.invocation_type import REQUEST_RESPONSE
from samcli.lib.utils.resources import AWS_LAMBDA_FUNCTION

LAMBDA_TEST_EVENT_REGISTRY = "lambda-testevent-schemas"
Expand Down Expand Up @@ -142,7 +143,14 @@ def limit_versions(self, schema_name: str, registry_name: str = LAMBDA_TEST_EVEN
)
self._api_caller.delete_version(registry_name, schema_name, version_to_delete)

def add_event_to_schema(self, schema: str, event: str, event_name: str, replace_if_exists: bool = False) -> str:
def add_event_to_schema(
self,
schema: str,
event: str,
event_name: str,
metadata: Optional[dict[str, str]],
replace_if_exists: bool = False,
) -> str:
"""
Adds an event to a schema

Expand Down Expand Up @@ -176,6 +184,8 @@ def add_event_to_schema(self, schema: str, event: str, event_name: str, replace_
schema_dict["components"]["examples"][event_name] = {
"value": event_dict,
}
if metadata:
schema_dict["components"]["examples"][event_name]["x-metadata"] = metadata

schema = json.dumps(schema_dict)

Expand All @@ -200,7 +210,7 @@ def get_event_from_schema(self, schema: str, event_name: str) -> Any:
the name of the event to retrieve from the schema
Returns
-------
The event data of the event "event_name"
The event data and metadata of the event "event_name"
"""
try:
schema_dict = json.loads(schema)
Expand All @@ -211,7 +221,7 @@ def get_event_from_schema(self, schema: str, event_name: str) -> Any:
raise ResourceNotFound(f"Event {event_name} not found")

# can use square brackets here since we know each of these subdicts exist
return existing_events[event_name]["value"]
return existing_events[event_name]

except JSONDecodeError as ex:
LOG.error("Error decoding schema when getting event %s", event_name)
Expand Down Expand Up @@ -263,6 +273,7 @@ def create_event(
event_name: str,
function_resource: CloudFormationResourceSummary,
event_data: str,
invocation_type: str = REQUEST_RESPONSE,
force: bool = False,
registry_name: str = LAMBDA_TEST_EVENT_REGISTRY,
) -> str:
Expand All @@ -277,6 +288,8 @@ def create_event(
The function where the event will be created
event_data: str
The JSON data of the event to be created
invocation_type: str
The Lambda invocation type of the event to be created
registry_name: str
The name of the registry that contains the schema
"""
Expand All @@ -287,16 +300,19 @@ def create_event(

schema_name = self._get_schema_name(function_resource)
original_schema = api_caller.get_schema(registry_name, schema_name)
metadata = {"invocationType": invocation_type}

if original_schema:
LOG.debug("Schema %s already exists, adding event %s", schema_name, event_name)
self.limit_versions(schema_name, registry_name)
schema = self.add_event_to_schema(original_schema, event_data, event_name, replace_if_exists=force)
schema = self.add_event_to_schema(
original_schema, event_data, event_name, metadata, replace_if_exists=force
)
api_caller.update_schema(schema, registry_name, schema_name, OPEN_API_TYPE)
else:
LOG.debug("Schema %s does not already exist, creating new", schema_name)
schema = api_caller.discover_schema(event_data, OPEN_API_TYPE)
schema = self.add_event_to_schema(schema, event_data, event_name)
schema = self.add_event_to_schema(schema, event_data, event_name, metadata)
api_caller.create_schema(schema, registry_name, schema_name, OPEN_API_TYPE)

return schema
Expand Down Expand Up @@ -346,7 +362,7 @@ def get_event(
event_name: str,
function_resource: CloudFormationResourceSummary,
registry_name: str = LAMBDA_TEST_EVENT_REGISTRY,
) -> str:
) -> Any:
"""
Returns a remote test event

Expand All @@ -360,7 +376,7 @@ def get_event(
The name of the registry that contains the schema
Returns
-------
The JSON data of the event
The parsed JSON data and metadata of the event
"""
api_caller = self._api_caller

Expand All @@ -377,7 +393,7 @@ def get_event(
event = self.get_event_from_schema(schema, event_name)

try:
return json.dumps(event)
return {"json": json.dumps(event["value"]), "metadata": event.get("x-metadata", {})}

except JSONDecodeError as ex:
LOG.error("Error parsing event %s from schema %s", event_name, schema_name)
Expand Down
6 changes: 6 additions & 0 deletions samcli/lib/utils/invocation_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""
Enums for determining InvocationType of a Lambda function invoke.
"""

EVENT = "Event"
REQUEST_RESPONSE = "RequestResponse"
138 changes: 137 additions & 1 deletion tests/unit/commands/remote/invoke/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ def test_remote_invoke_with_shared_test_event_command(

context_mock = Mock()
test_event_mock = Mock()
test_event_mock.get_event.return_value = "stuff"
test_event_mock.get_event.return_value = {"json": "stuff", "metadata": {}}
fn_resource = Mock()
fn_resource.resource_type = AWS_LAMBDA_FUNCTION
context_mock.resource_summary = fn_resource
Expand Down Expand Up @@ -191,6 +191,142 @@ def mock_tracker(name, value): # when track_event is called, append an equivale
# Assert metric was emitted
self.assertIn(["RemoteInvokeEventType", "remote_event"], tracked_events)

@patch("samcli.lib.remote_invoke.remote_invoke_executors.RemoteInvokeExecutionInfo")
@patch("samcli.lib.utils.boto_utils.get_boto_client_provider_with_config")
@patch("samcli.lib.utils.boto_utils.get_boto_resource_provider_with_config")
@patch("samcli.commands.remote.remote_invoke_context.RemoteInvokeContext")
@patch("samcli.lib.telemetry.event.EventTracker.track_event")
def test_remote_invoke_with_shared_test_event_metadata_command(
self,
mock_track_event,
mock_remote_invoke_context,
patched_get_boto_resource_provider_with_config,
patched_get_boto_client_provider_with_config,
patched_remote_invoke_execution_info,
):
given_client_provider = Mock()
patched_get_boto_client_provider_with_config.return_value = given_client_provider

given_resource_provider = Mock()
patched_get_boto_resource_provider_with_config.return_value = given_resource_provider

given_remote_invoke_execution_info = Mock()
patched_remote_invoke_execution_info.return_value = given_remote_invoke_execution_info

context_mock = Mock()
test_event_mock = Mock()
test_event_mock.get_event.return_value = {"json": "stuff", "metadata": {"invocationType": "Event"}}
fn_resource = Mock()
fn_resource.resource_type = AWS_LAMBDA_FUNCTION
context_mock.resource_summary = fn_resource
context_mock.get_lambda_shared_test_event_provider.return_value = test_event_mock
mock_remote_invoke_context.return_value.__enter__.return_value = context_mock

given_remote_invoke_result = Mock()
given_remote_invoke_result.is_succeeded.return_value = True
given_remote_invoke_result.log_output = "log_output"
context_mock.run.return_value = given_remote_invoke_result

tracked_events = []

def mock_tracker(name, value): # when track_event is called, append an equivalent event to our list
tracked_events.append([name, value])

mock_track_event.side_effect = mock_tracker

do_cli(
stack_name=self.stack_name,
resource_id=self.resource_id,
event=None,
event_file=None,
parameter={},
output=RemoteInvokeOutputFormat.TEXT,
test_event_name="event1",
region=self.region,
profile=self.profile,
config_file=self.config_file,
config_env=self.config_env,
)

test_event_mock.get_event.assert_called_with("event1", fn_resource)
patched_remote_invoke_execution_info.assert_called_with(
payload="stuff",
payload_file=None,
parameters={"InvocationType": "Event"},
output_format=RemoteInvokeOutputFormat.TEXT,
)
context_mock.run.assert_called_with(remote_invoke_input=given_remote_invoke_execution_info)
# Assert metric was emitted
self.assertIn(["RemoteInvokeEventType", "remote_event"], tracked_events)

@patch("samcli.lib.remote_invoke.remote_invoke_executors.RemoteInvokeExecutionInfo")
@patch("samcli.lib.utils.boto_utils.get_boto_client_provider_with_config")
@patch("samcli.lib.utils.boto_utils.get_boto_resource_provider_with_config")
@patch("samcli.commands.remote.remote_invoke_context.RemoteInvokeContext")
@patch("samcli.lib.telemetry.event.EventTracker.track_event")
def test_remote_invoke_with_shared_test_event_metadata_and_cli_overrides_command(
self,
mock_track_event,
mock_remote_invoke_context,
patched_get_boto_resource_provider_with_config,
patched_get_boto_client_provider_with_config,
patched_remote_invoke_execution_info,
):
given_client_provider = Mock()
patched_get_boto_client_provider_with_config.return_value = given_client_provider

given_resource_provider = Mock()
patched_get_boto_resource_provider_with_config.return_value = given_resource_provider

given_remote_invoke_execution_info = Mock()
patched_remote_invoke_execution_info.return_value = given_remote_invoke_execution_info

context_mock = Mock()
test_event_mock = Mock()
test_event_mock.get_event.return_value = {"json": "stuff", "metadata": {"invocationType": "Event"}}
fn_resource = Mock()
fn_resource.resource_type = AWS_LAMBDA_FUNCTION
context_mock.resource_summary = fn_resource
context_mock.get_lambda_shared_test_event_provider.return_value = test_event_mock
mock_remote_invoke_context.return_value.__enter__.return_value = context_mock

given_remote_invoke_result = Mock()
given_remote_invoke_result.is_succeeded.return_value = True
given_remote_invoke_result.log_output = "log_output"
context_mock.run.return_value = given_remote_invoke_result

tracked_events = []

def mock_tracker(name, value): # when track_event is called, append an equivalent event to our list
tracked_events.append([name, value])

mock_track_event.side_effect = mock_tracker

do_cli(
stack_name=self.stack_name,
resource_id=self.resource_id,
event=None,
event_file=None,
parameter={"InvocationType": "RequestResponse"},
output=RemoteInvokeOutputFormat.TEXT,
test_event_name="event1",
region=self.region,
profile=self.profile,
config_file=self.config_file,
config_env=self.config_env,
)

test_event_mock.get_event.assert_called_with("event1", fn_resource)
patched_remote_invoke_execution_info.assert_called_with(
payload="stuff",
payload_file=None,
parameters={"InvocationType": "RequestResponse"},
output_format=RemoteInvokeOutputFormat.TEXT,
)
context_mock.run.assert_called_with(remote_invoke_input=given_remote_invoke_execution_info)
# Assert metric was emitted
self.assertIn(["RemoteInvokeEventType", "remote_event"], tracked_events)

@patch("samcli.lib.remote_invoke.remote_invoke_executors.RemoteInvokeExecutionInfo")
@patch("samcli.lib.utils.boto_utils.get_boto_client_provider_with_config")
@patch("samcli.lib.utils.boto_utils.get_boto_resource_provider_with_config")
Expand Down
Loading
Loading