From 440213f854c2f77ce6e382cc0dd3edb6631cdae1 Mon Sep 17 00:00:00 2001 From: usmankb Date: Fri, 1 May 2020 21:16:33 +0530 Subject: [PATCH 1/2] Enhancement Adding SES get_send_statistics,create_configuration_set functions --- moto/ses/exceptions.py | 13 +++++++ moto/ses/models.py | 36 ++++++++++++++++- moto/ses/responses.py | 66 ++++++++++++++++++++++++++++++++ tests/test_ses/test_ses.py | 36 +++++++++++++++++ tests/test_ses/test_ses_boto3.py | 52 +++++++++++++++++++++++++ 5 files changed, 202 insertions(+), 1 deletion(-) diff --git a/moto/ses/exceptions.py b/moto/ses/exceptions.py index a905039e2..f57eadf77 100644 --- a/moto/ses/exceptions.py +++ b/moto/ses/exceptions.py @@ -7,3 +7,16 @@ class MessageRejectedError(RESTError): def __init__(self, message): super(MessageRejectedError, self).__init__("MessageRejected", message) + +class ConfigurationSetDoesNotExist(RESTError): + code = 400 + + def __init__(self, message): + super(ConfigurationSetDoesNotExist, self).__init__("ConfigurationSetDoesNotExist", message) + + +class EventDestinationAlreadyExists(RESTError): + code = 400 + + def __init__(self, message): + super(EventDestinationAlreadyExists, self).__init__("EventDestinationAlreadyExists", message) diff --git a/moto/ses/models.py b/moto/ses/models.py index 91241f706..62068e5a9 100644 --- a/moto/ses/models.py +++ b/moto/ses/models.py @@ -1,11 +1,12 @@ from __future__ import unicode_literals +import datetime import email from email.utils import parseaddr from moto.core import BaseBackend, BaseModel from moto.sns.models import sns_backends -from .exceptions import MessageRejectedError +from .exceptions import MessageRejectedError,ConfigurationSetDoesNotExist,EventDestinationAlreadyExists from .utils import get_random_message_id from .feedback import COMMON_MAIL, BOUNCE, COMPLAINT, DELIVERY @@ -81,7 +82,11 @@ class SESBackend(BaseBackend): self.domains = [] self.sent_messages = [] self.sent_message_count = 0 + self.rejected_messages_count = 0 self.sns_topics = {} + self.config_set = {} + self.config_set_event_destination = {} + self.event_destinations = {} def _is_verified_address(self, source): _, address = parseaddr(source) @@ -118,6 +123,7 @@ class SESBackend(BaseBackend): if recipient_count > RECIPIENT_LIMIT: raise MessageRejectedError("Too many recipients.") if not self._is_verified_address(source): + self.rejected_messages_count+=1 raise MessageRejectedError("Email address not verified %s" % source) self.__process_sns_feedback__(source, destinations, region) @@ -135,6 +141,7 @@ class SESBackend(BaseBackend): if recipient_count > RECIPIENT_LIMIT: raise MessageRejectedError("Too many recipients.") if not self._is_verified_address(source): + self.rejected_messages_count += 1 raise MessageRejectedError("Email address not verified %s" % source) self.__process_sns_feedback__(source, destinations, region) @@ -237,5 +244,32 @@ class SESBackend(BaseBackend): return {} + def create_configuration_set(self, configuration_set_name): + self.config_set[configuration_set_name] = 1 + return {} + + def create_configuration_set_event_destination(self,configuration_set_name, event_destination): + + if self.config_set.get(configuration_set_name) is None: + raise ConfigurationSetDoesNotExist("Invalid Configuration Set Name.") + + if self.event_destinations.get(event_destination["Name"]): + raise EventDestinationAlreadyExists("Duplicate Event destination Name.") + + self.config_set_event_destination[configuration_set_name] = event_destination + self.event_destinations[event_destination["Name"]] = 1 + + return {} + + def get_send_statistics(self): + + statistics = {} + statistics["DeliveryAttempts"] = self.sent_message_count + statistics["Rejects"] = self.rejected_messages_count + statistics["Complaints"] = 0 + statistics["Bounces"] = 0 + statistics["Timestamp"] = datetime.datetime.utcnow() + return statistics + ses_backend = SESBackend() diff --git a/moto/ses/responses.py b/moto/ses/responses.py index 1034aeb0d..8bf7bd942 100644 --- a/moto/ses/responses.py +++ b/moto/ses/responses.py @@ -133,6 +133,40 @@ class EmailResponse(BaseResponse): template = self.response_template(SET_IDENTITY_NOTIFICATION_TOPIC_RESPONSE) return template.render() + def get_send_statistics(self): + statistics = ses_backend.get_send_statistics() + template = self.response_template(GET_SEND_STATISTICS) + return template.render(all_statistics=[statistics]) + + def create_configuration_set(self): + configuration_set_name = self.querystring.get("ConfigurationSet.Name")[0] + ses_backend.create_configuration_set(configuration_set_name=configuration_set_name) + template = self.response_template(CREATE_CONFIGURATION_SET) + return template.render() + + def create_configuration_set_event_destination(self): + + configuration_set_name = self._get_param('ConfigurationSetName') + is_configuration_event_enabled = self.querystring.get("EventDestination.Enabled")[0] + configuration_event_name = self.querystring.get("EventDestination.Name")[0] + event_topic_arn = self.querystring.get("EventDestination.SNSDestination.TopicARN")[0] + event_matching_types = self._get_multi_param("EventDestination.MatchingEventTypes.member") + + event_destination = {"Name":configuration_event_name, + "Enabled":is_configuration_event_enabled, + "EventMatchingTypes":event_matching_types, + "SNSDestination":event_topic_arn + } + + ses_backend.create_configuration_set_event_destination( + configuration_set_name=configuration_set_name, + event_destination=event_destination + ) + + template = self.response_template(CREATE_CONFIGURATION_SET_EVENT_DESTINATION) + return template.render() + + VERIFY_EMAIL_IDENTITY = """ @@ -248,3 +282,35 @@ SET_IDENTITY_NOTIFICATION_TOPIC_RESPONSE = """47e0ef1a-9bf2-11e1-9279-0100e8cf109a """ + +GET_SEND_STATISTICS = """ + + {% for statistics in all_statistics %} + + {{ statistics["DeliveryAttempts"] }} + {{ statistics["Rejects"] }} + {{ statistics["Bounces"] }} + {{ statistics["Complaints"] }} + {{ statistics["Timestamp"] }} + + {% endfor %} + + + e0abcdfa-c866-11e0-b6d0-273d09173z49 + +""" + +CREATE_CONFIGURATION_SET = """ + + + 47e0ef1a-9bf2-11e1-9279-0100e8cf109a + +""" + + +CREATE_CONFIGURATION_SET_EVENT_DESTINATION = """ + + + 67e0ef1a-9bf2-11e1-9279-0100e8cf109a + +""" diff --git a/tests/test_ses/test_ses.py b/tests/test_ses/test_ses.py index 851327b9d..637931572 100644 --- a/tests/test_ses/test_ses.py +++ b/tests/test_ses/test_ses.py @@ -127,3 +127,39 @@ def test_send_raw_email(): send_quota["GetSendQuotaResponse"]["GetSendQuotaResult"]["SentLast24Hours"] ) sent_count.should.equal(1) + + +@mock_ses_deprecated +def test_get_send_statistics(): + conn = boto.connect_ses("the_key", "the_secret") + + conn.send_email.when.called_with( + "test@example.com", + "test subject", + "test body", + "test_to@example.com", + format="html", + ).should.throw(BotoServerError) + + # tests to verify rejects in get_send_statistics + result = conn.get_send_statistics() + + reject_count = int(result["GetSendStatisticsResponse"]["SendDataPoints"][0]["Rejects"]) + delivery_count = int(result["GetSendStatisticsResponse"]["SendDataPoints"][0]["DeliveryAttempts"]) + reject_count.should.equal(1) + delivery_count.should.equal(0) + + conn.verify_email_identity("test@example.com") + conn.send_email( + "test@example.com", "test subject", "test body", "test_to@example.com" + ) + + # tests to delivery attempts in get_send_statistics + result = conn.get_send_statistics() + + reject_count = int(result["GetSendStatisticsResponse"]["SendDataPoints"][0]["Rejects"]) + delivery_count = int(result["GetSendStatisticsResponse"]["SendDataPoints"][0]["DeliveryAttempts"]) + reject_count.should.equal(1) + delivery_count.should.equal(1) + + diff --git a/tests/test_ses/test_ses_boto3.py b/tests/test_ses/test_ses_boto3.py index de8aa0813..e14abda3f 100644 --- a/tests/test_ses/test_ses_boto3.py +++ b/tests/test_ses/test_ses_boto3.py @@ -4,6 +4,8 @@ import boto3 from botocore.exceptions import ClientError from six.moves.email_mime_multipart import MIMEMultipart from six.moves.email_mime_text import MIMEText +from nose.tools import assert_raises + import sure # noqa @@ -227,3 +229,53 @@ def test_send_email_notification_with_encoded_sender(): Message={"Subject": {"Data": "hi",}, "Body": {"Text": {"Data": "there",}}}, ) response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + +@mock_ses +def test_create_configuration_set(): + conn = boto3.client("ses", region_name="us-east-1") + conn.create_configuration_set(ConfigurationSet=dict({"Name": "test"})) + + conn.create_configuration_set_event_destination( + ConfigurationSetName='test', + EventDestination={ + 'Name': 'snsEvent', + 'Enabled': True, + 'MatchingEventTypes': [ + 'send', + ], + 'SNSDestination': { + 'TopicARN': 'arn:aws:sns:us-east-1:123456789012:myTopic' + } + }) + + with assert_raises(ClientError) as ex: + conn.create_configuration_set_event_destination( + ConfigurationSetName='failtest', + EventDestination={ + 'Name': 'snsEvent', + 'Enabled': True, + 'MatchingEventTypes': [ + 'send', + ], + 'SNSDestination': { + 'TopicARN': 'arn:aws:sns:us-east-1:123456789012:myTopic' + } + }) + + ex.exception.response["Error"]["Code"].should.equal("ConfigurationSetDoesNotExist") + + with assert_raises(ClientError) as ex: + conn.create_configuration_set_event_destination( + ConfigurationSetName='test', + EventDestination={ + 'Name': 'snsEvent', + 'Enabled': True, + 'MatchingEventTypes': [ + 'send', + ], + 'SNSDestination': { + 'TopicARN': 'arn:aws:sns:us-east-1:123456789012:myTopic' + } + }) + + ex.exception.response["Error"]["Code"].should.equal("EventDestinationAlreadyExists") \ No newline at end of file From 353bc08ac2f4a4af82987f1fa82ef28d8d4b4584 Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Mon, 4 May 2020 09:24:46 +0100 Subject: [PATCH 2/2] Linting --- moto/ses/exceptions.py | 9 +++-- moto/ses/models.py | 12 +++++-- moto/ses/responses.py | 36 +++++++++++-------- tests/test_ses/test_ses.py | 18 ++++++---- tests/test_ses/test_ses_boto3.py | 62 ++++++++++++++++---------------- 5 files changed, 80 insertions(+), 57 deletions(-) diff --git a/moto/ses/exceptions.py b/moto/ses/exceptions.py index f57eadf77..c15473188 100644 --- a/moto/ses/exceptions.py +++ b/moto/ses/exceptions.py @@ -8,15 +8,20 @@ class MessageRejectedError(RESTError): def __init__(self, message): super(MessageRejectedError, self).__init__("MessageRejected", message) + class ConfigurationSetDoesNotExist(RESTError): code = 400 def __init__(self, message): - super(ConfigurationSetDoesNotExist, self).__init__("ConfigurationSetDoesNotExist", message) + super(ConfigurationSetDoesNotExist, self).__init__( + "ConfigurationSetDoesNotExist", message + ) class EventDestinationAlreadyExists(RESTError): code = 400 def __init__(self, message): - super(EventDestinationAlreadyExists, self).__init__("EventDestinationAlreadyExists", message) + super(EventDestinationAlreadyExists, self).__init__( + "EventDestinationAlreadyExists", message + ) diff --git a/moto/ses/models.py b/moto/ses/models.py index 62068e5a9..d141e25ae 100644 --- a/moto/ses/models.py +++ b/moto/ses/models.py @@ -6,7 +6,11 @@ from email.utils import parseaddr from moto.core import BaseBackend, BaseModel from moto.sns.models import sns_backends -from .exceptions import MessageRejectedError,ConfigurationSetDoesNotExist,EventDestinationAlreadyExists +from .exceptions import ( + MessageRejectedError, + ConfigurationSetDoesNotExist, + EventDestinationAlreadyExists, +) from .utils import get_random_message_id from .feedback import COMMON_MAIL, BOUNCE, COMPLAINT, DELIVERY @@ -123,7 +127,7 @@ class SESBackend(BaseBackend): if recipient_count > RECIPIENT_LIMIT: raise MessageRejectedError("Too many recipients.") if not self._is_verified_address(source): - self.rejected_messages_count+=1 + self.rejected_messages_count += 1 raise MessageRejectedError("Email address not verified %s" % source) self.__process_sns_feedback__(source, destinations, region) @@ -248,7 +252,9 @@ class SESBackend(BaseBackend): self.config_set[configuration_set_name] = 1 return {} - def create_configuration_set_event_destination(self,configuration_set_name, event_destination): + def create_configuration_set_event_destination( + self, configuration_set_name, event_destination + ): if self.config_set.get(configuration_set_name) is None: raise ConfigurationSetDoesNotExist("Invalid Configuration Set Name.") diff --git a/moto/ses/responses.py b/moto/ses/responses.py index 8bf7bd942..62893094a 100644 --- a/moto/ses/responses.py +++ b/moto/ses/responses.py @@ -140,34 +140,42 @@ class EmailResponse(BaseResponse): def create_configuration_set(self): configuration_set_name = self.querystring.get("ConfigurationSet.Name")[0] - ses_backend.create_configuration_set(configuration_set_name=configuration_set_name) + ses_backend.create_configuration_set( + configuration_set_name=configuration_set_name + ) template = self.response_template(CREATE_CONFIGURATION_SET) return template.render() def create_configuration_set_event_destination(self): - configuration_set_name = self._get_param('ConfigurationSetName') - is_configuration_event_enabled = self.querystring.get("EventDestination.Enabled")[0] + configuration_set_name = self._get_param("ConfigurationSetName") + is_configuration_event_enabled = self.querystring.get( + "EventDestination.Enabled" + )[0] configuration_event_name = self.querystring.get("EventDestination.Name")[0] - event_topic_arn = self.querystring.get("EventDestination.SNSDestination.TopicARN")[0] - event_matching_types = self._get_multi_param("EventDestination.MatchingEventTypes.member") + event_topic_arn = self.querystring.get( + "EventDestination.SNSDestination.TopicARN" + )[0] + event_matching_types = self._get_multi_param( + "EventDestination.MatchingEventTypes.member" + ) - event_destination = {"Name":configuration_event_name, - "Enabled":is_configuration_event_enabled, - "EventMatchingTypes":event_matching_types, - "SNSDestination":event_topic_arn - } + event_destination = { + "Name": configuration_event_name, + "Enabled": is_configuration_event_enabled, + "EventMatchingTypes": event_matching_types, + "SNSDestination": event_topic_arn, + } ses_backend.create_configuration_set_event_destination( - configuration_set_name=configuration_set_name, - event_destination=event_destination - ) + configuration_set_name=configuration_set_name, + event_destination=event_destination, + ) template = self.response_template(CREATE_CONFIGURATION_SET_EVENT_DESTINATION) return template.render() - VERIFY_EMAIL_IDENTITY = """ diff --git a/tests/test_ses/test_ses.py b/tests/test_ses/test_ses.py index 637931572..719e4ede9 100644 --- a/tests/test_ses/test_ses.py +++ b/tests/test_ses/test_ses.py @@ -144,8 +144,12 @@ def test_get_send_statistics(): # tests to verify rejects in get_send_statistics result = conn.get_send_statistics() - reject_count = int(result["GetSendStatisticsResponse"]["SendDataPoints"][0]["Rejects"]) - delivery_count = int(result["GetSendStatisticsResponse"]["SendDataPoints"][0]["DeliveryAttempts"]) + reject_count = int( + result["GetSendStatisticsResponse"]["SendDataPoints"][0]["Rejects"] + ) + delivery_count = int( + result["GetSendStatisticsResponse"]["SendDataPoints"][0]["DeliveryAttempts"] + ) reject_count.should.equal(1) delivery_count.should.equal(0) @@ -157,9 +161,11 @@ def test_get_send_statistics(): # tests to delivery attempts in get_send_statistics result = conn.get_send_statistics() - reject_count = int(result["GetSendStatisticsResponse"]["SendDataPoints"][0]["Rejects"]) - delivery_count = int(result["GetSendStatisticsResponse"]["SendDataPoints"][0]["DeliveryAttempts"]) + reject_count = int( + result["GetSendStatisticsResponse"]["SendDataPoints"][0]["Rejects"] + ) + delivery_count = int( + result["GetSendStatisticsResponse"]["SendDataPoints"][0]["DeliveryAttempts"] + ) reject_count.should.equal(1) delivery_count.should.equal(1) - - diff --git a/tests/test_ses/test_ses_boto3.py b/tests/test_ses/test_ses_boto3.py index e14abda3f..0e6bb9bea 100644 --- a/tests/test_ses/test_ses_boto3.py +++ b/tests/test_ses/test_ses_boto3.py @@ -230,52 +230,50 @@ def test_send_email_notification_with_encoded_sender(): ) response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + @mock_ses def test_create_configuration_set(): conn = boto3.client("ses", region_name="us-east-1") conn.create_configuration_set(ConfigurationSet=dict({"Name": "test"})) conn.create_configuration_set_event_destination( - ConfigurationSetName='test', - EventDestination={ - 'Name': 'snsEvent', - 'Enabled': True, - 'MatchingEventTypes': [ - 'send', - ], - 'SNSDestination': { - 'TopicARN': 'arn:aws:sns:us-east-1:123456789012:myTopic' - } - }) + ConfigurationSetName="test", + EventDestination={ + "Name": "snsEvent", + "Enabled": True, + "MatchingEventTypes": ["send",], + "SNSDestination": { + "TopicARN": "arn:aws:sns:us-east-1:123456789012:myTopic" + }, + }, + ) with assert_raises(ClientError) as ex: conn.create_configuration_set_event_destination( - ConfigurationSetName='failtest', + ConfigurationSetName="failtest", EventDestination={ - 'Name': 'snsEvent', - 'Enabled': True, - 'MatchingEventTypes': [ - 'send', - ], - 'SNSDestination': { - 'TopicARN': 'arn:aws:sns:us-east-1:123456789012:myTopic' - } - }) + "Name": "snsEvent", + "Enabled": True, + "MatchingEventTypes": ["send",], + "SNSDestination": { + "TopicARN": "arn:aws:sns:us-east-1:123456789012:myTopic" + }, + }, + ) ex.exception.response["Error"]["Code"].should.equal("ConfigurationSetDoesNotExist") with assert_raises(ClientError) as ex: conn.create_configuration_set_event_destination( - ConfigurationSetName='test', + ConfigurationSetName="test", EventDestination={ - 'Name': 'snsEvent', - 'Enabled': True, - 'MatchingEventTypes': [ - 'send', - ], - 'SNSDestination': { - 'TopicARN': 'arn:aws:sns:us-east-1:123456789012:myTopic' - } - }) + "Name": "snsEvent", + "Enabled": True, + "MatchingEventTypes": ["send",], + "SNSDestination": { + "TopicARN": "arn:aws:sns:us-east-1:123456789012:myTopic" + }, + }, + ) - ex.exception.response["Error"]["Code"].should.equal("EventDestinationAlreadyExists") \ No newline at end of file + ex.exception.response["Error"]["Code"].should.equal("EventDestinationAlreadyExists")