Repository: getmoto/moto

Issue:
Moto should raise an error when SQS send_message_batch pass an empty entries
When doing `client.send_message_batch(QueueUrl=queue_url, Entries=[])`, boto will raise an error.

```
Traceback (most recent call last):
  File "<stdin>", line 3, in <module>
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/botocore/client.py", line 314, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/botocore/client.py", line 612, in _make_api_call
    raise error_class(parsed_response, operation_name)
botocore.errorfactory.EmptyBatchRequest: An error occurred (AWS.SimpleQueueService.EmptyBatchRequest) when calling the SendMessageBatch operation: There should be at least one SendMessageBatchRequestEntry in the request.
```

moto should raise the same error, rather than to be silent on that.

Environment:

```
$ python --version
Python 2.7.11
$ python -c "import boto3; print(boto3.__version__)"
1.4.7
$ python -c "import moto; print(moto.__version__)"
1.3.1
```


Discussion:
Thanks; setting as a feature request.

Resolving patch:
diff --git a/moto/sqs/exceptions.py b/moto/sqs/exceptions.py
--- a/moto/sqs/exceptions.py
+++ b/moto/sqs/exceptions.py
@@ -19,9 +19,12 @@ def __init__(self, description):
         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
--- a/moto/sqs/models.py
+++ b/moto/sqs/models.py
@@ -654,12 +654,21 @@ def remove_permission(self, queue_name, label):
 
     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
--- a/moto/sqs/responses.py
+++ b/moto/sqs/responses.py
@@ -10,7 +10,6 @@
 from .exceptions import (
     MessageAttributesInvalid,
     MessageNotInflight,
-    QueueDoesNotExist,
     ReceiptHandleIsInvalid,
 )
 
@@ -90,11 +89,7 @@ def get_queue_url(self):
         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 @@ def change_message_visibility_batch(self):
 
     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 @@ def send_message_batch(self):
 
         queue_name = self._get_queue_name()
 
+        self.sqs_backend.get_queue(queue_name)
+
+        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 in range(1, 11):
+        for index, entry in entries.items():
             # 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
-
-            message_user_id_key = 'SendMessageBatchRequestEntry.{0}.Id'.format(
-                index)
-            message_user_id = self.querystring.get(message_user_id_key)[0]
             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 @@ def send_message_batch(self):
         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 @@ def purge_queue(self):
     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 @@ def untag_queue(self):
         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()


Test patch:
diff --git a/tests/test_sqs/test_sqs.py b/tests/test_sqs/test_sqs.py
--- 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(