Feature: Elasticache (#4668)
This commit is contained in:
parent
17c0cedbb2
commit
0c5a3cc8ca
@ -1843,6 +1843,77 @@
|
||||
- [ ] update_nodegroup_version
|
||||
</details>
|
||||
|
||||
## elasticache
|
||||
<details>
|
||||
<summary>4% implemented</summary>
|
||||
|
||||
- [ ] 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
|
||||
</details>
|
||||
|
||||
## elasticbeanstalk
|
||||
<details>
|
||||
<summary>12% implemented</summary>
|
||||
@ -4860,7 +4931,6 @@
|
||||
- ebs
|
||||
- ecr-public
|
||||
- elastic-inference
|
||||
- elasticache
|
||||
- es
|
||||
- finspace
|
||||
- finspace-data
|
||||
|
100
docs/docs/services/elasticache.rst
Normal file
100
docs/docs/services/elasticache.rst
Normal file
@ -0,0 +1,100 @@
|
||||
.. _implementedservice_elasticache:
|
||||
|
||||
.. |start-h3| raw:: html
|
||||
|
||||
<h3>
|
||||
|
||||
.. |end-h3| raw:: html
|
||||
|
||||
</h3>
|
||||
|
||||
===========
|
||||
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
|
||||
|
@ -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):
|
||||
|
@ -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(
|
||||
|
4
moto/elasticache/__init__.py
Normal file
4
moto/elasticache/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
from .models import elasticache_backends
|
||||
from ..core.models import base_decorator
|
||||
|
||||
mock_elasticache = base_decorator(elasticache_backends)
|
65
moto/elasticache/exceptions.py
Normal file
65
moto/elasticache/exceptions.py
Normal file
@ -0,0 +1,65 @@
|
||||
from moto.core.exceptions import RESTError
|
||||
|
||||
EXCEPTION_RESPONSE = """<?xml version="1.0"?>
|
||||
<ErrorResponse xmlns="http://elasticache.amazonaws.com/doc/2015-02-02/">
|
||||
<Error>
|
||||
<Type>Sender</Type>
|
||||
<Code>{{ error_type }}</Code>
|
||||
<Message>{{ message }}</Message>
|
||||
</Error>
|
||||
<{{ request_id_tag }}>30c0dedb-92b1-4e2b-9be4-1188e3ed86ab</{{ request_id_tag }}>
|
||||
</ErrorResponse>"""
|
||||
|
||||
|
||||
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,
|
||||
)
|
106
moto/elasticache/models.py
Normal file
106
moto/elasticache/models.py
Normal file
@ -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)
|
119
moto/elasticache/responses.py
Normal file
119
moto/elasticache/responses.py
Normal file
@ -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 = """<UserId>{{ user.id }}</UserId>
|
||||
<UserName>{{ user.name }}</UserName>
|
||||
<Status>{{ user.status }}</Status>
|
||||
<Engine>{{ user.engine }}</Engine>
|
||||
<MinimumEngineVersion>{{ user.minimum_engine_version }}</MinimumEngineVersion>
|
||||
<AccessString>{{ user.access_string }}</AccessString>
|
||||
<UserGroupIds>
|
||||
{% for usergroupid in user.usergroupids %}
|
||||
<member>{{ usergroupid }}</member>
|
||||
{% endfor %}
|
||||
</UserGroupIds>
|
||||
<Authentication>
|
||||
{% if user.no_password_required %}
|
||||
<Type>no-password</Type>
|
||||
{% else %}
|
||||
<Type>password</Type>
|
||||
<PasswordCount>{{ user.passwords|length }}</PasswordCount>
|
||||
{% endif %}
|
||||
</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>
|
||||
"""
|
||||
+ USER_TEMPLATE
|
||||
+ """
|
||||
</CreateUserResult>
|
||||
</CreateUserResponse>"""
|
||||
)
|
||||
|
||||
DELETE_USER_TEMPLATE = (
|
||||
"""<DeleteUserResponse xmlns="http://elasticache.amazonaws.com/doc/2015-02-02/">
|
||||
<ResponseMetadata>
|
||||
<RequestId>1549581b-12b7-11e3-895e-1334aEXAMPLE</RequestId>
|
||||
</ResponseMetadata>
|
||||
<DeleteUserResult>
|
||||
"""
|
||||
+ USER_TEMPLATE
|
||||
+ """
|
||||
</DeleteUserResult>
|
||||
</DeleteUserResponse>"""
|
||||
)
|
||||
|
||||
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>
|
||||
"""
|
||||
+ USER_TEMPLATE
|
||||
+ """
|
||||
</member>
|
||||
{% endfor %}
|
||||
</Users>
|
||||
<Marker></Marker>
|
||||
</DescribeUsersResult>
|
||||
</DescribeUsersResponse>"""
|
||||
)
|
11
moto/elasticache/urls.py
Normal file
11
moto/elasticache/urls.py
Normal file
@ -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,
|
||||
}
|
@ -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
|
||||
|
0
tests/test_elasticache/__init__.py
Normal file
0
tests/test_elasticache/__init__.py
Normal file
216
tests/test_elasticache/test_elasticache.py
Normal file
216
tests/test_elasticache/test_elasticache.py
Normal file
@ -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.")
|
14
tests/test_elasticache/test_server.py
Normal file
14
tests/test_elasticache/test_server.py
Normal file
@ -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("<UserId>default</UserId>")
|
Loading…
Reference in New Issue
Block a user