Repository: getmoto/moto
Pull request #8378 (2024-12-07 11:07:39)

bump cryptography version from 3.3.1 to 35.0.0
The current min. cryptography version (`3.3.1`) isn't sufficient for moto's IoT submodule - versions of cryptography between `3.3.1` and `3.4.8` (inclusive) didn't support importing from `cryptography.hazmat._oid`:

```
.pyenv/versions/3.11.6/lib/python3.11/site-packages/moto/iot/models.py:9: in <module>
    from cryptography.hazmat._oid import NameOID
E   ImportError: cannot import name 'NameOID' from 'cryptography.hazmat._oid' (/home/vlas-sokolov/.pyenv/versions/3.11.6/lib/python3.11/site-packages/cryptography/hazmat/_oid.py)
```

This can be easily verified by installing any cryptography version prior to `35.0.0` and running moto's test suit from `tests/test_iot` - they will all fail. The import structure moto relies on was introduced in this [commit](https://github.com/pyca/cryptography/commit/7b5634911c892fbc64b363523b8af74a4b772f7b), which first appears in tag `35.0.0`, which is the version this PR bumps the dependent package to.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/8378?dropdown=coverage&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 94.59%. Comparing base [(`3b468c9`)](https://app.codecov.io/gh/getmoto/moto/commit/3b468c9d50280f3f5c18a356516d36c1eaa13e56?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) to head [(`5cc8325`)](https://app.codecov.io/gh/getmoto/moto/commit/5cc8325a978512c11b5d973dde72d7372cfe29e0?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).
> Report is 3 commits behind head on master.

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


