Repository: getmoto/moto
Pull request #8370 (2024-12-05 21:51:48)

Support cloudformation updates for IAM Roles


Patch:
diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index 1ed94cd6e..650962035 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -1956,7 +1956,7 @@
 
 ## directconnect
 <details>
-<summary>6% implemented</summary>
+<summary>12% implemented</summary>
 
 - [ ] accept_direct_connect_gateway_association_proposal
 - [ ] allocate_connection_on_interconnect
@@ -1966,7 +1966,7 @@
 - [ ] allocate_transit_virtual_interface
 - [ ] associate_connection_with_lag
 - [ ] associate_hosted_connection
-- [ ] associate_mac_sec_key
+- [x] associate_mac_sec_key
 - [ ] associate_virtual_interface
 - [ ] confirm_connection
 - [ ] confirm_customer_agreement
@@ -1979,7 +1979,7 @@
 - [ ] create_direct_connect_gateway_association
 - [ ] create_direct_connect_gateway_association_proposal
 - [ ] create_interconnect
-- [ ] create_lag
+- [x] create_lag
 - [ ] create_private_virtual_interface
 - [ ] create_public_virtual_interface
 - [ ] create_transit_virtual_interface
@@ -2002,7 +2002,7 @@
 - [ ] describe_hosted_connections
 - [ ] describe_interconnect_loa
 - [ ] describe_interconnects
-- [ ] describe_lags
+- [x] describe_lags
 - [ ] describe_loa
 - [ ] describe_locations
 - [ ] describe_router_configuration
@@ -2010,7 +2010,7 @@
 - [ ] describe_virtual_gateways
 - [ ] describe_virtual_interfaces
 - [ ] disassociate_connection_from_lag
-- [ ] disassociate_mac_sec_key
+- [x] disassociate_mac_sec_key
 - [ ] list_virtual_interface_test_history
 - [ ] start_bgp_failover_test
 - [ ] stop_bgp_failover_test
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/docs/docs/services/directconnect.rst b/docs/docs/services/directconnect.rst
index 5339e3942..b0f2318e0 100644
--- a/docs/docs/services/directconnect.rst
+++ b/docs/docs/services/directconnect.rst
@@ -24,7 +24,7 @@ directconnect
 - [ ] allocate_transit_virtual_interface
 - [ ] associate_connection_with_lag
 - [ ] associate_hosted_connection
-- [ ] associate_mac_sec_key
+- [x] associate_mac_sec_key
 - [ ] associate_virtual_interface
 - [ ] confirm_connection
 - [ ] confirm_customer_agreement
@@ -37,7 +37,7 @@ directconnect
 - [ ] create_direct_connect_gateway_association
 - [ ] create_direct_connect_gateway_association_proposal
 - [ ] create_interconnect
-- [ ] create_lag
+- [x] create_lag
 - [ ] create_private_virtual_interface
 - [ ] create_public_virtual_interface
 - [ ] create_transit_virtual_interface
@@ -60,7 +60,7 @@ directconnect
 - [ ] describe_hosted_connections
 - [ ] describe_interconnect_loa
 - [ ] describe_interconnects
-- [ ] describe_lags
+- [x] describe_lags
 - [ ] describe_loa
 - [ ] describe_locations
 - [ ] describe_router_configuration
@@ -68,7 +68,7 @@ directconnect
 - [ ] describe_virtual_gateways
 - [ ] describe_virtual_interfaces
 - [ ] disassociate_connection_from_lag
-- [ ] disassociate_mac_sec_key
+- [x] disassociate_mac_sec_key
 - [ ] list_virtual_interface_test_history
 - [ ] start_bgp_failover_test
 - [ ] stop_bgp_failover_test
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/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/kms/responses.py b/moto/kms/responses.py
index b68ec20b0..7f5b9b674 100644
--- a/moto/kms/responses.py
+++ b/moto/kms/responses.py
@@ -707,7 +707,6 @@ class KmsResponse(BaseResponse):
         key_id = self._get_param("KeyId")
 
         self._validate_key_id(key_id)
-        self._validate_cmk_id(key_id)
         key, public_key = self.kms_backend.get_public_key(key_id)
         return json.dumps(
             {
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_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_kms/test_kms_boto3.py b/tests/test_kms/test_kms_boto3.py
index d1ccaf402..bafccb20f 100644
--- a/tests/test_kms/test_kms_boto3.py
+++ b/tests/test_kms/test_kms_boto3.py
@@ -1692,6 +1692,34 @@ def test_get_public_key():
     assert "EncryptionAlgorithms" not in public_key_response
 
 
+@mock_aws
+def test_get_public_key_with_alias():
+    client = boto3.client("kms", region_name="eu-west-1")
+    key = client.create_key(KeyUsage="SIGN_VERIFY", KeySpec="RSA_2048")
+    key_id = key["KeyMetadata"]["KeyId"]
+    client.create_alias(
+        AliasName="alias/foo",
+        TargetKeyId=key_id,
+    )
+    alias_arn = client.list_aliases()["Aliases"][0]["AliasArn"]
+
+    public_key_response_with_name = client.get_public_key(KeyId="alias/foo")
+    public_key_response_with_arn = client.get_public_key(KeyId=alias_arn)
+
+    assert "PublicKey" in public_key_response_with_name
+    assert "PublicKey" in public_key_response_with_arn
+    assert (
+        public_key_response_with_name["SigningAlgorithms"]
+        == key["KeyMetadata"]["SigningAlgorithms"]
+    )
+    assert (
+        public_key_response_with_arn["SigningAlgorithms"]
+        == key["KeyMetadata"]["SigningAlgorithms"]
+    )
+    assert "EncryptionAlgorithms" not in public_key_response_with_name
+    assert "EncryptionAlgorithms" not in public_key_response_with_arn
+
+
 def create_simple_key(client, id_or_arn="KeyId", description=None, policy=None):
     with mock.patch.object(rsa, "generate_private_key", return_value=""):
         params = {}
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