Repository: localstack/localstack

Issue:
bug: Fifo batch publish silently fails
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Current Behavior

if a Fifo queue is created withotu deduplication  configuration such as `MessageDeduplicationId` or `ContentBasedDeduplication`

Sending a batch of message to such queue will silently fail (with http code 200)

### Expected Behavior

it should fail with error

`InvalidParameterValue: The Queue should either have ContentBasedDeduplication enabled or MessageDeduplicationId provided explicitly `

### How are you starting LocalStack?

With a `docker run` command

### Steps To Reproduce

#### How are you starting localstack (e.g., `bin/localstack` command, arguments, or `docker-compose.yml`)

    `docker run -p 127.0.0.1:4566:4566/tcp --network=host localstack/localstack:latest`

#### Client commands (e.g., AWS SDK code snippet, or sequence of "awslocal" commands)

```js
const { SQSClient, SendMessageCommand, ReceiveMessageCommand, CreateQueueCommand, SendMessageBatchCommand }  = require('@aws-sdk/client-sqs')

const credentials = {
  accessKeyId: 'test',
  secretAccessKey: 'test',
};

const connection = {
  region: 'us-east-1',
  accountId: '000000000000',
  endpoint: 'http://localhost:4566',
};

const queueName = 'test-queue6.fifo';

async function main() {
  const sqsClient = new SQSClient({
    ...connection,
    credentials,
  });

  const { QueueUrl } = await sqsClient.send(new CreateQueueCommand({
    QueueName: queueName,
    Attributes: {
      FifoQueue: 'true',
      //ContentBasedDeduplication: 'true',
    }
  }));

  await sqsClient.send(new SendMessageBatchCommand({
    QueueUrl,
    Entries: [{
      Id: '1',
      MessageBody: 'test4',
      MessageGroupId: 'test-group'
    }]
  }));


  const { Messages } = await sqsClient.send(new ReceiveMessageCommand({
    QueueUrl
  }));

  return Messages
}

main().then((r) => console.log(r)).catch((e) => console.error(e));
```

### Environment

```markdown
- OS: Osx
- LocalStack:
```


### Anything else?

Use the lastest version on AWS SDK (!)


Discussion:

Resolving patch:
diff --git a/localstack/services/sqs/models.py b/localstack/services/sqs/models.py
--- a/localstack/services/sqs/models.py
+++ b/localstack/services/sqs/models.py
@@ -31,6 +31,7 @@
     decode_receipt_handle,
     encode_receipt_handle,
     global_message_sequence,
+    is_message_deduplication_id_required,
 )
 from localstack.services.stores import AccountRegionBundle, BaseStore, LocalAttribute
 from localstack.utils.time import now
@@ -816,10 +817,7 @@ def put(
         if not message_group_id:
             raise MissingParameter("The request must contain the parameter MessageGroupId.")
         dedup_id = message_deduplication_id
-        content_based_deduplication = (
-            "true"
-            == (self.attributes.get(QueueAttributeName.ContentBasedDeduplication, "false")).lower()
-        )
+        content_based_deduplication = not is_message_deduplication_id_required(self)
         if not dedup_id and content_based_deduplication:
             dedup_id = hashlib.sha256(message.get("Body").encode("utf-8")).hexdigest()
         if not dedup_id:
diff --git a/localstack/services/sqs/provider.py b/localstack/services/sqs/provider.py
--- a/localstack/services/sqs/provider.py
+++ b/localstack/services/sqs/provider.py
@@ -77,7 +77,12 @@
     StandardQueue,
     sqs_stores,
 )
-from localstack.services.sqs.utils import generate_message_id, parse_queue_url
+from localstack.services.sqs.utils import (
+    generate_message_id,
+    is_fifo_queue,
+    is_message_deduplication_id_required,
+    parse_queue_url,
+)
 from localstack.utils.aws import aws_stack
 from localstack.utils.aws.arns import parse_arn
 from localstack.utils.aws.request_context import extract_region_from_headers