```diff
@@           Coverage Diff           @@
##           master    #8378   +/-   ##
=======================================
  Coverage   94.59%   94.59%           
=======================================
  Files        1159     1159           
  Lines      101296   101320   +24     
=======================================
+ Hits        95816    95840   +24     
  Misses       5480     5480           
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/8378/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/8378/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `28.87% <ø> (+0.01%)` | :arrow_up: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/8378/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `94.56% <ø> (+<0.01%)` | :arrow_up: |

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/8378?dropdown=coverage&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/services/cf.rst b/docs/docs/services/cf.rst
index 275f1d4cc..ada1cdd26 100644
--- a/docs/docs/services/cf.rst
+++ b/docs/docs/services/cf.rst
@@ -161,16 +161,20 @@ Please let us know if you'd like support for a resource not yet listed here.
   +---------------------------------------+--------+--------+--------+----------------------------------------+
   |AWS::IAM::Policy                       |        |        |        | - [ ] Id                               |
   +---------------------------------------+--------+--------+--------+----------------------------------------+
-  |AWS::IAM::Role                         |    x   |        |    x   | - [x] Arn                              |
+  |AWS::IAM::Role                         |    x   |    x   |    x   | - [x] Arn                              |
   +---------------------------------------+--------+--------+--------+ - [x] RoleId                           |
   |                                       |        |        |        |                                        |
   +---------------------------------------+--------+--------+--------+----------------------------------------+
   |AWS::IAM::User                         |    x   |    x   |    x   | - [x] Arn                              |
   +---------------------------------------+--------+--------+--------+----------------------------------------+
+  |AWS::IoT::JobTemplate                  |    x   |    x   |    x   | - [x] Arn                              |
+  +---------------------------------------+--------+--------+--------+----------------------------------------+
   |AWS::IoT::Policy                       |    x   |    x   |    x   | - [x] Id                               |
   +---------------------------------------+--------+--------+--------+ - [x] Arn                              |
   |                                       |        |        |        |                                        |
   +---------------------------------------+--------+--------+--------+----------------------------------------+
+  |AWS::IoT::RoleAlias                    |    x   |    x   |    x   | - [x] RoleAliasArn                     |
+  +---------------------------------------+--------+--------+--------+----------------------------------------+
   |AWS::IoT::Thing                        |    x   |    x   |    x   | - [x] Id                               |
   +---------------------------------------+--------+--------+--------+ - [x] Arn                              |
   |                                       |        |        |        |                                        |
diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py
index f39f00c70..59020c64e 100644
--- a/moto/awslambda/models.py
+++ b/moto/awslambda/models.py
@@ -651,16 +651,15 @@ class LambdaFunction(CloudFormationModel, DockerModel):
 
         self.logs_group_name = f"/aws/lambda/{self.function_name}"
 
-        # this isn't finished yet. it needs to find out the VpcId value
-        self._vpc_config = spec.get(
-            "VpcConfig", {"SubnetIds": [], "SecurityGroupIds": []}
-        )
+        # VpcConfig has no default, if not supplied
+        self._vpc_config = spec.get("VpcConfig")
 
         # auto-generated
         self.version = version
         self.last_modified = iso_8601_datetime_with_nanoseconds()
 
         self._set_function_code(self.code)
+        self.code_or_config_updated = False
 
         self.function_arn: str = make_function_arn(
             self.region, self.account_id, self.function_name
@@ -718,7 +717,9 @@ class LambdaFunction(CloudFormationModel, DockerModel):
         self._ephemeral_storage = ephemeral_storage
 
     @property
-    def vpc_config(self) -> Dict[str, Any]:  # type: ignore[misc]
+    def vpc_config(self) -> Optional[Dict[str, Any]]:  # type: ignore[misc]
+        if not self._vpc_config:
+            return None
         config = self._vpc_config.copy()
         if config["SecurityGroupIds"]:
             config.update({"VpcId": "vpc-123abc"})
@@ -794,7 +795,7 @@ class LambdaFunction(CloudFormationModel, DockerModel):
         resp = {"Configuration": self.get_configuration()}
         if "S3Key" in self.code:
             resp["Code"] = {
-                "Location": f"s3://awslambda-{self.region}-tasks.s3-{self.region}.amazonaws.com/{self.code['S3Key']}",
+                "Location": f"https://awslambda-{self.region}-tasks.s3.{self.region}.amazonaws.com/{self.code['S3Key']}",
                 "RepositoryType": "S3",
             }
         elif "ImageUri" in self.code:
@@ -838,6 +839,8 @@ class LambdaFunction(CloudFormationModel, DockerModel):
             elif key == "Layers":
                 self.layers = self._get_layers_data(value)
 
+        self.code_or_config_updated = True
+
         return self.get_configuration()
 
     def _set_function_code(self, updated_spec: Dict[str, Any]) -> None:
@@ -935,6 +938,7 @@ class LambdaFunction(CloudFormationModel, DockerModel):
 
     def update_function_code(self, updated_spec: Dict[str, Any]) -> Dict[str, Any]:
         self._set_function_code(updated_spec)
+        self.code_or_config_updated = True
         return self.get_configuration()
 
     @staticmethod
@@ -1688,9 +1692,11 @@ class LambdaStorage(object):
         all_versions = self._functions[name]["versions"]
         if all_versions:
             latest_published = all_versions[-1]
-            if latest_published.code_sha_256 == function.code_sha_256:
+            if not function.code_or_config_updated:
                 # Nothing has changed, don't publish
                 return latest_published
+            else:
+                function.code_or_config_updated = False
 
         new_version = len(all_versions) + 1
         fn = copy.copy(self._functions[name]["latest"])
diff --git a/moto/ds/models.py b/moto/ds/models.py
index b55e95932..c9ce45e11 100644
--- a/moto/ds/models.py
+++ b/moto/ds/models.py
@@ -15,7 +15,7 @@ from moto.ds.exceptions import (
     UnsupportedOperationException,
     ValidationException,
 )
-from moto.ds.utils import PAGINATION_MODEL
+from moto.ds.utils import PAGINATION_MODEL, SETTINGS_ENTRIES_MODEL
 from moto.ds.validations import validate_args
 from moto.ec2 import ec2_backends
 from moto.ec2.exceptions import InvalidSubnetIdError
@@ -138,9 +138,13 @@ class Directory(BaseModel):  # pylint: disable=too-many-instance-attributes
         self.stage = "Active"
         self.launch_time = unix_time()
         self.stage_last_updated_date_time = unix_time()
-        # add type annotation to line below
         self.ldaps_settings_info: List[LdapsSettingInfo] = []
         self.trusts: List[Trust] = []
+        self.settings = (
+            SETTINGS_ENTRIES_MODEL.copy()
+            if self.directory_type == "MicrosoftAD"
+            else []
+        )
 
         if self.directory_type == "ADConnector":
             self.security_group_id = self.create_security_group(
@@ -641,16 +645,23 @@ class DirectoryServiceBackend(BaseBackend):
 
     @paginate(pagination_model=PAGINATION_MODEL)
     def describe_trusts(
-        self, directory_id: str, trust_ids: Optional[List[str]]
+        self, directory_id: Optional[str], trust_ids: Optional[List[str]]
     ) -> List[Trust]:
-        self._validate_directory_id(directory_id)
-        directory = self.directories[directory_id]
-        trusts = directory.trusts
+        if directory_id:
+            self._validate_directory_id(directory_id)
+            directory = self.directories[directory_id]
+            trusts = directory.trusts
+        else:
+            trusts = [
+                trust
+                for directory in self.directories.values()
+                for trust in directory.trusts
+            ]
         if trust_ids:
-            trusts = [t for t in directory.trusts if t.trust_id in trust_ids]
+            queried_trusts = [t for t in trusts if t.trust_id in trust_ids]
         else:
-            trusts = directory.trusts
-        return trusts
+            queried_trusts = trusts
+        return queried_trusts
 
     def delete_trust(
         self, trust_id: str, delete_associated_conditional_forwarder: Optional[bool]
@@ -692,5 +703,41 @@ class DirectoryServiceBackend(BaseBackend):
         directory = self.directories[directory_id]
         directory.enable_ldaps(False)
 
+    @paginate(pagination_model=PAGINATION_MODEL)
+    def describe_settings(
+        self, directory_id: str, status: Optional[str]
+    ) -> List[Dict[str, str]]:
+        """Describe settings for a Directory"""
+        self._validate_directory_id(directory_id)
+        directory = self.directories[directory_id]
+        if directory.directory_type not in ("MicrosoftAD"):
+            raise InvalidParameterException(
+                "This operation is only supported for Microsoft AD"
+            )
+        if status:
+            queried_settings = [
+                setting
+                for setting in directory.settings
+                if setting["RequestStatus"] == status
+            ]
+        else:
+            queried_settings = directory.settings
+        return queried_settings
+
+    def update_settings(self, directory_id: str, settings: List[Dict[str, Any]]) -> str:
+        self._validate_directory_id(directory_id)
+        directory = self.directories[directory_id]
+        if directory.directory_type not in ("MicrosoftAD"):
+            raise InvalidParameterException(
+                "This operation is only supported for Microsoft AD"
+            )
+        for s in settings:
+            for setting in directory.settings:
+                if setting["Name"] == s["Name"]:
+                    # TODO: Add validation for the value
+                    setting["AppliedValue"] = s["Value"]
+
+        return directory_id
+
 
 ds_backends = BackendDict(DirectoryServiceBackend, service_name="ds")
diff --git a/moto/ds/responses.py b/moto/ds/responses.py
index 2a6068e8e..5575ff14e 100644
--- a/moto/ds/responses.py
+++ b/moto/ds/responses.py
@@ -241,3 +241,30 @@ class DirectoryServiceResponse(BaseResponse):
             type=type,
         )
         return ""
+
+    def describe_settings(self) -> str:
+        directory_id = self._get_param("DirectoryId")
+        status = self._get_param("Status")
+        next_token = self._get_param("NextToken")
+        setting_entries, next_token = self.ds_backend.describe_settings(
+            directory_id=directory_id,
+            status=status,
+            next_token=next_token,
+        )
+
+        return json.dumps(
+            dict(
+                DirectoryId=directory_id,
+                SettingEntries=setting_entries,
+                NextToken=next_token,
+            )
+        )
+
+    def update_settings(self) -> str:
+        directory_id = self._get_param("DirectoryId")
+        settings = self._get_param("Settings")
+        directory_id = self.ds_backend.update_settings(
+            directory_id=directory_id,
+            settings=settings,
+        )
+        return json.dumps(dict(DirectoryId=directory_id))
diff --git a/moto/ds/utils.py b/moto/ds/utils.py
index a098c14cf..04e3ff922 100644
--- a/moto/ds/utils.py
+++ b/moto/ds/utils.py
@@ -24,4 +24,151 @@ PAGINATION_MODEL = {
         "limit_default": 100,
         "unique_attribute": "trust_id",
     },
+    "describe_settings": {
+        "input_token": "next_token",
+        "limit_key": "limit",
+        "limit_default": 100,
+        "unique_attribute": "Name",
+    },
 }
+
+# List of directory security settings
+# https://docs.aws.amazon.com/directoryservice/latest/admin-guide/ms_ad_directory_settings.html#list-ds-settings
+SETTINGS_ENTRIES_MODEL = [
+    {
+        "Type": "Protocol",
+        "Name": "PCT_1_0",
+        "AllowedValues": '["Enable","Disable"]',
+        "AppliedValue": "Enable",
+        "RequestStatus": "Default",
+        "DataType": "Boolean",
+    },
+    {
+        "Type": "Protocol",
+        "Name": "SSL_2_0",
+        "AllowedValues": '["Enable","Disable"]',
+        "AppliedValue": "Enable",
+        "RequestStatus": "Default",
+        "DataType": "Boolean",
+    },
+    {
+        "Type": "Protocol",
+        "Name": "SSL_3_0",
+        "AllowedValues": '["Enable","Disable"]',
+        "AppliedValue": "Enable",
+        "RequestStatus": "Default",
+        "DataType": "Boolean",
+    },
+    {
+        "Type": "Protocol",
+        "Name": "TLS_1_0",
+        "AllowedValues": '["Enable","Disable"]',
+        "AppliedValue": "Enable",
+        "RequestStatus": "Default",
+        "DataType": "Boolean",
+    },
+    {
+        "Type": "Protocol",
+        "Name": "TLS_1_1",
+        "AllowedValues": '["Enable","Disable"]',
+        "AppliedValue": "Enable",
+        "RequestStatus": "Default",
+        "DataType": "Boolean",
+    },
+    {
+        "Type": "Cipher",
+        "Name": "AES_128_128",
+        "AllowedValues": '["Enable","Disable"]',
+        "AppliedValue": "Enable",
+        "RequestStatus": "Default",
+        "DataType": "Boolean",
+    },
+    {
+        "Type": "Cipher",
+        "Name": "DES_56_56",
+        "AllowedValues": '["Enable","Disable"]',
+        "AppliedValue": "Enable",
+        "RequestStatus": "Default",
+        "DataType": "Boolean",
+    },
+    {
+        "Type": "Cipher",
+        "Name": "RC2_40_128",
+        "AllowedValues": '["Enable","Disable"]',
+        "AppliedValue": "Enable",
+        "RequestStatus": "Default",
+        "DataType": "Boolean",
+    },
+    {
+        "Type": "Cipher",
+        "Name": "RC2_56_128",
+        "AllowedValues": '["Enable","Disable"]',
+        "AppliedValue": "Enable",
+        "RequestStatus": "Default",
+        "DataType": "Boolean",
+    },
+    {
+        "Type": "Cipher",
+        "Name": "RC2_128_128",
+        "AllowedValues": '["Enable","Disable"]',
+        "AppliedValue": "Enable",
+        "RequestStatus": "Default",
+        "DataType": "Boolean",
+    },
+    {
+        "Type": "Cipher",
+        "Name": "RC4_40_128",
+        "AllowedValues": '["Enable","Disable"]',
+        "AppliedValue": "Enable",
+        "RequestStatus": "Default",
+        "DataType": "Boolean",
+    },
+    {
+        "Type": "Cipher",
+        "Name": "RC4_56_128",
+        "AllowedValues": '["Enable","Disable"]',
+        "AppliedValue": "Enable",
+        "RequestStatus": "Default",
+        "DataType": "Boolean",
+    },
+    {
+        "Type": "Cipher",
+        "Name": "RC4_64_128",
+        "AllowedValues": '["Enable","Disable"]',
+        "AppliedValue": "Enable",
+        "RequestStatus": "Default",
+        "DataType": "Boolean",
+    },
+    {
+        "Type": "Cipher",
+        "Name": "RC4_128_128",
+        "AllowedValues": '["Enable","Disable"]',
+        "AppliedValue": "Enable",
+        "RequestStatus": "Default",
+        "DataType": "Boolean",
+    },
+    {
+        "Type": "Cipher",
+        "Name": "3DES_168_168",
+        "AllowedValues": '["Enable","Disable"]',
+        "AppliedValue": "Enable",
+        "RequestStatus": "Default",
+        "DataType": "Boolean",
+    },
+    {
+        "Type": "Certificate-Based Authentication",
+        "Name": "CERTIFICATE_BACKDATING_COMPENSATION",
+        "AllowedValues": '{"MinimumValue": 1, "MaximumValue": 1577000000}',
+        "AppliedValue": "600",
+        "RequestStatus": "Default",
+        "DataType": "DurationInSeconds",
+    },
+    {
+        "Type": "Certificate-Based Authentication",
+        "Name": "CERTIFICATE_STRONG_ENFORCEMENT",
+        "AllowedValues": '["Compatibility","Full_Enforcement"]',
+        "AppliedValue": "Compatibility",
+        "RequestStatus": "Default",
+        "DataType": "Enum",
+    },
+]
diff --git a/moto/efs/models.py b/moto/efs/models.py
index 9094f2eda..44cbb764c 100644
--- a/moto/efs/models.py
+++ b/moto/efs/models.py
@@ -758,5 +758,17 @@ class EFSBackend(BaseBackend):
     def untag_resource(self, resource_id: str, tag_keys: List[str]) -> None:
         self.tagging_service.untag_resource_using_names(resource_id, tag_keys)
 
+    def describe_file_system_policy(self, file_system_id: str) -> str:
+        policy = self.file_systems_by_id[file_system_id].file_system_policy
+        if not policy:
+            raise PolicyNotFound("None")
+        return policy
+
+    def put_file_system_policy(
+        self, file_system_id: str, policy: str, bypass_policy_lockout_safety_check: bool
+    ) -> None:
+        file_system = self.file_systems_by_id[file_system_id]
+        file_system.file_system_policy = policy
+
 
 efs_backends = BackendDict(EFSBackend, "efs")
diff --git a/moto/efs/responses.py b/moto/efs/responses.py
index 97fa89378..971d7aa82 100644
--- a/moto/efs/responses.py
+++ b/moto/efs/responses.py
@@ -197,3 +197,29 @@ class EFSResponse(BaseResponse):
         tag_keys = self.querystring.get("tagKeys", [])
         self.efs_backend.untag_resource(resource_id, tag_keys)
         return "{}", {"Content-Type": "application/json"}
+
+    def describe_file_system_policy(self) -> TYPE_RESPONSE:
+        file_system_id = self._get_param("FileSystemId")
+        policy = self.efs_backend.describe_file_system_policy(
+            file_system_id=file_system_id,
+        )
+        return (
+            json.dumps(dict(FileSystemId=file_system_id, Policy=policy)),
+            {"Content-Type": "application/json"},
+        )
+
+    def put_file_system_policy(self) -> TYPE_RESPONSE:
+        file_system_id = self._get_param("FileSystemId")
+        policy = self._get_param("Policy")
+        bypass_policy_lockout_safety_check = (
+            self._get_param("BypassPolicyLockoutSafetyCheck") or False
+        )
+        self.efs_backend.put_file_system_policy(
+            file_system_id=file_system_id,
+            policy=policy,
+            bypass_policy_lockout_safety_check=bypass_policy_lockout_safety_check,
+        )
+        return (
+            json.dumps(dict(FileSystemId=file_system_id, Policy=policy)),
+            {"Content-Type": "application/json"},
+        )
diff --git a/moto/efs/urls.py b/moto/efs/urls.py
index 7cc53f31b..e7256a02b 100644
--- a/moto/efs/urls.py
+++ b/moto/efs/urls.py
@@ -17,6 +17,7 @@ url_paths = {
     "/2015-02-01/file-systems/<file_system_id>": response.dispatch,
     "/2015-02-01/file-systems/<file_system_id>/backup-policy": response.dispatch,
     "/2015-02-01/file-systems/<file_system_id>/lifecycle-configuration": response.dispatch,
+    "/2015-02-01/file-systems/<file_system_id>/policy": response.dispatch,
     "/2015-02-01/mount-targets": response.dispatch,
     "/2015-02-01/mount-targets/<mount_target_id>": response.dispatch,
     "/2015-02-01/mount-targets/<mount_target_id>/security-groups": response.dispatch,
diff --git a/moto/iam/models.py b/moto/iam/models.py
index aece6258f..868e30797 100644
--- a/moto/iam/models.py
+++ b/moto/iam/models.py
@@ -776,6 +776,24 @@ class Role(CloudFormationModel):
 
         return role
 
+    @classmethod
+    def update_from_cloudformation_json(  # type: ignore[misc]
+        cls,
+        original_resource: "Role",
+        new_resource_name: str,
+        cloudformation_json: Dict[str, Any],
+        account_id: str,
+        region_name: str,
+    ) -> "Role":
+        iam_backend = iam_backends[account_id][get_partition(region_name)]
+        properties = cloudformation_json["Properties"]
+        role_name = properties.get("RoleName", original_resource.name)
+        role_description = properties.get("Description", "")
+        max_session_duration = properties.get("RoleDescription", 3600)
+        return iam_backend.update_role(
+            role_name, role_description, max_session_duration
+        )
+
     @classmethod
     def delete_from_cloudformation_json(  # type: ignore[misc]
         cls,
diff --git a/setup.cfg b/setup.cfg
index d30a2c193..6ee4835fe 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -29,7 +29,7 @@ python_requires = >=3.8
 install_requires =
     boto3>=1.9.201
     botocore>=1.14.0,!=1.35.45,!=1.35.46
-    cryptography>=3.3.1
+    cryptography>=35.0.0
     requests>=2.5
     xmltodict
     werkzeug>=0.5,!=2.2.0,!=2.2.1


Test patch:
diff --git a/tests/test_awslambda/__init__.py b/tests/test_awslambda/__init__.py
index 5e856146e..5dfd89f8a 100644
--- a/tests/test_awslambda/__init__.py
+++ b/tests/test_awslambda/__init__.py
@@ -25,7 +25,7 @@ def lambda_aws_verified(func):
     """
 
     @wraps(func)
-    def pagination_wrapper():
+    def pagination_wrapper(**kwargs):
         role_name = "moto_test_role_" + str(uuid4())[0:6]
 
         allow_aws_request = (
@@ -33,12 +33,12 @@ def lambda_aws_verified(func):
         )
 
         if allow_aws_request:
-            return create_role_and_test(role_name)
+            return create_role_and_test(kwargs, role_name)
         else:
             with mock_aws():
-                return create_role_and_test(role_name)
+                return create_role_and_test(kwargs, role_name)
 
-    def create_role_and_test(role_name):
+    def create_role_and_test(kwargs, role_name):
         from .test_lambda import _lambda_region
 
         policy_doc = {
@@ -63,7 +63,8 @@ def lambda_aws_verified(func):
             # Role is not immediately available
             check_iam_role_is_ready_to_use(iam_role_arn, role_name, _lambda_region)
 
-            resp = func(iam_role_arn)
+            kwargs["iam_role_arn"] = iam_role_arn
+            resp = func(**kwargs)
         finally:
             iam.delete_role(RoleName=role_name)
 
diff --git a/tests/test_awslambda/test_lambda.py b/tests/test_awslambda/test_lambda.py
index e22c99584..8c60ae1c3 100644
--- a/tests/test_awslambda/test_lambda.py
+++ b/tests/test_awslambda/test_lambda.py
@@ -15,6 +15,7 @@ from moto import mock_aws, settings
 from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
 from moto.utilities.distutils_version import LooseVersion
 from tests.test_ecr.test_ecr_helpers import _create_image_manifest
+from tests.test_s3 import s3_aws_verified
 
 from . import lambda_aws_verified
 from .utilities import (
@@ -23,7 +24,6 @@ from .utilities import (
     get_role_name,
     get_test_zip_file1,
     get_test_zip_file2,
-    get_test_zip_file3,
 )
 
 PYTHON_VERSION = "python3.11"
@@ -245,7 +245,6 @@ def test_create_function_from_zipfile():
             "utf-8"
         ),
         "Version": "1",
-        "VpcConfig": {"SecurityGroupIds": [], "SubnetIds": []},
         "ResponseMetadata": {"HTTPStatusCode": 201},
         "State": "Active",
         "Layers": [],
@@ -582,25 +581,26 @@ def test_create_function_from_mocked_ecr_missing_image(
     )
 
 
-@mock_aws
-@freeze_time("2015-01-01 00:00:00")
-def test_get_function():
-    bucket_name = str(uuid4())
-    s3_conn = boto3.client("s3", _lambda_region)
-    s3_conn.create_bucket(
-        Bucket=bucket_name,
-        CreateBucketConfiguration={"LocationConstraint": _lambda_region},
-    )
+@pytest.mark.aws_verified
+@lambda_aws_verified
+@s3_aws_verified
+def test_get_function(iam_role_arn=None, bucket_name=None):
+    region = "us-east-1"
+
+    sts = boto3.client("sts", region)
+    account_id = sts.get_caller_identity()["Account"]
+
+    s3_conn = boto3.client("s3", region)
 
     zip_content = get_test_zip_file1()
     s3_conn.put_object(Bucket=bucket_name, Key="test.zip", Body=zip_content)
-    conn = boto3.client("lambda", _lambda_region)
+    conn = boto3.client("lambda", region)
     function_name = str(uuid4())[0:6]
 
     conn.create_function(
         FunctionName=function_name,
         Runtime=PYTHON_VERSION,
-        Role=get_role_name(),
+        Role=iam_role_arn,
         Handler="lambda_function.lambda_handler",
         Code={"S3Bucket": bucket_name, "S3Key": "test.zip"},
         Description="test lambda function",
@@ -611,16 +611,7 @@ def test_get_function():
     )
 
     result = conn.get_function(FunctionName=function_name)
-    # this is hard to match against, so remove it
-    result["ResponseMetadata"].pop("HTTPHeaders", None)
-    # Botocore inserts retry attempts not seen in Python27
-    result["ResponseMetadata"].pop("RetryAttempts", None)
-    result["Configuration"].pop("LastModified")
 
-    assert (
-        result["Code"]["Location"]
-        == f"s3://awslambda-{_lambda_region}-tasks.s3-{_lambda_region}.amazonaws.com/test.zip"
-    )
     assert result["Code"]["RepositoryType"] == "S3"
 
     assert result["Configuration"]["CodeSha256"] == base64.b64encode(
@@ -632,13 +623,11 @@ def test_get_function():
     assert result["Configuration"]["FunctionName"] == function_name
     assert result["Configuration"]["Handler"] == "lambda_function.lambda_handler"
     assert result["Configuration"]["MemorySize"] == 128
-    assert result["Configuration"]["Role"] == get_role_name()
+    assert result["Configuration"]["Role"] == iam_role_arn
     assert result["Configuration"]["Runtime"] == PYTHON_VERSION
     assert result["Configuration"]["Timeout"] == 3
     assert result["Configuration"]["Version"] == "$LATEST"
-    assert "VpcConfig" in result["Configuration"]
-    assert "Environment" in result["Configuration"]
-    assert "Variables" in result["Configuration"]["Environment"]
+    assert "VpcConfig" not in result["Configuration"]
     assert result["Configuration"]["Environment"]["Variables"] == {
         "test_variable": "test_value"
     }
@@ -648,7 +637,7 @@ def test_get_function():
     assert result["Configuration"]["Version"] == "$LATEST"
     assert (
         result["Configuration"]["FunctionArn"]
-        == f"arn:aws:lambda:us-west-2:{ACCOUNT_ID}:function:{function_name}:$LATEST"
+        == f"arn:aws:lambda:{region}:{account_id}:function:{function_name}:$LATEST"
     )
 
     # Test get function with version
@@ -656,7 +645,7 @@ def test_get_function():
     assert result["Configuration"]["Version"] == "1"
     assert (
         result["Configuration"]["FunctionArn"]
-        == f"arn:aws:lambda:us-west-2:{ACCOUNT_ID}:function:{function_name}:1"
+        == f"arn:aws:lambda:{region}:{account_id}:function:{function_name}:1"
     )
 
     # Test get function with version inside of name
@@ -664,9 +653,16 @@ def test_get_function():
     assert result["Configuration"]["Version"] == "1"
     assert (
         result["Configuration"]["FunctionArn"]
-        == f"arn:aws:lambda:us-west-2:{ACCOUNT_ID}:function:{function_name}:1"
+        == f"arn:aws:lambda:{region}:{account_id}:function:{function_name}:1"
     )
 
+    conn.delete_function(FunctionName=function_name)
+
+
+@mock_aws
+def test_get_unknown_function():
+    conn = boto3.client("lambda", _lambda_region)
+
     # Test get function when can't find function name
     with pytest.raises(conn.exceptions.ResourceNotFoundException):
         conn.get_function(FunctionName="junk", Qualifier="$LATEST")
@@ -717,9 +713,6 @@ def test_get_function_configuration(key):
     assert result["Runtime"] == PYTHON_VERSION
     assert result["Timeout"] == 3
     assert result["Version"] == "$LATEST"
-    assert "VpcConfig" in result
-    assert "Environment" in result
-    assert "Variables" in result["Environment"]
     assert result["Environment"]["Variables"] == {"test_variable": "test_value"}
 
     # Test get function with qualifier
@@ -1002,7 +995,7 @@ def test_list_create_list_get_delete_list():
     )
     expected_function_result = {
         "Code": {
-            "Location": f"s3://awslambda-{_lambda_region}-tasks.s3-{_lambda_region}.amazonaws.com/test.zip",
+            "Location": f"https://awslambda-{_lambda_region}-tasks.s3.{_lambda_region}.amazonaws.com/test.zip",
             "RepositoryType": "S3",
         },
         "Configuration": {
@@ -1019,7 +1012,6 @@ def test_list_create_list_get_delete_list():
             "Runtime": PYTHON_VERSION,
             "Timeout": 3,
             "Version": "$LATEST",
-            "VpcConfig": {"SecurityGroupIds": [], "SubnetIds": []},
             "State": "Active",
             "Layers": [],
             "LastUpdateStatus": "Successful",
@@ -1105,7 +1097,7 @@ def test_get_function_created_with_zipfile():
     assert len(response["Code"]) == 2
     assert response["Code"]["RepositoryType"] == "S3"
     assert response["Code"]["Location"].startswith(
-        f"s3://awslambda-{_lambda_region}-tasks.s3-{_lambda_region}.amazonaws.com"
+        f"https://awslambda-{_lambda_region}-tasks.s3.{_lambda_region}.amazonaws.com"
     )
     assert "Configuration" in response
     config = response["Configuration"]
@@ -1344,25 +1336,23 @@ def test_list_versions_by_function_for_nonexistent_function():
     assert len(versions["Versions"]) == 0
 
 
+@pytest.mark.aws_verified
 @pytest.mark.parametrize("key", ["FunctionName", "FunctionArn"])
-@mock_aws
-def test_update_configuration(key):
-    bucket_name = str(uuid4())
+@lambda_aws_verified
+@s3_aws_verified
+def test_update_configuration(key, bucket_name=None, iam_role_arn=None):
+    region = "us-east-1"
     function_name = str(uuid4())[0:6]
-    s3_conn = boto3.client("s3", _lambda_region)
-    s3_conn.create_bucket(
-        Bucket=bucket_name,
-        CreateBucketConfiguration={"LocationConstraint": _lambda_region},
-    )
+    s3_conn = boto3.client("s3", region)
 
     zip_content = get_test_zip_file2()
     s3_conn.put_object(Bucket=bucket_name, Key="test.zip", Body=zip_content)
-    conn = boto3.client("lambda", _lambda_region)
+    conn = boto3.client("lambda", region)
 
     fxn = conn.create_function(
         FunctionName=function_name,
         Runtime=PYTHON_VERSION,
-        Role=get_role_name(),
+        Role=iam_role_arn,
         Handler="lambda_function.lambda_handler",
         Code={"S3Bucket": bucket_name, "S3Key": "test.zip"},
         Description="test lambda function",
@@ -1373,6 +1363,9 @@ def test_update_configuration(key):
     )
     name_or_arn = fxn[key]
 
+    wait_for_func = conn.get_waiter("function_active")
+    wait_for_func.wait(FunctionName=function_name, WaiterConfig={"Delay": 1})
+
     assert fxn["Description"] == "test lambda function"
     assert fxn["Handler"] == "lambda_function.lambda_handler"
     assert fxn["MemorySize"] == 128
@@ -1383,31 +1376,37 @@ def test_update_configuration(key):
         FunctionName=name_or_arn,
         Description="updated test lambda function",
         Handler="lambda_function.new_lambda_handler",
-        Runtime="python3.6",
+        Runtime="python3.12",
         Timeout=7,
-        VpcConfig={"SecurityGroupIds": ["sg-123abc"], "SubnetIds": ["subnet-123abc"]},
         Environment={"Variables": {"test_environment": "test_value"}},
     )
 
+    wait_for_func = conn.get_waiter("function_updated")
+    wait_for_func.wait(FunctionName=function_name, WaiterConfig={"Delay": 1})
+
     assert updated_config["ResponseMetadata"]["HTTPStatusCode"] == 200
     assert updated_config["Description"] == "updated test lambda function"
     assert updated_config["Handler"] == "lambda_function.new_lambda_handler"
     assert updated_config["MemorySize"] == 128
-    assert updated_config["Runtime"] == "python3.6"
+    assert updated_config["Runtime"] == "python3.12"
     assert updated_config["Timeout"] == 7
     assert updated_config["Environment"]["Variables"] == {
         "test_environment": "test_value"
     }
-    assert updated_config["VpcConfig"] == {
-        "SecurityGroupIds": ["sg-123abc"],
-        "SubnetIds": ["subnet-123abc"],
-        "VpcId": "vpc-123abc",
-    }
 
+    # publish - results in a new version because the config is updated
+    assert conn.publish_version(FunctionName=name_or_arn)["Version"] == "2"
 
+    # publish again - no changes
+    assert conn.publish_version(FunctionName=name_or_arn)["Version"] == "2"
+
+    conn.delete_function(FunctionName=function_name)
+
+
+@pytest.mark.aws_verified
 @pytest.mark.parametrize("key", ["FunctionName", "FunctionArn"])
-@mock_aws
-def test_update_function_zip(key):
+@lambda_aws_verified
+def test_update_function_zip(key, iam_role_arn=None):
     conn = boto3.client("lambda", _lambda_region)
 
     zip_content_one = get_test_zip_file1()
@@ -1416,7 +1415,7 @@ def test_update_function_zip(key):
     fxn = conn.create_function(
         FunctionName=function_name,
         Runtime=PYTHON_VERSION,
-        Role=get_role_name(),
+        Role=iam_role_arn,
         Handler="lambda_function.lambda_handler",
         Code={"ZipFile": zip_content_one},
         Description="test lambda function",
@@ -1425,6 +1424,10 @@ def test_update_function_zip(key):
         Publish=True,
     )
     name_or_arn = fxn[key]
+
+    wait_for_func = conn.get_waiter("function_active")
+    wait_for_func.wait(FunctionName=function_name, WaiterConfig={"Delay": 1})
+
     first_sha = fxn["CodeSha256"]
 
     zip_content_two = get_test_zip_file2()
@@ -1434,49 +1437,61 @@ def test_update_function_zip(key):
     )
     assert update1["CodeSha256"] != first_sha
 
+    wait_for_func = conn.get_waiter("function_updated")
+    wait_for_func.wait(FunctionName=function_name, WaiterConfig={"Delay": 1})
+
     response = conn.get_function(FunctionName=function_name, Qualifier="2")
 
-    assert response["ResponseMetadata"]["HTTPStatusCode"] == 200
-    assert len(response["Code"]) == 2
     assert response["Code"]["RepositoryType"] == "S3"
     assert response["Code"]["Location"].startswith(
-        f"s3://awslambda-{_lambda_region}-tasks.s3-{_lambda_region}.amazonaws.com"
+        f"https://awslambda-{_lambda_region}-tasks.s3.{_lambda_region}.amazonaws.com"
     )
 
     config = response["Configuration"]
     assert config["CodeSize"] == len(zip_content_two)
-    assert config["Description"] == "test lambda function"
-    assert (
-        config["FunctionArn"]
-        == f"arn:aws:lambda:{_lambda_region}:{ACCOUNT_ID}:function:{function_name}:2"
-    )
+    assert config["FunctionArn"].endswith(f"{function_name}:2")
     assert config["FunctionName"] == function_name
     assert config["Version"] == "2"
-    assert config["LastUpdateStatus"] == "Successful"
     assert config["CodeSha256"] == update1["CodeSha256"]
 
     most_recent_config = conn.get_function(FunctionName=function_name)
     assert most_recent_config["Configuration"]["CodeSha256"] == update1["CodeSha256"]
 
-    # Publishing this again, with the same code, gives us the same version
+    # Update the same code, and manually publish
+    conn.update_function_code(FunctionName=name_or_arn, ZipFile=zip_content_two)
+    wait_for_func = conn.get_waiter("function_updated")
+    wait_for_func.wait(FunctionName=function_name, WaiterConfig={"Delay": 1})
+
+    resp = conn.publish_version(FunctionName=name_or_arn)
+    # The code is the same, but the version is still updated
+    assert resp["CodeSha256"] == update1["CodeSha256"]
+    assert resp["Version"] == "3"
+
+    # Calling publish again
+    resp = conn.publish_version(FunctionName=name_or_arn)
+    # Code is the same, as is the version - just because we didn't call 'update_function_code'
+    assert resp["CodeSha256"] == update1["CodeSha256"]
+    assert resp["Version"] == "3"
+
+    # Explicitly publishing this again, with the same code, gives us a new version
     same_update = conn.update_function_code(
         FunctionName=name_or_arn, ZipFile=zip_content_two, Publish=True
     )
-    assert (
-        same_update["FunctionArn"]
-        == most_recent_config["Configuration"]["FunctionArn"] + ":2"
-    )
-    assert same_update["Version"] == "2"
+    assert same_update["FunctionArn"].endswith(f":{function_name}:4")
+    assert same_update["Version"] == "4"
 
-    # Only when updating the code should we have a new version
-    new_update = conn.update_function_code(
-        FunctionName=name_or_arn, ZipFile=get_test_zip_file3(), Publish=True
-    )
-    assert (
-        new_update["FunctionArn"]
-        == most_recent_config["Configuration"]["FunctionArn"] + ":3"
-    )
-    assert new_update["Version"] == "3"
+    wait_for_func = conn.get_waiter("function_updated")
+    wait_for_func.wait(FunctionName=function_name, WaiterConfig={"Delay": 1})
+
+    resp = conn.publish_version(FunctionName=name_or_arn)
+    # The code is the same, so is the version, as the previous update was already published
+    assert resp["CodeSha256"] == update1["CodeSha256"]
+    assert resp["Version"] == "4"
+
+    wait_for_func = conn.get_waiter("function_updated")
+    wait_for_func.wait(FunctionName=function_name, WaiterConfig={"Delay": 1})
+
+    conn.delete_function(FunctionName=function_name)
 
 
 @mock_aws
@@ -1522,7 +1537,7 @@ def test_update_function_s3():
     assert len(response["Code"]) == 2
     assert response["Code"]["RepositoryType"] == "S3"
     assert response["Code"]["Location"].startswith(
-        f"s3://awslambda-{_lambda_region}-tasks.s3-{_lambda_region}.amazonaws.com"
+        f"https://awslambda-{_lambda_region}-tasks.s3.{_lambda_region}.amazonaws.com"
     )
 
     config = response["Configuration"]
diff --git a/tests/test_ds/test_ds.py b/tests/test_ds/test_ds.py
index 88b0a7809..2bbbbdeb3 100644
--- a/tests/test_ds/test_ds.py
+++ b/tests/test_ds/test_ds.py
@@ -390,6 +390,10 @@ def test_describe_trusts():
     assert trusts[0]["TrustId"] == trust_ids[1]
     assert trusts[1]["TrustId"] == trust_ids[2]
 
+    # Describe all the trusts in the account
+    trusts = client.describe_trusts()["Trusts"]
+    assert len(trusts) == 5
+
 
 @mock_aws
 def test_delete_trust():
@@ -455,3 +459,18 @@ def test_ldaps_exceptions_non_microsoftad():
         client.disable_ldaps(DirectoryId=directory_id, Type="Client")
     err = exc.value.response["Error"]
     assert err["Code"] == "UnsupportedOperationException"
+
+
+@mock_aws
+def test_settings_exception_non_microsoftad():
+    """Test Settings operations on non-Microsoft AD directories."""
+
+    client = boto3.client("ds", region_name=TEST_REGION)
+    ec2_client = boto3.client("ec2", region_name=TEST_REGION)
+
+    directory_id = create_test_directory(client, ec2_client)
+    # Test describing Settings on a non-Microsoft AD directory.
+    with pytest.raises(ClientError) as exc:
+        client.describe_settings(DirectoryId=directory_id)
+    err = exc.value.response["Error"]
+    assert err["Code"] == "InvalidParameterException"
diff --git a/tests/test_ds/test_ds_microsoft_ad.py b/tests/test_ds/test_ds_microsoft_ad.py
index 2d8c7d563..af9840d4f 100644
--- a/tests/test_ds/test_ds_microsoft_ad.py
+++ b/tests/test_ds/test_ds_microsoft_ad.py
@@ -258,3 +258,43 @@ def test_enable_describe_disable_ldaps():
     ]
     assert len(ldaps) == 1
     assert ldaps[0]["LDAPSStatus"] == "Disabled"
+
+
+@mock_aws
+def test_describe_settings():
+    client = boto3.client("ds", region_name=TEST_REGION)
+    ec2_client = boto3.client("ec2", region_name=TEST_REGION)
+    directory_id = create_test_microsoft_ad(client, ec2_client)
+
+    # Describe the settings for a Microsoft AD directory
+    settings = client.describe_settings(DirectoryId=directory_id)["SettingEntries"]
+    assert len(settings) > 0
+    assert "TLS_1_0" in [s["Name"] for s in settings]
+
+
+@mock_aws
+def test_update_settings():
+    client = boto3.client("ds", region_name=TEST_REGION)
+    ec2_client = boto3.client("ec2", region_name=TEST_REGION)
+    directory_id = create_test_microsoft_ad(client, ec2_client)
+
+    # Check the current setting for TLS 1.0
+    directory_settings = client.describe_settings(DirectoryId=directory_id)[
+        "SettingEntries"
+    ]
+    tls_1_0_setting = next(
+        (s for s in directory_settings if s["Name"] == "TLS_1_0"), None
+    )
+    assert tls_1_0_setting["AppliedValue"] == "Enable"
+
+    new_setting = {"Name": "TLS_1_0", "Value": "Disable"}
+    client.update_settings(DirectoryId=directory_id, Settings=[new_setting])
+
+    # Check the updated setting for TLS 1.0
+    new_directory_settings = client.describe_settings(DirectoryId=directory_id)[
+        "SettingEntries"
+    ]
+    new_tls_1_0_setting = next(
+        (s for s in new_directory_settings if s["Name"] == "TLS_1_0"), None
+    )
+    assert new_tls_1_0_setting["AppliedValue"] == "Disable"
diff --git a/tests/test_efs/test_filesystem_policy.py b/tests/test_efs/test_filesystem_policy.py
new file mode 100644
index 000000000..d88c4799b
--- /dev/null
+++ b/tests/test_efs/test_filesystem_policy.py
@@ -0,0 +1,32 @@
+import pytest
+from botocore.exceptions import ClientError
+
+from . import fixture_efs  # noqa # pylint: disable=unused-import
+
+
+def test_describe_file_system_policy__initial(efs):
+    create_fs_resp = efs.create_file_system(CreationToken="foobar")
+    fs_id = create_fs_resp["FileSystemId"]
+
+    with pytest.raises(ClientError) as exc_info:
+        efs.describe_file_system_policy(FileSystemId=fs_id)
+    err = exc_info.value.response["Error"]
+    assert err["Code"] == "PolicyNotFound"
+
+
+def test_put_file_system_policy(efs):
+    # Create the file system.
+    create_fs_resp = efs.create_file_system(CreationToken="foobar")
+    create_fs_resp.pop("ResponseMetadata")
+    fs_id = create_fs_resp["FileSystemId"]
+
+    # Create the filesystem policy
+    policy = '{\n  "Version" : "2012-10-17",\n  "Id" : "efs-policy-1234",\n  "Statement" : [ {\n    "Sid" : "efs-statement-1234",\n    "Effect" : "Allow",\n    "Principal" : {\n      "AWS" : "*"\n    },\n    "Action" : [ "elasticfilesystem:ClientRootAccess", "elasticfilesystem:ClientWrite" ],\n    "Resource" : "arn:aws:elasticfilesystem:us-east-1:1234:file-system/fs-1234",\n    "Condition" : {\n      "Bool" : {\n        "elasticfilesystem:AccessedViaMountTarget" : "true"\n      }\n    }\n  } ]\n}'
+    resp = efs.put_file_system_policy(FileSystemId=fs_id, Policy=policy)
+    assert len(resp["Policy"]) > 1
+    assert "ClientRootAccess" in resp["Policy"]
+
+    # Describe the filesystem policy
+    resp = efs.describe_file_system_policy(FileSystemId=fs_id)
+    assert len(resp["Policy"]) > 1
+    assert "ClientRootAccess" in resp["Policy"]
diff --git a/tests/test_iam/test_iam_cloudformation.py b/tests/test_iam/test_iam_cloudformation.py
index ae66a3fb6..5650d81ca 100644
--- a/tests/test_iam/test_iam_cloudformation.py
+++ b/tests/test_iam/test_iam_cloudformation.py
@@ -18,6 +18,7 @@ Resources:
   RootRole:
     Type: 'AWS::IAM::Role'
     Properties:
+      Description: {0}
       AssumeRolePolicyDocument:
         Version: 2012-10-17
         Statement:
@@ -31,9 +32,9 @@ Outputs:
   RootRole:
     Value: !Ref RootRole
   RoleARN:
-    Value: {"Fn::GetAtt": ["RootRole", "Arn"]}
+    Value: {{"Fn::GetAtt": ["RootRole", "Arn"]}}
   RoleID:
-    Value: {"Fn::GetAtt": ["RootRole", "RoleId"]}
+    Value: {{"Fn::GetAtt": ["RootRole", "RoleId"]}}
 """
 
 
@@ -1370,8 +1371,9 @@ def test_iam_cloudformation_create_role():
     cf_client = boto3.client("cloudformation", region_name="us-east-1")
 
     stack_name = "MyStack"
+    description = "initial description"
 
-    template = TEMPLATE_MINIMAL_ROLE.strip()
+    template = TEMPLATE_MINIMAL_ROLE.strip().format(description)
     cf_client.create_stack(StackName=stack_name, TemplateBody=template)
 
     resources = cf_client.list_stack_resources(StackName=stack_name)[
@@ -1390,6 +1392,19 @@ def test_iam_cloudformation_create_role():
     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]
+    assert roles[0]["Description"] == description
+
+    description = "new description"
+    template = TEMPLATE_MINIMAL_ROLE.strip().format(description)
+    cf_client.update_stack(StackName=stack_name, TemplateBody=template)
+
+    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]
+    assert roles[0]["Description"] == description
 
     cf_client.delete_stack(StackName=stack_name)