diff --git a/CHANGELOG.md b/CHANGELOG.md index 4379b43b3bb..9db2d5a0675 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -119,6 +119,128 @@ * **docs:** enable privacy plugin in docs ([#6036](https://github.com/aws-powertools/powertools-lambda-python/issues/6036)) + +## [v3.5.0] - 2025-01-28 +## Bug Fixes + +* **security:** fix encryption_context handling in data masking operations ([#6074](https://github.com/aws-powertools/powertools-lambda-python/issues/6074)) + +## Documentation + +* **roadmap:** update roadmap ([#6077](https://github.com/aws-powertools/powertools-lambda-python/issues/6077)) + +## Maintenance + +* **ci:** new pre-release 3.6.1a0 ([#6084](https://github.com/aws-powertools/powertools-lambda-python/issues/6084)) +* **deps:** bump aws-actions/configure-aws-credentials from 4.0.3 to 4.1.0 ([#6082](https://github.com/aws-powertools/powertools-lambda-python/issues/6082)) +* **deps-dev:** bump coverage from 7.6.11 to 7.6.12 ([#6080](https://github.com/aws-powertools/powertools-lambda-python/issues/6080)) +* **deps-dev:** bump mkdocstrings-python from 1.14.6 to 1.15.0 ([#6079](https://github.com/aws-powertools/powertools-lambda-python/issues/6079)) +* **deps-dev:** bump boto3-stubs from 1.36.16 to 1.36.17 ([#6078](https://github.com/aws-powertools/powertools-lambda-python/issues/6078)) + + + +## [v3.6.0] - 2025-02-11 +## Bug Fixes + +* **docs:** typo in a service name in Event Handler ([#5944](https://github.com/aws-powertools/powertools-lambda-python/issues/5944)) +* **logger:** child logger must respect log level ([#5950](https://github.com/aws-powertools/powertools-lambda-python/issues/5950)) + +## Code Refactoring + +* **metrics:** Improve type annotations for metrics decorator ([#6000](https://github.com/aws-powertools/powertools-lambda-python/issues/6000)) + +## Documentation + +* **api:** migrating the event handler utility to mkdocstrings ([#6023](https://github.com/aws-powertools/powertools-lambda-python/issues/6023)) +* **api:** migrating the metrics utility to mkdocstrings ([#6022](https://github.com/aws-powertools/powertools-lambda-python/issues/6022)) +* **api:** migrating the logger utility to mkdocstrings ([#6021](https://github.com/aws-powertools/powertools-lambda-python/issues/6021)) +* **api:** migrating the Middleware Factory utility to mkdocstrings ([#6019](https://github.com/aws-powertools/powertools-lambda-python/issues/6019)) +* **api:** migrating the tracer utility to mkdocstrings ([#6017](https://github.com/aws-powertools/powertools-lambda-python/issues/6017)) +* **api:** migrating the batch utility to mkdocstrings ([#6016](https://github.com/aws-powertools/powertools-lambda-python/issues/6016)) +* **api:** migrating the event source data classes utility to mkdocstrings ([#6015](https://github.com/aws-powertools/powertools-lambda-python/issues/6015)) +* **api:** migrating the data masking utility to mkdocstrings ([#6013](https://github.com/aws-powertools/powertools-lambda-python/issues/6013)) +* **api:** migrating the AppConfig utility to mkdocstrings ([#6008](https://github.com/aws-powertools/powertools-lambda-python/issues/6008)) +* **api:** migrating the idempotency utility to mkdocstrings ([#6007](https://github.com/aws-powertools/powertools-lambda-python/issues/6007)) +* **api:** migrating the jmespath utility to mkdocstrings ([#6006](https://github.com/aws-powertools/powertools-lambda-python/issues/6006)) +* **api:** migrating the parameters utility to mkdocstrings ([#6005](https://github.com/aws-powertools/powertools-lambda-python/issues/6005)) +* **api:** migrating the parser utility to mkdocstrings ([#6004](https://github.com/aws-powertools/powertools-lambda-python/issues/6004)) +* **api:** migrating the streaming utility to mkdocstrings ([#6003](https://github.com/aws-powertools/powertools-lambda-python/issues/6003)) +* **api:** migrating the typing utility to mkdocstrings ([#5996](https://github.com/aws-powertools/powertools-lambda-python/issues/5996)) +* **api:** migrating the validation utility to mkdocstrings ([#5972](https://github.com/aws-powertools/powertools-lambda-python/issues/5972)) +* **layer:** update layer version number - v3.5.0 ([#5952](https://github.com/aws-powertools/powertools-lambda-python/issues/5952)) + +## Features + +* **data-masking:** add custom mask functionalities ([#5837](https://github.com/aws-powertools/powertools-lambda-python/issues/5837)) +* **event_source:** add class APIGatewayAuthorizerResponseWebSocket ([#6058](https://github.com/aws-powertools/powertools-lambda-python/issues/6058)) +* **logger:** add clear_state method ([#5956](https://github.com/aws-powertools/powertools-lambda-python/issues/5956)) +* **metrics:** disable metrics flush via environment variables ([#6046](https://github.com/aws-powertools/powertools-lambda-python/issues/6046)) +* **openapi:** enhance support for tuple return type validation ([#5997](https://github.com/aws-powertools/powertools-lambda-python/issues/5997)) + +## Maintenance + +* version bump +* **ci:** new pre-release 3.5.1a9 ([#6069](https://github.com/aws-powertools/powertools-lambda-python/issues/6069)) +* **ci:** new pre-release 3.5.1a0 ([#5945](https://github.com/aws-powertools/powertools-lambda-python/issues/5945)) +* **ci:** new pre-release 3.5.1a1 ([#5954](https://github.com/aws-powertools/powertools-lambda-python/issues/5954)) +* **ci:** new pre-release 3.5.1a8 ([#6061](https://github.com/aws-powertools/powertools-lambda-python/issues/6061)) +* **ci:** install & configure mkdocstrings plugin ([#5959](https://github.com/aws-powertools/powertools-lambda-python/issues/5959)) +* **ci:** new pre-release 3.5.1a2 ([#5970](https://github.com/aws-powertools/powertools-lambda-python/issues/5970)) +* **ci:** new pre-release 3.5.1a3 ([#5998](https://github.com/aws-powertools/powertools-lambda-python/issues/5998)) +* **ci:** new pre-release 3.5.1a7 ([#6044](https://github.com/aws-powertools/powertools-lambda-python/issues/6044)) +* **ci:** new pre-release 3.5.1a4 ([#6018](https://github.com/aws-powertools/powertools-lambda-python/issues/6018)) +* **ci:** remove pdoc3 library ([#6024](https://github.com/aws-powertools/powertools-lambda-python/issues/6024)) +* **ci:** new pre-release 3.5.1a5 ([#6026](https://github.com/aws-powertools/powertools-lambda-python/issues/6026)) +* **ci:** add new script to bump Lambda layer version ([#6001](https://github.com/aws-powertools/powertools-lambda-python/issues/6001)) +* **ci:** new pre-release 3.5.1a6 ([#6033](https://github.com/aws-powertools/powertools-lambda-python/issues/6033)) +* **deps:** bump squidfunk/mkdocs-material from `471695f` to `7e841df` in /docs ([#6012](https://github.com/aws-powertools/powertools-lambda-python/issues/6012)) +* **deps:** bump zgosalvez/github-actions-ensure-sha-pinned-actions from 3.0.20 to 3.0.21 ([#6064](https://github.com/aws-powertools/powertools-lambda-python/issues/6064)) +* **deps:** bump actions/setup-python from 5.3.0 to 5.4.0 ([#5960](https://github.com/aws-powertools/powertools-lambda-python/issues/5960)) +* **deps:** bump docker/setup-qemu-action from 3.2.0 to 3.3.0 ([#5961](https://github.com/aws-powertools/powertools-lambda-python/issues/5961)) +* **deps:** bump codecov/codecov-action from 5.1.2 to 5.3.1 ([#5964](https://github.com/aws-powertools/powertools-lambda-python/issues/5964)) +* **deps:** bump squidfunk/mkdocs-material from `7e841df` to `c62453b` in /docs ([#6052](https://github.com/aws-powertools/powertools-lambda-python/issues/6052)) +* **deps:** bump actions/setup-node from 4.1.0 to 4.2.0 ([#5963](https://github.com/aws-powertools/powertools-lambda-python/issues/5963)) +* **deps:** bump actions/upload-artifact from 4.5.0 to 4.6.0 ([#5962](https://github.com/aws-powertools/powertools-lambda-python/issues/5962)) +* **deps:** bump release-drafter/release-drafter from 6.0.0 to 6.1.0 ([#5976](https://github.com/aws-powertools/powertools-lambda-python/issues/5976)) +* **deps:** bump zgosalvez/github-actions-ensure-sha-pinned-actions from 3.0.18 to 3.0.20 ([#5977](https://github.com/aws-powertools/powertools-lambda-python/issues/5977)) +* **deps:** bump pypa/gh-action-pypi-publish from 1.12.3 to 1.12.4 ([#5980](https://github.com/aws-powertools/powertools-lambda-python/issues/5980)) +* **deps:** bump docker/setup-buildx-action from 3.8.0 to 3.9.0 ([#6042](https://github.com/aws-powertools/powertools-lambda-python/issues/6042)) +* **deps:** bump docker/setup-qemu-action from 3.3.0 to 3.4.0 ([#6043](https://github.com/aws-powertools/powertools-lambda-python/issues/6043)) +* **deps:** bump aws-actions/configure-aws-credentials from 4.0.2 to 4.0.3 ([#5975](https://github.com/aws-powertools/powertools-lambda-python/issues/5975)) +* **deps:** bump squidfunk/mkdocs-material from `41942f7` to `471695f` in /docs ([#5979](https://github.com/aws-powertools/powertools-lambda-python/issues/5979)) +* **deps:** bump actions/setup-go from 5.2.0 to 5.3.0 ([#5978](https://github.com/aws-powertools/powertools-lambda-python/issues/5978)) +* **deps-dev:** bump aws-cdk from 2.178.0 to 2.178.1 ([#6053](https://github.com/aws-powertools/powertools-lambda-python/issues/6053)) +* **deps-dev:** bump mkdocstrings-python from 1.13.0 to 1.14.2 ([#6011](https://github.com/aws-powertools/powertools-lambda-python/issues/6011)) +* **deps-dev:** bump mkdocs-material from 9.6.1 to 9.6.2 ([#6009](https://github.com/aws-powertools/powertools-lambda-python/issues/6009)) +* **deps-dev:** bump aws-cdk-lib from 2.178.0 to 2.178.1 ([#6047](https://github.com/aws-powertools/powertools-lambda-python/issues/6047)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.178.0a0 to 2.178.1a0 ([#6048](https://github.com/aws-powertools/powertools-lambda-python/issues/6048)) +* **deps-dev:** bump boto3-stubs from 1.36.14 to 1.36.15 ([#6049](https://github.com/aws-powertools/powertools-lambda-python/issues/6049)) +* **deps-dev:** bump boto3-stubs from 1.36.10 to 1.36.11 ([#6010](https://github.com/aws-powertools/powertools-lambda-python/issues/6010)) +* **deps-dev:** bump boto3-stubs from 1.36.10 to 1.36.12 ([#6014](https://github.com/aws-powertools/powertools-lambda-python/issues/6014)) +* **deps-dev:** bump ruff from 0.9.5 to 0.9.6 ([#6066](https://github.com/aws-powertools/powertools-lambda-python/issues/6066)) +* **deps-dev:** bump mkdocstrings-python from 1.14.2 to 1.14.4 ([#6025](https://github.com/aws-powertools/powertools-lambda-python/issues/6025)) +* **deps-dev:** bump aws-cdk-aws-lambda-python-alpha from 2.177.0a0 to 2.178.0a0 ([#6041](https://github.com/aws-powertools/powertools-lambda-python/issues/6041)) +* **deps-dev:** bump mkdocs-material from 9.5.50 to 9.6.1 ([#5966](https://github.com/aws-powertools/powertools-lambda-python/issues/5966)) +* **deps-dev:** bump black from 24.10.0 to 25.1.0 ([#5968](https://github.com/aws-powertools/powertools-lambda-python/issues/5968)) +* **deps-dev:** bump ruff from 0.9.3 to 0.9.4 ([#5969](https://github.com/aws-powertools/powertools-lambda-python/issues/5969)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.291 to 0.1.292 ([#6051](https://github.com/aws-powertools/powertools-lambda-python/issues/6051)) +* **deps-dev:** bump cfn-lint from 1.22.7 to 1.23.1 ([#5967](https://github.com/aws-powertools/powertools-lambda-python/issues/5967)) +* **deps-dev:** bump mkdocstrings-python from 1.14.5 to 1.14.6 ([#6050](https://github.com/aws-powertools/powertools-lambda-python/issues/6050)) +* **deps-dev:** bump isort from 5.13.2 to 6.0.0 ([#5965](https://github.com/aws-powertools/powertools-lambda-python/issues/5965)) +* **deps-dev:** bump ruff from 0.9.4 to 0.9.5 ([#6039](https://github.com/aws-powertools/powertools-lambda-python/issues/6039)) +* **deps-dev:** bump aws-cdk-lib from 2.177.0 to 2.178.0 ([#6038](https://github.com/aws-powertools/powertools-lambda-python/issues/6038)) +* **deps-dev:** bump mypy from 1.14.1 to 1.15.0 ([#6028](https://github.com/aws-powertools/powertools-lambda-python/issues/6028)) +* **deps-dev:** bump mkdocstrings-python from 1.14.4 to 1.14.5 ([#6032](https://github.com/aws-powertools/powertools-lambda-python/issues/6032)) +* **deps-dev:** bump cfn-lint from 1.23.1 to 1.24.0 ([#6030](https://github.com/aws-powertools/powertools-lambda-python/issues/6030)) +* **deps-dev:** bump boto3-stubs from 1.36.14 to 1.36.16 ([#6057](https://github.com/aws-powertools/powertools-lambda-python/issues/6057)) +* **deps-dev:** bump cdklabs-generative-ai-cdk-constructs from 0.1.290 to 0.1.291 ([#6031](https://github.com/aws-powertools/powertools-lambda-python/issues/6031)) +* **deps-dev:** bump boto3-stubs from 1.36.12 to 1.36.14 ([#6029](https://github.com/aws-powertools/powertools-lambda-python/issues/6029)) +* **deps-dev:** bump mkdocs-material from 9.6.2 to 9.6.3 ([#6065](https://github.com/aws-powertools/powertools-lambda-python/issues/6065)) +* **deps-dev:** bump coverage from 7.6.10 to 7.6.11 ([#6067](https://github.com/aws-powertools/powertools-lambda-python/issues/6067)) +* **deps-dev:** bump aws-cdk from 2.177.0 to 2.178.0 ([#6040](https://github.com/aws-powertools/powertools-lambda-python/issues/6040)) +* **docs:** enable privacy plugin in docs ([#6036](https://github.com/aws-powertools/powertools-lambda-python/issues/6036)) + + ## [v3.5.0] - 2025-01-28 ## Bug Fixes diff --git a/aws_lambda_powertools/event_handler/async_execution/__init__.py b/aws_lambda_powertools/event_handler/async_execution/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/aws_lambda_powertools/event_handler/async_execution/exceptions.py b/aws_lambda_powertools/event_handler/async_execution/exceptions.py new file mode 100644 index 00000000000..3e4c091eb04 --- /dev/null +++ b/aws_lambda_powertools/event_handler/async_execution/exceptions.py @@ -0,0 +1,2 @@ +class RouteNotFoundError(Exception): + pass diff --git a/aws_lambda_powertools/event_handler/async_execution/router.py b/aws_lambda_powertools/event_handler/async_execution/router.py new file mode 100644 index 00000000000..f6cfc1bbaca --- /dev/null +++ b/aws_lambda_powertools/event_handler/async_execution/router.py @@ -0,0 +1,169 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Callable + +from .routes.aws_config_rule import AwsConfigRuleRoute +from .routes.cloud_watch_alarm import CloudWatchAlarmRoute +from .routes.cloud_watch_logs import CloudWatchLogsRoute +from .routes.code_deploy_lifecycle_hook import CodeDeployLifecycleHookRoute +from .routes.event_bridge import EventBridgeRoute +from .routes.s3 import S3Route +from .routes.secrets_manager import SecretsManagerRoute +from .routes.ses import SESRoute +from .routes.sns import SNSRoute + +if TYPE_CHECKING: + from .routes.base import BaseRoute + + +class Router: + _routes: list[BaseRoute] + + def __init__(self): + self._routes = [] + + def aws_config_rule( + self, + arn: str | None = None, + rule_name: str | None = None, + rule_name_prefix: str | None = None, + rule_id: str | None = None, + ) -> Callable: + def wrapper_aws_config_rule(func: Callable): + self._routes.append( + AwsConfigRuleRoute( + func=func, + arn=arn, + rule_name=rule_name, + rule_name_prefix=rule_name_prefix, + rule_id=rule_id, + ), + ) + + return wrapper_aws_config_rule + + def cloud_watch_alarm( + self, + arn: str | None = None, + alarm_name: str | None = None, + alarm_name_prefix: str | None = None, + ) -> Callable: + def wrapper_cloud_watch_alarm(func: Callable): + self._routes.append( + CloudWatchAlarmRoute(func=func, arn=arn, alarm_name=alarm_name, alarm_name_prefix=alarm_name_prefix), + ) + + return wrapper_cloud_watch_alarm + + def cloud_watch_logs( + self, + log_group: str | None = None, + log_group_prefix: str | None = None, + log_stream: str | None = None, + log_stream_prefix: str | None = None, + subscription_filters: str | list[str] | None = None, + ) -> Callable: + def wrapper_cloud_watch_logs(func: Callable): + self._routes.append( + CloudWatchLogsRoute( + func=func, + log_group=log_group, + log_group_prefix=log_group_prefix, + log_stream=log_stream, + log_stream_prefix=log_stream_prefix, + subscription_filters=subscription_filters, + ), + ) + + return wrapper_cloud_watch_logs + + def code_deploy_lifecycle_hook(self) -> Callable: + def wrapper_code_deploy_lifecycle_hook(func: Callable): + self._routes.append(CodeDeployLifecycleHookRoute(func=func)) + + return wrapper_code_deploy_lifecycle_hook + + def event_bridge( + self, + detail_type: str | None = None, + source: str | None = None, + resources: str | list[str] | None = None, + ) -> Callable: + def wrap_event_bridge(func: Callable): + self._routes.append( + EventBridgeRoute(func=func, detail_type=detail_type, source=source, resources=resources), + ) + + return wrap_event_bridge + + def s3( + self, + bucket: str | None = None, + bucket_prefix: str | None = None, + key: str | None = None, + key_prefix: str | None = None, + key_suffix: str | None = None, + event_name: str | None = None, + ) -> Callable: + def wrap_s3(func: Callable): + self._routes.append( + S3Route( + func=func, + bucket=bucket, + bucket_prefix=bucket_prefix, + key=key, + key_prefix=key_prefix, + key_suffix=key_suffix, + event_name=event_name, + ), + ) + + return wrap_s3 + + def secrets_manager(self, secret_id: str | None = None, secret_name_prefix: str | None = None): + def wrap_secrets_manager(func: Callable): + self._routes.append( + SecretsManagerRoute(func=func, secret_id=secret_id, secret_name_prefix=secret_name_prefix), + ) + + return wrap_secrets_manager + + def ses( + self, + mail_to: str | list[str] | None = None, + mail_from: str | list[str] | None = None, + mail_subject: str | None = None, + ) -> Callable: + def wrap_ses(func: Callable): + self._routes.append(SESRoute(func=func, mail_to=mail_to, mail_from=mail_from, mail_subject=mail_subject)) + + return wrap_ses + + def sns( + self, + arn: str | None = None, + name: str | None = None, + name_prefix: str | None = None, + subject: str | None = None, + subject_prefix: str | None = None, + ) -> Callable: + def wrap_sns(func: Callable): + self._routes.append( + SNSRoute( + func=func, + arn=arn, + name=name, + name_prefix=name_prefix, + subject=subject, + subject_prefix=subject_prefix, + ), + ) + + return wrap_sns + + def resolve_route(self, event: dict[str, Any]) -> tuple[Callable, Any] | None: + for route in self._routes: + data = route.match(event=event) + if data is not None: + return data + return None diff --git a/aws_lambda_powertools/event_handler/async_execution/routes/__init__.py b/aws_lambda_powertools/event_handler/async_execution/routes/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/aws_lambda_powertools/event_handler/async_execution/routes/aws_config_rule.py b/aws_lambda_powertools/event_handler/async_execution/routes/aws_config_rule.py new file mode 100644 index 00000000000..e96959f26da --- /dev/null +++ b/aws_lambda_powertools/event_handler/async_execution/routes/aws_config_rule.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +from typing import Any, Callable + +from aws_lambda_powertools.utilities.data_classes.aws_config_rule_event import ( + AWSConfigRuleEvent, +) + +from .base import BaseRoute + + +class AwsConfigRuleRoute(BaseRoute): + arn: str | None + rule_name: str | None + rule_name_prefix: str | None + rule_id: str | None + + def __init__( + self, + func: Callable, + arn: str | None = None, + rule_name: str | None = None, + rule_name_prefix: str | None = None, + rule_id: str | None = None, + ): + self.func = func + self.arn = arn + self.rule_name = rule_name + self.rule_name_prefix = rule_name_prefix + self.rule_id = rule_id + + if not self.arn and not self.rule_name and not self.rule_name_prefix and not self.rule_id: + raise ValueError("arn, rule_name, rule_name_prefix, or rule_id must be not null") + + def is_target_with_arn(self, arn: str | None) -> bool: + if not arn: + return False + elif self.arn: + return self.arn == arn + else: + return False + + def is_target_with_rule_name(self, rule_name: str | None) -> bool: + if not rule_name: + return False + elif self.rule_name: + return self.rule_name == rule_name + elif self.rule_name_prefix: + return rule_name.find(self.rule_name_prefix) == 0 + else: + return False + + def is_target_with_rule_id(self, rule_id: str | None) -> bool: + if not rule_id: + return False + elif self.rule_id: + return self.rule_id == rule_id + else: + return False + + def match(self, event: dict[str, Any]) -> tuple[Callable, AWSConfigRuleEvent] | None: + if not isinstance(event, dict): + return None + + arn = event.get("configRuleArn") + rule_name = event.get("configRuleName") + rule_id = event.get("configRuleId") + + if not arn and not rule_name and not rule_id: + return None + + if not self.arn: + arn = None + + if not self.rule_name and not self.rule_name_prefix: + rule_name = None + + if not self.rule_id: + rule_id = None + + flag_arn = self.is_target_with_arn(arn=arn) + flag_rule_name = self.is_target_with_rule_name(rule_name=rule_name) + flag_rule_id = self.is_target_with_rule_id(rule_id=rule_id) + + text = ", ".join( + [ + "arn: x" if arn is None else "arn: o", + "rule_name: x" if rule_name is None else "rule_name: o", + "rule_id: x" if rule_id is None else "rule_id: o", + ], + ) + + mapping = { + "arn: o, rule_name: o, rule_id: o": flag_arn and flag_rule_name and flag_rule_id, + "arn: o, rule_name: o, rule_id: x": flag_arn and flag_rule_name, + "arn: o, rule_name: x, rule_id: o": flag_arn and flag_rule_id, + "arn: x, rule_name: o, rule_id: o": flag_rule_name and flag_rule_id, + "arn: o, rule_name: x, rule_id: x": flag_arn, + "arn: x, rule_name: o, rule_id: x": flag_rule_name, + "arn: x, rule_name: x, rule_id: o": flag_rule_id, + "arn: x, rule_name: x, rule_id: x": False, + } + + if mapping[text]: + return self.func, AWSConfigRuleEvent(event) + else: + return None diff --git a/aws_lambda_powertools/event_handler/async_execution/routes/base.py b/aws_lambda_powertools/event_handler/async_execution/routes/base.py new file mode 100644 index 00000000000..8d71faada26 --- /dev/null +++ b/aws_lambda_powertools/event_handler/async_execution/routes/base.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Any, Callable + + +class BaseRoute(ABC): + func: Callable + + @abstractmethod + def match(self, event: dict[str, Any]) -> tuple[Callable, Any] | None: + raise NotImplementedError() diff --git a/aws_lambda_powertools/event_handler/async_execution/routes/cloud_watch_alarm.py b/aws_lambda_powertools/event_handler/async_execution/routes/cloud_watch_alarm.py new file mode 100644 index 00000000000..a6735bd201e --- /dev/null +++ b/aws_lambda_powertools/event_handler/async_execution/routes/cloud_watch_alarm.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +from typing import Any, Callable + +from aws_lambda_powertools.utilities.data_classes.cloud_watch_alarm_event import ( + CloudWatchAlarmEvent, +) + +from .base import BaseRoute + + +class CloudWatchAlarmRoute(BaseRoute): + arn: str | None + alarm_name: str | None + alarm_name_prefix: str | None + + def __init__( + self, + func: Callable, + arn: str | None = None, + alarm_name: str | None = None, + alarm_name_prefix: str | None = None, + ): + self.func = func + self.arn = arn + self.alarm_name = alarm_name + self.alarm_name_prefix = alarm_name_prefix + + if not self.arn and not self.alarm_name and not self.alarm_name_prefix: + raise ValueError("arn, alarm_name, or alarm_name_prefix must be not null") + + def is_target_with_arn(self, arn: str | None) -> bool: + if not arn: + return False + elif self.arn: + return self.arn == arn + else: + return False + + def is_target_with_alarm_name(self, alarm_name: str | None) -> bool: + if not alarm_name: + return False + elif self.alarm_name: + return self.alarm_name == alarm_name + elif self.alarm_name_prefix: + return alarm_name.find(self.alarm_name_prefix) == 0 + else: + return False + + def match(self, event: dict[str, Any]) -> tuple[Callable, CloudWatchAlarmEvent] | None: + if not isinstance(event, dict): + return None + + arn: str | None = event.get("alarmArn") + alarm_name: str | None = event.get("alarmData", {}).get("alarmName") + + if not arn and not alarm_name: + return None + + if not self.arn: + arn = None + + if not self.alarm_name and not self.alarm_name_prefix: + alarm_name = None + + flag_arn = self.is_target_with_arn(arn=arn) + flag_alarm_name = self.is_target_with_alarm_name(alarm_name=alarm_name) + + text = ", ".join( + ["arn: x" if arn is None else "arn: o", "alarm_name: x" if alarm_name is None else "alarm_name: o"], + ) + + mapping = { + "arn: o, alarm_name: o": flag_arn and flag_alarm_name, + "arn: o, alarm_name: x": flag_arn, + "arn: x, alarm_name: o": flag_alarm_name, + "arn: x, alarm_name: x": False, + } + + if mapping[text]: + return self.func, CloudWatchAlarmEvent(event) + else: + return None diff --git a/aws_lambda_powertools/event_handler/async_execution/routes/cloud_watch_logs.py b/aws_lambda_powertools/event_handler/async_execution/routes/cloud_watch_logs.py new file mode 100644 index 00000000000..c8d598c7512 --- /dev/null +++ b/aws_lambda_powertools/event_handler/async_execution/routes/cloud_watch_logs.py @@ -0,0 +1,137 @@ +from __future__ import annotations + +from typing import Any, Callable + +from aws_lambda_powertools.utilities.data_classes.cloud_watch_logs_event import ( + CloudWatchLogsEvent, +) + +from .base import BaseRoute + + +class CloudWatchLogsRoute(BaseRoute): + log_group: str | None + log_group_prefix: str | None + log_stream: str | None + log_stream_prefix: str | None + subscription_filters: list[str] | None + + def __init__( + self, + func: Callable, + log_group: str | None = None, + log_group_prefix: str | None = None, + log_stream: str | None = None, + log_stream_prefix: str | None = None, + subscription_filters: str | list[str] | None = None, + ): + self.func = func + self.log_group = log_group + self.log_group_prefix = log_group_prefix + self.log_stream = log_stream + self.log_stream_prefix = log_stream_prefix + + if isinstance(subscription_filters, str): + self.subscription_filters = [subscription_filters] + else: + self.subscription_filters = subscription_filters + + if ( + not self.log_group + and not self.log_group_prefix + and not self.log_stream + and not self.log_stream_prefix + and not self.subscription_filters + ): + raise ValueError( + "log_group, log_group_prefix, log_stream, log_stream_prefix, or subscription_filters must be not null", + ) + + def is_target_with_log_group(self, log_group: str | None) -> bool: + if not log_group: + return False + elif self.log_group: + return self.log_group == log_group + elif self.log_group_prefix: + return log_group.find(self.log_group_prefix) == 0 + else: + return False + + def is_target_with_log_stream(self, log_stream: str | None) -> bool: + if not log_stream: + return False + elif self.log_stream: + return self.log_stream == log_stream + elif self.log_stream_prefix: + return log_stream.find(self.log_stream_prefix) == 0 + else: + return False + + def is_target_with_subscription_filters(self, subscription_filters: list[str] | None) -> bool: + if not subscription_filters: + return False + elif not self.subscription_filters: + return False + + for name in self.subscription_filters: + if name in subscription_filters: + return True + + return False + + def match(self, event: dict[str, Any]) -> tuple[Callable, CloudWatchLogsEvent] | None: + if not isinstance(event, dict): + return None + + raw_text = event.get("awslogs", {}).get("data") + + if not isinstance(raw_text, str): + return None + + data = CloudWatchLogsEvent(event) + decoded = data.parse_logs_data() + + if self.log_group or self.log_group_prefix: + log_group = decoded.log_group + else: + log_group = None + + if self.log_stream or self.log_stream_prefix: + log_stream = decoded.log_stream + else: + log_stream = None + + if self.subscription_filters: + subscription_filters = decoded.subscription_filters + else: + subscription_filters = None + + flag_log_group = self.is_target_with_log_group(log_group=log_group) + flag_log_stream = self.is_target_with_log_stream(log_stream=log_stream) + flag_subscription_filters = self.is_target_with_subscription_filters(subscription_filters=subscription_filters) + + text = ", ".join( + [ + "log_group: x" if log_group is None else "log_group: o", + "log_stream: x" if log_stream is None else "log_stream: o", + "subscription_filters: x" if subscription_filters is None else "subscription_filters: o", + ], + ) + + mapping = { + "log_group: o, log_stream: o, subscription_filters: o": flag_log_group + and flag_log_stream + and flag_subscription_filters, + "log_group: o, log_stream: o, subscription_filters: x": flag_log_group and flag_log_stream, + "log_group: o, log_stream: x, subscription_filters: o": flag_log_group and flag_subscription_filters, + "log_group: x, log_stream: o, subscription_filters: o": flag_log_stream and flag_subscription_filters, + "log_group: o, log_stream: x, subscription_filters: x": flag_log_group, + "log_group: x, log_stream: o, subscription_filters: x": flag_log_stream, + "log_group: x, log_stream: x, subscription_filters: o": flag_subscription_filters, + "log_group: x, log_stream: x, subscription_filters: x": False, + } + + if mapping[text]: + return self.func, data + else: + return None diff --git a/aws_lambda_powertools/event_handler/async_execution/routes/code_deploy_lifecycle_hook.py b/aws_lambda_powertools/event_handler/async_execution/routes/code_deploy_lifecycle_hook.py new file mode 100644 index 00000000000..1148b601f89 --- /dev/null +++ b/aws_lambda_powertools/event_handler/async_execution/routes/code_deploy_lifecycle_hook.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from typing import Any, Callable + +from aws_lambda_powertools.utilities.data_classes.code_deploy_lifecycle_hook_event import ( + CodeDeployLifecycleHookEvent, +) + +from .base import BaseRoute + + +class CodeDeployLifecycleHookRoute(BaseRoute): + def __init__(self, func: Callable): + self.func = func + + def match(self, event: dict[str, Any]) -> tuple[Callable, CodeDeployLifecycleHookEvent] | None: + if not isinstance(event, dict): + return None + elif "DeploymentId" in event and "LifecycleEventHookExecutionId" in event: + return self.func, CodeDeployLifecycleHookEvent(event) + else: + return None diff --git a/aws_lambda_powertools/event_handler/async_execution/routes/event_bridge.py b/aws_lambda_powertools/event_handler/async_execution/routes/event_bridge.py new file mode 100644 index 00000000000..9c82c1780c5 --- /dev/null +++ b/aws_lambda_powertools/event_handler/async_execution/routes/event_bridge.py @@ -0,0 +1,102 @@ +from __future__ import annotations + +from typing import Any, Callable + +from aws_lambda_powertools.utilities.data_classes.event_bridge_event import ( + EventBridgeEvent, +) + +from .base import BaseRoute + + +class EventBridgeRoute(BaseRoute): + detail_type: str | None + source: str | None + resources: list[str] | None + + def __init__( + self, + func: Callable, + detail_type: str | None = None, + source: str | None = None, + resources: str | list[str] | None = None, + ): + self.func = func + self.detail_type = detail_type + self.source = source + + if isinstance(resources, str): + self.resources = [resources] + else: + self.resources = resources + + if not self.detail_type and not self.source and not self.resources: + raise ValueError("detail_type, source, or resources must be not null") + + def is_target_with_detail_type(self, detail_type: str | None) -> bool: + if detail_type and self.detail_type: + return self.detail_type == detail_type + else: + return False + + def is_target_with_source(self, source: str | None) -> bool: + if source and self.source: + return self.source == source + else: + return False + + def is_target_with_resources(self, resources: list[str] | None) -> bool: + if resources and self.resources: + for item in self.resources: + if item in resources: + return True + + return False + + def match(self, event: dict[str, Any]) -> tuple[Callable, EventBridgeEvent] | None: + if not isinstance(event, dict): + return None + + detail_type: str | None = event.get("detail-type") + source: str | None = event.get("source") + resources: list[str] | None = event.get("resources") + + if not detail_type and not source and not resources: + return None + + if not self.detail_type: + detail_type = None + + if not self.source: + source = None + + if not self.resources: + resources = None + + flag_detail_type = self.is_target_with_detail_type(detail_type=detail_type) + flag_source = self.is_target_with_source(source=source) + flag_resources = self.is_target_with_resources(resources=resources) + + text = ", ".join( + [ + "detail_type: x" if detail_type is None else "detail_type: o", + "source: x" if source is None else "source: o", + "resources: x" if resources is None else "resources: o", + ], + ) + + mapping = { + "detail_type: o, source: o, resources: o": flag_detail_type and flag_source and flag_resources, + "detail_type: o, source: o, resources: x": flag_detail_type and flag_source, + "detail_type: o, source: x, resources: o": flag_detail_type and flag_resources, + "detail_type: x, source: o, resources: o": flag_source and flag_resources, + "detail_type: o, source: x, resources: x": flag_detail_type, + "detail_type: x, source: o, resources: x": flag_source, + "detail_type: x, source: x, resources: o": flag_resources, + "detail_type: x, source: x, resources: x": False, + } + + if mapping[text]: + return self.func, EventBridgeEvent(event) + else: + return None diff --git a/aws_lambda_powertools/event_handler/async_execution/routes/s3.py b/aws_lambda_powertools/event_handler/async_execution/routes/s3.py new file mode 100644 index 00000000000..836fd6bdb13 --- /dev/null +++ b/aws_lambda_powertools/event_handler/async_execution/routes/s3.py @@ -0,0 +1,197 @@ +from __future__ import annotations + +from typing import Any, Callable + +from aws_lambda_powertools.utilities.data_classes.s3_event import S3Event + +from .base import BaseRoute + + +class S3Route(BaseRoute): + bucket: str | None + bucket_prefix: str | None + key: str | None + key_prefix: str | None + key_suffix: str | None + event_name: str | None + event_name_prefix: str | None + configuration_id: str | None + configuration_id_prefix: str | None + + def __init__( + self, + func: Callable, + bucket: str | None = None, + bucket_prefix: str | None = None, + key: str | None = None, + key_prefix: str | None = None, + key_suffix: str | None = None, + event_name: str | None = None, + event_name_prefix: str | None = None, + configuration_id: str | None = None, + configuration_id_prefix: str | None = None, + ): + self.func = func + self.bucket = bucket + self.bucket_prefix = bucket_prefix + self.key = key + self.key_prefix = key_prefix + self.key_suffix = key_suffix + self.event_name = event_name + self.event_name_prefix = event_name_prefix + self.configuration_id = configuration_id + self.configuration_id_prefix = configuration_id_prefix + + if ( + not self.bucket + and not self.bucket_prefix + and not self.key + and not self.key_prefix + and not self.key_suffix + and not self.event_name + and not self.event_name_prefix + and not self.configuration_id + and not self.configuration_id_prefix + ): + raise ValueError( + ( + "bucket, bucket_prefix, key, key_prefix, key_suffix, event_name, event_name_prefix, " + "configuration_id, or configuration_id_prefix must be not null" + ), + ) + + def is_target_with_bucket(self, bucket: str | None) -> bool: + if not bucket: + return False + elif self.bucket: + return self.bucket == bucket + elif self.bucket_prefix: + return bucket.find(self.bucket_prefix) == 0 + else: + return False + + def is_target_with_key(self, key: str | None) -> bool: + if not key: + return False + elif self.key: + return self.key == key + elif self.key_prefix and not self.key_suffix: + return key.find(self.key_prefix) == 0 + elif not self.key_prefix and self.key_suffix: + length_suffix = len(self.key_suffix) + suffix = key[-length_suffix:] + return self.key_suffix == suffix + elif self.key_prefix and self.key_suffix: + length_suffix = len(self.key_suffix) + suffix = key[-length_suffix:] + return key.find(self.key_prefix) == 0 and self.key_suffix == suffix + else: + return False + + def is_target_with_event_name(self, event_name: str | None) -> bool: + if not event_name: + return False + elif self.event_name: + return self.event_name == event_name + elif self.event_name_prefix: + return event_name.find(self.event_name_prefix) == 0 + else: + return False + + def is_target_with_configuration_id(self, configuration_id: str | None) -> bool: + if not configuration_id: + return False + elif self.configuration_id: + return self.configuration_id == configuration_id + elif self.configuration_id_prefix: + return configuration_id.find(self.configuration_id_prefix) == 0 + else: + return False + + def is_target( + self, + bucket: str | None, + key: str | None, + event_name: str | None, + configuration_id: str | None, + ) -> bool: + flag_bucket = self.is_target_with_bucket(bucket=bucket) + flag_key = self.is_target_with_key(key=key) + flag_event_name = self.is_target_with_event_name(event_name=event_name) + flag_configuration_id = self.is_target_with_configuration_id(configuration_id=configuration_id) + + text = ", ".join( + [ + "bucket: x" if bucket is None else "bucket: o", + "key: x" if key is None else "key: o", + "event_name: x" if event_name is None else "event_name: o", + "configuration_id: x" if configuration_id is None else "configuration_id: o", + ], + ) + + mapping = { + "bucket: o, key: o, event_name: o, configuration_id: o": flag_bucket + and flag_key + and flag_event_name + and flag_configuration_id, + "bucket: o, key: o, event_name: o, configuration_id: x": flag_bucket and flag_key and flag_event_name, + "bucket: o, key: o, event_name: x, configuration_id: o": flag_bucket and flag_key and flag_configuration_id, + "bucket: o, key: x, event_name: o, configuration_id: o": flag_bucket + and flag_event_name + and flag_configuration_id, + "bucket: x, key: o, event_name: o, configuration_id: o": flag_key + and flag_event_name + and flag_configuration_id, + "bucket: o, key: o, event_name: x, configuration_id: x": flag_bucket and flag_key, + "bucket: o, key: x, event_name: o, configuration_id: x": flag_bucket and flag_event_name, + "bucket: x, key: o, event_name: o, configuration_id: x": flag_key and flag_event_name, + "bucket: o, key: x, event_name: x, configuration_id: o": flag_bucket and flag_configuration_id, + "bucket: x, key: o, event_name: x, configuration_id: o": flag_key and flag_configuration_id, + "bucket: x, key: x, event_name: o, configuration_id: o": flag_event_name and flag_configuration_id, + "bucket: o, key: x, event_name: x, configuration_id: x": flag_bucket, + "bucket: x, key: o, event_name: x, configuration_id: x": flag_key, + "bucket: x, key: x, event_name: o, configuration_id: x": flag_event_name, + "bucket: x, key: x, event_name: x, configuration_id: o": flag_configuration_id, + "bucket: x, key: x, event_name: x, configuration_id: x": False, + } + + return mapping[text] + + def match(self, event: dict[str, Any]) -> tuple[Callable, S3Event] | None: + if not isinstance(event, dict): + return None + + all_records: list[dict[str, Any]] = event.get("Records", []) + + if len(all_records) == 0: + return None + + record = all_records[0] + event_name = record.get("eventName") + s3_data = record.get("s3") + if not event_name or not s3_data: + return None + + bucket: str | None = s3_data.get("bucket", {}).get("name") + key: str | None = s3_data.get("object", {}).get("key") + configuration_id: str | None = s3_data.get("configurationId") + + if not bucket and not key and not configuration_id: + return None + + if not self.bucket and not self.bucket_prefix: + bucket = None + + if not self.key and not self.key_prefix and not self.key_suffix: + key = None + + if not self.event_name and not self.event_name_prefix: + event_name = None + + if not self.configuration_id and not self.configuration_id_prefix: + configuration_id = None + + if self.is_target(bucket, key, event_name, configuration_id): + return self.func, S3Event(event) + else: + return None diff --git a/aws_lambda_powertools/event_handler/async_execution/routes/secrets_manager.py b/aws_lambda_powertools/event_handler/async_execution/routes/secrets_manager.py new file mode 100644 index 00000000000..88fc059bb93 --- /dev/null +++ b/aws_lambda_powertools/event_handler/async_execution/routes/secrets_manager.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +from typing import Any, Callable + +from aws_lambda_powertools.utilities.data_classes.secrets_manager_event import ( + SecretsManagerEvent, +) + +from .base import BaseRoute + + +class SecretsManagerRoute(BaseRoute): + secret_id: str | None + secret_name_prefix: str | None + + def __init__(self, func: Callable, secret_id: str | None = None, secret_name_prefix: str | None = None): + self.func = func + self.secret_id = secret_id + self.secret_name_prefix = secret_name_prefix + + if not self.secret_id and not self.secret_name_prefix: + raise ValueError("secret_id, or secret_name_prefix must be not null") + + def match(self, event: dict[str, Any]) -> tuple[Callable, SecretsManagerEvent] | None: + if not isinstance(event, dict): + return None + + secret_id: str | None = event.get("SecretId") + + if not secret_id: + return None + elif self.secret_id and self.secret_name_prefix: + part = secret_id.split(":") + secret_name = part[-1] + if self.secret_id == secret_id and secret_name.find(self.secret_name_prefix) == 0: + return self.func, SecretsManagerEvent(event) + elif self.secret_id: + if self.secret_id == secret_id: + return self.func, SecretsManagerEvent(event) + elif self.secret_name_prefix: + part = secret_id.split(":") + secret_name = part[-1] + if secret_name.find(self.secret_name_prefix) == 0: + return self.func, SecretsManagerEvent(event) + + return None diff --git a/aws_lambda_powertools/event_handler/async_execution/routes/ses.py b/aws_lambda_powertools/event_handler/async_execution/routes/ses.py new file mode 100644 index 00000000000..ba367ff8723 --- /dev/null +++ b/aws_lambda_powertools/event_handler/async_execution/routes/ses.py @@ -0,0 +1,207 @@ +from __future__ import annotations + +from typing import Any, Callable + +from aws_lambda_powertools.utilities.data_classes.ses_event import SESEvent + +from .base import BaseRoute + + +def match_prefix(all_address: list[str], all_prefix: list[str]) -> bool: + for address in all_address: + for prefix in all_prefix: + if address.find(prefix) == 0: + return True + return False + + +def match_suffix(all_address: list[str], all_suffix: list[str]) -> bool: + for address in all_address: + for suffix in all_suffix: + length_suffix = len(suffix) + address_suffix = address[-length_suffix:] + if address_suffix == suffix: + return True + return False + + +def match_prefix_and_suffix(all_address: list[str], all_prefix: list[str], all_suffix: list[str]) -> bool: + for address in all_address: + for prefix in all_prefix: + for suffix in all_suffix: + length_suffix = len(suffix) + address_suffix = address[-length_suffix:] + if address.find(prefix) == 0 and address_suffix == suffix: + return True + return False + + +class SESRoute(BaseRoute): + mail_to: list[str] | None + mail_to_prefix: list[str] | None + mail_to_suffix: list[str] | None + mail_from: list[str] | None + mail_from_prefix: list[str] | None + mail_from_suffix: list[str] | None + mail_subject: str | None + mail_subject_prefix: str | None + + def __init__( + self, + func: Callable, + mail_to: str | list[str] | None = None, + mail_to_prefix: str | list[str] | None = None, + mail_to_suffix: str | list[str] | None = None, + mail_from: str | list[str] | None = None, + mail_from_prefix: str | list[str] | None = None, + mail_from_suffix: str | list[str] | None = None, + mail_subject: str | None = None, + mail_subject_prefix: str | None = None, + ): + self.func = func + + if isinstance(mail_to, str): + self.mail_to = [mail_to] + else: + self.mail_to = mail_to + + if isinstance(mail_to_prefix, str): + self.mail_to_prefix = [mail_to_prefix] + else: + self.mail_to_prefix = mail_to_prefix + + if isinstance(mail_to_suffix, str): + self.mail_to_suffix = [mail_to_suffix] + else: + self.mail_to_suffix = mail_to_suffix + + if isinstance(mail_from, str): + self.mail_from = [mail_from] + else: + self.mail_from = mail_from + + if isinstance(mail_from_prefix, str): + self.mail_from_prefix = [mail_from_prefix] + else: + self.mail_from_prefix = mail_from_prefix + + if isinstance(mail_from_suffix, str): + self.mail_from_suffix = [mail_from_suffix] + else: + self.mail_from_suffix = mail_from_suffix + + self.mail_subject = mail_subject + self.mail_subject_prefix = mail_subject_prefix + + if not self.mail_to and not self.mail_from and not self.mail_subject: + raise ValueError( + ( + "mail_to, mail_to_prefix, mail_to_suffix, mail_from, mail_from_prefix, mail_from_suffix, " + "mail_subject, or mail_subject_prefix must be not null" + ), + ) + + def is_target_with_mail_to(self, mail_to: list[str] | None) -> bool: + if not mail_to: + return False + elif self.mail_to: + for address in mail_to: + if address in self.mail_to: + return True + elif self.mail_to_prefix and self.mail_to_suffix: + return match_prefix_and_suffix( + all_address=mail_to, + all_prefix=self.mail_to_prefix, + all_suffix=self.mail_to_suffix, + ) + elif self.mail_to_prefix: + return match_prefix(all_address=mail_to, all_prefix=self.mail_to_prefix) + elif self.mail_to_suffix: + return match_suffix(all_address=mail_to, all_suffix=self.mail_to_suffix) + + return False + + def is_target_with_mail_from(self, mail_from: list[str] | None) -> bool: + if not mail_from: + return False + elif self.mail_from: + for address in mail_from: + if address in self.mail_from: + return True + elif self.mail_from_prefix and self.mail_from_suffix: + return match_prefix_and_suffix( + all_address=mail_from, + all_prefix=self.mail_from_prefix, + all_suffix=self.mail_from_suffix, + ) + elif self.mail_from_prefix: + return match_prefix(all_address=mail_from, all_prefix=self.mail_from_prefix) + elif self.mail_from_suffix: + return match_suffix(all_address=mail_from, all_suffix=self.mail_from_suffix) + + return False + + def is_target_with_mail_subject(self, mail_subject: str | None) -> bool: + if not mail_subject: + return False + elif self.mail_subject: + return self.mail_subject == mail_subject + elif self.mail_subject_prefix: + return mail_subject.find(self.mail_subject_prefix) == 0 + else: + return False + + def match(self, event: dict[str, Any]) -> tuple[Callable, SESEvent] | None: + if not isinstance(event, dict): + return None + + all_records: list[dict[str, Any]] = event.get("Records", []) + + if len(all_records) == 0: + return None + + common_header: dict[str, Any] | None = all_records[0].get("ses", {}).get("mail", {}).get("commonHeaders") + + if common_header is None: + return None + + mail_to = common_header.get("to") + mail_from = common_header.get("from") + mail_subject = common_header.get("subject") + + if not self.mail_to and not self.mail_to_prefix and not self.mail_to_suffix: + mail_to = None + + if not self.mail_from and not self.mail_from_prefix and not self.mail_from_suffix: + mail_from = None + + if not self.mail_subject and not self.mail_subject_prefix: + mail_subject = None + + flag_mail_to = self.is_target_with_mail_to(mail_to=mail_to) + flag_mail_from = self.is_target_with_mail_from(mail_from=mail_from) + flag_mail_subject = self.is_target_with_mail_subject(mail_subject=mail_subject) + + text = ", ".join( + [ + "mail_to: x" if mail_to is None else "mail_to: o", + "mail_from: x" if mail_from is None else "mail_from: o", + "mail_subject: x" if mail_subject is None else "mail_subject: o", + ], + ) + + mapping = { + "mail_to: o, mail_from: o, mail_subject: o": flag_mail_to and flag_mail_from and flag_mail_subject, + "mail_to: o, mail_from: o, mail_subject: x": flag_mail_to and flag_mail_from, + "mail_to: o, mail_from: x, mail_subject: o": flag_mail_to and flag_mail_subject, + "mail_to: x, mail_from: o, mail_subject: o": flag_mail_from and flag_mail_subject, + "mail_to: o, mail_from: x, mail_subject: x": flag_mail_to, + "mail_to: x, mail_from: o, mail_subject: x": flag_mail_from, + "mail_to: x, mail_from: x, mail_subject: o": flag_mail_subject, + "mail_to: x, mail_from: x, mail_subject: x": False, + } + + if mapping[text]: + return self.func, SESEvent(event) + else: + return None diff --git a/aws_lambda_powertools/event_handler/async_execution/routes/sns.py b/aws_lambda_powertools/event_handler/async_execution/routes/sns.py new file mode 100644 index 00000000000..f0c15baed0a --- /dev/null +++ b/aws_lambda_powertools/event_handler/async_execution/routes/sns.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +from typing import Any, Callable + +from aws_lambda_powertools.utilities.data_classes.sns_event import SNSEvent + +from .base import BaseRoute + + +class SNSRoute(BaseRoute): + arn: str | None + name: str | None + name_prefix: str | None + subject: str | None + subject_prefix: str | None + + def __init__( + self, + func: Callable, + arn: str | None = None, + name: str | None = None, + name_prefix: str | None = None, + subject: str | None = None, + subject_prefix: str | None = None, + ): + self.func = func + self.arn = arn + self.name = name + self.name_prefix = name_prefix + self.subject = subject + self.subject_prefix = subject_prefix + + if not self.arn and not self.name and not self.name_prefix and not self.subject and not self.subject_prefix: + raise ValueError("arn, name, name_prefix, subject, or subject_prefix must be not null") + + def is_target_with_arn(self, arn: str | None) -> bool: + if not arn: + return False + elif self.arn: + return self.arn == arn + + part = arn.split(":") + name = part[-1] + if self.name: + return self.name == name + elif self.name_prefix: + return name.find(self.name_prefix) == 0 + else: + return False + + def is_target_with_subject(self, subject: str | None) -> bool: + if not subject: + return False + elif self.subject: + return self.subject == subject + elif self.subject_prefix: + return subject.find(self.subject_prefix) == 0 + else: + return False + + def is_target(self, arn: str | None, subject: str | None) -> bool: + if arn and subject: + return self.is_target_with_arn(arn) and self.is_target_with_subject(subject) + elif arn and not subject: + return self.is_target_with_arn(arn) + elif not arn and subject: + return self.is_target_with_subject(subject) + else: + return False + + def match(self, event: dict[str, Any]) -> tuple[Callable, SNSEvent] | None: + if not isinstance(event, dict): + return None + + all_records: list[dict[str, Any]] = event.get("Records", []) + + if len(all_records) == 0: + return None + + sns_data: dict[str, Any] | None = all_records[0].get("Sns") + if not sns_data: + return None + + if self.arn or self.name or self.name_prefix: + arn = sns_data.get("TopicArn") + else: + arn = None + + if self.subject or self.subject_prefix: + subject = sns_data.get("Subject") + else: + subject = None + + flag_arn = self.is_target_with_arn(arn=arn) + flag_subject = self.is_target_with_subject(subject=subject) + + text = ", ".join(["arn: x" if arn is None else "arn: o", "subject: x" if subject is None else "subject: o"]) + + mapping = { + "arn: o, subject: o": flag_arn and flag_subject, + "arn: o, subject: x": flag_arn, + "arn: x, subject: o": flag_subject, + "arn: x, subject: x": False, + } + + if mapping[text]: + return self.func, SNSEvent(event) + else: + return None diff --git a/aws_lambda_powertools/event_handler/async_trigger.py b/aws_lambda_powertools/event_handler/async_trigger.py new file mode 100644 index 00000000000..60fd00f59e8 --- /dev/null +++ b/aws_lambda_powertools/event_handler/async_trigger.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from .async_execution.exceptions import RouteNotFoundError +from .async_execution.router import Router + +if TYPE_CHECKING: + from aws_lambda_powertools.utilities.typing import LambdaContext + + +class AsyncTriggerResolver(Router): + current_event: Any + lambda_context: LambdaContext | None + raise_when_not_found: bool + + def __init__(self, raise_when_not_found: bool = True): + super().__init__() + self.current_event = None + self.lambda_context = None + self.raise_when_not_found = raise_when_not_found + + def resolve(self, event: dict[str, Any], context: LambdaContext): + data = self.resolve_route(event=event) + if data is None: + if self.raise_when_not_found: + raise RouteNotFoundError() + else: + return + func, current_event = data + try: + self.current_event = current_event + self.lambda_context = context + return func() + finally: + self.current_event = None + self.lambda_context = None diff --git a/tests/unit/event_handler/_async_execution/__init__.py b/tests/unit/event_handler/_async_execution/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/unit/event_handler/_async_execution/_routes/__init__.py b/tests/unit/event_handler/_async_execution/_routes/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/unit/event_handler/_async_execution/_routes/test_aws_config_rule.py b/tests/unit/event_handler/_async_execution/_routes/test_aws_config_rule.py new file mode 100644 index 00000000000..b17065b8d59 --- /dev/null +++ b/tests/unit/event_handler/_async_execution/_routes/test_aws_config_rule.py @@ -0,0 +1,1080 @@ +import pytest + +from aws_lambda_powertools.event_handler.async_execution.router import ( + AwsConfigRuleRoute, +) +from aws_lambda_powertools.utilities.data_classes.aws_config_rule_event import ( + AWSConfigRuleEvent, +) +from tests.functional.utils import load_event + + +class TestAwsConfigRuleRoute: + def test_constructor_error(self): + with pytest.raises(ValueError): + AwsConfigRuleRoute(func=lambda _: None) + + @pytest.mark.parametrize( + "option_constructor, option_func, expected", + [ + ({"func": None, "arn": "test-arn"}, {"arn": None}, False), + ({"func": None, "arn": "test-arn"}, {"arn": "test-arn"}, True), + ({"func": None, "arn": "test-arn-v2"}, {"arn": "test-arn"}, False), + ({"func": None, "rule_name": "test-rule"}, {"arn": "test-arn"}, False), + ], + ) + def test_is_target_with_arn(self, option_constructor, option_func, expected): + route = AwsConfigRuleRoute(**option_constructor) + actual = route.is_target_with_arn(**option_func) + assert actual == expected + + @pytest.mark.parametrize( + "option_constructor, option_func, expected", + [ + ({"func": None, "rule_name": "test-rule"}, {"rule_name": None}, False), + ({"func": None, "rule_name": "test-rule"}, {"rule_name": "test-rule"}, True), + ({"func": None, "rule_name": "test-rule-v2"}, {"rule_name": "test-rule"}, False), + ({"func": None, "rule_name_prefix": "test-r"}, {"rule_name": "test-rule"}, True), + ({"func": None, "rule_name_prefix": "test-rr"}, {"rule_name": "test-rule"}, False), + ({"func": None, "rule_name": "test-rule", "rule_name_prefix": "test-r"}, {"rule_name": "test-rule"}, True), + ({"func": None, "rule_name": "test-rule", "rule_name_prefix": "test-rr"}, {"rule_name": "test-rule"}, True), + ( + {"func": None, "rule_name": "test-rule-v2", "rule_name_prefix": "test-r"}, + {"rule_name": "test-rule"}, + False, + ), + ( + {"func": None, "rule_name": "test-rule-v2", "rule_name_prefix": "test-r"}, + {"rule_name": "test-rule"}, + False, + ), + ({"func": None, "arn": "test-arn"}, {"rule_name": "test-rule"}, False), + ], + ) + def test_is_target_with_rule_name(self, option_constructor, option_func, expected): + route = AwsConfigRuleRoute(**option_constructor) + actual = route.is_target_with_rule_name(**option_func) + assert actual == expected + + @pytest.mark.parametrize( + "option_constructor, option_func, expected", + [ + ({"func": None, "rule_id": "test-id"}, {"rule_id": None}, False), + ({"func": None, "rule_id": "test-id"}, {"rule_id": "test-id"}, True), + ({"func": None, "rule_id": "test-id-v2"}, {"rule_id": "test-id"}, False), + ({"func": None, "arn": "test-arn"}, {"rule_id": "test-id"}, False), + ], + ) + def test_is_target_with_rule_id(self, option_constructor, option_func, expected): + route = AwsConfigRuleRoute(**option_constructor) + actual = route.is_target_with_rule_id(**option_func) + assert actual == expected + + @pytest.mark.parametrize( + "event_name, option_constructor, expected_true", + [ + # awsConfigRuleConfigurationChanged.json + # with arn, rule_name, rule_name_prefix, and rule_id + # match all + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-i9y8j9", + "rule_name": "MyRule", + "rule_name_prefix": "MyR", + "rule_id": "config-rule-i9y8j9", + }, + True, + ), + # awsConfigRuleConfigurationChanged.json + # with arn, rule_name, rule_name_prefix, and rule_id + # match 3, unmatch 1 + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-i9y8j9", + "rule_name": "MyRule", + "rule_name_prefix": "MyR", + "rule_id": "config-rule-000000", + }, + False, + ), + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-i9y8j9", + "rule_name": "MyRule", + "rule_name_prefix": "MyRR", + "rule_id": "config-rule-i9y8j9", + }, + True, + ), + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-i9y8j9", + "rule_name": "MyRuleV2", + "rule_name_prefix": "MyR", + "rule_id": "config-rule-i9y8j9", + }, + False, + ), + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-000000", + "rule_name": "MyRule", + "rule_name_prefix": "MyR", + "rule_id": "config-rule-i9y8j9", + }, + False, + ), + # awsConfigRuleConfigurationChanged.json + # with arn, rule_name, rule_name_prefix, and rule_id + # match 2, unmatch 2 + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-i9y8j9", + "rule_name": "MyRule", + "rule_name_prefix": "MyRR", + "rule_id": "config-rule-000000", + }, + False, + ), + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-i9y8j9", + "rule_name": "MyRuleV2", + "rule_name_prefix": "MyR", + "rule_id": "config-rule-000000", + }, + False, + ), + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-000000", + "rule_name": "MyRule", + "rule_name_prefix": "MyR", + "rule_id": "config-rule-000000", + }, + False, + ), + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-i9y8j9", + "rule_name": "MyRuleV2", + "rule_name_prefix": "MyRR", + "rule_id": "config-rule-i9y8j9", + }, + False, + ), + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-000000", + "rule_name": "MyRule", + "rule_name_prefix": "MyRR", + "rule_id": "config-rule-i9y8j9", + }, + False, + ), + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-000000", + "rule_name": "MyRuleV2", + "rule_name_prefix": "MyR", + "rule_id": "config-rule-i9y8j9", + }, + False, + ), + # awsConfigRuleConfigurationChanged.json + # with arn, rule_name, rule_name_prefix, and rule_id + # match 1, unmatch 3 + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-i9y8j9", + "rule_name": "MyRuleV2", + "rule_name_prefix": "MyRR", + "rule_id": "config-rule-000000", + }, + False, + ), + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-000000", + "rule_name": "MyRule", + "rule_name_prefix": "MyRR", + "rule_id": "config-rule-000000", + }, + False, + ), + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-000000", + "rule_name": "MyRuleV2", + "rule_name_prefix": "MyR", + "rule_id": "config-rule-000000", + }, + False, + ), + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-000000", + "rule_name": "MyRuleV2", + "rule_name_prefix": "MyRR", + "rule_id": "config-rule-i9y8j9", + }, + False, + ), + # awsConfigRuleConfigurationChanged.json + # with arn, rule_name, rule_name_prefix, and rule_id + # unmatch all + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-000000", + "rule_name": "MyRuleV2", + "rule_name_prefix": "MyRR", + "rule_id": "config-rule-000000", + }, + False, + ), + # awsConfigRuleConfigurationChanged.json + # with arn, rule_name, and rule_name_prefix + # match all + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-i9y8j9", + "rule_name": "MyRule", + "rule_name_prefix": "MyR", + }, + True, + ), + # awsConfigRuleConfigurationChanged.json + # with arn, rule_name, and rule_name_prefix + # match 2, unmatch 1 + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-i9y8j9", + "rule_name": "MyRule", + "rule_name_prefix": "MyRR", + }, + True, + ), + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-i9y8j9", + "rule_name": "MyRuleV2", + "rule_name_prefix": "MyR", + }, + False, + ), + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-000000", + "rule_name": "MyRule", + "rule_name_prefix": "MyR", + }, + False, + ), + # awsConfigRuleConfigurationChanged.json + # with arn, rule_name, and rule_name_prefix + # match 1, unmatch 2 + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-i9y8j9", + "rule_name": "MyRuleV2", + "rule_name_prefix": "MyRR", + }, + False, + ), + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-000000", + "rule_name": "MyRule", + "rule_name_prefix": "MyRR", + }, + False, + ), + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-000000", + "rule_name": "MyRuleV2", + "rule_name_prefix": "MyR", + }, + False, + ), + # awsConfigRuleConfigurationChanged.json + # with arn, rule_name, and rule_name_prefix + # unmatch all + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-000000", + "rule_name": "MyRuleV2", + "rule_name_prefix": "MyRR", + }, + False, + ), + # awsConfigRuleConfigurationChanged.json + # with arn, rule_name, and rule_id + # match all + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-i9y8j9", + "rule_name": "MyRule", + "rule_id": "config-rule-i9y8j9", + }, + True, + ), + # awsConfigRuleConfigurationChanged.json + # with arn, rule_name, and rule_id + # match 2, unmatch 1 + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-i9y8j9", + "rule_name": "MyRule", + "rule_id": "config-rule-000000", + }, + False, + ), + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-i9y8j9", + "rule_name": "MyRuleV2", + "rule_id": "config-rule-i9y8j9", + }, + False, + ), + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-000000", + "rule_name": "MyRule", + "rule_id": "config-rule-i9y8j9", + }, + False, + ), + # awsConfigRuleConfigurationChanged.json + # with arn, rule_name, and rule_id + # match 1, unmatch 2 + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-i9y8j9", + "rule_name": "MyRuleV2", + "rule_id": "config-rule-000000", + }, + False, + ), + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-000000", + "rule_name": "MyRule", + "rule_id": "config-rule-000000", + }, + False, + ), + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-000000", + "rule_name": "MyRuleV2", + "rule_id": "config-rule-i9y8j9", + }, + False, + ), + # awsConfigRuleConfigurationChanged.json + # with arn, rule_name, and rule_id + # unmatch all + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-000000", + "rule_name": "MyRuleV2", + "rule_id": "config-rule-000000", + }, + False, + ), + # awsConfigRuleConfigurationChanged.json + # with arn, rule_name_prefix, and rule_id + # match all + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-i9y8j9", + "rule_name_prefix": "MyR", + "rule_id": "config-rule-i9y8j9", + }, + True, + ), + # awsConfigRuleConfigurationChanged.json + # with arn, rule_name_prefix, and rule_id + # match 2, unmatch 1 + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-i9y8j9", + "rule_name_prefix": "MyR", + "rule_id": "config-rule-000000", + }, + False, + ), + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-i9y8j9", + "rule_name_prefix": "MyRR", + "rule_id": "config-rule-i9y8j9", + }, + False, + ), + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-i9y8j9", + "rule_name_prefix": "MyR", + "rule_id": "config-rule-000000", + }, + False, + ), + # awsConfigRuleConfigurationChanged.json + # with arn, rule_name_prefix, and rule_id + # match 1, unmatch 2 + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-i9y8j9", + "rule_name_prefix": "MyRR", + "rule_id": "config-rule-000000", + }, + False, + ), + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-000000", + "rule_name_prefix": "MyR", + "rule_id": "config-rule-000000", + }, + False, + ), + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-000000", + "rule_name_prefix": "MyRR", + "rule_id": "config-rule-i9y8j9", + }, + False, + ), + # awsConfigRuleConfigurationChanged.json + # with arn, rule_name_prefix, and rule_id + # unmatch all + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-000000", + "rule_name_prefix": "MyRR", + "rule_id": "config-rule-000000", + }, + False, + ), + # awsConfigRuleConfigurationChanged.json + # with rule_name, rule_name_prefix, and rule_id + # match all + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "rule_name": "MyRule", + "rule_name_prefix": "MyR", + "rule_id": "config-rule-i9y8j9", + }, + True, + ), + # awsConfigRuleConfigurationChanged.json + # with rule_name, rule_name_prefix, and rule_id + # match 2, unmatch 1 + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "rule_name": "MyRule", + "rule_name_prefix": "MyR", + "rule_id": "config-rule-000000", + }, + False, + ), + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "rule_name": "MyRule", + "rule_name_prefix": "MyRR", + "rule_id": "config-rule-i9y8j9", + }, + True, + ), + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "rule_name": "MyRuleV2", + "rule_name_prefix": "MyR", + "rule_id": "config-rule-i9y8j9", + }, + False, + ), + # awsConfigRuleConfigurationChanged.json + # with rule_name, rule_name_prefix, and rule_id + # match 1, unmatch 2 + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "rule_name": "MyRule", + "rule_name_prefix": "MyRR", + "rule_id": "config-rule-000000", + }, + False, + ), + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "rule_name": "MyRuleV2", + "rule_name_prefix": "MyR", + "rule_id": "config-rule-000000", + }, + False, + ), + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "rule_name": "MyRuleV2", + "rule_name_prefix": "MyRR", + "rule_id": "config-rule-i9y8j9", + }, + False, + ), + # awsConfigRuleConfigurationChanged.json + # with rule_name, rule_name_prefix, and rule_id + # unmatch all + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "rule_name": "MyRuleV2", + "rule_name_prefix": "MyRR", + "rule_id": "config-rule-000000", + }, + False, + ), + # awsConfigRuleConfigurationChanged.json + # with arn and rule_name + # match all + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-i9y8j9", + "rule_name": "MyRule", + }, + True, + ), + # awsConfigRuleConfigurationChanged.json + # with arn and rule_name + # match 1, unmatch 1 + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-i9y8j9", + "rule_name": "MyRuleV2", + }, + False, + ), + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-000000", + "rule_name": "MyRule", + }, + False, + ), + # awsConfigRuleConfigurationChanged.json + # with arn and rule_name + # unmatch all + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-000000", + "rule_name": "MyRuleV2", + }, + False, + ), + # awsConfigRuleConfigurationChanged.json + # with arn and rule_name_prefix + # match all + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-i9y8j9", + "rule_name_prefix": "MyR", + }, + True, + ), + # awsConfigRuleConfigurationChanged.json + # with arn and rule_name_prefix + # match 1, unmatch 1 + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-i9y8j9", + "rule_name_prefix": "MyRR", + }, + False, + ), + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-000000", + "rule_name_prefix": "MyR", + }, + False, + ), + # awsConfigRuleConfigurationChanged.json + # with arn and rule_name_prefix + # unmatch all + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-000000", + "rule_name_prefix": "MyRR", + }, + False, + ), + # awsConfigRuleConfigurationChanged.json + # with arn and rule_id + # match all + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-i9y8j9", + "rule_id": "config-rule-i9y8j9", + }, + True, + ), + # awsConfigRuleConfigurationChanged.json + # with arn and rule_id + # match 1, unmatch 1 + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-i9y8j9", + "rule_id": "config-rule-000000", + }, + False, + ), + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-000000", + "rule_id": "config-rule-i9y8j9", + }, + False, + ), + # awsConfigRuleConfigurationChanged.json + # with arn and rule_id + # unmatch all + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-000000", + "rule_id": "config-rule-000000", + }, + False, + ), + # awsConfigRuleConfigurationChanged.json + # with rule_name and rule_name_prefix + # match all + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "rule_name": "MyRule", + "rule_name_prefix": "MyR", + }, + True, + ), + # awsConfigRuleConfigurationChanged.json + # with rule_name and rule_name_prefix + # match 1, unmatch 1 + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "rule_name": "MyRule", + "rule_name_prefix": "MyRR", + }, + True, + ), + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "rule_name": "MyRuleV2", + "rule_name_prefix": "MyR", + }, + False, + ), + # awsConfigRuleConfigurationChanged.json + # with rule_name and rule_name_prefix + # unmatch all + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "rule_name": "MyRuleV2", + "rule_name_prefix": "MyRR", + }, + False, + ), + # awsConfigRuleConfigurationChanged.json + # with rule_name and rule_id + # match all + ( + "awsConfigRuleConfigurationChanged.json", + {"func": lambda *_: None, "rule_name": "MyRule", "rule_id": "config-rule-i9y8j9"}, + True, + ), + # awsConfigRuleConfigurationChanged.json + # with rule_name and rule_id + # match 1, unmatch 1 + ( + "awsConfigRuleConfigurationChanged.json", + {"func": lambda *_: None, "rule_name": "MyRule", "rule_id": "config-rule-000000"}, + False, + ), + ( + "awsConfigRuleConfigurationChanged.json", + {"func": lambda *_: None, "rule_name": "MyRuleV2", "rule_id": "config-rule-i9y8j9"}, + False, + ), + # awsConfigRuleConfigurationChanged.json + # with rule_name and rule_id + # unmatch all + ( + "awsConfigRuleConfigurationChanged.json", + {"func": lambda *_: None, "rule_name": "MyRuleV2", "rule_id": "config-rule-000000"}, + False, + ), + # awsConfigRuleConfigurationChanged.json + # with rule_name_prefix and rule_id + # match all + ( + "awsConfigRuleConfigurationChanged.json", + {"func": lambda *_: None, "rule_name_prefix": "MyR", "rule_id": "config-rule-i9y8j9"}, + True, + ), + # awsConfigRuleConfigurationChanged.json + # with rule_name_prefix and rule_id + # match 1, unmatch 1 + ( + "awsConfigRuleConfigurationChanged.json", + {"func": lambda *_: None, "rule_name_prefix": "MyR", "rule_id": "config-rule-000000"}, + False, + ), + ( + "awsConfigRuleConfigurationChanged.json", + {"func": lambda *_: None, "rule_name_prefix": "MyRR", "rule_id": "config-rule-i9y8j9"}, + False, + ), + # awsConfigRuleConfigurationChanged.json + # with rule_name_prefix and rule_id + # unmatch all + ( + "awsConfigRuleConfigurationChanged.json", + {"func": lambda *_: None, "rule_name_prefix": "MyRR", "rule_id": "config-rule-000000"}, + False, + ), + # awsConfigRuleConfigurationChanged.json + # with arn + # match all + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-i9y8j9", + }, + True, + ), + # awsConfigRuleConfigurationChanged.json + # with arn + # unmatch all + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "arn": "arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-000000", + }, + False, + ), + # awsConfigRuleConfigurationChanged.json + # with rule_name + # match all + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "rule_name": "MyRule", + }, + True, + ), + # awsConfigRuleConfigurationChanged.json + # with rule_name + # unmatch all + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "rule_name": "MyRuleV2", + }, + False, + ), + # awsConfigRuleConfigurationChanged.json + # with rule_name_prefix + # match all + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "rule_name_prefix": "MyR", + }, + True, + ), + # awsConfigRuleConfigurationChanged.json + # with rule_name_prefix + # unmatch all + ( + "awsConfigRuleConfigurationChanged.json", + { + "func": lambda *_: None, + "rule_name_prefix": "MyRR", + }, + False, + ), + # awsConfigRuleConfigurationChanged.json + # with rule_id + # match all + ( + "awsConfigRuleConfigurationChanged.json", + {"func": lambda *_: None, "rule_id": "config-rule-i9y8j9"}, + True, + ), + # awsConfigRuleConfigurationChanged.json + # with rule_id + # unmatch all + ( + "awsConfigRuleConfigurationChanged.json", + {"func": lambda *_: None, "rule_id": "config-rule-000000"}, + False, + ), + ], + ) + def test_match_with_aws_config_rule_event(self, event_name, option_constructor, expected_true): + event = load_event(file_name=event_name) + route = AwsConfigRuleRoute(**option_constructor) + expected_return = (route.func, AWSConfigRuleEvent(event)) + actual = route.match(event=event) + if expected_true: + assert actual == expected_return + else: + assert actual is None + + @pytest.mark.parametrize( + "event_name", + [ + "activeMQEvent.json", + "albEvent.json", + "albEventPathTrailingSlash.json", + "albMultiValueHeadersEvent.json", + "albMultiValueQueryStringEvent.json", + "apiGatewayAuthorizerRequestEvent.json", + "apiGatewayAuthorizerTokenEvent.json", + "apiGatewayAuthorizerV2Event.json", + "apiGatewayProxyEvent.json", + "apiGatewayProxyEventAnotherPath.json", + "apiGatewayProxyEventNoOrigin.json", + "apiGatewayProxyEventPathTrailingSlash.json", + "apiGatewayProxyEventPrincipalId.json", + "apiGatewayProxyEvent_noVersionAuth.json", + "apiGatewayProxyOtherEvent.json", + "apiGatewayProxyV2Event.json", + "apiGatewayProxyV2EventPathTrailingSlash.json", + "apiGatewayProxyV2Event_GET.json", + "apiGatewayProxyV2IamEvent.json", + "apiGatewayProxyV2LambdaAuthorizerEvent.json", + "apiGatewayProxyV2OtherGetEvent.json", + "apiGatewayProxyV2SchemaMiddlwareInvalidEvent.json", + "apiGatewayProxyV2SchemaMiddlwareValidEvent.json", + "apigatewayeSchemaMiddlwareInvalidEvent.json", + "apigatewayeSchemaMiddlwareValidEvent.json", + "appSyncAuthorizerEvent.json", + "appSyncAuthorizerResponse.json", + "appSyncBatchEvent.json", + "appSyncDirectResolver.json", + "appSyncResolverEvent.json", + "bedrockAgentEvent.json", + "bedrockAgentEventWithPathParams.json", + "bedrockAgentPostEvent.json", + "cloudWatchAlarmEventCompositeMetric.json", + "cloudWatchAlarmEventSingleMetric.json", + "cloudWatchDashboardEvent.json", + "cloudWatchLogEvent.json", + "cloudWatchLogEventWithPolicyLevel.json", + "cloudformationCustomResourceCreate.json", + "cloudformationCustomResourceDelete.json", + "cloudformationCustomResourceUpdate.json", + "codeDeployLifecycleHookEvent.json", + "codePipelineEvent.json", + "codePipelineEventData.json", + "codePipelineEventEmptyUserParameters.json", + "codePipelineEventWithEncryptionKey.json", + "cognitoCreateAuthChallengeEvent.json", + "cognitoCustomEmailSenderEvent.json", + "cognitoCustomMessageEvent.json", + "cognitoCustomSMSSenderEvent.json", + "cognitoDefineAuthChallengeEvent.json", + "cognitoPostAuthenticationEvent.json", + "cognitoPostConfirmationEvent.json", + "cognitoPreAuthenticationEvent.json", + "cognitoPreSignUpEvent.json", + "cognitoPreTokenGenerationEvent.json", + "cognitoPreTokenV2GenerationEvent.json", + "cognitoUserMigrationEvent.json", + "cognitoVerifyAuthChallengeResponseEvent.json", + "connectContactFlowEventAll.json", + "connectContactFlowEventMin.json", + "dynamoStreamEvent.json", + "eventBridgeEvent.json", + "kafkaEventMsk.json", + "kafkaEventSelfManaged.json", + "kinesisFirehoseKinesisEvent.json", + "kinesisFirehosePutEvent.json", + "kinesisFirehoseSQSEvent.json", + "kinesisStreamCloudWatchLogsEvent.json", + "kinesisStreamEvent.json", + "kinesisStreamEventOneRecord.json", + "lambdaFunctionUrlEvent.json", + "lambdaFunctionUrlEventPathTrailingSlash.json", + "lambdaFunctionUrlEventWithHeaders.json", + "lambdaFunctionUrlIAMEvent.json", + "rabbitMQEvent.json", + "s3BatchOperationEventSchemaV1.json", + "s3BatchOperationEventSchemaV2.json", + "s3Event.json", + "s3EventBridgeNotificationObjectCreatedEvent.json", + "s3EventBridgeNotificationObjectDeletedEvent.json", + "s3EventBridgeNotificationObjectExpiredEvent.json", + "s3EventBridgeNotificationObjectRestoreCompletedEvent.json", + "s3EventDecodedKey.json", + "s3EventDeleteObject.json", + "s3EventGlacier.json", + "s3ObjectEventIAMUser.json", + "s3ObjectEventTempCredentials.json", + "s3SqsEvent.json", + "secretsManagerEvent.json", + "sesEvent.json", + "snsEvent.json", + "snsSqsEvent.json", + "snsSqsFifoEvent.json", + "sqsDlqTriggerEvent.json", + "sqsEvent.json", + "vpcLatticeEvent.json", + "vpcLatticeEventPathTrailingSlash.json", + "vpcLatticeEventV2PathTrailingSlash.json", + "vpcLatticeV2Event.json", + "vpcLatticeV2EventWithHeaders.json", + ], + ) + def test_match_for_not_aws_config_rule_event(self, event_name): + event = load_event(file_name=event_name) + route = AwsConfigRuleRoute( + func=None, + arn="arn:aws:config:us-east-1:0123456789012:config-rule/config-rule-i9y8j9", + ) + actual = route.match(event=event) + assert actual is None diff --git a/tests/unit/event_handler/_async_execution/_routes/test_cloud_watch_alarm.py b/tests/unit/event_handler/_async_execution/_routes/test_cloud_watch_alarm.py new file mode 100644 index 00000000000..af6e44a9032 --- /dev/null +++ b/tests/unit/event_handler/_async_execution/_routes/test_cloud_watch_alarm.py @@ -0,0 +1,310 @@ +import pytest + +from aws_lambda_powertools.event_handler.async_execution.routes.cloud_watch_alarm import ( + CloudWatchAlarmRoute, +) +from aws_lambda_powertools.utilities.data_classes.cloud_watch_alarm_event import ( + CloudWatchAlarmEvent, +) +from tests.functional.utils import load_event + + +class TestCloudWatchAlarmRoute: + def test_constructor_error(self): + with pytest.raises(ValueError): + CloudWatchAlarmRoute(func=None) + + @pytest.mark.parametrize( + "option_constructor, option_func, expected", + [ + ( + {"func": None, "arn": "arn:aws:cloudwatch:us-east-1:111122223333:alarm:SuppressionDemo.Main"}, + {"arn": None}, + False, + ), + ( + {"func": None, "arn": "arn:aws:cloudwatch:us-east-1:111122223333:alarm:SuppressionDemo.Main"}, + {"arn": "arn:aws:cloudwatch:us-east-1:111122223333:alarm:SuppressionDemo.Main"}, + True, + ), + ( + {"func": None, "arn": "arn:aws:cloudwatch:us-east-1:111122223333:alarm:SuppressionDemo.MainV2"}, + {"arn": "arn:aws:cloudwatch:us-east-1:111122223333:alarm:SuppressionDemo.Main"}, + False, + ), + ( + {"func": None, "alarm_name": "SuppressionDemo.Main"}, + {"arn": "arn:aws:cloudwatch:us-east-1:111122223333:alarm:SuppressionDemo.Main"}, + False, + ), + ], + ) + def test_is_target_with_arn(self, option_constructor, option_func, expected): + route = CloudWatchAlarmRoute(**option_constructor) + actual = route.is_target_with_arn(**option_func) + assert actual == expected + + @pytest.mark.parametrize( + "option_constructor, option_func, expected", + [ + # with alarm_name and alarm_name_prefix + # match all + ( + {"func": None, "alarm_name": "CompositeDemo.Main", "alarm_name_prefix": "CompositeDemo.M"}, + {"alarm_name": "CompositeDemo.Main"}, + True, + ), + # with alarm_name and alarm_name_prefix + # match 1, unmatch 1 + ( + {"func": None, "alarm_name": "CompositeDemo.Main", "alarm_name_prefix": "ompositeDemo.M"}, + {"alarm_name": "CompositeDemo.Main"}, + True, + ), + ( + {"func": None, "alarm_name": "CompositeDemo.MainV2", "alarm_name_prefix": "CompositeDemo.M"}, + {"alarm_name": "CompositeDemo.Main"}, + False, + ), + # with alarm_name and alarm_name_prefix + # unmatch all + ( + {"func": None, "alarm_name": "CompositeDemo.MainV2", "alarm_name_prefix": "ompositeDemo.M"}, + {"alarm_name": "CompositeDemo.Main"}, + False, + ), + # with alarm_name + ( + { + "func": None, + "alarm_name": "CompositeDemo.Main", + }, + {"alarm_name": "CompositeDemo.Main"}, + True, + ), + ( + { + "func": None, + "alarm_name": "CompositeDemo.MainV2", + }, + {"alarm_name": "CompositeDemo.Main"}, + False, + ), + # with alarm_name_prefix + ({"func": None, "alarm_name_prefix": "CompositeDemo.M"}, {"alarm_name": "CompositeDemo.Main"}, True), + ({"func": None, "alarm_name_prefix": "ompositeDemo.M"}, {"alarm_name": "CompositeDemo.Main"}, False), + # without alarm_name and alarm_name_prefix + ( + {"func": None, "arn": "arn:aws:cloudwatch:us-east-1:111122223333:alarm:SuppressionDemo.Main"}, + {"alarm_name": "CompositeDemo.Main"}, + False, + ), + # without alarm_name at option_func + ({"func": None, "alarm_name_prefix": "ompositeDemo.M"}, {"alarm_name": None}, False), + ], + ) + def test_is_target_with_alarm_name(self, option_constructor, option_func, expected): + route = CloudWatchAlarmRoute(**option_constructor) + actual = route.is_target_with_alarm_name(**option_func) + assert actual == expected + + @pytest.mark.parametrize( + "event_name, option_constructor, is_match", + [ + # with arn and alarm_name + # match all + ( + "cloudWatchAlarmEventCompositeMetric.json", + { + "func": lambda *_: None, + "arn": "arn:aws:cloudwatch:us-east-1:111122223333:alarm:SuppressionDemo.Main", + "alarm_name": "CompositeDemo.Main", + }, + True, + ), + # with arn and alarm_name + # match 1, unmatch 1 + ( + "cloudWatchAlarmEventCompositeMetric.json", + { + "func": lambda *_: None, + "arn": "arn:aws:cloudwatch:us-east-1:111122223333:alarm:SuppressionDemo.MainV2", + "alarm_name": "CompositeDemo.Main", + }, + False, + ), + ( + "cloudWatchAlarmEventCompositeMetric.json", + { + "func": lambda *_: None, + "arn": "arn:aws:cloudwatch:us-east-1:111122223333:alarm:SuppressionDemo.Main", + "alarm_name": "CompositeDemo.MainV2", + }, + False, + ), + # with arn and alarm_name + # unmatch all + ( + "cloudWatchAlarmEventCompositeMetric.json", + { + "func": lambda *_: None, + "arn": "arn:aws:cloudwatch:us-east-1:111122223333:alarm:SuppressionDemo.MainV2", + "alarm_name": "CompositeDemo.MainV2", + }, + False, + ), + # with arn + ( + "cloudWatchAlarmEventCompositeMetric.json", + { + "func": lambda *_: None, + "arn": "arn:aws:cloudwatch:us-east-1:111122223333:alarm:SuppressionDemo.Main", + }, + True, + ), + ( + "cloudWatchAlarmEventCompositeMetric.json", + { + "func": lambda *_: None, + "arn": "arn:aws:cloudwatch:us-east-1:111122223333:alarm:SuppressionDemo.MainV2", + }, + False, + ), + # with alarm_name + ( + "cloudWatchAlarmEventCompositeMetric.json", + {"func": lambda *_: None, "alarm_name": "CompositeDemo.Main"}, + True, + ), + ( + "cloudWatchAlarmEventCompositeMetric.json", + {"func": lambda *_: None, "alarm_name": "CompositeDemo.MainV2"}, + False, + ), + ], + ) + def test_match_for_cloud_watch_alarm_event(self, event_name, option_constructor, is_match): + event = load_event(file_name=event_name) + route = CloudWatchAlarmRoute(**option_constructor) + actual = route.match(event=event) + if is_match: + expected = route.func, CloudWatchAlarmEvent(event) + assert actual == expected + else: + assert actual is None + + @pytest.mark.parametrize( + "event_name", + [ + "activeMQEvent.json", + "albEvent.json", + "albEventPathTrailingSlash.json", + "albMultiValueHeadersEvent.json", + "albMultiValueQueryStringEvent.json", + "apiGatewayAuthorizerRequestEvent.json", + "apiGatewayAuthorizerTokenEvent.json", + "apiGatewayAuthorizerV2Event.json", + "apiGatewayProxyEvent.json", + "apiGatewayProxyEventAnotherPath.json", + "apiGatewayProxyEventNoOrigin.json", + "apiGatewayProxyEventPathTrailingSlash.json", + "apiGatewayProxyEventPrincipalId.json", + "apiGatewayProxyEvent_noVersionAuth.json", + "apiGatewayProxyOtherEvent.json", + "apiGatewayProxyV2Event.json", + "apiGatewayProxyV2EventPathTrailingSlash.json", + "apiGatewayProxyV2Event_GET.json", + "apiGatewayProxyV2IamEvent.json", + "apiGatewayProxyV2LambdaAuthorizerEvent.json", + "apiGatewayProxyV2OtherGetEvent.json", + "apiGatewayProxyV2SchemaMiddlwareInvalidEvent.json", + "apiGatewayProxyV2SchemaMiddlwareValidEvent.json", + "apigatewayeSchemaMiddlwareInvalidEvent.json", + "apigatewayeSchemaMiddlwareValidEvent.json", + "appSyncAuthorizerEvent.json", + "appSyncAuthorizerResponse.json", + "appSyncBatchEvent.json", + "appSyncDirectResolver.json", + "appSyncResolverEvent.json", + "awsConfigRuleConfigurationChanged.json", + "awsConfigRuleOversizedConfiguration.json", + "awsConfigRuleScheduled.json", + "bedrockAgentEvent.json", + "bedrockAgentEventWithPathParams.json", + "bedrockAgentPostEvent.json", + "cloudWatchDashboardEvent.json", + "cloudWatchLogEvent.json", + "cloudWatchLogEventWithPolicyLevel.json", + "cloudformationCustomResourceCreate.json", + "cloudformationCustomResourceDelete.json", + "cloudformationCustomResourceUpdate.json", + "codeDeployLifecycleHookEvent.json", + "codePipelineEvent.json", + "codePipelineEventData.json", + "codePipelineEventEmptyUserParameters.json", + "codePipelineEventWithEncryptionKey.json", + "cognitoCreateAuthChallengeEvent.json", + "cognitoCustomEmailSenderEvent.json", + "cognitoCustomMessageEvent.json", + "cognitoCustomSMSSenderEvent.json", + "cognitoDefineAuthChallengeEvent.json", + "cognitoPostAuthenticationEvent.json", + "cognitoPostConfirmationEvent.json", + "cognitoPreAuthenticationEvent.json", + "cognitoPreSignUpEvent.json", + "cognitoPreTokenGenerationEvent.json", + "cognitoPreTokenV2GenerationEvent.json", + "cognitoUserMigrationEvent.json", + "cognitoVerifyAuthChallengeResponseEvent.json", + "connectContactFlowEventAll.json", + "connectContactFlowEventMin.json", + "dynamoStreamEvent.json", + "eventBridgeEvent.json", + "kafkaEventMsk.json", + "kafkaEventSelfManaged.json", + "kinesisFirehoseKinesisEvent.json", + "kinesisFirehosePutEvent.json", + "kinesisFirehoseSQSEvent.json", + "kinesisStreamCloudWatchLogsEvent.json", + "kinesisStreamEvent.json", + "kinesisStreamEventOneRecord.json", + "lambdaFunctionUrlEvent.json", + "lambdaFunctionUrlEventPathTrailingSlash.json", + "lambdaFunctionUrlEventWithHeaders.json", + "lambdaFunctionUrlIAMEvent.json", + "rabbitMQEvent.json", + "s3BatchOperationEventSchemaV1.json", + "s3BatchOperationEventSchemaV2.json", + "s3Event.json", + "s3EventBridgeNotificationObjectCreatedEvent.json", + "s3EventBridgeNotificationObjectDeletedEvent.json", + "s3EventBridgeNotificationObjectExpiredEvent.json", + "s3EventBridgeNotificationObjectRestoreCompletedEvent.json", + "s3EventDecodedKey.json", + "s3EventDeleteObject.json", + "s3EventGlacier.json", + "s3ObjectEventIAMUser.json", + "s3ObjectEventTempCredentials.json", + "s3SqsEvent.json", + "secretsManagerEvent.json", + "sesEvent.json", + "snsEvent.json", + "snsSqsEvent.json", + "snsSqsFifoEvent.json", + "sqsDlqTriggerEvent.json", + "sqsEvent.json", + "vpcLatticeEvent.json", + "vpcLatticeEventPathTrailingSlash.json", + "vpcLatticeEventV2PathTrailingSlash.json", + "vpcLatticeV2Event.json", + "vpcLatticeV2EventWithHeaders.json", + ], + ) + def test_match_for_not_cloud_watch_alarm_event(self, event_name): + event = load_event(file_name=event_name) + route = CloudWatchAlarmRoute( + func=None, + arn="arn:aws:cloudwatch:us-east-1:111122223333:alarm:SuppressionDemo.Main", + ) + actual = route.match(event=event) + assert actual is None diff --git a/tests/unit/event_handler/_async_execution/_routes/test_cloud_watch_logs.py b/tests/unit/event_handler/_async_execution/_routes/test_cloud_watch_logs.py new file mode 100644 index 00000000000..54a7cecf07a --- /dev/null +++ b/tests/unit/event_handler/_async_execution/_routes/test_cloud_watch_logs.py @@ -0,0 +1,551 @@ +import pytest + +from aws_lambda_powertools.event_handler.async_execution.routes.cloud_watch_logs import ( + CloudWatchLogsRoute, +) +from aws_lambda_powertools.utilities.data_classes.cloud_watch_logs_event import ( + CloudWatchLogsEvent, +) +from tests.functional.utils import load_event + + +class TestCloudWatchLogsRoute: + def test_constructor_error(self): + with pytest.raises(ValueError): + CloudWatchLogsRoute(func=None) + + @pytest.mark.parametrize( + "option, expected", + [ + ( + {"func": None, "log_group": "test"}, + { + "func": None, + "log_group": "test", + "log_group_prefix": None, + "log_stream": None, + "log_stream_prefix": None, + "subscription_filters": None, + }, + ), + ( + {"func": None, "log_group_prefix": "test"}, + { + "func": None, + "log_group": None, + "log_group_prefix": "test", + "log_stream": None, + "log_stream_prefix": None, + "subscription_filters": None, + }, + ), + ( + {"func": None, "log_stream": "test"}, + { + "func": None, + "log_group": None, + "log_group_prefix": None, + "log_stream": "test", + "log_stream_prefix": None, + "subscription_filters": None, + }, + ), + ( + {"func": None, "log_stream_prefix": "test"}, + { + "func": None, + "log_group": None, + "log_group_prefix": None, + "log_stream": None, + "log_stream_prefix": "test", + "subscription_filters": None, + }, + ), + ( + {"func": None, "subscription_filters": "test"}, + { + "func": None, + "log_group": None, + "log_group_prefix": None, + "log_stream": None, + "log_stream_prefix": None, + "subscription_filters": ["test"], + }, + ), + ( + {"func": None, "subscription_filters": ["test", "name"]}, + { + "func": None, + "log_group": None, + "log_group_prefix": None, + "log_stream": None, + "log_stream_prefix": None, + "subscription_filters": ["test", "name"], + }, + ), + ], + ) + def test_constructor_normal(self, option, expected): + route = CloudWatchLogsRoute(**option) + assert route.__dict__ == expected + + @pytest.mark.parametrize( + "option_constructor, option, expected", + [ + # with log_group and log_group_prefix + # match all + ( + {"func": None, "log_group": "testLogGroup", "log_group_prefix": "testL"}, + {"log_group": "testLogGroup"}, + True, + ), + # with log_group and log_group_prefix + # match 1, unmatch 1 + ( + {"func": None, "log_group": "testLogGroup", "log_group_prefix": "estL"}, + {"log_group": "testLogGroup"}, + True, + ), + ( + {"func": None, "log_group": "testLogGroupV2", "log_group_prefix": "testL"}, + {"log_group": "testLogGroup"}, + False, + ), + # with log_group and log_group_prefix + # unmatch all + ( + {"func": None, "log_group": "testLogGroupV2", "log_group_prefix": "estL"}, + {"log_group": "testLogGroup"}, + False, + ), + # with log_group + ( + { + "func": None, + "log_group": "testLogGroup", + }, + {"log_group": "testLogGroup"}, + True, + ), + ( + { + "func": None, + "log_group": "testLogGroupV2", + }, + {"log_group": "testLogGroup"}, + False, + ), + # with log_group_prefix + ({"func": None, "log_group_prefix": "testL"}, {"log_group": "testLogGroup"}, True), + ({"func": None, "log_group_prefix": "estL"}, {"log_group": "testLogGroup"}, False), + # without log_group and log_group_prefix + ({"func": None, "log_stream": "testLogStream"}, {"log_group": "testLogGroup"}, False), + # without log_group at option_func + ({"func": None, "log_group": "testLogGroup"}, {"log_group": None}, False), + ], + ) + def test_is_target_with_log_group(self, option_constructor, option, expected): + route = CloudWatchLogsRoute(**option_constructor) + actual = route.is_target_with_log_group(**option) + assert actual == expected + + @pytest.mark.parametrize( + "option_constructor, option, expected", + [ + # with log_stream and log_stream_prefix + # match all + ( + {"func": None, "log_stream": "testLogStream", "log_stream_prefix": "testL"}, + {"log_stream": "testLogStream"}, + True, + ), + # with log_stream and log_stream_prefix + # match 1, unmatch 1 + ( + {"func": None, "log_stream": "testLogStream", "log_stream_prefix": "estL"}, + {"log_stream": "testLogStream"}, + True, + ), + ( + {"func": None, "log_stream": "testLogStreamV2", "log_stream_prefix": "testL"}, + {"log_stream": "testLogStream"}, + False, + ), + # with log_stream and log_stream_prefix + # unmatch all + ( + {"func": None, "log_stream": "testLogStreamV2", "log_stream_prefix": "estL"}, + {"log_stream": "testLogStream"}, + False, + ), + # with log_stream + ( + { + "func": None, + "log_stream": "testLogStream", + }, + {"log_stream": "testLogStream"}, + True, + ), + ( + { + "func": None, + "log_stream": "testLogStreamV2", + }, + {"log_stream": "testLogStream"}, + False, + ), + # with log_stream_prefix + ({"func": None, "log_stream_prefix": "testL"}, {"log_stream": "testLogStream"}, True), + ({"func": None, "log_stream_prefix": "estL"}, {"log_stream": "testLogStream"}, False), + # without log_stream and log_stream_prefix + ({"func": None, "log_group": "testLogGroup"}, {"log_stream": "testLogStream"}, False), + # without log_stream at option_func + ({"func": None, "log_stream_prefix": "testL"}, {"log_stream": None}, False), + ], + ) + def test_is_target_with_log_stream(self, option_constructor, option, expected): + route = CloudWatchLogsRoute(**option_constructor) + actual = route.is_target_with_log_stream(**option) + assert actual == expected + + @pytest.mark.parametrize( + "option_constructor, option, expected", + [ + # without subscription_filters at option_func + ({"func": None, "subscription_filters": ["test"]}, {"subscription_filters": None}, False), + # without subscription_filters + ({"func": None, "log_group": "test"}, {"subscription_filters": ["test"]}, False), + # with subscription_filters + # match + ({"func": None, "subscription_filters": ["test"]}, {"subscription_filters": ["test"]}, True), + ({"func": None, "subscription_filters": ["test", "name"]}, {"subscription_filters": ["test"]}, True), + ({"func": None, "subscription_filters": ["test"]}, {"subscription_filters": ["test", "name"]}, True), + # with subscription_filters + # unmatch + ({"func": None, "subscription_filters": ["test"]}, {"subscription_filters": ["name"]}, False), + ], + ) + def test_is_target_with_subscription_filters(self, option_constructor, option, expected): + route = CloudWatchLogsRoute(**option_constructor) + actual = route.is_target_with_subscription_filters(**option) + assert actual == expected + + @pytest.mark.parametrize( + "event_name, option_constructor, is_match", + [ + # with log_group, log_stream, and subscription_filters + # match all + ( + "cloudWatchLogEvent.json", + { + "func": lambda *_: None, + "log_group": "testLogGroup", + "log_stream": "testLogStream", + "subscription_filters": ["testFilter"], + }, + True, + ), + # with log_group, log_stream, and subscription_filters + # match 2, unmatch 1 + ( + "cloudWatchLogEvent.json", + { + "func": lambda *_: None, + "log_group": "testLogGroup", + "log_stream": "testLogStream", + "subscription_filters": ["testFilterV2"], + }, + False, + ), + ( + "cloudWatchLogEvent.json", + { + "func": lambda *_: None, + "log_group": "testLogGroup", + "log_stream": "testLogStreamV2", + "subscription_filters": ["testFilter"], + }, + False, + ), + ( + "cloudWatchLogEvent.json", + { + "func": lambda *_: None, + "log_group": "testLogGroupV2", + "log_stream": "testLogStream", + "subscription_filters": ["testFilter"], + }, + False, + ), + # with log_group, log_stream, and subscription_filters + # match 1, unmatch 2 + ( + "cloudWatchLogEvent.json", + { + "func": lambda *_: None, + "log_group": "testLogGroup", + "log_stream": "testLogStreamV2", + "subscription_filters": ["testFilterV2"], + }, + False, + ), + ( + "cloudWatchLogEvent.json", + { + "func": lambda *_: None, + "log_group": "testLogGroupV2", + "log_stream": "testLogStream", + "subscription_filters": ["testFilterV2"], + }, + False, + ), + ( + "cloudWatchLogEvent.json", + { + "func": lambda *_: None, + "log_group": "testLogGroupV2", + "log_stream": "testLogStreamV2", + "subscription_filters": ["testFilter"], + }, + False, + ), + # with log_group, log_stream, and subscription_filters + # unmatch all + ( + "cloudWatchLogEvent.json", + { + "func": lambda *_: None, + "log_group": "testLogGroupV2", + "log_stream": "testLogStreamV2", + "subscription_filters": ["testFilterV2"], + }, + False, + ), + # with log_group and log_stream + # match all + ( + "cloudWatchLogEvent.json", + { + "func": lambda *_: None, + "log_group": "testLogGroup", + "log_stream": "testLogStream", + }, + True, + ), + # with log_group and log_stream + # match 1, unmatch 1 + ( + "cloudWatchLogEvent.json", + { + "func": lambda *_: None, + "log_group": "testLogGroup", + "log_stream": "testLogStreamV2", + }, + False, + ), + ( + "cloudWatchLogEvent.json", + { + "func": lambda *_: None, + "log_group": "testLogGroupV2", + "log_stream": "testLogStream", + }, + False, + ), + # with log_group and log_stream + # unmatch all + ( + "cloudWatchLogEvent.json", + { + "func": lambda *_: None, + "log_group": "testLogGroupV2", + "log_stream": "testLogStreamV2", + }, + False, + ), + # with log_group and subscription_filters + # match all + ( + "cloudWatchLogEvent.json", + {"func": lambda *_: None, "log_group": "testLogGroup", "subscription_filters": ["testFilter"]}, + True, + ), + # with log_group and subscription_filters + # match 1, unmatch 1 + ( + "cloudWatchLogEvent.json", + {"func": lambda *_: None, "log_group": "testLogGroupV2", "subscription_filters": ["testFilter"]}, + False, + ), + ( + "cloudWatchLogEvent.json", + {"func": lambda *_: None, "log_group": "testLogGroup", "subscription_filters": ["testFilterV2"]}, + False, + ), + # with log_group and subscription_filters + # unmatch all + ( + "cloudWatchLogEvent.json", + {"func": lambda *_: None, "log_group": "testLogGroupV2", "subscription_filters": ["testFilterV2"]}, + False, + ), + # with log_stream and subscription_filters + # match all + ( + "cloudWatchLogEvent.json", + {"func": lambda *_: None, "log_stream": "testLogStream", "subscription_filters": ["testFilter"]}, + True, + ), + # with log_stream and subscription_filters + # match 1, unmatch 1 + ( + "cloudWatchLogEvent.json", + {"func": lambda *_: None, "log_stream": "testLogStream", "subscription_filters": ["testFilterV2"]}, + False, + ), + ( + "cloudWatchLogEvent.json", + {"func": lambda *_: None, "log_stream": "testLogStreamV2", "subscription_filters": ["testFilter"]}, + False, + ), + # with log_stream and subscription_filters + # unmatch all + ( + "cloudWatchLogEvent.json", + {"func": lambda *_: None, "log_stream": "testLogStreamV2", "subscription_filters": ["testFilter"]}, + False, + ), + # with log_group + ("cloudWatchLogEvent.json", {"func": lambda *_: None, "log_group": "testLogGroup"}, True), + ("cloudWatchLogEvent.json", {"func": lambda *_: None, "log_group": "testLogGroupV2"}, False), + # with log_stream + ("cloudWatchLogEvent.json", {"func": lambda *_: None, "log_stream": "testLogStream"}, True), + ("cloudWatchLogEvent.json", {"func": lambda *_: None, "log_stream": "testLogStreamV2"}, False), + # with subscription_filters + ("cloudWatchLogEvent.json", {"func": lambda *_: None, "subscription_filters": ["testFilter"]}, True), + ("cloudWatchLogEvent.json", {"func": lambda *_: None, "subscription_filters": ["testFilterV2"]}, False), + ], + ) + def test_match_for_cloud_watch_logs_event(self, event_name, option_constructor, is_match): + event = load_event(file_name=event_name) + route = CloudWatchLogsRoute(**option_constructor) + actual = route.match(event=event) + if is_match: + expected = route.func, CloudWatchLogsEvent(event) + assert actual == expected + else: + assert actual is None + + @pytest.mark.parametrize( + "event_name", + [ + "activeMQEvent.json", + "albEvent.json", + "albEventPathTrailingSlash.json", + "albMultiValueHeadersEvent.json", + "albMultiValueQueryStringEvent.json", + "apiGatewayAuthorizerRequestEvent.json", + "apiGatewayAuthorizerTokenEvent.json", + "apiGatewayAuthorizerV2Event.json", + "apiGatewayProxyEvent.json", + "apiGatewayProxyEventAnotherPath.json", + "apiGatewayProxyEventNoOrigin.json", + "apiGatewayProxyEventPathTrailingSlash.json", + "apiGatewayProxyEventPrincipalId.json", + "apiGatewayProxyEvent_noVersionAuth.json", + "apiGatewayProxyOtherEvent.json", + "apiGatewayProxyV2Event.json", + "apiGatewayProxyV2EventPathTrailingSlash.json", + "apiGatewayProxyV2Event_GET.json", + "apiGatewayProxyV2IamEvent.json", + "apiGatewayProxyV2LambdaAuthorizerEvent.json", + "apiGatewayProxyV2OtherGetEvent.json", + "apiGatewayProxyV2SchemaMiddlwareInvalidEvent.json", + "apiGatewayProxyV2SchemaMiddlwareValidEvent.json", + "apigatewayeSchemaMiddlwareInvalidEvent.json", + "apigatewayeSchemaMiddlwareValidEvent.json", + "appSyncAuthorizerEvent.json", + "appSyncAuthorizerResponse.json", + "appSyncBatchEvent.json", + "appSyncDirectResolver.json", + "appSyncResolverEvent.json", + "awsConfigRuleConfigurationChanged.json", + "awsConfigRuleOversizedConfiguration.json", + "awsConfigRuleScheduled.json", + "bedrockAgentEvent.json", + "bedrockAgentEventWithPathParams.json", + "bedrockAgentPostEvent.json", + "cloudWatchAlarmEventCompositeMetric.json", + "cloudWatchAlarmEventSingleMetric.json", + "cloudWatchDashboardEvent.json", + "cloudformationCustomResourceCreate.json", + "cloudformationCustomResourceDelete.json", + "cloudformationCustomResourceUpdate.json", + "codeDeployLifecycleHookEvent.json", + "codePipelineEvent.json", + "codePipelineEventData.json", + "codePipelineEventEmptyUserParameters.json", + "codePipelineEventWithEncryptionKey.json", + "cognitoCreateAuthChallengeEvent.json", + "cognitoCustomEmailSenderEvent.json", + "cognitoCustomMessageEvent.json", + "cognitoCustomSMSSenderEvent.json", + "cognitoDefineAuthChallengeEvent.json", + "cognitoPostAuthenticationEvent.json", + "cognitoPostConfirmationEvent.json", + "cognitoPreAuthenticationEvent.json", + "cognitoPreSignUpEvent.json", + "cognitoPreTokenGenerationEvent.json", + "cognitoPreTokenV2GenerationEvent.json", + "cognitoUserMigrationEvent.json", + "cognitoVerifyAuthChallengeResponseEvent.json", + "connectContactFlowEventAll.json", + "connectContactFlowEventMin.json", + "dynamoStreamEvent.json", + "eventBridgeEvent.json", + "kafkaEventMsk.json", + "kafkaEventSelfManaged.json", + "kinesisFirehoseKinesisEvent.json", + "kinesisFirehosePutEvent.json", + "kinesisFirehoseSQSEvent.json", + "kinesisStreamCloudWatchLogsEvent.json", + "kinesisStreamEvent.json", + "kinesisStreamEventOneRecord.json", + "lambdaFunctionUrlEvent.json", + "lambdaFunctionUrlEventPathTrailingSlash.json", + "lambdaFunctionUrlEventWithHeaders.json", + "lambdaFunctionUrlIAMEvent.json", + "rabbitMQEvent.json", + "s3BatchOperationEventSchemaV1.json", + "s3BatchOperationEventSchemaV2.json", + "s3Event.json", + "s3EventBridgeNotificationObjectCreatedEvent.json", + "s3EventBridgeNotificationObjectDeletedEvent.json", + "s3EventBridgeNotificationObjectExpiredEvent.json", + "s3EventBridgeNotificationObjectRestoreCompletedEvent.json", + "s3EventDecodedKey.json", + "s3EventDeleteObject.json", + "s3EventGlacier.json", + "s3ObjectEventIAMUser.json", + "s3ObjectEventTempCredentials.json", + "s3SqsEvent.json", + "secretsManagerEvent.json", + "sesEvent.json", + "snsEvent.json", + "snsSqsEvent.json", + "snsSqsFifoEvent.json", + "sqsDlqTriggerEvent.json", + "sqsEvent.json", + "vpcLatticeEvent.json", + "vpcLatticeEventPathTrailingSlash.json", + "vpcLatticeEventV2PathTrailingSlash.json", + "vpcLatticeV2Event.json", + "vpcLatticeV2EventWithHeaders.json", + ], + ) + def test_match_for_not_cloud_watch_logs_event(self, event_name): + event = load_event(file_name=event_name) + route = CloudWatchLogsRoute(func=None, log_group="testLogGroup") + actual = route.match(event=event) + assert actual is None diff --git a/tests/unit/event_handler/_async_execution/_routes/test_code_deploy_lifecycle_hook.py b/tests/unit/event_handler/_async_execution/_routes/test_code_deploy_lifecycle_hook.py new file mode 100644 index 00000000000..9b561e4bd4f --- /dev/null +++ b/tests/unit/event_handler/_async_execution/_routes/test_code_deploy_lifecycle_hook.py @@ -0,0 +1,138 @@ +import pytest + +from aws_lambda_powertools.event_handler.async_execution.routes.code_deploy_lifecycle_hook import ( + CodeDeployLifecycleHookRoute, +) +from aws_lambda_powertools.utilities.data_classes.code_deploy_lifecycle_hook_event import ( + CodeDeployLifecycleHookEvent, +) +from tests.functional.utils import load_event + + +class TestCodeDeployLifecycleHookRoute: + @pytest.mark.parametrize( + "event_name, option_constructor", + [ + ("codeDeployLifecycleHookEvent.json", {"func": None}), + ], + ) + def test_match_true(self, event_name, option_constructor): + event = load_event(file_name=event_name) + route = CodeDeployLifecycleHookRoute(**option_constructor) + expected = (route.func, CodeDeployLifecycleHookEvent(event)) + actual = route.match(event=event) + assert actual == expected + + @pytest.mark.parametrize( + "event_name", + [ + "activeMQEvent.json", + "albEvent.json", + "albEventPathTrailingSlash.json", + "albMultiValueHeadersEvent.json", + "albMultiValueQueryStringEvent.json", + "apiGatewayAuthorizerRequestEvent.json", + "apiGatewayAuthorizerTokenEvent.json", + "apiGatewayAuthorizerV2Event.json", + "apiGatewayProxyEvent.json", + "apiGatewayProxyEventAnotherPath.json", + "apiGatewayProxyEventNoOrigin.json", + "apiGatewayProxyEventPathTrailingSlash.json", + "apiGatewayProxyEventPrincipalId.json", + "apiGatewayProxyEvent_noVersionAuth.json", + "apiGatewayProxyOtherEvent.json", + "apiGatewayProxyV2Event.json", + "apiGatewayProxyV2EventPathTrailingSlash.json", + "apiGatewayProxyV2Event_GET.json", + "apiGatewayProxyV2IamEvent.json", + "apiGatewayProxyV2LambdaAuthorizerEvent.json", + "apiGatewayProxyV2OtherGetEvent.json", + "apiGatewayProxyV2SchemaMiddlwareInvalidEvent.json", + "apiGatewayProxyV2SchemaMiddlwareValidEvent.json", + "apigatewayeSchemaMiddlwareInvalidEvent.json", + "apigatewayeSchemaMiddlwareValidEvent.json", + "appSyncAuthorizerEvent.json", + "appSyncAuthorizerResponse.json", + "appSyncBatchEvent.json", + "appSyncDirectResolver.json", + "appSyncResolverEvent.json", + "awsConfigRuleConfigurationChanged.json", + "awsConfigRuleOversizedConfiguration.json", + "awsConfigRuleScheduled.json", + "bedrockAgentEvent.json", + "bedrockAgentEventWithPathParams.json", + "bedrockAgentPostEvent.json", + "cloudWatchAlarmEventCompositeMetric.json", + "cloudWatchAlarmEventSingleMetric.json", + "cloudWatchDashboardEvent.json", + "cloudWatchLogEvent.json", + "cloudWatchLogEventWithPolicyLevel.json", + "cloudformationCustomResourceCreate.json", + "cloudformationCustomResourceDelete.json", + "cloudformationCustomResourceUpdate.json", + "codePipelineEvent.json", + "codePipelineEventData.json", + "codePipelineEventEmptyUserParameters.json", + "codePipelineEventWithEncryptionKey.json", + "cognitoCreateAuthChallengeEvent.json", + "cognitoCustomEmailSenderEvent.json", + "cognitoCustomMessageEvent.json", + "cognitoCustomSMSSenderEvent.json", + "cognitoDefineAuthChallengeEvent.json", + "cognitoPostAuthenticationEvent.json", + "cognitoPostConfirmationEvent.json", + "cognitoPreAuthenticationEvent.json", + "cognitoPreSignUpEvent.json", + "cognitoPreTokenGenerationEvent.json", + "cognitoPreTokenV2GenerationEvent.json", + "cognitoUserMigrationEvent.json", + "cognitoVerifyAuthChallengeResponseEvent.json", + "connectContactFlowEventAll.json", + "connectContactFlowEventMin.json", + "dynamoStreamEvent.json", + "eventBridgeEvent.json", + "kafkaEventMsk.json", + "kafkaEventSelfManaged.json", + "kinesisFirehoseKinesisEvent.json", + "kinesisFirehosePutEvent.json", + "kinesisFirehoseSQSEvent.json", + "kinesisStreamCloudWatchLogsEvent.json", + "kinesisStreamEvent.json", + "kinesisStreamEventOneRecord.json", + "lambdaFunctionUrlEvent.json", + "lambdaFunctionUrlEventPathTrailingSlash.json", + "lambdaFunctionUrlEventWithHeaders.json", + "lambdaFunctionUrlIAMEvent.json", + "rabbitMQEvent.json", + "s3BatchOperationEventSchemaV1.json", + "s3BatchOperationEventSchemaV2.json", + "s3Event.json", + "s3EventBridgeNotificationObjectCreatedEvent.json", + "s3EventBridgeNotificationObjectDeletedEvent.json", + "s3EventBridgeNotificationObjectExpiredEvent.json", + "s3EventBridgeNotificationObjectRestoreCompletedEvent.json", + "s3EventDecodedKey.json", + "s3EventDeleteObject.json", + "s3EventGlacier.json", + "s3ObjectEventIAMUser.json", + "s3ObjectEventTempCredentials.json", + "s3SqsEvent.json", + "secretsManagerEvent.json", + "sesEvent.json", + "snsEvent.json", + "snsSqsEvent.json", + "snsSqsFifoEvent.json", + "sqsDlqTriggerEvent.json", + "sqsEvent.json", + "vpcLatticeEvent.json", + "vpcLatticeEventPathTrailingSlash.json", + "vpcLatticeEventV2PathTrailingSlash.json", + "vpcLatticeV2Event.json", + "vpcLatticeV2EventWithHeaders.json", + ], + ) + def test_match_for_not_code_deploy_lifecycle_hook_event(self, event_name): + event = load_event(file_name=event_name) + route = CodeDeployLifecycleHookRoute(func=None) + actual = route.match(event=event) + assert actual is None diff --git a/tests/unit/event_handler/_async_execution/_routes/test_event_bridge.py b/tests/unit/event_handler/_async_execution/_routes/test_event_bridge.py new file mode 100644 index 00000000000..a396f174d82 --- /dev/null +++ b/tests/unit/event_handler/_async_execution/_routes/test_event_bridge.py @@ -0,0 +1,539 @@ +import pytest + +from aws_lambda_powertools.event_handler.async_execution.routes.event_bridge import ( + EventBridgeRoute, +) +from aws_lambda_powertools.utilities.data_classes.event_bridge_event import ( + EventBridgeEvent, +) +from tests.functional.utils import load_event + + +class TestEventBridgeRoute: + def test_constructor_error(self): + with pytest.raises(ValueError): + EventBridgeRoute(func=lambda *_: None) + + @pytest.mark.parametrize( + "option_constructor, expected", + [ + ( + {"func": None, "resources": "test"}, + {"func": None, "detail_type": None, "source": None, "resources": ["test"]}, + ), + ( + {"func": None, "resources": ["test"]}, + {"func": None, "detail_type": None, "source": None, "resources": ["test"]}, + ), + ( + {"func": None, "resources": ["test", "name"]}, + {"func": None, "detail_type": None, "source": None, "resources": ["test", "name"]}, + ), + ], + ) + def test_constructor_normal(self, option_constructor, expected): + route = EventBridgeRoute(**option_constructor) + assert route.__dict__ == expected + + @pytest.mark.parametrize( + "option_constructor, option_func, expected", + [ + # without detail_type at option_func + ({"func": None, "detail_type": "test type"}, {"detail_type": None}, False), + # without detail_type + ({"func": None, "source": "aws.ec2"}, {"detail_type": "test type"}, False), + # with detail_type + ({"func": None, "detail_type": "test type"}, {"detail_type": "test type 2"}, False), + ({"func": None, "detail_type": "test type"}, {"detail_type": "test type"}, True), + ], + ) + def test_is_target_with_detail_type(self, option_constructor, option_func, expected): + route = EventBridgeRoute(**option_constructor) + actual = route.is_target_with_detail_type(**option_func) + assert actual == expected + + @pytest.mark.parametrize( + "option_constructor, option_func, expected", + [ + # without source at option_func + ({"func": None, "source": "aws.ec2"}, {"source": None}, False), + # without source + ({"func": None, "detail_type": "test type"}, {"source": "aws.ec2"}, False), + # with source + ({"func": None, "source": "aws.ec2"}, {"source": "aws.lambda"}, False), + ({"func": None, "source": "aws.ec2"}, {"source": "aws.ec2"}, True), + ], + ) + def test_is_target_with_source(self, option_constructor, option_func, expected): + route = EventBridgeRoute(**option_constructor) + actual = route.is_target_with_source(**option_func) + assert actual == expected + + @pytest.mark.parametrize( + "option_constructor, option_func, expected", + [ + # without resources at option_func + ( + {"func": None, "resources": ["arn:aws:ec2:us-west-1:123456789012:instance/i-1234567890abcdef0"]}, + {"resources": None}, + False, + ), + # without resources + ( + {"func": None, "source": "aws.ec2"}, + {"resources": ["arn:aws:ec2:us-west-1:123456789012:instance/i-9999999999abcdef9"]}, + False, + ), + # with resources + ( + {"func": None, "resources": ["arn:aws:ec2:us-west-1:123456789012:instance/i-1234567890abcdef0"]}, + {"resources": ["arn:aws:ec2:us-west-1:123456789012:instance/i-9999999999abcdef9"]}, + False, + ), + ( + {"func": None, "resources": ["arn:aws:ec2:us-west-1:123456789012:instance/i-1234567890abcdef0"]}, + {"resources": ["arn:aws:ec2:us-west-1:123456789012:instance/i-1234567890abcdef0"]}, + True, + ), + ( + { + "func": None, + "resources": [ + "arn:aws:ec2:us-west-1:123456789012:instance/i-1234567890abcdef0", + "arn:aws:ec2:us-west-1:123456789012:instance/i-2222222222abcdef2", + ], + }, + {"resources": ["arn:aws:ec2:us-west-1:123456789012:instance/i-1234567890abcdef0"]}, + True, + ), + ( + { + "func": None, + "resources": [ + "arn:aws:ec2:us-west-1:123456789012:instance/i-1234567890abcdef0", + ], + }, + { + "resources": [ + "arn:aws:ec2:us-west-1:123456789012:instance/i-2222222222abcdef2", + "arn:aws:ec2:us-west-1:123456789012:instance/i-1234567890abcdef0", + ], + }, + True, + ), + ( + { + "func": None, + "resources": [ + "arn:aws:ec2:us-west-1:123456789012:instance/i-1234567890abcdef0", + "arn:aws:ec2:us-west-1:123456789012:instance/i-3333333333abcdef3", + ], + }, + { + "resources": [ + "arn:aws:ec2:us-west-1:123456789012:instance/i-2222222222abcdef2", + "arn:aws:ec2:us-west-1:123456789012:instance/i-1234567890abcdef0", + ], + }, + True, + ), + ], + ) + def test_is_target_with_resources(self, option_constructor, option_func, expected): + route = EventBridgeRoute(**option_constructor) + actual = route.is_target_with_resources(**option_func) + assert actual == expected + + @pytest.mark.parametrize( + "event_name, option_constructor, is_match", + [ + # with detail_type, source, resources + # match all + ( + "eventBridgeEvent.json", + { + "func": lambda *_: None, + "detail_type": "EC2 Instance State-change Notification", + "source": "aws.ec2", + "resources": ["arn:aws:ec2:us-west-1:123456789012:instance/i-1234567890abcdef0"], + }, + True, + ), + # with detail_type, source, resources + # match 2, unmatch 1 + ( + "eventBridgeEvent.json", + { + "func": lambda *_: None, + "detail_type": "EC2 Instance State-change Notification", + "source": "aws.ec2", + "resources": ["arn:aws:ec2:us-west-1:123456789012:instance/i-99999999999999999"], + }, + False, + ), + ( + "eventBridgeEvent.json", + { + "func": lambda *_: None, + "detail_type": "EC2 Instance State-change Notification", + "source": "aws.lambda", + "resources": ["arn:aws:ec2:us-west-1:123456789012:instance/i-1234567890abcdef0"], + }, + False, + ), + ( + "eventBridgeEvent.json", + { + "func": lambda *_: None, + "detail_type": "EC2 Instance State-change Notification V2", + "source": "aws.ec2", + "resources": ["arn:aws:ec2:us-west-1:123456789012:instance/i-1234567890abcdef0"], + }, + False, + ), + # with detail_type, source, resources + # match 1, unmatch 2 + ( + "eventBridgeEvent.json", + { + "func": lambda *_: None, + "detail_type": "EC2 Instance State-change Notification", + "source": "aws.lambda", + "resources": ["arn:aws:ec2:us-west-1:123456789012:instance/i-99999999999999999"], + }, + False, + ), + ( + "eventBridgeEvent.json", + { + "func": lambda *_: None, + "detail_type": "EC2 Instance State-change Notification V2", + "source": "aws.ec2", + "resources": ["arn:aws:ec2:us-west-1:123456789012:instance/i-99999999999999999"], + }, + False, + ), + ( + "eventBridgeEvent.json", + { + "func": lambda *_: None, + "detail_type": "EC2 Instance State-change Notification V2", + "source": "aws.lambda", + "resources": ["arn:aws:ec2:us-west-1:123456789012:instance/i-1234567890abcdef0"], + }, + False, + ), + # with detail_type, source, resources + # unmatch all + ( + "eventBridgeEvent.json", + { + "func": lambda *_: None, + "detail_type": "EC2 Instance State-change Notification V2", + "source": "aws.lambda", + "resources": ["arn:aws:ec2:us-west-1:123456789012:instance/i-99999999999999999"], + }, + False, + ), + # with detail_type and source + # match all + ( + "eventBridgeEvent.json", + { + "func": lambda *_: None, + "detail_type": "EC2 Instance State-change Notification", + "source": "aws.ec2", + }, + True, + ), + # with detail_type and source + # match 1, unmatch 1 + ( + "eventBridgeEvent.json", + { + "func": lambda *_: None, + "detail_type": "EC2 Instance State-change Notification", + "source": "aws.lambda", + }, + False, + ), + ( + "eventBridgeEvent.json", + { + "func": lambda *_: None, + "detail_type": "EC2 Instance State-change Notification V2", + "source": "aws.ec2", + }, + False, + ), + # with detail_type and source + # unmatch all + ( + "eventBridgeEvent.json", + { + "func": lambda *_: None, + "detail_type": "EC2 Instance State-change Notification V2", + "source": "aws.lambda", + }, + False, + ), + # with detail_type and resources + # match all + ( + "eventBridgeEvent.json", + { + "func": lambda *_: None, + "detail_type": "EC2 Instance State-change Notification", + "resources": ["arn:aws:ec2:us-west-1:123456789012:instance/i-1234567890abcdef0"], + }, + True, + ), + # with detail_type and resources + # match 1, unmatch 1 + ( + "eventBridgeEvent.json", + { + "func": lambda *_: None, + "detail_type": "EC2 Instance State-change Notification", + "resources": ["arn:aws:ec2:us-west-1:123456789012:instance/i-99999999999999999"], + }, + False, + ), + ( + "eventBridgeEvent.json", + { + "func": lambda *_: None, + "detail_type": "EC2 Instance State-change Notification V2", + "resources": ["arn:aws:ec2:us-west-1:123456789012:instance/i-1234567890abcdef0"], + }, + False, + ), + # with detail_type and resources + # unmatch all + ( + "eventBridgeEvent.json", + { + "func": lambda *_: None, + "detail_type": "EC2 Instance State-change Notification V2", + "resources": ["arn:aws:ec2:us-west-1:123456789012:instance/i-99999999999999999"], + }, + False, + ), + # with source and resources + # match all + ( + "eventBridgeEvent.json", + { + "func": lambda *_: None, + "source": "aws.ec2", + "resources": ["arn:aws:ec2:us-west-1:123456789012:instance/i-1234567890abcdef0"], + }, + True, + ), + # with source and resources + # match 1, unmatch 1 + ( + "eventBridgeEvent.json", + { + "func": lambda *_: None, + "source": "aws.ec2", + "resources": ["arn:aws:ec2:us-west-1:123456789012:instance/i-99999999999999999"], + }, + False, + ), + ( + "eventBridgeEvent.json", + { + "func": lambda *_: None, + "source": "aws.lambda", + "resources": ["arn:aws:ec2:us-west-1:123456789012:instance/i-1234567890abcdef0"], + }, + False, + ), + # with source and resources + # unmatch all + ( + "eventBridgeEvent.json", + { + "func": lambda *_: None, + "source": "aws.lambda", + "resources": ["arn:aws:ec2:us-west-1:123456789012:instance/i-99999999999999999"], + }, + False, + ), + # with detail_type + ( + "eventBridgeEvent.json", + { + "func": lambda *_: None, + "detail_type": "EC2 Instance State-change Notification", + }, + True, + ), + ( + "eventBridgeEvent.json", + { + "func": lambda *_: None, + "detail_type": "EC2 Instance State-change Notification V2", + }, + False, + ), + # with source + ( + "eventBridgeEvent.json", + { + "func": lambda *_: None, + "source": "aws.ec2", + }, + True, + ), + ( + "eventBridgeEvent.json", + { + "func": lambda *_: None, + "source": "aws.lambda", + }, + False, + ), + # with resources + ( + "eventBridgeEvent.json", + { + "func": lambda *_: None, + "resources": ["arn:aws:ec2:us-west-1:123456789012:instance/i-1234567890abcdef0"], + }, + True, + ), + ( + "eventBridgeEvent.json", + { + "func": lambda *_: None, + "resources": ["arn:aws:ec2:us-west-1:123456789012:instance/i-99999999999999999"], + }, + False, + ), + ], + ) + def test_match_for_event_bridge_event(self, event_name, option_constructor, is_match): + event = load_event(file_name=event_name) + route = EventBridgeRoute(**option_constructor) + actual = route.match(event=event) + if is_match: + expected = route.func, EventBridgeEvent(event) + assert actual == expected + else: + assert actual is None + + @pytest.mark.parametrize( + "event_name", + [ + "activeMQEvent.json", + "albEvent.json", + "albEventPathTrailingSlash.json", + "albMultiValueHeadersEvent.json", + "albMultiValueQueryStringEvent.json", + "apiGatewayAuthorizerRequestEvent.json", + "apiGatewayAuthorizerTokenEvent.json", + "apiGatewayAuthorizerV2Event.json", + "apiGatewayProxyEvent.json", + "apiGatewayProxyEventAnotherPath.json", + "apiGatewayProxyEventNoOrigin.json", + "apiGatewayProxyEventPathTrailingSlash.json", + "apiGatewayProxyEventPrincipalId.json", + "apiGatewayProxyEvent_noVersionAuth.json", + "apiGatewayProxyOtherEvent.json", + "apiGatewayProxyV2Event.json", + "apiGatewayProxyV2EventPathTrailingSlash.json", + "apiGatewayProxyV2Event_GET.json", + "apiGatewayProxyV2IamEvent.json", + "apiGatewayProxyV2LambdaAuthorizerEvent.json", + "apiGatewayProxyV2OtherGetEvent.json", + "apiGatewayProxyV2SchemaMiddlwareInvalidEvent.json", + "apiGatewayProxyV2SchemaMiddlwareValidEvent.json", + "apigatewayeSchemaMiddlwareInvalidEvent.json", + "apigatewayeSchemaMiddlwareValidEvent.json", + "appSyncAuthorizerEvent.json", + "appSyncAuthorizerResponse.json", + "appSyncBatchEvent.json", + "appSyncDirectResolver.json", + "appSyncResolverEvent.json", + "awsConfigRuleConfigurationChanged.json", + "awsConfigRuleOversizedConfiguration.json", + "awsConfigRuleScheduled.json", + "bedrockAgentEvent.json", + "bedrockAgentEventWithPathParams.json", + "bedrockAgentPostEvent.json", + "cloudWatchAlarmEventCompositeMetric.json", + "cloudWatchAlarmEventSingleMetric.json", + "cloudWatchDashboardEvent.json", + "cloudWatchLogEvent.json", + "cloudWatchLogEventWithPolicyLevel.json", + "cloudformationCustomResourceCreate.json", + "cloudformationCustomResourceDelete.json", + "cloudformationCustomResourceUpdate.json", + "codeDeployLifecycleHookEvent.json", + "codePipelineEvent.json", + "codePipelineEventData.json", + "codePipelineEventEmptyUserParameters.json", + "codePipelineEventWithEncryptionKey.json", + "cognitoCreateAuthChallengeEvent.json", + "cognitoCustomEmailSenderEvent.json", + "cognitoCustomMessageEvent.json", + "cognitoCustomSMSSenderEvent.json", + "cognitoDefineAuthChallengeEvent.json", + "cognitoPostAuthenticationEvent.json", + "cognitoPostConfirmationEvent.json", + "cognitoPreAuthenticationEvent.json", + "cognitoPreSignUpEvent.json", + "cognitoPreTokenGenerationEvent.json", + "cognitoPreTokenV2GenerationEvent.json", + "cognitoUserMigrationEvent.json", + "cognitoVerifyAuthChallengeResponseEvent.json", + "connectContactFlowEventAll.json", + "connectContactFlowEventMin.json", + "dynamoStreamEvent.json", + "kafkaEventMsk.json", + "kafkaEventSelfManaged.json", + "kinesisFirehoseKinesisEvent.json", + "kinesisFirehosePutEvent.json", + "kinesisFirehoseSQSEvent.json", + "kinesisStreamCloudWatchLogsEvent.json", + "kinesisStreamEvent.json", + "kinesisStreamEventOneRecord.json", + "lambdaFunctionUrlEvent.json", + "lambdaFunctionUrlEventPathTrailingSlash.json", + "lambdaFunctionUrlEventWithHeaders.json", + "lambdaFunctionUrlIAMEvent.json", + "rabbitMQEvent.json", + "s3BatchOperationEventSchemaV1.json", + "s3BatchOperationEventSchemaV2.json", + "s3Event.json", + "s3EventBridgeNotificationObjectCreatedEvent.json", + "s3EventBridgeNotificationObjectDeletedEvent.json", + "s3EventBridgeNotificationObjectExpiredEvent.json", + "s3EventBridgeNotificationObjectRestoreCompletedEvent.json", + "s3EventDecodedKey.json", + "s3EventDeleteObject.json", + "s3EventGlacier.json", + "s3ObjectEventIAMUser.json", + "s3ObjectEventTempCredentials.json", + "s3SqsEvent.json", + "secretsManagerEvent.json", + "sesEvent.json", + "snsEvent.json", + "snsSqsEvent.json", + "snsSqsFifoEvent.json", + "sqsDlqTriggerEvent.json", + "sqsEvent.json", + "vpcLatticeEvent.json", + "vpcLatticeEventPathTrailingSlash.json", + "vpcLatticeEventV2PathTrailingSlash.json", + "vpcLatticeV2Event.json", + "vpcLatticeV2EventWithHeaders.json", + ], + ) + def test_match_for_not_event_bridge_event(self, event_name): + event = load_event(file_name=event_name) + route = EventBridgeRoute(func=None, detail_type="EC2 Instance State-change Notification") + actual = route.match(event=event) + assert actual is None diff --git a/tests/unit/event_handler/_async_execution/_routes/test_s3.py b/tests/unit/event_handler/_async_execution/_routes/test_s3.py new file mode 100644 index 00000000000..0ad7c8062b5 --- /dev/null +++ b/tests/unit/event_handler/_async_execution/_routes/test_s3.py @@ -0,0 +1,1514 @@ +import pytest + +from aws_lambda_powertools.event_handler.async_execution.routes.s3 import S3Route +from aws_lambda_powertools.utilities.data_classes.s3_event import S3Event +from tests.functional.utils import load_event + + +class TestS3Route: + def test_constructor_error(self): + with pytest.raises(ValueError): + S3Route(func=None) + + @pytest.mark.parametrize( + "option_constructor, option_func, expected", + [ + # without bucket at option_func + ( + {"func": None, "bucket": "lambda-artifacts-deafc19498e3f2df", "bucket_prefix": "lambda-artifacts-d"}, + {"bucket": None}, + False, + ), + # with bucket and bucket_prefix + # match all + ( + {"func": None, "bucket": "lambda-artifacts-deafc19498e3f2df", "bucket_prefix": "lambda-artifacts-d"}, + {"bucket": "lambda-artifacts-deafc19498e3f2df"}, + True, + ), + # with bucket and bucket_prefix + # match 1, unmatch 1 + ( + {"func": None, "bucket": "lambda-artifacts-deafc19498e3f2df", "bucket_prefix": "ambda-artifacts-d"}, + {"bucket": "lambda-artifacts-deafc19498e3f2df"}, + True, + ), + ( + {"func": None, "bucket": "lambda-artifacts-9999999999999999", "bucket_prefix": "lambda-artifacts-d"}, + {"bucket": "lambda-artifacts-deafc19498e3f2df"}, + False, + ), + # with bucket and bucket_prefix + # unmatch all + ( + {"func": None, "bucket": "lambda-artifacts-9999999999999999", "bucket_prefix": "ambda-artifacts-d"}, + {"bucket": "lambda-artifacts-deafc19498e3f2df"}, + False, + ), + # with bucket + ( + { + "func": None, + "bucket": "lambda-artifacts-deafc19498e3f2df", + }, + {"bucket": "lambda-artifacts-deafc19498e3f2df"}, + True, + ), + ( + { + "func": None, + "bucket": "lambda-artifacts-9999999999999999", + }, + {"bucket": "lambda-artifacts-deafc19498e3f2df"}, + False, + ), + # with bucket_prefix + ( + { + "func": None, + "bucket_prefix": "lambda-a", + }, + {"bucket": "lambda-artifacts-deafc19498e3f2df"}, + True, + ), + ( + { + "func": None, + "bucket_prefix": "ambda-a", + }, + {"bucket": "lambda-artifacts-deafc19498e3f2df"}, + False, + ), + # without bucket and bucket_prefix + ( + {"func": None, "key": "b21b84d653bb07b05b1e6b33684dc11b"}, + {"bucket": "lambda-artifacts-deafc19498e3f2df"}, + False, + ), + ], + ) + def test_is_target_with_bucket(self, option_constructor, option_func, expected): + route = S3Route(**option_constructor) + actual = route.is_target_with_bucket(**option_func) + assert actual == expected + + @pytest.mark.parametrize( + "option_constructor, option_func, expected", + [ + # without key at option_func + ( + {"func": None, "key": "b21b84d653bb07b05b1e6b33684dc11b", "key_prefix": "b21b", "key_suffix": "c11b"}, + {"key": None}, + False, + ), + # without key, key_prefix, and key_suffix + ( + {"func": None, "bucket": "lambda-artifacts-deafc19498e3f2df"}, + {"key": "b21b84d653bb07b05b1e6b33684dc11b"}, + False, + ), + # with key, key_prefix, and key_suffix + # match all + ( + {"func": None, "key": "b21b84d653bb07b05b1e6b33684dc11b", "key_prefix": "b21b", "key_suffix": "c11b"}, + {"key": "b21b84d653bb07b05b1e6b33684dc11b"}, + True, + ), + # with key, key_prefix, and key_suffix + # match 2, unmatch 1 + ( + {"func": None, "key": "b21b84d653bb07b05b1e6b33684dc11b", "key_prefix": "b21b", "key_suffix": "9999"}, + {"key": "b21b84d653bb07b05b1e6b33684dc11b"}, + True, + ), + ( + {"func": None, "key": "b21b84d653bb07b05b1e6b33684dc11b", "key_prefix": "9999", "key_suffix": "c11b"}, + {"key": "b21b84d653bb07b05b1e6b33684dc11b"}, + True, + ), + ( + {"func": None, "key": "99999999999999999999999999999999", "key_prefix": "b21b", "key_suffix": "c11b"}, + {"key": "b21b84d653bb07b05b1e6b33684dc11b"}, + False, + ), + # with key, key_prefix, and key_suffix + # match 1, unmatch 2 + ( + {"func": None, "key": "b21b84d653bb07b05b1e6b33684dc11b", "key_prefix": "9999", "key_suffix": "9999"}, + {"key": "b21b84d653bb07b05b1e6b33684dc11b"}, + True, + ), + ( + {"func": None, "key": "99999999999999999999999999999999", "key_prefix": "b21b", "key_suffix": "9999"}, + {"key": "b21b84d653bb07b05b1e6b33684dc11b"}, + False, + ), + ( + {"func": None, "key": "99999999999999999999999999999999", "key_prefix": "9999", "key_suffix": "c11b"}, + {"key": "b21b84d653bb07b05b1e6b33684dc11b"}, + False, + ), + # with key, key_prefix, and key_suffix + # unmatch all + ( + {"func": None, "key": "99999999999999999999999999999999", "key_prefix": "9999", "key_suffix": "9999"}, + {"key": "b21b84d653bb07b05b1e6b33684dc11b"}, + False, + ), + # with key, key_prefix + # match all + ( + { + "func": None, + "key": "b21b84d653bb07b05b1e6b33684dc11b", + "key_prefix": "b21b", + }, + {"key": "b21b84d653bb07b05b1e6b33684dc11b"}, + True, + ), + # with key, key_prefix + # match 1, unmatch 1 + ( + { + "func": None, + "key": "b21b84d653bb07b05b1e6b33684dc11b", + "key_prefix": "9999", + }, + {"key": "b21b84d653bb07b05b1e6b33684dc11b"}, + True, + ), + ( + { + "func": None, + "key": "99999999999999999999999999999999", + "key_prefix": "b21b", + }, + {"key": "b21b84d653bb07b05b1e6b33684dc11b"}, + False, + ), + # with key, key_prefix + # unmatch all + ( + { + "func": None, + "key": "99999999999999999999999999999999", + "key_prefix": "9999", + }, + {"key": "b21b84d653bb07b05b1e6b33684dc11b"}, + False, + ), + # with key, key_suffix + # match all + ( + { + "func": None, + "key": "b21b84d653bb07b05b1e6b33684dc11b", + "key_suffix": "c11b", + }, + {"key": "b21b84d653bb07b05b1e6b33684dc11b"}, + True, + ), + # with key, key_suffix + # match 1, unmatch 1 + ( + { + "func": None, + "key": "b21b84d653bb07b05b1e6b33684dc11b", + "key_suffix": "9999", + }, + {"key": "b21b84d653bb07b05b1e6b33684dc11b"}, + True, + ), + ( + { + "func": None, + "key": "99999999999999999999999999999999", + "key_suffix": "c11b", + }, + {"key": "b21b84d653bb07b05b1e6b33684dc11b"}, + False, + ), + # with key, key_suffix + # unmatch all + ( + { + "func": None, + "key": "99999999999999999999999999999999", + "key_suffix": "9999", + }, + {"key": "b21b84d653bb07b05b1e6b33684dc11b"}, + False, + ), + # with key_prefix, key_suffix + # match all + ( + { + "func": None, + "key_prefix": "b21b", + "key_suffix": "c11b", + }, + {"key": "b21b84d653bb07b05b1e6b33684dc11b"}, + True, + ), + # with key_prefix, key_suffix + # match 1, unmatch 1 + ( + { + "func": None, + "key_prefix": "b21b", + "key_suffix": "9999", + }, + {"key": "b21b84d653bb07b05b1e6b33684dc11b"}, + False, + ), + ( + { + "func": None, + "key_prefix": "9999", + "key_suffix": "c11b", + }, + {"key": "b21b84d653bb07b05b1e6b33684dc11b"}, + False, + ), + # with key_prefix, key_suffix + # unmatch all + ( + { + "func": None, + "key_prefix": "9999", + "key_suffix": "9999", + }, + {"key": "b21b84d653bb07b05b1e6b33684dc11b"}, + False, + ), + # with key + ( + { + "func": None, + "key": "b21b84d653bb07b05b1e6b33684dc11b", + }, + {"key": "b21b84d653bb07b05b1e6b33684dc11b"}, + True, + ), + ( + { + "func": None, + "key": "99999999999999999999999999999999", + }, + {"key": "b21b84d653bb07b05b1e6b33684dc11b"}, + False, + ), + # with key_prefix + ( + { + "func": None, + "key_prefix": "b21b", + }, + {"key": "b21b84d653bb07b05b1e6b33684dc11b"}, + True, + ), + ( + { + "func": None, + "key_prefix": "9999", + }, + {"key": "b21b84d653bb07b05b1e6b33684dc11b"}, + False, + ), + # with_key_suffix + ( + { + "func": None, + "key_suffix": "c11b", + }, + {"key": "b21b84d653bb07b05b1e6b33684dc11b"}, + True, + ), + ( + { + "func": None, + "key_suffix": "9999", + }, + {"key": "b21b84d653bb07b05b1e6b33684dc11b"}, + False, + ), + ], + ) + def test_is_target_with_key(self, option_constructor, option_func, expected): + route = S3Route(**option_constructor) + actual = route.is_target_with_key(**option_func) + assert actual == expected + + @pytest.mark.parametrize( + "option_constructor, option_func, expected", + [ + # without event_name and event_name_prefix + ({"func": None, "bucket": "lambda-artifacts-deafc19498e3f2df"}, {"event_name": "ObjectCreated:Put"}, False), + # without event_name at option_func + ( + {"func": None, "event_name": "ObjectCreated:Put", "event_name_prefix": "ObjectCreated:"}, + {"event_name": None}, + False, + ), + # with event_name and event_name_prefix + # match all + ( + {"func": None, "event_name": "ObjectCreated:Put", "event_name_prefix": "ObjectCreated:"}, + {"event_name": "ObjectCreated:Put"}, + True, + ), + # with event_name and event_name_prefix + # match 1, unmatch 1 + ( + {"func": None, "event_name": "ObjectCreated:Put", "event_name_prefix": "bjectCreated:"}, + {"event_name": "ObjectCreated:Put"}, + True, + ), + ( + {"func": None, "event_name": "ObjectCreated:PutV2", "event_name_prefix": "ObjectCreated:"}, + {"event_name": "ObjectCreated:Put"}, + False, + ), + # with event_name and event_name_prefix + # unmatch all + ( + {"func": None, "event_name": "ObjectCreated:PutV2", "event_name_prefix": "bjectCreated:"}, + {"event_name": "ObjectCreated:Put"}, + False, + ), + # with event_name + ( + { + "func": None, + "event_name": "ObjectCreated:Put", + }, + {"event_name": "ObjectCreated:Put"}, + True, + ), + ( + { + "func": None, + "event_name": "ObjectCreated:PutV2", + }, + {"event_name": "ObjectCreated:Put"}, + False, + ), + # with event_name_prefix + ( + { + "func": None, + "event_name_prefix": "ObjectCreated:", + }, + {"event_name": "ObjectCreated:Put"}, + True, + ), + ( + { + "func": None, + "event_name_prefix": "bjectCreated:", + }, + {"event_name": "ObjectCreated:Put"}, + False, + ), + ], + ) + def test_is_target_with_event_name(self, option_constructor, option_func, expected): + route = S3Route(**option_constructor) + actual = route.is_target_with_event_name(**option_func) + assert actual == expected + + @pytest.mark.parametrize( + "option_constructor, option_func, expected", + [ + # without configuration_id and configuration_id_prefix + ( + {"func": None, "bucket": "lambda-artifacts-deafc19498e3f2df"}, + {"configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1"}, + False, + ), + # without configuration_id at option_func + ( + { + "func": None, + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + "configuration_id_prefix": "828aa6fc-f", + }, + {"configuration_id": None}, + False, + ), + # with configuration_id and configuration_id_prefix + # match all + ( + { + "func": None, + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + "configuration_id_prefix": "828aa6fc-f", + }, + { + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + }, + True, + ), + # with configuration_id and configuration_id_prefix + # match 1, unmatch 1 + ( + { + "func": None, + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + "configuration_id_prefix": "28aa6fc-f", + }, + { + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + }, + True, + ), + ( + { + "func": None, + "configuration_id": "99999999-9999-9999-9999-999999999999", + "configuration_id_prefix": "828aa6fc-f", + }, + { + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + }, + False, + ), + # with configuration_id and configuration_id_prefix + # unmatch all + ( + { + "func": None, + "configuration_id": "99999999-9999-9999-9999-999999999999", + "configuration_id_prefix": "28aa6fc-f", + }, + { + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + }, + False, + ), + # with configuration_id + ( + { + "func": None, + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + }, + { + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + }, + True, + ), + ( + { + "func": None, + "configuration_id": "99999999-9999-9999-9999-999999999999", + }, + { + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + }, + False, + ), + # with configuration_id_prefix + ( + { + "func": None, + "configuration_id_prefix": "828aa6fc-f", + }, + { + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + }, + True, + ), + ( + { + "func": None, + "configuration_id_prefix": "28aa6fc-f", + }, + { + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + }, + False, + ), + ], + ) + def test_is_target_with_configuration_id(self, option_constructor, option_func, expected): + route = S3Route(**option_constructor) + actual = route.is_target_with_configuration_id(**option_func) + assert actual == expected + + @pytest.mark.parametrize( + "event_name, option_constructor, is_match", + [ + # with bucket, key, event_name, and configuration_id + # match all + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-deafc19498e3f2df", + "key": "b21b84d653bb07b05b1e6b33684dc11b", + "event_name": "ObjectCreated:Put", + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + }, + True, + ), + # with bucket, key, event_name, and configuration_id + # match 3, unmatch 1 + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-deafc19498e3f2df", + "key": "b21b84d653bb07b05b1e6b33684dc11b", + "event_name": "ObjectCreated:Put", + "configuration_id": "828aa6fc-f7b5-4305-8584-999999999999", + }, + False, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-deafc19498e3f2df", + "key": "b21b84d653bb07b05b1e6b33684dc11b", + "event_name": "ObjectCreated:PutV2", + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + }, + False, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-deafc19498e3f2df", + "key": "99999999999999999999999999999999", + "event_name": "ObjectCreated:Put", + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + }, + False, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-9999999999999999", + "key": "b21b84d653bb07b05b1e6b33684dc11b", + "event_name": "ObjectCreated:Put", + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + }, + False, + ), + # with bucket, key, event_name, and configuration_id + # match 2, unmatch 2 + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-deafc19498e3f2df", + "key": "b21b84d653bb07b05b1e6b33684dc11b", + "event_name": "ObjectCreated:PutV2", + "configuration_id": "828aa6fc-f7b5-4305-8584-999999999999", + }, + False, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-deafc19498e3f2df", + "key": "99999999999999999999999999999999", + "event_name": "ObjectCreated:Put", + "configuration_id": "828aa6fc-f7b5-4305-8584-999999999999", + }, + False, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-9999999999999999", + "key": "b21b84d653bb07b05b1e6b33684dc11b", + "event_name": "ObjectCreated:Put", + "configuration_id": "828aa6fc-f7b5-4305-8584-999999999999", + }, + False, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-deafc19498e3f2df", + "key": "99999999999999999999999999999999", + "event_name": "ObjectCreated:PutV2", + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + }, + False, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-9999999999999999", + "key": "b21b84d653bb07b05b1e6b33684dc11b", + "event_name": "ObjectCreated:PutV2", + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + }, + False, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-9999999999999999", + "key": "99999999999999999999999999999999", + "event_name": "ObjectCreated:Put", + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + }, + False, + ), + # with bucket, key, event_name, and configuration_id + # match 1, unmatch 3 + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-deafc19498e3f2df", + "key": "99999999999999999999999999999999", + "event_name": "ObjectCreated:PutV2", + "configuration_id": "828aa6fc-f7b5-4305-8584-999999999999", + }, + False, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-9999999999999999", + "key": "b21b84d653bb07b05b1e6b33684dc11b", + "event_name": "ObjectCreated:PutV2", + "configuration_id": "828aa6fc-f7b5-4305-8584-999999999999", + }, + False, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-9999999999999999", + "key": "99999999999999999999999999999999", + "event_name": "ObjectCreated:Put", + "configuration_id": "828aa6fc-f7b5-4305-8584-999999999999", + }, + False, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-9999999999999999", + "key": "99999999999999999999999999999999", + "event_name": "ObjectCreated:PutV2", + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + }, + False, + ), + # with bucket, key, event_name, and configuration_id + # unmatch all + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-9999999999999999", + "key": "99999999999999999999999999999999", + "event_name": "ObjectCreated:PutV2", + "configuration_id": "828aa6fc-f7b5-4305-8584-999999999999", + }, + False, + ), + # with bucket, key, and event_name + # match all + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-deafc19498e3f2df", + "key": "b21b84d653bb07b05b1e6b33684dc11b", + "event_name": "ObjectCreated:Put", + }, + True, + ), + # with bucket, key, and event_name + # match 2, unmatch 1 + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-deafc19498e3f2df", + "key": "b21b84d653bb07b05b1e6b33684dc11b", + "event_name": "ObjectCreated:PutV2", + }, + False, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-deafc19498e3f2df", + "key": "99999999999999999999999999999999", + "event_name": "ObjectCreated:Put", + }, + False, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-9999999999999999", + "key": "b21b84d653bb07b05b1e6b33684dc11b", + "event_name": "ObjectCreated:Put", + }, + False, + ), + # with bucket, key, and event_name + # match 1, unmatch 2 + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-deafc19498e3f2df", + "key": "99999999999999999999999999999999", + "event_name": "ObjectCreated:PutV2", + }, + False, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-9999999999999999", + "key": "b21b84d653bb07b05b1e6b33684dc11b", + "event_name": "ObjectCreated:PutV2", + }, + False, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-9999999999999999", + "key": "99999999999999999999999999999999", + "event_name": "ObjectCreated:Put", + }, + False, + ), + # with bucket, key, and event_name + # unmatch all + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-9999999999999999", + "key": "99999999999999999999999999999999", + "event_name": "ObjectCreated:PutV2", + }, + False, + ), + # with bucket, key, and configuration_id + # match all + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-deafc19498e3f2df", + "key": "b21b84d653bb07b05b1e6b33684dc11b", + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + }, + True, + ), + # with bucket, key, and configuration_id + # match 2, unmatch 1 + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-deafc19498e3f2df", + "key": "b21b84d653bb07b05b1e6b33684dc11b", + "configuration_id": "828aa6fc-f7b5-4305-8584-999999999999", + }, + False, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-deafc19498e3f2df", + "key": "99999999999999999999999999999999", + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + }, + False, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-9999999999999999", + "key": "b21b84d653bb07b05b1e6b33684dc11b", + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + }, + False, + ), + # with bucket, key, and configuration_id + # match 1, unmatch 2 + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-deafc19498e3f2df", + "key": "99999999999999999999999999999999", + "configuration_id": "828aa6fc-f7b5-4305-8584-999999999999", + }, + False, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-9999999999999999", + "key": "b21b84d653bb07b05b1e6b33684dc11b", + "configuration_id": "828aa6fc-f7b5-4305-8584-999999999999", + }, + False, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-deafc19498e3f2df", + "key": "99999999999999999999999999999999", + "configuration_id": "828aa6fc-f7b5-4305-8584-999999999999", + }, + False, + ), + # with bucket, key, and configuration_id + # unmatch all + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-9999999999999999", + "key": "99999999999999999999999999999999", + "configuration_id": "828aa6fc-f7b5-4305-8584-999999999999", + }, + False, + ), + # with bucket, event_name, and configuration_id + # match all + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-deafc19498e3f2df", + "event_name": "ObjectCreated:Put", + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + }, + True, + ), + # with bucket, event_name, and configuration_id + # match 2, unmatch 1 + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-deafc19498e3f2df", + "event_name": "ObjectCreated:Put", + "configuration_id": "828aa6fc-f7b5-4305-8584-999999999999", + }, + False, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-deafc19498e3f2df", + "event_name": "ObjectCreated:PutV2", + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + }, + False, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-9999999999999999", + "event_name": "ObjectCreated:Put", + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + }, + False, + ), + # with bucket, event_name, and configuration_id + # match 1, unmatch 2 + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-deafc19498e3f2df", + "event_name": "ObjectCreated:PutV2", + "configuration_id": "828aa6fc-f7b5-4305-8584-999999999999", + }, + False, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-9999999999999999", + "event_name": "ObjectCreated:Put", + "configuration_id": "828aa6fc-f7b5-4305-8584-999999999999", + }, + False, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-9999999999999999", + "event_name": "ObjectCreated:PutV2", + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + }, + False, + ), + # with bucket, event_name, and configuration_id + # unmatch all + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-9999999999999999", + "event_name": "ObjectCreated:PutV2", + "configuration_id": "828aa6fc-f7b5-4305-8584-999999999999", + }, + False, + ), + # with key, event_name, configuration_id + # match all + ( + "s3Event.json", + { + "func": lambda *_: None, + "key": "b21b84d653bb07b05b1e6b33684dc11b", + "event_name": "ObjectCreated:Put", + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + }, + True, + ), + # with key, event_name, configuration_id + # match 2, unmatch 1 + ( + "s3Event.json", + { + "func": lambda *_: None, + "key": "b21b84d653bb07b05b1e6b33684dc11b", + "event_name": "ObjectCreated:Put", + "configuration_id": "828aa6fc-f7b5-4305-8584-999999999999", + }, + False, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "key": "b21b84d653bb07b05b1e6b33684dc11b", + "event_name": "ObjectCreated:PutV2", + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + }, + False, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "key": "99999999999999999999999999999999", + "event_name": "ObjectCreated:Put", + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + }, + False, + ), + # with key, event_name, configuration_id + # match 1, unmatch 2 + ( + "s3Event.json", + { + "func": lambda *_: None, + "key": "b21b84d653bb07b05b1e6b33684dc11b", + "event_name": "ObjectCreated:PutV2", + "configuration_id": "828aa6fc-f7b5-4305-8584-999999999999", + }, + False, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "key": "99999999999999999999999999999999", + "event_name": "ObjectCreated:Put", + "configuration_id": "828aa6fc-f7b5-4305-8584-999999999999", + }, + False, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "key": "99999999999999999999999999999999", + "event_name": "ObjectCreated:PutV2", + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + }, + False, + ), + # with key, event_name, and configuration_id + # unmatch all + ( + "s3Event.json", + { + "func": lambda *_: None, + "key": "99999999999999999999999999999999", + "event_name": "ObjectCreated:PutV2", + "configuration_id": "828aa6fc-f7b5-4305-8584-999999999999", + }, + False, + ), + # with bucket and key + # match all + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-deafc19498e3f2df", + "key": "b21b84d653bb07b05b1e6b33684dc11b", + }, + True, + ), + # with bucket and key + # match 1, unmatch 1 + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-deafc19498e3f2df", + "key": "99999999999999999999999999999999", + }, + False, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-9999999999999999", + "key": "b21b84d653bb07b05b1e6b33684dc11b", + }, + False, + ), + # with bucket and key + # unmatch all + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-9999999999999999", + "key": "99999999999999999999999999999999", + }, + False, + ), + # with bucket and event_name + # match all + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-deafc19498e3f2df", + "event_name": "ObjectCreated:Put", + }, + True, + ), + # with bucket and event_name + # match 1, unmatch 1 + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-deafc19498e3f2df", + "event_name": "ObjectCreated:PutV2", + }, + False, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-9999999999999999", + "event_name": "ObjectCreated:Put", + }, + False, + ), + # with bucket and event_name + # unmatch all + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-9999999999999999", + "event_name": "ObjectCreated:PutV2", + }, + False, + ), + # with bucket and configuration_id + # match all + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-deafc19498e3f2df", + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + }, + True, + ), + # with bucket and configuration_id + # match 1, unmatch 1 + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-deafc19498e3f2df", + "configuration_id": "828aa6fc-f7b5-4305-8584-999999999999", + }, + False, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-9999999999999999", + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + }, + False, + ), + # with bucket and configuration_id + # unmatch all + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-9999999999999999", + "configuration_id": "828aa6fc-f7b5-4305-8584-999999999999", + }, + False, + ), + # with key and event_name + # match all + ( + "s3Event.json", + { + "func": lambda *_: None, + "key": "b21b84d653bb07b05b1e6b33684dc11b", + "event_name": "ObjectCreated:Put", + }, + True, + ), + # with key and event_name + # match 1, unmatch 1 + ( + "s3Event.json", + { + "func": lambda *_: None, + "key": "b21b84d653bb07b05b1e6b33684dc11b", + "event_name": "ObjectCreated:PutV2", + }, + False, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "key": "99999999999999999999999999999999", + "event_name": "ObjectCreated:Put", + }, + False, + ), + # with key and event_name + # unmatch all + ( + "s3Event.json", + { + "func": lambda *_: None, + "key": "99999999999999999999999999999999", + "event_name": "ObjectCreated:PutV2", + }, + False, + ), + # with key and configuration_id + # match all + ( + "s3Event.json", + { + "func": lambda *_: None, + "key": "b21b84d653bb07b05b1e6b33684dc11b", + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + }, + True, + ), + # with key and configuration_id + # match 1, unmatch 1 + ( + "s3Event.json", + { + "func": lambda *_: None, + "key": "b21b84d653bb07b05b1e6b33684dc11b", + "configuration_id": "828aa6fc-f7b5-4305-8584-999999999999", + }, + False, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "key": "99999999999999999999999999999999", + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + }, + False, + ), + # with key and configuration_id + # unmatch all + ( + "s3Event.json", + { + "func": lambda *_: None, + "key": "99999999999999999999999999999999", + "configuration_id": "828aa6fc-f7b5-4305-8584-999999999999", + }, + False, + ), + # with event_name and configuration_id + # match all + ( + "s3Event.json", + { + "func": lambda *_: None, + "event_name": "ObjectCreated:Put", + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + }, + True, + ), + # with event_name and configuration_id + # match 1, unmatch 1 + ( + "s3Event.json", + { + "func": lambda *_: None, + "event_name": "ObjectCreated:Put", + "configuration_id": "828aa6fc-f7b5-4305-8584-999999999999", + }, + False, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "event_name": "ObjectCreated:PutV2", + "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1", + }, + False, + ), + # with event_name and configuration_id + # unmatch all + ( + "s3Event.json", + { + "func": lambda *_: None, + "event_name": "ObjectCreated:PutV2", + "configuration_id": "828aa6fc-f7b5-4305-8584-999999999999", + }, + False, + ), + # with bucket + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-deafc19498e3f2df", + }, + True, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "bucket": "lambda-artifacts-9999999999999999", + }, + False, + ), + # with key + ( + "s3Event.json", + { + "func": lambda *_: None, + "key": "b21b84d653bb07b05b1e6b33684dc11b", + }, + True, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "key": "99999999999999999999999999999999", + }, + False, + ), + # with event_name + ( + "s3Event.json", + { + "func": lambda *_: None, + "event_name": "ObjectCreated:Put", + }, + True, + ), + ( + "s3Event.json", + { + "func": lambda *_: None, + "event_name": "ObjectCreated:PutV2", + }, + False, + ), + # with configuration_id + ( + "s3Event.json", + {"func": lambda *_: None, "configuration_id": "828aa6fc-f7b5-4305-8584-487c791949c1"}, + True, + ), + ( + "s3Event.json", + {"func": lambda *_: None, "configuration_id": "828aa6fc-f7b5-4305-8584-999999999999"}, + False, + ), + ], + ) + def test_match_for_s3_event(self, event_name, option_constructor, is_match): + event = load_event(file_name=event_name) + route = S3Route(**option_constructor) + actual = route.match(event=event) + if is_match: + expected = route.func, S3Event(event) + assert actual == expected + else: + assert actual is None + + @pytest.mark.parametrize( + "event_name", + [ + "activeMQEvent.json", + "albEvent.json", + "albEventPathTrailingSlash.json", + "albMultiValueHeadersEvent.json", + "albMultiValueQueryStringEvent.json", + "apiGatewayAuthorizerRequestEvent.json", + "apiGatewayAuthorizerTokenEvent.json", + "apiGatewayAuthorizerV2Event.json", + "apiGatewayProxyEvent.json", + "apiGatewayProxyEventAnotherPath.json", + "apiGatewayProxyEventNoOrigin.json", + "apiGatewayProxyEventPathTrailingSlash.json", + "apiGatewayProxyEventPrincipalId.json", + "apiGatewayProxyEvent_noVersionAuth.json", + "apiGatewayProxyOtherEvent.json", + "apiGatewayProxyV2Event.json", + "apiGatewayProxyV2EventPathTrailingSlash.json", + "apiGatewayProxyV2Event_GET.json", + "apiGatewayProxyV2IamEvent.json", + "apiGatewayProxyV2LambdaAuthorizerEvent.json", + "apiGatewayProxyV2OtherGetEvent.json", + "apiGatewayProxyV2SchemaMiddlwareInvalidEvent.json", + "apiGatewayProxyV2SchemaMiddlwareValidEvent.json", + "apigatewayeSchemaMiddlwareInvalidEvent.json", + "apigatewayeSchemaMiddlwareValidEvent.json", + "appSyncAuthorizerEvent.json", + "appSyncAuthorizerResponse.json", + "appSyncBatchEvent.json", + "appSyncDirectResolver.json", + "appSyncResolverEvent.json", + "awsConfigRuleConfigurationChanged.json", + "awsConfigRuleOversizedConfiguration.json", + "awsConfigRuleScheduled.json", + "bedrockAgentEvent.json", + "bedrockAgentEventWithPathParams.json", + "bedrockAgentPostEvent.json", + "cloudWatchAlarmEventCompositeMetric.json", + "cloudWatchAlarmEventSingleMetric.json", + "cloudWatchDashboardEvent.json", + "cloudWatchLogEvent.json", + "cloudWatchLogEventWithPolicyLevel.json", + "cloudformationCustomResourceCreate.json", + "cloudformationCustomResourceDelete.json", + "cloudformationCustomResourceUpdate.json", + "codeDeployLifecycleHookEvent.json", + "codePipelineEvent.json", + "codePipelineEventData.json", + "codePipelineEventEmptyUserParameters.json", + "codePipelineEventWithEncryptionKey.json", + "cognitoCreateAuthChallengeEvent.json", + "cognitoCustomEmailSenderEvent.json", + "cognitoCustomMessageEvent.json", + "cognitoCustomSMSSenderEvent.json", + "cognitoDefineAuthChallengeEvent.json", + "cognitoPostAuthenticationEvent.json", + "cognitoPostConfirmationEvent.json", + "cognitoPreAuthenticationEvent.json", + "cognitoPreSignUpEvent.json", + "cognitoPreTokenGenerationEvent.json", + "cognitoPreTokenV2GenerationEvent.json", + "cognitoUserMigrationEvent.json", + "cognitoVerifyAuthChallengeResponseEvent.json", + "connectContactFlowEventAll.json", + "connectContactFlowEventMin.json", + "dynamoStreamEvent.json", + "eventBridgeEvent.json", + "kafkaEventMsk.json", + "kafkaEventSelfManaged.json", + "kinesisFirehoseKinesisEvent.json", + "kinesisFirehosePutEvent.json", + "kinesisFirehoseSQSEvent.json", + "kinesisStreamCloudWatchLogsEvent.json", + "kinesisStreamEvent.json", + "kinesisStreamEventOneRecord.json", + "lambdaFunctionUrlEvent.json", + "lambdaFunctionUrlEventPathTrailingSlash.json", + "lambdaFunctionUrlEventWithHeaders.json", + "lambdaFunctionUrlIAMEvent.json", + "rabbitMQEvent.json", + "s3BatchOperationEventSchemaV1.json", + "s3BatchOperationEventSchemaV2.json", + "s3EventBridgeNotificationObjectCreatedEvent.json", + "s3EventBridgeNotificationObjectDeletedEvent.json", + "s3EventBridgeNotificationObjectExpiredEvent.json", + "s3EventBridgeNotificationObjectRestoreCompletedEvent.json", + "s3ObjectEventIAMUser.json", + "s3ObjectEventTempCredentials.json", + "s3SqsEvent.json", + "secretsManagerEvent.json", + "sesEvent.json", + "snsEvent.json", + "snsSqsEvent.json", + "snsSqsFifoEvent.json", + "sqsDlqTriggerEvent.json", + "sqsEvent.json", + "vpcLatticeEvent.json", + "vpcLatticeEventPathTrailingSlash.json", + "vpcLatticeEventV2PathTrailingSlash.json", + "vpcLatticeV2Event.json", + "vpcLatticeV2EventWithHeaders.json", + ], + ) + def test_match_for_not_s3_event(self, event_name): + event = load_event(file_name=event_name) + route = S3Route(func=None, bucket="lambda-artifacts-deafc19498e3f2df") + actual = route.match(event=event) + assert actual is None diff --git a/tests/unit/event_handler/_async_execution/_routes/test_secrets_manager.py b/tests/unit/event_handler/_async_execution/_routes/test_secrets_manager.py new file mode 100644 index 00000000000..cbe5496d989 --- /dev/null +++ b/tests/unit/event_handler/_async_execution/_routes/test_secrets_manager.py @@ -0,0 +1,209 @@ +import pytest + +from aws_lambda_powertools.event_handler.async_execution.routes.secrets_manager import SecretsManagerRoute +from aws_lambda_powertools.utilities.data_classes.secrets_manager_event import SecretsManagerEvent +from tests.functional.utils import load_event + + +class TestSecretsManagerRoute: + def test_constructor_error(self): + with pytest.raises(ValueError): + SecretsManagerRoute(func=lambda _: None) + + @pytest.mark.parametrize( + "event_name, option_constructor, is_match", + [ + # with secret_id and secret_name_prefix + # match all + ( + "secretsManagerEvent.json", + { + "func": lambda *_: None, + "secret_id": "arn:aws:secretsmanager:us-west-2:123456789012:secret:MyTestDatabaseSecret-a1b2c3", + "secret_name_prefix": "MyTestDatabaseSecret", + }, + True, + ), + # with secret_id and secret_name_prefix + # match 1, unmatch 1 + ( + "secretsManagerEvent.json", + { + "func": lambda *_: None, + "secret_id": "arn:aws:secretsmanager:us-west-2:123456789012:secret:MyTestDatabaseSecret-a1b2c3", + "secret_name_prefix": "MyTestDatabaseSecretV2", + }, + False, + ), + ( + "secretsManagerEvent.json", + { + "func": lambda *_: None, + "secret_id": "arn:aws:secretsmanager:us-west-2:123456789012:secret:MyTestDatabaseSecret-999999", + "secret_name_prefix": "MyTestDatabaseSecret", + }, + False, + ), + # with secret_id and secret_name_prefix + # unmatch all + ( + "secretsManagerEvent.json", + { + "func": lambda *_: None, + "secret_id": "arn:aws:secretsmanager:us-west-2:123456789012:secret:MyTestDatabaseSecret-999999", + "secret_name_prefix": "MyTestDatabaseSecretV2", + }, + False, + ), + # with secret_id + ( + "secretsManagerEvent.json", + { + "func": lambda *_: None, + "secret_id": "arn:aws:secretsmanager:us-west-2:123456789012:secret:MyTestDatabaseSecret-a1b2c3", + }, + True, + ), + ( + "secretsManagerEvent.json", + { + "func": lambda *_: None, + "secret_id": "arn:aws:secretsmanager:us-west-2:123456789012:secret:MyTestDatabaseSecret-999999", + }, + False, + ), + # with secret_name_prefix + ("secretsManagerEvent.json", {"func": lambda *_: None, "secret_name_prefix": "MyTestDatabaseSecret"}, True), + ( + "secretsManagerEvent.json", + {"func": lambda *_: None, "secret_name_prefix": "MyTestDatabaseSecretV2"}, + False, + ), + ], + ) + def test_match(self, event_name, option_constructor, is_match): + event = load_event(file_name=event_name) + route = SecretsManagerRoute(**option_constructor) + actual = route.match(event=event) + if is_match: + expected = (route.func, SecretsManagerEvent(event)) + assert actual == expected + else: + assert actual is None + + @pytest.mark.parametrize( + "event_name", + [ + "activeMQEvent.json", + "albEvent.json", + "albEventPathTrailingSlash.json", + "albMultiValueHeadersEvent.json", + "albMultiValueQueryStringEvent.json", + "apiGatewayAuthorizerRequestEvent.json", + "apiGatewayAuthorizerTokenEvent.json", + "apiGatewayAuthorizerV2Event.json", + "apiGatewayProxyEvent.json", + "apiGatewayProxyEventAnotherPath.json", + "apiGatewayProxyEventNoOrigin.json", + "apiGatewayProxyEventPathTrailingSlash.json", + "apiGatewayProxyEventPrincipalId.json", + "apiGatewayProxyEvent_noVersionAuth.json", + "apiGatewayProxyOtherEvent.json", + "apiGatewayProxyV2Event.json", + "apiGatewayProxyV2EventPathTrailingSlash.json", + "apiGatewayProxyV2Event_GET.json", + "apiGatewayProxyV2IamEvent.json", + "apiGatewayProxyV2LambdaAuthorizerEvent.json", + "apiGatewayProxyV2OtherGetEvent.json", + "apiGatewayProxyV2SchemaMiddlwareInvalidEvent.json", + "apiGatewayProxyV2SchemaMiddlwareValidEvent.json", + "apigatewayeSchemaMiddlwareInvalidEvent.json", + "apigatewayeSchemaMiddlwareValidEvent.json", + "appSyncAuthorizerEvent.json", + "appSyncAuthorizerResponse.json", + "appSyncBatchEvent.json", + "appSyncDirectResolver.json", + "appSyncResolverEvent.json", + "awsConfigRuleConfigurationChanged.json", + "awsConfigRuleOversizedConfiguration.json", + "awsConfigRuleScheduled.json", + "bedrockAgentEvent.json", + "bedrockAgentEventWithPathParams.json", + "bedrockAgentPostEvent.json", + "cloudWatchAlarmEventCompositeMetric.json", + "cloudWatchAlarmEventSingleMetric.json", + "cloudWatchDashboardEvent.json", + "cloudWatchLogEvent.json", + "cloudWatchLogEventWithPolicyLevel.json", + "cloudformationCustomResourceCreate.json", + "cloudformationCustomResourceDelete.json", + "cloudformationCustomResourceUpdate.json", + "codeDeployLifecycleHookEvent.json", + "codePipelineEvent.json", + "codePipelineEventData.json", + "codePipelineEventEmptyUserParameters.json", + "codePipelineEventWithEncryptionKey.json", + "cognitoCreateAuthChallengeEvent.json", + "cognitoCustomEmailSenderEvent.json", + "cognitoCustomMessageEvent.json", + "cognitoCustomSMSSenderEvent.json", + "cognitoDefineAuthChallengeEvent.json", + "cognitoPostAuthenticationEvent.json", + "cognitoPostConfirmationEvent.json", + "cognitoPreAuthenticationEvent.json", + "cognitoPreSignUpEvent.json", + "cognitoPreTokenGenerationEvent.json", + "cognitoPreTokenV2GenerationEvent.json", + "cognitoUserMigrationEvent.json", + "cognitoVerifyAuthChallengeResponseEvent.json", + "connectContactFlowEventAll.json", + "connectContactFlowEventMin.json", + "dynamoStreamEvent.json", + "eventBridgeEvent.json", + "kafkaEventMsk.json", + "kafkaEventSelfManaged.json", + "kinesisFirehoseKinesisEvent.json", + "kinesisFirehosePutEvent.json", + "kinesisFirehoseSQSEvent.json", + "kinesisStreamCloudWatchLogsEvent.json", + "kinesisStreamEvent.json", + "kinesisStreamEventOneRecord.json", + "lambdaFunctionUrlEvent.json", + "lambdaFunctionUrlEventPathTrailingSlash.json", + "lambdaFunctionUrlEventWithHeaders.json", + "lambdaFunctionUrlIAMEvent.json", + "rabbitMQEvent.json", + "s3BatchOperationEventSchemaV1.json", + "s3BatchOperationEventSchemaV2.json", + "s3Event.json", + "s3EventBridgeNotificationObjectCreatedEvent.json", + "s3EventBridgeNotificationObjectDeletedEvent.json", + "s3EventBridgeNotificationObjectExpiredEvent.json", + "s3EventBridgeNotificationObjectRestoreCompletedEvent.json", + "s3EventDecodedKey.json", + "s3EventDeleteObject.json", + "s3EventGlacier.json", + "s3ObjectEventIAMUser.json", + "s3ObjectEventTempCredentials.json", + "s3SqsEvent.json", + "sesEvent.json", + "snsEvent.json", + "snsSqsEvent.json", + "snsSqsFifoEvent.json", + "sqsDlqTriggerEvent.json", + "sqsEvent.json", + "vpcLatticeEvent.json", + "vpcLatticeEventPathTrailingSlash.json", + "vpcLatticeEventV2PathTrailingSlash.json", + "vpcLatticeV2Event.json", + "vpcLatticeV2EventWithHeaders.json", + ], + ) + def test_match_for_not_secrets_manager_event(self, event_name): + event = load_event(file_name=event_name) + route = SecretsManagerRoute( + func=None, + secret_id="arn:aws:secretsmanager:us-west-2:123456789012:secret:MyTestDatabaseSecret-a1b2c3", + ) + actual = route.match(event=event) + assert actual is None diff --git a/tests/unit/event_handler/_async_execution/_routes/test_ses.py b/tests/unit/event_handler/_async_execution/_routes/test_ses.py new file mode 100644 index 00000000000..ebe4b4441fe --- /dev/null +++ b/tests/unit/event_handler/_async_execution/_routes/test_ses.py @@ -0,0 +1,120 @@ +import pytest + +from aws_lambda_powertools.event_handler.async_execution.routes.ses import SESRoute +from tests.functional.utils import load_event + + +class TestSESRoute: + @pytest.mark.parametrize( + "event_name", + [ + "activeMQEvent.json", + "albEvent.json", + "albEventPathTrailingSlash.json", + "albMultiValueHeadersEvent.json", + "albMultiValueQueryStringEvent.json", + "apiGatewayAuthorizerRequestEvent.json", + "apiGatewayAuthorizerTokenEvent.json", + "apiGatewayAuthorizerV2Event.json", + "apiGatewayProxyEvent.json", + "apiGatewayProxyEventAnotherPath.json", + "apiGatewayProxyEventNoOrigin.json", + "apiGatewayProxyEventPathTrailingSlash.json", + "apiGatewayProxyEventPrincipalId.json", + "apiGatewayProxyEvent_noVersionAuth.json", + "apiGatewayProxyOtherEvent.json", + "apiGatewayProxyV2Event.json", + "apiGatewayProxyV2EventPathTrailingSlash.json", + "apiGatewayProxyV2Event_GET.json", + "apiGatewayProxyV2IamEvent.json", + "apiGatewayProxyV2LambdaAuthorizerEvent.json", + "apiGatewayProxyV2OtherGetEvent.json", + "apiGatewayProxyV2SchemaMiddlwareInvalidEvent.json", + "apiGatewayProxyV2SchemaMiddlwareValidEvent.json", + "apigatewayeSchemaMiddlwareInvalidEvent.json", + "apigatewayeSchemaMiddlwareValidEvent.json", + "appSyncAuthorizerEvent.json", + "appSyncAuthorizerResponse.json", + "appSyncBatchEvent.json", + "appSyncDirectResolver.json", + "appSyncResolverEvent.json", + "awsConfigRuleConfigurationChanged.json", + "awsConfigRuleOversizedConfiguration.json", + "awsConfigRuleScheduled.json", + "bedrockAgentEvent.json", + "bedrockAgentEventWithPathParams.json", + "bedrockAgentPostEvent.json", + "cloudWatchAlarmEventCompositeMetric.json", + "cloudWatchAlarmEventSingleMetric.json", + "cloudWatchDashboardEvent.json", + "cloudWatchLogEvent.json", + "cloudWatchLogEventWithPolicyLevel.json", + "cloudformationCustomResourceCreate.json", + "cloudformationCustomResourceDelete.json", + "cloudformationCustomResourceUpdate.json", + "codeDeployLifecycleHookEvent.json", + "codePipelineEvent.json", + "codePipelineEventData.json", + "codePipelineEventEmptyUserParameters.json", + "codePipelineEventWithEncryptionKey.json", + "cognitoCreateAuthChallengeEvent.json", + "cognitoCustomEmailSenderEvent.json", + "cognitoCustomMessageEvent.json", + "cognitoCustomSMSSenderEvent.json", + "cognitoDefineAuthChallengeEvent.json", + "cognitoPostAuthenticationEvent.json", + "cognitoPostConfirmationEvent.json", + "cognitoPreAuthenticationEvent.json", + "cognitoPreSignUpEvent.json", + "cognitoPreTokenGenerationEvent.json", + "cognitoPreTokenV2GenerationEvent.json", + "cognitoUserMigrationEvent.json", + "cognitoVerifyAuthChallengeResponseEvent.json", + "connectContactFlowEventAll.json", + "connectContactFlowEventMin.json", + "dynamoStreamEvent.json", + "eventBridgeEvent.json", + "kafkaEventMsk.json", + "kafkaEventSelfManaged.json", + "kinesisFirehoseKinesisEvent.json", + "kinesisFirehosePutEvent.json", + "kinesisFirehoseSQSEvent.json", + "kinesisStreamCloudWatchLogsEvent.json", + "kinesisStreamEvent.json", + "kinesisStreamEventOneRecord.json", + "lambdaFunctionUrlEvent.json", + "lambdaFunctionUrlEventPathTrailingSlash.json", + "lambdaFunctionUrlEventWithHeaders.json", + "lambdaFunctionUrlIAMEvent.json", + "rabbitMQEvent.json", + "s3BatchOperationEventSchemaV1.json", + "s3BatchOperationEventSchemaV2.json", + "s3Event.json", + "s3EventBridgeNotificationObjectCreatedEvent.json", + "s3EventBridgeNotificationObjectDeletedEvent.json", + "s3EventBridgeNotificationObjectExpiredEvent.json", + "s3EventBridgeNotificationObjectRestoreCompletedEvent.json", + "s3EventDecodedKey.json", + "s3EventDeleteObject.json", + "s3EventGlacier.json", + "s3ObjectEventIAMUser.json", + "s3ObjectEventTempCredentials.json", + "s3SqsEvent.json", + "secretsManagerEvent.json", + "snsEvent.json", + "snsSqsEvent.json", + "snsSqsFifoEvent.json", + "sqsDlqTriggerEvent.json", + "sqsEvent.json", + "vpcLatticeEvent.json", + "vpcLatticeEventPathTrailingSlash.json", + "vpcLatticeEventV2PathTrailingSlash.json", + "vpcLatticeV2Event.json", + "vpcLatticeV2EventWithHeaders.json", + ], + ) + def test_match_for_not_ses_event(self, event_name): + event = load_event(file_name=event_name) + route = SESRoute(func=None, mail_to=["johndoe@example.com"]) + actual = route.match(event=event) + assert actual is None diff --git a/tests/unit/event_handler/_async_execution/_routes/test_sns.py b/tests/unit/event_handler/_async_execution/_routes/test_sns.py new file mode 100644 index 00000000000..e4dbb952e95 --- /dev/null +++ b/tests/unit/event_handler/_async_execution/_routes/test_sns.py @@ -0,0 +1,120 @@ +import pytest + +from aws_lambda_powertools.event_handler.async_execution.routes.sns import SNSRoute +from tests.functional.utils import load_event + + +class TestSNSRoute: + @pytest.mark.parametrize( + "event_name", + [ + "activeMQEvent.json", + "albEvent.json", + "albEventPathTrailingSlash.json", + "albMultiValueHeadersEvent.json", + "albMultiValueQueryStringEvent.json", + "apiGatewayAuthorizerRequestEvent.json", + "apiGatewayAuthorizerTokenEvent.json", + "apiGatewayAuthorizerV2Event.json", + "apiGatewayProxyEvent.json", + "apiGatewayProxyEventAnotherPath.json", + "apiGatewayProxyEventNoOrigin.json", + "apiGatewayProxyEventPathTrailingSlash.json", + "apiGatewayProxyEventPrincipalId.json", + "apiGatewayProxyEvent_noVersionAuth.json", + "apiGatewayProxyOtherEvent.json", + "apiGatewayProxyV2Event.json", + "apiGatewayProxyV2EventPathTrailingSlash.json", + "apiGatewayProxyV2Event_GET.json", + "apiGatewayProxyV2IamEvent.json", + "apiGatewayProxyV2LambdaAuthorizerEvent.json", + "apiGatewayProxyV2OtherGetEvent.json", + "apiGatewayProxyV2SchemaMiddlwareInvalidEvent.json", + "apiGatewayProxyV2SchemaMiddlwareValidEvent.json", + "apigatewayeSchemaMiddlwareInvalidEvent.json", + "apigatewayeSchemaMiddlwareValidEvent.json", + "appSyncAuthorizerEvent.json", + "appSyncAuthorizerResponse.json", + "appSyncBatchEvent.json", + "appSyncDirectResolver.json", + "appSyncResolverEvent.json", + "awsConfigRuleConfigurationChanged.json", + "awsConfigRuleOversizedConfiguration.json", + "awsConfigRuleScheduled.json", + "bedrockAgentEvent.json", + "bedrockAgentEventWithPathParams.json", + "bedrockAgentPostEvent.json", + "cloudWatchAlarmEventCompositeMetric.json", + "cloudWatchAlarmEventSingleMetric.json", + "cloudWatchDashboardEvent.json", + "cloudWatchLogEvent.json", + "cloudWatchLogEventWithPolicyLevel.json", + "cloudformationCustomResourceCreate.json", + "cloudformationCustomResourceDelete.json", + "cloudformationCustomResourceUpdate.json", + "codeDeployLifecycleHookEvent.json", + "codePipelineEvent.json", + "codePipelineEventData.json", + "codePipelineEventEmptyUserParameters.json", + "codePipelineEventWithEncryptionKey.json", + "cognitoCreateAuthChallengeEvent.json", + "cognitoCustomEmailSenderEvent.json", + "cognitoCustomMessageEvent.json", + "cognitoCustomSMSSenderEvent.json", + "cognitoDefineAuthChallengeEvent.json", + "cognitoPostAuthenticationEvent.json", + "cognitoPostConfirmationEvent.json", + "cognitoPreAuthenticationEvent.json", + "cognitoPreSignUpEvent.json", + "cognitoPreTokenGenerationEvent.json", + "cognitoPreTokenV2GenerationEvent.json", + "cognitoUserMigrationEvent.json", + "cognitoVerifyAuthChallengeResponseEvent.json", + "connectContactFlowEventAll.json", + "connectContactFlowEventMin.json", + "dynamoStreamEvent.json", + "eventBridgeEvent.json", + "kafkaEventMsk.json", + "kafkaEventSelfManaged.json", + "kinesisFirehoseKinesisEvent.json", + "kinesisFirehosePutEvent.json", + "kinesisFirehoseSQSEvent.json", + "kinesisStreamCloudWatchLogsEvent.json", + "kinesisStreamEvent.json", + "kinesisStreamEventOneRecord.json", + "lambdaFunctionUrlEvent.json", + "lambdaFunctionUrlEventPathTrailingSlash.json", + "lambdaFunctionUrlEventWithHeaders.json", + "lambdaFunctionUrlIAMEvent.json", + "rabbitMQEvent.json", + "s3BatchOperationEventSchemaV1.json", + "s3BatchOperationEventSchemaV2.json", + "s3Event.json", + "s3EventBridgeNotificationObjectCreatedEvent.json", + "s3EventBridgeNotificationObjectDeletedEvent.json", + "s3EventBridgeNotificationObjectExpiredEvent.json", + "s3EventBridgeNotificationObjectRestoreCompletedEvent.json", + "s3EventDecodedKey.json", + "s3EventDeleteObject.json", + "s3EventGlacier.json", + "s3ObjectEventIAMUser.json", + "s3ObjectEventTempCredentials.json", + "s3SqsEvent.json", + "secretsManagerEvent.json", + "sesEvent.json", + "snsSqsEvent.json", + "snsSqsFifoEvent.json", + "sqsDlqTriggerEvent.json", + "sqsEvent.json", + "vpcLatticeEvent.json", + "vpcLatticeEventPathTrailingSlash.json", + "vpcLatticeEventV2PathTrailingSlash.json", + "vpcLatticeV2Event.json", + "vpcLatticeV2EventWithHeaders.json", + ], + ) + def test_match_for_not_sns_event(self, event_name): + event = load_event(file_name=event_name) + route = SNSRoute(func=None, arn="arn:aws:sns:us-east-2:123456789012:sns-lambda") + actual = route.match(event=event) + assert actual is None