diff --git a/moto/iam/exceptions.py b/moto/iam/exceptions.py index b4d89c0f2..84f15f51f 100644 --- a/moto/iam/exceptions.py +++ b/moto/iam/exceptions.py @@ -7,7 +7,7 @@ class IAMNotFoundException(RESTError): def __init__(self, message): super(IAMNotFoundException, self).__init__( - "Not Found", message) + "NoSuchEntity", message) class IAMConflictException(RESTError): diff --git a/moto/iam/models.py b/moto/iam/models.py index c7ee70ca2..da11d58b2 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -569,6 +569,13 @@ class IAMBackend(BaseBackend): return role raise IAMNotFoundException("Role {0} not found".format(role_name)) + def delete_role(self, role_name): + for role in self.get_roles(): + if role.name == role_name: + del self.roles[role.id] + return + raise IAMNotFoundException("Role {0} not found".format(role_name)) + def get_roles(self): return self.roles.values() diff --git a/moto/iam/responses.py b/moto/iam/responses.py index 3c40a323f..138c08d23 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -62,6 +62,12 @@ class IamResponse(BaseResponse): template = self.response_template(GET_ROLE_TEMPLATE) return template.render(role=role) + def delete_role(self): + role_name = self._get_param('RoleName') + iam_backend.delete_role(role_name) + template = self.response_template(GENERIC_EMPTY_TEMPLATE) + return template.render(name="DeleteRoleResponse") + def list_role_policies(self): role_name = self._get_param('RoleName') role_policies_names = iam_backend.list_role_policies(role_name) diff --git a/moto/sqs/models.py b/moto/sqs/models.py index cedf03199..d2c538ecb 100644 --- a/moto/sqs/models.py +++ b/moto/sqs/models.py @@ -1,7 +1,10 @@ from __future__ import unicode_literals +import base64 import hashlib import re +import six +import struct from xml.sax.saxutils import escape import boto.sqs @@ -17,6 +20,8 @@ from .exceptions import ( DEFAULT_ACCOUNT_ID = 123456789012 DEFAULT_SENDER_ID = "AIDAIT2UOQQY3AUEKVGXU" +TRANSPORT_TYPE_ENCODINGS = {'String': b'\x01', 'Binary': b'\x02', 'Number': b'\x01'} + class Message(BaseModel): @@ -33,10 +38,58 @@ class Message(BaseModel): self.delayed_until = 0 @property - def md5(self): - body_md5 = hashlib.md5() - body_md5.update(self._body.encode('utf-8')) - return body_md5.hexdigest() + def body_md5(self): + md5 = hashlib.md5() + md5.update(self._body.encode('utf-8')) + return md5.hexdigest() + + @property + def attribute_md5(self): + """ + The MD5 of all attributes is calculated by first generating a + utf-8 string from each attribute and MD5-ing the concatenation + of them all. Each attribute is encoded with some bytes that + describe the length of each part and the type of attribute. + + Not yet implemented: + List types (https://github.com/aws/aws-sdk-java/blob/7844c64cf248aed889811bf2e871ad6b276a89ca/aws-java-sdk-sqs/src/main/java/com/amazonaws/services/sqs/MessageMD5ChecksumHandler.java#L58k) + """ + def utf8(str): + if isinstance(str, six.string_types): + return str.encode('utf-8') + return str + md5 = hashlib.md5() + for name in sorted(self.message_attributes.keys()): + attr = self.message_attributes[name] + data_type = attr['data_type'] + + encoded = utf8('') + # Each part of each attribute is encoded right after it's + # own length is packed into a 4-byte integer + # 'timestamp' -> b'\x00\x00\x00\t' + encoded += struct.pack("!I", len(utf8(name))) + utf8(name) + # The datatype is additionally given a final byte + # representing which type it is + encoded += struct.pack("!I", len(data_type)) + utf8(data_type) + encoded += TRANSPORT_TYPE_ENCODINGS[data_type] + + if data_type == 'String' or data_type == 'Number': + value = attr['string_value'] + elif data_type == 'Binary': + print(data_type, attr['binary_value'], type(attr['binary_value'])) + value = base64.b64decode(attr['binary_value']) + else: + print("Moto hasn't implemented MD5 hashing for {} attributes".format(data_type)) + # The following should be enough of a clue to users that + # they are not, in fact, looking at a correct MD5 while + # also following the character and length constraints of + # MD5 so as not to break client softwre + return('deadbeefdeadbeefdeadbeefdeadbeef') + + encoded += struct.pack("!I", len(utf8(value))) + utf8(value) + + md5.update(encoded) + return md5.hexdigest() @property def body(self): diff --git a/moto/sqs/responses.py b/moto/sqs/responses.py index 75602b1b7..53bbac6ef 100644 --- a/moto/sqs/responses.py +++ b/moto/sqs/responses.py @@ -337,11 +337,9 @@ SET_QUEUE_ATTRIBUTE_RESPONSE = """ SEND_MESSAGE_RESPONSE = """ - {{- message.md5 -}} + {{- message.body_md5 -}} - {% if message.message_attributes.items()|count > 0 %} - 324758f82d026ac6ec5b31a3b192d1e3 - {% endif %} + {{- message.attribute_md5 -}} {{- message.id -}} @@ -357,7 +355,7 @@ RECEIVE_MESSAGE_RESPONSE = """ {{ message.id }} {{ message.receipt_handle }} - {{ message.md5 }} + {{ message.body_md5 }} {{ message.body }} SenderId @@ -375,9 +373,7 @@ RECEIVE_MESSAGE_RESPONSE = """ ApproximateFirstReceiveTimestamp {{ message.approximate_first_receive_timestamp }} - {% if message.message_attributes.items()|count > 0 %} - 324758f82d026ac6ec5b31a3b192d1e3 - {% endif %} + {{- message.attribute_md5 -}} {% for name, value in message.message_attributes.items() %} {{ name }} @@ -405,10 +401,8 @@ SEND_MESSAGE_BATCH_RESPONSE = """ {{ message.user_id }} {{ message.id }} - {{ message.md5 }} - {% if message.message_attributes.items()|count > 0 %} - 324758f82d026ac6ec5b31a3b192d1e3 - {% endif %} + {{ message.body_md5 }} + {{- message.attribute_md5 -}} {% endfor %} diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py index f2c77685f..46b727360 100644 --- a/tests/test_iam/test_iam.py +++ b/tests/test_iam/test_iam.py @@ -8,7 +8,7 @@ from boto.exception import BotoServerError from botocore.exceptions import ClientError from moto import mock_iam, mock_iam_deprecated from moto.iam.models import aws_managed_policies -from nose.tools import assert_raises, assert_equals, assert_not_equals +from nose.tools import assert_raises, assert_equals from nose.tools import raises from tests.helpers import requires_boto_gte @@ -114,6 +114,23 @@ def test_remove_role_from_instance_profile(): dict(profile.roles).should.be.empty +@mock_iam() +def test_delete_role(): + conn = boto3.client('iam', region_name='us-east-1') + + with assert_raises(ClientError): + conn.delete_role(RoleName="my-role") + + conn.create_role(RoleName="my-role", AssumeRolePolicyDocument="some policy", Path="/my-path/") + role = conn.get_role(RoleName="my-role") + role.get('Role').get('Arn').should.equal('arn:aws:iam::123456789012:role/my-path/my-role') + + conn.delete_role(RoleName="my-role") + + with assert_raises(ClientError): + conn.get_role(RoleName="my-role") + + @mock_iam_deprecated() def test_list_instance_profiles(): conn = boto.connect_iam() diff --git a/tests/test_sqs/test_sqs.py b/tests/test_sqs/test_sqs.py index f179d9f85..bc1f5d7bd 100644 --- a/tests/test_sqs/test_sqs.py +++ b/tests/test_sqs/test_sqs.py @@ -7,6 +7,7 @@ import botocore.exceptions from boto.exception import SQSError from boto.sqs.message import RawMessage, Message +import base64 import requests import sure # noqa import time @@ -43,10 +44,44 @@ def test_get_inexistent_queue(): def test_message_send(): sqs = boto3.resource('sqs', region_name='us-east-1') queue = sqs.create_queue(QueueName="blah") - msg = queue.send_message(MessageBody="derp") - + msg = queue.send_message( + MessageBody="derp", + MessageAttributes={ + 'timestamp': { + 'StringValue': '1493147359900', + 'DataType': 'Number', + } + } + ) msg.get('MD5OfMessageBody').should.equal( '58fd9edd83341c29f1aebba81c31e257') + msg.get('MD5OfMessageAttributes').should.equal( + '235c5c510d26fb653d073faed50ae77c') + msg.get('ResponseMetadata', {}).get('RequestId').should.equal( + '27daac76-34dd-47df-bd01-1f6e873584a0') + msg.get('MessageId').should_not.contain(' \n') + + messages = queue.receive_messages() + messages.should.have.length_of(1) + + +@mock_sqs +def test_message_with_complex_attributes(): + sqs = boto3.resource('sqs', region_name='us-east-1') + queue = sqs.create_queue(QueueName="blah") + msg = queue.send_message( + MessageBody="derp", + MessageAttributes={ + 'ccc': {'StringValue': 'testjunk', 'DataType': 'String'}, + 'aaa': {'BinaryValue': b'\x02\x03\x04', 'DataType': 'Binary'}, + 'zzz': {'DataType': 'Number', 'StringValue': '0230.01'}, + 'öther_encodings': {'DataType': 'String', 'StringValue': 'T\xFCst'} + } + ) + msg.get('MD5OfMessageBody').should.equal( + '58fd9edd83341c29f1aebba81c31e257') + msg.get('MD5OfMessageAttributes').should.equal( + '8ae21a7957029ef04146b42aeaa18a22') msg.get('ResponseMetadata', {}).get('RequestId').should.equal( '27daac76-34dd-47df-bd01-1f6e873584a0') msg.get('MessageId').should_not.contain(' \n') @@ -126,7 +161,7 @@ def test_delete_queue(): sqs = boto3.resource('sqs', region_name='us-east-1') conn = boto3.client("sqs", region_name='us-east-1') conn.create_queue(QueueName="test-queue", - Attributes={"VisibilityTimeout": "60"}) + Attributes={"VisibilityTimeout": "3"}) queue = sqs.Queue('test-queue') conn.list_queues()['QueueUrls'].should.have.length_of(1) @@ -143,10 +178,10 @@ def test_set_queue_attribute(): sqs = boto3.resource('sqs', region_name='us-east-1') conn = boto3.client("sqs", region_name='us-east-1') conn.create_queue(QueueName="test-queue", - Attributes={"VisibilityTimeout": '60'}) + Attributes={"VisibilityTimeout": '3'}) queue = sqs.Queue("test-queue") - queue.attributes['VisibilityTimeout'].should.equal('60') + queue.attributes['VisibilityTimeout'].should.equal('3') queue.set_attributes(Attributes={"VisibilityTimeout": '45'}) queue = sqs.Queue("test-queue") @@ -176,7 +211,7 @@ def test_send_message(): @mock_sqs_deprecated def test_send_message_with_xml_characters(): conn = boto.connect_sqs('the_key', 'the_secret') - queue = conn.create_queue("test-queue", visibility_timeout=60) + queue = conn.create_queue("test-queue", visibility_timeout=3) queue.set_message_class(RawMessage) body_one = '< & >' @@ -192,14 +227,15 @@ def test_send_message_with_xml_characters(): @mock_sqs_deprecated def test_send_message_with_attributes(): conn = boto.connect_sqs('the_key', 'the_secret') - queue = conn.create_queue("test-queue", visibility_timeout=60) + queue = conn.create_queue("test-queue", visibility_timeout=3) queue.set_message_class(RawMessage) body = 'this is a test message' message = queue.new_message(body) + BASE64_BINARY = base64.b64encode(b'binary value').decode('utf-8') message_attributes = { 'test.attribute_name': {'data_type': 'String', 'string_value': 'attribute value'}, - 'test.binary_attribute': {'data_type': 'Binary', 'binary_value': 'binary value'}, + 'test.binary_attribute': {'data_type': 'Binary', 'binary_value': BASE64_BINARY}, 'test.number_attribute': {'data_type': 'Number', 'string_value': 'string value'} } message.message_attributes = message_attributes @@ -217,13 +253,13 @@ def test_send_message_with_attributes(): @mock_sqs_deprecated def test_send_message_with_delay(): conn = boto.connect_sqs('the_key', 'the_secret') - queue = conn.create_queue("test-queue", visibility_timeout=60) + queue = conn.create_queue("test-queue", visibility_timeout=3) queue.set_message_class(RawMessage) body_one = 'this is a test message' body_two = 'this is another test message' - queue.write(queue.new_message(body_one), delay_seconds=60) + queue.write(queue.new_message(body_one), delay_seconds=3) queue.write(queue.new_message(body_two)) queue.count().should.equal(1) @@ -238,7 +274,7 @@ def test_send_message_with_delay(): @mock_sqs_deprecated def test_send_large_message_fails(): conn = boto.connect_sqs('the_key', 'the_secret') - queue = conn.create_queue("test-queue", visibility_timeout=60) + queue = conn.create_queue("test-queue", visibility_timeout=3) queue.set_message_class(RawMessage) body_one = 'test message' * 200000 @@ -271,7 +307,7 @@ def test_message_becomes_inflight_when_received(): @mock_sqs_deprecated def test_receive_message_with_explicit_visibility_timeout(): conn = boto.connect_sqs('the_key', 'the_secret') - queue = conn.create_queue("test-queue", visibility_timeout=60) + queue = conn.create_queue("test-queue", visibility_timeout=3) queue.set_message_class(RawMessage) body_one = 'this is another test message' @@ -360,7 +396,7 @@ def test_read_message_from_queue(): @mock_sqs_deprecated def test_queue_length(): conn = boto.connect_sqs('the_key', 'the_secret') - queue = conn.create_queue("test-queue", visibility_timeout=60) + queue = conn.create_queue("test-queue", visibility_timeout=3) queue.set_message_class(RawMessage) queue.write(queue.new_message('this is a test message')) @@ -371,7 +407,7 @@ def test_queue_length(): @mock_sqs_deprecated def test_delete_message(): conn = boto.connect_sqs('the_key', 'the_secret') - queue = conn.create_queue("test-queue", visibility_timeout=60) + queue = conn.create_queue("test-queue", visibility_timeout=3) queue.set_message_class(RawMessage) queue.write(queue.new_message('this is a test message')) @@ -414,7 +450,7 @@ def test_send_batch_operation(): @mock_sqs_deprecated def test_send_batch_operation_with_message_attributes(): conn = boto.connect_sqs('the_key', 'the_secret') - queue = conn.create_queue("test-queue", visibility_timeout=60) + queue = conn.create_queue("test-queue", visibility_timeout=3) queue.set_message_class(RawMessage) message_tuple = ("my_first_message", 'test message 1', 0, { @@ -431,7 +467,7 @@ def test_send_batch_operation_with_message_attributes(): @mock_sqs_deprecated def test_delete_batch_operation(): conn = boto.connect_sqs('the_key', 'the_secret') - queue = conn.create_queue("test-queue", visibility_timeout=60) + queue = conn.create_queue("test-queue", visibility_timeout=3) conn.send_message_batch(queue, [ ("my_first_message", 'test message 1', 0), @@ -450,7 +486,7 @@ def test_queue_attributes(): conn = boto.connect_sqs('the_key', 'the_secret') queue_name = 'test-queue' - visibility_timeout = 60 + visibility_timeout = 3 queue = conn.create_queue( queue_name, visibility_timeout=visibility_timeout)