diff --git a/moto/apigateway/models.py b/moto/apigateway/models.py index 9d6305ef9..6f7fc97a9 100644 --- a/moto/apigateway/models.py +++ b/moto/apigateway/models.py @@ -332,15 +332,16 @@ class ApiKey(BaseModel, dict): class UsagePlan(BaseModel, dict): - def __init__(self, name=None, description=None, apiStages=[], - throttle=None, quota=None): + def __init__(self, name=None, description=None, apiStages=None, + throttle=None, quota=None, tags=None): super(UsagePlan, self).__init__() self['id'] = create_id() self['name'] = name self['description'] = description - self['apiStages'] = apiStages + self['apiStages'] = apiStages if apiStages else [] self['throttle'] = throttle self['quota'] = quota + self['tags'] = tags class UsagePlanKey(BaseModel, dict): diff --git a/moto/dynamodb2/models.py b/moto/dynamodb2/models.py index 9b0e74a1d..1bf21588a 100644 --- a/moto/dynamodb2/models.py +++ b/moto/dynamodb2/models.py @@ -235,7 +235,33 @@ class Item(BaseModel): value = re.sub(r'{0}\b'.format(k), v, value) if action == "REMOVE": - self.attrs.pop(value, None) + key = value + if '.' not in key: + self.attrs.pop(value, None) + else: + # Handle nested dict updates + key_parts = key.split('.') + attr = key_parts.pop(0) + if attr not in self.attrs: + raise ValueError + + last_val = self.attrs[attr].value + for key_part in key_parts[:-1]: + # Hack but it'll do, traverses into a dict + last_val_type = list(last_val.keys()) + if last_val_type and last_val_type[0] == 'M': + last_val = last_val['M'] + + if key_part not in last_val: + last_val[key_part] = {'M': {}} + + last_val = last_val[key_part] + + last_val_type = list(last_val.keys()) + if last_val_type and last_val_type[0] == 'M': + last_val['M'].pop(key_parts[-1], None) + else: + last_val.pop(key_parts[-1], None) elif action == 'SET': key, value = value.split("=", 1) key = key.strip() @@ -1119,12 +1145,23 @@ class DynamoDBBackend(BaseBackend): item.update_with_attribute_updates(attribute_updates) return item - def delete_item(self, table_name, keys): + def delete_item(self, table_name, key, expression_attribute_names=None, expression_attribute_values=None, + condition_expression=None): table = self.get_table(table_name) if not table: return None - hash_key, range_key = self.get_keys_value(table, keys) - return table.delete_item(hash_key, range_key) + + hash_value, range_value = self.get_keys_value(table, key) + item = table.get_item(hash_value, range_value) + + condition_op = get_filter_expression( + condition_expression, + expression_attribute_names, + expression_attribute_values) + if not condition_op.expr(item): + raise ValueError('The conditional request failed') + + return table.delete_item(hash_value, range_value) def update_ttl(self, table_name, ttl_spec): table = self.tables.get(table_name) diff --git a/moto/dynamodb2/responses.py b/moto/dynamodb2/responses.py index c04cf0ed6..e3ce1b39e 100644 --- a/moto/dynamodb2/responses.py +++ b/moto/dynamodb2/responses.py @@ -582,7 +582,7 @@ class DynamoHandler(BaseResponse): def delete_item(self): name = self.body['TableName'] - keys = self.body['Key'] + key = self.body['Key'] return_values = self.body.get('ReturnValues', 'NONE') if return_values not in ('ALL_OLD', 'NONE'): er = 'com.amazonaws.dynamodb.v20111205#ValidationException' @@ -593,7 +593,21 @@ class DynamoHandler(BaseResponse): er = 'com.amazonaws.dynamodb.v20120810#ConditionalCheckFailedException' return self.error(er, 'A condition specified in the operation could not be evaluated.') - item = self.dynamodb_backend.delete_item(name, keys) + # Attempt to parse simple ConditionExpressions into an Expected + # expression + condition_expression = self.body.get('ConditionExpression') + expression_attribute_names = self.body.get('ExpressionAttributeNames', {}) + expression_attribute_values = self.body.get('ExpressionAttributeValues', {}) + + try: + item = self.dynamodb_backend.delete_item( + name, key, expression_attribute_names, expression_attribute_values, + condition_expression + ) + except ValueError: + er = 'com.amazonaws.dynamodb.v20111205#ConditionalCheckFailedException' + return self.error(er, 'A condition specified in the operation could not be evaluated.') + if item and return_values == 'ALL_OLD': item_dict = item.to_json() else: diff --git a/moto/s3/models.py b/moto/s3/models.py index b5aef34d3..67293f385 100644 --- a/moto/s3/models.py +++ b/moto/s3/models.py @@ -329,7 +329,8 @@ class FakeGrant(BaseModel): class FakeAcl(BaseModel): - def __init__(self, grants=[]): + def __init__(self, grants=None): + grants = grants or [] self.grants = grants @property @@ -396,7 +397,7 @@ class FakeTag(BaseModel): class LifecycleFilter(BaseModel): def __init__(self, prefix=None, tag=None, and_filter=None): - self.prefix = prefix or '' + self.prefix = prefix self.tag = tag self.and_filter = and_filter @@ -404,7 +405,7 @@ class LifecycleFilter(BaseModel): class LifecycleAndFilter(BaseModel): def __init__(self, prefix=None, tags=None): - self.prefix = prefix or '' + self.prefix = prefix self.tags = tags @@ -478,6 +479,8 @@ class FakeBucket(BaseModel): self.logging = {} self.notification_configuration = None self.accelerate_configuration = None + self.payer = 'BucketOwner' + self.creation_date = datetime.datetime.utcnow() @property def location(self): @@ -494,6 +497,11 @@ class FakeBucket(BaseModel): expiration = rule.get('Expiration') transition = rule.get('Transition') + try: + top_level_prefix = rule['Prefix'] or '' # If it's `None` the set to the empty string + except KeyError: + top_level_prefix = None + nve_noncurrent_days = None if rule.get('NoncurrentVersionExpiration') is not None: if rule["NoncurrentVersionExpiration"].get('NoncurrentDays') is None: @@ -528,13 +536,22 @@ class FakeBucket(BaseModel): if rule.get("Filter"): # Can't have both `Filter` and `Prefix` (need to check for the presence of the key): try: + # 'Prefix' cannot be outside of a Filter: if rule["Prefix"] or not rule["Prefix"]: raise MalformedXML() except KeyError: pass + filters = 0 + try: + prefix_filter = rule['Filter']['Prefix'] or '' # If it's `None` the set to the empty string + filters += 1 + except KeyError: + prefix_filter = None + and_filter = None if rule["Filter"].get("And"): + filters += 1 and_tags = [] if rule["Filter"]["And"].get("Tag"): if not isinstance(rule["Filter"]["And"]["Tag"], list): @@ -543,17 +560,34 @@ class FakeBucket(BaseModel): for t in rule["Filter"]["And"]["Tag"]: and_tags.append(FakeTag(t["Key"], t.get("Value", ''))) - and_filter = LifecycleAndFilter(prefix=rule["Filter"]["And"]["Prefix"], tags=and_tags) + try: + and_prefix = rule["Filter"]["And"]["Prefix"] or '' # If it's `None` then set to the empty string + except KeyError: + and_prefix = None + + and_filter = LifecycleAndFilter(prefix=and_prefix, tags=and_tags) filter_tag = None if rule["Filter"].get("Tag"): + filters += 1 filter_tag = FakeTag(rule["Filter"]["Tag"]["Key"], rule["Filter"]["Tag"].get("Value", '')) - lc_filter = LifecycleFilter(prefix=rule["Filter"]["Prefix"], tag=filter_tag, and_filter=and_filter) + # Can't have more than 1 filter: + if filters > 1: + raise MalformedXML() + + lc_filter = LifecycleFilter(prefix=prefix_filter, tag=filter_tag, and_filter=and_filter) + + # If no top level prefix and no filter is present, then this is invalid: + if top_level_prefix is None: + try: + rule['Filter'] + except KeyError: + raise MalformedXML() self.rules.append(LifecycleRule( id=rule.get('ID'), - prefix=rule.get('Prefix'), + prefix=top_level_prefix, lc_filter=lc_filter, status=rule['Status'], expiration_days=expiration.get('Days') if expiration else None, diff --git a/moto/s3/responses.py b/moto/s3/responses.py index b13da69bd..ae6662579 100644 --- a/moto/s3/responses.py +++ b/moto/s3/responses.py @@ -1310,7 +1310,7 @@ S3_ALL_BUCKETS = """ {{ rule.id }} {% if rule.filter %} + {% if rule.filter.prefix != None %} {{ rule.filter.prefix }} + {% endif %} {% if rule.filter.tag %} {{ rule.filter.tag.key }} @@ -1425,7 +1427,9 @@ S3_BUCKET_LIFECYCLE_CONFIGURATION = """ {% endif %} {% if rule.filter.and_filter %} + {% if rule.filter.and_filter.prefix != None %} {{ rule.filter.and_filter.prefix }} + {% endif %} {% for tag in rule.filter.and_filter.tags %} {{ tag.key }} @@ -1436,7 +1440,9 @@ S3_BUCKET_LIFECYCLE_CONFIGURATION = """ {% endif %} {% else %} - {{ rule.prefix if rule.prefix != None }} + {% if rule.prefix != None %} + {{ rule.prefix }} + {% endif %} {% endif %} {{ rule.status }} {% if rule.storage_class %} diff --git a/moto/ssm/exceptions.py b/moto/ssm/exceptions.py new file mode 100644 index 000000000..4e01843fb --- /dev/null +++ b/moto/ssm/exceptions.py @@ -0,0 +1,34 @@ +from __future__ import unicode_literals +from moto.core.exceptions import JsonRESTError + + +class InvalidFilterKey(JsonRESTError): + code = 400 + + def __init__(self, message): + super(InvalidFilterKey, self).__init__( + "InvalidFilterKey", message) + + +class InvalidFilterOption(JsonRESTError): + code = 400 + + def __init__(self, message): + super(InvalidFilterOption, self).__init__( + "InvalidFilterOption", message) + + +class InvalidFilterValue(JsonRESTError): + code = 400 + + def __init__(self, message): + super(InvalidFilterValue, self).__init__( + "InvalidFilterValue", message) + + +class ValidationException(JsonRESTError): + code = 400 + + def __init__(self, message): + super(ValidationException, self).__init__( + "ValidationException", message) diff --git a/moto/ssm/models.py b/moto/ssm/models.py index 2f316a3ac..39bd63ede 100644 --- a/moto/ssm/models.py +++ b/moto/ssm/models.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import re from collections import defaultdict from moto.core import BaseBackend, BaseModel @@ -12,6 +13,8 @@ import time import uuid import itertools +from .exceptions import ValidationException, InvalidFilterValue, InvalidFilterOption, InvalidFilterKey + class Parameter(BaseModel): def __init__(self, name, value, type, description, allowed_pattern, keyid, @@ -25,12 +28,15 @@ class Parameter(BaseModel): self.version = version if self.type == 'SecureString': + if not self.keyid: + self.keyid = 'alias/aws/ssm' + self.value = self.encrypt(value) else: self.value = value def encrypt(self, value): - return 'kms:{}:'.format(self.keyid or 'default') + value + return 'kms:{}:'.format(self.keyid) + value def decrypt(self, value): if self.type != 'SecureString': @@ -217,6 +223,7 @@ class SimpleSystemManagerBackend(BaseBackend): self._parameters = {} self._resource_tags = defaultdict(lambda: defaultdict(dict)) self._commands = [] + self._errors = [] # figure out what region we're in for region, backend in ssm_backends.items(): @@ -239,6 +246,179 @@ class SimpleSystemManagerBackend(BaseBackend): pass return result + def describe_parameters(self, filters, parameter_filters): + if filters and parameter_filters: + raise ValidationException('You can use either Filters or ParameterFilters in a single request.') + + self._validate_parameter_filters(parameter_filters, by_path=False) + + result = [] + for param in self._parameters: + ssm_parameter = self._parameters[param] + if not self._match_filters(ssm_parameter, parameter_filters): + continue + + if filters: + for filter in filters: + if filter['Key'] == 'Name': + k = ssm_parameter.name + for v in filter['Values']: + if k.startswith(v): + result.append(ssm_parameter) + break + elif filter['Key'] == 'Type': + k = ssm_parameter.type + for v in filter['Values']: + if k == v: + result.append(ssm_parameter) + break + elif filter['Key'] == 'KeyId': + k = ssm_parameter.keyid + if k: + for v in filter['Values']: + if k == v: + result.append(ssm_parameter) + break + continue + + result.append(ssm_parameter) + + return result + + def _validate_parameter_filters(self, parameter_filters, by_path): + for index, filter_obj in enumerate(parameter_filters or []): + key = filter_obj['Key'] + values = filter_obj.get('Values', []) + + if key == 'Path': + option = filter_obj.get('Option', 'OneLevel') + else: + option = filter_obj.get('Option', 'Equals') + + if not re.match(r'^tag:.+|Name|Type|KeyId|Path|Label|Tier$', key): + self._errors.append(self._format_error( + key='parameterFilters.{index}.member.key'.format(index=(index + 1)), + value=key, + constraint='Member must satisfy regular expression pattern: tag:.+|Name|Type|KeyId|Path|Label|Tier', + )) + + if len(key) > 132: + self._errors.append(self._format_error( + key='parameterFilters.{index}.member.key'.format(index=(index + 1)), + value=key, + constraint='Member must have length less than or equal to 132', + )) + + if len(option) > 10: + self._errors.append(self._format_error( + key='parameterFilters.{index}.member.option'.format(index=(index + 1)), + value='over 10 chars', + constraint='Member must have length less than or equal to 10', + )) + + if len(values) > 50: + self._errors.append(self._format_error( + key='parameterFilters.{index}.member.values'.format(index=(index + 1)), + value=values, + constraint='Member must have length less than or equal to 50', + )) + + if any(len(value) > 1024 for value in values): + self._errors.append(self._format_error( + key='parameterFilters.{index}.member.values'.format(index=(index + 1)), + value=values, + constraint='[Member must have length less than or equal to 1024, Member must have length greater than or equal to 1]', + )) + + self._raise_errors() + + filter_keys = [] + for filter_obj in (parameter_filters or []): + key = filter_obj['Key'] + values = filter_obj.get('Values') + + if key == 'Path': + option = filter_obj.get('Option', 'OneLevel') + else: + option = filter_obj.get('Option', 'Equals') + + if not by_path and key == 'Label': + raise InvalidFilterKey('The following filter key is not valid: Label. Valid filter keys include: [Path, Name, Type, KeyId, Tier].') + + if not values: + raise InvalidFilterValue('The following filter values are missing : null for filter key Name.') + + if key in filter_keys: + raise InvalidFilterKey( + 'The following filter is duplicated in the request: Name. A request can contain only one occurrence of a specific filter.' + ) + + if key == 'Path': + if option not in ['Recursive', 'OneLevel']: + raise InvalidFilterOption( + 'The following filter option is not valid: {option}. Valid options include: [Recursive, OneLevel].'.format(option=option) + ) + if any(value.lower().startswith(('/aws', '/ssm')) for value in values): + raise ValidationException( + 'Filters for common parameters can\'t be prefixed with "aws" or "ssm" (case-insensitive). ' + 'When using global parameters, please specify within a global namespace.' + ) + for value in values: + if value.lower().startswith(('/aws', '/ssm')): + raise ValidationException( + 'Filters for common parameters can\'t be prefixed with "aws" or "ssm" (case-insensitive). ' + 'When using global parameters, please specify within a global namespace.' + ) + if ('//' in value or + not value.startswith('/') or + not re.match('^[a-zA-Z0-9_.-/]*$', value)): + raise ValidationException( + 'The parameter doesn\'t meet the parameter name requirements. The parameter name must begin with a forward slash "/". ' + 'It can\'t be prefixed with \"aws\" or \"ssm\" (case-insensitive). ' + 'It must use only letters, numbers, or the following symbols: . (period), - (hyphen), _ (underscore). ' + 'Special characters are not allowed. All sub-paths, if specified, must use the forward slash symbol "/". ' + 'Valid example: /get/parameters2-/by1./path0_.' + ) + + if key == 'Tier': + for value in values: + if value not in ['Standard', 'Advanced', 'Intelligent-Tiering']: + raise InvalidFilterOption( + 'The following filter value is not valid: {value}. Valid values include: [Standard, Advanced, Intelligent-Tiering].'.format(value=value) + ) + + if key == 'Type': + for value in values: + if value not in ['String', 'StringList', 'SecureString']: + raise InvalidFilterOption( + 'The following filter value is not valid: {value}. Valid values include: [String, StringList, SecureString].'.format(value=value) + ) + + if key != 'Path' and option not in ['Equals', 'BeginsWith']: + raise InvalidFilterOption( + 'The following filter option is not valid: {option}. Valid options include: [BeginsWith, Equals].'.format(option=option) + ) + + filter_keys.append(key) + + def _format_error(self, key, value, constraint): + return 'Value "{value}" at "{key}" failed to satisfy constraint: {constraint}'.format( + constraint=constraint, + key=key, + value=value, + ) + + def _raise_errors(self): + if self._errors: + count = len(self._errors) + plural = "s" if len(self._errors) > 1 else "" + errors = "; ".join(self._errors) + self._errors = [] # reset collected errors + + raise ValidationException('{count} validation error{plural} detected: {errors}'.format( + count=count, plural=plural, errors=errors, + )) + def get_all_parameters(self): result = [] for k, _ in self._parameters.items(): @@ -269,26 +449,53 @@ class SimpleSystemManagerBackend(BaseBackend): return result - @staticmethod - def _match_filters(parameter, filters=None): + def _match_filters(self, parameter, filters=None): """Return True if the given parameter matches all the filters""" for filter_obj in (filters or []): key = filter_obj['Key'] - option = filter_obj.get('Option', 'Equals') values = filter_obj.get('Values', []) - what = None - if key == 'Type': - what = parameter.type - elif key == 'KeyId': - what = parameter.keyid + if key == 'Path': + option = filter_obj.get('Option', 'OneLevel') + else: + option = filter_obj.get('Option', 'Equals') - if option == 'Equals'\ - and not any(what == value for value in values): + what = None + if key == 'KeyId': + what = parameter.keyid + elif key == 'Name': + what = '/' + parameter.name.lstrip('/') + values = ['/' + value.lstrip('/') for value in values] + elif key == 'Path': + what = '/' + parameter.name.lstrip('/') + values = ['/' + value.strip('/') for value in values] + elif key == 'Type': + what = parameter.type + + if what is None: return False - elif option == 'BeginsWith'\ - and not any(what.startswith(value) for value in values): + elif (option == 'BeginsWith' and + not any(what.startswith(value) for value in values)): return False + elif (option == 'Equals' and + not any(what == value for value in values)): + return False + elif option == 'OneLevel': + if any(value == '/' and len(what.split('/')) == 2 for value in values): + continue + elif any(value != '/' and + what.startswith(value + '/') and + len(what.split('/')) - 1 == len(value.split('/')) for value in values): + continue + else: + return False + elif option == 'Recursive': + if any(value == '/' for value in values): + continue + elif any(what.startswith(value + '/') for value in values): + continue + else: + return False # True if no false match (or no filters at all) return True diff --git a/moto/ssm/responses.py b/moto/ssm/responses.py index c47d4127a..27a5f8e35 100644 --- a/moto/ssm/responses.py +++ b/moto/ssm/responses.py @@ -104,6 +104,7 @@ class SimpleSystemManagerResponse(BaseResponse): def describe_parameters(self): page_size = 10 filters = self._get_param('Filters') + parameter_filters = self._get_param('ParameterFilters') token = self._get_param('NextToken') if hasattr(token, 'strip'): token = token.strip() @@ -111,42 +112,17 @@ class SimpleSystemManagerResponse(BaseResponse): token = '0' token = int(token) - result = self.ssm_backend.get_all_parameters() + result = self.ssm_backend.describe_parameters( + filters, parameter_filters + ) + response = { 'Parameters': [], } end = token + page_size for parameter in result[token:]: - param_data = parameter.describe_response_object(False) - add = False - - if filters: - for filter in filters: - if filter['Key'] == 'Name': - k = param_data['Name'] - for v in filter['Values']: - if k.startswith(v): - add = True - break - elif filter['Key'] == 'Type': - k = param_data['Type'] - for v in filter['Values']: - if k == v: - add = True - break - elif filter['Key'] == 'KeyId': - k = param_data.get('KeyId') - if k: - for v in filter['Values']: - if k == v: - add = True - break - else: - add = True - - if add: - response['Parameters'].append(param_data) + response['Parameters'].append(parameter.describe_response_object(False)) token = token + 1 if len(response['Parameters']) == page_size: diff --git a/tests/test_apigateway/test_apigateway.py b/tests/test_apigateway/test_apigateway.py index 20cc078b8..42e9d7254 100644 --- a/tests/test_apigateway/test_apigateway.py +++ b/tests/test_apigateway/test_apigateway.py @@ -1029,20 +1029,27 @@ def test_usage_plans(): usage_plan['name'].should.equal(usage_plan_name) usage_plan['apiStages'].should.equal([]) - usage_plan_name = 'TEST-PLAN-2' - usage_plan_description = 'Description' - usage_plan_quota = {'limit': 10, 'period': 'DAY', 'offset': 0} - usage_plan_throttle = {'rateLimit': 2, 'burstLimit': 1} - usage_plan_api_stages = [{'apiId': 'foo', 'stage': 'bar'}] - payload = {'name': usage_plan_name, 'description': usage_plan_description, 'quota': usage_plan_quota, 'throttle': usage_plan_throttle, 'apiStages': usage_plan_api_stages} + payload = { + 'name': 'TEST-PLAN-2', + 'description': 'Description', + 'quota': {'limit': 10, 'period': 'DAY', 'offset': 0}, + 'throttle': {'rateLimit': 2, 'burstLimit': 1}, + 'apiStages': [{'apiId': 'foo', 'stage': 'bar'}], + 'tags': {'tag_key': 'tag_value'}, + } response = client.create_usage_plan(**payload) usage_plan_id = response['id'] usage_plan = client.get_usage_plan(usagePlanId=usage_plan_id) - usage_plan['name'].should.equal(usage_plan_name) - usage_plan['description'].should.equal(usage_plan_description) - usage_plan['apiStages'].should.equal(usage_plan_api_stages) - usage_plan['throttle'].should.equal(usage_plan_throttle) - usage_plan['quota'].should.equal(usage_plan_quota) + + # The payload should remain unchanged + for key, value in payload.items(): + usage_plan.should.have.key(key).which.should.equal(value) + + # Status code should be 200 + usage_plan['ResponseMetadata'].should.have.key('HTTPStatusCode').which.should.equal(200) + + # An Id should've been generated + usage_plan.should.have.key('id').which.should_not.be.none response = client.get_usage_plans() len(response['items']).should.equal(2) diff --git a/tests/test_dynamodb2/test_dynamodb.py b/tests/test_dynamodb2/test_dynamodb.py index d3997a59e..46d5f8f37 100644 --- a/tests/test_dynamodb2/test_dynamodb.py +++ b/tests/test_dynamodb2/test_dynamodb.py @@ -1995,6 +1995,23 @@ def test_condition_expressions(): }, ) + with assert_raises(client.exceptions.ConditionalCheckFailedException): + client.delete_item( + TableName = 'test1', + Key = { + 'client': {'S': 'client1'}, + 'app': {'S': 'app1'}, + }, + ConditionExpression = 'attribute_not_exists(#existing)', + ExpressionAttributeValues = { + ':match': {'S': 'match'} + }, + ExpressionAttributeNames = { + '#existing': 'existing', + '#match': 'match', + }, + ) + @mock_dynamodb2 def test_condition_expression__attr_doesnt_exist(): diff --git a/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py b/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py index 1880c7cab..b2209d990 100644 --- a/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py +++ b/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py @@ -450,6 +450,70 @@ def test_update_item_remove(): }) +@mock_dynamodb2_deprecated +def test_update_item_nested_remove(): + conn = boto.dynamodb2.connect_to_region("us-east-1") + table = Table.create('messages', schema=[ + HashKey('username') + ]) + + data = { + 'username': "steve", + 'Meta': { + 'FullName': 'Steve Urkel' + } + } + table.put_item(data=data) + key_map = { + 'username': {"S": "steve"} + } + + # Then remove the Meta.FullName field + conn.update_item("messages", key_map, + update_expression="REMOVE Meta.FullName") + + returned_item = table.get_item(username="steve") + dict(returned_item).should.equal({ + 'username': "steve", + 'Meta': {} + }) + + +@mock_dynamodb2_deprecated +def test_update_item_double_nested_remove(): + conn = boto.dynamodb2.connect_to_region("us-east-1") + table = Table.create('messages', schema=[ + HashKey('username') + ]) + + data = { + 'username': "steve", + 'Meta': { + 'Name': { + 'First': 'Steve', + 'Last': 'Urkel' + } + } + } + table.put_item(data=data) + key_map = { + 'username': {"S": "steve"} + } + + # Then remove the Meta.FullName field + conn.update_item("messages", key_map, + update_expression="REMOVE Meta.Name.First") + + returned_item = table.get_item(username="steve") + dict(returned_item).should.equal({ + 'username': "steve", + 'Meta': { + 'Name': { + 'Last': 'Urkel' + } + } + }) + @mock_dynamodb2_deprecated def test_update_item_set(): conn = boto.dynamodb2.connect_to_region("us-east-1") diff --git a/tests/test_s3/test_s3_lifecycle.py b/tests/test_s3/test_s3_lifecycle.py index 3d533a641..5b05fe518 100644 --- a/tests/test_s3/test_s3_lifecycle.py +++ b/tests/test_s3/test_s3_lifecycle.py @@ -59,15 +59,43 @@ def test_lifecycle_with_filters(): with assert_raises(KeyError): assert result["Rules"][0]["Prefix"] - # With a tag: - lfc["Rules"][0]["Filter"]["Tag"] = { - "Key": "mytag", - "Value": "mytagvalue" + # Without any prefixes and an empty filter (this is by default a prefix for the whole bucket): + lfc = { + "Rules": [ + { + "Expiration": { + "Days": 7 + }, + "ID": "wholebucket", + "Filter": {}, + "Status": "Enabled" + } + ] } client.put_bucket_lifecycle_configuration(Bucket="bucket", LifecycleConfiguration=lfc) result = client.get_bucket_lifecycle_configuration(Bucket="bucket") assert len(result["Rules"]) == 1 - assert result["Rules"][0]["Filter"]["Prefix"] == '' + with assert_raises(KeyError): + assert result["Rules"][0]["Prefix"] + + # If we remove the filter -- and don't specify a Prefix, then this is bad: + lfc['Rules'][0].pop('Filter') + with assert_raises(ClientError) as err: + client.put_bucket_lifecycle_configuration(Bucket="bucket", LifecycleConfiguration=lfc) + assert err.exception.response["Error"]["Code"] == "MalformedXML" + + # With a tag: + lfc["Rules"][0]["Filter"] = { + 'Tag': { + "Key": "mytag", + "Value": "mytagvalue" + } + } + client.put_bucket_lifecycle_configuration(Bucket="bucket", LifecycleConfiguration=lfc) + result = client.get_bucket_lifecycle_configuration(Bucket="bucket") + assert len(result["Rules"]) == 1 + with assert_raises(KeyError): + assert result["Rules"][0]["Filter"]['Prefix'] assert not result["Rules"][0]["Filter"].get("And") assert result["Rules"][0]["Filter"]["Tag"]["Key"] == "mytag" assert result["Rules"][0]["Filter"]["Tag"]["Value"] == "mytagvalue" @@ -75,25 +103,25 @@ def test_lifecycle_with_filters(): assert result["Rules"][0]["Prefix"] # With And (single tag): - lfc["Rules"][0]["Filter"]["And"] = { - "Prefix": "some/prefix", - "Tags": [ - { - "Key": "mytag", - "Value": "mytagvalue" - } - ] + lfc["Rules"][0]["Filter"] = { + "And": { + "Prefix": "some/prefix", + "Tags": [ + { + "Key": "mytag", + "Value": "mytagvalue" + } + ] + } } client.put_bucket_lifecycle_configuration(Bucket="bucket", LifecycleConfiguration=lfc) result = client.get_bucket_lifecycle_configuration(Bucket="bucket") assert len(result["Rules"]) == 1 - assert result["Rules"][0]["Filter"]["Prefix"] == "" + assert not result["Rules"][0]["Filter"].get("Prefix") assert result["Rules"][0]["Filter"]["And"]["Prefix"] == "some/prefix" assert len(result["Rules"][0]["Filter"]["And"]["Tags"]) == 1 assert result["Rules"][0]["Filter"]["And"]["Tags"][0]["Key"] == "mytag" assert result["Rules"][0]["Filter"]["And"]["Tags"][0]["Value"] == "mytagvalue" - assert result["Rules"][0]["Filter"]["Tag"]["Key"] == "mytag" - assert result["Rules"][0]["Filter"]["Tag"]["Value"] == "mytagvalue" with assert_raises(KeyError): assert result["Rules"][0]["Prefix"] @@ -114,17 +142,39 @@ def test_lifecycle_with_filters(): client.put_bucket_lifecycle_configuration(Bucket="bucket", LifecycleConfiguration=lfc) result = client.get_bucket_lifecycle_configuration(Bucket="bucket") assert len(result["Rules"]) == 1 - assert result["Rules"][0]["Filter"]["Prefix"] == "" + assert not result["Rules"][0]["Filter"].get("Prefix") assert result["Rules"][0]["Filter"]["And"]["Prefix"] == "some/prefix" assert len(result["Rules"][0]["Filter"]["And"]["Tags"]) == 2 assert result["Rules"][0]["Filter"]["And"]["Tags"][0]["Key"] == "mytag" assert result["Rules"][0]["Filter"]["And"]["Tags"][0]["Value"] == "mytagvalue" - assert result["Rules"][0]["Filter"]["Tag"]["Key"] == "mytag" - assert result["Rules"][0]["Filter"]["Tag"]["Value"] == "mytagvalue" assert result["Rules"][0]["Filter"]["And"]["Tags"][1]["Key"] == "mytag2" assert result["Rules"][0]["Filter"]["And"]["Tags"][1]["Value"] == "mytagvalue2" - assert result["Rules"][0]["Filter"]["Tag"]["Key"] == "mytag" - assert result["Rules"][0]["Filter"]["Tag"]["Value"] == "mytagvalue" + with assert_raises(KeyError): + assert result["Rules"][0]["Prefix"] + + # And filter without Prefix but multiple Tags: + lfc["Rules"][0]["Filter"]["And"] = { + "Tags": [ + { + "Key": "mytag", + "Value": "mytagvalue" + }, + { + "Key": "mytag2", + "Value": "mytagvalue2" + } + ] + } + client.put_bucket_lifecycle_configuration(Bucket="bucket", LifecycleConfiguration=lfc) + result = client.get_bucket_lifecycle_configuration(Bucket="bucket") + assert len(result["Rules"]) == 1 + with assert_raises(KeyError): + assert result["Rules"][0]["Filter"]["And"]["Prefix"] + assert len(result["Rules"][0]["Filter"]["And"]["Tags"]) == 2 + assert result["Rules"][0]["Filter"]["And"]["Tags"][0]["Key"] == "mytag" + assert result["Rules"][0]["Filter"]["And"]["Tags"][0]["Value"] == "mytagvalue" + assert result["Rules"][0]["Filter"]["And"]["Tags"][1]["Key"] == "mytag2" + assert result["Rules"][0]["Filter"]["And"]["Tags"][1]["Value"] == "mytagvalue2" with assert_raises(KeyError): assert result["Rules"][0]["Prefix"] @@ -146,6 +196,73 @@ def test_lifecycle_with_filters(): assert not result["Rules"][0].get("Filter") assert result["Rules"][0]["Prefix"] == "some/path" + # Can't have Tag, Prefix, and And in a filter: + del lfc['Rules'][0]['Prefix'] + lfc["Rules"][0]["Filter"] = { + "Prefix": "some/prefix", + "Tag": { + "Key": "mytag", + "Value": "mytagvalue" + } + } + with assert_raises(ClientError) as err: + client.put_bucket_lifecycle_configuration(Bucket="bucket", LifecycleConfiguration=lfc) + assert err.exception.response["Error"]["Code"] == "MalformedXML" + + lfc["Rules"][0]["Filter"] = { + "Tag": { + "Key": "mytag", + "Value": "mytagvalue" + }, + "And": { + "Prefix": "some/prefix", + "Tags": [ + { + "Key": "mytag", + "Value": "mytagvalue" + }, + { + "Key": "mytag2", + "Value": "mytagvalue2" + } + ] + } + } + with assert_raises(ClientError) as err: + client.put_bucket_lifecycle_configuration(Bucket="bucket", LifecycleConfiguration=lfc) + assert err.exception.response["Error"]["Code"] == "MalformedXML" + + # Make sure multiple rules work: + lfc = { + "Rules": [ + { + "Expiration": { + "Days": 7 + }, + "ID": "wholebucket", + "Filter": { + "Prefix": "" + }, + "Status": "Enabled" + }, + { + "Expiration": { + "Days": 10 + }, + "ID": "Tags", + "Filter": { + "Tag": {'Key': 'somekey', 'Value': 'somevalue'} + }, + "Status": "Enabled" + } + ] + } + client.put_bucket_lifecycle_configuration(Bucket="bucket", LifecycleConfiguration=lfc) + result = client.get_bucket_lifecycle_configuration(Bucket="bucket")['Rules'] + assert len(result) == 2 + assert result[0]['ID'] == 'wholebucket' + assert result[1]['ID'] == 'Tags' + @mock_s3 def test_lifecycle_with_eodm(): diff --git a/tests/test_ssm/test_ssm_boto3.py b/tests/test_ssm/test_ssm_boto3.py index 77d439d83..33870e383 100644 --- a/tests/test_ssm/test_ssm_boto3.py +++ b/tests/test_ssm/test_ssm_boto3.py @@ -7,7 +7,7 @@ import datetime import uuid import json -from botocore.exceptions import ClientError +from botocore.exceptions import ClientError, ParamValidationError from nose.tools import assert_raises from moto import mock_ssm, mock_cloudformation @@ -336,10 +336,11 @@ def test_describe_parameters(): response = client.describe_parameters() - len(response['Parameters']).should.equal(1) - response['Parameters'][0]['Name'].should.equal('test') - response['Parameters'][0]['Type'].should.equal('String') - response['Parameters'][0]['AllowedPattern'].should.equal(r'.*') + parameters = response['Parameters'] + parameters.should.have.length_of(1) + parameters[0]['Name'].should.equal('test') + parameters[0]['Type'].should.equal('String') + parameters[0]['AllowedPattern'].should.equal(r'.*') @mock_ssm @@ -354,28 +355,28 @@ def test_describe_parameters_paging(): ) response = client.describe_parameters() - len(response['Parameters']).should.equal(10) + response['Parameters'].should.have.length_of(10) response['NextToken'].should.equal('10') response = client.describe_parameters(NextToken=response['NextToken']) - len(response['Parameters']).should.equal(10) + response['Parameters'].should.have.length_of(10) response['NextToken'].should.equal('20') response = client.describe_parameters(NextToken=response['NextToken']) - len(response['Parameters']).should.equal(10) + response['Parameters'].should.have.length_of(10) response['NextToken'].should.equal('30') response = client.describe_parameters(NextToken=response['NextToken']) - len(response['Parameters']).should.equal(10) + response['Parameters'].should.have.length_of(10) response['NextToken'].should.equal('40') response = client.describe_parameters(NextToken=response['NextToken']) - len(response['Parameters']).should.equal(10) + response['Parameters'].should.have.length_of(10) response['NextToken'].should.equal('50') response = client.describe_parameters(NextToken=response['NextToken']) - len(response['Parameters']).should.equal(0) - ''.should.equal(response.get('NextToken', '')) + response['Parameters'].should.have.length_of(0) + response.should_not.have.key('NextToken') @mock_ssm @@ -399,10 +400,12 @@ def test_describe_parameters_filter_names(): 'Values': ['param-22'] }, ]) - len(response['Parameters']).should.equal(1) - response['Parameters'][0]['Name'].should.equal('param-22') - response['Parameters'][0]['Type'].should.equal('String') - ''.should.equal(response.get('NextToken', '')) + + parameters = response['Parameters'] + parameters.should.have.length_of(1) + parameters[0]['Name'].should.equal('param-22') + parameters[0]['Type'].should.equal('String') + response.should_not.have.key('NextToken') @mock_ssm @@ -426,9 +429,11 @@ def test_describe_parameters_filter_type(): 'Values': ['SecureString'] }, ]) - len(response['Parameters']).should.equal(10) - response['Parameters'][0]['Type'].should.equal('SecureString') - '10'.should.equal(response.get('NextToken', '')) + + parameters = response['Parameters'] + parameters.should.have.length_of(10) + parameters[0]['Type'].should.equal('SecureString') + response.should.have.key('NextToken').which.should.equal('10') @mock_ssm @@ -452,10 +457,508 @@ def test_describe_parameters_filter_keyid(): 'Values': ['key:10'] }, ]) - len(response['Parameters']).should.equal(1) - response['Parameters'][0]['Name'].should.equal('param-10') - response['Parameters'][0]['Type'].should.equal('SecureString') - ''.should.equal(response.get('NextToken', '')) + + parameters = response['Parameters'] + parameters.should.have.length_of(1) + parameters[0]['Name'].should.equal('param-10') + parameters[0]['Type'].should.equal('SecureString') + response.should_not.have.key('NextToken') + + +@mock_ssm +def test_describe_parameters_with_parameter_filters_keyid(): + client = boto3.client('ssm', region_name='us-east-1') + client.put_parameter( + Name='secure-param', + Value='secure-value', + Type='SecureString' + ) + client.put_parameter( + Name='custom-secure-param', + Value='custom-secure-value', + Type='SecureString', + KeyId='alias/custom' + ) + client.put_parameter( + Name = 'param', + Value = 'value', + Type = 'String' + ) + + response = client.describe_parameters( + ParameterFilters=[{ + 'Key': 'KeyId', + 'Values': ['alias/aws/ssm'] + }] + ) + + parameters = response['Parameters'] + parameters.should.have.length_of(1) + parameters[0]['Name'].should.equal('secure-param') + parameters[0]['Type'].should.equal('SecureString') + response.should_not.have.key('NextToken') + + response = client.describe_parameters( + ParameterFilters = [{ + 'Key': 'KeyId', + 'Values': ['alias/custom'] + }] + ) + + parameters = response['Parameters'] + parameters.should.have.length_of(1) + parameters[0]['Name'].should.equal('custom-secure-param') + parameters[0]['Type'].should.equal('SecureString') + response.should_not.have.key('NextToken') + + response = client.describe_parameters( + ParameterFilters = [{ + 'Key': 'KeyId', + 'Option': 'BeginsWith', + 'Values': ['alias'] + }] + ) + + parameters = response['Parameters'] + parameters.should.have.length_of(2) + response.should_not.have.key('NextToken') + + +@mock_ssm +def test_describe_parameters_with_parameter_filters_name(): + client = boto3.client('ssm', region_name='us-east-1') + client.put_parameter( + Name='param', + Value='value', + Type='String' + ) + client.put_parameter( + Name = '/param-2', + Value = 'value-2', + Type = 'String' + ) + + response = client.describe_parameters( + ParameterFilters=[{ + 'Key': 'Name', + 'Values': ['param'] + }] + ) + + parameters = response['Parameters'] + parameters.should.have.length_of(1) + parameters[0]['Name'].should.equal('param') + parameters[0]['Type'].should.equal('String') + response.should_not.have.key('NextToken') + + response = client.describe_parameters( + ParameterFilters=[{ + 'Key': 'Name', + 'Values': ['/param'] + }] + ) + + parameters = response['Parameters'] + parameters.should.have.length_of(1) + parameters[0]['Name'].should.equal('param') + parameters[0]['Type'].should.equal('String') + response.should_not.have.key('NextToken') + + response = client.describe_parameters( + ParameterFilters=[{ + 'Key': 'Name', + 'Values': ['param-2'] + }] + ) + + parameters = response['Parameters'] + parameters.should.have.length_of(1) + parameters[0]['Name'].should.equal('/param-2') + parameters[0]['Type'].should.equal('String') + response.should_not.have.key('NextToken') + + response = client.describe_parameters( + ParameterFilters = [{ + 'Key': 'Name', + 'Option': 'BeginsWith', + 'Values': ['param'] + }] + ) + + parameters = response['Parameters'] + parameters.should.have.length_of(2) + response.should_not.have.key('NextToken') + + +@mock_ssm +def test_describe_parameters_with_parameter_filters_path(): + client = boto3.client('ssm', region_name='us-east-1') + client.put_parameter( + Name='/foo/name1', + Value='value1', + Type='String') + + client.put_parameter( + Name='/foo/name2', + Value='value2', + Type='String') + + client.put_parameter( + Name='/bar/name3', + Value='value3', + Type='String') + + client.put_parameter( + Name='/bar/name3/name4', + Value='value4', + Type='String') + + client.put_parameter( + Name='foo', + Value='bar', + Type='String') + + response = client.describe_parameters( + ParameterFilters = [{ + 'Key': 'Path', + 'Values': ['/fo'] + }] + ) + + parameters = response['Parameters'] + parameters.should.have.length_of(0) + response.should_not.have.key('NextToken') + + response = client.describe_parameters( + ParameterFilters = [{ + 'Key': 'Path', + 'Values': ['/'] + }] + ) + + parameters = response['Parameters'] + parameters.should.have.length_of(1) + parameters[0]['Name'].should.equal('foo') + parameters[0]['Type'].should.equal('String') + response.should_not.have.key('NextToken') + + response = client.describe_parameters( + ParameterFilters = [{ + 'Key': 'Path', + 'Values': ['/', '/foo'] + }] + ) + + parameters = response['Parameters'] + parameters.should.have.length_of(3) + {parameter['Name'] for parameter in response['Parameters']}.should.equal( + {'/foo/name1', '/foo/name2', 'foo'} + ) + response.should_not.have.key('NextToken') + + response = client.describe_parameters( + ParameterFilters = [{ + 'Key': 'Path', + 'Values': ['/foo/'] + }] + ) + + parameters = response['Parameters'] + parameters.should.have.length_of(2) + {parameter['Name'] for parameter in response['Parameters']}.should.equal( + {'/foo/name1', '/foo/name2'} + ) + response.should_not.have.key('NextToken') + + response = client.describe_parameters( + ParameterFilters = [{ + 'Key': 'Path', + 'Option': 'OneLevel', + 'Values': ['/bar/name3'] + }] + ) + + parameters = response['Parameters'] + parameters.should.have.length_of(1) + parameters[0]['Name'].should.equal('/bar/name3/name4') + parameters[0]['Type'].should.equal('String') + response.should_not.have.key('NextToken') + + + response = client.describe_parameters( + ParameterFilters = [{ + 'Key': 'Path', + 'Option': 'Recursive', + 'Values': ['/fo'] + }] + ) + + parameters = response['Parameters'] + parameters.should.have.length_of(0) + response.should_not.have.key('NextToken') + + response = client.describe_parameters( + ParameterFilters = [{ + 'Key': 'Path', + 'Option': 'Recursive', + 'Values': ['/'] + }] + ) + + parameters = response['Parameters'] + parameters.should.have.length_of(5) + response.should_not.have.key('NextToken') + + response = client.describe_parameters( + ParameterFilters = [{ + 'Key': 'Path', + 'Option': 'Recursive', + 'Values': ['/foo', '/bar'] + }] + ) + + parameters = response['Parameters'] + parameters.should.have.length_of(4) + {parameter['Name'] for parameter in response['Parameters']}.should.equal( + {'/foo/name1', '/foo/name2', '/bar/name3', '/bar/name3/name4'} + ) + response.should_not.have.key('NextToken') + + response = client.describe_parameters( + ParameterFilters = [{ + 'Key': 'Path', + 'Option': 'Recursive', + 'Values': ['/foo/'] + }] + ) + + parameters = response['Parameters'] + parameters.should.have.length_of(2) + {parameter['Name'] for parameter in response['Parameters']}.should.equal( + {'/foo/name1', '/foo/name2'} + ) + response.should_not.have.key('NextToken') + + response = client.describe_parameters( + ParameterFilters = [{ + 'Key': 'Path', + 'Option': 'Recursive', + 'Values': ['/bar/name3'] + }] + ) + + parameters = response['Parameters'] + parameters.should.have.length_of(1) + parameters[0]['Name'].should.equal('/bar/name3/name4') + parameters[0]['Type'].should.equal('String') + response.should_not.have.key('NextToken') + + +@mock_ssm +def test_describe_parameters_invalid_parameter_filters(): + client = boto3.client('ssm', region_name='us-east-1') + + client.describe_parameters.when.called_with( + Filters=[{ + 'Key': 'Name', + 'Values': ['test'] + }], + ParameterFilters=[{ + 'Key': 'Name', + 'Values': ['test'] + }] + ).should.throw( + ClientError, + 'You can use either Filters or ParameterFilters in a single request.' + ) + + client.describe_parameters.when.called_with( + ParameterFilters=[{}] + ).should.throw( + ParamValidationError, + 'Parameter validation failed:\nMissing required parameter in ParameterFilters[0]: "Key"' + ) + + client.describe_parameters.when.called_with( + ParameterFilters=[{ + 'Key': 'key', + }] + ).should.throw( + ClientError, + '1 validation error detected: Value "key" at "parameterFilters.1.member.key" failed to satisfy constraint: ' + 'Member must satisfy regular expression pattern: tag:.+|Name|Type|KeyId|Path|Label|Tier' + ) + + long_key = 'tag:' + 't' * 129 + client.describe_parameters.when.called_with( + ParameterFilters=[{ + 'Key': long_key, + }] + ).should.throw( + ClientError, + '1 validation error detected: Value "{value}" at "parameterFilters.1.member.key" failed to satisfy constraint: ' + 'Member must have length less than or equal to 132'.format(value=long_key) + ) + + client.describe_parameters.when.called_with( + ParameterFilters=[{ + 'Key': 'Name', + 'Option': 'over 10 chars' + }] + ).should.throw( + ClientError, + '1 validation error detected: Value "over 10 chars" at "parameterFilters.1.member.option" failed to satisfy constraint: ' + 'Member must have length less than or equal to 10' + ) + + many_values = ['test'] * 51 + client.describe_parameters.when.called_with( + ParameterFilters=[{ + 'Key': 'Name', + 'Values': many_values + }] + ).should.throw( + ClientError, + '1 validation error detected: Value "{value}" at "parameterFilters.1.member.values" failed to satisfy constraint: ' + 'Member must have length less than or equal to 50'.format(value=many_values) + ) + + long_value = ['t' * 1025] + client.describe_parameters.when.called_with( + ParameterFilters=[{ + 'Key': 'Name', + 'Values': long_value + }] + ).should.throw( + ClientError, + '1 validation error detected: Value "{value}" at "parameterFilters.1.member.values" failed to satisfy constraint: ' + '[Member must have length less than or equal to 1024, Member must have length greater than or equal to 1]'.format(value=long_value) + ) + + client.describe_parameters.when.called_with( + ParameterFilters=[{ + 'Key': 'Name', + 'Option': 'over 10 chars' + },{ + 'Key': 'key', + }] + ).should.throw( + ClientError, + '2 validation errors detected: ' + 'Value "over 10 chars" at "parameterFilters.1.member.option" failed to satisfy constraint: ' + 'Member must have length less than or equal to 10; ' + 'Value "key" at "parameterFilters.2.member.key" failed to satisfy constraint: ' + 'Member must satisfy regular expression pattern: tag:.+|Name|Type|KeyId|Path|Label|Tier' + ) + + client.describe_parameters.when.called_with( + ParameterFilters=[{ + 'Key': 'Label', + }] + ).should.throw( + ClientError, + 'The following filter key is not valid: Label. Valid filter keys include: [Path, Name, Type, KeyId, Tier].' + ) + + client.describe_parameters.when.called_with( + ParameterFilters=[{ + 'Key': 'Name', + }] + ).should.throw( + ClientError, + 'The following filter values are missing : null for filter key Name.' + ) + + client.describe_parameters.when.called_with( + ParameterFilters=[{ + 'Key': 'Name', + 'Values': [] + }] + ).should.throw( + ParamValidationError, + 'Invalid length for parameter ParameterFilters[0].Values, value: 0, valid range: 1-inf' + ) + + client.describe_parameters.when.called_with( + ParameterFilters=[{ + 'Key': 'Name', + 'Values': ['test'] + },{ + 'Key': 'Name', + 'Values': ['test test'] + }] + ).should.throw( + ClientError, + 'The following filter is duplicated in the request: Name. A request can contain only one occurrence of a specific filter.' + ) + + for value in ['/###', '//', 'test']: + client.describe_parameters.when.called_with( + ParameterFilters=[{ + 'Key': 'Path', + 'Values': [value] + }] + ).should.throw( + ClientError, + 'The parameter doesn\'t meet the parameter name requirements. The parameter name must begin with a forward slash "/". ' + 'It can\'t be prefixed with \"aws\" or \"ssm\" (case-insensitive). ' + 'It must use only letters, numbers, or the following symbols: . (period), - (hyphen), _ (underscore). ' + 'Special characters are not allowed. All sub-paths, if specified, must use the forward slash symbol "/". ' + 'Valid example: /get/parameters2-/by1./path0_.' + ) + + client.describe_parameters.when.called_with( + ParameterFilters=[{ + 'Key': 'Path', + 'Values': ['/aws', '/ssm'] + }] + ).should.throw( + ClientError, + 'Filters for common parameters can\'t be prefixed with "aws" or "ssm" (case-insensitive). ' + 'When using global parameters, please specify within a global namespace.' + ) + + client.describe_parameters.when.called_with( + ParameterFilters=[{ + 'Key': 'Path', + 'Option': 'Equals', + 'Values': ['test'] + }] + ).should.throw( + ClientError, + 'The following filter option is not valid: Equals. Valid options include: [Recursive, OneLevel].' + ) + + client.describe_parameters.when.called_with( + ParameterFilters=[{ + 'Key': 'Tier', + 'Values': ['test'] + }] + ).should.throw( + ClientError, + 'The following filter value is not valid: test. Valid values include: [Standard, Advanced, Intelligent-Tiering]' + ) + + client.describe_parameters.when.called_with( + ParameterFilters=[{ + 'Key': 'Type', + 'Values': ['test'] + }] + ).should.throw( + ClientError, + 'The following filter value is not valid: test. Valid values include: [String, StringList, SecureString]' + ) + + client.describe_parameters.when.called_with( + ParameterFilters=[{ + 'Key': 'Name', + 'Option': 'option', + 'Values': ['test'] + }] + ).should.throw( + ClientError, + 'The following filter option is not valid: option. Valid options include: [BeginsWith, Equals].' + ) @mock_ssm @@ -476,15 +979,17 @@ def test_describe_parameters_attributes(): ) response = client.describe_parameters() - len(response['Parameters']).should.equal(2) - response['Parameters'][0]['Description'].should.equal('my description') - response['Parameters'][0]['Version'].should.equal(1) - response['Parameters'][0]['LastModifiedDate'].should.be.a(datetime.date) - response['Parameters'][0]['LastModifiedUser'].should.equal('N/A') + parameters = response['Parameters'] + parameters.should.have.length_of(2) - response['Parameters'][1].get('Description').should.be.none - response['Parameters'][1]['Version'].should.equal(1) + parameters[0]['Description'].should.equal('my description') + parameters[0]['Version'].should.equal(1) + parameters[0]['LastModifiedDate'].should.be.a(datetime.date) + parameters[0]['LastModifiedUser'].should.equal('N/A') + + parameters[1].should_not.have.key('Description') + parameters[1]['Version'].should.equal(1) @mock_ssm @@ -519,7 +1024,7 @@ def test_put_parameter_secure_default_kms(): len(response['Parameters']).should.equal(1) response['Parameters'][0]['Name'].should.equal('test') - response['Parameters'][0]['Value'].should.equal('kms:default:value') + response['Parameters'][0]['Value'].should.equal('kms:alias/aws/ssm:value') response['Parameters'][0]['Type'].should.equal('SecureString') response = client.get_parameters(