Repository: localstack/localstack

Issue:
S3 bucket notification configuration with multiple QueueConfigurations
Hi guys,

When I try to add a configuration with multiple QueueConfigurations I got this error:

```
2017-11-06T16:27:30:ERROR:localstack.services.generic_proxy: Error forwarding request: 'list' object has no attribute 'get' Traceback (most recent call last):
  File "/usr/local/lib/python3.6/site-packages/localstack/services/generic_proxy.py", line 161, in forward
    path=path, data=data, headers=forward_headers)
  File "/usr/local/lib/python3.6/site-packages/localstack/services/s3/s3_listener.py", line 402, in forward_request
    events = config.get('Event')
AttributeError: 'list' object has no attribute 'get'
```

**My S3 configuration file:**
```
{
    "QueueConfigurations": [
        {
            "Id": "DevInventoryLoaderForOffersPut",
            "QueueArn": "arn:aws:sqs:eu-west-1:211526072230:devInventoryLoaderOffersEvents",
            "Events": [
                "s3:ObjectCreated:Put",
                "s3:ObjectCreated:CompleteMultipartUpload"
            ],
            "Filter": {
                "Key": {
                    "FilterRules": [
                        {
                            "Name": "Prefix",
                            "Value": "workspace/offer/"
                        }
                    ]
                }
            }
        },
        {
            "Id": "DevInventoryLoaderForClientPut",
            "QueueArn": "arn:aws:sqs:eu-west-1:211526072230:devInventoryLoaderClientsEvents",
            "Events": [
                "s3:ObjectCreated:Put",
                "s3:ObjectCreated:CompleteMultipartUpload"
            ],
            "Filter": {
                "Key": {
                    "FilterRules": [
                        {
                            "Name": "Prefix",
                            "Value": "workspace/client/"
                        }
                    ]
                }
            }
        },
        {
            "Id": "DevInventoryLoaderForProductsPut",
            "QueueArn": "arn:aws:sqs:eu-west-1:211526072230:devInventoryLoaderProductsEvents",
            "Events": [
                "s3:ObjectCreated:Put",
                "s3:ObjectCreated:CompleteMultipartUpload"
            ],
            "Filter": {
                "Key": {
                    "FilterRules": [
                        {
                            "Name": "Prefix",
                            "Value": "workspace/product/"
                        }
                    ]
                }
            }
        },
        {
            "Id": "DevInventoryLoaderForStocksPut",
            "QueueArn": "arn:aws:sqs:eu-west-1:211526072230:devInventoryLoaderStocksEvents",
            "Events": [
                "s3:ObjectCreated:Put",
                "s3:ObjectCreated:CompleteMultipartUpload"
            ],
            "Filter": {
                "Key": {
                    "FilterRules": [
                        {
                            "Name": "Prefix",
                            "Value": "workspace/stock/"
                        }
                    ]
                }
            }
        }
    ]
}

```

Thanks.


Discussion:
I'm having the same error on the latest Docker image. The error disappears when the list of QueueConfiguration contains only a single element. 
This also happens for me.  When there is one QueueConfiguration things work fine, but with more than one I get a timeout error.  This happens both via the AWS S3 CLI and the C# S3 client (SDK v3.x)
Also experiencing this, can confirm goes away when `QueueConfiguration` only contains 1 element.
Closing this issue - should have been fixed in the meantime. Please re-open this issue if the problem persists. Thanks!
The problem is still happening on 0.9.1.
Could you please reopen the issue ?
Same problem exists with `TopicConfigurations` as well.

Resolving patch:
diff --git a/localstack/services/s3/s3_listener.py b/localstack/services/s3/s3_listener.py
--- a/localstack/services/s3/s3_listener.py
+++ b/localstack/services/s3/s3_listener.py
@@ -10,7 +10,6 @@
 import six
 import datetime
 import dateutil.parser
-from six import iteritems
 from six.moves.urllib import parse as urlparse
 from botocore.client import ClientError
 from requests.models import Response, Request
