Repository: getmoto/moto
Pull request #6754 (2023-09-01 17:53:56)

ElastiCache: Add the `create_cache_cluster`, `delete_cache_cluster` and `describe_cache_clusters` methods support
Added the base configuration for the following Elasticache methods: 

1. create_cache_cluster
2. delete_cache_cluster
3. describe_cache_clusters

The pagination functionality for the `describe_cache_clusters` needs to be implemented, but the base configuration that I completed so far can be used to validate the necessary integrations.



Patch:
diff --git a/docs/docs/services/elasticache.rst b/docs/docs/services/elasticache.rst
index 8ede5a9f9..2c5a64e3b 100644
--- a/docs/docs/services/elasticache.rst
+++ b/docs/docs/services/elasticache.rst
@@ -33,7 +33,7 @@ elasticache
 - [ ] batch_stop_update_action
 - [ ] complete_migration
 - [ ] copy_snapshot
-- [ ] create_cache_cluster
+- [x] create_cache_cluster
 - [ ] create_cache_parameter_group
 - [ ] create_cache_security_group
 - [ ] create_cache_subnet_group
@@ -44,7 +44,7 @@ elasticache
 - [ ] create_user_group
 - [ ] decrease_node_groups_in_global_replication_group
 - [ ] decrease_replica_count
-- [ ] delete_cache_cluster
+- [x] delete_cache_cluster
 - [ ] delete_cache_parameter_group
 - [ ] delete_cache_security_group
 - [ ] delete_cache_subnet_group
@@ -53,7 +53,7 @@ elasticache
 - [ ] delete_snapshot
 - [X] delete_user
 - [ ] delete_user_group
-- [ ] describe_cache_clusters
+- [x] describe_cache_clusters
 - [ ] describe_cache_engine_versions
 - [ ] describe_cache_parameter_groups
 - [ ] describe_cache_parameters
@@ -70,10 +70,10 @@ elasticache
 - [ ] describe_update_actions
 - [ ] describe_user_groups
 - [X] describe_users
-  
+
         Only the `user_id` parameter is currently supported.
         Pagination is not yet implemented.
-        
+
 
 - [ ] disassociate_global_replication_group
 - [ ] failover_global_replication_group
diff --git a/moto/__init__.py b/moto/__init__.py
index cb1527a08..29bae381b 100644
--- a/moto/__init__.py
+++ b/moto/__init__.py
@@ -1,9 +1,9 @@
 import importlib
 import sys
 from contextlib import ContextDecorator
-from moto.core.models import BaseMockAWS
 from typing import Any, Callable, List, Optional, TypeVar
 
+from moto.core.models import BaseMockAWS
 
 TEST_METHOD = TypeVar("TEST_METHOD", bound=Callable[..., Any])
 
@@ -189,6 +189,9 @@ mock_xray = lazy_load(".xray", "mock_xray")
 mock_xray_client = lazy_load(".xray", "mock_xray_client")
 mock_wafv2 = lazy_load(".wafv2", "mock_wafv2")
 mock_textract = lazy_load(".textract", "mock_textract")
+mock_elasticache = lazy_load(
+    ".elasticache", "mock_elasticache", boto3_name="elasticache"
+)
 
 
 class MockAll(ContextDecorator):
diff --git a/moto/elasticache/exceptions.py b/moto/elasticache/exceptions.py
index fe14cb050..f5e7f5b65 100644
--- a/moto/elasticache/exceptions.py
+++ b/moto/elasticache/exceptions.py
@@ -1,4 +1,5 @@
 from typing import Any
