From 1e60a4fc321a54a600e0d26a7a4b6ae0d7d1d017 Mon Sep 17 00:00:00 2001 From: Akira Noda Date: Fri, 26 Jan 2024 19:52:27 +0900 Subject: [PATCH 01/10] S3: Adding notification for eventbridge --- moto/events/utils.py | 12 ++++++++++++ moto/s3/models.py | 11 ++++++----- moto/s3/notifications.py | 17 +++++++++++++++++ tests/test_s3/test_s3_config.py | 2 ++ 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/moto/events/utils.py b/moto/events/utils.py index ad359ab6dc2f..60f7ebc218a7 100644 --- a/moto/events/utils.py +++ b/moto/events/utils.py @@ -14,3 +14,15 @@ "fail_on_invalid_token": False, }, } + +_BASE_EVENT_MESSAGE = { + "version": "0", + "id": "17793124-05d4-b198-2fde-7ededc63b103", + "detail-type": "", + "source": "", + "account": "", + "time": "2021-11-12T00:00:00Z", + "region": "", + "resources": [], + "detail": {}, +} diff --git a/moto/s3/models.py b/moto/s3/models.py index 480601e476e1..77573439e74c 100644 --- a/moto/s3/models.py +++ b/moto/s3/models.py @@ -880,6 +880,7 @@ def __init__( topic: Optional[List[Dict[str, Any]]] = None, queue: Optional[List[Dict[str, Any]]] = None, cloud_function: Optional[List[Dict[str, Any]]] = None, + event_bridge: Optional[Dict] = None, ): self.topic = ( [ @@ -920,6 +921,7 @@ def __init__( if cloud_function else [] ) + self.event_bridge = event_bridge def to_config_dict(self) -> Dict[str, Any]: data: Dict[str, Any] = {"configurations": {}} @@ -942,6 +944,8 @@ def to_config_dict(self) -> Dict[str, Any]: cf_config["type"] = "LambdaConfiguration" data["configurations"][cloud_function.id] = cf_config + if self.event_bridge is not None: + data["configurations"]["EventBridgeConfiguration"] = self.event_bridge return data @@ -1322,6 +1326,7 @@ def set_notification_configuration( topic=notification_config.get("TopicConfiguration"), queue=notification_config.get("QueueConfiguration"), cloud_function=notification_config.get("CloudFunctionConfiguration"), + event_bridge=notification_config.get("EventBridgeConfiguration"), ) # Validate that the region is correct: @@ -2311,11 +2316,7 @@ def put_bucket_notification_configuration( - AWSLambda - SNS - SQS - - For the following events: - - - 's3:ObjectCreated:Copy' - - 's3:ObjectCreated:Put' + - EventBridge """ bucket = self.get_bucket(bucket_name) bucket.set_notification_configuration(notification_config) diff --git a/moto/s3/notifications.py b/moto/s3/notifications.py index 389b60070dd9..c618192a32ac 100644 --- a/moto/s3/notifications.py +++ b/moto/s3/notifications.py @@ -3,6 +3,8 @@ from enum import Enum from typing import Any, Dict, List +from moto.events.notifications import _BASE_EVENT_MESSAGE + _EVENT_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%f" @@ -147,6 +149,21 @@ def _send_sns_message( pass +def _send_event_bridge_message( + account_id: str, event_body: Any, region_name: str, resources: Any, detail: Any +): + try: + from moto.events.models import events_backends + event = None + if source == "aws.s3" and event_name in + except: # noqa + # This is an async action in AWS. + # Even if this part fails, the calling function should pass, so catch all errors + # Possible exceptions that could be thrown: + # - EventBridge does not exist + pass + + def _invoke_awslambda( account_id: str, event_body: Any, fn_arn: str, region_name: str ) -> None: diff --git a/tests/test_s3/test_s3_config.py b/tests/test_s3/test_s3_config.py index c4c64d200f57..e64edc44cdee 100644 --- a/tests/test_s3/test_s3_config.py +++ b/tests/test_s3/test_s3_config.py @@ -321,6 +321,7 @@ def test_s3_notification_config_dict(): }, } ], + "EventBridgeConfiguration": {}, } s3_config_query.backends[DEFAULT_ACCOUNT_ID][ @@ -371,6 +372,7 @@ def test_s3_notification_config_dict(): "queueARN": "arn:aws:lambda:us-west-2:012345678910:function:mylambda", "type": "LambdaConfiguration", }, + "EventBridgeConfiguration": {}, } } From 8b4a8c8b250d77d067f307f77c292cff042f2cfb Mon Sep 17 00:00:00 2001 From: Akira Noda Date: Wed, 31 Jan 2024 19:35:19 +0900 Subject: [PATCH 02/10] adding a unit tests --- moto/events/utils.py | 2 +- moto/s3/notifications.py | 31 +++++++++-- moto/s3/responses.py | 7 +++ .../test_s3_eventbridge_integration.py | 52 +++++++++++++++++++ 4 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 tests/test_s3/test_s3_eventbridge_integration.py diff --git a/moto/events/utils.py b/moto/events/utils.py index 9feeabb83183..93ef9ec5aa73 100644 --- a/moto/events/utils.py +++ b/moto/events/utils.py @@ -37,7 +37,7 @@ }, } -_BASE_EVENT_MESSAGE = { +_BASE_EVENT_MESSAGE: EventMessageType = { "version": "0", "id": "17793124-05d4-b198-2fde-7ededc63b103", "detail-type": "", diff --git a/moto/s3/notifications.py b/moto/s3/notifications.py index 4e06af0c1d10..5035dad717b8 100644 --- a/moto/s3/notifications.py +++ b/moto/s3/notifications.py @@ -3,7 +3,9 @@ from enum import Enum from typing import Any, Dict, List -from moto.events.notifications import _BASE_EVENT_MESSAGE +from moto.core.utils import ( + unix_time, +) _EVENT_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%f" @@ -124,6 +126,10 @@ def send_event( _send_sns_message(account_id, event_body, topic_arn, region_name) + if bucket.notification_configuration.event_bridge is not None: + event_body = {"test event": 123} + _send_event_bridge_message(account_id, event_body, "us-east-1") + def _send_sqs_message( account_id: str, event_body: Any, queue_name: str, region_name: str @@ -160,12 +166,29 @@ def _send_sns_message( def _send_event_bridge_message( - account_id: str, event_body: Any, region_name: str, resources: Any, detail: Any + account_id: str, + event_body: Any, + region_name: str, # resources: Any, detail: Any ): try: from moto.events.models import events_backends - event = None - if source == "aws.s3" and event_name in + + event = { + "version": "0", + "id": "17793124-05d4-b198-2fde-7ededc63b103", + "detail-type": "Object Created Custom Event Here", + "source": "aws.s3", + "account": "123456789012", + "time": unix_time(), + "region": "us-west-2", + "resources": [], + "detail": {}, + } + events_backend = events_backends[account_id][region_name] + for event_bus in events_backend.event_buses.values(): + for rule in event_bus.rules.values(): + rule.send_to_targets(event) + except: # noqa # This is an async action in AWS. # Even if this part fails, the calling function should pass, so catch all errors diff --git a/moto/s3/responses.py b/moto/s3/responses.py index d9ed982cb704..9408a663d407 100644 --- a/moto/s3/responses.py +++ b/moto/s3/responses.py @@ -2092,12 +2092,19 @@ def _notification_config_from_body(self) -> Dict[str, Any]: ("Topic", "sns"), ("Queue", "sqs"), ("CloudFunction", "lambda"), + ("EventBridge", "events"), ] found_notifications = ( 0 # Tripwire -- if this is not ever set, then there were no notifications ) for name, arn_string in notification_fields: + # EventBridgeConfiguration is passed as an empty dict. + if name == "EventBridge": + events_field = f"{name}Configuration" + if events_field in parsed_xml["NotificationConfiguration"]: + parsed_xml["NotificationConfiguration"][events_field] = {} + found_notifications += 1 # 1st verify that the proper notification configuration has been passed in (with an ARN that is close # to being correct -- nothing too complex in the ARN logic): the_notification = parsed_xml["NotificationConfiguration"].get( diff --git a/tests/test_s3/test_s3_eventbridge_integration.py b/tests/test_s3/test_s3_eventbridge_integration.py new file mode 100644 index 000000000000..1077b92c2580 --- /dev/null +++ b/tests/test_s3/test_s3_eventbridge_integration.py @@ -0,0 +1,52 @@ +import json +from uuid import uuid4 + +import boto3 + +from moto import mock_aws +from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID + +REGION_NAME = "us-east-1" + + +@mock_aws +def test_objectcreated_put__unknown_lambda_is_handled_gracefully(): + s3_res = boto3.resource("s3", region_name=REGION_NAME) + s3_client = boto3.client("s3", region_name=REGION_NAME) + events_client = boto3.client("events", region_name=REGION_NAME) + logs_client = boto3.client("logs", region_name=REGION_NAME) + + rule_name = "test-rule" + events_client.put_rule( + Name=rule_name, EventPattern=json.dumps({"account": [ACCOUNT_ID]}) + ) + log_group_name = "/test-group" + logs_client.create_log_group(logGroupName=log_group_name) + events_client.put_targets( + Rule=rule_name, + Targets=[ + { + "Id": "test", + "Arn": f"arn:aws:logs:{REGION_NAME}:{ACCOUNT_ID}:log-group:{log_group_name}", + } + ], + ) + + # Create S3 bucket + bucket_name = str(uuid4()) + s3_res.create_bucket(Bucket=bucket_name) + + # Put Notification + s3_client.put_bucket_notification_configuration( + Bucket=bucket_name, + NotificationConfiguration={"EventBridgeConfiguration": {}}, + ) + + # Put Object + s3_client.put_object(Bucket=bucket_name, Key="keyname", Body="bodyofnewobject") + + events = sorted( + logs_client.filter_log_events(logGroupName=log_group_name)["events"], + key=lambda item: item["eventId"], + ) + print(events) From 027b3d712faf4e0a6828ee97e48fce2950b59d60 Mon Sep 17 00:00:00 2001 From: Akira Noda Date: Mon, 5 Feb 2024 18:05:37 +0900 Subject: [PATCH 03/10] adding unit tests --- moto/s3/models.py | 2 +- moto/s3/notifications.py | 77 ++++++++++++++++--- .../test_s3_eventbridge_integration.py | 8 +- 3 files changed, 75 insertions(+), 12 deletions(-) diff --git a/moto/s3/models.py b/moto/s3/models.py index 13c84b8f107e..6d2e9da8b955 100644 --- a/moto/s3/models.py +++ b/moto/s3/models.py @@ -883,7 +883,7 @@ def __init__( topic: Optional[List[Dict[str, Any]]] = None, queue: Optional[List[Dict[str, Any]]] = None, cloud_function: Optional[List[Dict[str, Any]]] = None, - event_bridge: Optional[Dict] = None, + event_bridge: Optional[Dict[str, Any]] = None, ): self.topic = ( [ diff --git a/moto/s3/notifications.py b/moto/s3/notifications.py index 5035dad717b8..cb54996d57f7 100644 --- a/moto/s3/notifications.py +++ b/moto/s3/notifications.py @@ -127,8 +127,7 @@ def send_event( _send_sns_message(account_id, event_body, topic_arn, region_name) if bucket.notification_configuration.event_bridge is not None: - event_body = {"test event": 123} - _send_event_bridge_message(account_id, event_body, "us-east-1") + _send_event_bridge_message(account_id, bucket, event_name, key) def _send_sqs_message( @@ -167,24 +166,41 @@ def _send_sns_message( def _send_event_bridge_message( account_id: str, - event_body: Any, - region_name: str, # resources: Any, detail: Any -): + bucket: Any, + event_name: str, + key: Any, +) -> None: try: from moto.events.models import events_backends event = { "version": "0", "id": "17793124-05d4-b198-2fde-7ededc63b103", - "detail-type": "Object Created Custom Event Here", + "detail-type": _detail_type(event_name), "source": "aws.s3", "account": "123456789012", "time": unix_time(), - "region": "us-west-2", - "resources": [], - "detail": {}, + "region": bucket.region_name, + "resources": [f"arn:aws:s3:::{bucket.name}"], + "detail": { + "version": "0", + "bucket": {"name": bucket.name}, + "object": { + "key": key.name, + "size": key.size, + "eTag": key.etag.replace('"', ""), + "version-id": "IYV3p45BT0ac8hjHg1houSdS1a.Mro8e", + "sequencer": "617f08299329d189", + }, + "request-id": "N4N7GDK58NMKJ12R", + "requester": "123456789012", + "source-ip-address": "1.2.3.4", + # ex) s3:ObjectCreated:Put -> ObjectCreated + "reason": event_name.split(":")[1], + }, } - events_backend = events_backends[account_id][region_name] + + events_backend = events_backends[account_id][bucket.region_name] for event_bus in events_backend.event_buses.values(): for rule in event_bus.rules.values(): rule.send_to_targets(event) @@ -197,6 +213,47 @@ def _send_event_bridge_message( pass +def _detail_type(event_name: str) -> str: + if event_name in [e for e in S3NotificationEvent.events() if "ObjectCreated" in e]: + return "Object Created" + elif event_name in [ + e + for e in S3NotificationEvent.events() + if "ObjectRemoved" in e or "LifecycleExpiration" in e + ]: + return "Object Deleted" + elif event_name in [ + e for e in S3NotificationEvent.events() if "ObjectRestore" in e + ]: + if event_name == S3NotificationEvent.OBJECT_RESTORE_POST_EVENT: + return "Object Restore Initiated" + elif event_name == S3NotificationEvent.OBJECT_RESTORE_COMPLETED_EVENT: + return "Object Restore Completed" + else: + # s3:ObjectRestore:Delete event + return "Object Restore Expired" + elif event_name in [ + e for e in S3NotificationEvent.events() if "LifecycleTransition" in e + ]: + return "Object Storage Class Changed" + elif event_name in [ + e for e in S3NotificationEvent.events() if "IntelligentTiering" in e + ]: + return "Object Access Tier Changed" + elif event_name in [e for e in S3NotificationEvent.events() if "ObjectAcl" in e]: + return "Object ACL Updated" + elif event_name in [e for e in S3NotificationEvent.events() if "ObjectTagging"]: + if event_name == S3NotificationEvent.OBJECT_TAGGING_PUT_EVENT: + return "Object Tags Added" + else: + # s3:ObjectTagging:Delete event + return "Object Tags Deleted" + else: + raise ValueError( + "unsupported event for s3 eventbridge notification (https://docs.aws.amazon.com/AmazonS3/latest/userguide/EventBridge.html)" + ) + + def _invoke_awslambda( account_id: str, event_body: Any, fn_arn: str, region_name: str ) -> None: diff --git a/tests/test_s3/test_s3_eventbridge_integration.py b/tests/test_s3/test_s3_eventbridge_integration.py index 1077b92c2580..3ca5e4f990ea 100644 --- a/tests/test_s3/test_s3_eventbridge_integration.py +++ b/tests/test_s3/test_s3_eventbridge_integration.py @@ -49,4 +49,10 @@ def test_objectcreated_put__unknown_lambda_is_handled_gracefully(): logs_client.filter_log_events(logGroupName=log_group_name)["events"], key=lambda item: item["eventId"], ) - print(events) + assert len(events) == 1 + event_message = json.loads(events[0]["message"]) + assert event_message["detail-type"] == "Object Created" + assert event_message["source"] == "aws.s3" + assert event_message["region"] == REGION_NAME + assert event_message["detail"]["bucket"]["name"] == bucket_name + assert event_message["detail"]["reason"] == "ObjectCreated" From cb5047f82e7ec05482bf991365c3cb3655a2237d Mon Sep 17 00:00:00 2001 From: Akira Noda Date: Mon, 5 Feb 2024 19:19:38 +0900 Subject: [PATCH 04/10] adding typign --- moto/events/utils.py | 4 +- moto/s3/notifications.py | 53 ++++++++++--------- .../test_s3_eventbridge_integration.py | 1 + 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/moto/events/utils.py b/moto/events/utils.py index 93ef9ec5aa73..0c73fa71986c 100644 --- a/moto/events/utils.py +++ b/moto/events/utils.py @@ -13,7 +13,7 @@ "detail-type": "Required[Union[str, List[str]]]", "source": "Required[Union[str, List[str]]]", "account": str, - "time": str, + "time": "Union[str, float]", "region": str, "resources": List[str], "detail": "Required[Dict[str, Any]]", @@ -43,7 +43,7 @@ "detail-type": "", "source": "", "account": "", - "time": "2021-11-12T00:00:00Z", + "time": "", "region": "", "resources": [], "detail": {}, diff --git a/moto/s3/notifications.py b/moto/s3/notifications.py index cb54996d57f7..7c66a929d38d 100644 --- a/moto/s3/notifications.py +++ b/moto/s3/notifications.py @@ -1,11 +1,11 @@ +import copy import json from datetime import datetime from enum import Enum from typing import Any, Dict, List -from moto.core.utils import ( - unix_time, -) +from moto.core.utils import unix_time +from moto.events.utils import _BASE_EVENT_MESSAGE _EVENT_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%f" @@ -173,31 +173,28 @@ def _send_event_bridge_message( try: from moto.events.models import events_backends - event = { + event = copy.deepcopy(_BASE_EVENT_MESSAGE) + event["detail-type"] = _detail_type(event_name) + event["source"] = "aws.s3" + event["account"] = account_id + event["time"] = unix_time() + event["region"] = bucket.region_name + event["resources"] = [f"arn:aws:s3:::{bucket.name}"] + event["detail"] = { "version": "0", - "id": "17793124-05d4-b198-2fde-7ededc63b103", - "detail-type": _detail_type(event_name), - "source": "aws.s3", - "account": "123456789012", - "time": unix_time(), - "region": bucket.region_name, - "resources": [f"arn:aws:s3:::{bucket.name}"], - "detail": { - "version": "0", - "bucket": {"name": bucket.name}, - "object": { - "key": key.name, - "size": key.size, - "eTag": key.etag.replace('"', ""), - "version-id": "IYV3p45BT0ac8hjHg1houSdS1a.Mro8e", - "sequencer": "617f08299329d189", - }, - "request-id": "N4N7GDK58NMKJ12R", - "requester": "123456789012", - "source-ip-address": "1.2.3.4", - # ex) s3:ObjectCreated:Put -> ObjectCreated - "reason": event_name.split(":")[1], + "bucket": {"name": bucket.name}, + "object": { + "key": key.name, + "size": key.size, + "eTag": key.etag.replace('"', ""), + "version-id": "IYV3p45BT0ac8hjHg1houSdS1a.Mro8e", + "sequencer": "617f08299329d189", }, + "request-id": "N4N7GDK58NMKJ12R", + "requester": "123456789012", + "source-ip-address": "1.2.3.4", + # ex) s3:ObjectCreated:Put -> ObjectCreated + "reason": event_name.split(":")[1], } events_backend = events_backends[account_id][bucket.region_name] @@ -214,6 +211,10 @@ def _send_event_bridge_message( def _detail_type(event_name: str) -> str: + """Detail type field values for event messages of s3 EventBridge notification + + document: https://docs.aws.amazon.com/AmazonS3/latest/userguide/EventBridge.html + """ if event_name in [e for e in S3NotificationEvent.events() if "ObjectCreated" in e]: return "Object Created" elif event_name in [ diff --git a/tests/test_s3/test_s3_eventbridge_integration.py b/tests/test_s3/test_s3_eventbridge_integration.py index 3ca5e4f990ea..4372f9fa7a02 100644 --- a/tests/test_s3/test_s3_eventbridge_integration.py +++ b/tests/test_s3/test_s3_eventbridge_integration.py @@ -53,6 +53,7 @@ def test_objectcreated_put__unknown_lambda_is_handled_gracefully(): event_message = json.loads(events[0]["message"]) assert event_message["detail-type"] == "Object Created" assert event_message["source"] == "aws.s3" + assert event_message["account"] == ACCOUNT_ID assert event_message["region"] == REGION_NAME assert event_message["detail"]["bucket"]["name"] == bucket_name assert event_message["detail"]["reason"] == "ObjectCreated" From 0a712c734153de0d95d1a3d9e2c4afdfbda2e9d6 Mon Sep 17 00:00:00 2001 From: tsugumi-sys Date: Mon, 12 Feb 2024 16:14:45 +0900 Subject: [PATCH 05/10] adding unit tests --- moto/s3/notifications.py | 2 +- tests/test_s3/test_s3_notifications.py | 131 +++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 tests/test_s3/test_s3_notifications.py diff --git a/moto/s3/notifications.py b/moto/s3/notifications.py index 7c66a929d38d..7bb25d2fb5cb 100644 --- a/moto/s3/notifications.py +++ b/moto/s3/notifications.py @@ -251,7 +251,7 @@ def _detail_type(event_name: str) -> str: return "Object Tags Deleted" else: raise ValueError( - "unsupported event for s3 eventbridge notification (https://docs.aws.amazon.com/AmazonS3/latest/userguide/EventBridge.html)" + f"unsupported event `{event_name}` for s3 eventbridge notification (https://docs.aws.amazon.com/AmazonS3/latest/userguide/EventBridge.html)" ) diff --git a/tests/test_s3/test_s3_notifications.py b/tests/test_s3/test_s3_notifications.py new file mode 100644 index 000000000000..f7a5448f31bf --- /dev/null +++ b/tests/test_s3/test_s3_notifications.py @@ -0,0 +1,131 @@ +import json +from typing import List + +import boto3 +import pytest + +from moto import mock_aws +from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID +from moto.s3.models import FakeBucket, FakeKey +from moto.s3.notifications import ( + S3NotificationEvent, + _detail_type, + _send_event_bridge_message, +) + +REGION_NAME = "us-east-1" + + +@pytest.mark.parametrize( + "event_names, expected_event_message", + [ + ( + [ + S3NotificationEvent.OBJECT_CREATED_PUT_EVENT, + S3NotificationEvent.OBJECT_CREATED_POST_EVENT, + S3NotificationEvent.OBJECT_CREATED_COPY_EVENT, + S3NotificationEvent.OBJECT_CREATED_COMPLETE_MULTIPART_UPLOAD_EVENT, + ], + "Object Created", + ), + ( + [ + S3NotificationEvent.OBJECT_REMOVED_DELETE_EVENT, + S3NotificationEvent.OBJECT_REMOVED_DELETE_MARKER_CREATED_EVENT, + ], + "Object Deleted", + ), + ([S3NotificationEvent.OBJECT_RESTORE_POST_EVENT], "Object Restore Initiated"), + ( + [S3NotificationEvent.OBJECT_RESTORE_COMPLETED_EVENT], + "Object Restore Completed", + ), + ( + [S3NotificationEvent.OBJECT_RESTORE_DELETE_EVENT], + "Object Restore Expired", + ), + ( + [S3NotificationEvent.LIFECYCLE_TRANSITION_EVENT], + "Object Storage Class Changed", + ), + ([S3NotificationEvent.INTELLIGENT_TIERING_EVENT], "Object Access Tier Changed"), + ([S3NotificationEvent.OBJECT_ACL_EVENT], "Object ACL Updated"), + ([S3NotificationEvent.OBJECT_TAGGING_PUT_EVENT], "Object Tags Added"), + ([S3NotificationEvent.OBJECT_TAGGING_DELETE_EVENT], "Object Tags Deleted"), + ], +) +def test_detail_type(event_names: List[str], expected_event_message: str): + for event_name in event_names: + assert _detail_type(event_name) == expected_event_message + + +def test_detail_type_unknown_event(): + with pytest.raises(ValueError) as ex: + _detail_type("unknown event") + assert ( + str(ex.value) + == "unsupported event `unknown event` for s3 eventbridge notification (https://docs.aws.amazon.com/AmazonS3/latest/userguide/EventBridge.html)" + ) + + +@mock_aws +def test_send_event_bridge_message(): + # setup mocks + events_client = boto3.client("events", region_name=REGION_NAME) + logs_client = boto3.client("logs", region_name=REGION_NAME) + rule_name = "test-rule" + events_client.put_rule( + Name=rule_name, EventPattern=json.dumps({"account": [ACCOUNT_ID]}) + ) + log_group_name = "/test-group" + logs_client.create_log_group(logGroupName=log_group_name) + mocked_bucket = FakeBucket("test-bucket", ACCOUNT_ID, REGION_NAME) + mocked_key = FakeKey( + "test-key", bytes("test content", encoding="utf-8"), ACCOUNT_ID + ) + + # do nothing if event target does not exists. + _send_event_bridge_message( + ACCOUNT_ID, + mocked_bucket, + S3NotificationEvent.OBJECT_CREATED_PUT_EVENT, + mocked_key, + ) + assert ( + len(logs_client.filter_log_events(logGroupName=log_group_name)["events"]) == 0 + ) + + # do nothing even if an error is raised while sending events. + events_client.put_targets( + Rule=rule_name, + Targets=[ + { + "Id": "test", + "Arn": f"arn:aws:logs:{REGION_NAME}:{ACCOUNT_ID}:log-group:{log_group_name}", + } + ], + ) + + _send_event_bridge_message(ACCOUNT_ID, mocked_bucket, "unknown-event", mocked_key) + assert ( + len(logs_client.filter_log_events(logGroupName=log_group_name)["events"]) == 0 + ) + + # an event is correctlly sent to the log group. + _send_event_bridge_message( + ACCOUNT_ID, + mocked_bucket, + S3NotificationEvent.OBJECT_CREATED_PUT_EVENT, + mocked_key, + ) + events = logs_client.filter_log_events(logGroupName=log_group_name)["events"] + assert len(events) == 1 + event_msg = json.loads(events[0]["message"]) + assert event_msg["detail-type"] == "Object Created" + assert event_msg["source"] == "aws.s3" + assert event_msg["region"] == REGION_NAME + assert event_msg["resources"] == [f"arn:aws:s3:::{mocked_bucket.name}"] + event_detail = event_msg["detail"] + assert event_detail["bucket"] == {"name": mocked_bucket.name} + assert event_detail["object"]["key"] == mocked_key.name + assert event_detail["reason"] == "ObjectCreated" From 074a73bae3689c036bb0fd1b0560375ad9929eef Mon Sep 17 00:00:00 2001 From: tsugumi-sys Date: Mon, 12 Feb 2024 17:23:18 +0900 Subject: [PATCH 06/10] skip tests when proxy mode --- tests/test_s3/test_s3_notifications.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/test_s3/test_s3_notifications.py b/tests/test_s3/test_s3_notifications.py index f7a5448f31bf..3fe63cad2ad1 100644 --- a/tests/test_s3/test_s3_notifications.py +++ b/tests/test_s3/test_s3_notifications.py @@ -1,10 +1,11 @@ import json from typing import List +from unittest import SkipTest import boto3 import pytest -from moto import mock_aws +from moto import mock_aws, settings from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID from moto.s3.models import FakeBucket, FakeKey from moto.s3.notifications import ( @@ -111,7 +112,9 @@ def test_send_event_bridge_message(): len(logs_client.filter_log_events(logGroupName=log_group_name)["events"]) == 0 ) - # an event is correctlly sent to the log group. + if settings.is_test_proxy_mode(): + raise SkipTest(("Doesn't quite work right with the Proxy")) + # an event is correctly sent to the log group. _send_event_bridge_message( ACCOUNT_ID, mocked_bucket, From e6e8311a95543968574deb5f585fb7e66a04a612 Mon Sep 17 00:00:00 2001 From: tsugumi-sys Date: Mon, 12 Feb 2024 17:43:15 +0900 Subject: [PATCH 07/10] avoid bucket already exists error --- tests/test_s3/test_s3_notifications.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_s3/test_s3_notifications.py b/tests/test_s3/test_s3_notifications.py index 3fe63cad2ad1..e6bc53529e69 100644 --- a/tests/test_s3/test_s3_notifications.py +++ b/tests/test_s3/test_s3_notifications.py @@ -1,6 +1,7 @@ import json from typing import List from unittest import SkipTest +from uuid import uuid4 import boto3 import pytest @@ -80,7 +81,7 @@ def test_send_event_bridge_message(): ) log_group_name = "/test-group" logs_client.create_log_group(logGroupName=log_group_name) - mocked_bucket = FakeBucket("test-bucket", ACCOUNT_ID, REGION_NAME) + mocked_bucket = FakeBucket(str(uuid4()), ACCOUNT_ID, REGION_NAME) mocked_key = FakeKey( "test-key", bytes("test content", encoding="utf-8"), ACCOUNT_ID ) From 6d840205290b4f37948321278c40972ec77e3be2 Mon Sep 17 00:00:00 2001 From: tsugumi-sys Date: Mon, 12 Feb 2024 18:06:23 +0900 Subject: [PATCH 08/10] skip test if not decorator mode --- tests/test_s3/test_s3_notifications.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_s3/test_s3_notifications.py b/tests/test_s3/test_s3_notifications.py index e6bc53529e69..a7c1c5c4aa1c 100644 --- a/tests/test_s3/test_s3_notifications.py +++ b/tests/test_s3/test_s3_notifications.py @@ -113,8 +113,8 @@ def test_send_event_bridge_message(): len(logs_client.filter_log_events(logGroupName=log_group_name)["events"]) == 0 ) - if settings.is_test_proxy_mode(): - raise SkipTest(("Doesn't quite work right with the Proxy")) + if not settings.TEST_DECORATOR_MODE: + raise SkipTest(("Doesn't quite work right with the Proxy or Server")) # an event is correctly sent to the log group. _send_event_bridge_message( ACCOUNT_ID, From 96a8d3a401df606030e1be16faa2a8c438b1f4aa Mon Sep 17 00:00:00 2001 From: tsugumi-sys Date: Mon, 12 Feb 2024 18:37:52 +0900 Subject: [PATCH 09/10] fix test name --- tests/test_s3/test_s3_eventbridge_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_s3/test_s3_eventbridge_integration.py b/tests/test_s3/test_s3_eventbridge_integration.py index 4372f9fa7a02..40854e6a56a9 100644 --- a/tests/test_s3/test_s3_eventbridge_integration.py +++ b/tests/test_s3/test_s3_eventbridge_integration.py @@ -10,7 +10,7 @@ @mock_aws -def test_objectcreated_put__unknown_lambda_is_handled_gracefully(): +def test_pub_object_notification(): s3_res = boto3.resource("s3", region_name=REGION_NAME) s3_client = boto3.client("s3", region_name=REGION_NAME) events_client = boto3.client("events", region_name=REGION_NAME) From 15c3b92308e49a0504ab0e96cc6079b4b1e46840 Mon Sep 17 00:00:00 2001 From: Akira Noda Date: Fri, 16 Feb 2024 11:14:34 +0900 Subject: [PATCH 10/10] add comments --- moto/s3/models.py | 4 ++++ moto/s3/notifications.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/moto/s3/models.py b/moto/s3/models.py index 6d2e9da8b955..0983631ebec8 100644 --- a/moto/s3/models.py +++ b/moto/s3/models.py @@ -2321,6 +2321,10 @@ def put_bucket_notification_configuration( - SNS - SQS - EventBridge + + For the following events: + - 's3:ObjectCreated:Copy' + - 's3:ObjectCreated:Put' """ bucket = self.get_bucket(bucket_name) bucket.set_notification_configuration(notification_config) diff --git a/moto/s3/notifications.py b/moto/s3/notifications.py index 7bb25d2fb5cb..bc9208695ea8 100644 --- a/moto/s3/notifications.py +++ b/moto/s3/notifications.py @@ -5,7 +5,6 @@ from typing import Any, Dict, List from moto.core.utils import unix_time -from moto.events.utils import _BASE_EVENT_MESSAGE _EVENT_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%f" @@ -172,6 +171,7 @@ def _send_event_bridge_message( ) -> None: try: from moto.events.models import events_backends + from moto.events.utils import _BASE_EVENT_MESSAGE event = copy.deepcopy(_BASE_EVENT_MESSAGE) event["detail-type"] = _detail_type(event_name) @@ -187,7 +187,7 @@ def _send_event_bridge_message( "key": key.name, "size": key.size, "eTag": key.etag.replace('"', ""), - "version-id": "IYV3p45BT0ac8hjHg1houSdS1a.Mro8e", + "version-id": key.version_id, "sequencer": "617f08299329d189", }, "request-id": "N4N7GDK58NMKJ12R",