@@ -57,6 +56,7 @@
 def event_type_matches(events, action, api_method):
     """ check whether any of the event types in `events` matches the
         given `action` and `api_method`, and return the first match. """
+    events = events or []
     for event in events:
         regex = event.replace('*', '[^:]*')
         action_string = 's3:%s:%s' % (action, api_method)
@@ -139,7 +139,7 @@ def queue_url_for_arn(queue_arn):
 
 
 def send_notifications(method, bucket_name, object_path, version_id):
-    for bucket, b_cfg in iteritems(S3_NOTIFICATIONS):
+    for bucket, notifs in S3_NOTIFICATIONS.items():
         if bucket == bucket_name:
             action = {'PUT': 'ObjectCreated', 'POST': 'ObjectCreated', 'DELETE': 'ObjectRemoved'}[method]
             # TODO: support more detailed methods, e.g., DeleteMarkerCreated
@@ -150,45 +150,52 @@ def send_notifications(method, bucket_name, object_path, version_id):
                 api_method = {'PUT': 'Put', 'POST': 'Post', 'DELETE': 'Delete'}[method]
 
             event_name = '%s:%s' % (action, api_method)
-            if (event_type_matches(b_cfg['Event'], action, api_method) and
-                    filter_rules_match(b_cfg.get('Filter'), object_path)):
-                # send notification
-                message = get_event_message(
-                    event_name=event_name, bucket_name=bucket_name,
-                    file_name=urlparse.urlparse(object_path[1:]).path,
-                    version_id=version_id
-                )
-                message = json.dumps(message)
-                if b_cfg.get('Queue'):
-                    sqs_client = aws_stack.connect_to_service('sqs')
-                    try:
-                        queue_url = queue_url_for_arn(b_cfg['Queue'])
-                        sqs_client.send_message(QueueUrl=queue_url, MessageBody=message)
-                    except Exception as e:
-                        LOGGER.warning('Unable to send notification for S3 bucket "%s" to SQS queue "%s": %s' %
-                            (bucket_name, b_cfg['Queue'], e))
-                if b_cfg.get('Topic'):
-                    sns_client = aws_stack.connect_to_service('sns')
-                    try:
-                        sns_client.publish(TopicArn=b_cfg['Topic'], Message=message, Subject='Amazon S3 Notification')
-                    except Exception:
-                        LOGGER.warning('Unable to send notification for S3 bucket "%s" to SNS topic "%s".' %
-                            (bucket_name, b_cfg['Topic']))
-                # CloudFunction and LambdaFunction are semantically identical
-                lambda_function_config = b_cfg.get('CloudFunction') or b_cfg.get('LambdaFunction')
-                if lambda_function_config:
-                    # make sure we don't run into a socket timeout
-                    connection_config = botocore.config.Config(read_timeout=300)
-                    lambda_client = aws_stack.connect_to_service('lambda', config=connection_config)
-                    try:
-                        lambda_client.invoke(FunctionName=lambda_function_config,
-                                             InvocationType='Event', Payload=message)
-                    except Exception:
-                        LOGGER.warning('Unable to send notification for S3 bucket "%s" to Lambda function "%s".' %
-                            (bucket_name, lambda_function_config))
-                if not filter(lambda x: b_cfg.get(x), NOTIFICATION_DESTINATION_TYPES):
-                    LOGGER.warning('Neither of %s defined for S3 notification.' %
-                        '/'.join(NOTIFICATION_DESTINATION_TYPES))
+            for notif in notifs:
+                send_notification_for_subscriber(notif, bucket_name, object_path,
+                    version_id, api_method, action, event_name)
+
+
+def send_notification_for_subscriber(notif, bucket_name, object_path, version_id, api_method, action, event_name):
+    if (not event_type_matches(notif['Event'], action, api_method) or
+            not filter_rules_match(notif.get('Filter'), object_path)):
+        return
+    # send notification
+    message = get_event_message(
+        event_name=event_name, bucket_name=bucket_name,
+        file_name=urlparse.urlparse(object_path[1:]).path,
+        version_id=version_id
+    )
+    message = json.dumps(message)
+    if notif.get('Queue'):
+        sqs_client = aws_stack.connect_to_service('sqs')
+        try:
+            queue_url = queue_url_for_arn(notif['Queue'])
+            sqs_client.send_message(QueueUrl=queue_url, MessageBody=message)
+        except Exception as e:
+            LOGGER.warning('Unable to send notification for S3 bucket "%s" to SQS queue "%s": %s' %
+                (bucket_name, notif['Queue'], e))
+    if notif.get('Topic'):
+        sns_client = aws_stack.connect_to_service('sns')
+        try:
+            sns_client.publish(TopicArn=notif['Topic'], Message=message, Subject='Amazon S3 Notification')
+        except Exception:
+            LOGGER.warning('Unable to send notification for S3 bucket "%s" to SNS topic "%s".' %
+                (bucket_name, notif['Topic']))
+    # CloudFunction and LambdaFunction are semantically identical
+    lambda_function_config = notif.get('CloudFunction') or notif.get('LambdaFunction')
+    if lambda_function_config:
+        # make sure we don't run into a socket timeout
+        connection_config = botocore.config.Config(read_timeout=300)
+        lambda_client = aws_stack.connect_to_service('lambda', config=connection_config)
+        try:
+            lambda_client.invoke(FunctionName=lambda_function_config,
+                                 InvocationType='Event', Payload=message)
+        except Exception:
+            LOGGER.warning('Unable to send notification for S3 bucket "%s" to Lambda function "%s".' %
+                (bucket_name, lambda_function_config))
+    if not filter(lambda x: notif.get(x), NOTIFICATION_DESTINATION_TYPES):
+        LOGGER.warning('Neither of %s defined for S3 notification.' %
+            '/'.join(NOTIFICATION_DESTINATION_TYPES))
 
 
 def get_cors(bucket_name):
