Repository: getmoto/moto
Pull request #7441 (2024-03-08 13:06:43)

Docs: refer to client, not client2, in sample
Later examples have a `client1` and a `client2`. This one only has `client`.


Patch:
diff --git a/docs/docs/multi_account.rst b/docs/docs/multi_account.rst
index 18c06a3b2..31a873b3a 100644
--- a/docs/docs/multi_account.rst
+++ b/docs/docs/multi_account.rst
@@ -26,11 +26,11 @@ Here is an example of what this looks like in practice:
     os.environ["MOTO_ACCOUNT_ID"] = "111111111111"
     client.create_bucket(Bucket="bucket-in-account-2")
 
-    assert [b["Name"] for b in client2.list_buckets()["Buckets"]] == ["bucket-in-account-2"]
+    assert [b["Name"] for b in client.list_buckets()["Buckets"]] == ["bucket-in-account-2"]
 
     # Now revert to the default account, by removing the environment variable
     del os.environ["MOTO_ACCOUNT_ID"]
-    assert [b["Name"] for b in client2.list_buckets()["Buckets"]] == ["bucket-default-account"]
+    assert [b["Name"] for b in client.list_buckets()["Buckets"]] == ["bucket-default-account"]
 
 
 
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()