Repository: getmoto/moto
Pull request #7442 (2024-03-08 19:19:51)

CloudFormation: AWS::IAM::Role now supports RoleId
See #7381 

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/7442?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 [(`16b934a`)](https://app.codecov.io/gh/getmoto/moto/pull/7442?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).
> Report is 84 commits behind head on master.


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


```diff
@@            Coverage Diff             @@
##           master    #7442      +/-   ##
==========================================
- Coverage   95.88%   95.80%   -0.08%     
==========================================
  Files         843      849       +6     
  Lines       82578    83693    +1115     
==========================================
+ Hits        79178    80183    +1005     
- Misses       3400     3510     +110     
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/7442/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/7442/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/7442/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/7442?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/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/iam/models.py b/moto/iam/models.py
index 1231544cb..b998da051 100644
--- a/moto/iam/models.py
+++ b/moto/iam/models.py
@@ -838,13 +838,15 @@ class Role(CloudFormationModel):
 
     @classmethod
     def has_cfn_attr(cls, attr: str) -> bool:
-        return attr in ["Arn"]
+        return attr in ["Arn", "RoleId"]
 
     def get_cfn_attribute(self, attribute_name: str) -> str:
         from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
 
         if attribute_name == "Arn":
             return self.arn
+        if attribute_name == "RoleId":
+            return self.id
         raise UnformattedGetAttTemplateException()
 
     def get_tags(self) -> List[Dict[str, str]]:
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_iam/test_iam_cloudformation.py b/tests/test_iam/test_iam_cloudformation.py
index 2ac97ff82..678939ae1 100644
--- a/tests/test_iam/test_iam_cloudformation.py
+++ b/tests/test_iam/test_iam_cloudformation.py
@@ -27,6 +27,13 @@ Resources:
               - ec2.amazonaws.com
             Action:
               - 'sts:AssumeRole'
+Outputs:
+  RootRole:
+    Value: !Ref RootRole
+  RoleARN:
+    Value: {"Fn::GetAtt": ["RootRole", "Arn"]}
+  RoleID:
+    Value: {"Fn::GetAtt": ["RootRole", "RoleId"]}
 """
 
 
@@ -1417,8 +1424,16 @@ def test_iam_cloudformation_create_role():
     role = [res for res in resources if res["ResourceType"] == "AWS::IAM::Role"][0]
     assert role["LogicalResourceId"] == "RootRole"
 
+    outputs = cf_client.describe_stacks(StackName=stack_name)["Stacks"][0]["Outputs"]
+    outputs = {o["OutputKey"]: o["OutputValue"] for o in outputs}
+
     iam_client = boto3.client("iam", region_name="us-east-1")
-    assert len(iam_client.list_roles()["Roles"]) == 1
+    roles = iam_client.list_roles()["Roles"]
+    assert len(roles) == 1
+
+    assert roles[0]["RoleName"] == [v for k, v in outputs.items() if k == "RootRole"][0]
+    assert roles[0]["Arn"] == [v for k, v in outputs.items() if k == "RoleARN"][0]
+    assert roles[0]["RoleId"] == [v for k, v in outputs.items() if k == "RoleID"][0]
 
     cf_client.delete_stack(StackName=stack_name)
 
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()