@@ -445,28 +452,30 @@ def handle_notification_request(bucket, method, data):
         # TODO check if bucket exists
         result = '<NotificationConfiguration xmlns="%s">' % XMLNS_S3
         if bucket in S3_NOTIFICATIONS:
-            notif = S3_NOTIFICATIONS[bucket]
-            for dest in NOTIFICATION_DESTINATION_TYPES:
-                if dest in notif:
-                    dest_dict = {
-                        '%sConfiguration' % dest: {
-                            'Id': uuid.uuid4(),
-                            dest: notif[dest],
-                            'Event': notif['Event'],
-                            'Filter': notif['Filter']
+            notifs = S3_NOTIFICATIONS[bucket]
+            for notif in notifs:
+                for dest in NOTIFICATION_DESTINATION_TYPES:
+                    if dest in notif:
+                        dest_dict = {
+                            '%sConfiguration' % dest: {
+                                'Id': uuid.uuid4(),
+                                dest: notif[dest],
+                                'Event': notif['Event'],
+                                'Filter': notif['Filter']
+                            }
                         }
-                    }
-                    result += xmltodict.unparse(dest_dict, full_document=False)
+                        result += xmltodict.unparse(dest_dict, full_document=False)
         result += '</NotificationConfiguration>'
         response._content = result
 
     if method == 'PUT':
         parsed = xmltodict.parse(data)
         notif_config = parsed.get('NotificationConfiguration')
-        S3_NOTIFICATIONS.pop(bucket, None)
+        S3_NOTIFICATIONS[bucket] = []
         for dest in NOTIFICATION_DESTINATION_TYPES:
             config = notif_config.get('%sConfiguration' % (dest))
-            if config:
+            configs = config if isinstance(config, list) else [config] if config else []
+            for config in configs:
                 events = config.get('Event')
                 if isinstance(events, six.string_types):
                     events = [events]
@@ -482,8 +491,7 @@ def handle_notification_request(bucket, method, data):
                     dest: config.get(dest),
                     'Filter': event_filter
                 }
