Repository: getmoto/moto

Issue:
sqs: send_message_batch doesn't validate group ID for FIFO queues
Hello,

When putting a message into a FIFO queue, the `MessageGroupId` is mandatory. While moto checks it for messages sent one at a time, it looks like there is no check for batch sends.

The [PR](https://github.com/spulec/moto/pull/3025) that introduced the check on `send_message()` doesn't seem to have done so for `send_messages()` too.

Failing unit test on fe7430b36:
```diff
diff --git a/tests/test_sqs/test_sqs.py b/tests/test_sqs/test_sqs.py
index 3fe2e025..977a3165 100644
--- a/tests/test_sqs/test_sqs.py
+++ b/tests/test_sqs/test_sqs.py
@@ -2569,6 +2569,20 @@ def test_send_messages_to_fifo_without_message_group_id():
     ex.response["Error"]["Message"].should.equal(
         "The request must contain the parameter MessageGroupId."
     )
+    with pytest.raises(Exception) as e:
+        queue.send_messages(
+            Entries=[
+                {
+                    "Id": "id_1",
+                    "MessageBody": "body_1",
+                },
+            ]
+        )
+    ex = e.value
+    ex.response["Error"]["Code"].should.equal("MissingParameter")
+    ex.response["Error"]["Message"].should.equal(
+        "The request must contain the parameter MessageGroupId."
+    )
 
 
 @mock_logs
```


Discussion:
Thanks for raising this and providing the test case @yveoch!

Resolving patch:
diff --git a/moto/sqs/exceptions.py b/moto/sqs/exceptions.py
--- a/moto/sqs/exceptions.py
+++ b/moto/sqs/exceptions.py
@@ -123,9 +123,10 @@ def __init__(self, message):
 class MissingParameter(RESTError):
     code = 400
 
-    def __init__(self):
+    def __init__(self, parameter):
         super(MissingParameter, self).__init__(
-            "MissingParameter", "The request must contain the parameter Actions."
+            "MissingParameter",
+            "The request must contain the parameter {}.".format(parameter),
         )
 
 
diff --git a/moto/sqs/models.py b/moto/sqs/models.py
--- a/moto/sqs/models.py
+++ b/moto/sqs/models.py
@@ -706,7 +706,13 @@ def send_message(
             message.sequence_number = "".join(
                 random.choice(string.digits) for _ in range(20)
             )
-        if group_id is not None:
+
+        if group_id is None:
+            # MessageGroupId is a mandatory parameter for all
+            # messages in a fifo queue
+            if queue.fifo_queue:
+                raise MissingParameter("MessageGroupId")
+        else:
             message.group_id = group_id
 
         if message_attributes:
@@ -925,7 +931,7 @@ def add_permission(self, queue_name, actions, account_ids, label):
         queue = self.get_queue(queue_name)
 
         if not actions:
-            raise MissingParameter()
+            raise MissingParameter("Actions")
 
         if not account_ids:
             raise InvalidParameterValue(
@@ -998,14 +1004,11 @@ 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."
-            )
+            raise MissingParameter("Tags")
 
         if len(tags) > 50:
-            raise RESTError(
-                "InvalidParameterValue",
-                "Too many tags added for queue {}.".format(queue_name),
+            raise InvalidParameterValue(
+                "Too many tags added for queue {}.".format(queue_name)
             )
 
         queue.tags.update(tags)
diff --git a/moto/sqs/responses.py b/moto/sqs/responses.py
--- a/moto/sqs/responses.py
+++ b/moto/sqs/responses.py
@@ -2,6 +2,7 @@
 
 import re
 
+from moto.core.exceptions import RESTError
 from moto.core.responses import BaseResponse
 from moto.core.utils import (
     amz_crc32,
@@ -234,23 +235,19 @@ def send_message(self):
 
         queue_name = self._get_queue_name()
 
-        if not message_group_id:
-            queue = self.sqs_backend.get_queue(queue_name)
-            if queue.attributes.get("FifoQueue", False):
-                return self._error(
-                    "MissingParameter",
-                    "The request must contain the parameter MessageGroupId.",
-                )
+        try:
+            message = self.sqs_backend.send_message(
+                queue_name,
+                message,
+                message_attributes=message_attributes,
+                delay_seconds=delay_seconds,
+                deduplication_id=message_dedupe_id,
+                group_id=message_group_id,
+                system_attributes=system_message_attributes,
+            )
+        except RESTError as err:
+            return self._error(err.error_type, err.message)
 
-        message = self.sqs_backend.send_message(
-            queue_name,
-            message,
-            message_attributes=message_attributes,
-            delay_seconds=delay_seconds,
-            deduplication_id=message_dedupe_id,
-            group_id=message_group_id,
-            system_attributes=system_message_attributes,
-        )
         template = self.response_template(SEND_MESSAGE_RESPONSE)
         return template.render(message=message, message_attributes=message_attributes)
 


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
@@ -2555,7 +2555,7 @@ def test_list_queues_limits_to_1000_queues():
 
 
 @mock_sqs
-def test_send_messages_to_fifo_without_message_group_id():
+def test_send_message_to_fifo_without_message_group_id():
     sqs = boto3.resource("sqs", region_name="eu-west-3")
     queue = sqs.create_queue(
         QueueName="blah.fifo",
@@ -2571,6 +2571,25 @@ def test_send_messages_to_fifo_without_message_group_id():
     )
 
 
+@mock_sqs
+def test_send_messages_to_fifo_without_message_group_id():
+    sqs = boto3.resource("sqs", region_name="eu-west-3")
+    queue = sqs.create_queue(
+        QueueName="blah.fifo",
+        Attributes={"FifoQueue": "true", "ContentBasedDeduplication": "true"},
+    )
+
+    with pytest.raises(Exception) as e:
+        queue.send_messages(
+            Entries=[{"Id": "id_1", "MessageBody": "body_1",},]
+        )
+    ex = e.value
+    ex.response["Error"]["Code"].should.equal("MissingParameter")
+    ex.response["Error"]["Message"].should.equal(
+        "The request must contain the parameter MessageGroupId."
+    )
+
+
 @mock_logs
 @mock_lambda
 @mock_sqs