diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index dfe047a81..0f2e7c4ff 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -1843,6 +1843,77 @@
- [ ] update_nodegroup_version
+## elasticache
+
+4% implemented
+
+- [ ] add_tags_to_resource
+- [ ] authorize_cache_security_group_ingress
+- [ ] batch_apply_update_action
+- [ ] batch_stop_update_action
+- [ ] complete_migration
+- [ ] copy_snapshot
+- [ ] create_cache_cluster
+- [ ] create_cache_parameter_group
+- [ ] create_cache_security_group
+- [ ] create_cache_subnet_group
+- [ ] create_global_replication_group
+- [ ] create_replication_group
+- [ ] create_snapshot
+- [X] create_user
+- [ ] create_user_group
+- [ ] decrease_node_groups_in_global_replication_group
+- [ ] decrease_replica_count
+- [ ] delete_cache_cluster
+- [ ] delete_cache_parameter_group
+- [ ] delete_cache_security_group
+- [ ] delete_cache_subnet_group
+- [ ] delete_global_replication_group
+- [ ] delete_replication_group
+- [ ] delete_snapshot
+- [X] delete_user
+- [ ] delete_user_group
+- [ ] describe_cache_clusters
+- [ ] describe_cache_engine_versions
+- [ ] describe_cache_parameter_groups
+- [ ] describe_cache_parameters
+- [ ] describe_cache_security_groups
+- [ ] describe_cache_subnet_groups
+- [ ] describe_engine_default_parameters
+- [ ] describe_events
+- [ ] describe_global_replication_groups
+- [ ] describe_replication_groups
+- [ ] describe_reserved_cache_nodes
+- [ ] describe_reserved_cache_nodes_offerings
+- [ ] describe_service_updates
+- [ ] describe_snapshots
+- [ ] describe_update_actions
+- [ ] describe_user_groups
+- [X] describe_users
+- [ ] disassociate_global_replication_group
+- [ ] failover_global_replication_group
+- [ ] increase_node_groups_in_global_replication_group
+- [ ] increase_replica_count
+- [ ] list_allowed_node_type_modifications
+- [ ] list_tags_for_resource
+- [ ] modify_cache_cluster
+- [ ] modify_cache_parameter_group
+- [ ] modify_cache_subnet_group
+- [ ] modify_global_replication_group
+- [ ] modify_replication_group
+- [ ] modify_replication_group_shard_configuration
+- [ ] modify_user
+- [ ] modify_user_group
+- [ ] purchase_reserved_cache_nodes_offering
+- [ ] rebalance_slots_in_global_replication_group
+- [ ] reboot_cache_cluster
+- [ ] remove_tags_from_resource
+- [ ] reset_cache_parameter_group
+- [ ] revoke_cache_security_group_ingress
+- [ ] start_migration
+- [ ] test_failover
+
+
## elasticbeanstalk
12% implemented
@@ -4860,7 +4931,6 @@
- ebs
- ecr-public
- elastic-inference
-- elasticache
- es
- finspace
- finspace-data
diff --git a/docs/docs/services/elasticache.rst b/docs/docs/services/elasticache.rst
new file mode 100644
index 000000000..1d72a928b
--- /dev/null
+++ b/docs/docs/services/elasticache.rst
@@ -0,0 +1,100 @@
+.. _implementedservice_elasticache:
+
+.. |start-h3| raw:: html
+
+
+
+.. |end-h3| raw:: html
+
+
+
+===========
+elasticache
+===========
+
+.. autoclass:: moto.elasticache.models.ElastiCacheBackend
+
+|start-h3| Example usage |end-h3|
+
+.. sourcecode:: python
+
+ @mock_elasticache
+ def test_elasticache_behaviour:
+ boto3.client("elasticache")
+ ...
+
+
+
+|start-h3| Implemented features for this service |end-h3|
+
+- [ ] add_tags_to_resource
+- [ ] authorize_cache_security_group_ingress
+- [ ] batch_apply_update_action
+- [ ] batch_stop_update_action
+- [ ] complete_migration
+- [ ] copy_snapshot
+- [ ] create_cache_cluster
+- [ ] create_cache_parameter_group
+- [ ] create_cache_security_group
+- [ ] create_cache_subnet_group
+- [ ] create_global_replication_group
+- [ ] create_replication_group
+- [ ] create_snapshot
+- [X] create_user
+- [ ] create_user_group
+- [ ] decrease_node_groups_in_global_replication_group
+- [ ] decrease_replica_count
+- [ ] delete_cache_cluster
+- [ ] delete_cache_parameter_group
+- [ ] delete_cache_security_group
+- [ ] delete_cache_subnet_group
+- [ ] delete_global_replication_group
+- [ ] delete_replication_group
+- [ ] delete_snapshot
+- [X] delete_user
+- [ ] delete_user_group
+- [ ] describe_cache_clusters
+- [ ] describe_cache_engine_versions
+- [ ] describe_cache_parameter_groups
+- [ ] describe_cache_parameters
+- [ ] describe_cache_security_groups
+- [ ] describe_cache_subnet_groups
+- [ ] describe_engine_default_parameters
+- [ ] describe_events
+- [ ] describe_global_replication_groups
+- [ ] describe_replication_groups
+- [ ] describe_reserved_cache_nodes
+- [ ] describe_reserved_cache_nodes_offerings
+- [ ] describe_service_updates
+- [ ] describe_snapshots
+- [ ] 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
+- [ ] increase_node_groups_in_global_replication_group
+- [ ] increase_replica_count
+- [ ] list_allowed_node_type_modifications
+- [ ] list_tags_for_resource
+- [ ] modify_cache_cluster
+- [ ] modify_cache_parameter_group
+- [ ] modify_cache_subnet_group
+- [ ] modify_global_replication_group
+- [ ] modify_replication_group
+- [ ] modify_replication_group_shard_configuration
+- [ ] modify_user
+- [ ] modify_user_group
+- [ ] purchase_reserved_cache_nodes_offering
+- [ ] rebalance_slots_in_global_replication_group
+- [ ] reboot_cache_cluster
+- [ ] remove_tags_from_resource
+- [ ] reset_cache_parameter_group
+- [ ] revoke_cache_security_group_ingress
+- [ ] start_migration
+- [ ] test_failover
+
diff --git a/moto/__init__.py b/moto/__init__.py
index 75d7bba20..b5879e4c0 100644
--- a/moto/__init__.py
+++ b/moto/__init__.py
@@ -171,6 +171,9 @@ mock_mediastoredata = lazy_load(
mock_efs = lazy_load(".efs", "mock_efs")
mock_wafv2 = lazy_load(".wafv2", "mock_wafv2")
mock_sdb = lazy_load(".sdb", "mock_sdb")
+mock_elasticache = lazy_load(
+ ".elasticache", "mock_elasticache", boto3_name="elasticache"
+)
class MockAll(ContextDecorator):
diff --git a/moto/backend_index.py b/moto/backend_index.py
index 11702c00a..b85b7e3a2 100644
--- a/moto/backend_index.py
+++ b/moto/backend_index.py
@@ -45,6 +45,7 @@ backend_url_patterns = [
("efs", re.compile("https?://elasticfilesystem\\.(.+)\\.amazonaws.com")),
("efs", re.compile("https?://elasticfilesystem\\.amazonaws.com")),
("eks", re.compile("https?://eks\\.(.+)\\.amazonaws.com")),
+ ("elasticache", re.compile("https?://elasticache\\.(.+)\\.amazonaws\\.com")),
(
"elasticbeanstalk",
re.compile(
diff --git a/moto/elasticache/__init__.py b/moto/elasticache/__init__.py
new file mode 100644
index 000000000..44da97871
--- /dev/null
+++ b/moto/elasticache/__init__.py
@@ -0,0 +1,4 @@
+from .models import elasticache_backends
+from ..core.models import base_decorator
+
+mock_elasticache = base_decorator(elasticache_backends)
diff --git a/moto/elasticache/exceptions.py b/moto/elasticache/exceptions.py
new file mode 100644
index 000000000..e30a2c5d9
--- /dev/null
+++ b/moto/elasticache/exceptions.py
@@ -0,0 +1,65 @@
+from moto.core.exceptions import RESTError
+
+EXCEPTION_RESPONSE = """
+
+
+ Sender
+ {{ error_type }}
+ {{ message }}
+
+ <{{ request_id_tag }}>30c0dedb-92b1-4e2b-9be4-1188e3ed86ab{{ request_id_tag }}>
+"""
+
+
+class ElastiCacheException(RESTError):
+
+ code = 400
+
+ def __init__(self, *args, **kwargs):
+ kwargs.setdefault("template", "ecerror")
+ self.templates["ecerror"] = EXCEPTION_RESPONSE
+ super().__init__(*args, **kwargs)
+
+
+class PasswordTooShort(ElastiCacheException):
+
+ code = 404
+
+ def __init__(self, **kwargs):
+ super().__init__(
+ "InvalidParameterValue",
+ message="Passwords length must be between 16-128 characters.",
+ **kwargs,
+ )
+
+
+class PasswordRequired(ElastiCacheException):
+
+ code = 404
+
+ def __init__(self, **kwargs):
+ super().__init__(
+ "InvalidParameterValue",
+ message="No password was provided. If you want to create/update the user without password, please use the NoPasswordRequired flag.",
+ **kwargs,
+ )
+
+
+class UserAlreadyExists(ElastiCacheException):
+
+ code = 404
+
+ def __init__(self, **kwargs):
+ super().__init__(
+ "UserAlreadyExists", message="User user1 already exists.", **kwargs,
+ )
+
+
+class UserNotFound(ElastiCacheException):
+
+ code = 404
+
+ def __init__(self, user_id, **kwargs):
+ super().__init__(
+ "UserNotFound", message=f"User {user_id} not found.", **kwargs,
+ )
diff --git a/moto/elasticache/models.py b/moto/elasticache/models.py
new file mode 100644
index 000000000..ba8dd52a8
--- /dev/null
+++ b/moto/elasticache/models.py
@@ -0,0 +1,106 @@
+from boto3 import Session
+
+from moto.core import ACCOUNT_ID, BaseBackend, BaseModel
+
+from .exceptions import UserAlreadyExists, UserNotFound
+
+
+class User(BaseModel):
+ def __init__(
+ self,
+ region,
+ user_id,
+ user_name,
+ access_string,
+ engine,
+ no_password_required,
+ passwords=None,
+ ):
+ self.id = user_id
+ self.name = user_name
+ self.engine = engine
+ self.passwords = passwords or []
+ self.access_string = access_string
+ self.no_password_required = no_password_required
+ self.status = "active"
+ self.minimum_engine_version = "6.0"
+ self.usergroupids = []
+ self.region = region
+
+ @property
+ def arn(self):
+ return f"arn:aws:elasticache:{self.region}:{ACCOUNT_ID}:user:{self.id}"
+
+
+class ElastiCacheBackend(BaseBackend):
+ """Implementation of ElastiCache APIs."""
+
+ def __init__(self, region_name=None):
+ self.region_name = region_name
+ self.users = dict()
+ self.users["default"] = User(
+ region=self.region_name,
+ user_id="default",
+ user_name="default",
+ engine="redis",
+ access_string="on ~* +@all",
+ no_password_required=True,
+ )
+
+ def reset(self):
+ region_name = self.region_name
+ self.__dict__ = {}
+ self.__init__(region_name)
+
+ def create_user(
+ self, user_id, user_name, engine, passwords, access_string, no_password_required
+ ):
+ if user_id in self.users:
+ raise UserAlreadyExists
+ user = User(
+ region=self.region_name,
+ user_id=user_id,
+ user_name=user_name,
+ engine=engine,
+ passwords=passwords,
+ access_string=access_string,
+ no_password_required=no_password_required,
+ )
+ self.users[user_id] = user
+ return user
+
+ def delete_user(self, user_id):
+ if user_id in self.users:
+ user = self.users[user_id]
+ if user.status == "active":
+ user.status = "deleting"
+ return user
+ raise UserNotFound(user_id)
+
+ def describe_users(self, user_id):
+ """
+ Only the `user_id` parameter is currently supported.
+ Pagination is not yet implemented.
+ """
+ if user_id:
+ if user_id in self.users:
+ user = self.users[user_id]
+ if user.status == "deleting":
+ self.users.pop(user_id)
+ return [user]
+ else:
+ raise UserNotFound(user_id)
+ return self.users.values()
+
+
+elasticache_backends = {}
+for available_region in Session().get_available_regions("elasticache"):
+ elasticache_backends[available_region] = ElastiCacheBackend(available_region)
+for available_region in Session().get_available_regions(
+ "elasticache", partition_name="aws-us-gov"
+):
+ elasticache_backends[available_region] = ElastiCacheBackend(available_region)
+for available_region in Session().get_available_regions(
+ "elasticache", partition_name="aws-cn"
+):
+ elasticache_backends[available_region] = ElastiCacheBackend(available_region)
diff --git a/moto/elasticache/responses.py b/moto/elasticache/responses.py
new file mode 100644
index 000000000..b63e029e8
--- /dev/null
+++ b/moto/elasticache/responses.py
@@ -0,0 +1,119 @@
+from moto.core.responses import BaseResponse
+from .exceptions import PasswordTooShort, PasswordRequired
+from .models import elasticache_backends
+
+
+class ElastiCacheResponse(BaseResponse):
+ """Handler for ElastiCache requests and responses."""
+
+ @property
+ def elasticache_backend(self):
+ """Return backend instance specific for this region."""
+ return elasticache_backends[self.region]
+
+ def create_user(self):
+ params = self._get_params()
+ user_id = params.get("UserId")
+ user_name = params.get("UserName")
+ engine = params.get("Engine")
+ passwords = params.get("Passwords", [])
+ no_password_required = self._get_bool_param("NoPasswordRequired", False)
+ password_required = not no_password_required
+ if password_required and not passwords:
+ raise PasswordRequired
+ if any([len(p) < 16 for p in passwords]):
+ raise PasswordTooShort
+ access_string = params.get("AccessString")
+ user = self.elasticache_backend.create_user(
+ user_id=user_id,
+ user_name=user_name,
+ engine=engine,
+ passwords=passwords,
+ access_string=access_string,
+ no_password_required=no_password_required,
+ )
+ template = self.response_template(CREATE_USER_TEMPLATE)
+ return template.render(user=user)
+
+ def delete_user(self):
+ params = self._get_params()
+ user_id = params.get("UserId")
+ user = self.elasticache_backend.delete_user(user_id=user_id)
+ template = self.response_template(DELETE_USER_TEMPLATE)
+ return template.render(user=user)
+
+ def describe_users(self):
+ params = self._get_params()
+ user_id = params.get("UserId")
+ users = self.elasticache_backend.describe_users(user_id=user_id)
+ template = self.response_template(DESCRIBE_USERS_TEMPLATE)
+ return template.render(users=users)
+
+
+USER_TEMPLATE = """{{ user.id }}
+ {{ user.name }}
+ {{ user.status }}
+ {{ user.engine }}
+ {{ user.minimum_engine_version }}
+ {{ user.access_string }}
+
+{% for usergroupid in user.usergroupids %}
+ {{ usergroupid }}
+{% endfor %}
+
+
+ {% if user.no_password_required %}
+ no-password
+ {% else %}
+ password
+ {{ user.passwords|length }}
+ {% endif %}
+
+ {{ user.arn }}"""
+
+
+CREATE_USER_TEMPLATE = (
+ """
+
+ 1549581b-12b7-11e3-895e-1334aEXAMPLE
+
+
+ """
+ + USER_TEMPLATE
+ + """
+
+"""
+)
+
+DELETE_USER_TEMPLATE = (
+ """
+
+ 1549581b-12b7-11e3-895e-1334aEXAMPLE
+
+
+ """
+ + USER_TEMPLATE
+ + """
+
+"""
+)
+
+DESCRIBE_USERS_TEMPLATE = (
+ """
+
+ 1549581b-12b7-11e3-895e-1334aEXAMPLE
+
+
+
+{% for user in users %}
+
+ """
+ + USER_TEMPLATE
+ + """
+
+{% endfor %}
+
+
+
+"""
+)
diff --git a/moto/elasticache/urls.py b/moto/elasticache/urls.py
new file mode 100644
index 000000000..755d63237
--- /dev/null
+++ b/moto/elasticache/urls.py
@@ -0,0 +1,11 @@
+"""elasticache base URL and path."""
+from .responses import ElastiCacheResponse
+
+url_bases = [
+ r"https?://elasticache\.(.+)\.amazonaws\.com",
+]
+
+
+url_paths = {
+ "{0}/$": ElastiCacheResponse.dispatch,
+}
diff --git a/scripts/scaffold.py b/scripts/scaffold.py
index d70f89add..4189485d2 100755
--- a/scripts/scaffold.py
+++ b/scripts/scaffold.py
@@ -355,7 +355,8 @@ def _get_subtree(name, shape, replace_list, name_prefix=None):
name_prefix = []
class_name = shape.__class__.__name__
- if class_name in ("StringShape", "Shape"):
+ shape_type = shape.type_name
+ if class_name in ("StringShape", "Shape") or shape_type == "structure":
tree = etree.Element(name) # pylint: disable=c-extension-no-member
if name_prefix:
tree.text = f"{{{{ {name_prefix[-1]}.{to_snake_case(name)} }}}}"
@@ -363,23 +364,24 @@ def _get_subtree(name, shape, replace_list, name_prefix=None):
tree.text = f"{{{{ {to_snake_case(name)} }}}}"
return tree
- if class_name in ("ListShape",):
+ if class_name in ("ListShape",) or shape_type == "list":
# pylint: disable=c-extension-no-member
replace_list.append((name, name_prefix))
tree = etree.Element(name)
t_member = etree.Element("member")
tree.append(t_member)
- for nested_name, nested_shape in shape.member.members.items():
- t_member.append(
- _get_subtree(
- nested_name,
- nested_shape,
- replace_list,
- name_prefix + [singularize(name.lower())],
+ if hasattr(shape.member, "members"):
+ for nested_name, nested_shape in shape.member.members.items():
+ t_member.append(
+ _get_subtree(
+ nested_name,
+ nested_shape,
+ replace_list,
+ name_prefix + [singularize(name.lower())],
+ )
)
- )
return tree
- raise ValueError("Not supported Shape")
+ raise ValueError(f"Not supported Shape: {shape}")
def get_response_query_template(service, operation): # pylint: disable=too-many-locals
diff --git a/tests/test_elasticache/__init__.py b/tests/test_elasticache/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/test_elasticache/test_elasticache.py b/tests/test_elasticache/test_elasticache.py
new file mode 100644
index 000000000..adee2ff0a
--- /dev/null
+++ b/tests/test_elasticache/test_elasticache.py
@@ -0,0 +1,216 @@
+import boto3
+import pytest
+import sure # noqa # pylint: disable=unused-import
+
+from botocore.exceptions import ClientError
+from moto import mock_elasticache
+from moto.core import ACCOUNT_ID
+
+# 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
+
+
+@mock_elasticache
+def test_create_user_no_password_required():
+ client = boto3.client("elasticache", region_name="ap-southeast-1")
+ user_id = "user1"
+ resp = client.create_user(
+ UserId=user_id,
+ UserName="User1",
+ Engine="Redis",
+ AccessString="on ~* +@all",
+ NoPasswordRequired=True,
+ )
+
+ resp.should.have.key("UserId").equals(user_id)
+ resp.should.have.key("UserName").equals("User1")
+ resp.should.have.key("Status").equals("active")
+ resp.should.have.key("Engine").equals("Redis")
+ resp.should.have.key("MinimumEngineVersion").equals("6.0")
+ resp.should.have.key("AccessString").equals("on ~* +@all")
+ resp.should.have.key("UserGroupIds").equals([])
+ resp.should.have.key("Authentication")
+ resp["Authentication"].should.have.key("Type").equals("no-password")
+ resp["Authentication"].shouldnt.have.key("PasswordCount")
+ resp.should.have.key("ARN").equals(
+ f"arn:aws:elasticache:ap-southeast-1:{ACCOUNT_ID}:user:{user_id}"
+ )
+
+
+@mock_elasticache
+def test_create_user_with_password_too_short():
+ client = boto3.client("elasticache", region_name="ap-southeast-1")
+ user_id = "user1"
+ with pytest.raises(ClientError) as exc:
+ client.create_user(
+ UserId=user_id,
+ UserName="User1",
+ Engine="Redis",
+ AccessString="on ~* +@all",
+ Passwords=["mysecretpass"],
+ )
+ err = exc.value.response["Error"]
+ err["Code"].should.equal("InvalidParameterValue")
+ err["Message"].should.equal("Passwords length must be between 16-128 characters.")
+
+
+@mock_elasticache
+def test_create_user_with_password():
+ client = boto3.client("elasticache", region_name="ap-southeast-1")
+ user_id = "user1"
+ resp = client.create_user(
+ UserId=user_id,
+ UserName="User1",
+ Engine="Redis",
+ AccessString="on ~* +@all",
+ Passwords=["mysecretpassthatsverylong"],
+ )
+
+ resp.should.have.key("UserId").equals(user_id)
+ resp.should.have.key("UserName").equals("User1")
+ resp.should.have.key("Status").equals("active")
+ resp.should.have.key("Engine").equals("Redis")
+ resp.should.have.key("MinimumEngineVersion").equals("6.0")
+ resp.should.have.key("AccessString").equals("on ~* +@all")
+ resp.should.have.key("UserGroupIds").equals([])
+ resp.should.have.key("Authentication")
+ resp["Authentication"].should.have.key("Type").equals("password")
+ resp["Authentication"].should.have.key("PasswordCount").equals(1)
+ resp.should.have.key("ARN").equals(
+ f"arn:aws:elasticache:ap-southeast-1:{ACCOUNT_ID}:user:{user_id}"
+ )
+
+
+@mock_elasticache
+def test_create_user_without_password():
+ client = boto3.client("elasticache", region_name="ap-southeast-1")
+ with pytest.raises(ClientError) as exc:
+ client.create_user(
+ UserId="user1", UserName="User1", Engine="Redis", AccessString="?"
+ )
+ err = exc.value.response["Error"]
+ err["Code"].should.equal("InvalidParameterValue")
+ err["Message"].should.equal(
+ "No password was provided. If you want to create/update the user without password, please use the NoPasswordRequired flag."
+ )
+
+
+@mock_elasticache
+def test_create_user_twice():
+ client = boto3.client("elasticache", region_name="ap-southeast-1")
+ user_id = "user1"
+ client.create_user(
+ UserId=user_id,
+ UserName="User1",
+ Engine="Redis",
+ AccessString="on ~* +@all",
+ Passwords=["mysecretpassthatsverylong"],
+ )
+
+ with pytest.raises(ClientError) as exc:
+ client.create_user(
+ UserId=user_id,
+ UserName="User1",
+ Engine="Redis",
+ AccessString="on ~* +@all",
+ Passwords=["mysecretpassthatsverylong"],
+ )
+ err = exc.value.response["Error"]
+ err["Code"].should.equal("UserAlreadyExists")
+ err["Message"].should.equal("User user1 already exists.")
+
+
+@mock_elasticache
+def test_delete_user_unknown():
+ client = boto3.client("elasticache", region_name="ap-southeast-1")
+ with pytest.raises(ClientError) as exc:
+ client.delete_user(UserId="unknown")
+ err = exc.value.response["Error"]
+ err["Code"].should.equal("UserNotFound")
+ err["Message"].should.equal("User unknown not found.")
+
+
+@mock_elasticache
+def test_delete_user():
+ client = boto3.client("elasticache", region_name="ap-southeast-1")
+
+ client.create_user(
+ UserId="user1",
+ UserName="User1",
+ Engine="Redis",
+ AccessString="on ~* +@all",
+ Passwords=["mysecretpassthatsverylong"],
+ )
+
+ client.delete_user(UserId="user1")
+
+ # Initial status is 'deleting'
+ resp = client.describe_users(UserId="user1")
+ resp["Users"][0]["Status"].should.equal("deleting")
+
+ # User is only deleted after some time
+ with pytest.raises(ClientError) as exc:
+ client.describe_users(UserId="unknown")
+ exc.value.response["Error"]["Code"].should.equal("UserNotFound")
+
+
+@mock_elasticache
+def test_describe_users_initial():
+ client = boto3.client("elasticache", region_name="us-east-2")
+ resp = client.describe_users()
+
+ resp.should.have.key("Users").length_of(1)
+ resp["Users"][0].should.equal(
+ {
+ "UserId": "default",
+ "UserName": "default",
+ "Status": "active",
+ "Engine": "redis",
+ "MinimumEngineVersion": "6.0",
+ "AccessString": "on ~* +@all",
+ "UserGroupIds": [],
+ "Authentication": {"Type": "no-password"},
+ "ARN": f"arn:aws:elasticache:us-east-2:{ACCOUNT_ID}:user:default",
+ }
+ )
+
+
+@mock_elasticache
+def test_describe_users():
+ client = boto3.client("elasticache", region_name="ap-southeast-1")
+
+ client.create_user(
+ UserId="user1",
+ UserName="User1",
+ Engine="Redis",
+ AccessString="on ~* +@all",
+ Passwords=["mysecretpassthatsverylong"],
+ )
+
+ resp = client.describe_users()
+
+ resp.should.have.key("Users").length_of(2)
+ resp["Users"].should.contain(
+ {
+ "UserId": "user1",
+ "UserName": "User1",
+ "Status": "active",
+ "Engine": "Redis",
+ "MinimumEngineVersion": "6.0",
+ "AccessString": "on ~* +@all",
+ "UserGroupIds": [],
+ "Authentication": {"Type": "password", "PasswordCount": 1},
+ "ARN": f"arn:aws:elasticache:ap-southeast-1:{ACCOUNT_ID}:user:user1",
+ }
+ )
+
+
+@mock_elasticache
+def test_describe_users_unknown_userid():
+ client = boto3.client("elasticache", region_name="ap-southeast-1")
+
+ with pytest.raises(ClientError) as exc:
+ client.describe_users(UserId="unknown")
+ err = exc.value.response["Error"]
+ err["Code"].should.equal("UserNotFound")
+ err["Message"].should.equal("User unknown not found.")
diff --git a/tests/test_elasticache/test_server.py b/tests/test_elasticache/test_server.py
new file mode 100644
index 000000000..1217d5d50
--- /dev/null
+++ b/tests/test_elasticache/test_server.py
@@ -0,0 +1,14 @@
+import sure # noqa # pylint: disable=unused-import
+
+import moto.server as server
+
+
+def test_elasticache_describe_users():
+ backend = server.create_backend_app("elasticache")
+ test_client = backend.test_client()
+
+ data = "Action=DescribeUsers"
+ headers = {"Host": "elasticache.us-east-1.amazonaws.com"}
+ resp = test_client.post("/", data=data, headers=headers)
+ resp.status_code.should.equal(200)
+ str(resp.data).should.contain("default")