-                # TODO: what if we have multiple destinations - would we overwrite the config?
-                S3_NOTIFICATIONS[bucket] = clone(notification_details)
+                S3_NOTIFICATIONS[bucket].append(clone(notification_details))
     return response
 
 


Test patch:
diff --git a/tests/integration/test_notifications.py b/tests/integration/test_notifications.py
--- a/tests/integration/test_notifications.py
+++ b/tests/integration/test_notifications.py
@@ -1,4 +1,5 @@
 import json
+import unittest
 from io import BytesIO
 from localstack.utils import testutil
 from localstack.utils.aws import aws_stack
@@ -11,208 +12,216 @@
 TEST_QUEUE_NAME_FOR_SNS = 'test_queue_for_sns'
 
 
-def receive_assert_delete(queue_url, assertions, sqs_client=None, required_subject=None):
-    if not sqs_client:
+class TestNotifications(unittest.TestCase):
+
+    def test_sqs_queue_names(self):
         sqs_client = aws_stack.connect_to_service('sqs')
+        queue_name = '%s.fifo' % short_uid()
+        # make sure we can create *.fifo queues
+        queue_url = sqs_client.create_queue(QueueName=queue_name, Attributes={'FifoQueue': 'true'})['QueueUrl']
+        sqs_client.delete_queue(QueueUrl=queue_url)
 
