Repository: getmoto/moto
Pull request #7435 (2024-03-07 04:41:22)

S3: Sending notification to EventBridge `OBJECT_CREATED_COMPLETE_MULTIPART_UPLOAD_EVENT`
- Sending a notification to EventBridge when multipart upload is completed.
- Unittest is added.


related #7363


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/7435?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 95.80%. Comparing base [(`9aef694`)](https://app.codecov.io/gh/getmoto/moto/commit/9aef6944da6a6f1b84be99cefb34f1724e6ca58f?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) to head [(`32088ce`)](https://app.codecov.io/gh/getmoto/moto/pull/7435?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).
> Report is 83 commits behind head on master.


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #7435      +/-   ##
==========================================
- Coverage   95.88%   95.80%   -0.08%     
==========================================
  Files         843      849       +6     
  Lines       82578    83692    +1114     
==========================================
+ Hits        79178    80181    +1003     
- Misses       3400     3511     +111     
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/7435/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Coverage Δ | |
|---|---|---|
| [servertests](https://app.codecov.io/gh/getmoto/moto/pull/7435/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `32.30% <0.00%> (-0.22%)` | :arrow_down: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/7435/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `95.77% <100.00%> (-0.08%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#carryforward-flags-in-the-pull-request-comment) to find out more.


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/getmoto/moto/pull/7435?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).


Patch:
diff --git a/moto/moto_proxy/proxy3.py b/moto/moto_proxy/proxy3.py
index 8fd0ef1df..2d98bde04 100644
--- a/moto/moto_proxy/proxy3.py
+++ b/moto/moto_proxy/proxy3.py
@@ -159,7 +159,7 @@ class ProxyRequestHandler(BaseHTTPRequestHandler):
             boundary = self.headers["Content-Type"].split("boundary=")[-1]
             req_body, form_data = get_body_from_form_data(req_body, boundary)  # type: ignore
             for key, val in form_data.items():
-                self.headers[key] = [val]
+                self.headers[key] = [val]  # type: ignore[assignment]
         else:
             form_data = {}
 
diff --git a/moto/s3/models.py b/moto/s3/models.py
index 14bd40c5c..af52a9b79 100644
--- a/moto/s3/models.py
+++ b/moto/s3/models.py
@@ -2455,6 +2455,13 @@ class S3Backend(BaseBackend, CloudWatchMetricProvider):
             key_name=key.name,
             acl=multipart.acl,
         )
+
+        notifications.send_event(
+            self.account_id,
+            notifications.S3NotificationEvent.OBJECT_CREATED_COMPLETE_MULTIPART_UPLOAD_EVENT,
+            bucket,
+            bucket.keys.get(multipart.key_name),
+        )
         return key
 
     def get_all_multiparts(self, bucket_name: str) -> Dict[str, FakeMultipart]:


Test patch:
diff --git a/tests/test_s3/test_s3_eventbridge_integration.py b/tests/test_s3/test_s3_eventbridge_integration.py
index dda1f2c04..9ccb25cbb 100644
--- a/tests/test_s3/test_s3_eventbridge_integration.py
+++ b/tests/test_s3/test_s3_eventbridge_integration.py
@@ -9,10 +9,15 @@ import requests
 
 from moto import mock_aws, settings
 from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
+from moto.settings import S3_UPLOAD_PART_MIN_SIZE
 from tests.test_s3.test_s3_multipart import reduced_min_part_size
 
 REGION_NAME = "us-east-1"
-REDUCED_PART_SIZE = 256
+
+if settings.TEST_DECORATOR_MODE:
+    REDUCED_PART_SIZE = 256
+else:
+    REDUCED_PART_SIZE = S3_UPLOAD_PART_MIN_SIZE
 
 
 def _seteup_bucket_notification_eventbridge(
@@ -144,6 +149,56 @@ def test_copy_object_notification():
     assert event_message["detail"]["reason"] == "ObjectCreated"
 
 
+@mock_aws
+@reduced_min_part_size
+def test_complete_multipart_upload_notification():
+    resource_names = _seteup_bucket_notification_eventbridge()
+    bucket_name = resource_names["bucket_name"]
+    s3_client = boto3.client("s3", region_name=REGION_NAME)
+    object_key = "testkey"
+
+    part1 = b"0" * REDUCED_PART_SIZE
+    part2 = b"1"
+    multipart = s3_client.create_multipart_upload(Bucket=bucket_name, Key=object_key)
+    up1 = s3_client.upload_part(
+        Body=BytesIO(part1),
+        PartNumber=1,
+        Bucket=bucket_name,
+        Key=object_key,
+        UploadId=multipart["UploadId"],
+    )
+    up2 = s3_client.upload_part(
+        Body=BytesIO(part2),
+        PartNumber=2,
+        Bucket=bucket_name,
+        Key=object_key,
+        UploadId=multipart["UploadId"],
+    )
+
+    s3_client.complete_multipart_upload(
+        Bucket=bucket_name,
+        Key=object_key,
+        MultipartUpload={
+            "Parts": [
+                {"ETag": up1["ETag"], "PartNumber": 1},
+                {"ETag": up2["ETag"], "PartNumber": 2},
+            ]
+        },
+        UploadId=multipart["UploadId"],
+    )
+
+    events = _get_send_events()
+    assert len(events) == 2  # [PutObject event, CompleteMultipartUpload event]
+    event_message = json.loads(events[-1]["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"]["object"]["key"] == object_key
+    assert event_message["detail"]["reason"] == "ObjectCreated"
+
+
 @mock_aws
 def test_delete_object_notification():
     resource_names = _seteup_bucket_notification_eventbridge()