Repository: localstack/localstack

Issue:
bug: Publishing a message to SNS with empty body should fail
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Current Behavior

When you publish a message to SNS without a *body* (e.g. only attributes) it succeeds.

### Expected Behavior

Trying to publish a message without a *body* should fail (at least it does when trying to do this in AWS)

### How are you starting LocalStack?

Custom (please describe below)

### Steps To Reproduce

#### How are you starting localstack (e.g., `bin/localstack` command, arguments, or `docker-compose.yml`)

using Java testcontainers...

#### Client commands (e.g., AWS SDK code snippet, or sequence of "awslocal" commands)

```bash
#!/bin/sh
 
queue_arn=$(awslocal sqs create-queue --queue-name $QUEUE_NAME --output text)
echo "Queue ARN: $queue_arn"

topic_arn=$(awslocal sns create-topic --name $TOPIC_NAME --output text)
echo "Topic ARN: $topic_arn"

subscription_arn=$(awslocal sns subscribe \
    --attributes RawMessageDelivery=true \
    --topic-arn "$topic_arn" \
    --protocol sqs \
    --notification-endpoint "$queue_arn" \
    --output text)
echo "Subscription ARN: $subscription_arn" 
```


### Environment

```markdown
- OS: OSX 11.5.2
- LocalStack: 0.13.0
- Docker Desktop: v20.10.8
```


### Anything else?

_No response_


Discussion:

Resolving patch:
diff --git a/localstack/services/sns/sns_listener.py b/localstack/services/sns/sns_listener.py
--- a/localstack/services/sns/sns_listener.py
+++ b/localstack/services/sns/sns_listener.py
@@ -165,6 +165,12 @@ def forward_request(self, method, path, data, headers):
             elif req_action == "Publish":
                 if req_data.get("Subject") == [""]:
                     return make_error(code=400, code_string="InvalidParameter", message="Subject")
+                if not req_data.get("Message") or all(
+                    not message for message in req_data.get("Message")
+                ):
+                    return make_error(
+                        code=400, code_string="InvalidParameter", message="Empty message"
+                    )
                 sns_backend = SNSBackend.get()
                 # No need to create a topic to send SMS or single push notifications with SNS
                 # but we can't mock a sending so we only return that it went well


Test patch:
diff --git a/tests/integration/fixtures.py b/tests/integration/fixtures.py
--- a/tests/integration/fixtures.py
+++ b/tests/integration/fixtures.py
@@ -29,6 +29,7 @@
     from mypy_boto3_secretsmanager import SecretsManagerClient
     from mypy_boto3_ses import SESClient
     from mypy_boto3_sns import SNSClient
+    from mypy_boto3_sns.type_defs import GetTopicAttributesResponseTypeDef
     from mypy_boto3_sqs import SQSClient
     from mypy_boto3_ssm import SSMClient
     from mypy_boto3_stepfunctions import SFNClient
@@ -229,13 +230,29 @@ def sqs_queue(sqs_create_queue):
 
 
 @pytest.fixture
-def sns_topic(sns_client):
-    # TODO: add fixture factories
-    topic_name = "test-topic-%s" % short_uid()
-    response = sns_client.create_topic(Name=topic_name)
-    topic_arn = response["TopicArn"]
-    yield sns_client.get_topic_attributes(TopicArn=topic_arn)
-    sns_client.delete_topic(TopicArn=topic_arn)
+def sns_create_topic(sns_client):
+    topic_arns = []
+
+    def _create_topic(**kwargs):
+        if "Name" not in kwargs:
+            kwargs["Name"] = "test-topic-%s" % short_uid()
+        response = sns_client.create_topic(**kwargs)
+        topic_arns.append(response["TopicArn"])
+        return response
+
+    yield _create_topic
+
+    for topic_arn in topic_arns:
+        try:
+            sns_client.delete_topic(TopicArn=topic_arn)
+        except Exception as e:
+            LOG.debug("error cleaning up topic %s: %s", topic_arn, e)
+
+
+@pytest.fixture
+def sns_topic(sns_client, sns_create_topic) -> "GetTopicAttributesResponseTypeDef":
+    topic_arn = sns_create_topic()["TopicArn"]
+    return sns_client.get_topic_attributes(TopicArn=topic_arn)
 
 
 @pytest.fixture
diff --git a/tests/integration/test_sns.py b/tests/integration/test_sns.py
--- a/tests/integration/test_sns.py
+++ b/tests/integration/test_sns.py
@@ -1068,3 +1068,20 @@ def test_not_found_error_on_get_subscription_attributes(self):
         # cleanup
         self.sns_client.delete_topic(TopicArn=topic_arn)
         self.sqs_client.delete_queue(QueueUrl=queue_url)
+
+
+def test_empty_sns_message(sns_client, sqs_client, sns_topic, sqs_queue):
+    topic_arn = sns_topic["Attributes"]["TopicArn"]
+    queue_arn = sqs_client.get_queue_attributes(QueueUrl=sqs_queue, AttributeNames=["QueueArn"])[
+        "Attributes"
+    ]["QueueArn"]
+    sns_client.subscribe(TopicArn=topic_arn, Protocol="sqs", Endpoint=queue_arn)
+    with pytest.raises(ClientError) as e:
+        sns_client.publish(Message="", TopicArn=topic_arn)
+    assert e.match("Empty message")
+    assert (
+        sqs_client.get_queue_attributes(
+            QueueUrl=sqs_queue, AttributeNames=["ApproximateNumberOfMessages"]
+        )["Attributes"]["ApproximateNumberOfMessages"]
+        == "0"
+    )