From 56c65bc67cf9cc73c7417f786e3585affac8079b Mon Sep 17 00:00:00 2001 From: Terry Cain Date: Tue, 26 Sep 2017 00:21:07 +0100 Subject: [PATCH] Added in publish by phone number --- moto/sns/models.py | 6 +++ moto/sns/responses.py | 32 +++++++++++++++- moto/sns/utils.py | 7 ++++ tests/test_sns/test_publishing_boto3.py | 44 ++++++++++++++++++++++ tests/test_sns/test_subscriptions_boto3.py | 33 ++++++++++++++++ 5 files changed, 121 insertions(+), 1 deletion(-) diff --git a/moto/sns/models.py b/moto/sns/models.py index 9feed0198..36336aaac 100644 --- a/moto/sns/models.py +++ b/moto/sns/models.py @@ -221,6 +221,12 @@ 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) diff --git a/moto/sns/responses.py b/moto/sns/responses.py index 92092dc42..85764aa58 100644 --- a/moto/sns/responses.py +++ b/moto/sns/responses.py @@ -6,6 +6,8 @@ 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 +from .utils import is_e164 class SNSResponse(BaseResponse): @@ -136,6 +138,13 @@ class SNSResponse(BaseResponse): topic_arn = self._get_param('TopicArn') endpoint = self._get_param('Endpoint') protocol = self._get_param('Protocol') + + if protocol == 'sms' and not is_e164(endpoint): + return self._error( + 'InvalidParameter', + 'Phone number does not meet the E164 format' + ), dict(status=400) + subscription = self.backend.subscribe(topic_arn, endpoint, protocol) if self.request_json: @@ -229,7 +238,28 @@ class SNSResponse(BaseResponse): def publish(self): target_arn = self._get_param('TargetArn') topic_arn = self._get_param('TopicArn') - arn = target_arn if target_arn else topic_arn + phone_number = self._get_param('PhoneNumber') + if phone_number is not None: + # Check phone is correct syntax (e164) + if not is_e164(phone_number): + return self._error( + 'InvalidParameter', + 'Phone number does not meet the E164 format' + ), 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: + arn = topic_arn + message = self._get_param('Message') message_id = self.backend.publish(arn, message) diff --git a/moto/sns/utils.py b/moto/sns/utils.py index 864c3af6b..7793b0f6d 100644 --- a/moto/sns/utils.py +++ b/moto/sns/utils.py @@ -1,6 +1,9 @@ from __future__ import unicode_literals +import re import uuid +E164_REGEX = re.compile(r'^\+?[1-9]\d{1,14}$') + def make_arn_for_topic(account_id, name, region_name): return "arn:aws:sns:{0}:{1}:{2}".format(region_name, account_id, name) @@ -9,3 +12,7 @@ def make_arn_for_topic(account_id, name, region_name): def make_arn_for_subscription(topic_arn): subscription_id = uuid.uuid4() return "{0}:{1}".format(topic_arn, subscription_id) + + +def is_e164(number): + return E164_REGEX.match(number) is not None diff --git a/tests/test_sns/test_publishing_boto3.py b/tests/test_sns/test_publishing_boto3.py index a53744d63..6228f212f 100644 --- a/tests/test_sns/test_publishing_boto3.py +++ b/tests/test_sns/test_publishing_boto3.py @@ -10,6 +10,7 @@ from freezegun import freeze_time import sure # noqa from moto.packages.responses import responses +from botocore.exceptions import ClientError from moto import mock_sns, mock_sqs from freezegun import freeze_time @@ -43,6 +44,49 @@ def test_publish_to_sqs(): acquired_message.should.equal(expected) +@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') + + +@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 + client.publish(PhoneNumber="NAA+15551234567", Message="my message") + except ClientError as err: + err.response['Error']['Code'].should.equal('InvalidParameter') + + try: + # Test not found number + client.publish(PhoneNumber="+44001234567", Message="my message") + except ClientError as err: + err.response['Error']['Code'].should.equal('ParameterValueInvalid') + + @mock_sqs @mock_sns def test_publish_to_sqs_dump_json(): diff --git a/tests/test_sns/test_subscriptions_boto3.py b/tests/test_sns/test_subscriptions_boto3.py index e600d6422..4446febfc 100644 --- a/tests/test_sns/test_subscriptions_boto3.py +++ b/tests/test_sns/test_subscriptions_boto3.py @@ -11,6 +11,39 @@ from moto import mock_sns from moto.sns.models import DEFAULT_PAGE_SIZE +@mock_sns +def test_subscribe_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'] + + resp = client.subscribe( + TopicArn=arn, + Protocol='sms', + Endpoint='+15551234567' + ) + resp.should.contain('SubscriptionArn') + + +@mock_sns +def test_subscribe_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'] + + try: + # Test invalid number + client.subscribe( + TopicArn=arn, + Protocol='sms', + Endpoint='NAA+15551234567' + ) + except ClientError as err: + err.response['Error']['Code'].should.equal('InvalidParameter') + + @mock_sns def test_creating_subscription(): conn = boto3.client('sns', region_name='us-east-1')