Repository: localstack/localstack

Issue:
Message attributes are not delivered when publishing a message on a SNS topic and receiving it from an SQS queue when using raw message delivery
Relates to: https://github.com/localstack/localstack/issues/137

For the special case of using raw message delivery, the message attributes are not passed along to SQS. Trivial to reproduce from https://github.com/localstack/localstack/issues/137 after enabling raw message delivery for the subscription.

<!-- Love localstack? Please consider supporting our collective:
👉  https://opencollective.com/localstack/donate -->


Discussion:
Seeing the same thing here.  "Attributes" seem to work fine, but not "MessageAttributes".  Tested with aws command-line and boto3 Python client -- same results.

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
@@ -84,7 +84,8 @@ def forward_request(self, method, path, data, headers):
                             try:
                                 sqs_client.send_message(
                                     QueueUrl=queue_url,
-                                    MessageBody=create_sns_message_body(subscriber, req_data)
+                                    MessageBody=create_sns_message_body(subscriber, req_data),
+                                    MessageAttributes=create_sqs_message_attributes(subscriber, message_attributes)
                                 )
                             except Exception as exc:
                                 return make_error(message=str(exc), code=400)
@@ -233,6 +234,23 @@ def create_sns_message_body(subscriber, req_data):
     return json.dumps(data)
 
 
+def create_sqs_message_attributes(subscriber, attributes):
+    if subscriber.get('RawMessageDelivery') not in ('true', True):
+        return {}
+
+    message_attributes = {}
+    for key, value in attributes.items():
+        attribute = {}
+        attribute['DataType'] = value['Type']
+        if value['Type'] == 'Binary':
+            attribute['BinaryValue'] = value['Value']
+        else:
+            attribute['StringValue'] = value['Value']
+        message_attributes[key] = attribute
+
+    return message_attributes
+
+
 def get_message_attributes(req_data):
     attributes = {}
     x = 1


Test patch:
diff --git a/tests/unit/test_sns_listener.py b/tests/unit/test_sns_listener.py
--- a/tests/unit/test_sns_listener.py
+++ b/tests/unit/test_sns_listener.py
@@ -18,25 +18,25 @@ def setUp(self):
     def test_unsubscribe_without_arn_should_error(self):
         sns = sns_listener.ProxyListenerSNS()
         error = sns.forward_request('POST', '/', 'Action=Unsubscribe', '')
-        assert(error is not None)
-        assert(error.status_code == 400)
+        self.assertTrue(error is not None)
+        self.assertEqual(error.status_code, 400)
 
     def test_unsubscribe_should_remove_listener(self):
         sub_arn = 'arn:aws:sns:us-east-1:123456789012:test-topic:45e61c7f-dca5-4fcd-be2b-4e1b0d6eef72'
         topic_arn = 'arn:aws:sns:us-east-1:123456789012:test-topic'
 
-        assert(sns_listener.get_topic_by_arn(topic_arn) is None)
+        self.assertFalse(sns_listener.get_topic_by_arn(topic_arn))
         sns_listener.do_create_topic(topic_arn)
-        assert(sns_listener.get_topic_by_arn(topic_arn) is not None)
+        self.assertTrue(sns_listener.get_topic_by_arn(topic_arn) is not None)
         sns_listener.do_subscribe(
             topic_arn,
             'http://localhost:1234/listen',
             'http',
             sub_arn
         )
-        assert(sns_listener.get_subscription_by_arn(sub_arn) is not None)
+        self.assertTrue(sns_listener.get_subscription_by_arn(sub_arn))
         sns_listener.do_unsubscribe(sub_arn)
-        assert(sns_listener.get_subscription_by_arn(sub_arn) is None)
+        self.assertFalse(sns_listener.get_subscription_by_arn(sub_arn))
 
     def test_create_sns_message_body_raw_message_delivery(self):
         self.subscriber['RawMessageDelivery'] = 'true'
@@ -44,7 +44,7 @@ def test_create_sns_message_body_raw_message_delivery(self):
             'Message': ['msg']
         }
         result = sns_listener.create_sns_message_body(self.subscriber, action)
-        assert (result == 'msg')
+        self.assertEqual(result, 'msg')
 
     def test_create_sns_message_body(self):
         action = {
@@ -99,7 +99,7 @@ def test_create_sns_message_body_json_structure(self):
         result_str = sns_listener.create_sns_message_body(self.subscriber, action)
         result = json.loads(result_str)
 
-        assert (result['Message'] == {'message': 'abc'})
+        self.assertEqual(result['Message'], {'message': 'abc'})
 
     def test_create_sns_message_body_json_structure_without_default_key(self):
         action = {
@@ -108,7 +108,7 @@ def test_create_sns_message_body_json_structure_without_default_key(self):
         }
         with assert_raises(Exception) as exc:
             sns_listener.create_sns_message_body(self.subscriber, action)
-        assert str(exc.exception) == "Unable to find 'default' key in message payload"
+        self.assertEqual(str(exc.exception), "Unable to find 'default' key in message payload")
 
     def test_create_sns_message_body_json_structure_sqs_protocol(self):
         action = {
@@ -118,7 +118,33 @@ def test_create_sns_message_body_json_structure_sqs_protocol(self):
         result_str = sns_listener.create_sns_message_body(self.subscriber, action)
         result = json.loads(result_str)
 
-        assert (result['Message'] == 'sqs message')
+        self.assertEqual(result['Message'], 'sqs message')
+
+    def test_create_sqs_message_attributes(self):
+        self.subscriber['RawMessageDelivery'] = 'true'
+        action = {
+            'Message': ['msg'],
+            'Subject': ['subject'],
+            'MessageAttributes.entry.1.Name': ['attr1'],
+            'MessageAttributes.entry.1.Value.DataType': ['String'],
+            'MessageAttributes.entry.1.Value.StringValue': ['value1'],
+            'MessageAttributes.entry.2.Name': ['attr2'],
+            'MessageAttributes.entry.2.Value.DataType': ['Binary'],
+            'MessageAttributes.entry.2.Value.BinaryValue': ['value2'.encode('utf-8')],
+            'MessageAttributes.entry.3.Name': ['attr3'],
+            'MessageAttributes.entry.3.Value.DataType': ['Number'],
+            'MessageAttributes.entry.3.Value.StringValue': ['value3'],
+        }
+
+        attributes = sns_listener.get_message_attributes(action)
+        result = sns_listener.create_sqs_message_attributes(self.subscriber, attributes)
+
+        self.assertEqual(result['attr1']['DataType'], 'String')
+        self.assertEqual(result['attr1']['StringValue'], 'value1')
+        self.assertEqual(result['attr2']['DataType'], 'Binary')
+        self.assertEqual(result['attr2']['BinaryValue'], 'value2'.encode('utf-8'))
+        self.assertEqual(result['attr3']['DataType'], 'Number')
+        self.assertEqual(result['attr3']['StringValue'], 'value3')
 
 
 def test_filter_policy():