From 8a551a975451c38e9002cd445d8a0b44f5f955ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Ma=C5=84kowski?= Date: Tue, 25 Aug 2020 14:05:49 +0200 Subject: [PATCH] [SNS] Mock sending directly SMS (#3253) * [SNS] Mock sending directly SMS Proper behaviour when publishing to PhoneNumber is sending message directly to this number, without any topic or previous confirmation. https://docs.aws.amazon.com/sns/latest/dg/sns-mobile-phone-number-as-subscriber.html * Fix arguments order * Omit checking local backend when tests in server mode --- moto/ses/models.py | 2 +- moto/sns/models.py | 26 +++++++++++++----- moto/sns/responses.py | 21 +++++---------- tests/test_sns/test_publishing_boto3.py | 36 +++++++++++-------------- 4 files changed, 43 insertions(+), 42 deletions(-) diff --git a/moto/ses/models.py b/moto/ses/models.py index e90f66fa8..a817444bd 100644 --- a/moto/ses/models.py +++ b/moto/ses/models.py @@ -202,7 +202,7 @@ class SESBackend(BaseBackend): if sns_topic is not None: message = self.__generate_feedback__(msg_type) if message: - sns_backends[region].publish(sns_topic, message) + sns_backends[region].publish(message, arn=sns_topic) def send_raw_email(self, source, destinations, raw_data, region): if source is not None: diff --git a/moto/sns/models.py b/moto/sns/models.py index 8a4771a37..779a0fb06 100644 --- a/moto/sns/models.py +++ b/moto/sns/models.py @@ -35,6 +35,7 @@ from moto.core import ACCOUNT_ID as DEFAULT_ACCOUNT_ID DEFAULT_PAGE_SIZE = 100 MAXIMUM_MESSAGE_LENGTH = 262144 # 256 KiB +MAXIMUM_SMS_MESSAGE_BYTES = 1600 # Amazon limit for a single publish SMS action class Topic(CloudFormationModel): @@ -365,6 +366,7 @@ class SNSBackend(BaseBackend): self.platform_endpoints = {} self.region_name = region_name self.sms_attributes = {} + self.sms_messages = OrderedDict() self.opt_out_numbers = [ "+447420500600", "+447420505401", @@ -432,12 +434,6 @@ class SNSBackend(BaseBackend): except KeyError: raise SNSNotFoundError("Topic with arn {0} not found".format(arn)) - def get_topic_from_phone_number(self, number): - for subscription in self.subscriptions.values(): - if subscription.protocol == "sms" and subscription.endpoint == number: - return subscription.topic.arn - raise SNSNotFoundError("Could not find valid subscription") - def set_topic_attribute(self, topic_arn, attribute_name, attribute_value): topic = self.get_topic(topic_arn) setattr(topic, attribute_name, attribute_value) @@ -501,11 +497,27 @@ class SNSBackend(BaseBackend): else: return self._get_values_nexttoken(self.subscriptions, next_token) - def publish(self, arn, message, subject=None, message_attributes=None): + def publish( + self, + message, + arn=None, + phone_number=None, + subject=None, + message_attributes=None, + ): if subject is not None and len(subject) > 100: # Note that the AWS docs around length are wrong: https://github.com/spulec/moto/issues/1503 raise ValueError("Subject must be less than 100 characters") + if phone_number: + # This is only an approximation. In fact, we should try to use GSM-7 or UCS-2 encoding to count used bytes + if len(message) > MAXIMUM_SMS_MESSAGE_BYTES: + raise ValueError("SMS message must be less than 1600 bytes") + + message_id = six.text_type(uuid.uuid4()) + self.sms_messages[message_id] = (phone_number, message) + return message_id + if len(message) > MAXIMUM_MESSAGE_LENGTH: raise InvalidParameterValue( "An error occurred (InvalidParameter) when calling the Publish operation: Invalid parameter: Message too long" diff --git a/moto/sns/responses.py b/moto/sns/responses.py index c2eb3e7c3..7fdc37ab6 100644 --- a/moto/sns/responses.py +++ b/moto/sns/responses.py @@ -6,7 +6,7 @@ from collections import defaultdict from moto.core.responses import BaseResponse from moto.core.utils import camelcase_to_underscores from .models import sns_backends -from .exceptions import SNSNotFoundError, InvalidParameterValue +from .exceptions import InvalidParameterValue from .utils import is_e164 @@ -327,6 +327,7 @@ class SNSResponse(BaseResponse): message_attributes = self._parse_message_attributes() + arn = None if phone_number is not None: # Check phone is correct syntax (e164) if not is_e164(phone_number): @@ -336,18 +337,6 @@ class SNSResponse(BaseResponse): ), dict(status=400), ) - - # Look up topic arn by phone number - try: - arn = self.backend.get_topic_from_phone_number(phone_number) - except SNSNotFoundError: - return ( - self._error( - "ParameterValueInvalid", - "Could not find topic associated with phone number", - ), - dict(status=400), - ) elif target_arn is not None: arn = target_arn else: @@ -357,7 +346,11 @@ class SNSResponse(BaseResponse): try: message_id = self.backend.publish( - arn, message, subject=subject, message_attributes=message_attributes + message, + arn=arn, + phone_number=phone_number, + subject=subject, + message_attributes=message_attributes, ) except ValueError as err: error_response = self._error("InvalidParameter", str(err)) diff --git a/tests/test_sns/test_publishing_boto3.py b/tests/test_sns/test_publishing_boto3.py index fddd9125c..99e7ae7a4 100644 --- a/tests/test_sns/test_publishing_boto3.py +++ b/tests/test_sns/test_publishing_boto3.py @@ -11,8 +11,9 @@ import sure # noqa import responses from botocore.exceptions import ClientError from nose.tools import assert_raises -from moto import mock_sns, mock_sqs +from moto import mock_sns, mock_sqs, settings from moto.core import ACCOUNT_ID +from moto.sns import sns_backend MESSAGE_FROM_SQS_TEMPLATE = ( '{\n "Message": "%s",\n "MessageId": "%s",\n "Signature": "EXAMPLElDMXvB8r9R83tGoNn0ecwd5UjllzsvSvbItzfaMpN2nk5HVSw7XnOn/49IkxDKz8YrlH2qJXj2iZB0Zo2O71c4qQk1fMUDi3LGpij7RCW7AW9vYYsSqIKRnFS94ilu7NFhUzLiieYr4BKHpdTmdD6c0esKEYBpabxDSc=",\n "SignatureVersion": "1",\n "SigningCertURL": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-f3ecfb7224c7233fe7bb5f59f96de52f.pem",\n "Subject": "my subject",\n "Timestamp": "2015-01-01T12:00:00.000Z",\n "TopicArn": "arn:aws:sns:%s:' @@ -223,36 +224,31 @@ def test_publish_to_sqs_msg_attr_number_type(): @mock_sns def test_publish_sms(): client = boto3.client("sns", region_name="us-east-1") - client.create_topic(Name="some-topic") - resp = client.create_topic(Name="some-topic") - arn = resp["TopicArn"] - - client.subscribe(TopicArn=arn, Protocol="sms", Endpoint="+15551234567") result = client.publish(PhoneNumber="+15551234567", Message="my message") + result.should.contain("MessageId") + if not settings.TEST_SERVER_MODE: + sns_backend.sms_messages.should.have.key(result["MessageId"]).being.equal( + ("+15551234567", "my message") + ) @mock_sns def test_publish_bad_sms(): client = boto3.client("sns", region_name="us-east-1") - client.create_topic(Name="some-topic") - resp = client.create_topic(Name="some-topic") - arn = resp["TopicArn"] - client.subscribe(TopicArn=arn, Protocol="sms", Endpoint="+15551234567") - - try: - # Test invalid number + # Test invalid number + with assert_raises(ClientError) as cm: client.publish(PhoneNumber="NAA+15551234567", Message="my message") - except ClientError as err: - err.response["Error"]["Code"].should.equal("InvalidParameter") + cm.exception.response["Error"]["Code"].should.equal("InvalidParameter") + cm.exception.response["Error"]["Message"].should.contain("not meet the E164") - try: - # Test not found number - client.publish(PhoneNumber="+44001234567", Message="my message") - except ClientError as err: - err.response["Error"]["Code"].should.equal("ParameterValueInvalid") + # Test to long ASCII message + with assert_raises(ClientError) as cm: + client.publish(PhoneNumber="+15551234567", Message="a" * 1601) + cm.exception.response["Error"]["Code"].should.equal("InvalidParameter") + cm.exception.response["Error"]["Message"].should.contain("must be less than 1600") @mock_sqs