Repository: getmoto/moto

Issue:
Sending SMS via SNS is not mocked; ARN is required
```
$ python
Python 3.5.3 (default, Jan 19 2017, 14:11:04) 
[GCC 6.3.0 20170118] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import moto
>>> moto.__version__
'1.0.1'
```

Example:
```
import boto3
from moto import mock_sns

mock = mock_sns()
mock.start()
client = boto3.client("sns")
client.publish(PhoneNumber="+15551234567", Message="my message")
```

Expected:
Return a dictionary per http://boto3.readthedocs.io/en/latest/reference/services/sns.html#SNS.Client.publish

Actual:
```
Traceback (most recent call last):
  File "/home/gavin/backend/notifications/tmp.py", line 7, in <module>
    client.publish(PhoneNumber="+15551234567", Message="my message")
  File "/home/gavin/backend/notifications/venv/lib/python3.5/site-packages/botocore/client.py", line 312, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/home/gavin/backend/notifications/venv/lib/python3.5/site-packages/botocore/client.py", line 601, in _make_api_call
    raise error_class(parsed_response, operation_name)
botocore.errorfactory.NotFoundException: An error occurred (NotFound) when calling the Publish operation: Endpoint with arn None not found
```


Discussion:
Will look into it
@JackDanger can I get permissions to tag issues, have confirmed this is a bug.

Have found the offending code. As publishing with SNS matters, will need to add the ability to look up phone numbers linked to topics, get the topic arn then we'll be good.
```python
   def publish(self):
        target_arn = self._get_param('TargetArn')
        topic_arn = self._get_param('TopicArn')
        arn = target_arn if target_arn else topic_arn
        message = self._get_param('Message')
        message_id = self.backend.publish(arn, message)
```

Resolving patch:
diff --git a/moto/sns/models.py b/moto/sns/models.py
--- a/moto/sns/models.py
+++ b/moto/sns/models.py
@@ -221,6 +221,12 @@ def get_topic(self, arn):
         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
--- a/moto/sns/responses.py
+++ b/moto/sns/responses.py
@@ -6,6 +6,8 @@
 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 @@ def subscribe(self):
         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 @@ def list_subscriptions_by_topic(self):
     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
--- 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


Test patch:
diff --git a/tests/test_sns/test_publishing_boto3.py b/tests/test_sns/test_publishing_boto3.py
--- a/tests/test_sns/test_publishing_boto3.py
+++ b/tests/test_sns/test_publishing_boto3.py
@@ -10,6 +10,7 @@
 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
--- a/tests/test_sns/test_subscriptions_boto3.py
+++ b/tests/test_sns/test_subscriptions_boto3.py
@@ -11,6 +11,39 @@
 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')