diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index 57f169b8a..284f4d68a 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -3163,7 +3163,7 @@ - [ ] describe_events ## iam -55% implemented +57% implemented - [ ] add_client_id_to_open_id_connect_provider - [X] add_role_to_instance_profile - [X] add_user_to_group @@ -3176,7 +3176,7 @@ - [X] create_group - [X] create_instance_profile - [X] create_login_profile -- [ ] create_open_id_connect_provider +- [X] create_open_id_connect_provider - [X] create_policy - [X] create_policy_version - [X] create_role @@ -3193,8 +3193,8 @@ - [ ] delete_group_policy - [ ] delete_instance_profile - [X] delete_login_profile -- [ ] delete_open_id_connect_provider -- [ ] delete_policy +- [X] delete_open_id_connect_provider +- [X] delete_policy - [X] delete_policy_version - [X] delete_role - [ ] delete_role_permissions_boundary @@ -3227,7 +3227,7 @@ - [X] get_group_policy - [X] get_instance_profile - [X] get_login_profile -- [ ] get_open_id_connect_provider +- [X] get_open_id_connect_provider - [ ] get_organizations_access_report - [X] get_policy - [X] get_policy_version @@ -3253,7 +3253,7 @@ - [ ] list_instance_profiles - [ ] list_instance_profiles_for_role - [X] list_mfa_devices -- [ ] list_open_id_connect_providers +- [X] list_open_id_connect_providers - [X] list_policies - [ ] list_policies_granting_service_access - [X] list_policy_versions @@ -5902,7 +5902,7 @@ - [x] untag_resource ## sqs -65% implemented +75% implemented - [X] add_permission - [X] change_message_visibility - [ ] change_message_visibility_batch @@ -5913,13 +5913,13 @@ - [ ] get_queue_attributes - [ ] get_queue_url - [X] list_dead_letter_source_queues -- [ ] list_queue_tags +- [x] list_queue_tags - [X] list_queues - [X] purge_queue - [ ] receive_message - [X] remove_permission - [X] send_message -- [ ] send_message_batch +- [x] send_message_batch - [X] set_queue_attributes - [X] tag_queue - [X] untag_queue diff --git a/moto/dynamodb2/responses.py b/moto/dynamodb2/responses.py index 333213c2a..b5e5e11a8 100644 --- a/moto/dynamodb2/responses.py +++ b/moto/dynamodb2/responses.py @@ -457,6 +457,9 @@ class DynamoHandler(BaseResponse): range_comparison = None range_values = [] + if '=' not in hash_key_expression: + return self.error('com.amazonaws.dynamodb.v20111205#ValidationException', + 'Query key condition not supported') hash_key_value_alias = hash_key_expression.split("=")[1].strip() # Temporary fix until we get proper KeyConditionExpression function hash_key = value_alias_map.get(hash_key_value_alias, {'S': hash_key_value_alias}) diff --git a/moto/ec2/utils.py b/moto/ec2/utils.py index e67cb39f4..a718c7812 100644 --- a/moto/ec2/utils.py +++ b/moto/ec2/utils.py @@ -50,7 +50,7 @@ EC2_PREFIX_TO_RESOURCE = dict((v, k) for (k, v) in EC2_RESOURCE_TO_PREFIX.items( def random_resource_id(size=8): chars = list(range(10)) + ['a', 'b', 'c', 'd', 'e', 'f'] - resource_id = ''.join(six.text_type(random.choice(chars)) for x in range(size)) + resource_id = ''.join(six.text_type(random.choice(chars)) for _ in range(size)) return resource_id @@ -460,8 +460,8 @@ def generic_filter(filters, objects): def simple_aws_filter_to_re(filter_string): - tmp_filter = filter_string.replace('\?', '[?]') - tmp_filter = tmp_filter.replace('\*', '[*]') + tmp_filter = filter_string.replace(r'\?', '[?]') + tmp_filter = tmp_filter.replace(r'\*', '[*]') tmp_filter = fnmatch.translate(tmp_filter) return tmp_filter @@ -491,7 +491,7 @@ def get_prefix(resource_id): 'network-interface-attachment'] if resource_id_prefix not in EC2_RESOURCE_TO_PREFIX.values(): uuid4hex = re.compile( - '[0-9a-f]{12}4[0-9a-f]{3}[89ab][0-9a-f]{15}\Z', re.I) + r'[0-9a-f]{12}4[0-9a-f]{3}[89ab][0-9a-f]{15}\Z', re.I) if uuid4hex.match(resource_id) is not None: resource_id_prefix = EC2_RESOURCE_TO_PREFIX['reserved-instance'] else: @@ -510,7 +510,7 @@ def is_valid_resource_id(resource_id): def is_valid_cidr(cird): - cidr_pattern = '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/(\d|[1-2]\d|3[0-2]))$' + cidr_pattern = r'^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/(\d|[1-2]\d|3[0-2]))$' cidr_pattern_re = re.compile(cidr_pattern) return cidr_pattern_re.match(cird) is not None diff --git a/moto/iam/exceptions.py b/moto/iam/exceptions.py index ac08e0d88..0511fa144 100644 --- a/moto/iam/exceptions.py +++ b/moto/iam/exceptions.py @@ -93,3 +93,27 @@ class TooManyTags(RESTError): super(TooManyTags, self).__init__( 'ValidationError', "1 validation error detected: Value '{}' at '{}' failed to satisfy " "constraint: Member must have length less than or equal to 50.".format(tags, param)) + + +class EntityAlreadyExists(RESTError): + code = 409 + + def __init__(self): + super(EntityAlreadyExists, self).__init__( + 'EntityAlreadyExists', "Unknown") + + +class ValidationError(RESTError): + code = 400 + + def __init__(self, message): + super(ValidationError, self).__init__( + 'ValidationError', message) + + +class InvalidInput(RESTError): + code = 400 + + def __init__(self, message): + super(InvalidInput, self).__init__( + 'InvalidInput', message) diff --git a/moto/iam/models.py b/moto/iam/models.py index d76df8a28..ba7d0ac82 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -7,6 +7,7 @@ import re from cryptography import x509 from cryptography.hazmat.backends import default_backend +from six.moves.urllib.parse import urlparse from moto.core.exceptions import RESTError from moto.core import BaseBackend, BaseModel @@ -14,8 +15,9 @@ from moto.core.utils import iso_8601_datetime_without_milliseconds, iso_8601_dat from moto.iam.policy_validation import IAMPolicyDocumentValidator from .aws_managed_policies import aws_managed_policies_data -from .exceptions import IAMNotFoundException, IAMConflictException, IAMReportNotPresentException, IAMLimitExceededException, \ - MalformedCertificate, DuplicateTags, TagKeyTooBig, InvalidTagCharacters, TooManyTags, TagValueTooBig +from .exceptions import (IAMNotFoundException, IAMConflictException, IAMReportNotPresentException, IAMLimitExceededException, + MalformedCertificate, DuplicateTags, TagKeyTooBig, InvalidTagCharacters, TooManyTags, TagValueTooBig, + EntityAlreadyExists, ValidationError, InvalidInput) from .utils import random_access_key, random_alphanumeric, random_resource_id, random_policy_id ACCOUNT_ID = 123456789012 @@ -93,6 +95,82 @@ class SAMLProvider(BaseModel): return "arn:aws:iam::{0}:saml-provider/{1}".format(ACCOUNT_ID, self.name) +class OpenIDConnectProvider(BaseModel): + def __init__(self, url, thumbprint_list, client_id_list=None): + self._errors = [] + self._validate(url, thumbprint_list, client_id_list) + + parsed_url = urlparse(url) + self.url = parsed_url.netloc + parsed_url.path + self.thumbprint_list = thumbprint_list + self.client_id_list = client_id_list + self.create_date = datetime.utcnow() + + @property + def arn(self): + return 'arn:aws:iam::{0}:oidc-provider/{1}'.format(ACCOUNT_ID, self.url) + + @property + def created_iso_8601(self): + return iso_8601_datetime_without_milliseconds(self.create_date) + + def _validate(self, url, thumbprint_list, client_id_list): + if any(len(client_id) > 255 for client_id in client_id_list): + self._errors.append(self._format_error( + key='clientIDList', + value=client_id_list, + constraint='Member must satisfy constraint: ' + '[Member must have length less than or equal to 255, ' + 'Member must have length greater than or equal to 1]', + )) + + if any(len(thumbprint) > 40 for thumbprint in thumbprint_list): + self._errors.append(self._format_error( + key='thumbprintList', + value=thumbprint_list, + constraint='Member must satisfy constraint: ' + '[Member must have length less than or equal to 40, ' + 'Member must have length greater than or equal to 40]', + )) + + if len(url) > 255: + self._errors.append(self._format_error( + key='url', + value=url, + constraint='Member must have length less than or equal to 255', + )) + + self._raise_errors() + + parsed_url = urlparse(url) + if not parsed_url.scheme or not parsed_url.netloc: + raise ValidationError('Invalid Open ID Connect Provider URL') + + if len(thumbprint_list) > 5: + raise InvalidInput('Thumbprint list must contain fewer than 5 entries.') + + if len(client_id_list) > 100: + raise IAMLimitExceededException('Cannot exceed quota for ClientIdsPerOpenIdConnectProvider: 100') + + 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 ValidationError('{count} validation error{plural} detected: {errors}'.format( + count=count, plural=plural, errors=errors, + )) + + class PolicyVersion(object): def __init__(self, @@ -515,6 +593,7 @@ class IAMBackend(BaseBackend): self.managed_policies = self._init_managed_policies() self.account_aliases = [] self.saml_providers = {} + self.open_id_providers = {} self.policy_arn_regex = re.compile( r'^arn:aws:iam::[0-9]*:policy/.*$') super(IAMBackend, self).__init__() @@ -1099,6 +1178,9 @@ class IAMBackend(BaseBackend): user = self.get_user(user_name) user.delete_policy(policy_name) + def delete_policy(self, policy_arn): + del self.managed_policies[policy_arn] + def create_access_key(self, user_name=None): user = self.get_user(user_name) key = user.create_access_key() @@ -1261,5 +1343,28 @@ class IAMBackend(BaseBackend): return user return None + def create_open_id_connect_provider(self, url, thumbprint_list, client_id_list): + open_id_provider = OpenIDConnectProvider(url, thumbprint_list, client_id_list) + + if open_id_provider.arn in self.open_id_providers: + raise EntityAlreadyExists + + self.open_id_providers[open_id_provider.arn] = open_id_provider + return open_id_provider + + def delete_open_id_connect_provider(self, arn): + self.open_id_providers.pop(arn, None) + + def get_open_id_connect_provider(self, arn): + open_id_provider = self.open_id_providers.get(arn) + + if not open_id_provider: + raise IAMNotFoundException('OpenIDConnect Provider not found for arn {}'.format(arn)) + + return open_id_provider + + def list_open_id_connect_providers(self): + return list(self.open_id_providers.keys()) + iam_backend = IAMBackend() diff --git a/moto/iam/responses.py b/moto/iam/responses.py index 806dd37f4..1c00d211c 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -604,6 +604,12 @@ class IamResponse(BaseResponse): template = self.response_template(GENERIC_EMPTY_TEMPLATE) return template.render(name='DeleteUser') + def delete_policy(self): + policy_arn = self._get_param('PolicyArn') + iam_backend.delete_policy(policy_arn) + template = self.response_template(GENERIC_EMPTY_TEMPLATE) + return template.render(name='DeletePolicy') + def delete_login_profile(self): user_name = self._get_param('UserName') iam_backend.delete_login_profile(user_name) @@ -749,6 +755,38 @@ class IamResponse(BaseResponse): template = self.response_template(UNTAG_ROLE_TEMPLATE) return template.render() + def create_open_id_connect_provider(self): + open_id_provider_url = self._get_param('Url') + thumbprint_list = self._get_multi_param('ThumbprintList.member') + client_id_list = self._get_multi_param('ClientIDList.member') + + open_id_provider = iam_backend.create_open_id_connect_provider(open_id_provider_url, thumbprint_list, client_id_list) + + template = self.response_template(CREATE_OPEN_ID_CONNECT_PROVIDER_TEMPLATE) + return template.render(open_id_provider=open_id_provider) + + def delete_open_id_connect_provider(self): + open_id_provider_arn = self._get_param('OpenIDConnectProviderArn') + + iam_backend.delete_open_id_connect_provider(open_id_provider_arn) + + template = self.response_template(DELETE_OPEN_ID_CONNECT_PROVIDER_TEMPLATE) + return template.render() + + def get_open_id_connect_provider(self): + open_id_provider_arn = self._get_param('OpenIDConnectProviderArn') + + open_id_provider = iam_backend.get_open_id_connect_provider(open_id_provider_arn) + + template = self.response_template(GET_OPEN_ID_CONNECT_PROVIDER_TEMPLATE) + return template.render(open_id_provider=open_id_provider) + + def list_open_id_connect_providers(self): + open_id_provider_arns = iam_backend.list_open_id_connect_providers() + + template = self.response_template(LIST_OPEN_ID_CONNECT_PROVIDERS_TEMPLATE) + return template.render(open_id_provider_arns=open_id_provider_arns) + LIST_ENTITIES_FOR_POLICY_TEMPLATE = """ @@ -1968,3 +2006,57 @@ UNTAG_ROLE_TEMPLATE = """ + + {{ open_id_provider.arn }} + + + f248366a-4f64-11e4-aefa-bfd6aEXAMPLE + +""" + + +DELETE_OPEN_ID_CONNECT_PROVIDER_TEMPLATE = """ + + b5e49e29-4f64-11e4-aefa-bfd6aEXAMPLE + +""" + + +GET_OPEN_ID_CONNECT_PROVIDER_TEMPLATE = """ + + + {% for thumbprint in open_id_provider.thumbprint_list %} + {{ thumbprint }} + {% endfor %} + + {{ open_id_provider.created_iso_8601 }} + + {% for client_id in open_id_provider.client_id_list %} + {{ client_id }} + {% endfor %} + + {{ open_id_provider.url }} + + + 2c91531b-4f65-11e4-aefa-bfd6aEXAMPLE + +""" + + +LIST_OPEN_ID_CONNECT_PROVIDERS_TEMPLATE = """ + + + {% for open_id_provider_arn in open_id_provider_arns %} + + {{ open_id_provider_arn }} + + {% endfor %} + + + + de2c0228-4f63-11e4-aefa-bfd6aEXAMPLE + +""" diff --git a/moto/rds2/models.py b/moto/rds2/models.py index 4c0daa230..cd56599e6 100644 --- a/moto/rds2/models.py +++ b/moto/rds2/models.py @@ -56,6 +56,8 @@ class Database(BaseModel): else: self.kms_key_id = kwargs.get("kms_key_id") self.storage_type = kwargs.get("storage_type") + if self.storage_type is None: + self.storage_type = Database.default_storage_type(iops=self.iops) self.master_username = kwargs.get('master_username') self.master_user_password = kwargs.get('master_user_password') self.auto_minor_version_upgrade = kwargs.get( @@ -63,6 +65,8 @@ class Database(BaseModel): if self.auto_minor_version_upgrade is None: self.auto_minor_version_upgrade = True self.allocated_storage = kwargs.get('allocated_storage') + if self.allocated_storage is None: + self.allocated_storage = Database.default_allocated_storage(engine=self.engine, storage_type=self.storage_type) self.db_instance_identifier = kwargs.get('db_instance_identifier') self.source_db_identifier = kwargs.get("source_db_identifier") self.db_instance_class = kwargs.get('db_instance_class') @@ -292,6 +296,78 @@ class Database(BaseModel): 'sqlserver-web': 1433, }[engine] + @staticmethod + def default_storage_type(iops): + if iops is None: + return 'gp2' + else: + return 'io1' + + @staticmethod + def default_allocated_storage(engine, storage_type): + return { + 'aurora': { + 'gp2': 0, + 'io1': 0, + 'standard': 0, + }, + 'mysql': { + 'gp2': 20, + 'io1': 100, + 'standard': 5, + }, + 'mariadb': { + 'gp2': 20, + 'io1': 100, + 'standard': 5, + }, + 'postgres': { + 'gp2': 20, + 'io1': 100, + 'standard': 5, + }, + 'oracle-ee': { + 'gp2': 20, + 'io1': 100, + 'standard': 10, + }, + 'oracle-se2': { + 'gp2': 20, + 'io1': 100, + 'standard': 10, + }, + 'oracle-se1': { + 'gp2': 20, + 'io1': 100, + 'standard': 10, + }, + 'oracle-se': { + 'gp2': 20, + 'io1': 100, + 'standard': 10, + }, + 'sqlserver-ee': { + 'gp2': 200, + 'io1': 200, + 'standard': 200, + }, + 'sqlserver-ex': { + 'gp2': 20, + 'io1': 100, + 'standard': 20, + }, + 'sqlserver-se': { + 'gp2': 200, + 'io1': 200, + 'standard': 200, + }, + 'sqlserver-web': { + 'gp2': 20, + 'io1': 100, + 'standard': 20, + }, + }[engine][storage_type] + @classmethod def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): properties = cloudformation_json['Properties'] diff --git a/moto/rds2/responses.py b/moto/rds2/responses.py index 7b8d0b63a..fdba73248 100644 --- a/moto/rds2/responses.py +++ b/moto/rds2/responses.py @@ -42,7 +42,7 @@ class RDS2Response(BaseResponse): "region": self.region, "security_groups": self._get_multi_param('DBSecurityGroups.DBSecurityGroupName'), "storage_encrypted": self._get_param("StorageEncrypted"), - "storage_type": self._get_param("StorageType", 'standard'), + "storage_type": self._get_param("StorageType", None), "vpc_security_group_ids": self._get_multi_param("VpcSecurityGroupIds.VpcSecurityGroupId"), "tags": list(), } diff --git a/moto/s3/models.py b/moto/s3/models.py index ef49d7f95..6210aecd6 100644 --- a/moto/s3/models.py +++ b/moto/s3/models.py @@ -1107,7 +1107,7 @@ class S3Backend(BaseBackend): key = key_version break - if part_number and key.multipart: + if part_number and key and key.multipart: key = key.multipart.parts[part_number] if isinstance(key, FakeKey): @@ -1115,8 +1115,8 @@ class S3Backend(BaseBackend): else: return None - def set_key_tagging(self, bucket_name, key_name, tagging): - key = self.get_key(bucket_name, key_name) + def set_key_tagging(self, bucket_name, key_name, tagging, version_id=None): + key = self.get_key(bucket_name, key_name, version_id) if key is None: raise MissingKey(key_name) key.set_tagging(tagging) diff --git a/moto/s3/responses.py b/moto/s3/responses.py index ae6662579..e0e65850c 100644 --- a/moto/s3/responses.py +++ b/moto/s3/responses.py @@ -905,8 +905,12 @@ class ResponseObject(_TemplateEnvironmentMixin, ActionAuthenticatorMixin): return 200, response_headers, "" if 'tagging' in query: + if 'versionId' in query: + version_id = query['versionId'][0] + else: + version_id = None tagging = self._tagging_from_xml(body) - self.backend.set_key_tagging(bucket_name, key_name, tagging) + self.backend.set_key_tagging(bucket_name, key_name, tagging, version_id) return 200, response_headers, "" if 'x-amz-copy-source' in request.headers: diff --git a/moto/secretsmanager/exceptions.py b/moto/secretsmanager/exceptions.py index fa81b6d8b..7ef1a9239 100644 --- a/moto/secretsmanager/exceptions.py +++ b/moto/secretsmanager/exceptions.py @@ -7,11 +7,32 @@ class SecretsManagerClientError(JsonRESTError): class ResourceNotFoundException(SecretsManagerClientError): - def __init__(self): + def __init__(self, message): self.code = 404 super(ResourceNotFoundException, self).__init__( "ResourceNotFoundException", - "Secrets Manager can't find the specified secret" + message, + ) + + +# Using specialised exception due to the use of a non-ASCII character +class SecretNotFoundException(SecretsManagerClientError): + def __init__(self): + self.code = 404 + super(SecretNotFoundException, self).__init__( + "ResourceNotFoundException", + message=u"Secrets Manager can\u2019t find the specified secret." + ) + + +# Using specialised exception due to the use of a non-ASCII character +class SecretHasNoValueException(SecretsManagerClientError): + def __init__(self, version_stage): + self.code = 404 + super(SecretHasNoValueException, self).__init__( + "ResourceNotFoundException", + message=u"Secrets Manager can\u2019t find the specified secret " + u"value for staging label: {}".format(version_stage) ) diff --git a/moto/secretsmanager/models.py b/moto/secretsmanager/models.py index 63d847c49..e1a380c39 100644 --- a/moto/secretsmanager/models.py +++ b/moto/secretsmanager/models.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from __future__ import unicode_literals import time @@ -9,7 +10,8 @@ import boto3 from moto.core import BaseBackend, BaseModel from .exceptions import ( - ResourceNotFoundException, + SecretNotFoundException, + SecretHasNoValueException, InvalidParameterException, ResourceExistsException, InvalidRequestException, @@ -46,7 +48,7 @@ class SecretsManagerBackend(BaseBackend): def get_secret_value(self, secret_id, version_id, version_stage): if not self._is_valid_identifier(secret_id): - raise ResourceNotFoundException() + raise SecretNotFoundException() if not version_id and version_stage: # set version_id to match version_stage @@ -56,7 +58,7 @@ class SecretsManagerBackend(BaseBackend): version_id = ver_id break if not version_id: - raise ResourceNotFoundException() + raise SecretNotFoundException() # TODO check this part if 'deleted_date' in self.secrets[secret_id]: @@ -84,6 +86,9 @@ class SecretsManagerBackend(BaseBackend): if 'secret_binary' in secret_version: response_data["SecretBinary"] = secret_version['secret_binary'] + if 'secret_string' not in secret_version and 'secret_binary' not in secret_version: + raise SecretHasNoValueException(version_stage or u"AWSCURRENT") + response = json.dumps(response_data) return response @@ -169,7 +174,7 @@ class SecretsManagerBackend(BaseBackend): def describe_secret(self, secret_id): if not self._is_valid_identifier(secret_id): - raise ResourceNotFoundException + raise SecretNotFoundException() secret = self.secrets[secret_id] @@ -198,7 +203,7 @@ class SecretsManagerBackend(BaseBackend): rotation_days = 'AutomaticallyAfterDays' if not self._is_valid_identifier(secret_id): - raise ResourceNotFoundException + raise SecretNotFoundException() if 'deleted_date' in self.secrets[secret_id]: raise InvalidRequestException( @@ -340,7 +345,7 @@ class SecretsManagerBackend(BaseBackend): def delete_secret(self, secret_id, recovery_window_in_days, force_delete_without_recovery): if not self._is_valid_identifier(secret_id): - raise ResourceNotFoundException + raise SecretNotFoundException() if 'deleted_date' in self.secrets[secret_id]: raise InvalidRequestException( @@ -370,7 +375,7 @@ class SecretsManagerBackend(BaseBackend): secret = self.secrets.get(secret_id, None) if not secret: - raise ResourceNotFoundException + raise SecretNotFoundException() arn = secret_arn(self.region, secret['secret_id']) name = secret['name'] @@ -380,7 +385,7 @@ class SecretsManagerBackend(BaseBackend): def restore_secret(self, secret_id): if not self._is_valid_identifier(secret_id): - raise ResourceNotFoundException + raise SecretNotFoundException() self.secrets[secret_id].pop('deleted_date', None) diff --git a/moto/sns/models.py b/moto/sns/models.py index 51b5c2b2c..90bb92754 100644 --- a/moto/sns/models.py +++ b/moto/sns/models.py @@ -343,6 +343,14 @@ class SNSBackend(BaseBackend): return old_subscription topic = self.get_topic(topic_arn) subscription = Subscription(topic, endpoint, protocol) + attributes = { + 'PendingConfirmation': 'false', + 'Endpoint': endpoint, + 'TopicArn': topic_arn, + 'Protocol': protocol, + 'SubscriptionArn': subscription.arn + } + subscription.attributes = attributes self.subscriptions[subscription.arn] = subscription return subscription diff --git a/moto/sqs/exceptions.py b/moto/sqs/exceptions.py index 5f1cc46b2..02f28b2d2 100644 --- a/moto/sqs/exceptions.py +++ b/moto/sqs/exceptions.py @@ -19,9 +19,12 @@ class MessageAttributesInvalid(Exception): self.description = description -class QueueDoesNotExist(Exception): - status_code = 404 - description = "The specified queue does not exist for this wsdl version." +class QueueDoesNotExist(RESTError): + code = 404 + + def __init__(self): + super(QueueDoesNotExist, self).__init__( + "QueueDoesNotExist", "The specified queue does not exist for this wsdl version.") class QueueAlreadyExists(RESTError): diff --git a/moto/sqs/models.py b/moto/sqs/models.py index 922da704f..eb237e437 100644 --- a/moto/sqs/models.py +++ b/moto/sqs/models.py @@ -654,12 +654,21 @@ class SQSBackend(BaseBackend): def tag_queue(self, queue_name, tags): queue = self.get_queue(queue_name) + + if not len(tags): + raise RESTError('MissingParameter', + 'The request must contain the parameter Tags.') + + if len(tags) > 50: + raise RESTError('InvalidParameterValue', + 'Too many tags added for queue {}.'.format(queue_name)) + queue.tags.update(tags) def untag_queue(self, queue_name, tag_keys): queue = self.get_queue(queue_name) - if len(tag_keys) == 0: + if not len(tag_keys): raise RESTError('InvalidParameterValue', 'Tag keys must be between 1 and 128 characters in length.') for key in tag_keys: diff --git a/moto/sqs/responses.py b/moto/sqs/responses.py index 747fa2363..b6f717f3b 100644 --- a/moto/sqs/responses.py +++ b/moto/sqs/responses.py @@ -10,7 +10,6 @@ from .models import sqs_backends from .exceptions import ( MessageAttributesInvalid, MessageNotInflight, - QueueDoesNotExist, ReceiptHandleIsInvalid, ) @@ -90,11 +89,7 @@ class SQSResponse(BaseResponse): request_url = urlparse(self.uri) queue_name = self._get_param("QueueName") - try: - queue = self.sqs_backend.get_queue(queue_name) - except QueueDoesNotExist as e: - return self._error('AWS.SimpleQueueService.NonExistentQueue', - e.description) + queue = self.sqs_backend.get_queue(queue_name) if queue: template = self.response_template(GET_QUEUE_URL_RESPONSE) @@ -175,11 +170,8 @@ class SQSResponse(BaseResponse): def get_queue_attributes(self): queue_name = self._get_queue_name() - try: - queue = self.sqs_backend.get_queue(queue_name) - except QueueDoesNotExist as e: - return self._error('AWS.SimpleQueueService.NonExistentQueue', - e.description) + + queue = self.sqs_backend.get_queue(queue_name) template = self.response_template(GET_QUEUE_ATTRIBUTES_RESPONSE) return template.render(queue=queue) @@ -242,25 +234,55 @@ class SQSResponse(BaseResponse): queue_name = self._get_queue_name() - messages = [] - for index in range(1, 11): - # Loop through looking for messages - message_key = 'SendMessageBatchRequestEntry.{0}.MessageBody'.format( - index) - message_body = self.querystring.get(message_key) - if not message_body: - # Found all messages - break + self.sqs_backend.get_queue(queue_name) - message_user_id_key = 'SendMessageBatchRequestEntry.{0}.Id'.format( - index) - message_user_id = self.querystring.get(message_user_id_key)[0] + if self.querystring.get('Entries'): + return self._error('AWS.SimpleQueueService.EmptyBatchRequest', + 'There should be at least one SendMessageBatchRequestEntry in the request.') + + entries = {} + for key, value in self.querystring.items(): + match = re.match(r'^SendMessageBatchRequestEntry\.(\d+)\.Id', key) + if match: + entries[match.group(1)] = { + 'Id': value[0], + 'MessageBody': self.querystring.get( + 'SendMessageBatchRequestEntry.{}.MessageBody'.format(match.group(1)))[0] + } + + if any(not re.match(r'^[\w-]{1,80}$', entry['Id']) for entry in entries.values()): + return self._error('AWS.SimpleQueueService.InvalidBatchEntryId', + 'A batch entry id can only contain alphanumeric characters, ' + 'hyphens and underscores. It can be at most 80 letters long.') + + body_length = next( + (len(entry['MessageBody']) for entry in entries.values() if len(entry['MessageBody']) > MAXIMUM_MESSAGE_LENGTH), + False + ) + if body_length: + return self._error('AWS.SimpleQueueService.BatchRequestTooLong', + 'Batch requests cannot be longer than 262144 bytes. ' + 'You have sent {} bytes.'.format(body_length)) + + duplicate_id = self._get_first_duplicate_id([entry['Id'] for entry in entries.values()]) + if duplicate_id: + return self._error('AWS.SimpleQueueService.BatchEntryIdsNotDistinct', + 'Id {} repeated.'.format(duplicate_id)) + + if len(entries) > 10: + return self._error('AWS.SimpleQueueService.TooManyEntriesInBatchRequest', + 'Maximum number of entries per request are 10. ' + 'You have sent 11.') + + messages = [] + for index, entry in entries.items(): + # Loop through looking for messages delay_key = 'SendMessageBatchRequestEntry.{0}.DelaySeconds'.format( index) delay_seconds = self.querystring.get(delay_key, [None])[0] message = self.sqs_backend.send_message( - queue_name, message_body[0], delay_seconds=delay_seconds) - message.user_id = message_user_id + queue_name, entry['MessageBody'], delay_seconds=delay_seconds) + message.user_id = entry['Id'] message_attributes = parse_message_attributes( self.querystring, base='SendMessageBatchRequestEntry.{0}.'.format(index)) @@ -273,6 +295,14 @@ class SQSResponse(BaseResponse): template = self.response_template(SEND_MESSAGE_BATCH_RESPONSE) return template.render(messages=messages) + def _get_first_duplicate_id(self, ids): + unique_ids = set() + for id in ids: + if id in unique_ids: + return id + unique_ids.add(id) + return None + def delete_message(self): queue_name = self._get_queue_name() receipt_handle = self.querystring.get("ReceiptHandle")[0] @@ -321,10 +351,7 @@ class SQSResponse(BaseResponse): def receive_message(self): queue_name = self._get_queue_name() - try: - queue = self.sqs_backend.get_queue(queue_name) - except QueueDoesNotExist as e: - return self._error('QueueDoesNotExist', e.description) + queue = self.sqs_backend.get_queue(queue_name) try: message_count = int(self.querystring.get("MaxNumberOfMessages")[0]) @@ -406,11 +433,7 @@ class SQSResponse(BaseResponse): queue_name = self._get_queue_name() tag_keys = self._get_multi_param('TagKey') - try: - self.sqs_backend.untag_queue(queue_name, tag_keys) - except QueueDoesNotExist as e: - return self._error('AWS.SimpleQueueService.NonExistentQueue', - e.description) + self.sqs_backend.untag_queue(queue_name, tag_keys) template = self.response_template(UNTAG_QUEUE_RESPONSE) return template.render() diff --git a/tests/test_awslambda/test_lambda.py b/tests/test_awslambda/test_lambda.py index 2f47b6284..d57722074 100644 --- a/tests/test_awslambda/test_lambda.py +++ b/tests/test_awslambda/test_lambda.py @@ -1134,7 +1134,7 @@ def test_invoke_function_from_sqs_exception(): if 'I failed!' in event['message']: messages = queue.receive_messages(MaxNumberOfMessages=10) # Verify messages are still visible and unprocessed - assert len(messages) is 3 + assert len(messages) == 3 return time.sleep(1) diff --git a/tests/test_awslambda/test_lambda_eventsourcemappings.py b/tests/test_awslambda/test_lambda_eventsourcemappings.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_dynamodb2/test_dynamodb.py b/tests/test_dynamodb2/test_dynamodb.py index 9debe49d6..3d9914f14 100644 --- a/tests/test_dynamodb2/test_dynamodb.py +++ b/tests/test_dynamodb2/test_dynamodb.py @@ -2705,6 +2705,31 @@ def test_item_size_is_under_400KB(): Item={'id': {'S': 'foo'}, 'itemlist': {'L': [{'M': {'item1': {'S': large_item}}}]}}) +@mock_dynamodb2 +# https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_Query.html#DDB-Query-request-KeyConditionExpression +def test_hash_key_cannot_use_begins_with_operations(): + dynamodb = boto3.resource('dynamodb') + table = dynamodb.create_table( + TableName='test-table', + KeySchema=[{'AttributeName': 'key', 'KeyType': 'HASH'}], + AttributeDefinitions=[{'AttributeName': 'key', 'AttributeType': 'S'}], + ProvisionedThroughput={'ReadCapacityUnits': 1, 'WriteCapacityUnits': 1}) + + items = [{'key': 'prefix-$LATEST', 'value': '$LATEST'}, + {'key': 'prefix-DEV', 'value': 'DEV'}, + {'key': 'prefix-PROD', 'value': 'PROD'}] + + with table.batch_writer() as batch: + for item in items: + batch.put_item(Item=item) + + table = dynamodb.Table('test-table') + with assert_raises(ClientError) as ex: + table.query(KeyConditionExpression=Key('key').begins_with('prefix-')) + ex.exception.response['Error']['Code'].should.equal('ValidationException') + ex.exception.response['Error']['Message'].should.equal('Query key condition not supported') + + def assert_failure_due_to_item_size(func, **kwargs): with assert_raises(ClientError) as ex: func(**kwargs) diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py index fe2117a3a..2374fb599 100644 --- a/tests/test_iam/test_iam.py +++ b/tests/test_iam/test_iam.py @@ -9,6 +9,8 @@ import sure # noqa import sys from boto.exception import BotoServerError from botocore.exceptions import ClientError +from dateutil.tz import tzutc + from moto import mock_iam, mock_iam_deprecated from moto.iam.models import aws_managed_policies from nose.tools import assert_raises, assert_equals @@ -338,6 +340,15 @@ def test_create_policy(): response['Policy']['Arn'].should.equal("arn:aws:iam::123456789012:policy/TestCreatePolicy") +@mock_iam +def test_delete_policy(): + conn = boto3.client('iam', region_name='us-east-1') + response = conn.create_policy(PolicyName="TestCreatePolicy", PolicyDocument=MOCK_POLICY) + [pol['PolicyName'] for pol in conn.list_policies(Scope='Local')['Policies']].should.equal(['TestCreatePolicy']) + conn.delete_policy(PolicyArn=response['Policy']['Arn']) + assert conn.list_policies(Scope='Local')['Policies'].should.be.empty + + @mock_iam def test_create_policy_versions(): conn = boto3.client('iam', region_name='us-east-1') @@ -713,7 +724,7 @@ def test_mfa_devices(): @mock_iam_deprecated() -def test_delete_user(): +def test_delete_user_deprecated(): conn = boto.connect_iam() with assert_raises(BotoServerError): conn.delete_user('my-user') @@ -721,6 +732,17 @@ def test_delete_user(): conn.delete_user('my-user') +@mock_iam() +def test_delete_user(): + conn = boto3.client('iam', region_name='us-east-1') + with assert_raises(ClientError): + conn.delete_user(UserName='my-user') + conn.create_user(UserName='my-user') + [user['UserName'] for user in conn.list_users()['Users']].should.equal(['my-user']) + conn.delete_user(UserName='my-user') + assert conn.list_users()['Users'].should.be.empty + + @mock_iam_deprecated() def test_generate_credential_report(): conn = boto.connect_iam() @@ -1545,3 +1567,251 @@ def test_create_role_with_permissions_boundary(): # Ensure the PermissionsBoundary is included in role listing as well conn.list_roles().get('Roles')[0].get('PermissionsBoundary').should.equal(expected) + + +@mock_iam +def test_create_open_id_connect_provider(): + client = boto3.client('iam', region_name='us-east-1') + response = client.create_open_id_connect_provider( + Url='https://example.com', + ThumbprintList=[] # even it is required to provide at least one thumbprint, AWS accepts an empty list + ) + + response['OpenIDConnectProviderArn'].should.equal( + 'arn:aws:iam::123456789012:oidc-provider/example.com' + ) + + response = client.create_open_id_connect_provider( + Url='http://example.org', + ThumbprintList=[ + 'b' * 40 + ], + ClientIDList=[ + 'b' + ] + ) + + response['OpenIDConnectProviderArn'].should.equal( + 'arn:aws:iam::123456789012:oidc-provider/example.org' + ) + + response = client.create_open_id_connect_provider( + Url='http://example.org/oidc', + ThumbprintList=[] + ) + + response['OpenIDConnectProviderArn'].should.equal( + 'arn:aws:iam::123456789012:oidc-provider/example.org/oidc' + ) + + response = client.create_open_id_connect_provider( + Url='http://example.org/oidc-query?test=true', + ThumbprintList=[] + ) + + response['OpenIDConnectProviderArn'].should.equal( + 'arn:aws:iam::123456789012:oidc-provider/example.org/oidc-query' + ) + + +@mock_iam +def test_create_open_id_connect_provider_errors(): + client = boto3.client('iam', region_name='us-east-1') + client.create_open_id_connect_provider( + Url='https://example.com', + ThumbprintList=[] + ) + + client.create_open_id_connect_provider.when.called_with( + Url='https://example.com', + ThumbprintList=[] + ).should.throw( + ClientError, + 'Unknown' + ) + + client.create_open_id_connect_provider.when.called_with( + Url='example.org', + ThumbprintList=[] + ).should.throw( + ClientError, + 'Invalid Open ID Connect Provider URL' + ) + + client.create_open_id_connect_provider.when.called_with( + Url='example', + ThumbprintList=[] + ).should.throw( + ClientError, + 'Invalid Open ID Connect Provider URL' + ) + + client.create_open_id_connect_provider.when.called_with( + Url='http://example.org', + ThumbprintList=[ + 'a' * 40, + 'b' * 40, + 'c' * 40, + 'd' * 40, + 'e' * 40, + 'f' * 40, + ] + ).should.throw( + ClientError, + 'Thumbprint list must contain fewer than 5 entries.' + ) + + too_many_client_ids = ['{}'.format(i) for i in range(101)] + client.create_open_id_connect_provider.when.called_with( + Url='http://example.org', + ThumbprintList=[], + ClientIDList=too_many_client_ids + ).should.throw( + ClientError, + 'Cannot exceed quota for ClientIdsPerOpenIdConnectProvider: 100' + ) + + too_long_url = 'b' * 256 + too_long_thumbprint = 'b' * 41 + too_long_client_id = 'b' * 256 + client.create_open_id_connect_provider.when.called_with( + Url=too_long_url, + ThumbprintList=[ + too_long_thumbprint + ], + ClientIDList=[ + too_long_client_id + ] + ).should.throw( + ClientError, + '3 validation errors detected: ' + 'Value "{0}" at "clientIDList" failed to satisfy constraint: ' + 'Member must satisfy constraint: ' + '[Member must have length less than or equal to 255, ' + 'Member must have length greater than or equal to 1]; ' + 'Value "{1}" at "thumbprintList" failed to satisfy constraint: ' + 'Member must satisfy constraint: ' + '[Member must have length less than or equal to 40, ' + 'Member must have length greater than or equal to 40]; ' + 'Value "{2}" at "url" failed to satisfy constraint: ' + 'Member must have length less than or equal to 255'.format([too_long_client_id], [too_long_thumbprint], too_long_url) + ) + + +@mock_iam +def test_delete_open_id_connect_provider(): + client = boto3.client('iam', region_name='us-east-1') + response = client.create_open_id_connect_provider( + Url='https://example.com', + ThumbprintList=[] + ) + open_id_arn = response['OpenIDConnectProviderArn'] + + client.delete_open_id_connect_provider( + OpenIDConnectProviderArn=open_id_arn + ) + + client.get_open_id_connect_provider.when.called_with( + OpenIDConnectProviderArn=open_id_arn + ).should.throw( + ClientError, + 'OpenIDConnect Provider not found for arn {}'.format(open_id_arn) + ) + + # deleting a non existing provider should be successful + client.delete_open_id_connect_provider( + OpenIDConnectProviderArn=open_id_arn + ) + + +@mock_iam +def test_get_open_id_connect_provider(): + client = boto3.client('iam', region_name='us-east-1') + response = client.create_open_id_connect_provider( + Url='https://example.com', + ThumbprintList=[ + 'b' * 40 + ], + ClientIDList=[ + 'b' + ] + ) + open_id_arn = response['OpenIDConnectProviderArn'] + + response = client.get_open_id_connect_provider( + OpenIDConnectProviderArn=open_id_arn + ) + + response['Url'].should.equal('example.com') + response['ThumbprintList'].should.equal([ + 'b' * 40 + ]) + response['ClientIDList'].should.equal([ + 'b' + ]) + response.should.have.key('CreateDate').should.be.a(datetime) + + +@mock_iam +def test_get_open_id_connect_provider_errors(): + client = boto3.client('iam', region_name='us-east-1') + response = client.create_open_id_connect_provider( + Url='https://example.com', + ThumbprintList=[ + 'b' * 40 + ], + ClientIDList=[ + 'b' + ] + ) + open_id_arn = response['OpenIDConnectProviderArn'] + + client.get_open_id_connect_provider.when.called_with( + OpenIDConnectProviderArn=open_id_arn + '-not-existing' + ).should.throw( + ClientError, + 'OpenIDConnect Provider not found for arn {}'.format(open_id_arn + '-not-existing') + ) + + +@mock_iam +def test_list_open_id_connect_providers(): + client = boto3.client('iam', region_name='us-east-1') + response = client.create_open_id_connect_provider( + Url='https://example.com', + ThumbprintList=[] + ) + open_id_arn_1 = response['OpenIDConnectProviderArn'] + + response = client.create_open_id_connect_provider( + Url='http://example.org', + ThumbprintList=[ + 'b' * 40 + ], + ClientIDList=[ + 'b' + ] + ) + open_id_arn_2 = response['OpenIDConnectProviderArn'] + + response = client.create_open_id_connect_provider( + Url='http://example.org/oidc', + ThumbprintList=[] + ) + open_id_arn_3 = response['OpenIDConnectProviderArn'] + + response = client.list_open_id_connect_providers() + + sorted(response['OpenIDConnectProviderList'], key=lambda i: i['Arn']).should.equal( + [ + { + 'Arn': open_id_arn_1 + }, + { + 'Arn': open_id_arn_2 + }, + { + 'Arn': open_id_arn_3 + } + ] + ) diff --git a/tests/test_rds2/test_rds2.py b/tests/test_rds2/test_rds2.py index aacaf04f1..911f682a8 100644 --- a/tests/test_rds2/test_rds2.py +++ b/tests/test_rds2/test_rds2.py @@ -39,6 +39,20 @@ def test_create_database(): db_instance['VpcSecurityGroups'][0]['VpcSecurityGroupId'].should.equal('sg-123456') +@mock_rds2 +def test_create_database_no_allocated_storage(): + conn = boto3.client('rds', region_name='us-west-2') + database = conn.create_db_instance( + DBInstanceIdentifier='db-master-1', + Engine='postgres', + DBName='staging-postgres', + DBInstanceClass='db.m1.small') + db_instance = database['DBInstance'] + db_instance['Engine'].should.equal('postgres') + db_instance['StorageType'].should.equal('gp2') + db_instance['AllocatedStorage'].should.equal(20) + + @mock_rds2 def test_create_database_non_existing_option_group(): conn = boto3.client('rds', region_name='us-west-2') diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py index 292093893..8d535420a 100644 --- a/tests/test_s3/test_s3.py +++ b/tests/test_s3/test_s3.py @@ -1534,6 +1534,18 @@ def test_boto3_get_object(): e.exception.response['Error']['Code'].should.equal('NoSuchKey') +@mock_s3 +def test_boto3_get_missing_object_with_part_number(): + s3 = boto3.resource('s3', region_name='us-east-1') + s3.create_bucket(Bucket="blah") + + with assert_raises(ClientError) as e: + s3.Object('blah', 'hello.txt').meta.client.head_object( + Bucket='blah', Key='hello.txt', PartNumber=123) + + e.exception.response['Error']['Code'].should.equal('404') + + @mock_s3 def test_boto3_head_object_with_versioning(): s3 = boto3.resource('s3', region_name='us-east-1') @@ -2587,6 +2599,160 @@ def test_boto3_put_object_tagging(): resp['ResponseMetadata']['HTTPStatusCode'].should.equal(200) +@mock_s3 +def test_boto3_put_object_tagging_on_earliest_version(): + s3 = boto3.client('s3', region_name='us-east-1') + bucket_name = 'mybucket' + key = 'key-with-tags' + s3.create_bucket(Bucket=bucket_name) + s3_resource = boto3.resource('s3') + bucket_versioning = s3_resource.BucketVersioning(bucket_name) + bucket_versioning.enable() + bucket_versioning.status.should.equal('Enabled') + + with assert_raises(ClientError) as err: + s3.put_object_tagging( + Bucket=bucket_name, + Key=key, + Tagging={'TagSet': [ + {'Key': 'item1', 'Value': 'foo'}, + {'Key': 'item2', 'Value': 'bar'}, + ]} + ) + + e = err.exception + e.response['Error'].should.equal({ + 'Code': 'NoSuchKey', + 'Message': 'The specified key does not exist.', + 'RequestID': '7a62c49f-347e-4fc4-9331-6e8eEXAMPLE', + }) + + s3.put_object( + Bucket=bucket_name, + Key=key, + Body='test' + ) + s3.put_object( + Bucket=bucket_name, + Key=key, + Body='test_updated' + ) + + object_versions = list(s3_resource.Bucket(bucket_name).object_versions.all()) + first_object = object_versions[0] + second_object = object_versions[1] + + resp = s3.put_object_tagging( + Bucket=bucket_name, + Key=key, + Tagging={'TagSet': [ + {'Key': 'item1', 'Value': 'foo'}, + {'Key': 'item2', 'Value': 'bar'}, + ]}, + VersionId=first_object.id + ) + + resp['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + + # Older version has tags while the most recent does not + resp = s3.get_object_tagging(Bucket=bucket_name, Key=key, VersionId=first_object.id) + resp['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + resp['TagSet'].should.equal( + [ + {'Key': 'item1', 'Value': 'foo'}, + {'Key': 'item2', 'Value': 'bar'} + ] + ) + + resp = s3.get_object_tagging(Bucket=bucket_name, Key=key, VersionId=second_object.id) + resp['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + resp['TagSet'].should.equal([]) + + +@mock_s3 +def test_boto3_put_object_tagging_on_both_version(): + s3 = boto3.client('s3', region_name='us-east-1') + bucket_name = 'mybucket' + key = 'key-with-tags' + s3.create_bucket(Bucket=bucket_name) + s3_resource = boto3.resource('s3') + bucket_versioning = s3_resource.BucketVersioning(bucket_name) + bucket_versioning.enable() + bucket_versioning.status.should.equal('Enabled') + + with assert_raises(ClientError) as err: + s3.put_object_tagging( + Bucket=bucket_name, + Key=key, + Tagging={'TagSet': [ + {'Key': 'item1', 'Value': 'foo'}, + {'Key': 'item2', 'Value': 'bar'}, + ]} + ) + + e = err.exception + e.response['Error'].should.equal({ + 'Code': 'NoSuchKey', + 'Message': 'The specified key does not exist.', + 'RequestID': '7a62c49f-347e-4fc4-9331-6e8eEXAMPLE', + }) + + s3.put_object( + Bucket=bucket_name, + Key=key, + Body='test' + ) + s3.put_object( + Bucket=bucket_name, + Key=key, + Body='test_updated' + ) + + object_versions = list(s3_resource.Bucket(bucket_name).object_versions.all()) + first_object = object_versions[0] + second_object = object_versions[1] + + resp = s3.put_object_tagging( + Bucket=bucket_name, + Key=key, + Tagging={'TagSet': [ + {'Key': 'item1', 'Value': 'foo'}, + {'Key': 'item2', 'Value': 'bar'}, + ]}, + VersionId=first_object.id + ) + resp['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + + resp = s3.put_object_tagging( + Bucket=bucket_name, + Key=key, + Tagging={'TagSet': [ + {'Key': 'item1', 'Value': 'baz'}, + {'Key': 'item2', 'Value': 'bin'}, + ]}, + VersionId=second_object.id + ) + resp['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + + resp = s3.get_object_tagging(Bucket=bucket_name, Key=key, VersionId=first_object.id) + resp['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + resp['TagSet'].should.equal( + [ + {'Key': 'item1', 'Value': 'foo'}, + {'Key': 'item2', 'Value': 'bar'} + ] + ) + + resp = s3.get_object_tagging(Bucket=bucket_name, Key=key, VersionId=second_object.id) + resp['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + resp['TagSet'].should.equal( + [ + {'Key': 'item1', 'Value': 'baz'}, + {'Key': 'item2', 'Value': 'bin'} + ] + ) + + @mock_s3 def test_boto3_put_object_tagging_with_single_tag(): s3 = boto3.client('s3', region_name='us-east-1') diff --git a/tests/test_secretsmanager/test_secretsmanager.py b/tests/test_secretsmanager/test_secretsmanager.py index 62de93bab..e2fc266ea 100644 --- a/tests/test_secretsmanager/test_secretsmanager.py +++ b/tests/test_secretsmanager/test_secretsmanager.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from __future__ import unicode_literals import boto3 @@ -8,7 +9,7 @@ import string import pytz from datetime import datetime import sure # noqa -from nose.tools import assert_raises +from nose.tools import assert_raises, assert_equal from six import b DEFAULT_SECRET_NAME = 'test-secret' @@ -38,9 +39,14 @@ def test_get_secret_value_binary(): def test_get_secret_that_does_not_exist(): conn = boto3.client('secretsmanager', region_name='us-west-2') - with assert_raises(ClientError): + with assert_raises(ClientError) as cm: result = conn.get_secret_value(SecretId='i-dont-exist') + assert_equal( + u"Secrets Manager can\u2019t find the specified secret.", + cm.exception.response['Error']['Message'] + ) + @mock_secretsmanager def test_get_secret_that_does_not_match(): @@ -48,9 +54,14 @@ def test_get_secret_that_does_not_match(): create_secret = conn.create_secret(Name='java-util-test-password', SecretString="foosecret") - with assert_raises(ClientError): + with assert_raises(ClientError) as cm: result = conn.get_secret_value(SecretId='i-dont-match') + assert_equal( + u"Secrets Manager can\u2019t find the specified secret.", + cm.exception.response['Error']['Message'] + ) + @mock_secretsmanager def test_get_secret_value_that_is_marked_deleted(): @@ -65,6 +76,21 @@ def test_get_secret_value_that_is_marked_deleted(): result = conn.get_secret_value(SecretId='test-secret') +@mock_secretsmanager +def test_get_secret_that_has_no_value(): + conn = boto3.client('secretsmanager', region_name='us-west-2') + + create_secret = conn.create_secret(Name="java-util-test-password") + + with assert_raises(ClientError) as cm: + result = conn.get_secret_value(SecretId='java-util-test-password') + + assert_equal( + u"Secrets Manager can\u2019t find the specified secret value for staging label: AWSCURRENT", + cm.exception.response['Error']['Message'] + ) + + @mock_secretsmanager def test_create_secret(): conn = boto3.client('secretsmanager', region_name='us-east-1') diff --git a/tests/test_secretsmanager/test_server.py b/tests/test_secretsmanager/test_server.py index 23d823239..6955d8232 100644 --- a/tests/test_secretsmanager/test_server.py +++ b/tests/test_secretsmanager/test_server.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from __future__ import unicode_literals import json @@ -49,7 +50,7 @@ def test_get_secret_that_does_not_exist(): "X-Amz-Target": "secretsmanager.GetSecretValue"}, ) json_data = json.loads(get_secret.data.decode("utf-8")) - assert json_data['message'] == "Secrets Manager can't find the specified secret" + assert json_data['message'] == u"Secrets Manager can\u2019t find the specified secret." assert json_data['__type'] == 'ResourceNotFoundException' @mock_secretsmanager @@ -70,7 +71,27 @@ def test_get_secret_that_does_not_match(): "X-Amz-Target": "secretsmanager.GetSecretValue"}, ) json_data = json.loads(get_secret.data.decode("utf-8")) - assert json_data['message'] == "Secrets Manager can't find the specified secret" + assert json_data['message'] == u"Secrets Manager can\u2019t find the specified secret." + assert json_data['__type'] == 'ResourceNotFoundException' + +@mock_secretsmanager +def test_get_secret_that_has_no_value(): + backend = server.create_backend_app('secretsmanager') + test_client = backend.test_client() + + create_secret = test_client.post('/', + data={"Name": DEFAULT_SECRET_NAME}, + headers={ + "X-Amz-Target": "secretsmanager.CreateSecret"}, + ) + get_secret = test_client.post('/', + data={"SecretId": DEFAULT_SECRET_NAME}, + headers={ + "X-Amz-Target": "secretsmanager.GetSecretValue"}, + ) + + json_data = json.loads(get_secret.data.decode("utf-8")) + assert json_data['message'] == u"Secrets Manager can\u2019t find the specified secret value for staging label: AWSCURRENT" assert json_data['__type'] == 'ResourceNotFoundException' @mock_secretsmanager @@ -158,7 +179,7 @@ def test_describe_secret_that_does_not_exist(): ) json_data = json.loads(describe_secret.data.decode("utf-8")) - assert json_data['message'] == "Secrets Manager can't find the specified secret" + assert json_data['message'] == u"Secrets Manager can\u2019t find the specified secret." assert json_data['__type'] == 'ResourceNotFoundException' @mock_secretsmanager @@ -182,7 +203,7 @@ def test_describe_secret_that_does_not_match(): ) json_data = json.loads(describe_secret.data.decode("utf-8")) - assert json_data['message'] == "Secrets Manager can't find the specified secret" + assert json_data['message'] == u"Secrets Manager can\u2019t find the specified secret." assert json_data['__type'] == 'ResourceNotFoundException' @mock_secretsmanager @@ -283,7 +304,7 @@ def test_rotate_secret_that_does_not_exist(): ) json_data = json.loads(rotate_secret.data.decode("utf-8")) - assert json_data['message'] == "Secrets Manager can't find the specified secret" + assert json_data['message'] == u"Secrets Manager can\u2019t find the specified secret." assert json_data['__type'] == 'ResourceNotFoundException' @mock_secretsmanager @@ -307,7 +328,7 @@ def test_rotate_secret_that_does_not_match(): ) json_data = json.loads(rotate_secret.data.decode("utf-8")) - assert json_data['message'] == "Secrets Manager can't find the specified secret" + assert json_data['message'] == u"Secrets Manager can\u2019t find the specified secret." assert json_data['__type'] == 'ResourceNotFoundException' @mock_secretsmanager diff --git a/tests/test_sns/test_subscriptions_boto3.py b/tests/test_sns/test_subscriptions_boto3.py index 012cd6470..282ec4652 100644 --- a/tests/test_sns/test_subscriptions_boto3.py +++ b/tests/test_sns/test_subscriptions_boto3.py @@ -181,6 +181,35 @@ def test_subscription_paging(): int(DEFAULT_PAGE_SIZE / 3)) topic1_subscriptions.shouldnt.have("NextToken") +@mock_sns +def test_subscribe_attributes(): + client = boto3.client('sns', region_name='us-east-1') + client.create_topic(Name="some-topic") + resp = client.create_topic(Name="some-topic") + arn = resp['TopicArn'] + + resp = client.subscribe( + TopicArn=arn, + Protocol='http', + Endpoint='http://test.com' + ) + + attributes = client.get_subscription_attributes( + SubscriptionArn=resp['SubscriptionArn'] + ) + + attributes.should.contain('Attributes') + attributes['Attributes'].should.contain('PendingConfirmation') + attributes['Attributes']['PendingConfirmation'].should.equal('false') + attributes['Attributes'].should.contain('Endpoint') + attributes['Attributes']['Endpoint'].should.equal('http://test.com') + attributes['Attributes'].should.contain('TopicArn') + attributes['Attributes']['TopicArn'].should.equal(arn) + attributes['Attributes'].should.contain('Protocol') + attributes['Attributes']['Protocol'].should.equal('http') + attributes['Attributes'].should.contain('SubscriptionArn') + attributes['Attributes']['SubscriptionArn'].should.equal(resp['SubscriptionArn']) + @mock_sns def test_creating_subscription_with_attributes(): diff --git a/tests/test_sqs/test_sqs.py b/tests/test_sqs/test_sqs.py index bc9fa8e4d..1ad2e1a80 100644 --- a/tests/test_sqs/test_sqs.py +++ b/tests/test_sqs/test_sqs.py @@ -882,6 +882,127 @@ def test_delete_message_after_visibility_timeout(): assert new_queue.count() == 0 +@mock_sqs +def test_send_message_batch_errors(): + client = boto3.client('sqs', region_name = 'us-east-1') + + response = client.create_queue(QueueName='test-queue-with-tags') + queue_url = response['QueueUrl'] + + client.send_message_batch.when.called_with( + QueueUrl=queue_url + '-not-existing', + Entries=[ + { + 'Id': 'id_1', + 'MessageBody': 'body_1' + } + ] + ).should.throw( + ClientError, + 'The specified queue does not exist for this wsdl version.' + ) + + client.send_message_batch.when.called_with( + QueueUrl=queue_url, + Entries=[] + ).should.throw( + ClientError, + 'There should be at least one SendMessageBatchRequestEntry in the request.' + ) + + client.send_message_batch.when.called_with( + QueueUrl=queue_url, + Entries=[ + { + 'Id': '', + 'MessageBody': 'body_1' + } + ] + ).should.throw( + ClientError, + 'A batch entry id can only contain alphanumeric characters, ' + 'hyphens and underscores. It can be at most 80 letters long.' + ) + + client.send_message_batch.when.called_with( + QueueUrl=queue_url, + Entries=[ + { + 'Id': '.!@#$%^&*()+=', + 'MessageBody': 'body_1' + } + ] + ).should.throw( + ClientError, + 'A batch entry id can only contain alphanumeric characters, ' + 'hyphens and underscores. It can be at most 80 letters long.' + ) + + client.send_message_batch.when.called_with( + QueueUrl=queue_url, + Entries=[ + { + 'Id': 'i' * 81, + 'MessageBody': 'body_1' + } + ] + ).should.throw( + ClientError, + 'A batch entry id can only contain alphanumeric characters, ' + 'hyphens and underscores. It can be at most 80 letters long.' + ) + + client.send_message_batch.when.called_with( + QueueUrl=queue_url, + Entries=[ + { + 'Id': 'id_1', + 'MessageBody': 'b' * 262145 + } + ] + ).should.throw( + ClientError, + 'Batch requests cannot be longer than 262144 bytes. ' + 'You have sent 262145 bytes.' + ) + + # only the first duplicated Id is reported + client.send_message_batch.when.called_with( + QueueUrl=queue_url, + Entries=[ + { + 'Id': 'id_1', + 'MessageBody': 'body_1' + }, + { + 'Id': 'id_2', + 'MessageBody': 'body_2' + }, + { + 'Id': 'id_2', + 'MessageBody': 'body_2' + }, + { + 'Id': 'id_1', + 'MessageBody': 'body_1' + } + ] + ).should.throw( + ClientError, + 'Id id_2 repeated.' + ) + + entries = [{'Id': 'id_{}'.format(i), 'MessageBody': 'body_{}'.format(i)} for i in range(11)] + client.send_message_batch.when.called_with( + QueueUrl=queue_url, + Entries=entries + ).should.throw( + ClientError, + 'Maximum number of entries per request are 10. ' + 'You have sent 11.' + ) + + @mock_sqs def test_batch_change_message_visibility(): if os.environ.get('TEST_SERVER_MODE', 'false').lower() == 'true': @@ -987,6 +1108,73 @@ def test_tags(): }) +@mock_sqs +def test_list_queue_tags_errors(): + client = boto3.client('sqs', region_name='us-east-1') + + response = client.create_queue( + QueueName='test-queue-with-tags', + tags={ + 'tag_key_1': 'tag_value_X' + } + ) + queue_url = response['QueueUrl'] + + client.list_queue_tags.when.called_with( + QueueUrl=queue_url + '-not-existing', + ).should.throw( + ClientError, + 'The specified queue does not exist for this wsdl version.' + ) + + +@mock_sqs +def test_tag_queue_errors(): + client = boto3.client('sqs', region_name='us-east-1') + + response = client.create_queue( + QueueName='test-queue-with-tags', + tags={ + 'tag_key_1': 'tag_value_X' + } + ) + queue_url = response['QueueUrl'] + + client.tag_queue.when.called_with( + QueueUrl=queue_url + '-not-existing', + Tags={ + 'tag_key_1': 'tag_value_1' + } + ).should.throw( + ClientError, + 'The specified queue does not exist for this wsdl version.' + ) + + client.tag_queue.when.called_with( + QueueUrl=queue_url, + Tags={} + ).should.throw( + ClientError, + 'The request must contain the parameter Tags.' + ) + + too_many_tags = {'tag_key_{}'.format(i): 'tag_value_{}'.format(i) for i in range(51)} + client.tag_queue.when.called_with( + QueueUrl=queue_url, + Tags=too_many_tags + ).should.throw( + ClientError, + 'Too many tags added for queue test-queue-with-tags.' + ) + + # when the request fails, the tags should not be updated + client.list_queue_tags(QueueUrl=queue_url)['Tags'].should.equal( + { + 'tag_key_1': 'tag_value_X' + } + ) + + @mock_sqs def test_untag_queue_errors(): client = boto3.client('sqs', region_name='us-east-1') @@ -1006,7 +1194,7 @@ def test_untag_queue_errors(): ] ).should.throw( ClientError, - "The specified queue does not exist for this wsdl version." + 'The specified queue does not exist for this wsdl version.' ) client.untag_queue.when.called_with(