+
 from moto.core.exceptions import RESTError
 
 EXCEPTION_RESPONSE = """<?xml version="1.0"?>
@@ -58,3 +59,25 @@ class UserNotFound(ElastiCacheException):
 
     def __init__(self, user_id: str):
         super().__init__("UserNotFound", message=f"User {user_id} not found.")
+
+
+class CacheClusterAlreadyExists(ElastiCacheException):
+
+    code = 404
+
+    def __init__(self, cache_cluster_id: str):
+        super().__init__(
+            "CacheClusterAlreadyExists",
+            message=f"Cache cluster {cache_cluster_id} already exists.",
+        ),
+
+
+class CacheClusterNotFound(ElastiCacheException):
+
+    code = 404
+
+    def __init__(self, cache_cluster_id: str):
+        super().__init__(
+            "CacheClusterNotFound",
+            message=f"Cache cluster {cache_cluster_id} not found.",
+        )
diff --git a/moto/elasticache/models.py b/moto/elasticache/models.py
index 198d75dee..25e37807c 100644
--- a/moto/elasticache/models.py
+++ b/moto/elasticache/models.py
@@ -1,7 +1,15 @@
-from typing import List, Optional
+from datetime import datetime
+from typing import List, Optional, Dict, Any, Tuple
+
 from moto.core import BaseBackend, BackendDict, BaseModel
 
-from .exceptions import UserAlreadyExists, UserNotFound
+from .exceptions import (
+    UserAlreadyExists,
+    UserNotFound,
+    CacheClusterAlreadyExists,
+    CacheClusterNotFound,
+)
+from ..moto_api._internal import mock_random
 
 
 class User(BaseModel):
@@ -29,6 +37,93 @@ class User(BaseModel):
         self.arn = f"arn:aws:elasticache:{self.region}:{account_id}:user:{self.id}"
 
 
+class CacheCluster(BaseModel):
+    def __init__(
+        self,
+        account_id: str,
+        region_name: str,
+        cache_cluster_id: str,
+        replication_group_id: Optional[str],
+        az_mode: Optional[str],
+        preferred_availability_zone: Optional[str],
+        num_cache_nodes: Optional[int],
+        cache_node_type: Optional[str],
+        engine: Optional[str],
+        engine_version: Optional[str],
+        cache_parameter_group_name: Optional[str],
+        cache_subnet_group_name: Optional[str],
+        transit_encryption_enabled: Optional[bool],
+        network_type: Optional[str],
+        ip_discovery: Optional[str],
+        snapshot_name: Optional[str],
+        preferred_maintenance_window: Optional[str],
+        port: Optional[int],
+        notification_topic_arn: Optional[str],
+        auto_minor_version_upgrade: Optional[bool],
+        snapshot_retention_limit: Optional[int],
+        snapshot_window: Optional[str],
+        auth_token: Optional[str],
+        outpost_mode: Optional[str],
+        preferred_outpost_arn: Optional[str],
+        preferred_availability_zones: Optional[List[str]],
+        cache_security_group_names: Optional[List[str]],
+        security_group_ids: Optional[List[str]],
+        tags: Optional[List[Dict[str, str]]],
+        snapshot_arns: Optional[List[str]],
+        preferred_outpost_arns: Optional[List[str]],
+        log_delivery_configurations: List[Dict[str, Any]],
+        cache_node_ids_to_remove: Optional[List[str]],
+        cache_node_ids_to_reboot: Optional[List[str]],
+    ):
+        if tags is None:
+            tags = []
+
+        self.cache_cluster_id = cache_cluster_id
+        self.az_mode = az_mode
+        self.preferred_availability_zone = preferred_availability_zone
+        self.preferred_availability_zones = preferred_availability_zones or []
+        self.engine = engine or "redis"
+        self.engine_version = engine_version
+        if engine == "redis":
+            self.num_cache_nodes = 1
+            self.replication_group_id = replication_group_id
+            self.snapshot_arns = snapshot_arns or []
+            self.snapshot_name = snapshot_name
+            self.snapshot_window = snapshot_window
+        if engine == "memcached":
+            if num_cache_nodes is None:
+                self.num_cache_nodes = 1
+            elif 1 <= num_cache_nodes <= 40:
+                self.num_cache_nodes = num_cache_nodes
+        self.cache_node_type = cache_node_type
+        self.cache_parameter_group_name = cache_parameter_group_name
+        self.cache_subnet_group_name = cache_subnet_group_name
+        self.cache_security_group_names = cache_security_group_names or []
+        self.security_group_ids = security_group_ids or []
+        self.tags = tags
+        self.preferred_maintenance_window = preferred_maintenance_window
+        self.port = port or 6379
+        self.notification_topic_arn = notification_topic_arn
+        self.auto_minor_version_upgrade = auto_minor_version_upgrade
+        self.snapshot_retention_limit = snapshot_retention_limit or 0
+        self.auth_token = auth_token
+        self.outpost_mode = outpost_mode
+        self.preferred_outpost_arn = preferred_outpost_arn
+        self.preferred_outpost_arns = preferred_outpost_arns or []
+        self.log_delivery_configurations = log_delivery_configurations or []
+        self.transit_encryption_enabled = transit_encryption_enabled
+        self.network_type = network_type
+        self.ip_discovery = ip_discovery
+        self.cache_node_ids_to_remove = cache_node_ids_to_remove
+        self.cache_node_ids_to_reboot = cache_node_ids_to_reboot
+
+        self.cache_cluster_create_time = datetime.utcnow()
+        self.auth_token_last_modified_date = datetime.utcnow()
+        self.cache_cluster_status = "available"
+        self.arn = f"arn:aws:elasticache:{region_name}:{account_id}:{cache_cluster_id}"
+        self.cache_node_id = str(mock_random.uuid4())
+
+
 class ElastiCacheBackend(BaseBackend):
     """Implementation of ElastiCache APIs."""
 
@@ -45,6 +140,45 @@ class ElastiCacheBackend(BaseBackend):
             no_password_required=True,
         )
 
+        # Define the cache_clusters dictionary to detect duplicates
+        self.cache_clusters = dict()
+        self.cache_clusters["default"] = CacheCluster(
+            account_id=self.account_id,
+            region_name=self.region_name,
+            cache_cluster_id="default",
+            replication_group_id=None,
+            az_mode=None,
+            preferred_availability_zone=None,
+            num_cache_nodes=1,
+            cache_node_type=None,
+            engine="redis",
+            engine_version=None,
+            cache_parameter_group_name=None,
+            cache_subnet_group_name=None,
+            transit_encryption_enabled=True,
+            network_type=None,
+            ip_discovery=None,
+            snapshot_name=None,
+            preferred_maintenance_window=None,
+            port=6379,
+            notification_topic_arn=None,
+            auto_minor_version_upgrade=True,
+            snapshot_retention_limit=0,
+            snapshot_window=None,
+            auth_token=None,
+            outpost_mode=None,
+            preferred_outpost_arn=None,
+            preferred_availability_zones=[],
+            cache_security_group_names=[],
+            security_group_ids=[],
+            tags=[],
+            snapshot_arns=[],
+            preferred_outpost_arns=[],
+            log_delivery_configurations=[],
+            cache_node_ids_to_remove=[],
+            cache_node_ids_to_reboot=[],
+        )
+
     def create_user(
         self,
         user_id: str,
@@ -92,5 +226,109 @@ class ElastiCacheBackend(BaseBackend):
                 raise UserNotFound(user_id)
         return list(self.users.values())
 
+    def create_cache_cluster(
+        self,
+        cache_cluster_id: str,
+        replication_group_id: str,
+        az_mode: str,
+        preferred_availability_zone: str,
+        num_cache_nodes: int,
+        cache_node_type: str,
+        engine: str,
+        engine_version: str,
+        cache_parameter_group_name: str,
+        cache_subnet_group_name: str,
+        transit_encryption_enabled: bool,
+        network_type: str,
+        ip_discovery: str,
+        snapshot_name: str,
+        preferred_maintenance_window: str,
+        port: int,
+        notification_topic_arn: str,
+        auto_minor_version_upgrade: bool,
+        snapshot_retention_limit: int,
+        snapshot_window: str,
+        auth_token: str,
+        outpost_mode: str,
+        preferred_outpost_arn: str,
+        preferred_availability_zones: List[str],
+        cache_security_group_names: List[str],
+        security_group_ids: List[str],
+        tags: List[Dict[str, str]],
+        snapshot_arns: List[str],
+        preferred_outpost_arns: List[str],
+        log_delivery_configurations: List[Dict[str, Any]],
+        cache_node_ids_to_remove: List[str],
+        cache_node_ids_to_reboot: List[str],
+    ) -> CacheCluster:
+        if cache_cluster_id in self.cache_clusters:
+            raise CacheClusterAlreadyExists(cache_cluster_id)
+        cache_cluster = CacheCluster(
+            account_id=self.account_id,
+            region_name=self.region_name,
+            cache_cluster_id=cache_cluster_id,
+            replication_group_id=replication_group_id,
+            az_mode=az_mode,
+            preferred_availability_zone=preferred_availability_zone,
+            preferred_availability_zones=preferred_availability_zones,
+            num_cache_nodes=num_cache_nodes,
+            cache_node_type=cache_node_type,
+            engine=engine,
+            engine_version=engine_version,
+            cache_parameter_group_name=cache_parameter_group_name,
+            cache_subnet_group_name=cache_subnet_group_name,
+            cache_security_group_names=cache_security_group_names,
+            security_group_ids=security_group_ids,
+            tags=tags,
+            snapshot_arns=snapshot_arns,
+            snapshot_name=snapshot_name,
+            preferred_maintenance_window=preferred_maintenance_window,
+            port=port,
+            notification_topic_arn=notification_topic_arn,
+            auto_minor_version_upgrade=auto_minor_version_upgrade,
+            snapshot_retention_limit=snapshot_retention_limit,
+            snapshot_window=snapshot_window,
+            auth_token=auth_token,
+            outpost_mode=outpost_mode,
+            preferred_outpost_arn=preferred_outpost_arn,
+            preferred_outpost_arns=preferred_outpost_arns,
+            log_delivery_configurations=log_delivery_configurations,
+            transit_encryption_enabled=transit_encryption_enabled,
+            network_type=network_type,
+            ip_discovery=ip_discovery,
+            cache_node_ids_to_remove=cache_node_ids_to_remove,
+            cache_node_ids_to_reboot=cache_node_ids_to_reboot,
+        )
+        self.cache_clusters[cache_cluster_id] = cache_cluster
+        return cache_cluster
+
+    def describe_cache_clusters(
+        self,
+        cache_cluster_id: str,
+        max_records: int,
+        marker: str,
+    ) -> Tuple[str, List[CacheCluster]]:
+        if marker is None:
+            marker = str(mock_random.uuid4())
+        if max_records is None:
+            max_records = 100
+        if cache_cluster_id:
+            if cache_cluster_id in self.cache_clusters:
+                cache_cluster = self.cache_clusters[cache_cluster_id]
+                return marker, [cache_cluster]
+            else:
+                raise CacheClusterNotFound(cache_cluster_id)
+        cache_clusters = list(self.cache_clusters.values())[:max_records]
+
+        return marker, cache_clusters
+
+    def delete_cache_cluster(self, cache_cluster_id: str) -> CacheCluster:
+        if cache_cluster_id:
+            if cache_cluster_id in self.cache_clusters:
+                cache_cluster = self.cache_clusters[cache_cluster_id]
+                cache_cluster.cache_cluster_status = "deleting"
+                return cache_cluster
+        raise CacheClusterNotFound(cache_cluster_id)
+
 
 elasticache_backends = BackendDict(ElastiCacheBackend, "elasticache")
diff --git a/moto/elasticache/responses.py b/moto/elasticache/responses.py
index 273fa002d..66a78a8e6 100644
--- a/moto/elasticache/responses.py
+++ b/moto/elasticache/responses.py
@@ -1,4 +1,5 @@
 from moto.core.responses import BaseResponse
+
 from .exceptions import PasswordTooShort, PasswordRequired
 from .models import elasticache_backends, ElastiCacheBackend
 
@@ -52,6 +53,97 @@ class ElastiCacheResponse(BaseResponse):
         template = self.response_template(DESCRIBE_USERS_TEMPLATE)
         return template.render(users=users)
 
+    def create_cache_cluster(self) -> str:
+        cache_cluster_id = self._get_param("CacheClusterId")
+        replication_group_id = self._get_param("ReplicationGroupId")
+        az_mode = self._get_param("AZMode")
+        preferred_availability_zone = self._get_param("PreferredAvailabilityZone")
+        preferred_availability_zones = self._get_param("PreferredAvailabilityZones")
+        num_cache_nodes = self._get_int_param("NumCacheNodes")
+        cache_node_type = self._get_param("CacheNodeType")
+        engine = self._get_param("Engine")
+        engine_version = self._get_param("EngineVersion")
+        cache_parameter_group_name = self._get_param("CacheParameterGroupName")
+        cache_subnet_group_name = self._get_param("CacheSubnetGroupName")
+        cache_security_group_names = self._get_param("CacheSecurityGroupNames")
+        security_group_ids = self._get_param("SecurityGroupIds")
+        tags = self._get_param("Tags")
+        snapshot_arns = self._get_param("SnapshotArns")
+        snapshot_name = self._get_param("SnapshotName")
+        preferred_maintenance_window = self._get_param("PreferredMaintenanceWindow")
+        port = self._get_param("Port")
+        notification_topic_arn = self._get_param("NotificationTopicArn")
+        auto_minor_version_upgrade = self._get_bool_param("AutoMinorVersionUpgrade")
+        snapshot_retention_limit = self._get_int_param("SnapshotRetentionLimit")
+        snapshot_window = self._get_param("SnapshotWindow")
+        auth_token = self._get_param("AuthToken")
+        outpost_mode = self._get_param("OutpostMode")
+        preferred_outpost_arn = self._get_param("PreferredOutpostArn")
+        preferred_outpost_arns = self._get_param("PreferredOutpostArns")
+        log_delivery_configurations = self._get_param("LogDeliveryConfigurations")
+        transit_encryption_enabled = self._get_bool_param("TransitEncryptionEnabled")
+        network_type = self._get_param("NetworkType")
+        ip_discovery = self._get_param("IpDiscovery")
+        # Define the following attributes as they're included in the response even during creation of a cache cluster
+        cache_node_ids_to_remove = self._get_param("CacheNodeIdsToRemove", [])
+        cache_node_ids_to_reboot = self._get_param("CacheNodeIdsToReboot", [])
+        cache_cluster = self.elasticache_backend.create_cache_cluster(
+            cache_cluster_id=cache_cluster_id,
+            replication_group_id=replication_group_id,
+            az_mode=az_mode,
+            preferred_availability_zone=preferred_availability_zone,
+            preferred_availability_zones=preferred_availability_zones,
+            num_cache_nodes=num_cache_nodes,
+            cache_node_type=cache_node_type,
+            engine=engine,
+            engine_version=engine_version,
+            cache_parameter_group_name=cache_parameter_group_name,
+            cache_subnet_group_name=cache_subnet_group_name,
+            cache_security_group_names=cache_security_group_names,
+            security_group_ids=security_group_ids,
+            tags=tags,
+            snapshot_arns=snapshot_arns,
+            snapshot_name=snapshot_name,
+            preferred_maintenance_window=preferred_maintenance_window,
+            port=port,
+            notification_topic_arn=notification_topic_arn,
+            auto_minor_version_upgrade=auto_minor_version_upgrade,
+            snapshot_retention_limit=snapshot_retention_limit,
+            snapshot_window=snapshot_window,
+            auth_token=auth_token,
+            outpost_mode=outpost_mode,
+            preferred_outpost_arn=preferred_outpost_arn,
+            preferred_outpost_arns=preferred_outpost_arns,
+            log_delivery_configurations=log_delivery_configurations,
+            transit_encryption_enabled=transit_encryption_enabled,
+            network_type=network_type,
+            ip_discovery=ip_discovery,
+            cache_node_ids_to_remove=cache_node_ids_to_remove,
+            cache_node_ids_to_reboot=cache_node_ids_to_reboot,
+        )
+        template = self.response_template(CREATE_CACHE_CLUSTER_TEMPLATE)
+        return template.render(cache_cluster=cache_cluster)
+
+    def describe_cache_clusters(self) -> str:
+        cache_cluster_id = self._get_param("CacheClusterId")
+        max_records = self._get_int_param("MaxRecords")
+        marker = self._get_param("Marker")
+        marker, cache_clusters = self.elasticache_backend.describe_cache_clusters(
+            cache_cluster_id=cache_cluster_id,
+            marker=marker,
+            max_records=max_records,
+        )
+        template = self.response_template(DESCRIBE_CACHE_CLUSTERS_TEMPLATE)
+        return template.render(marker=marker, cache_clusters=cache_clusters)
+
+    def delete_cache_cluster(self) -> str:
+        cache_cluster_id = self._get_param("CacheClusterId")
+        cache_cluster = self.elasticache_backend.delete_cache_cluster(
+            cache_cluster_id=cache_cluster_id,
+        )
+        template = self.response_template(DELETE_CACHE_CLUSTER_TEMPLATE)
+        return template.render(cache_cluster=cache_cluster)
+
 
 USER_TEMPLATE = """<UserId>{{ user.id }}</UserId>
     <UserName>{{ user.name }}</UserName>
