Repository: getmoto/moto

Issue:
Publishing to FIFO sns topic with FIFO sqs queue subscription doesn't work
Creating a FIFO sns topic and subscribing a FIFO sqs queue works, but there's no way to publish to it since the MessageGroupId parameter is not forwarded.

Example test case:

```python
from moto import mock_sns, mock_sqs
import boto3

def test_sns_sqs():
    with mock_sns(), mock_sqs():
        sns = boto3.resource('sns')
        sqs = boto3.resource('sqs')
        queue = sqs.create_queue(
            QueueName="queue.fifo",
            Attributes={
                'FifoQueue': 'true',
                'ContentBasedDeduplication': 'true',
            },
        )

        topic = sns.create_topic(
            Name="topic.fifo",
            Attributes={
                'FifoTopic': 'true',
                'ContentBasedDeduplication': 'true',
            },
        )
        topic.subscribe(
            Protocol='sqs',
            Endpoint=queue.attributes['QueueArn'],
        )

        topic.publish(Message="message", MessageGroupId="message_group_id")

```

Traceback:

```
______________________________________________ test_sns_sqs ______________________________________________

    def test_sns_sqs():
    
        with mock_sns(), mock_sqs():
            sns = boto3.resource('sns')
            sqs = boto3.resource('sqs')
            queue = sqs.create_queue(
                QueueName="queue.fifo",
                Attributes={
                    'FifoQueue': 'true',
                    'ContentBasedDeduplication': 'true',
                },
            )
    
            topic = sns.create_topic(
                Name="topic.fifo",
                Attributes={
                    'FifoTopic': 'true',
                    'ContentBasedDeduplication': 'true',
                },
            )
            topic.subscribe(
                Protocol='sqs',
                Endpoint=queue.attributes['QueueArn'],
            )
    
>           topic.publish(Message="message", MessageGroupId="message_group_id")

app/test_main.py:71: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
../../.direnv/python-3.10.1/lib/python3.10/site-packages/boto3/resources/factory.py:520: in do_action
    response = action(self, *args, **kwargs)
../../.direnv/python-3.10.1/lib/python3.10/site-packages/boto3/resources/action.py:83: in __call__
    response = getattr(parent.meta.client, operation_name)(*args, **params)
../../.direnv/python-3.10.1/lib/python3.10/site-packages/botocore/client.py:491: in _api_call
    return self._make_api_call(operation_name, kwargs)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <botocore.client.SNS object at 0x7f5b1b134610>, operation_name = 'Publish'
api_params = {'Message': 'message', 'MessageGroupId': 'message_group_id', 'TopicArn': 'arn:aws:sns:eu-west-1:123456789012:topic.fifo'}

    def _make_api_call(self, operation_name, api_params):
        operation_model = self._service_model.operation_model(operation_name)
        service_name = self._service_model.service_name
        history_recorder.record(
            'API_CALL',
            {
                'service': service_name,
                'operation': operation_name,
                'params': api_params,
            },
        )
        if operation_model.deprecated:
            logger.debug(
                'Warning: %s.%s() is deprecated', service_name, operation_name
            )
        request_context = {
            'client_region': self.meta.region_name,
            'client_config': self.meta.config,
            'has_streaming_input': operation_model.has_streaming_input,
            'auth_type': operation_model.auth_type,
        }
        request_dict = self._convert_to_request_dict(
            api_params, operation_model, context=request_context
        )
    
        service_id = self._service_model.service_id.hyphenize()
        handler, event_response = self.meta.events.emit_until_response(
            'before-call.{service_id}.{operation_name}'.format(
                service_id=service_id, operation_name=operation_name
            ),
            model=operation_model,
            params=request_dict,
            request_signer=self._request_signer,
            context=request_context,
        )
    
        if event_response is not None:
            http, parsed_response = event_response
        else:
            http, parsed_response = self._make_request(
                operation_model, request_dict, request_context
            )
    
        self.meta.events.emit(
            'after-call.{service_id}.{operation_name}'.format(
                service_id=service_id, operation_name=operation_name
            ),
            http_response=http,
            parsed=parsed_response,
            model=operation_model,
            context=request_context,
        )
    
        if http.status_code >= 300:
            error_code = parsed_response.get("Error", {}).get("Code")
            error_class = self.exceptions.from_code(error_code)
>           raise error_class(parsed_response, operation_name)
E           botocore.exceptions.ClientError: An error occurred (MissingParameter) when calling the Publish operation: The request must contain the parameter MessageGroupId.

../../.direnv/python-3.10.1/lib/python3.10/site-packages/botocore/client.py:897: ClientError

```


