diff --git a/localstack-core/localstack/services/events/provider.py b/localstack-core/localstack/services/events/provider.py index 0ca9e58d35420..0e134a1c88e6f 100644 --- a/localstack-core/localstack/services/events/provider.py +++ b/localstack-core/localstack/services/events/provider.py @@ -2,12 +2,19 @@ import json import logging import re -from typing import Callable, Optional +import uuid +from datetime import datetime +from typing import Any, Callable, Dict, Optional from localstack.aws.api import RequestContext, handler from localstack.aws.api.config import TagsList from localstack.aws.api.events import ( Action, + ApiDestination, + ApiDestinationDescription, + ApiDestinationHttpMethod, + ApiDestinationInvocationRateLimitPerSecond, + ApiDestinationName, ArchiveDescription, ArchiveName, ArchiveResponseList, @@ -16,11 +23,24 @@ Boolean, CancelReplayResponse, Condition, + Connection, + ConnectionArn, + ConnectionAuthorizationType, + ConnectionDescription, + ConnectionName, + ConnectionState, + CreateApiDestinationResponse, CreateArchiveResponse, + CreateConnectionAuthRequestParameters, + CreateConnectionResponse, CreateEventBusResponse, DeadLetterConfig, + DeleteApiDestinationResponse, DeleteArchiveResponse, + DeleteConnectionResponse, + DescribeApiDestinationResponse, DescribeArchiveResponse, + DescribeConnectionResponse, DescribeEventBusResponse, DescribeReplayResponse, DescribeRuleResponse, @@ -32,11 +52,14 @@ EventPattern, EventsApi, EventSourceName, + HttpsEndpoint, InternalException, InvalidEventPatternException, KmsKeyIdentifier, LimitMax100, + ListApiDestinationsResponse, ListArchivesResponse, + ListConnectionsResponse, ListEventBusesResponse, ListReplaysResponse, ListRuleNamesByTargetResponse, @@ -84,7 +107,10 @@ TestEventPatternResponse, Timestamp, UntagResourceResponse, + UpdateApiDestinationResponse, UpdateArchiveResponse, + UpdateConnectionAuthRequestParameters, + UpdateConnectionResponse, ) from localstack.aws.api.events import Archive as ApiTypeArchive from localstack.aws.api.events import EventBus as ApiTypeEventBus @@ -132,13 +158,15 @@ from localstack.services.plugins import ServiceLifecycleHook from localstack.utils.common import truncate from localstack.utils.event_matcher import matches_event -from localstack.utils.strings import long_uid +from localstack.utils.strings import long_uid, short_uid from localstack.utils.time import TIMESTAMP_FORMAT_TZ, timestamp LOG = logging.getLogger(__name__) ARCHIVE_TARGET_ID_NAME_PATTERN = re.compile(r"^Events-Archive-(?P[a-zA-Z0-9_-]+)$") +VALID_AUTH_TYPES = [t.value for t in ConnectionAuthorizationType] + def decode_next_token(token: NextToken) -> int: """Decode a pagination token from base64 to integer.""" @@ -188,6 +216,8 @@ def __init__(self): self._target_sender_store: TargetSenderDict = {} self._archive_service_store: ArchiveServiceDict = {} self._replay_service_store: ReplayServiceDict = {} + self._connections: Dict[str, Connection] = {} + self._api_destinations: Dict[str, ApiDestination] = {} def on_before_start(self): JobScheduler.start() @@ -195,6 +225,588 @@ def on_before_start(self): def on_before_stop(self): JobScheduler.shutdown() + ########## + # Helper Methods for connections and api destinations + ########## + + def _validate_api_destination_name(self, name: str) -> None: + """Validate the API destination name according to AWS rules.""" + if not re.match(r"^[\.\-_A-Za-z0-9]+$", name): + raise ValidationException( + f"1 validation error detected: Value '{name}' at 'name' failed to satisfy constraint: " + "Member must satisfy regular expression pattern: [\\.\\-_A-Za-z0-9]+" + ) + if not (1 <= len(name) <= 64): + raise ValidationException( + f"1 validation error detected: Value '{name}' at 'name' failed to satisfy constraint: " + "Member must have length between 1 and 64" + ) + + def _validate_connection_name(self, name: str) -> None: + """Validate the connection name according to AWS rules.""" + if not re.match("^[\\.\\-_A-Za-z0-9]+$", name): + raise ValidationException( + f"1 validation error detected: Value '{name}' at 'name' failed to satisfy constraint: " + "Member must satisfy regular expression pattern: [\\.\\-_A-Za-z0-9]+" + ) + + def _validate_auth_type(self, auth_type: str) -> None: + """Validate the authorization type is one of the allowed values.""" + if auth_type not in VALID_AUTH_TYPES: + raise ValidationException( + f"1 validation error detected: Value '{auth_type}' at 'authorizationType' failed to satisfy constraint: " + f"Member must satisfy enum value set: [{', '.join(VALID_AUTH_TYPES)}]" + ) + + def _get_connection_by_arn(self, connection_arn: str) -> Optional[Dict]: + """Retrieve a connection by its ARN.""" + return next( + ( + conn + for conn in self._connections.values() + if conn["ConnectionArn"] == connection_arn + ), + None, + ) + + def _get_public_parameters(self, auth_type: str, auth_parameters: dict) -> dict: + """Extract public parameters (without secrets) based on auth type.""" + public_params = {} + + if auth_type == "BASIC" and "BasicAuthParameters" in auth_parameters: + public_params["BasicAuthParameters"] = { + "Username": auth_parameters["BasicAuthParameters"]["Username"] + } + + elif auth_type == "API_KEY" and "ApiKeyAuthParameters" in auth_parameters: + public_params["ApiKeyAuthParameters"] = { + "ApiKeyName": auth_parameters["ApiKeyAuthParameters"]["ApiKeyName"] + } + + elif auth_type == "OAUTH_CLIENT_CREDENTIALS" and "OAuthParameters" in auth_parameters: + oauth_params = auth_parameters["OAuthParameters"] + public_params["OAuthParameters"] = { + "AuthorizationEndpoint": oauth_params["AuthorizationEndpoint"], + "HttpMethod": oauth_params["HttpMethod"], + "ClientParameters": {"ClientID": oauth_params["ClientParameters"]["ClientID"]}, + } + + if "InvocationHttpParameters" in auth_parameters: + public_params["InvocationHttpParameters"] = auth_parameters["InvocationHttpParameters"] + + return public_params + + def _get_initial_state(self, auth_type: str) -> ConnectionState: + """Get initial connection state based on auth type.""" + if auth_type == "OAUTH_CLIENT_CREDENTIALS": + return ConnectionState.AUTHORIZING + return ConnectionState.AUTHORIZED + + def _determine_api_destination_state(self, connection_state: str) -> str: + """Determine ApiDestinationState based on ConnectionState.""" + return "ACTIVE" if connection_state == "AUTHORIZED" else "INACTIVE" + + def _create_api_destination_object( + self, + context: RequestContext, + name: str, + connection_arn: str, + invocation_endpoint: str, + http_method: str, + description: Optional[str] = None, + invocation_rate_limit_per_second: Optional[int] = None, + api_destination_state: Optional[str] = "ACTIVE", + ) -> ApiDestination: + """Create a standardized API destination object.""" + now = datetime.utcnow() + api_destination_arn = f"arn:aws:events:{context.region}:{context.account_id}:api-destination/{name}/{short_uid()}" + + api_destination: ApiDestination = { + "ApiDestinationArn": api_destination_arn, + "Name": name, + "ConnectionArn": connection_arn, + "InvocationEndpoint": invocation_endpoint, + "HttpMethod": http_method, + "Description": description, + "InvocationRateLimitPerSecond": invocation_rate_limit_per_second or 300, + "CreationTime": now, + "LastModifiedTime": now, + "ApiDestinationState": api_destination_state, + } + return api_destination + + def _create_connection_arn( + self, context: RequestContext, name: str, connection_uuid: str + ) -> str: + """Create a standardized connection ARN.""" + return f"arn:aws:events:{context.region}:{context.account_id}:connection/{name}/{connection_uuid}" + + def _create_secret_arn(self, context: RequestContext, name: str, connection_uuid: str) -> str: + """Create a standardized secret ARN.""" + return f"arn:aws:secretsmanager:{context.region}:{context.account_id}:secret:events!connection/{name}/{connection_uuid}" + + def _create_connection_object( + self, + context: RequestContext, + name: str, + authorization_type: str, + auth_parameters: dict, + description: Optional[str] = None, + connection_state: Optional[str] = None, + creation_time: Optional[datetime] = None, + ) -> Dict[str, Any]: + """Create a standardized connection object.""" + current_time = creation_time or datetime.utcnow() + connection_uuid = str(uuid.uuid4()) + + connection: Dict[str, Any] = { + "ConnectionArn": self._create_connection_arn(context, name, connection_uuid), + "Name": name, + "ConnectionState": connection_state or self._get_initial_state(authorization_type), + "AuthorizationType": authorization_type, + "AuthParameters": self._get_public_parameters(authorization_type, auth_parameters), + "SecretArn": self._create_secret_arn(context, name, connection_uuid), + "CreationTime": current_time, + "LastModifiedTime": current_time, + "LastAuthorizedTime": current_time, + } + + if description: + connection["Description"] = description + + return connection + + def _handle_api_destination_operation(self, operation_name: str, func: Callable) -> Any: + """Generic error handler for API destination operations.""" + try: + return func() + except ( + ValidationException, + ResourceNotFoundException, + ResourceAlreadyExistsException, + ) as e: + raise e + except Exception as e: + raise ValidationException(f"Error {operation_name} API destination: {str(e)}") + + def _handle_connection_operation(self, operation_name: str, func: Callable) -> Any: + """Generic error handler for connection operations.""" + try: + return func() + except ( + ValidationException, + ResourceNotFoundException, + ResourceAlreadyExistsException, + ) as e: + raise e + except Exception as e: + raise ValidationException(f"Error {operation_name} connection: {str(e)}") + + def _create_connection_response( + self, connection: Dict[str, Any], override_state: Optional[str] = None + ) -> dict: + """Create a standardized response for connection operations.""" + response = { + "ConnectionArn": connection["ConnectionArn"], + "ConnectionState": override_state or connection["ConnectionState"], + "CreationTime": connection["CreationTime"], + "LastModifiedTime": connection["LastModifiedTime"], + "LastAuthorizedTime": connection.get("LastAuthorizedTime"), + } + if "SecretArn" in connection: + response["SecretArn"] = connection["SecretArn"] + return response + + ########## + # Connections + ########## + + @handler("CreateConnection") + def create_connection( + self, + context: RequestContext, + name: ConnectionName, + authorization_type: ConnectionAuthorizationType, + auth_parameters: CreateConnectionAuthRequestParameters, + description: ConnectionDescription = None, + **kwargs, + ) -> CreateConnectionResponse: + def create(): + auth_type = authorization_type + if hasattr(authorization_type, "value"): + auth_type = authorization_type.value + self._validate_auth_type(auth_type) + self._validate_connection_name(name) + + if name in self._connections: + raise ResourceAlreadyExistsException(f"Connection {name} already exists.") + + connection = self._create_connection_object( + context, name, auth_type, auth_parameters, description + ) + self._connections[name] = connection + + return CreateConnectionResponse(**self._create_connection_response(connection)) + + return self._handle_connection_operation("creating", create) + + @handler("DescribeConnection") + def describe_connection( + self, context: RequestContext, name: ConnectionName, **kwargs + ) -> DescribeConnectionResponse: + try: + if name not in self._connections: + raise ResourceNotFoundException( + f"Failed to describe the connection(s). Connection '{name}' does not exist." + ) + + return DescribeConnectionResponse(**self._connections[name]) + + except ResourceNotFoundException as e: + raise e + except Exception as e: + raise ValidationException(f"Error describing connection: {str(e)}") + + @handler("UpdateConnection") + def update_connection( + self, + context: RequestContext, + name: ConnectionName, + description: ConnectionDescription = None, + authorization_type: ConnectionAuthorizationType = None, + auth_parameters: UpdateConnectionAuthRequestParameters = None, + **kwargs, + ) -> UpdateConnectionResponse: + def update(): + if name not in self._connections: + raise ResourceNotFoundException( + f"Failed to describe the connection(s). Connection '{name}' does not exist." + ) + + existing_connection = self._connections[name] + + # Use existing values if not provided in update + if authorization_type: + auth_type = ( + authorization_type.value + if hasattr(authorization_type, "value") + else authorization_type + ) + self._validate_auth_type(auth_type) + else: + auth_type = existing_connection["AuthorizationType"] + + auth_params = ( + auth_parameters if auth_parameters else existing_connection["AuthParameters"] + ) + desc = description if description else existing_connection.get("Description") + + connection = self._create_connection_object( + context, + name, + auth_type, + auth_params, + desc, + ConnectionState.AUTHORIZED, + existing_connection["CreationTime"], + ) + self._connections[name] = connection + + return UpdateConnectionResponse(**self._create_connection_response(connection)) + + return self._handle_connection_operation("updating", update) + + @handler("DeleteConnection") + def delete_connection( + self, context: RequestContext, name: ConnectionName, **kwargs + ) -> DeleteConnectionResponse: + def delete(): + if name not in self._connections: + raise ResourceNotFoundException( + f"Failed to describe the connection(s). Connection '{name}' does not exist." + ) + + connection = self._connections[name] + del self._connections[name] + + return DeleteConnectionResponse( + **self._create_connection_response(connection, ConnectionState.DELETING) + ) + + return self._handle_connection_operation("deleting", delete) + + @handler("ListConnections") + def list_connections( + self, + context: RequestContext, + name_prefix: ConnectionName = None, + connection_state: ConnectionState = None, + next_token: NextToken = None, + limit: LimitMax100 = None, + **kwargs, + ) -> ListConnectionsResponse: + try: + connections = [] + + for conn in self._connections.values(): + if name_prefix and not conn["Name"].startswith(name_prefix): + continue + + if connection_state and conn["ConnectionState"] != connection_state: + continue + + connection_summary = { + "ConnectionArn": conn["ConnectionArn"], + "ConnectionState": conn["ConnectionState"], + "CreationTime": conn["CreationTime"], + "LastAuthorizedTime": conn.get("LastAuthorizedTime"), + "LastModifiedTime": conn["LastModifiedTime"], + "Name": conn["Name"], + "AuthorizationType": conn["AuthorizationType"], + } + connections.append(connection_summary) + + connections.sort(key=lambda x: x["CreationTime"]) + + if limit: + connections = connections[:limit] + + return ListConnectionsResponse(Connections=connections) + + except Exception as e: + raise ValidationException(f"Error listing connections: {str(e)}") + + ########## + # API Destinations + ########## + + @handler("CreateApiDestination") + def create_api_destination( + self, + context: RequestContext, + name: ApiDestinationName, + connection_arn: ConnectionArn, + invocation_endpoint: HttpsEndpoint, + http_method: ApiDestinationHttpMethod, + description: ApiDestinationDescription = None, + invocation_rate_limit_per_second: ApiDestinationInvocationRateLimitPerSecond = None, + **kwargs, + ) -> CreateApiDestinationResponse: + def create(): + validation_errors = [] + if not re.match(r"^[\.\-_A-Za-z0-9]+$", name): + validation_errors.append( + f"Value '{name}' at 'name' failed to satisfy constraint: " + "Member must satisfy regular expression pattern: [\\.\\-_A-Za-z0-9]+" + ) + if not (1 <= len(name) <= 64): + validation_errors.append( + f"Value '{name}' at 'name' failed to satisfy constraint: " + "Member must have length between 1 and 64" + ) + + connection_arn_pattern = r"^arn:aws([a-z]|\-)*:events:[a-z0-9\-]+:\d{12}:connection/[\.\-_A-Za-z0-9]+/[\-A-Za-z0-9]+$" + if not re.match(connection_arn_pattern, connection_arn): + validation_errors.append( + f"Value '{connection_arn}' at 'connectionArn' failed to satisfy constraint: " + "Member must satisfy regular expression pattern: " + "^arn:aws([a-z]|\\-)*:events:([a-z]|\\d|\\-)*:([0-9]{12})?:connection\\/[\\.\\-_A-Za-z0-9]+\\/[\\-A-Za-z0-9]+$" + ) + + allowed_methods = ["HEAD", "POST", "PATCH", "DELETE", "PUT", "GET", "OPTIONS"] + if http_method not in allowed_methods: + validation_errors.append( + f"Value '{http_method}' at 'httpMethod' failed to satisfy constraint: " + f"Member must satisfy enum value set: [{', '.join(allowed_methods)}]" + ) + + endpoint_pattern = ( + r"^((%[0-9A-Fa-f]{2}|[-()_.!~*';/?:@&=+$,A-Za-z0-9])+)([).!';/?:,])?$" + ) + if not re.match(endpoint_pattern, invocation_endpoint): + validation_errors.append( + f"Value '{invocation_endpoint}' at 'invocationEndpoint' failed to satisfy constraint: " + "Member must satisfy regular expression pattern: " + "^((%[0-9A-Fa-f]{2}|[-()_.!~*';/?:@&=+$,A-Za-z0-9])+)([).!';/?:,])?$" + ) + + if validation_errors: + error_message = f"{len(validation_errors)} validation error{'s' if len(validation_errors) > 1 else ''} detected: " + error_message += "; ".join(validation_errors) + raise ValidationException(error_message) + + if name in self._api_destinations: + raise ResourceAlreadyExistsException(f"An api-destination '{name}' already exists.") + + connection = self._get_connection_by_arn(connection_arn) + if not connection: + raise ResourceNotFoundException(f"Connection '{connection_arn}' does not exist.") + + api_destination_state = self._determine_api_destination_state( + connection["ConnectionState"] + ) + + api_destination = self._create_api_destination_object( + context, + name, + connection_arn, + invocation_endpoint, + http_method, + description, + invocation_rate_limit_per_second, + api_destination_state=api_destination_state, + ) + self._api_destinations[name] = api_destination + + return CreateApiDestinationResponse( + ApiDestinationArn=api_destination["ApiDestinationArn"], + ApiDestinationState=api_destination["ApiDestinationState"], + CreationTime=api_destination["CreationTime"], + LastModifiedTime=api_destination["LastModifiedTime"], + ) + + return self._handle_api_destination_operation("creating", create) + + @handler("DescribeApiDestination") + def describe_api_destination( + self, context: RequestContext, name: ApiDestinationName, **kwargs + ) -> DescribeApiDestinationResponse: + try: + if name not in self._api_destinations: + raise ResourceNotFoundException( + f"Failed to describe the api-destination(s). An api-destination '{name}' does not exist." + ) + api_destination = self._api_destinations[name] + return DescribeApiDestinationResponse(**api_destination) + except ResourceNotFoundException as e: + raise e + except Exception as e: + raise ValidationException(f"Error describing API destination: {str(e)}") + + @handler("UpdateApiDestination") + def update_api_destination( + self, + context: RequestContext, + name: ApiDestinationName, + description: ApiDestinationDescription = None, + connection_arn: ConnectionArn = None, + invocation_endpoint: HttpsEndpoint = None, + http_method: ApiDestinationHttpMethod = None, + invocation_rate_limit_per_second: ApiDestinationInvocationRateLimitPerSecond = None, + **kwargs, + ) -> UpdateApiDestinationResponse: + def update(): + if name not in self._api_destinations: + raise ResourceNotFoundException( + f"Failed to describe the api-destination(s). An api-destination '{name}' does not exist." + ) + api_destination = self._api_destinations[name] + + if description is not None: + api_destination["Description"] = description + if connection_arn is not None: + connection = self._get_connection_by_arn(connection_arn) + if not connection: + raise ResourceNotFoundException( + f"Connection '{connection_arn}' does not exist." + ) + api_destination["ConnectionArn"] = connection_arn + api_destination["ApiDestinationState"] = self._determine_api_destination_state( + connection["ConnectionState"] + ) + else: + connection = self._get_connection_by_arn(api_destination["ConnectionArn"]) + if connection: + api_destination["ApiDestinationState"] = self._determine_api_destination_state( + connection["ConnectionState"] + ) + else: + api_destination["ApiDestinationState"] = "INACTIVE" + + if invocation_endpoint is not None: + api_destination["InvocationEndpoint"] = invocation_endpoint + if http_method is not None: + api_destination["HttpMethod"] = http_method + if invocation_rate_limit_per_second is not None: + api_destination["InvocationRateLimitPerSecond"] = invocation_rate_limit_per_second + else: + if "InvocationRateLimitPerSecond" not in api_destination: + api_destination["InvocationRateLimitPerSecond"] = 300 + + api_destination["LastModifiedTime"] = datetime.utcnow() + + return UpdateApiDestinationResponse( + ApiDestinationArn=api_destination["ApiDestinationArn"], + ApiDestinationState=api_destination["ApiDestinationState"], + CreationTime=api_destination["CreationTime"], + LastModifiedTime=api_destination["LastModifiedTime"], + ) + + return self._handle_api_destination_operation("updating", update) + + @handler("DeleteApiDestination") + def delete_api_destination( + self, context: RequestContext, name: ApiDestinationName, **kwargs + ) -> DeleteApiDestinationResponse: + def delete(): + if name not in self._api_destinations: + raise ResourceNotFoundException( + f"Failed to describe the api-destination(s). An api-destination '{name}' does not exist." + ) + del self._api_destinations[name] + return DeleteApiDestinationResponse() + + return self._handle_api_destination_operation("deleting", delete) + + @handler("ListApiDestinations") + def list_api_destinations( + self, + context: RequestContext, + name_prefix: ApiDestinationName = None, + connection_arn: ConnectionArn = None, + next_token: NextToken = None, + limit: LimitMax100 = None, + **kwargs, + ) -> ListApiDestinationsResponse: + try: + api_destinations = list(self._api_destinations.values()) + + if name_prefix: + api_destinations = [ + dest for dest in api_destinations if dest["Name"].startswith(name_prefix) + ] + if connection_arn: + api_destinations = [ + dest for dest in api_destinations if dest["ConnectionArn"] == connection_arn + ] + + api_destinations.sort(key=lambda x: x["Name"]) + if limit: + api_destinations = api_destinations[:limit] + + # Prepare summaries + api_destination_summaries = [] + for dest in api_destinations: + summary = { + "ApiDestinationArn": dest["ApiDestinationArn"], + "Name": dest["Name"], + "ApiDestinationState": dest["ApiDestinationState"], + "ConnectionArn": dest["ConnectionArn"], + "InvocationEndpoint": dest["InvocationEndpoint"], + "HttpMethod": dest["HttpMethod"], + "CreationTime": dest["CreationTime"], + "LastModifiedTime": dest["LastModifiedTime"], + "InvocationRateLimitPerSecond": dest.get("InvocationRateLimitPerSecond", 300), + } + api_destination_summaries.append(summary) + + return ListApiDestinationsResponse( + ApiDestinations=api_destination_summaries, + NextToken=None, # Pagination token handling can be added if needed + ) + except Exception as e: + raise ValidationException(f"Error listing API destinations: {str(e)}") + ########## # EventBus ########## diff --git a/localstack-core/localstack/testing/snapshots/transformer_utility.py b/localstack-core/localstack/testing/snapshots/transformer_utility.py index 6ec0a1950e0dd..1e0f4c7ee64d8 100644 --- a/localstack-core/localstack/testing/snapshots/transformer_utility.py +++ b/localstack-core/localstack/testing/snapshots/transformer_utility.py @@ -740,6 +740,41 @@ def stepfunctions_api(): # def custom(fn: Callable[[dict], dict]) -> Transformer: # return GenericTransformer(fn) + @staticmethod + def eventbridge_api_destination(snapshot, connection_name: str): + """ + Add common transformers for EventBridge connection tests. + + Args: + snapshot: The snapshot instance to add transformers to + connection_name: The name of the connection to transform in the snapshot + """ + snapshot.add_transformer(snapshot.transform.regex(connection_name, "")) + snapshot.add_transformer( + snapshot.transform.key_value("ApiDestinationArn", reference_replacement=False) + ) + snapshot.add_transformer( + snapshot.transform.key_value("ConnectionArn", reference_replacement=False) + ) + return snapshot + + @staticmethod + def eventbridge_connection(snapshot, connection_name: str): + """ + Add common transformers for EventBridge connection tests. + Args: + snapshot: The snapshot instance to add transformers to + connection_name: The name of the connection to transform in the snapshot + """ + snapshot.add_transformer(snapshot.transform.regex(connection_name, "")) + snapshot.add_transformer( + snapshot.transform.key_value("ConnectionArn", reference_replacement=False) + ) + snapshot.add_transformer( + snapshot.transform.key_value("SecretArn", reference_replacement=False) + ) + return snapshot + def _sns_pem_file_token_transformer(key: str, val: str) -> str: if isinstance(val, str) and key.lower() == "SigningCertURL".lower(): diff --git a/tests/aws/services/events/conftest.py b/tests/aws/services/events/conftest.py index a8b88478de13d..a602350df07e0 100644 --- a/tests/aws/services/events/conftest.py +++ b/tests/aws/services/events/conftest.py @@ -528,3 +528,55 @@ def _get_primary_secondary_clients(cross_scenario: str): } return _get_primary_secondary_clients + + +@pytest.fixture +def connection_name(): + return f"test-connection-{short_uid()}" + + +@pytest.fixture +def destination_name(): + return f"test-destination-{short_uid()}" + + +@pytest.fixture +def create_connection(aws_client, connection_name): + """Fixture to create a connection with given auth type and parameters.""" + + def _create_connection(auth_type_or_auth, auth_parameters=None): + # Handle both formats: + # 1. (auth_type, auth_parameters) - used by TestEventBridgeConnections + # 2. (auth) - used by TestEventBridgeApiDestinations + if auth_parameters is None: + # Format 2: Single auth dict parameter + auth = auth_type_or_auth + return aws_client.events.create_connection( + Name=connection_name, + AuthorizationType=auth.get("type"), + AuthParameters={ + auth.get("key"): auth.get("parameters"), + }, + ) + else: + # Format 1: auth type and auth parameters + return aws_client.events.create_connection( + Name=connection_name, + AuthorizationType=auth_type_or_auth, + AuthParameters=auth_parameters, + ) + + return _create_connection + + +@pytest.fixture +def create_api_destination(aws_client, destination_name): + """Fixture to create an API destination with given parameters.""" + + def _create_api_destination(**kwargs): + return aws_client.events.create_api_destination( + Name=destination_name, + **kwargs, + ) + + return _create_api_destination diff --git a/tests/aws/services/events/test_events.py b/tests/aws/services/events/test_events.py index e5032a4d48de3..a1c4fe2644d2c 100644 --- a/tests/aws/services/events/test_events.py +++ b/tests/aws/services/events/test_events.py @@ -19,6 +19,7 @@ from localstack.testing.aws.eventbus_utils import allow_event_rule_to_sqs_queue from localstack.testing.aws.util import is_aws_cloud from localstack.testing.pytest import markers +from localstack.testing.snapshots.transformer_utility import TransformerUtility from localstack.utils.aws import arns from localstack.utils.files import load_file from localstack.utils.strings import long_uid, short_uid, to_str @@ -1680,3 +1681,317 @@ def test_put_target_id_validation( {"Id": target_id, "Arn": queue_arn, "InputPath": "$.detail"}, ], ) + + +API_DESTINATION_AUTH_PARAMS = [ + { + "AuthorizationType": "BASIC", + "AuthParameters": { + "BasicAuthParameters": {"Username": "user", "Password": "pass"}, + }, + }, + { + "AuthorizationType": "API_KEY", + "AuthParameters": { + "ApiKeyAuthParameters": {"ApiKeyName": "ApiKey", "ApiKeyValue": "secret"}, + }, + }, + { + "AuthorizationType": "OAUTH_CLIENT_CREDENTIALS", + "AuthParameters": { + "OAuthParameters": { + "AuthorizationEndpoint": "https://example.com/oauth", + "ClientParameters": {"ClientID": "client_id", "ClientSecret": "client_secret"}, + "HttpMethod": "POST", + } + }, + }, +] + + +class TestEventBridgeConnections: + @pytest.fixture + def connection_snapshots(self, snapshot, connection_name): + """Common snapshot transformers for connection tests.""" + return TransformerUtility.eventbridge_connection(snapshot, connection_name) + + @markers.aws.validated + @pytest.mark.skipif( + is_old_provider(), + reason="V1 provider does not support this feature", + ) + def test_create_connection( + self, aws_client, connection_snapshots, create_connection, connection_name + ): + response = create_connection( + "API_KEY", + { + "ApiKeyAuthParameters": {"ApiKeyName": "ApiKey", "ApiKeyValue": "secret"}, + "InvocationHttpParameters": {}, + }, + ) + connection_snapshots.match("create-connection", response) + + describe_response = aws_client.events.describe_connection(Name=connection_name) + connection_snapshots.match("describe-connection", describe_response) + + @markers.aws.validated + @pytest.mark.skipif( + is_old_provider(), + reason="V1 provider does not support this feature", + ) + @pytest.mark.parametrize("auth_params", API_DESTINATION_AUTH_PARAMS) + def test_create_connection_with_auth( + self, aws_client, connection_snapshots, create_connection, auth_params, connection_name + ): + response = create_connection( + auth_params["AuthorizationType"], + auth_params["AuthParameters"], + ) + connection_snapshots.match("create-connection-auth", response) + + describe_response = aws_client.events.describe_connection(Name=connection_name) + connection_snapshots.match("describe-connection-auth", describe_response) + + @markers.aws.validated + @pytest.mark.skipif( + is_old_provider(), + reason="V1 provider does not support this feature", + ) + def test_list_connections( + self, aws_client, connection_snapshots, create_connection, connection_name + ): + create_connection( + "BASIC", + { + "BasicAuthParameters": {"Username": "user", "Password": "pass"}, + "InvocationHttpParameters": {}, + }, + ) + + response = aws_client.events.list_connections(NamePrefix=connection_name) + connection_snapshots.match("list-connections", response) + + @markers.aws.validated + @pytest.mark.skipif( + is_old_provider(), + reason="V1 provider does not support this feature", + ) + def test_delete_connection( + self, aws_client, connection_snapshots, create_connection, connection_name + ): + create_connection( + "API_KEY", + { + "ApiKeyAuthParameters": {"ApiKeyName": "ApiKey", "ApiKeyValue": "secret"}, + "InvocationHttpParameters": {}, + }, + ) + + delete_response = aws_client.events.delete_connection(Name=connection_name) + connection_snapshots.match("delete-connection", delete_response) + + with pytest.raises(aws_client.events.exceptions.ResourceNotFoundException) as exc: + aws_client.events.describe_connection(Name=connection_name) + assert f"Connection '{connection_name}' does not exist" in str(exc.value) + + @markers.aws.validated + @pytest.mark.skipif( + is_old_provider(), + reason="V1 provider does not support this feature", + ) + def test_create_connection_invalid_parameters( + self, aws_client, connection_snapshots, connection_name + ): + with pytest.raises(ClientError) as e: + aws_client.events.create_connection( + Name=connection_name, + AuthorizationType="INVALID_AUTH_TYPE", + AuthParameters={}, + ) + connection_snapshots.match("create-connection-invalid-auth-error", e.value.response) + + @markers.aws.validated + @pytest.mark.skipif( + is_old_provider(), + reason="V1 provider does not support this feature", + ) + def test_update_connection( + self, aws_client, connection_snapshots, create_connection, connection_name + ): + create_response = create_connection( + "BASIC", + { + "BasicAuthParameters": {"Username": "user", "Password": "pass"}, + "InvocationHttpParameters": {}, + }, + ) + connection_snapshots.match("create-connection", create_response) + + update_response = aws_client.events.update_connection( + Name=connection_name, + AuthorizationType="BASIC", + AuthParameters={ + "BasicAuthParameters": {"Username": "new_user", "Password": "new_pass"}, + "InvocationHttpParameters": {}, + }, + ) + connection_snapshots.match("update-connection", update_response) + + describe_response = aws_client.events.describe_connection(Name=connection_name) + connection_snapshots.match("describe-updated-connection", describe_response) + + @markers.aws.validated + @pytest.mark.skipif( + is_old_provider(), + reason="V1 provider does not support this feature", + ) + def test_create_connection_name_validation( + self, aws_client, connection_snapshots, connection_name + ): + invalid_name = "Invalid Name With Spaces!" + + with pytest.raises(ClientError) as e: + aws_client.events.create_connection( + Name=invalid_name, + AuthorizationType="API_KEY", + AuthParameters={ + "ApiKeyAuthParameters": {"ApiKeyName": "ApiKey", "ApiKeyValue": "secret"}, + "InvocationHttpParameters": {}, + }, + ) + connection_snapshots.match("create-connection-invalid-name-error", e.value.response) + + +API_DESTINATION_AUTHS = [ + { + "type": "BASIC", + "key": "BasicAuthParameters", + "parameters": {"Username": "user", "Password": "pass"}, + }, + { + "type": "API_KEY", + "key": "ApiKeyAuthParameters", + "parameters": {"ApiKeyName": "ApiKey", "ApiKeyValue": "secret"}, + }, + { + "type": "OAUTH_CLIENT_CREDENTIALS", + "key": "OAuthParameters", + "parameters": { + "ClientParameters": {"ClientID": "id", "ClientSecret": "password"}, + "AuthorizationEndpoint": "https://example.com/oauth", + "HttpMethod": "POST", + "OAuthHttpParameters": { + "BodyParameters": [{"Key": "oauthbody", "Value": "value1", "IsValueSecret": False}], + "HeaderParameters": [ + {"Key": "oauthheader", "Value": "value2", "IsValueSecret": False} + ], + "QueryStringParameters": [ + {"Key": "oauthquery", "Value": "value3", "IsValueSecret": False} + ], + }, + }, + }, +] + + +class TestEventBridgeApiDestinations: + @pytest.fixture + def api_destination_snapshots(self, snapshot, destination_name): + """Common snapshot transformers for API destination tests.""" + return TransformerUtility.eventbridge_api_destination(snapshot, destination_name) + + @markers.aws.validated + @pytest.mark.parametrize("auth", API_DESTINATION_AUTHS) + @pytest.mark.skipif( + is_old_provider(), + reason="V1 provider does not support this feature", + ) + def test_api_destinations( + self, + aws_client, + api_destination_snapshots, + create_connection, + create_api_destination, + connection_name, + destination_name, + auth, + ): + connection_response = create_connection(auth) + connection_arn = connection_response["ConnectionArn"] + + response = create_api_destination( + ConnectionArn=connection_arn, + HttpMethod="POST", + InvocationEndpoint="https://example.com/api", + Description="Test API destination", + ) + api_destination_snapshots.match("create-api-destination", response) + + describe_response = aws_client.events.describe_api_destination(Name=destination_name) + api_destination_snapshots.match("describe-api-destination", describe_response) + + list_response = aws_client.events.list_api_destinations(NamePrefix=destination_name) + api_destination_snapshots.match("list-api-destinations", list_response) + + update_response = aws_client.events.update_api_destination( + Name=destination_name, + ConnectionArn=connection_arn, + HttpMethod="PUT", + InvocationEndpoint="https://example.com/api/v2", + Description="Updated API destination", + ) + api_destination_snapshots.match("update-api-destination", update_response) + + describe_updated_response = aws_client.events.describe_api_destination( + Name=destination_name + ) + api_destination_snapshots.match( + "describe-updated-api-destination", describe_updated_response + ) + + delete_response = aws_client.events.delete_api_destination(Name=destination_name) + api_destination_snapshots.match("delete-api-destination", delete_response) + + with pytest.raises(aws_client.events.exceptions.ResourceNotFoundException) as exc_info: + aws_client.events.describe_api_destination(Name=destination_name) + api_destination_snapshots.match( + "describe-api-destination-not-found-error", exc_info.value.response + ) + + @markers.aws.validated + @pytest.mark.skipif(is_old_provider(), reason="V1 provider does not support this feature") + def test_create_api_destination_invalid_parameters( + self, aws_client, api_destination_snapshots, connection_name, destination_name + ): + with pytest.raises(ClientError) as e: + aws_client.events.create_api_destination( + Name=destination_name, + ConnectionArn="invalid-connection-arn", + HttpMethod="INVALID_METHOD", + InvocationEndpoint="invalid-endpoint", + ) + api_destination_snapshots.match( + "create-api-destination-invalid-parameters-error", e.value.response + ) + + @markers.aws.validated + @pytest.mark.skipif(is_old_provider(), reason="V1 provider does not support this feature") + def test_create_api_destination_name_validation( + self, aws_client, api_destination_snapshots, create_connection, connection_name + ): + invalid_name = "Invalid Name With Spaces!" + + connection_response = create_connection(API_DESTINATION_AUTHS[0]) + connection_arn = connection_response["ConnectionArn"] + + with pytest.raises(ClientError) as e: + aws_client.events.create_api_destination( + Name=invalid_name, + ConnectionArn=connection_arn, + HttpMethod="POST", + InvocationEndpoint="https://example.com/api", + ) + api_destination_snapshots.match( + "create-api-destination-invalid-name-error", e.value.response + ) diff --git a/tests/aws/services/events/test_events.snapshot.json b/tests/aws/services/events/test_events.snapshot.json index 436e8332d2fe5..bed3a14b6a197 100644 --- a/tests/aws/services/events/test_events.snapshot.json +++ b/tests/aws/services/events/test_events.snapshot.json @@ -1753,5 +1753,600 @@ } ] } + }, + "tests/aws/services/events/test_events.py::TestEventBridgeConnections::test_create_connection": { + "recorded-date": "12-11-2024, 16:49:40", + "recorded-content": { + "create-connection": { + "ConnectionArn": "connection-arn", + "ConnectionState": "AUTHORIZED", + "CreationTime": "datetime", + "LastModifiedTime": "datetime", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-connection": { + "AuthParameters": { + "ApiKeyAuthParameters": { + "ApiKeyName": "ApiKey" + }, + "InvocationHttpParameters": {} + }, + "AuthorizationType": "API_KEY", + "ConnectionArn": "connection-arn", + "ConnectionState": "AUTHORIZED", + "CreationTime": "datetime", + "LastAuthorizedTime": "datetime", + "LastModifiedTime": "datetime", + "Name": "", + "SecretArn": "secret-arn", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventBridgeConnections::test_create_connection_with_auth[auth_params0]": { + "recorded-date": "12-11-2024, 16:49:41", + "recorded-content": { + "create-connection-auth": { + "ConnectionArn": "connection-arn", + "ConnectionState": "AUTHORIZED", + "CreationTime": "datetime", + "LastModifiedTime": "datetime", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-connection-auth": { + "AuthParameters": { + "BasicAuthParameters": { + "Username": "user" + } + }, + "AuthorizationType": "BASIC", + "ConnectionArn": "connection-arn", + "ConnectionState": "AUTHORIZED", + "CreationTime": "datetime", + "LastAuthorizedTime": "datetime", + "LastModifiedTime": "datetime", + "Name": "", + "SecretArn": "secret-arn", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventBridgeConnections::test_create_connection_with_auth[auth_params1]": { + "recorded-date": "12-11-2024, 16:49:41", + "recorded-content": { + "create-connection-auth": { + "ConnectionArn": "connection-arn", + "ConnectionState": "AUTHORIZED", + "CreationTime": "datetime", + "LastModifiedTime": "datetime", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-connection-auth": { + "AuthParameters": { + "ApiKeyAuthParameters": { + "ApiKeyName": "ApiKey" + } + }, + "AuthorizationType": "API_KEY", + "ConnectionArn": "connection-arn", + "ConnectionState": "AUTHORIZED", + "CreationTime": "datetime", + "LastAuthorizedTime": "datetime", + "LastModifiedTime": "datetime", + "Name": "", + "SecretArn": "secret-arn", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventBridgeConnections::test_create_connection_with_auth[auth_params2]": { + "recorded-date": "12-11-2024, 16:49:42", + "recorded-content": { + "create-connection-auth": { + "ConnectionArn": "connection-arn", + "ConnectionState": "AUTHORIZING", + "CreationTime": "datetime", + "LastModifiedTime": "datetime", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-connection-auth": { + "AuthParameters": { + "OAuthParameters": { + "AuthorizationEndpoint": "https://example.com/oauth", + "ClientParameters": { + "ClientID": "client_id" + }, + "HttpMethod": "POST" + } + }, + "AuthorizationType": "OAUTH_CLIENT_CREDENTIALS", + "ConnectionArn": "connection-arn", + "ConnectionState": "AUTHORIZING", + "CreationTime": "datetime", + "LastAuthorizedTime": "datetime", + "LastModifiedTime": "datetime", + "Name": "", + "SecretArn": "secret-arn", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventBridgeConnections::test_list_connections": { + "recorded-date": "12-11-2024, 16:49:42", + "recorded-content": { + "list-connections": { + "Connections": [ + { + "AuthorizationType": "BASIC", + "ConnectionArn": "connection-arn", + "ConnectionState": "AUTHORIZED", + "CreationTime": "datetime", + "LastAuthorizedTime": "datetime", + "LastModifiedTime": "datetime", + "Name": "" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventBridgeConnections::test_create_connection_invalid_parameters": { + "recorded-date": "12-11-2024, 16:49:47", + "recorded-content": { + "create-connection-invalid-auth-error": { + "Error": { + "Code": "ValidationException", + "Message": "1 validation error detected: Value 'INVALID_AUTH_TYPE' at 'authorizationType' failed to satisfy constraint: Member must satisfy enum value set: [BASIC, OAUTH_CLIENT_CREDENTIALS, API_KEY]" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventBridgeConnections::test_update_connection": { + "recorded-date": "12-11-2024, 16:49:48", + "recorded-content": { + "create-connection": { + "ConnectionArn": "connection-arn", + "ConnectionState": "AUTHORIZED", + "CreationTime": "datetime", + "LastModifiedTime": "datetime", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "update-connection": { + "ConnectionArn": "connection-arn", + "ConnectionState": "AUTHORIZED", + "CreationTime": "datetime", + "LastAuthorizedTime": "datetime", + "LastModifiedTime": "datetime", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-updated-connection": { + "AuthParameters": { + "BasicAuthParameters": { + "Username": "new_user" + }, + "InvocationHttpParameters": {} + }, + "AuthorizationType": "BASIC", + "ConnectionArn": "connection-arn", + "ConnectionState": "AUTHORIZED", + "CreationTime": "datetime", + "LastAuthorizedTime": "datetime", + "LastModifiedTime": "datetime", + "Name": "", + "SecretArn": "secret-arn", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventBridgeConnections::test_create_connection_name_validation": { + "recorded-date": "12-11-2024, 16:49:49", + "recorded-content": { + "create-connection-invalid-name-error": { + "Error": { + "Code": "ValidationException", + "Message": "1 validation error detected: Value 'Invalid Name With Spaces!' at 'name' failed to satisfy constraint: Member must satisfy regular expression pattern: [\\.\\-_A-Za-z0-9]+" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventBridgeConnections::test_delete_connection": { + "recorded-date": "12-11-2024, 16:49:46", + "recorded-content": { + "delete-connection": { + "ConnectionArn": "connection-arn", + "ConnectionState": "DELETING", + "CreationTime": "datetime", + "LastAuthorizedTime": "datetime", + "LastModifiedTime": "datetime", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-deleted-connection-error": { + "Error": { + "Code": "ResourceNotFoundException", + "Message": "Failed to describe the connection(s). Connection '' does not exist." + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventBridgeEndpoints::test_create_endpoint": { + "recorded-date": "16-11-2024, 13:01:35", + "recorded-content": {} + }, + "tests/aws/services/events/test_events.py::TestEventBridgeEndpoints::test_list_endpoints": { + "recorded-date": "16-11-2024, 13:01:36", + "recorded-content": {} + }, + "tests/aws/services/events/test_events.py::TestEventBridgeEndpoints::test_delete_endpoint": { + "recorded-date": "16-11-2024, 12:56:52", + "recorded-content": {} + }, + "tests/aws/services/events/test_events.py::TestEventBridgeEndpoints::test_update_endpoint": { + "recorded-date": "16-11-2024, 12:56:52", + "recorded-content": {} + }, + "tests/aws/services/events/test_events.py::TestEventBridgeEndpoints::test_create_endpoint_invalid_parameters": { + "recorded-date": "16-11-2024, 12:56:52", + "recorded-content": {} + }, + "tests/aws/services/events/test_events.py::TestEventBridgeEndpoints::test_create_endpoint_name_validation": { + "recorded-date": "16-11-2024, 12:56:52", + "recorded-content": {} + }, + "tests/aws/services/events/test_events.py::TestEventBridgeApiDestinations::test_api_destinations[auth0]": { + "recorded-date": "16-11-2024, 13:44:03", + "recorded-content": { + "create-api-destination": { + "ApiDestinationArn": "api-destination-arn", + "ApiDestinationState": "ACTIVE", + "CreationTime": "datetime", + "LastModifiedTime": "datetime", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-api-destination": { + "ApiDestinationArn": "api-destination-arn", + "ApiDestinationState": "ACTIVE", + "ConnectionArn": "connection-arn", + "CreationTime": "datetime", + "Description": "Test API destination", + "HttpMethod": "POST", + "InvocationEndpoint": "https://example.com/api", + "InvocationRateLimitPerSecond": 300, + "LastModifiedTime": "datetime", + "Name": "", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-api-destinations": { + "ApiDestinations": [ + { + "ApiDestinationArn": "api-destination-arn", + "ApiDestinationState": "ACTIVE", + "ConnectionArn": "connection-arn", + "CreationTime": "datetime", + "HttpMethod": "POST", + "InvocationEndpoint": "https://example.com/api", + "InvocationRateLimitPerSecond": 300, + "LastModifiedTime": "datetime", + "Name": "" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "update-api-destination": { + "ApiDestinationArn": "api-destination-arn", + "ApiDestinationState": "ACTIVE", + "CreationTime": "datetime", + "LastModifiedTime": "datetime", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-updated-api-destination": { + "ApiDestinationArn": "api-destination-arn", + "ApiDestinationState": "ACTIVE", + "ConnectionArn": "connection-arn", + "CreationTime": "datetime", + "Description": "Updated API destination", + "HttpMethod": "PUT", + "InvocationEndpoint": "https://example.com/api/v2", + "InvocationRateLimitPerSecond": 300, + "LastModifiedTime": "datetime", + "Name": "", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "delete-api-destination": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-api-destination-not-found-error": { + "Error": { + "Code": "ResourceNotFoundException", + "Message": "Failed to describe the api-destination(s). An api-destination '' does not exist." + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventBridgeApiDestinations::test_api_destinations[auth1]": { + "recorded-date": "16-11-2024, 13:44:04", + "recorded-content": { + "create-api-destination": { + "ApiDestinationArn": "api-destination-arn", + "ApiDestinationState": "ACTIVE", + "CreationTime": "datetime", + "LastModifiedTime": "datetime", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-api-destination": { + "ApiDestinationArn": "api-destination-arn", + "ApiDestinationState": "ACTIVE", + "ConnectionArn": "connection-arn", + "CreationTime": "datetime", + "Description": "Test API destination", + "HttpMethod": "POST", + "InvocationEndpoint": "https://example.com/api", + "InvocationRateLimitPerSecond": 300, + "LastModifiedTime": "datetime", + "Name": "", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-api-destinations": { + "ApiDestinations": [ + { + "ApiDestinationArn": "api-destination-arn", + "ApiDestinationState": "ACTIVE", + "ConnectionArn": "connection-arn", + "CreationTime": "datetime", + "HttpMethod": "POST", + "InvocationEndpoint": "https://example.com/api", + "InvocationRateLimitPerSecond": 300, + "LastModifiedTime": "datetime", + "Name": "" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "update-api-destination": { + "ApiDestinationArn": "api-destination-arn", + "ApiDestinationState": "ACTIVE", + "CreationTime": "datetime", + "LastModifiedTime": "datetime", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-updated-api-destination": { + "ApiDestinationArn": "api-destination-arn", + "ApiDestinationState": "ACTIVE", + "ConnectionArn": "connection-arn", + "CreationTime": "datetime", + "Description": "Updated API destination", + "HttpMethod": "PUT", + "InvocationEndpoint": "https://example.com/api/v2", + "InvocationRateLimitPerSecond": 300, + "LastModifiedTime": "datetime", + "Name": "", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "delete-api-destination": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-api-destination-not-found-error": { + "Error": { + "Code": "ResourceNotFoundException", + "Message": "Failed to describe the api-destination(s). An api-destination '' does not exist." + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventBridgeApiDestinations::test_api_destinations[auth2]": { + "recorded-date": "16-11-2024, 13:44:07", + "recorded-content": { + "create-api-destination": { + "ApiDestinationArn": "api-destination-arn", + "ApiDestinationState": "INACTIVE", + "CreationTime": "datetime", + "LastModifiedTime": "datetime", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-api-destination": { + "ApiDestinationArn": "api-destination-arn", + "ApiDestinationState": "INACTIVE", + "ConnectionArn": "connection-arn", + "CreationTime": "datetime", + "Description": "Test API destination", + "HttpMethod": "POST", + "InvocationEndpoint": "https://example.com/api", + "InvocationRateLimitPerSecond": 300, + "LastModifiedTime": "datetime", + "Name": "", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-api-destinations": { + "ApiDestinations": [ + { + "ApiDestinationArn": "api-destination-arn", + "ApiDestinationState": "INACTIVE", + "ConnectionArn": "connection-arn", + "CreationTime": "datetime", + "HttpMethod": "POST", + "InvocationEndpoint": "https://example.com/api", + "InvocationRateLimitPerSecond": 300, + "LastModifiedTime": "datetime", + "Name": "" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "update-api-destination": { + "ApiDestinationArn": "api-destination-arn", + "ApiDestinationState": "INACTIVE", + "CreationTime": "datetime", + "LastModifiedTime": "datetime", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-updated-api-destination": { + "ApiDestinationArn": "api-destination-arn", + "ApiDestinationState": "INACTIVE", + "ConnectionArn": "connection-arn", + "CreationTime": "datetime", + "Description": "Updated API destination", + "HttpMethod": "PUT", + "InvocationEndpoint": "https://example.com/api/v2", + "InvocationRateLimitPerSecond": 300, + "LastModifiedTime": "datetime", + "Name": "", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "delete-api-destination": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-api-destination-not-found-error": { + "Error": { + "Code": "ResourceNotFoundException", + "Message": "Failed to describe the api-destination(s). An api-destination '' does not exist." + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventBridgeApiDestinations::test_create_api_destination_invalid_parameters": { + "recorded-date": "16-11-2024, 13:44:07", + "recorded-content": { + "create-api-destination-invalid-parameters-error": { + "Error": { + "Code": "ValidationException", + "Message": "2 validation errors detected: Value 'invalid-connection-arn' at 'connectionArn' failed to satisfy constraint: Member must satisfy regular expression pattern: ^arn:aws([a-z]|\\-)*:events:([a-z]|\\d|\\-)*:([0-9]{12})?:connection\\/[\\.\\-_A-Za-z0-9]+\\/[\\-A-Za-z0-9]+$; Value 'INVALID_METHOD' at 'httpMethod' failed to satisfy constraint: Member must satisfy enum value set: [HEAD, POST, PATCH, DELETE, PUT, GET, OPTIONS]" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventBridgeApiDestinations::test_create_api_destination_name_validation": { + "recorded-date": "16-11-2024, 13:44:08", + "recorded-content": { + "create-api-destination-invalid-name-error": { + "Error": { + "Code": "ValidationException", + "Message": "1 validation error detected: Value 'Invalid Name With Spaces!' at 'name' failed to satisfy constraint: Member must satisfy regular expression pattern: [\\.\\-_A-Za-z0-9]+" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } } } diff --git a/tests/aws/services/events/test_events.validation.json b/tests/aws/services/events/test_events.validation.json index 5a3d6c0efb76a..433649bb393c4 100644 --- a/tests/aws/services/events/test_events.validation.json +++ b/tests/aws/services/events/test_events.validation.json @@ -1,4 +1,46 @@ { + "tests/aws/services/events/test_events.py::TestEventBridgeApiDestinations::test_api_destinations[auth0]": { + "last_validated_date": "2024-11-16T13:44:03+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventBridgeApiDestinations::test_api_destinations[auth1]": { + "last_validated_date": "2024-11-16T13:44:04+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventBridgeApiDestinations::test_api_destinations[auth2]": { + "last_validated_date": "2024-11-16T13:44:07+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventBridgeApiDestinations::test_create_api_destination_invalid_parameters": { + "last_validated_date": "2024-11-16T13:44:07+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventBridgeApiDestinations::test_create_api_destination_name_validation": { + "last_validated_date": "2024-11-16T13:44:08+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventBridgeConnections::test_create_connection": { + "last_validated_date": "2024-11-12T16:49:40+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventBridgeConnections::test_create_connection_invalid_parameters": { + "last_validated_date": "2024-11-12T16:49:47+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventBridgeConnections::test_create_connection_name_validation": { + "last_validated_date": "2024-11-12T16:49:49+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventBridgeConnections::test_create_connection_with_auth[auth_params0]": { + "last_validated_date": "2024-11-12T16:49:41+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventBridgeConnections::test_create_connection_with_auth[auth_params1]": { + "last_validated_date": "2024-11-12T16:49:41+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventBridgeConnections::test_create_connection_with_auth[auth_params2]": { + "last_validated_date": "2024-11-12T16:49:42+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventBridgeConnections::test_delete_connection": { + "last_validated_date": "2024-11-12T16:49:46+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventBridgeConnections::test_list_connections": { + "last_validated_date": "2024-11-12T16:49:42+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventBridgeConnections::test_update_connection": { + "last_validated_date": "2024-11-12T16:49:48+00:00" + }, "tests/aws/services/events/test_events.py::TestEventBus::test_create_list_describe_delete_custom_event_buses[regions0]": { "last_validated_date": "2024-06-19T10:54:07+00:00" },