def test_signal_sent(self, mock): """ Test that the subscription signal was sent Based on http://stackoverflow.com/questions/3817213/ """ # pylint: disable=attribute-defined-outside-init, unused-variable responsemock = Mock() responsemock.read.return_value = 'Return Value' mock.return_value = responsemock notification = loader('subscriptionconfirmation') self.signal_count = 0 @receiver(signals.subscription) def _signal_receiver(sender, **kwargs): """Signal Test Receiver""" self.signal_count += 1 self.signal_sender = sender self.signal_notification = kwargs['notification'] self.signal_result = kwargs['result'] response = utils.approve_subscription(notification) self.assertEqual(response.content.decode('ascii'), 'Return Value') self.assertEqual(self.signal_count, 1) self.assertEqual(self.signal_result, 'Return Value') self.assertEqual(self.signal_notification, notification)
def test_approve_subscription(self, mock): """Test the subscription approval mechanism""" responsemock = Mock() responsemock.read.return_value = 'Return Value' mock.return_value = responsemock notification = loader('subscriptionconfirmation') response = utils.approve_subscription(notification) mock.assert_called_with(notification['SubscribeURL']) self.assertEqual(response.status_code, 200) self.assertEqual(response.content.decode('ascii'), 'Return Value')
def test_bad_url(self): """Test to make sure an invalid URL isn't requested by our system""" old_setting = getattr(settings, 'BOUNCY_SUBSCRIBE_DOMAIN_REGEX', None) settings.BOUNCY_SUBSCRIBE_DOMAIN_REGEX = \ r"sns.[a-z0-9\-]+.amazonaws.com$" notification = loader('bounce_notification') notification['SubscribeURL'] = 'http://bucket.s3.amazonaws.com' result = utils.approve_subscription(notification) self.assertEqual(result.status_code, 400) self.assertEqual( result.content.decode('ascii'), 'Improper Subscription Domain') if old_setting is not None: settings.BOUNCY_SUBSCRIBE_DOMAIN_REGEX = old_setting
def test_bad_url(self): """Test to make sure an invalid URL isn't requested by our system""" old_setting = getattr(settings, 'BOUNCY_SUBSCRIBE_DOMAIN_REGEX', None) settings.BOUNCY_SUBSCRIBE_DOMAIN_REGEX = \ r"sns.[a-z0-9\-]+.amazonaws.com$" notification = loader('bounce_notification') notification['SubscribeURL'] = 'http://bucket.s3.amazonaws.com' result = utils.approve_subscription(notification) self.assertEqual(result.status_code, 400) self.assertEqual(result.content.decode('ascii'), 'Improper Subscription Domain') if old_setting is not None: settings.BOUNCY_SUBSCRIBE_DOMAIN_REGEX = old_setting
def endpoint(request): """Endpoint that SNS accesses. Includes logic verifying request""" # pylint: disable=too-many-return-statements,too-many-branches # In order to 'hide' the endpoint, all non-POST requests should return # the site's default HTTP404 if request.method != 'POST': raise Http404 # If necessary, check that the topic is correct if hasattr(settings, 'BOUNCY_TOPIC_ARN'): # Confirm that the proper topic header was sent if 'HTTP_X_AMZ_SNS_TOPIC_ARN' not in request.META: return HttpResponseBadRequest('No TopicArn Header') # Check to see if the topic is in the settings # Because you can have bounces and complaints coming from multiple # topics, BOUNCY_TOPIC_ARN is a list if (not request.META['HTTP_X_AMZ_SNS_TOPIC_ARN'] in settings.BOUNCY_TOPIC_ARN): return HttpResponseBadRequest('Bad Topic') # Load the JSON POST Body if isinstance(request.body, str): # requests return str in python 2.7 request_body = request.body else: # and return bytes in python 3.4 request_body = request.body.decode() try: data = json.loads(request_body) except ValueError: logger.warning('Notification Not Valid JSON: {}'.format(request_body)) return HttpResponseBadRequest('Not Valid JSON') # Ensure that the JSON we're provided contains all the keys we expect # Comparison code from http://stackoverflow.com/questions/1285911/ if not set(VITAL_NOTIFICATION_FIELDS) <= set(data): logger.warning('Request Missing Necessary Keys') return HttpResponseBadRequest('Request Missing Necessary Keys') # Ensure that the type of notification is one we'll accept if not data['Type'] in ALLOWED_TYPES: logger.info('Notification Type Not Known %s', data['Type']) return HttpResponseBadRequest('Unknown Notification Type') # Confirm that the signing certificate is hosted on a correct domain # AWS by default uses sns.{region}.amazonaws.com # On the off chance you need this to be a different domain, allow the # regex to be overridden in settings domain = urlparse(data['SigningCertURL']).netloc pattern = getattr(settings, 'BOUNCY_CERT_DOMAIN_REGEX', r"sns.[a-z0-9\-]+.amazonaws.com$") if not re.search(pattern, domain): logger.warning('Improper Certificate Location %s', data['SigningCertURL']) return HttpResponseBadRequest('Improper Certificate Location') # Verify that the notification is signed by Amazon if (getattr(settings, 'BOUNCY_VERIFY_CERTIFICATE', True) and not verify_notification(data)): logger.error('Verification Failure %s', ) return HttpResponseBadRequest('Improper Signature') # Send a signal to say a valid notification has been received signals.notification.send(sender='bouncy_endpoint', notification=data, request=request) # Handle subscription-based messages. if data['Type'] == 'SubscriptionConfirmation': # Allow the disabling of the auto-subscription feature if not getattr(settings, 'BOUNCY_AUTO_SUBSCRIBE', True): raise Http404 return approve_subscription(data) elif data['Type'] == 'UnsubscribeConfirmation': # We won't handle unsubscribe requests here. Return a 200 status code # so Amazon won't redeliver the request. If you want to remove this # endpoint, remove it either via the API or the AWS Console logger.info('UnsubscribeConfirmation Not Handled') return HttpResponse('UnsubscribeConfirmation Not Handled') try: message = json.loads(data['Message']) except ValueError: # This message is not JSON. But we need to return a 200 status code # so that Amazon doesn't attempt to deliver the message again logger.info('Non-Valid JSON Message Received') return HttpResponse('Message is not valid JSON') return process_message(message, data)
def endpoint(request): """Endpoint that SNS accesses. Includes logic verifying request""" # pylint: disable=too-many-return-statements,too-many-branches # In order to 'hide' the endpoint, all non-POST requests should return # the site's default HTTP404 if request.method != 'POST': raise Http404 # If necessary, check that the topic is correct if hasattr(settings, 'BOUNCY_TOPIC_ARN'): # Confirm that the proper topic header was sent if 'HTTP_X_AMZ_SNS_TOPIC_ARN' not in request.META: return HttpResponseBadRequest('No TopicArn Header') # Check to see if the topic is in the settings # Because you can have bounces and complaints coming from multiple # topics, BOUNCY_TOPIC_ARN is a list if (not request.META['HTTP_X_AMZ_SNS_TOPIC_ARN'] in settings.BOUNCY_TOPIC_ARN): return HttpResponseBadRequest('Bad Topic') # Load the JSON POST Body if isinstance(request.body, str): # requests return str in python 2.7 request_body = request.body else: # and return bytes in python 3.4 request_body = request.body.decode() try: data = json.loads(request_body) except ValueError: logger.warning('Notification Not Valid JSON: {}'.format(request_body)) return HttpResponseBadRequest('Not Valid JSON') # Ensure that the JSON we're provided contains all the keys we expect # Comparison code from http://stackoverflow.com/questions/1285911/ if not set(VITAL_NOTIFICATION_FIELDS) <= set(data): logger.warning('Request Missing Necessary Keys') return HttpResponseBadRequest('Request Missing Necessary Keys') # Ensure that the type of notification is one we'll accept if not data['Type'] in ALLOWED_TYPES: logger.info('Notification Type Not Known %s', data['Type']) return HttpResponseBadRequest('Unknown Notification Type') # Confirm that the signing certificate is hosted on a correct domain # AWS by default uses sns.{region}.amazonaws.com # On the off chance you need this to be a different domain, allow the # regex to be overridden in settings domain = urlparse(data['SigningCertURL']).netloc pattern = getattr( settings, 'BOUNCY_CERT_DOMAIN_REGEX', r"sns.[a-z0-9\-]+.amazonaws.com$" ) if not re.search(pattern, domain): logger.warning( 'Improper Certificate Location %s', data['SigningCertURL']) return HttpResponseBadRequest('Improper Certificate Location') # Verify that the notification is signed by Amazon if (getattr(settings, 'BOUNCY_VERIFY_CERTIFICATE', True) and not verify_notification(data)): logger.error('Verification Failure %s', ) return HttpResponseBadRequest('Improper Signature') # Send a signal to say a valid notification has been received signals.notification.send( sender='bouncy_endpoint', notification=data, request=request) # Handle subscription-based messages. if data['Type'] == 'SubscriptionConfirmation': # Allow the disabling of the auto-subscription feature if not getattr(settings, 'BOUNCY_AUTO_SUBSCRIBE', True): raise Http404 return approve_subscription(data) elif data['Type'] == 'UnsubscribeConfirmation': # We won't handle unsubscribe requests here. Return a 200 status code # so Amazon won't redeliver the request. If you want to remove this # endpoint, remove it either via the API or the AWS Console logger.info('UnsubscribeConfirmation Not Handled') return HttpResponse('UnsubscribeConfirmation Not Handled') try: message = json.loads(data['Message']) except ValueError: # This message is not JSON. But we need to return a 200 status code # so that Amazon doesn't attempt to deliver the message again logger.info('Non-Valid JSON Message Received') return HttpResponse('Message is not valid JSON') return process_message(message, data)