@@ -848,7 +853,11 @@ def send_message_batch(
     ) -> SendMessageBatchResult:
         queue = self._resolve_queue(context, queue_url=queue_url)
 
-        self._assert_batch(entries)
+        self._assert_batch(
+            entries,
+            require_fifo_queue_params=is_fifo_queue(queue),
+            require_message_deduplication_id=is_message_deduplication_id_required(queue),
+        )
         # check the total batch size first and raise BatchRequestTooLong id > DEFAULT_MAXIMUM_MESSAGE_SIZE.
         # This is checked before any messages in the batch are sent.  Raising the exception here should
         # cause error response, rather than batching error results and returning
@@ -1186,7 +1195,13 @@ def _validate_actions(self, actions: ActionNameList):
                     "WSDL for a list of valid actions. "
                 )
 
-    def _assert_batch(self, batch: List) -> None:
+    def _assert_batch(
+        self,
+        batch: List,
+        *,
+        require_fifo_queue_params: bool = False,
+        require_message_deduplication_id: bool = False,
+    ) -> None:
         if not batch:
             raise EmptyBatchRequest
         if batch and (no_entries := len(batch)) > MAX_NUMBER_OF_MESSAGES:
@@ -1201,6 +1216,15 @@ def _assert_batch(self, batch: List) -> None:
                     "A batch entry id can only contain alphanumeric characters, hyphens and underscores. "
                     "It can be at most 80 letters long."
                 )
+            if require_message_deduplication_id and not entry.get("MessageDeduplicationId"):
+                raise InvalidParameterValue(
+                    "The queue should either have ContentBasedDeduplication enabled or "
+                    "MessageDeduplicationId provided explicitly"
+                )
+            if require_fifo_queue_params and not entry.get("MessageGroupId"):
+                raise InvalidParameterValue(
+                    "The request must contain the parameter MessageGroupId."
+                )
             if entry_id in visited:
                 raise BatchEntryIdsNotDistinct()
             else:
diff --git a/localstack/services/sqs/utils.py b/localstack/services/sqs/utils.py
--- a/localstack/services/sqs/utils.py
+++ b/localstack/services/sqs/utils.py
@@ -9,7 +9,7 @@
 from moto.sqs.models import TRANSPORT_TYPE_ENCODINGS, Message
 
 from localstack.aws.accounts import get_aws_account_id
-from localstack.aws.api.sqs import ReceiptHandleIsInvalid
+from localstack.aws.api.sqs import QueueAttributeName, ReceiptHandleIsInvalid
 from localstack.utils.aws.arns import parse_arn
 from localstack.utils.common import clone
 from localstack.utils.objects import singleton_factory
@@ -22,6 +22,18 @@ def is_sqs_queue_url(url):
     return re.match(r"^/(queue|%s)/[a-zA-Z0-9_-]+(.fifo)?$" % get_aws_account_id(), path)
 
 