-    response = sqs_client.receive_message(QueueUrl=queue_url)
-
-    messages = [json.loads(to_str(m['Body'])) for m in response['Messages']]
-    testutil.assert_objects(assertions, messages)
-    for message in response['Messages']:
-        sqs_client.delete_message(QueueUrl=queue_url, ReceiptHandle=message['ReceiptHandle'])
-
-
-def test_sqs_queue_names():
-    sqs_client = aws_stack.connect_to_service('sqs')
-    queue_name = '%s.fifo' % short_uid()
-    # make sure we can create *.fifo queues
-    queue_url = sqs_client.create_queue(QueueName=queue_name, Attributes={'FifoQueue': 'true'})['QueueUrl']
-    sqs_client.delete_queue(QueueUrl=queue_url)
-
-
-def test_sns_to_sqs():
-    sqs_client = aws_stack.connect_to_service('sqs')
-    sns_client = aws_stack.connect_to_service('sns')
-
-    # create topic and queue
-    queue_info = sqs_client.create_queue(QueueName=TEST_QUEUE_NAME_FOR_SNS)
-    topic_info = sns_client.create_topic(Name=TEST_TOPIC_NAME)
-
-    # subscribe SQS to SNS, publish message
-    sns_client.subscribe(TopicArn=topic_info['TopicArn'], Protocol='sqs',
-        Endpoint=aws_stack.sqs_queue_arn(TEST_QUEUE_NAME_FOR_SNS))
-    test_value = short_uid()
-    sns_client.publish(TopicArn=topic_info['TopicArn'], Message='test message for SQS',
-        MessageAttributes={'attr1': {'DataType': 'String', 'StringValue': test_value}})
-
-    # receive, assert, and delete message from SQS
-    queue_url = queue_info['QueueUrl']
-    assertions = []
-    # make sure we receive the correct topic ARN in notifications
-    assertions.append({'TopicArn': topic_info['TopicArn']})
-    # make sure the notification contains message attributes
-    assertions.append({'Value': test_value})
-    receive_assert_delete(queue_url, assertions, sqs_client)
-
-
-def _delete_notification_config():
-    s3_client = aws_stack.connect_to_service('s3')
-    s3_client.put_bucket_notification_configuration(
-        Bucket=TEST_BUCKET_NAME_WITH_NOTIFICATIONS, NotificationConfiguration={})
-    config = s3_client.get_bucket_notification_configuration(Bucket=TEST_BUCKET_NAME_WITH_NOTIFICATIONS)
-    assert not config.get('QueueConfigurations')
-    assert not config.get('TopicConfiguration')
-
-
-def test_bucket_notifications():
-
-    s3_resource = aws_stack.connect_to_resource('s3')
-    s3_client = aws_stack.connect_to_service('s3')
-    sqs_client = aws_stack.connect_to_service('sqs')
-
-    # create test bucket and queue
-    s3_resource.create_bucket(Bucket=TEST_BUCKET_NAME_WITH_NOTIFICATIONS)
-    queue_info = sqs_client.create_queue(QueueName=TEST_QUEUE_NAME_FOR_S3)
-
-    # create notification on bucket
-    queue_url = queue_info['QueueUrl']
-    queue_arn = aws_stack.sqs_queue_arn(TEST_QUEUE_NAME_FOR_S3)
-    events = ['s3:ObjectCreated:*', 's3:ObjectRemoved:Delete']
-    filter_rules = {
-        'FilterRules': [{
-            'Name': 'prefix',
-            'Value': 'testupload/'
-        }, {
-            'Name': 'suffix',
-            'Value': 'testfile.txt'
-        }]
-    }
-    s3_client.put_bucket_notification_configuration(
-        Bucket=TEST_BUCKET_NAME_WITH_NOTIFICATIONS,
-        NotificationConfiguration={
-            'QueueConfigurations': [{
-                'Id': 'id123456',
-                'QueueArn': queue_arn,
-                'Events': events,
-                'Filter': {
-                    'Key': filter_rules
-                }
-            }]
-        }
-    )
-
-    # retrieve and check notification config
-    config = s3_client.get_bucket_notification_configuration(Bucket=TEST_BUCKET_NAME_WITH_NOTIFICATIONS)
-    config = config['QueueConfigurations'][0]
-    assert events == config['Events']
-    assert filter_rules == config['Filter']['Key']
-
-    # upload file to S3 (this should NOT trigger a notification)
-    test_key1 = '/testdata'
-    test_data1 = b'{"test": "bucket_notification1"}'
-    s3_client.upload_fileobj(BytesIO(test_data1), TEST_BUCKET_NAME_WITH_NOTIFICATIONS, test_key1)
-
-    # upload file to S3 (this should trigger a notification)
-    test_key2 = 'testupload/dir1/testfile.txt'
-    test_data2 = b'{"test": "bucket_notification2"}'
-    s3_client.upload_fileobj(BytesIO(test_data2), TEST_BUCKET_NAME_WITH_NOTIFICATIONS, test_key2)
-
-    # receive, assert, and delete message from SQS
-    receive_assert_delete(queue_url, [{'key': test_key2}, {'name': TEST_BUCKET_NAME_WITH_NOTIFICATIONS}], sqs_client)
-
-    # delete notification config
-    _delete_notification_config()
-
-    # put notification config with single event type
-    event = 's3:ObjectCreated:*'
-    s3_client.put_bucket_notification_configuration(Bucket=TEST_BUCKET_NAME_WITH_NOTIFICATIONS,
-        NotificationConfiguration={
-            'QueueConfigurations': [{
-                'Id': 'id123456',
-                'QueueArn': queue_arn,
-                'Events': [event]
+    def test_sns_to_sqs(self):
+        sqs_client = aws_stack.connect_to_service('sqs')
+        sns_client = aws_stack.connect_to_service('sns')
+
+        # create topic and queue
+        queue_info = sqs_client.create_queue(QueueName=TEST_QUEUE_NAME_FOR_SNS)
+        topic_info = sns_client.create_topic(Name=TEST_TOPIC_NAME)
+
+        # subscribe SQS to SNS, publish message
+        sns_client.subscribe(TopicArn=topic_info['TopicArn'], Protocol='sqs',
+            Endpoint=aws_stack.sqs_queue_arn(TEST_QUEUE_NAME_FOR_SNS))
+        test_value = short_uid()
+        sns_client.publish(TopicArn=topic_info['TopicArn'], Message='test message for SQS',
+            MessageAttributes={'attr1': {'DataType': 'String', 'StringValue': test_value}})
+
+        # receive, assert, and delete message from SQS
+        queue_url = queue_info['QueueUrl']
+        assertions = []
+        # make sure we receive the correct topic ARN in notifications
+        assertions.append({'TopicArn': topic_info['TopicArn']})
+        # make sure the notification contains message attributes
+        assertions.append({'Value': test_value})
+        self._receive_assert_delete(queue_url, assertions, sqs_client)
+
+    def test_bucket_notifications(self):
+
+        s3_resource = aws_stack.connect_to_resource('s3')
+        s3_client = aws_stack.connect_to_service('s3')
+        sqs_client = aws_stack.connect_to_service('sqs')
+
+        # create test bucket and queue
+        s3_resource.create_bucket(Bucket=TEST_BUCKET_NAME_WITH_NOTIFICATIONS)
+        queue_info = sqs_client.create_queue(QueueName=TEST_QUEUE_NAME_FOR_S3)
+
+        # create notification on bucket
+        queue_url = queue_info['QueueUrl']
+        queue_arn = aws_stack.sqs_queue_arn(TEST_QUEUE_NAME_FOR_S3)
+        events = ['s3:ObjectCreated:*', 's3:ObjectRemoved:Delete']
+        filter_rules = {
+            'FilterRules': [{
+                'Name': 'prefix',
+                'Value': 'testupload/'
+            }, {
+                'Name': 'suffix',
+                'Value': 'testfile.txt'
             }]
         }
-    )
-    config = s3_client.get_bucket_notification_configuration(Bucket=TEST_BUCKET_NAME_WITH_NOTIFICATIONS)
-    config = config['QueueConfigurations'][0]
-    assert config['Events'] == [event]
-
-    # put notification config with single event type
-    event = 's3:ObjectCreated:*'
-    filter_rules = {
-        'FilterRules': [{
-            'Name': 'prefix',
-            'Value': 'testupload/'
-        }]
-    }
-    s3_client.put_bucket_notification_configuration(Bucket=TEST_BUCKET_NAME_WITH_NOTIFICATIONS,
-        NotificationConfiguration={
-            'QueueConfigurations': [{
-                'Id': 'id123456',
-                'QueueArn': queue_arn,
-                'Events': [event],
-                'Filter': {
-                    'Key': filter_rules
-                }
+        s3_client.put_bucket_notification_configuration(
+            Bucket=TEST_BUCKET_NAME_WITH_NOTIFICATIONS,
+            NotificationConfiguration={
+                'QueueConfigurations': [{
+                    'Id': 'id0001',
+                    'QueueArn': queue_arn,
+                    'Events': events,
+                    'Filter': {
+                        'Key': filter_rules
+                    }
+                }, {
+                    # Add second dummy config to fix https://github.com/localstack/localstack/issues/450
+                    'Id': 'id0002',
+                    'QueueArn': queue_arn,
+                    'Events': [],
+                    'Filter': {
+                        'Key': filter_rules
+                    }
+                }]
+            }
+        )
+
+        # retrieve and check notification config
+        config = s3_client.get_bucket_notification_configuration(Bucket=TEST_BUCKET_NAME_WITH_NOTIFICATIONS)
+        self.assertEqual(len(config['QueueConfigurations']), 2)
+        config = [c for c in config['QueueConfigurations'] if c['Events']][0]
+        self.assertEqual(events, config['Events'])
+        self.assertEqual(filter_rules, config['Filter']['Key'])
+
+        # upload file to S3 (this should NOT trigger a notification)
+        test_key1 = '/testdata'
+        test_data1 = b'{"test": "bucket_notification1"}'
+        s3_client.upload_fileobj(BytesIO(test_data1), TEST_BUCKET_NAME_WITH_NOTIFICATIONS, test_key1)
+
+        # upload file to S3 (this should trigger a notification)
+        test_key2 = 'testupload/dir1/testfile.txt'
+        test_data2 = b'{"test": "bucket_notification2"}'
+        s3_client.upload_fileobj(BytesIO(test_data2), TEST_BUCKET_NAME_WITH_NOTIFICATIONS, test_key2)
+
+        # receive, assert, and delete message from SQS
+        self._receive_assert_delete(queue_url,
+            [{'key': test_key2}, {'name': TEST_BUCKET_NAME_WITH_NOTIFICATIONS}], sqs_client)
+
+        # delete notification config
+        self._delete_notification_config()
+
+        # put notification config with single event type
+        event = 's3:ObjectCreated:*'
+        s3_client.put_bucket_notification_configuration(Bucket=TEST_BUCKET_NAME_WITH_NOTIFICATIONS,
+            NotificationConfiguration={
+                'QueueConfigurations': [{
+                    'Id': 'id123456',
+                    'QueueArn': queue_arn,
+                    'Events': [event]
+                }]
+            }
+        )
+        config = s3_client.get_bucket_notification_configuration(Bucket=TEST_BUCKET_NAME_WITH_NOTIFICATIONS)
+        config = config['QueueConfigurations'][0]
+        self.assertEqual(config['Events'], [event])
+
+        # put notification config with single event type
+        event = 's3:ObjectCreated:*'
+        filter_rules = {
+            'FilterRules': [{
+                'Name': 'prefix',
+                'Value': 'testupload/'
             }]
         }
-    )
-    config = s3_client.get_bucket_notification_configuration(Bucket=TEST_BUCKET_NAME_WITH_NOTIFICATIONS)
-    config = config['QueueConfigurations'][0]
-    assert config['Events'] == [event]
-    assert filter_rules == config['Filter']['Key']
-
-    # upload file to S3 (this should trigger a notification)
-    test_key2 = 'testupload/dir1/testfile.txt'
-    test_data2 = b'{"test": "bucket_notification2"}'
-    s3_client.upload_fileobj(BytesIO(test_data2), TEST_BUCKET_NAME_WITH_NOTIFICATIONS, test_key2)
-    # receive, assert, and delete message from SQS
-    receive_assert_delete(queue_url, [{'key': test_key2}, {'name': TEST_BUCKET_NAME_WITH_NOTIFICATIONS}], sqs_client)
-
-    # delete notification config
-    _delete_notification_config()
-
-    #
-    # Tests s3->sns->sqs notifications
-    #
-    sns_client = aws_stack.connect_to_service('sns')
-    topic_info = sns_client.create_topic(Name=TEST_S3_TOPIC_NAME)
-
-    s3_client.put_bucket_notification_configuration(
-        Bucket=TEST_BUCKET_NAME_WITH_NOTIFICATIONS,
-        NotificationConfiguration={
-            'TopicConfigurations': [
-                {
-                    'Id': 'id123',
-                    'Events': ['s3:ObjectCreated:*'],
-                    'TopicArn': topic_info['TopicArn']
-                }
-            ]
-        })
-
-    sns_client.subscribe(TopicArn=topic_info['TopicArn'], Protocol='sqs', Endpoint=queue_arn)
-
-    test_key2 = 'testupload/dir1/testfile.txt'
-    test_data2 = b'{"test": "bucket_notification2"}'
-
-    s3_client.upload_fileobj(BytesIO(test_data2), TEST_BUCKET_NAME_WITH_NOTIFICATIONS, test_key2)
-
-    # verify subject and records
-
-    response = sqs_client.receive_message(QueueUrl=queue_url)
-    for message in response['Messages']:
-        snsObj = json.loads(message['Body'])
-        testutil.assert_object({'Subject': 'Amazon S3 Notification'}, snsObj)
-        notificationObj = json.loads(snsObj['Message'])
-#        notificationRecs = [ json.loads( rec_text ) for rec_text in notification
-        testutil.assert_objects(
-            [
-                {'key': test_key2},
-                {'name': TEST_BUCKET_NAME_WITH_NOTIFICATIONS}
-            ], notificationObj['Records'])
-
-        sqs_client.delete_message(QueueUrl=queue_url, ReceiptHandle=message['ReceiptHandle'])
-
-    _delete_notification_config()
+        s3_client.put_bucket_notification_configuration(Bucket=TEST_BUCKET_NAME_WITH_NOTIFICATIONS,
+            NotificationConfiguration={
+                'QueueConfigurations': [{
+                    'Id': 'id123456',
+                    'QueueArn': queue_arn,
+                    'Events': [event],
+                    'Filter': {
+                        'Key': filter_rules
+                    }
+                }]
+            }
+        )
+        config = s3_client.get_bucket_notification_configuration(Bucket=TEST_BUCKET_NAME_WITH_NOTIFICATIONS)
+        config = config['QueueConfigurations'][0]
+        self.assertEqual(config['Events'], [event])
+        self.assertEqual(filter_rules, config['Filter']['Key'])
+
+        # upload file to S3 (this should trigger a notification)
+        test_key2 = 'testupload/dir1/testfile.txt'
+        test_data2 = b'{"test": "bucket_notification2"}'
+        s3_client.upload_fileobj(BytesIO(test_data2), TEST_BUCKET_NAME_WITH_NOTIFICATIONS, test_key2)
+        # receive, assert, and delete message from SQS
+        self._receive_assert_delete(queue_url,
+            [{'key': test_key2}, {'name': TEST_BUCKET_NAME_WITH_NOTIFICATIONS}], sqs_client)
+
+        # delete notification config
+        self._delete_notification_config()
+
+        #
+        # Tests s3->sns->sqs notifications
+        #
+        sns_client = aws_stack.connect_to_service('sns')
+        topic_info = sns_client.create_topic(Name=TEST_S3_TOPIC_NAME)
+
+        s3_client.put_bucket_notification_configuration(
+            Bucket=TEST_BUCKET_NAME_WITH_NOTIFICATIONS,
+            NotificationConfiguration={
+                'TopicConfigurations': [
+                    {
+                        'Id': 'id123',
+                        'Events': ['s3:ObjectCreated:*'],
+                        'TopicArn': topic_info['TopicArn']
+                    }
+                ]
+            })
+
+        sns_client.subscribe(TopicArn=topic_info['TopicArn'], Protocol='sqs', Endpoint=queue_arn)
+
+        test_key2 = 'testupload/dir1/testfile.txt'
+        test_data2 = b'{"test": "bucket_notification2"}'
+
+        s3_client.upload_fileobj(BytesIO(test_data2), TEST_BUCKET_NAME_WITH_NOTIFICATIONS, test_key2)
+
+        # verify subject and records
+
+        response = sqs_client.receive_message(QueueUrl=queue_url)
+        for message in response['Messages']:
+            snsObj = json.loads(message['Body'])
+            testutil.assert_object({'Subject': 'Amazon S3 Notification'}, snsObj)
+            notificationObj = json.loads(snsObj['Message'])
+            testutil.assert_objects(
+                [
+                    {'key': test_key2},
+                    {'name': TEST_BUCKET_NAME_WITH_NOTIFICATIONS}
+                ], notificationObj['Records'])
+
+            sqs_client.delete_message(QueueUrl=queue_url, ReceiptHandle=message['ReceiptHandle'])
+
+        self._delete_notification_config()
+
+    def _delete_notification_config(self):
+        s3_client = aws_stack.connect_to_service('s3')
+        s3_client.put_bucket_notification_configuration(
+            Bucket=TEST_BUCKET_NAME_WITH_NOTIFICATIONS, NotificationConfiguration={})
+        config = s3_client.get_bucket_notification_configuration(Bucket=TEST_BUCKET_NAME_WITH_NOTIFICATIONS)
+        self.assertFalse(config.get('QueueConfigurations'))
+        self.assertFalse(config.get('TopicConfiguration'))
+
+    def _receive_assert_delete(self, queue_url, assertions, sqs_client=None, required_subject=None):
+        if not sqs_client:
+            sqs_client = aws_stack.connect_to_service('sqs')
+
+        response = sqs_client.receive_message(QueueUrl=queue_url)
+
+        messages = [json.loads(to_str(m['Body'])) for m in response['Messages']]
+        testutil.assert_objects(assertions, messages)
+        for message in response['Messages']:
+            sqs_client.delete_message(QueueUrl=queue_url, ReceiptHandle=message['ReceiptHandle'])