@@ -74,14 +166,13 @@ USER_TEMPLATE = """<UserId>{{ user.id }}</UserId>
     </Authentication>
     <ARN>{{ user.arn }}</ARN>"""
 
-
 CREATE_USER_TEMPLATE = (
     """<CreateUserResponse xmlns="http://elasticache.amazonaws.com/doc/2015-02-02/">
-  <ResponseMetadata>
-    <RequestId>1549581b-12b7-11e3-895e-1334aEXAMPLE</RequestId>
-  </ResponseMetadata>
-  <CreateUserResult>
-    """
+          <ResponseMetadata>
+            <RequestId>1549581b-12b7-11e3-895e-1334aEXAMPLE</RequestId>
+          </ResponseMetadata>
+          <CreateUserResult>
+            """
     + USER_TEMPLATE
     + """
   </CreateUserResult>
@@ -90,11 +181,11 @@ CREATE_USER_TEMPLATE = (
 
 DELETE_USER_TEMPLATE = (
     """<DeleteUserResponse xmlns="http://elasticache.amazonaws.com/doc/2015-02-02/">
-  <ResponseMetadata>
-    <RequestId>1549581b-12b7-11e3-895e-1334aEXAMPLE</RequestId>
-  </ResponseMetadata>
-  <DeleteUserResult>
-    """
+          <ResponseMetadata>
+            <RequestId>1549581b-12b7-11e3-895e-1334aEXAMPLE</RequestId>
+          </ResponseMetadata>
+          <DeleteUserResult>
+            """
     + USER_TEMPLATE
     + """
   </DeleteUserResult>
@@ -103,14 +194,14 @@ DELETE_USER_TEMPLATE = (
 
 DESCRIBE_USERS_TEMPLATE = (
     """<DescribeUsersResponse xmlns="http://elasticache.amazonaws.com/doc/2015-02-02/">
-  <ResponseMetadata>
-    <RequestId>1549581b-12b7-11e3-895e-1334aEXAMPLE</RequestId>
-  </ResponseMetadata>
-  <DescribeUsersResult>
-    <Users>
-{% for user in users %}
-      <member>
-        """
+          <ResponseMetadata>
+            <RequestId>1549581b-12b7-11e3-895e-1334aEXAMPLE</RequestId>
+          </ResponseMetadata>
+          <DescribeUsersResult>
+            <Users>
+        {% for user in users %}
+              <member>
+                """
     + USER_TEMPLATE
     + """
       </member>
@@ -120,3 +211,337 @@ DESCRIBE_USERS_TEMPLATE = (
   </DescribeUsersResult>
 </DescribeUsersResponse>"""
 )
+
+CREATE_CACHE_CLUSTER_TEMPLATE = """<CreateCacheClusterResponse xmlns="http://elasticache.amazonaws.com/doc/2015-02-02/">
+  <ResponseMetadata>
+    <RequestId>1549581b-12b7-11e3-895e-1334aEXAMPLE</RequestId>
+  </ResponseMetadata>
+  <CreateCacheClusterResult>
+    <CacheCluster>
+  <CacheClusterId>{{ cache_cluster.cache_cluster_id }}</CacheClusterId>
+  <ConfigurationEndpoint>
+    <Address>example.cache.amazonaws.com</Address>
+    <Port>{{ cache_cluster.port }}</Port>
+  </ConfigurationEndpoint>
+  <ClientDownloadLandingPage></ClientDownloadLandingPage>
+  <CacheNodeType>{{ cache_cluster.cache_node_type }}</CacheNodeType>
+  <Engine>{{ cache_cluster.engine }}</Engine>
+  <EngineVersion>{{ cache_cluster.engine_version }}</EngineVersion>
+  <CacheClusterStatus>available</CacheClusterStatus>
+  <NumCacheNodes>{{ cache_cluster.num_cache_nodes }}</NumCacheNodes>
+  <PreferredAvailabilityZone>{{ cache_cluster.preferred_availability_zone }}</PreferredAvailabilityZone>
+  <PreferredOutpostArn>{{ cache_cluster.preferred_outpost_arn }}</PreferredOutpostArn>
+  <CacheClusterCreateTime>{{ cache_cluster.cache_cluster_create_time }}</CacheClusterCreateTime>
+  <PreferredMaintenanceWindow>{{ cache_cluster.preferred_maintenance_window }}</PreferredMaintenanceWindow>
+  {% if cache_cluster.cache_node_ids_to_remove != [] %}
+  <PendingModifiedValues>
+    <NumCacheNodes>{{ cache_cluster.num_cache_nodes }}</NumCacheNodes>
+    {% for cache_node_id_to_remove in cache_cluster.cache_node_ids_to_remove %}
+    <CacheNodeIdsToRemove>{{ cache_node_id_to_remove }}</CacheNodeIdsToRemove>
+    {% endfor %}
+    <EngineVersion>{{ cache_cluster.engine_version }}</EngineVersion>
+    <CacheNodeType>{{ cache_cluster.cache_node_type }}</CacheNodeType>
+    <AuthTokenStatus>SETTING</AuthTokenStatus>
+    <LogDeliveryConfigurations>
+    {% for log_delivery_configuration in cache_cluster.log_delivery_configurations %}
+      <LogType>{{ log_delivery_configuration.LogType }}</LogType>
+      <DestinationType>{{ log_delivery_configuration.DestinationType }}</DestinationType>
+      <DestinationDetails>
+        <CloudWatchLogsDetails>
+          <LogGroup>{{ log_delivery_configuration.LogGroup }}</LogGroup>
+        </CloudWatchLogsDetails>
+        <KinesisFirehoseDetails>
+          <DeliveryStream>{{ log_delivery_configuration.DeliveryStream }}</DeliveryStream>
+        </KinesisFirehoseDetails>
+      </DestinationDetails>
+      <LogFormat>{{ log_delivery_configuration.LogFormat }}</LogFormat>
+      {% endfor %}
+    </LogDeliveryConfigurations>
+    <TransitEncryptionEnabled>{{ cache_cluster.transit_encryption_enabled }}</TransitEncryptionEnabled>
+    <TransitEncryptionMode>preferred</TransitEncryptionMode>
+  </PendingModifiedValues>
+  {% endif %}
+  <NotificationConfiguration>
+    <TopicArn>{{ cache_cluster.notification_topic_arn }}</TopicArn>
+    <TopicStatus>active</TopicStatus>
+  </NotificationConfiguration>
+  <CacheSecurityGroups>
+  {% for cache_security_group_name in cache_cluster.cache_security_group_names %}
+    <CacheSecurityGroupName>{{ cache_security_group_name }}</CacheSecurityGroupName>
+    {% endfor %}
+    <Status>active</Status>
+  </CacheSecurityGroups>
+  <CacheParameterGroup>
+    <CacheParameterGroupName>{{ cache_cluster.cache_parameter_group_name }}</CacheParameterGroupName>
+    <ParameterApplyStatus>active</ParameterApplyStatus>
+    {% for cache_node_id_to_reboot in cache_cluster.cache_node_ids_to_reboot %}
+    <CacheNodeIdsToReboot>
+    {{ cache_node_id_to_reboot }}
+    </CacheNodeIdsToReboot>
+    {% endfor %}
+  </CacheParameterGroup>
+  <CacheSubnetGroupName>{{ cache_cluster.cache_subnet_group_name }}</CacheSubnetGroupName>
+  <CacheNodes>
+    <CacheNodeId>{{ cache_cluster.cache_node_id }}</CacheNodeId>
+    <CacheNodeStatus>{{ cache_cluster.cache_node_status }}</CacheNodeStatus>
+    <CacheNodeCreateTime>{{ cache_cluster.cache_cluster_create_time }}</CacheNodeCreateTime>
+    <Endpoint>
+      <Address>{{ cache_cluster.address }}</Address>
+      <Port>{{ cache_cluster.port }}</Port>
+    </Endpoint>
+    <ParameterGroupStatus>active</ParameterGroupStatus>
+    <SourceCacheNodeId>{{ cache_cluster.cache_node_id }}</SourceCacheNodeId>
+    <CustomerAvailabilityZone>{{ cache_cluster.preferred_availability_zone }}</CustomerAvailabilityZone>
+    <CustomerOutpostArn>{{ cache_cluster.preferred_output_arn }}</CustomerOutpostArn>
+  </CacheNodes>
+  <AutoMinorVersionUpgrade>{{ cache_cluster.auto_minor_version_upgrade }}</AutoMinorVersionUpgrade>
+  <SecurityGroups>
+  {% for security_group_id in cache_cluster.security_group_ids %}
+    <SecurityGroupId>{{ security_group_id }}</SecurityGroupId>
+    <Status>active</Status>
+    {% endfor %}
+  </SecurityGroups>
+  {% if cache_cluster.engine == "redis" %}
+  <ReplicationGroupId>{{ cache_cluster.replication_group_id }}</ReplicationGroupId>
+  <SnapshotRetentionLimit>{{ cache_cluster.snapshot_retention_limit }}</SnapshotRetentionLimit>
+  <SnapshotWindow>{{ cache_cluster.snapshot_window }}</SnapshotWindow>
+  {% endif %}
+  <AuthTokenEnabled>true</AuthTokenEnabled>
+  <AuthTokenLastModifiedDate>{{ cache_cluster.cache_cluster_create_time }}</AuthTokenLastModifiedDate>
+  <TransitEncryptionEnabled>{{ cache_cluster.transit_encryption_enabled }}</TransitEncryptionEnabled>
+  <AtRestEncryptionEnabled>true</AtRestEncryptionEnabled>
+  <ARN>{{ cache_cluster.arn }}</ARN>
+  <ReplicationGroupLogDeliveryEnabled>true</ReplicationGroupLogDeliveryEnabled>
+  <LogDeliveryConfigurations>
+  {% for log_delivery_configuration in cache_cluster.log_delivery_configurations %}
+      <LogType>{{ log_delivery_configuration.LogType }}</LogType>
+      <DestinationType>{{ log_delivery_configuration.DestinationType }}</DestinationType>
+      <DestinationDetails>
+        <CloudWatchLogsDetails>
+          <LogGroup>{{ log_delivery_configuration.LogGroup }}</LogGroup>
+        </CloudWatchLogsDetails>
+        <KinesisFirehoseDetails>
+          <DeliveryStream>{{ log_delivery_configuration.DeliveryStream }}</DeliveryStream>
+        </KinesisFirehoseDetails>
+      </DestinationDetails>
+      <LogFormat>{{ log_delivery_configuration.LogFormat }}</LogFormat>
+      <Status>active</Status>
+      <Message></Message>
+  {% endfor %}
+  </LogDeliveryConfigurations>
+  <NetworkType>{{ cache_cluster.network_type }}</NetworkType>
+  <IpDiscovery>{{ cache_cluster.ip_discovery }}</IpDiscovery>
+  <TransitEncryptionMode>preferred</TransitEncryptionMode>
+</CacheCluster>
+  </CreateCacheClusterResult>
+</CreateCacheClusterResponse>"""
+
+DESCRIBE_CACHE_CLUSTERS_TEMPLATE = """<DescribeCacheClustersResponse xmlns="http://elasticache.amazonaws.com/doc/2015-02-02/">
+  <ResponseMetadata>
+    <RequestId>1549581b-12b7-11e3-895e-1334aEXAMPLE</RequestId>
+  </ResponseMetadata>
+  <DescribeCacheClustersResult>
+    <Marker>{{ marker }}</Marker>
+    <CacheClusters>
+{% for cache_cluster in cache_clusters %}
+      <member>
+        <CacheClusterId>{{ cache_cluster.cache_cluster_id }}</CacheClusterId>
+        <ConfigurationEndpoint>{{ cache_cluster.configuration_endpoint }}</ConfigurationEndpoint>
+        <ClientDownloadLandingPage>{{ cache_cluster.client_download_landing_page }}</ClientDownloadLandingPage>
+        <CacheNodeType>{{ cache_cluster.cache_node_type }}</CacheNodeType>
+        <Engine>{{ cache_cluster.engine }}</Engine>
+        <EngineVersion>{{ cache_cluster.engine_version }}</EngineVersion>
+        <CacheClusterStatus>{{ cache_cluster.cache_cluster_status }}</CacheClusterStatus>
+        <NumCacheNodes>{{ cache_cluster.num_cache_nodes }}</NumCacheNodes>
+        <PreferredAvailabilityZone>{{ cache_cluster.preferred_availability_zone }}</PreferredAvailabilityZone>
+        <PreferredOutpostArn>{{ cache_cluster.preferred_outpost_arn }}</PreferredOutpostArn>
+        <CacheClusterCreateTime>{{ cache_cluster.cache_cluster_create_time }}</CacheClusterCreateTime>
+        <PreferredMaintenanceWindow>{{ cache_cluster.preferred_maintenance_window }}</PreferredMaintenanceWindow>
+        <PendingModifiedValues>{{ cache_cluster.pending_modified_values }}</PendingModifiedValues>
+        <NotificationConfiguration>{{ cache_cluster.notification_configuration }}</NotificationConfiguration>
+        <CacheSecurityGroups>
+{% for cache_security_group in cache_cluster.cache_security_groups %}
+          <member>
+            <CacheSecurityGroupName>{{ cache_security_group.cache_security_group_name }}</CacheSecurityGroupName>
+            <Status>{{ cache_security_group.status }}</Status>
+          </member>
+{% endfor %}
+        </CacheSecurityGroups>
+        <CacheParameterGroup>{{ cache_cluster.cache_parameter_group }}</CacheParameterGroup>
+        <CacheSubnetGroupName>{{ cache_cluster.cache_subnet_group_name }}</CacheSubnetGroupName>
+        <CacheNodes>
+{% for cache_node in cache_cluster.cache_nodes %}
+          <member>
+            <CacheNodeId>{{ cache_node.cache_node_id }}</CacheNodeId>
+            <CacheNodeStatus>{{ cache_node.cache_node_status }}</CacheNodeStatus>
+            <CacheNodeCreateTime>{{ cache_node.cache_node_create_time }}</CacheNodeCreateTime>
+            <Endpoint>{{ cache_node.endpoint }}</Endpoint>
+            <ParameterGroupStatus>{{ cache_node.parameter_group_status }}</ParameterGroupStatus>
+            <SourceCacheNodeId>{{ cache_node.source_cache_node_id }}</SourceCacheNodeId>
+            <CustomerAvailabilityZone>{{ cache_node.customer_availability_zone }}</CustomerAvailabilityZone>
+            <CustomerOutpostArn>{{ cache_node.customer_outpost_arn }}</CustomerOutpostArn>
+          </member>
+{% endfor %}
+        </CacheNodes>
+        <AutoMinorVersionUpgrade>{{ cache_cluster.auto_minor_version_upgrade }}</AutoMinorVersionUpgrade>
+        <SecurityGroups>
+{% for security_group in cache_cluster.security_groups %}
+          <member>
+            <SecurityGroupId>{{ security_group.security_group_id }}</SecurityGroupId>
+            <Status>{{ security_group.status }}</Status>
+          </member>
+{% endfor %}
+        </SecurityGroups>
+        <ReplicationGroupId>{{ cache_cluster.replication_group_id }}</ReplicationGroupId>
+        <SnapshotRetentionLimit>{{ cache_cluster.snapshot_retention_limit }}</SnapshotRetentionLimit>
+        <SnapshotWindow>{{ cache_cluster.snapshot_window }}</SnapshotWindow>
+        <AuthTokenEnabled>{{ cache_cluster.auth_token_enabled }}</AuthTokenEnabled>
+        <AuthTokenLastModifiedDate>{{ cache_cluster.auth_token_last_modified_date }}</AuthTokenLastModifiedDate>
+        <TransitEncryptionEnabled>{{ cache_cluster.transit_encryption_enabled }}</TransitEncryptionEnabled>
+        <AtRestEncryptionEnabled>{{ cache_cluster.at_rest_encryption_enabled }}</AtRestEncryptionEnabled>
+        <ARN>{{ cache_cluster.arn }}</ARN>
+        <ReplicationGroupLogDeliveryEnabled>{{ cache_cluster.replication_group_log_delivery_enabled }}</ReplicationGroupLogDeliveryEnabled>
+        <LogDeliveryConfigurations>
+{% for log_delivery_configuration in cache_cluster.log_delivery_configurations %}
+          <member>
+            <LogType>{{ log_delivery_configuration.log_type }}</LogType>
+            <DestinationType>{{ log_delivery_configuration.destination_type }}</DestinationType>
+            <DestinationDetails>{{ log_delivery_configuration.destination_details }}</DestinationDetails>
+            <LogFormat>{{ log_delivery_configuration.log_format }}</LogFormat>
+            <Status>{{ log_delivery_configuration.status }}</Status>
+            <Message>{{ log_delivery_configuration.message }}</Message>
+          </member>
+{% endfor %}
+        </LogDeliveryConfigurations>
+        <NetworkType>{{ cache_cluster.network_type }}</NetworkType>
+        <IpDiscovery>{{ cache_cluster.ip_discovery }}</IpDiscovery>
+        <TransitEncryptionMode>{{ cache_cluster.transit_encryption_mode }}</TransitEncryptionMode>
+      </member>
+{% endfor %}
+    </CacheClusters>
+  </DescribeCacheClustersResult>
+</DescribeCacheClustersResponse>"""
+
+DELETE_CACHE_CLUSTER_TEMPLATE = """<DeleteCacheClusterResponse xmlns="http://elasticache.amazonaws.com/doc/2015-02-02/">
+  <ResponseMetadata>
+    <RequestId>1549581b-12b7-11e3-895e-1334aEXAMPLE</RequestId>
+  </ResponseMetadata>
+  <DeleteCacheClusterResult>
+    <CacheCluster>
+  <CacheClusterId>{{ cache_cluster.cache_cluster_id }}</CacheClusterId>
+  <ConfigurationEndpoint>
+    <Address>example.cache.amazonaws.com</Address>
+    <Port>{{ cache_cluster.port }}</Port>
+  </ConfigurationEndpoint>
+  <ClientDownloadLandingPage></ClientDownloadLandingPage>
+  <CacheNodeType>{{ cache_cluster.cache_node_type }}</CacheNodeType>
+  <Engine>{{ cache_cluster.engine }}</Engine>
+  <EngineVersion>{{ cache_cluster.engine_version }}</EngineVersion>
+  <CacheClusterStatus>available</CacheClusterStatus>
+  <NumCacheNodes>{{ cache_cluster.num_cache_nodes }}</NumCacheNodes>
+  <PreferredAvailabilityZone>{{ cache_cluster.preferred_availability_zone }}</PreferredAvailabilityZone>
+  <PreferredOutpostArn>{{ cache_cluster.preferred_outpost_arn }}</PreferredOutpostArn>
+  <CacheClusterCreateTime>{{ cache_cluster.cache_cluster_create_time }}</CacheClusterCreateTime>
+  <PreferredMaintenanceWindow>{{ cache_cluster.preferred_maintenance_window }}</PreferredMaintenanceWindow>
+  {% if cache_cluster.cache_node_ids_to_remove != [] %}
+  <PendingModifiedValues>
+    <NumCacheNodes>{{ cache_cluster.num_cache_nodes }}</NumCacheNodes>
+    {% for cache_node_id_to_remove in cache_cluster.cache_node_ids_to_remove %}
+    <CacheNodeIdsToRemove>{{ cache_node_id_to_remove }}</CacheNodeIdsToRemove>
+    {% endfor %}
+    <EngineVersion>{{ cache_cluster.engine_version }}</EngineVersion>
+    <CacheNodeType>{{ cache_cluster.cache_node_type }}</CacheNodeType>
+    <AuthTokenStatus>SETTING</AuthTokenStatus>
+    <LogDeliveryConfigurations>
+    {% for log_delivery_configuration in cache_cluster.log_delivery_configurations %}
+      <LogType>{{ log_delivery_configuration.LogType }}</LogType>
+      <DestinationType>{{ log_delivery_configuration.DestinationType }}</DestinationType>
+      <DestinationDetails>
+        <CloudWatchLogsDetails>
+          <LogGroup>{{ log_delivery_configuration.LogGroup }}</LogGroup>
+        </CloudWatchLogsDetails>
+        <KinesisFirehoseDetails>
+          <DeliveryStream>{{ log_delivery_configuration.DeliveryStream }}</DeliveryStream>
+        </KinesisFirehoseDetails>
+      </DestinationDetails>
+      <LogFormat>{{ log_delivery_configuration.LogFormat }}</LogFormat>
+      {% endfor %}
+    </LogDeliveryConfigurations>
+    <TransitEncryptionEnabled>{{ cache_cluster.transit_encryption_enabled }}</TransitEncryptionEnabled>
+    <TransitEncryptionMode>preferred</TransitEncryptionMode>
+  </PendingModifiedValues>
+  {% endif %}
+  <NotificationConfiguration>
+    <TopicArn>{{ cache_cluster.notification_topic_arn }}</TopicArn>
+    <TopicStatus>active</TopicStatus>
+  </NotificationConfiguration>
+  <CacheSecurityGroups>
+  {% for cache_security_group_name in cache_cluster.cache_security_group_names %}
+    <CacheSecurityGroupName>{{ cache_security_group_name }}</CacheSecurityGroupName>
+    {% endfor %}
+    <Status>active</Status>
+  </CacheSecurityGroups>
+  <CacheParameterGroup>
+    <CacheParameterGroupName>{{ cache_cluster.cache_parameter_group_name }}</CacheParameterGroupName>
+    <ParameterApplyStatus>active</ParameterApplyStatus>
+    {% for cache_node_id_to_reboot in cache_cluster.cache_node_ids_to_reboot %}
+    <CacheNodeIdsToReboot>
+    {{ cache_node_id_to_reboot }}
+    </CacheNodeIdsToReboot>
+    {% endfor %}
+  </CacheParameterGroup>
+  <CacheSubnetGroupName>{{ cache_cluster.cache_subnet_group_name }}</CacheSubnetGroupName>
+  <CacheNodes>
+    <CacheNodeId>{{ cache_cluster.cache_node_id }}</CacheNodeId>
+    <CacheNodeStatus>{{ cache_cluster.cache_node_status }}</CacheNodeStatus>
+    <CacheNodeCreateTime>{{ cache_cluster.cache_cluster_create_time }}</CacheNodeCreateTime>
+    <Endpoint>
+      <Address>{{ cache_cluster.address }}</Address>
+      <Port>{{ cache_cluster.port }}</Port>
+    </Endpoint>
+    <ParameterGroupStatus>active</ParameterGroupStatus>
+    <SourceCacheNodeId>{{ cache_cluster.cache_node_id }}</SourceCacheNodeId>
+    <CustomerAvailabilityZone>{{ cache_cluster.preferred_availability_zone }}</CustomerAvailabilityZone>
+    <CustomerOutpostArn>{{ cache_cluster.preferred_output_arn }}</CustomerOutpostArn>
+  </CacheNodes>
+  <AutoMinorVersionUpgrade>{{ cache_cluster.auto_minor_version_upgrade }}</AutoMinorVersionUpgrade>
+  <SecurityGroups>
+  {% for security_group_id in cache_cluster.security_group_ids %}
+    <SecurityGroupId>{{ security_group_id }}</SecurityGroupId>
+    <Status>active</Status>
+    {% endfor %}
+  </SecurityGroups>
+  {% if cache_cluster.engine == "redis" %}
+  <ReplicationGroupId>{{ cache_cluster.replication_group_id }}</ReplicationGroupId>
+  <SnapshotRetentionLimit>{{ cache_cluster.snapshot_retention_limit }}</SnapshotRetentionLimit>
+  <SnapshotWindow>{{ cache_cluster.snapshot_window }}</SnapshotWindow>
+  {% endif %}
+  <AuthTokenEnabled>true</AuthTokenEnabled>
+  <AuthTokenLastModifiedDate>{{ cache_cluster.cache_cluster_create_time }}</AuthTokenLastModifiedDate>
+  <TransitEncryptionEnabled>{{ cache_cluster.transit_encryption_enabled }}</TransitEncryptionEnabled>
+  <AtRestEncryptionEnabled>true</AtRestEncryptionEnabled>
+  <ARN>{{ cache_cluster.arn }}</ARN>
+  <ReplicationGroupLogDeliveryEnabled>true</ReplicationGroupLogDeliveryEnabled>
+  <LogDeliveryConfigurations>
+  {% for log_delivery_configuration in cache_cluster.log_delivery_configurations %}
+      <LogType>{{ log_delivery_configuration.LogType }}</LogType>
+      <DestinationType>{{ log_delivery_configuration.DestinationType }}</DestinationType>
+      <DestinationDetails>
+        <CloudWatchLogsDetails>
+          <LogGroup>{{ log_delivery_configuration.LogGroup }}</LogGroup>
+        </CloudWatchLogsDetails>
+        <KinesisFirehoseDetails>
+          <DeliveryStream>{{ log_delivery_configuration.DeliveryStream }}</DeliveryStream>
+        </KinesisFirehoseDetails>
+      </DestinationDetails>
+      <LogFormat>{{ log_delivery_configuration.LogFormat }}</LogFormat>
+      <Status>active</Status>
+      <Message></Message>
+  {% endfor %}
+  </LogDeliveryConfigurations>
+  <NetworkType>{{ cache_cluster.network_type }}</NetworkType>
+  <IpDiscovery>{{ cache_cluster.ip_discovery }}</IpDiscovery>
+  <TransitEncryptionMode>preferred</TransitEncryptionMode>
+</CacheCluster>
+  </DeleteCacheClusterResult>
+</DeleteCacheClusterResponse>"""
diff --git a/moto/sagemaker/models.py b/moto/sagemaker/models.py
index 726eab895..7d1419587 100644
--- a/moto/sagemaker/models.py
+++ b/moto/sagemaker/models.py
@@ -29,35 +29,36 @@ PAGINATION_MODEL = {
         "limit_key": "MaxResults",
         "limit_default": 100,
         "unique_attribute": "experiment_arn",
-        "fail_on_invalid_token": True,
     },
     "list_trials": {
         "input_token": "NextToken",
         "limit_key": "MaxResults",
         "limit_default": 100,
         "unique_attribute": "trial_arn",
-        "fail_on_invalid_token": True,
     },
     "list_trial_components": {
         "input_token": "NextToken",
         "limit_key": "MaxResults",
         "limit_default": 100,
         "unique_attribute": "trial_component_arn",
-        "fail_on_invalid_token": True,
     },
     "list_tags": {
         "input_token": "NextToken",
         "limit_key": "MaxResults",
         "limit_default": 50,
         "unique_attribute": "Key",
-        "fail_on_invalid_token": True,
     },
     "list_model_packages": {
         "input_token": "next_token",
         "limit_key": "max_results",
         "limit_default": 100,
         "unique_attribute": "ModelPackageArn",
-        "fail_on_invalid_token": True,
+    },
+    "list_notebook_instances": {
+        "input_token": "next_token",
+        "limit_key": "max_results",
+        "limit_default": 100,
+        "unique_attribute": "arn",
     },
 }
 
@@ -1132,7 +1133,7 @@ class FakeSagemakerNotebookInstance(CloudFormationModel):
         self.default_code_repository = default_code_repository
         self.additional_code_repositories = additional_code_repositories
         self.root_access = root_access
-        self.status: Optional[str] = None
+        self.status = "Pending"
         self.creation_time = self.last_modified_time = datetime.now()
         self.arn = arn_formatter(
             "notebook-instance", notebook_instance_name, account_id, region_name
@@ -1289,6 +1290,29 @@ class FakeSagemakerNotebookInstance(CloudFormationModel):
         backend.stop_notebook_instance(notebook_instance_name)
         backend.delete_notebook_instance(notebook_instance_name)
 
+    def to_dict(self) -> Dict[str, Any]:
+        return {
+            "NotebookInstanceArn": self.arn,
+            "NotebookInstanceName": self.notebook_instance_name,
+            "NotebookInstanceStatus": self.status,
+            "Url": self.url,
+            "InstanceType": self.instance_type,
+            "SubnetId": self.subnet_id,
+            "SecurityGroups": self.security_group_ids,
+            "RoleArn": self.role_arn,
+            "KmsKeyId": self.kms_key_id,
+            # ToDo: NetworkInterfaceId
+            "LastModifiedTime": str(self.last_modified_time),
+            "CreationTime": str(self.creation_time),
+            "NotebookInstanceLifecycleConfigName": self.lifecycle_config_name,
+            "DirectInternetAccess": self.direct_internet_access,
+            "VolumeSizeInGB": self.volume_size_in_gb,
+            "AcceleratorTypes": self.accelerator_types,
+            "DefaultCodeRepository": self.default_code_repository,
+            "AdditionalCodeRepositories": self.additional_code_repositories,
+            "RootAccess": self.root_access,
+        }
+
 
 class FakeSageMakerNotebookInstanceLifecycleConfig(BaseObject, CloudFormationModel):
     def __init__(
@@ -1942,6 +1966,38 @@ class SageMakerModelBackend(BaseBackend):
             raise ValidationError(message=message)
         del self.notebook_instances[notebook_instance_name]
 
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    def list_notebook_instances(
+        self,
+        sort_by: str,
+        sort_order: str,
+        name_contains: Optional[str],
+        status: Optional[str],
+    ) -> Iterable[FakeSagemakerNotebookInstance]:
+        """
+        The following parameters are not yet implemented:
+        CreationTimeBefore, CreationTimeAfter, LastModifiedTimeBefore, LastModifiedTimeAfter, NotebookInstanceLifecycleConfigNameContains, DefaultCodeRepositoryContains, AdditionalCodeRepositoryEquals
+        """
+        instances = list(self.notebook_instances.values())
+        if name_contains:
+            instances = [
+                i for i in instances if name_contains in i.notebook_instance_name
+            ]
+        if status:
+            instances = [i for i in instances if i.status == status]
+        reverse = sort_order == "Descending"
+        if sort_by == "Name":
+            instances = sorted(
+                instances, key=lambda x: x.notebook_instance_name, reverse=reverse
+            )
+        if sort_by == "CreationTime":
+            instances = sorted(
+                instances, key=lambda x: x.creation_time, reverse=reverse
+            )
+        if sort_by == "Status":
+            instances = sorted(instances, key=lambda x: x.status, reverse=reverse)
+        return instances
+
     def create_notebook_instance_lifecycle_config(
         self,
         notebook_instance_lifecycle_config_name: str,
diff --git a/moto/sagemaker/responses.py b/moto/sagemaker/responses.py
index d04184dfc..b40f8b537 100644
--- a/moto/sagemaker/responses.py
+++ b/moto/sagemaker/responses.py
@@ -73,33 +73,12 @@ class SageMakerResponse(BaseResponse):
         return 200, {}, json.dumps({"NotebookInstanceArn": sagemaker_notebook.arn})
 
     @amzn_request_id
-    def describe_notebook_instance(self) -> TYPE_RESPONSE:
+    def describe_notebook_instance(self) -> str:
         notebook_instance_name = self._get_param("NotebookInstanceName")
         notebook_instance = self.sagemaker_backend.get_notebook_instance(
             notebook_instance_name
         )
-        response = {
-            "NotebookInstanceArn": notebook_instance.arn,
-            "NotebookInstanceName": notebook_instance.notebook_instance_name,
-            "NotebookInstanceStatus": notebook_instance.status,
-            "Url": notebook_instance.url,
-            "InstanceType": notebook_instance.instance_type,
-            "SubnetId": notebook_instance.subnet_id,
-            "SecurityGroups": notebook_instance.security_group_ids,
-            "RoleArn": notebook_instance.role_arn,
-            "KmsKeyId": notebook_instance.kms_key_id,
-            # ToDo: NetworkInterfaceId
-            "LastModifiedTime": str(notebook_instance.last_modified_time),
-            "CreationTime": str(notebook_instance.creation_time),
-            "NotebookInstanceLifecycleConfigName": notebook_instance.lifecycle_config_name,
-            "DirectInternetAccess": notebook_instance.direct_internet_access,
-            "VolumeSizeInGB": notebook_instance.volume_size_in_gb,
-            "AcceleratorTypes": notebook_instance.accelerator_types,
-            "DefaultCodeRepository": notebook_instance.default_code_repository,
-            "AdditionalCodeRepositories": notebook_instance.additional_code_repositories,
-            "RootAccess": notebook_instance.root_access,
-        }
-        return 200, {}, json.dumps(response)
+        return json.dumps(notebook_instance.to_dict())
 
     @amzn_request_id
     def start_notebook_instance(self) -> TYPE_RESPONSE:
@@ -119,6 +98,29 @@ class SageMakerResponse(BaseResponse):
         self.sagemaker_backend.delete_notebook_instance(notebook_instance_name)
         return 200, {}, json.dumps("{}")
 
+    @amzn_request_id
+    def list_notebook_instances(self) -> str:
+        sort_by = self._get_param("SortBy", "Name")
+        sort_order = self._get_param("SortOrder", "Ascending")
+        name_contains = self._get_param("NameContains")
+        status = self._get_param("StatusEquals")
+        max_results = self._get_param("MaxResults")
+        next_token = self._get_param("NextToken")
+        instances, next_token = self.sagemaker_backend.list_notebook_instances(
+            sort_by=sort_by,
+            sort_order=sort_order,
+            name_contains=name_contains,
+            status=status,
+            max_results=max_results,
+            next_token=next_token,
+        )
+        return json.dumps(
+            {
+                "NotebookInstances": [i.to_dict() for i in instances],
+                "NextToken": next_token,
+            }
+        )
+
     @amzn_request_id
     def list_tags(self) -> TYPE_RESPONSE:
         arn = self._get_param("ResourceArn")
diff --git a/moto/ses/models.py b/moto/ses/models.py
index 39deca5f6..04d5bcd53 100644
--- a/moto/ses/models.py
+++ b/moto/ses/models.py
@@ -165,7 +165,7 @@ class SESBackend(BaseBackend):
             return True
         if address in self.email_addresses:
             return True
-        _, host = address.split("@", 1)
+        host = address.split("@", 1)[-1]
         return host in self.domains
 
     def verify_email_identity(self, address: str) -> None:
@@ -572,17 +572,17 @@ class SESBackend(BaseBackend):
         mail_from_domain: Optional[str] = None,
         behavior_on_mx_failure: Optional[str] = None,
     ) -> None:
-        if identity not in (self.domains + self.addresses):
+        if not self._is_verified_address(identity):
             raise InvalidParameterValue(f"Identity '{identity}' does not exist.")
 
         if mail_from_domain is None:
             self.identity_mail_from_domains.pop(identity)
             return
 
-        if not mail_from_domain.endswith(identity):
+        if not mail_from_domain.endswith(identity.split("@")[-1]):
             raise InvalidParameterValue(
                 f"Provided MAIL-FROM domain '{mail_from_domain}' is not subdomain of "
-                f"the domain of the identity '{identity}'."
+                f"the domain of the identity '{identity.split('@')[-1]}'."
             )
 
         if behavior_on_mx_failure not in (None, "RejectMessage", "UseDefaultValue"):


Test patch:
diff --git a/other_langs/tests_java/pom.xml b/other_langs/tests_java/pom.xml
index 09d00f7d0..560a3a917 100644
--- a/other_langs/tests_java/pom.xml
+++ b/other_langs/tests_java/pom.xml
@@ -22,7 +22,7 @@
     <dependency>
       <groupId>software.amazon.awssdk</groupId>
       <artifactId>bom</artifactId>
-      <version>2.20.117</version>
+      <version>2.20.139</version>
       <type>pom</type>
       <scope>import</scope>
     </dependency>
diff --git a/tests/test_elasticache/test_elasticache.py b/tests/test_elasticache/test_elasticache.py
index ecc18679a..e119fe1a4 100644
--- a/tests/test_elasticache/test_elasticache.py
+++ b/tests/test_elasticache/test_elasticache.py
@@ -1,10 +1,11 @@
 import boto3
 import pytest
-
 from botocore.exceptions import ClientError
-from moto import mock_elasticache
 from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
 
+from moto import mock_elasticache
+
+
 # See our Development Tips on writing tests for hints on how to write good tests:
 # http://docs.getmoto.org/en/latest/docs/contributing/development_tips/tests.html
 
@@ -208,3 +209,212 @@ def test_describe_users_unknown_userid():
     err = exc.value.response["Error"]
     assert err["Code"] == "UserNotFound"
     assert err["Message"] == "User unknown not found."
+
+
+@mock_elasticache
+def test_create_redis_cache_cluster():
+    client = boto3.client("elasticache", region_name="us-east-2")
+
+    test_redis_cache_cluster_exist = False
+
+    cache_cluster_id = "test-cache-cluster"
+    cache_cluster_engine = "redis"
+    cache_cluster_num_cache_nodes = 5
+
+    resp = client.create_cache_cluster(
+        CacheClusterId=cache_cluster_id,
+        Engine=cache_cluster_engine,
+        NumCacheNodes=cache_cluster_num_cache_nodes,
+    )
+    cache_cluster = resp["CacheCluster"]
+
+    if cache_cluster["CacheClusterId"] == cache_cluster_id:
+        if cache_cluster["Engine"] == cache_cluster_engine:
+            if cache_cluster["NumCacheNodes"] == 1:
+                test_redis_cache_cluster_exist = True
+
+    assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200
+    assert test_redis_cache_cluster_exist
+
+
+@mock_elasticache
+def test_create_memcached_cache_cluster():
+    client = boto3.client("elasticache", region_name="us-east-2")
+
+    test_memcached_cache_cluster_exist = False
+
+    cache_cluster_id = "test-cache-cluster"
+    cache_cluster_engine = "memcached"
+    cache_cluster_num_cache_nodes = 5
+
+    resp = client.create_cache_cluster(
+        CacheClusterId=cache_cluster_id,
+        Engine=cache_cluster_engine,
+        NumCacheNodes=cache_cluster_num_cache_nodes,
+    )
+    cache_cluster = resp["CacheCluster"]
+
+    if cache_cluster["CacheClusterId"] == cache_cluster_id:
+        if cache_cluster["Engine"] == cache_cluster_engine:
+            if cache_cluster["NumCacheNodes"] == 5:
+                test_memcached_cache_cluster_exist = True
+
+    assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200
+    assert test_memcached_cache_cluster_exist
+
+
+@mock_elasticache
+def test_create_duplicate_cache_cluster():
+    client = boto3.client("elasticache", region_name="us-east-2")
+
+    cache_cluster_id = "test-cache-cluster"
+    cache_cluster_engine = "memcached"
+    cache_cluster_num_cache_nodes = 5
+
+    client.create_cache_cluster(
+        CacheClusterId=cache_cluster_id,
+        Engine=cache_cluster_engine,
+        NumCacheNodes=cache_cluster_num_cache_nodes,
+    )
+
+    with pytest.raises(ClientError) as exc:
+        client.create_cache_cluster(
+            CacheClusterId=cache_cluster_id,
+            Engine=cache_cluster_engine,
+            NumCacheNodes=cache_cluster_num_cache_nodes,
+        )
+    err = exc.value.response["Error"]
+    assert err["Code"] == "CacheClusterAlreadyExists"
+    assert err["Message"] == f"Cache cluster {cache_cluster_id} already exists."
+
+
+@mock_elasticache
+def test_describe_all_cache_clusters():
+    client = boto3.client("elasticache", region_name="us-east-2")
+
+    test_memcached_cache_cluster_exist = False
+
+    cache_cluster_id = "test-cache-cluster"
+    cache_cluster_engine = "memcached"
+    cache_cluster_num_cache_nodes = 5
+
+    client.create_cache_cluster(
+        CacheClusterId=cache_cluster_id,
+        Engine=cache_cluster_engine,
+        NumCacheNodes=cache_cluster_num_cache_nodes,
+    )
+
+    resp = client.describe_cache_clusters()
+
+    cache_clusters = resp["CacheClusters"]
+
+    for cache_cluster in cache_clusters:
+        if cache_cluster["CacheClusterId"] == cache_cluster_id:
+            test_memcached_cache_cluster_exist = True
+
+    assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200
+    assert test_memcached_cache_cluster_exist
+
+
+@mock_elasticache
+def test_describe_specific_cache_clusters():
+    client = boto3.client("elasticache", region_name="us-east-2")
+
+    test_memcached_cache_cluster_exist = False
+
+    cache_cluster_id = "test-cache-cluster"
+    cache_cluster_engine = "memcached"
+    cache_cluster_num_cache_nodes = 5
+
+    client.create_cache_cluster(
+        CacheClusterId=cache_cluster_id,
+        Engine=cache_cluster_engine,
+        NumCacheNodes=cache_cluster_num_cache_nodes,
+    )
+
+    client.create_cache_cluster(
+        CacheClusterId="test-cache-cluster-2",
+        Engine=cache_cluster_engine,
+        NumCacheNodes=cache_cluster_num_cache_nodes,
+    )
+
+    resp = client.describe_cache_clusters(CacheClusterId=cache_cluster_id)
+
+    cache_clusters = resp["CacheClusters"]
+
+    for cache_cluster in cache_clusters:
+        if cache_cluster["CacheClusterId"] == cache_cluster_id:
+            test_memcached_cache_cluster_exist = True
+
+    assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200
+    assert test_memcached_cache_cluster_exist
+
+
+@mock_elasticache
+def test_describe_unknown_cache_cluster():
+    client = boto3.client("elasticache", region_name="us-east-2")
+
+    cache_cluster_id = "test-cache-cluster"
+    cache_cluster_id_unknown = "unknown-cache-cluster"
+    cache_cluster_engine = "memcached"
+    cache_cluster_num_cache_nodes = 5
+
+    client.create_cache_cluster(
+        CacheClusterId=cache_cluster_id,
+        Engine=cache_cluster_engine,
+        NumCacheNodes=cache_cluster_num_cache_nodes,
+    )
+
+    with pytest.raises(ClientError) as exc:
+        client.describe_cache_clusters(
+            CacheClusterId=cache_cluster_id_unknown,
+        )
+    err = exc.value.response["Error"]
+    assert err["Code"] == "CacheClusterNotFound"
+    assert err["Message"] == f"Cache cluster {cache_cluster_id_unknown} not found."
+
+
+@mock_elasticache
+def test_delete_cache_cluster():
+    client = boto3.client("elasticache", region_name="us-east-2")
+
+    cache_cluster_id = "test-cache-cluster"
+    cache_cluster_engine = "memcached"
+    cache_cluster_num_cache_nodes = 5
+
+    client.create_cache_cluster(
+        CacheClusterId=cache_cluster_id,
+        Engine=cache_cluster_engine,
+        NumCacheNodes=cache_cluster_num_cache_nodes,
+    )
+
+    client.delete_cache_cluster(CacheClusterId=cache_cluster_id)
+
+    resp = client.describe_cache_clusters(CacheClusterId=cache_cluster_id)
+
+    assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200
+    assert resp["CacheClusters"][0]["CacheClusterStatus"] == "deleting"
+
+
+@mock_elasticache
+def test_delete_unknown_cache_cluster():
+    client = boto3.client("elasticache", region_name="us-east-2")
+
+    cache_cluster_id = "test-cache-cluster"
+    cache_cluster_id_unknown = "unknown-cache-cluster"
+    cache_cluster_engine = "memcached"
+    cache_cluster_num_cache_nodes = 5
+
+    client.create_cache_cluster(
+        CacheClusterId=cache_cluster_id,
+        Engine=cache_cluster_engine,
+        NumCacheNodes=cache_cluster_num_cache_nodes,
+    )
+
+    with pytest.raises(ClientError) as exc:
+        client.delete_cache_cluster(
+            CacheClusterId=cache_cluster_id_unknown,
+        )
+    err = exc.value.response["Error"]
+    assert err["Code"] == "CacheClusterNotFound"
+    assert err["Message"] == f"Cache cluster {cache_cluster_id_unknown} not found."
diff --git a/tests/test_sagemaker/test_sagemaker_notebooks.py b/tests/test_sagemaker/test_sagemaker_notebooks.py
index 01556ef42..f1d7e015a 100644
--- a/tests/test_sagemaker/test_sagemaker_notebooks.py
+++ b/tests/test_sagemaker/test_sagemaker_notebooks.py
@@ -26,7 +26,7 @@ FAKE_NAME_PARAM = "MyNotebookInstance"
 FAKE_INSTANCE_TYPE_PARAM = "ml.t2.medium"
 
 
-@pytest.fixture(name="sagemaker_client")
+@pytest.fixture(name="client")
 def fixture_sagemaker_client():
     with mock_sagemaker():
         yield boto3.client("sagemaker", region_name=TEST_REGION_NAME)
@@ -43,19 +43,17 @@ def _get_notebook_instance_lifecycle_arn(lifecycle_name):
     )
 
 
-def test_create_notebook_instance_minimal_params(sagemaker_client):
+def test_create_notebook_instance_minimal_params(client):
     args = {
         "NotebookInstanceName": FAKE_NAME_PARAM,
         "InstanceType": FAKE_INSTANCE_TYPE_PARAM,
         "RoleArn": FAKE_ROLE_ARN,
     }
-    resp = sagemaker_client.create_notebook_instance(**args)
+    resp = client.create_notebook_instance(**args)
     expected_notebook_arn = _get_notebook_instance_arn(FAKE_NAME_PARAM)
     assert resp["NotebookInstanceArn"] == expected_notebook_arn
 
-    resp = sagemaker_client.describe_notebook_instance(
-        NotebookInstanceName=FAKE_NAME_PARAM
-    )
+    resp = client.describe_notebook_instance(NotebookInstanceName=FAKE_NAME_PARAM)
     assert resp["NotebookInstanceArn"] == expected_notebook_arn
     assert resp["NotebookInstanceName"] == FAKE_NAME_PARAM
     assert resp["NotebookInstanceStatus"] == "InService"
@@ -71,7 +69,7 @@ def test_create_notebook_instance_minimal_params(sagemaker_client):
 #    assert resp["RootAccess"] == True     # ToDo: Not sure if this defaults...
 
 
-def test_create_notebook_instance_params(sagemaker_client):
+def test_create_notebook_instance_params(client):
     fake_direct_internet_access_param = "Enabled"
     volume_size_in_gb_param = 7
     accelerator_types_param = ["ml.eia1.medium", "ml.eia2.medium"]
@@ -93,13 +91,11 @@ def test_create_notebook_instance_params(sagemaker_client):
         "AdditionalCodeRepositories": FAKE_ADDL_CODE_REPOS,
         "RootAccess": root_access_param,
     }
-    resp = sagemaker_client.create_notebook_instance(**args)
+    resp = client.create_notebook_instance(**args)
     expected_notebook_arn = _get_notebook_instance_arn(FAKE_NAME_PARAM)
     assert resp["NotebookInstanceArn"] == expected_notebook_arn
 
-    resp = sagemaker_client.describe_notebook_instance(
-        NotebookInstanceName=FAKE_NAME_PARAM
-    )
+    resp = client.describe_notebook_instance(NotebookInstanceName=FAKE_NAME_PARAM)
     assert resp["NotebookInstanceArn"] == expected_notebook_arn
     assert resp["NotebookInstanceName"] == FAKE_NAME_PARAM
     assert resp["NotebookInstanceStatus"] == "InService"
@@ -119,11 +115,11 @@ def test_create_notebook_instance_params(sagemaker_client):
     assert resp["DefaultCodeRepository"] == FAKE_DEFAULT_CODE_REPO
     assert resp["AdditionalCodeRepositories"] == FAKE_ADDL_CODE_REPOS
 
-    resp = sagemaker_client.list_tags(ResourceArn=resp["NotebookInstanceArn"])
+    resp = client.list_tags(ResourceArn=resp["NotebookInstanceArn"])
     assert resp["Tags"] == GENERIC_TAGS_PARAM
 
 
-def test_create_notebook_instance_invalid_instance_type(sagemaker_client):
+def test_create_notebook_instance_invalid_instance_type(client):
     instance_type = "undefined_instance_type"
     args = {
         "NotebookInstanceName": "MyNotebookInstance",
@@ -131,7 +127,7 @@ def test_create_notebook_instance_invalid_instance_type(sagemaker_client):
         "RoleArn": FAKE_ROLE_ARN,
     }
     with pytest.raises(ClientError) as ex:
-        sagemaker_client.create_notebook_instance(**args)
+        client.create_notebook_instance(**args)
     assert ex.value.response["Error"]["Code"] == "ValidationException"
     expected_message = (
         f"Value '{instance_type}' at 'instanceType' failed to satisfy "
@@ -141,23 +137,21 @@ def test_create_notebook_instance_invalid_instance_type(sagemaker_client):
     assert expected_message in ex.value.response["Error"]["Message"]
 
 
-def test_notebook_instance_lifecycle(sagemaker_client):
+def test_notebook_instance_lifecycle(client):
     args = {
         "NotebookInstanceName": FAKE_NAME_PARAM,
         "InstanceType": FAKE_INSTANCE_TYPE_PARAM,
         "RoleArn": FAKE_ROLE_ARN,
     }
-    resp = sagemaker_client.create_notebook_instance(**args)
+    resp = client.create_notebook_instance(**args)
     expected_notebook_arn = _get_notebook_instance_arn(FAKE_NAME_PARAM)
     assert resp["NotebookInstanceArn"] == expected_notebook_arn
 
-    resp = sagemaker_client.describe_notebook_instance(
-        NotebookInstanceName=FAKE_NAME_PARAM
-    )
+    resp = client.describe_notebook_instance(NotebookInstanceName=FAKE_NAME_PARAM)
     notebook_instance_arn = resp["NotebookInstanceArn"]
 
     with pytest.raises(ClientError) as ex:
-        sagemaker_client.delete_notebook_instance(NotebookInstanceName=FAKE_NAME_PARAM)
+        client.delete_notebook_instance(NotebookInstanceName=FAKE_NAME_PARAM)
     assert ex.value.response["Error"]["Code"] == "ValidationException"
     expected_message = (
         f"Status (InService) not in ([Stopped, Failed]). Unable to "
@@ -165,54 +159,48 @@ def test_notebook_instance_lifecycle(sagemaker_client):
     )
     assert expected_message in ex.value.response["Error"]["Message"]
 
-    sagemaker_client.stop_notebook_instance(NotebookInstanceName=FAKE_NAME_PARAM)
+    client.stop_notebook_instance(NotebookInstanceName=FAKE_NAME_PARAM)
 
-    resp = sagemaker_client.describe_notebook_instance(
-        NotebookInstanceName=FAKE_NAME_PARAM
-    )
+    resp = client.describe_notebook_instance(NotebookInstanceName=FAKE_NAME_PARAM)
     assert resp["NotebookInstanceStatus"] == "Stopped"
 
-    sagemaker_client.start_notebook_instance(NotebookInstanceName=FAKE_NAME_PARAM)
+    client.list_notebook_instances()
 
-    resp = sagemaker_client.describe_notebook_instance(
-        NotebookInstanceName=FAKE_NAME_PARAM
-    )
+    client.start_notebook_instance(NotebookInstanceName=FAKE_NAME_PARAM)
+
+    resp = client.describe_notebook_instance(NotebookInstanceName=FAKE_NAME_PARAM)
     assert resp["NotebookInstanceStatus"] == "InService"
 
-    sagemaker_client.stop_notebook_instance(NotebookInstanceName=FAKE_NAME_PARAM)
+    client.stop_notebook_instance(NotebookInstanceName=FAKE_NAME_PARAM)
 
-    resp = sagemaker_client.describe_notebook_instance(
-        NotebookInstanceName=FAKE_NAME_PARAM
-    )
+    resp = client.describe_notebook_instance(NotebookInstanceName=FAKE_NAME_PARAM)
     assert resp["NotebookInstanceStatus"] == "Stopped"
 
-    sagemaker_client.delete_notebook_instance(NotebookInstanceName=FAKE_NAME_PARAM)
+    client.delete_notebook_instance(NotebookInstanceName=FAKE_NAME_PARAM)
 
     with pytest.raises(ClientError) as ex:
-        sagemaker_client.describe_notebook_instance(
-            NotebookInstanceName=FAKE_NAME_PARAM
-        )
+        client.describe_notebook_instance(NotebookInstanceName=FAKE_NAME_PARAM)
     assert ex.value.response["Error"]["Message"] == "RecordNotFound"
 
 
-def test_describe_nonexistent_model(sagemaker_client):
+def test_describe_nonexistent_model(client):
     with pytest.raises(ClientError) as e:
-        sagemaker_client.describe_model(ModelName="Nonexistent")
+        client.describe_model(ModelName="Nonexistent")
     assert e.value.response["Error"]["Message"].startswith("Could not find model")
 
 
-def test_notebook_instance_lifecycle_config(sagemaker_client):
+def test_notebook_instance_lifecycle_config(client):
     name = "MyLifeCycleConfig"
     on_create = [{"Content": "Create Script Line 1"}]
     on_start = [{"Content": "Start Script Line 1"}]
-    resp = sagemaker_client.create_notebook_instance_lifecycle_config(
+    resp = client.create_notebook_instance_lifecycle_config(
         NotebookInstanceLifecycleConfigName=name, OnCreate=on_create, OnStart=on_start
     )
     expected_arn = _get_notebook_instance_lifecycle_arn(name)
     assert resp["NotebookInstanceLifecycleConfigArn"] == expected_arn
 
     with pytest.raises(ClientError) as e:
-        sagemaker_client.create_notebook_instance_lifecycle_config(
+        client.create_notebook_instance_lifecycle_config(
             NotebookInstanceLifecycleConfigName=name,
             OnCreate=on_create,
             OnStart=on_start,
@@ -221,7 +209,7 @@ def test_notebook_instance_lifecycle_config(sagemaker_client):
         "Notebook Instance Lifecycle Config already exists.)"
     )
 
-    resp = sagemaker_client.describe_notebook_instance_lifecycle_config(
+    resp = client.describe_notebook_instance_lifecycle_config(
         NotebookInstanceLifecycleConfigName=name
     )
     assert resp["NotebookInstanceLifecycleConfigName"] == name
@@ -231,12 +219,12 @@ def test_notebook_instance_lifecycle_config(sagemaker_client):
     assert isinstance(resp["LastModifiedTime"], datetime.datetime)
     assert isinstance(resp["CreationTime"], datetime.datetime)
 
-    sagemaker_client.delete_notebook_instance_lifecycle_config(
+    client.delete_notebook_instance_lifecycle_config(
         NotebookInstanceLifecycleConfigName=name
     )
 
     with pytest.raises(ClientError) as e:
-        sagemaker_client.describe_notebook_instance_lifecycle_config(
+        client.describe_notebook_instance_lifecycle_config(
             NotebookInstanceLifecycleConfigName=name
         )
     assert e.value.response["Error"]["Message"].endswith(
@@ -244,7 +232,7 @@ def test_notebook_instance_lifecycle_config(sagemaker_client):
     )
 
     with pytest.raises(ClientError) as e:
-        sagemaker_client.delete_notebook_instance_lifecycle_config(
+        client.delete_notebook_instance_lifecycle_config(
             NotebookInstanceLifecycleConfigName=name
         )
     assert e.value.response["Error"]["Message"].endswith(
@@ -252,43 +240,88 @@ def test_notebook_instance_lifecycle_config(sagemaker_client):
     )
 
 
-def test_add_tags_to_notebook(sagemaker_client):
+def test_add_tags_to_notebook(client):
     args = {
         "NotebookInstanceName": FAKE_NAME_PARAM,
         "InstanceType": FAKE_INSTANCE_TYPE_PARAM,
         "RoleArn": FAKE_ROLE_ARN,
     }
-    resp = sagemaker_client.create_notebook_instance(**args)
+    resp = client.create_notebook_instance(**args)
     resource_arn = resp["NotebookInstanceArn"]
 
     tags = [
         {"Key": "myKey", "Value": "myValue"},
     ]
-    response = sagemaker_client.add_tags(ResourceArn=resource_arn, Tags=tags)
+    response = client.add_tags(ResourceArn=resource_arn, Tags=tags)
     assert response["ResponseMetadata"]["HTTPStatusCode"] == 200
 
-    response = sagemaker_client.list_tags(ResourceArn=resource_arn)
+    response = client.list_tags(ResourceArn=resource_arn)
     assert response["Tags"] == tags
 
 
-def test_delete_tags_from_notebook(sagemaker_client):
+def test_delete_tags_from_notebook(client):
     args = {
         "NotebookInstanceName": FAKE_NAME_PARAM,
         "InstanceType": FAKE_INSTANCE_TYPE_PARAM,
         "RoleArn": FAKE_ROLE_ARN,
     }
-    resp = sagemaker_client.create_notebook_instance(**args)
+    resp = client.create_notebook_instance(**args)
     resource_arn = resp["NotebookInstanceArn"]
 
     tags = [
         {"Key": "myKey", "Value": "myValue"},
     ]
-    response = sagemaker_client.add_tags(ResourceArn=resource_arn, Tags=tags)
+    response = client.add_tags(ResourceArn=resource_arn, Tags=tags)
     assert response["ResponseMetadata"]["HTTPStatusCode"] == 200
 
     tag_keys = [tag["Key"] for tag in tags]
-    response = sagemaker_client.delete_tags(ResourceArn=resource_arn, TagKeys=tag_keys)
+    response = client.delete_tags(ResourceArn=resource_arn, TagKeys=tag_keys)
     assert response["ResponseMetadata"]["HTTPStatusCode"] == 200
 
-    response = sagemaker_client.list_tags(ResourceArn=resource_arn)
+    response = client.list_tags(ResourceArn=resource_arn)
     assert response["Tags"] == []
+
+
+def test_list_notebook_instances(client):
+    for i in range(3):
+        args = {
+            "NotebookInstanceName": f"Name{i}",
+            "InstanceType": FAKE_INSTANCE_TYPE_PARAM,
+            "RoleArn": FAKE_ROLE_ARN,
+        }
+        client.create_notebook_instance(**args)
+
+    client.stop_notebook_instance(NotebookInstanceName="Name1")
+
+    instances = client.list_notebook_instances()["NotebookInstances"]
+    assert [i["NotebookInstanceName"] for i in instances] == ["Name0", "Name1", "Name2"]
+
+    instances = client.list_notebook_instances(SortBy="Status")["NotebookInstances"]
+    assert [i["NotebookInstanceName"] for i in instances] == ["Name0", "Name2", "Name1"]
+
+    instances = client.list_notebook_instances(SortOrder="Descending")[
+        "NotebookInstances"
+    ]
+    assert [i["NotebookInstanceName"] for i in instances] == ["Name2", "Name1", "Name0"]
+
+    instances = client.list_notebook_instances(NameContains="1")["NotebookInstances"]
+    assert [i["NotebookInstanceName"] for i in instances] == ["Name1"]
+
+    instances = client.list_notebook_instances(StatusEquals="InService")[
+        "NotebookInstances"
+    ]
+    assert [i["NotebookInstanceName"] for i in instances] == ["Name0", "Name2"]
+
+    instances = client.list_notebook_instances(StatusEquals="Pending")[
+        "NotebookInstances"
+    ]
+    assert [i["NotebookInstanceName"] for i in instances] == []
+
+    resp = client.list_notebook_instances(MaxResults=1)
+    instances = resp["NotebookInstances"]
+    assert [i["NotebookInstanceName"] for i in instances] == ["Name0"]
+
+    resp = client.list_notebook_instances(NextToken=resp["NextToken"])
+    instances = resp["NotebookInstances"]
+    assert [i["NotebookInstanceName"] for i in instances] == ["Name1", "Name2"]
+    assert "NextToken" not in resp
diff --git a/tests/test_ses/test_ses_boto3.py b/tests/test_ses/test_ses_boto3.py
index ae80cf78d..7e04985c9 100644
--- a/tests/test_ses/test_ses_boto3.py
+++ b/tests/test_ses/test_ses_boto3.py
@@ -1472,6 +1472,22 @@ def test_set_identity_mail_from_domain():
     assert actual_attributes["BehaviorOnMXFailure"] == behaviour_on_mx_failure
     assert actual_attributes["MailFromDomainStatus"] == "Success"
 
+    # Can use email address as an identity
+    behaviour_on_mx_failure = "RejectMessage"
+    mail_from_domain = "lorem.foo.com"
+
+    conn.set_identity_mail_from_domain(
+        Identity="test@foo.com",
+        MailFromDomain=mail_from_domain,
+        BehaviorOnMXFailure=behaviour_on_mx_failure,
+    )
+
+    attributes = conn.get_identity_mail_from_domain_attributes(Identities=["foo.com"])
+    actual_attributes = attributes["MailFromDomainAttributes"]["foo.com"]
+    assert actual_attributes["MailFromDomain"] == mail_from_domain
+    assert actual_attributes["BehaviorOnMXFailure"] == behaviour_on_mx_failure
+    assert actual_attributes["MailFromDomainStatus"] == "Success"
+
     # Must unset config when MailFromDomain is null
     conn.set_identity_mail_from_domain(Identity="foo.com")