Discussion:
Thanks for raising this and providing the test case, @sindrig, that's very much appreciated. Marking it as a bug.

Resolving patch:
diff --git a/moto/sns/models.py b/moto/sns/models.py
--- a/moto/sns/models.py
+++ b/moto/sns/models.py
@@ -13,6 +13,7 @@
     BackendDict,
 )
 from moto.sqs import sqs_backends
+from moto.sqs.exceptions import MissingParameter
 
 from .exceptions import (
     SNSNotFoundError,
@@ -55,7 +56,7 @@ def __init__(self, name, sns_backend):
         self.fifo_topic = "false"
         self.content_based_deduplication = "false"
 
-    def publish(self, message, subject=None, message_attributes=None):
+    def publish(self, message, subject=None, message_attributes=None, group_id=None):
         message_id = str(uuid.uuid4())
         subscriptions, _ = self.sns_backend.list_subscriptions(self.arn)
         for subscription in subscriptions:
@@ -64,6 +65,7 @@ def publish(self, message, subject=None, message_attributes=None):
                 message_id,
                 subject=subject,
                 message_attributes=message_attributes,
+                group_id=group_id,
             )
         return message_id
 
@@ -177,7 +179,9 @@ def __init__(self, topic, endpoint, protocol):
         self._filter_policy = None  # filter policy as a dict, not json.
         self.confirmed = False
 
-    def publish(self, message, message_id, subject=None, message_attributes=None):
+    def publish(
+        self, message, message_id, subject=None, message_attributes=None, group_id=None
+    ):
         if not self._matches_filter_policy(message_attributes):
             return
 
@@ -198,6 +202,7 @@ def publish(self, message, message_id, subject=None, message_attributes=None):
                         indent=2,
                         separators=(",", ": "),
                     ),
+                    group_id=group_id,
                 )
             else:
                 raw_message_attributes = {}
@@ -215,7 +220,10 @@ def publish(self, message, message_id, subject=None, message_attributes=None):
                     }
 
                 sqs_backends[region].send_message(
-                    queue_name, message, message_attributes=raw_message_attributes
+                    queue_name,
+                    message,
+                    message_attributes=raw_message_attributes,
+                    group_id=group_id,
                 )
         elif self.protocol in ["http", "https"]:
             post_data = self.get_post_data(message, message_id, subject)
@@ -568,6 +576,7 @@ def publish(
         phone_number=None,
         subject=None,
         message_attributes=None,
+        group_id=None,
     ):
         if subject is not None and len(subject) > 100:
             # Note that the AWS docs around length are wrong: https://github.com/spulec/moto/issues/1503