+def is_message_deduplication_id_required(queue):
+    content_based_deduplication_disabled = (
+        "false"
+        == (queue.attributes.get(QueueAttributeName.ContentBasedDeduplication, "false")).lower()
+    )
+    return is_fifo_queue(queue) and content_based_deduplication_disabled
+
+
+def is_fifo_queue(queue):
+    return "true" == queue.attributes.get(QueueAttributeName.FifoQueue, "false").lower()
+
+
 def parse_queue_url(queue_url: str) -> Tuple[str, Optional[str], str]:
     """
     Parses an SQS Queue URL and returns a triple of account_id, region and queue_name.


Test patch:
diff --git a/tests/aws/services/sqs/test_sqs.py b/tests/aws/services/sqs/test_sqs.py
--- a/tests/aws/services/sqs/test_sqs.py
+++ b/tests/aws/services/sqs/test_sqs.py
@@ -1822,6 +1822,64 @@ def test_invalid_batch_id(self, sqs_create_queue, snapshot, aws_client):
             aws_client.sqs.send_message_batch(QueueUrl=queue_url, Entries=message_batch)
         snapshot.match("test_invalid_batch_id", e.value.response)
 
+    @markers.aws.validated
+    @markers.snapshot.skip_snapshot_verify(paths=["$..Error.Detail"])
+    def test_send_batch_missing_deduplication_id_for_fifo_queue(
+        self, sqs_create_queue, snapshot, aws_client
+    ):
+        queue_url = sqs_create_queue(
+            QueueName=f"queue-{short_uid()}.fifo",
+            Attributes={
+                "FifoQueue": "true",
+                "ContentBasedDeduplication": "false",
+            },
+        )
+        message_batch = [
+            {
+                "Id": f"message-{short_uid()}",
+                "MessageBody": "messageBody",
+                "MessageGroupId": "test-group",
+                "MessageDeduplicationId": f"dedup-{short_uid()}",
+            },
+            {
+                "Id": f"message-{short_uid()}",
+                "MessageBody": "messageBody",
+                "MessageGroupId": "test-group",
+            },
+        ]
+        with pytest.raises(ClientError) as e:
+            aws_client.sqs.send_message_batch(QueueUrl=queue_url, Entries=message_batch)
+        snapshot.match("test_missing_deduplication_id_for_fifo_queue", e.value.response)
+
+    @markers.parity.aws_validated
+    @markers.snapshot.skip_snapshot_verify(paths=["$..Error.Detail"])
+    def test_send_batch_missing_message_group_id_for_fifo_queue(
+        self, sqs_create_queue, snapshot, aws_client
+    ):
+        queue_url = sqs_create_queue(
+            QueueName=f"queue-{short_uid()}.fifo",
+            Attributes={
+                "FifoQueue": "true",
+                "ContentBasedDeduplication": "false",
+            },
+        )
+        message_batch = [
+            {
+                "Id": f"message-{short_uid()}",
+                "MessageBody": "messageBody",
+                "MessageGroupId": "test-group",
+                "MessageDeduplicationId": f"dedup-{short_uid()}",
+            },
+            {
+                "Id": f"message-{short_uid()}",
+                "MessageBody": "messageBody",
+                "MessageDeduplicationId": f"dedup-{short_uid()}",
+            },
+        ]
+        with pytest.raises(ClientError) as e:
+            aws_client.sqs.send_message_batch(QueueUrl=queue_url, Entries=message_batch)
+        snapshot.match("test_missing_message_group_id_for_fifo_queue", e.value.response)
+
     @markers.aws.validated
     def test_publish_get_delete_message_batch(self, sqs_create_queue, aws_client):
         message_count = 10
diff --git a/tests/aws/services/sqs/test_sqs.snapshot.json b/tests/aws/services/sqs/test_sqs.snapshot.json
--- a/tests/aws/services/sqs/test_sqs.snapshot.json
+++ b/tests/aws/services/sqs/test_sqs.snapshot.json
@@ -1065,5 +1065,39 @@
         }
       }
     }
+  },
+  "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_batch_missing_message_group_id_for_fifo_queue": {
+    "recorded-date": "02-08-2023, 12:36:06",
+    "recorded-content": {
+      "test_missing_message_group_id_for_fifo_queue": {
+        "Error": {
+          "Code": "InvalidParameterValue",
+          "Detail": null,
+          "Message": "The request must contain the parameter MessageGroupId.",
+          "Type": "Sender"
+        },
+        "ResponseMetadata": {
+          "HTTPHeaders": {},
+          "HTTPStatusCode": 400
+        }
+      }
+    }
+  },
+  "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_batch_missing_deduplication_id_for_fifo_queue": {
+    "recorded-date": "02-08-2023, 12:37:36",
+    "recorded-content": {
+      "test_missing_deduplication_id_for_fifo_queue": {
+        "Error": {
+          "Code": "InvalidParameterValue",
+          "Detail": null,
+          "Message": "The queue should either have ContentBasedDeduplication enabled or MessageDeduplicationId provided explicitly",
+          "Type": "Sender"
+        },
+        "ResponseMetadata": {
+          "HTTPHeaders": {},
+          "HTTPStatusCode": 400
+        }
+      }
+    }
   }
 }