Repository: getmoto/moto
Pull request #8375 (2024-12-06 15:22:56)

ds - Add settings support
Adds mocking support to Directory Services [Settings](https://docs.aws.amazon.com/directoryservice/latest/devguide/API_Setting.html)

- [ds.DescribeSettings](https://docs.aws.amazon.com/directoryservice/latest/devguide/API_DescribeSettings.html)
- [ds.UpdateSettings](https://docs.aws.amazon.com/directoryservice/latest/devguide/API_UpdateSettings.html)

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/8375?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
Attention: Patch coverage is `94.11765%` with `2 lines` in your changes missing coverage. Please review.
> Project coverage is 94.58%. Comparing base [(`60328d5`)](https://app.codecov.io/gh/getmoto/moto/commit/60328d54ee14c390f00d83167da94bf273a58a6b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) to head [(`44e3102`)](https://app.codecov.io/gh/getmoto/moto/commit/44e3102d469ba1866e955094242910922aa8396e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).
> Report is 28 commits behind head on master.

| [Files with missing lines](https://app.codecov.io/gh/getmoto/moto/pull/8375?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Patch % | Lines |
|---|---|---|
| [moto/ds/models.py](https://app.codecov.io/gh/getmoto/moto/pull/8375?src=pr&el=tree&filepath=moto%2Fds%2Fmodels.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#diff-bW90by9kcy9tb2RlbHMucHk=) | 90.90% | [2 Missing :warning: ](https://app.codecov.io/gh/getmoto/moto/pull/8375?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) |

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


```diff
@@           Coverage Diff            @@
##           master    #8375    +/-   ##
========================================
  Coverage   94.58%   94.58%            
========================================
  Files        1159     1159            
  Lines      101063   101329   +266     
========================================
+ Hits        95587    95847   +260     
- Misses       5476     5482     +6     
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/8375/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/8375/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `28.85% <14.70%> (-0.05%)` | :arrow_down: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/8375/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `94.56% <94.11%> (+<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/8375?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/core/base_backend.py b/moto/core/base_backend.py
index 59c92a851..cc637e70b 100644
--- a/moto/core/base_backend.py
+++ b/moto/core/base_backend.py
@@ -210,8 +210,6 @@ class AccountSpecificBackend(Dict[str, SERVICE_BACKEND]):
       account_specific_backend[region: str] = backend: BaseBackend
     """
 
-    session = Session()
-
     def __init__(
         self,
         service_name: str,
@@ -232,13 +230,25 @@ class AccountSpecificBackend(Dict[str, SERVICE_BACKEND]):
     @lru_cache()
     def _generate_regions(self, service_name: str) -> List[str]:
         regions = []
-        for partition in AccountSpecificBackend.session.get_available_partitions():
-            partition_regions = AccountSpecificBackend.session.get_available_regions(
-                service_name, partition_name=partition
+        for (
+            partition
+        ) in AccountSpecificBackend.get_session().get_available_partitions():
+            partition_regions = (
+                AccountSpecificBackend.get_session().get_available_regions(
+                    service_name, partition_name=partition
+                )
             )
             regions.extend(partition_regions)
         return regions
 
+    @classmethod
+    @lru_cache()
+    def get_session(cls) -> Session:  # type: ignore[misc]
+        # Only instantiate Session when we absolutely need it
+        # This gives the user time to remove any env variables that break botocore, like AWS_PROFILE
+        # See https://github.com/getmoto/moto/issues/5469#issuecomment-2474897120
+        return Session()
+
     def __hash__(self) -> int:  # type: ignore[override]
         return hash(self._id)
 
diff --git a/moto/ds/models.py b/moto/ds/models.py
index b55e95932..4c63f2d39 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(
@@ -692,5 +696,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/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/moto/rds/exceptions.py b/moto/rds/exceptions.py
index 1829bc3f0..f76efba4b 100644
--- a/moto/rds/exceptions.py
+++ b/moto/rds/exceptions.py
@@ -212,6 +212,23 @@ class InvalidDBInstanceIdentifier(InvalidParameterValue):
         )
 
 
+class InvalidDBSnapshotIdentifier(InvalidParameterValue):
+    def __init__(self, snapshot_identifier: str, parameter_name: str) -> None:
+        if snapshot_identifier == "":
+            exception_text = f"The parameter {parameter_name} must be provided and must not be blank."
+        elif not snapshot_identifier[0].isalpha():
+            # On AWS, this error message seems to be triggered when the first character is invalid.
+            # The two spaces before the snapshot_identifier are what AWS produces!
+            exception_text = f"Invalid snapshot identifier:  {snapshot_identifier}"
+        else:
+            exception_text = (
+                f"The parameter {parameter_name} is not a valid identifier. "
+                "Identifiers must begin with a letter; must contain only ASCII letters, digits, and hyphens; "
+                "and must not end with a hyphen or contain two consecutive hyphens."
+            )
+        super().__init__(exception_text)
+
+
 class InvalidDBInstanceEngine(InvalidParameterCombination):
     def __init__(self, instance_engine: str, cluster_engine: str) -> None:
         super().__init__(
diff --git a/moto/rds/models.py b/moto/rds/models.py
index a65d69ad3..a894b04cc 100644
--- a/moto/rds/models.py
+++ b/moto/rds/models.py
@@ -39,6 +39,7 @@ from .exceptions import (
     InvalidDBInstanceEngine,
     InvalidDBInstanceIdentifier,
     InvalidDBInstanceStateError,
+    InvalidDBSnapshotIdentifier,
     InvalidExportSourceStateError,
     InvalidGlobalClusterStateFault,
     InvalidParameterCombination,
@@ -1952,8 +1953,10 @@ class RDSBackend(BaseBackend):
             db_instance_identifier=None, db_snapshot_identifier=from_snapshot_id
         )[0]
         original_database = snapshot.database
-        new_instance_props = copy.deepcopy(original_database.__dict__)
-        new_instance_props.pop("backend")
+        new_instance_props = {}
+        for key, value in original_database.__dict__.items():
+            if key != "backend":
+                new_instance_props[key] = copy.deepcopy(value)
         if not original_database.option_group_supplied:
             # If the option group is not supplied originally, the 'option_group_name' will receive a default value
             # Force this reconstruction, and prevent any validation on the default value
@@ -2719,12 +2722,34 @@ class RDSBackend(BaseBackend):
         # # Must contain from 1 to 63 letters, numbers, or hyphens.
         # # First character must be a letter.
         # # Can't end with a hyphen or contain two consecutive hyphens.
-        if re.match(
-            "^(?!.*--)([a-zA-Z]?[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])$", db_identifier
+        if (
+            re.match(
+                "^(?!.*--)([a-zA-Z]?[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])$", db_identifier
+            )
+            and db_identifier[0].isalpha()
         ):
             return
         raise InvalidDBInstanceIdentifier
 
+    @staticmethod
+    def validate_db_snapshot_identifier(
+        db_snapshot_identifier: str, parameter_name: str
+    ) -> None:
+        # https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_CreateDBSnapshot.html
+        # Constraints:
+        # # Must contain from 1 to 255 letters, numbers, or hyphens.
+        # # First character must be a letter.
+        # # Can't end with a hyphen or contain two consecutive hyphens.
+        if (
+            re.match(
+                "^(?!.*--)([a-zA-Z]?[a-zA-Z0-9-]{0,253}[a-zA-Z0-9])$",
+                db_snapshot_identifier,
+            )
+            and db_snapshot_identifier[0].isalpha()
+        ):
+            return
+        raise InvalidDBSnapshotIdentifier(db_snapshot_identifier, parameter_name)
+
     def describe_orderable_db_instance_options(
         self, engine: str, engine_version: str
     ) -> List[Dict[str, Any]]:
diff --git a/moto/rds/responses.py b/moto/rds/responses.py
index fabafe7fa..a972a2473 100644
--- a/moto/rds/responses.py
+++ b/moto/rds/responses.py
@@ -277,6 +277,11 @@ class RDSResponse(BaseResponse):
     def delete_db_instance(self) -> str:
         db_instance_identifier = self._get_param("DBInstanceIdentifier")
         db_snapshot_name = self._get_param("FinalDBSnapshotIdentifier")
+        if db_snapshot_name is not None:
+            self.backend.validate_db_snapshot_identifier(
+                db_snapshot_name, parameter_name="FinalDBSnapshotIdentifier"
+            )
+
         database = self.backend.delete_db_instance(
             db_instance_identifier, db_snapshot_name
         )
@@ -293,8 +298,13 @@ class RDSResponse(BaseResponse):
         db_instance_identifier = self._get_param("DBInstanceIdentifier")
         db_snapshot_identifier = self._get_param("DBSnapshotIdentifier")
         tags = self.unpack_list_params("Tags", "Tag")
+        self.backend.validate_db_snapshot_identifier(
+            db_snapshot_identifier, parameter_name="DBSnapshotIdentifier"
+        )
         snapshot = self.backend.create_db_snapshot(
-            db_instance_identifier, db_snapshot_identifier, tags=tags
+            db_instance_identifier,
+            db_snapshot_identifier,
+            tags=tags,
         )
         template = self.response_template(CREATE_SNAPSHOT_TEMPLATE)
         return template.render(snapshot=snapshot)
@@ -304,6 +314,10 @@ class RDSResponse(BaseResponse):
         target_snapshot_identifier = self._get_param("TargetDBSnapshotIdentifier")
         tags = self.unpack_list_params("Tags", "Tag")
         copy_tags = self._get_param("CopyTags")
+        self.backend.validate_db_snapshot_identifier(
+            target_snapshot_identifier, parameter_name="TargetDBSnapshotIdentifier"
+        )
+
         snapshot = self.backend.copy_db_snapshot(
             source_snapshot_identifier, target_snapshot_identifier, tags, copy_tags
         )
@@ -378,6 +392,11 @@ class RDSResponse(BaseResponse):
     def stop_db_instance(self) -> str:
         db_instance_identifier = self._get_param("DBInstanceIdentifier")
         db_snapshot_identifier = self._get_param("DBSnapshotIdentifier")
+        if db_snapshot_identifier is not None:
+            self.backend.validate_db_snapshot_identifier(
+                db_snapshot_identifier, parameter_name="DBSnapshotIdentifier"
+            )
+
         database = self.backend.stop_db_instance(
             db_instance_identifier, db_snapshot_identifier
         )


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..1852fb05a 100644
--- a/tests/test_ds/test_ds.py
+++ b/tests/test_ds/test_ds.py
@@ -455,3 +455,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_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)
 
diff --git a/tests/test_rds/test_rds.py b/tests/test_rds/test_rds.py
index 7da065ace..50c862452 100644
--- a/tests/test_rds/test_rds.py
+++ b/tests/test_rds/test_rds.py
@@ -7,6 +7,8 @@ from botocore.exceptions import ClientError
 
 from moto import mock_aws
 from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
+from moto.rds.exceptions import InvalidDBInstanceIdentifier, InvalidDBSnapshotIdentifier
+from moto.rds.models import RDSBackend
 from tests import aws_verified
 
 DEFAULT_REGION = "us-west-2"
@@ -1221,8 +1223,11 @@ def test_delete_db_snapshot():
         conn.describe_db_snapshots(DBSnapshotIdentifier="snapshot-1")
 
 
+@pytest.mark.parametrize(
+    "custom_db_subnet_group", [True, False], ids=("custom_subnet", "default_subnet")
+)
 @mock_aws
-def test_restore_db_instance_from_db_snapshot():
+def test_restore_db_instance_from_db_snapshot(custom_db_subnet_group: bool):
     conn = boto3.client("rds", region_name=DEFAULT_REGION)
     conn.create_db_instance(
         DBInstanceIdentifier="db-primary-1",
@@ -1240,10 +1245,27 @@ def test_restore_db_instance_from_db_snapshot():
         DBInstanceIdentifier="db-primary-1", DBSnapshotIdentifier="snapshot-1"
     )
 
+    if custom_db_subnet_group:
+        ec2 = boto3.client("ec2", region_name=DEFAULT_REGION)
+        subnets = ec2.describe_subnets()["Subnets"]
+        conn.create_db_subnet_group(
+            DBSubnetGroupName="custom-subnet-group",
+            DBSubnetGroupDescription="xxx",
+            SubnetIds=[subnets[0]["SubnetId"]],
+        )
+
     # restore
-    new_instance = conn.restore_db_instance_from_db_snapshot(
-        DBInstanceIdentifier="db-restore-1", DBSnapshotIdentifier="snapshot-1"
-    )["DBInstance"]
+    kwargs = {
+        "DBInstanceIdentifier": "db-restore-1",
+        "DBSnapshotIdentifier": "snapshot-1",
+    }
+    if custom_db_subnet_group:
+        kwargs["DBSubnetGroupName"] = "custom-subnet-group"
+    new_instance = conn.restore_db_instance_from_db_snapshot(**kwargs)["DBInstance"]
+    if custom_db_subnet_group:
+        assert (
+            new_instance["DBSubnetGroup"]["DBSubnetGroupName"] == "custom-subnet-group"
+        )
     assert new_instance["DBInstanceIdentifier"] == "db-restore-1"
     assert new_instance["DBInstanceClass"] == "db.m1.small"
     assert new_instance["StorageType"] == "gp2"
@@ -2778,6 +2800,46 @@ def test_create_db_instance_with_availability_zone():
     assert resp["DBInstances"][0]["AvailabilityZone"] == availability_zone
 
 
+invalid_identifiers = ("-foo", "foo-", "2foo", "foo--bar", "", "foo_bar")
+invalid_db_identifiers = invalid_identifiers + ("x" * 64,)
+invalid_db_snapshot_identifiers = invalid_identifiers + ("x" * 256,)
+
+
+@pytest.mark.parametrize("invalid_db_identifier", invalid_db_identifiers)
+def test_validate_db_identifier_backend_invalid(invalid_db_identifier):
+    with pytest.raises(InvalidDBInstanceIdentifier):
+        RDSBackend._validate_db_identifier(invalid_db_identifier)
+
+
+@pytest.mark.parametrize(
+    "invalid_db_snapshot_identifier", invalid_db_snapshot_identifiers
+)
+def test_validate_db_snapshot_identifier_backend_invalid(
+    invalid_db_snapshot_identifier,
+):
+    with pytest.raises(InvalidDBSnapshotIdentifier):
+        RDSBackend.validate_db_snapshot_identifier(
+            invalid_db_snapshot_identifier, "DBSnapshotIdentifier"
+        )
+
+
+valid_identifiers = ("f", "foo", "FOO", "FOO-bar-123")
+valid_db_identifiers = valid_identifiers + ("x" * 63,)
+valid_db_snapshot_identifiers = valid_identifiers + ("x" * 255,)
+
+
+@pytest.mark.parametrize("valid_db_identifier", valid_db_identifiers)
+def test_validate_db_identifier_backend_valid(valid_db_identifier):
+    RDSBackend._validate_db_identifier(valid_db_identifier)
+
+
+@pytest.mark.parametrize("valid_db_snapshot_identifier", valid_db_snapshot_identifiers)
+def test_validate_db_snapshot_identifier_backend_valid(valid_db_snapshot_identifier):
+    RDSBackend.validate_db_snapshot_identifier(
+        valid_db_snapshot_identifier, "DBSnapshotIdentifier"
+    )
+
+
 @mock_aws
 def test_validate_db_identifier():
     client = boto3.client("rds", region_name=DEFAULT_REGION)
@@ -2817,6 +2879,97 @@ def test_validate_db_identifier():
     validation_helper(exc)
 
 
+@mock_aws
+def test_validate_db_snapshot_identifier_different_operations():
+    client = boto3.client("rds", region_name=DEFAULT_REGION)
+    db_instance_identifier = "valid-identifier"
+    valid_db_snapshot_identifier = "valid"
+    invalid_db_snapshot_identifier = "--invalid--"
+
+    client.create_db_instance(
+        DBInstanceIdentifier=db_instance_identifier,
+        Engine="postgres",
+        DBName="staging-postgres",
+        DBInstanceClass="db.m1.small",
+    )
+
+    expected_message = f"Invalid snapshot identifier:  {invalid_db_snapshot_identifier}"
+    with pytest.raises(ClientError) as exc:
+        client.create_db_snapshot(
+            DBSnapshotIdentifier=invalid_db_snapshot_identifier,
+            DBInstanceIdentifier=db_instance_identifier,
+        )
+    snapshot_validation_helper(exc, expected_message)
+
+    client.create_db_snapshot(
+        DBSnapshotIdentifier=valid_db_snapshot_identifier,
+        DBInstanceIdentifier=db_instance_identifier,
+    )
+
+    with pytest.raises(ClientError) as exc:
+        client.copy_db_snapshot(
+            SourceDBSnapshotIdentifier=valid_db_snapshot_identifier,
+            TargetDBSnapshotIdentifier=invalid_db_snapshot_identifier,
+        )
+    snapshot_validation_helper(exc, expected_message)
+
+    with pytest.raises(ClientError) as exc:
+        client.stop_db_instance(
+            DBInstanceIdentifier=db_instance_identifier,
+            DBSnapshotIdentifier=invalid_db_snapshot_identifier,
+        )
+    snapshot_validation_helper(exc, expected_message)
+
+    with pytest.raises(ClientError) as exc:
+        client.delete_db_instance(
+            DBInstanceIdentifier=db_instance_identifier,
+            FinalDBSnapshotIdentifier=invalid_db_snapshot_identifier,
+        )
+    snapshot_validation_helper(exc, expected_message)
+
+
+# Depending on what is wrong with the snapshot_identifier, AWS produces different error messages.
+snapshot_identifier_data = (
+    ("", "The parameter DBSnapshotIdentifier must be provided and must not be blank."),
+    ("-invalid", "Invalid snapshot identifier:  -invalid"),
+    (
+        "in--valid",
+        (
+            "The parameter DBSnapshotIdentifier is not a valid identifier. "
+            "Identifiers must begin with a letter; must contain only ASCII "
+            "letters, digits, and hyphens; and must not end with a hyphen "
+            "or contain two consecutive hyphens."
+        ),
+    ),
+)
+
+
+@pytest.mark.parametrize(
+    "invalid_db_snapshot_identifier,expected_message",
+    snapshot_identifier_data,
+    ids=("empty", "invalid_first_character", "default_message"),
+)
+@mock_aws
+def test_validate_db_snapshot_identifier_different_error_messages(
+    invalid_db_snapshot_identifier, expected_message
+):
+    client = boto3.client("rds", region_name=DEFAULT_REGION)
+    db_instance_identifier = "valid-identifier"
+    client.create_db_instance(
+        DBInstanceIdentifier=db_instance_identifier,
+        Engine="postgres",
+        DBName="staging-postgres",
+        DBInstanceClass="db.m1.small",
+    )
+
+    with pytest.raises(ClientError) as exc:
+        client.create_db_snapshot(
+            DBSnapshotIdentifier=invalid_db_snapshot_identifier,
+            DBInstanceIdentifier=db_instance_identifier,
+        )
+    snapshot_validation_helper(exc, expected_message)
+
+
 @mock_aws
 def test_createdb_instance_engine_with_invalid_value():
     client = boto3.client("rds", region_name=DEFAULT_REGION)
@@ -2951,3 +3104,9 @@ def validation_helper(exc):
         "letters, digits, and hyphens; "
         "and must not end with a hyphen or contain two consecutive hyphens."
     )
+
+
+def snapshot_validation_helper(exc, expected_message):
+    err = exc.value.response["Error"]
+    assert err["Code"] == "InvalidParameterValue"
+    assert err["Message"] == expected_message