@@ -589,8 +598,25 @@ def publish(
 
         try:
             topic = self.get_topic(arn)
+
+            fifo_topic = topic.fifo_topic == "true"
+            if group_id is None:
+                # MessageGroupId is a mandatory parameter for all
+                # messages in a fifo queue
+                if fifo_topic:
+                    raise MissingParameter("MessageGroupId")
+            else:
+                if not fifo_topic:
+                    msg = (
+                        "Value {} for parameter MessageGroupId is invalid. "
+                        "Reason: The request include parameter that is not valid for this queue type."
+                    ).format(group_id)
+                    raise InvalidParameterValue(msg)
             message_id = topic.publish(
-                message, subject=subject, message_attributes=message_attributes
+                message,
+                subject=subject,
+                message_attributes=message_attributes,
+                group_id=group_id,
             )
         except SNSNotFoundError:
             endpoint = self.get_endpoint(arn)
diff --git a/moto/sns/responses.py b/moto/sns/responses.py
--- a/moto/sns/responses.py
+++ b/moto/sns/responses.py
@@ -328,6 +328,7 @@ def publish(self):
         topic_arn = self._get_param("TopicArn")
         phone_number = self._get_param("PhoneNumber")
         subject = self._get_param("Subject")
+        message_group_id = self._get_param("MessageGroupId")
 
         message_attributes = self._parse_message_attributes()
 
@@ -355,6 +356,7 @@ def publish(self):
                 phone_number=phone_number,
                 subject=subject,
                 message_attributes=message_attributes,
+                group_id=message_group_id,
             )
         except ValueError as err:
             error_response = self._error("InvalidParameter", str(err))


Test patch:
diff --git a/tests/test_sns/test_publishing_boto3.py b/tests/test_sns/test_publishing_boto3.py
--- a/tests/test_sns/test_publishing_boto3.py
+++ b/tests/test_sns/test_publishing_boto3.py
@@ -83,6 +83,27 @@ def test_publish_to_sqs_raw():
     messages[0].body.should.equal(message)
 
 
+@mock_sns
+@mock_sqs
+def test_publish_to_sqs_fifo():
+    sns = boto3.resource("sns", region_name="us-east-1")
+    topic = sns.create_topic(
+        Name="topic.fifo",
+        Attributes={"FifoTopic": "true", "ContentBasedDeduplication": "true",},
+    )
+
+    sqs = boto3.resource("sqs", region_name="us-east-1")
+    queue = sqs.create_queue(
+        QueueName="queue.fifo",
+        Attributes={"FifoQueue": "true", "ContentBasedDeduplication": "true",},
+    )
+    topic.subscribe(
+        Protocol="sqs", Endpoint=queue.attributes["QueueArn"],
+    )
+
+    topic.publish(Message="message", MessageGroupId="message_group_id")
+
+
 @mock_sqs
 @mock_sns
 def test_publish_to_sqs_bad():
@@ -154,7 +175,7 @@ def test_publish_to_sqs_msg_attr_byte_value():
     sqs = boto3.resource("sqs", region_name="us-east-1")
     queue = sqs.create_queue(QueueName="test-queue")
     conn.subscribe(
-        TopicArn=topic_arn, Protocol="sqs", Endpoint=queue.attributes["QueueArn"],
+        TopicArn=topic_arn, Protocol="sqs", Endpoint=queue.attributes["QueueArn"]
     )
     queue_raw = sqs.create_queue(QueueName="test-queue-raw")
     conn.subscribe(
@@ -431,6 +452,39 @@ def test_publish_message_too_long():
     topic.publish(Message="".join(["." for i in range(0, 262144)]))
 
 
+@mock_sns
+def test_publish_fifo_needs_group_id():
+    sns = boto3.resource("sns", region_name="us-east-1")
+    topic = sns.create_topic(
+        Name="topic.fifo",
+        Attributes={"FifoTopic": "true", "ContentBasedDeduplication": "true",},
+    )
+
+    with pytest.raises(
+        ClientError, match="The request must contain the parameter MessageGroupId"
+    ):
+        topic.publish(Message="message")
+
+    # message group included - OK
+    topic.publish(Message="message", MessageGroupId="message_group_id")
+
+
+@mock_sns
+@mock_sqs
+def test_publish_group_id_to_non_fifo():
+    sns = boto3.resource("sns", region_name="us-east-1")
+    topic = sns.create_topic(Name="topic")
+
+    with pytest.raises(
+        ClientError,
+        match="The request include parameter that is not valid for this queue type",
+    ):
+        topic.publish(Message="message", MessageGroupId="message_group_id")
+
+    # message group not included - OK
+    topic.publish(Message="message")
+
+
 def _setup_filter_policy_test(filter_policy):
     sns = boto3.resource("sns", region_name="us-east-1")
     topic = sns.create_topic(Name="some-topic")