From 57d94d56c3641591ff73000b6e9a701c34011993 Mon Sep 17 00:00:00 2001 From: Terry Cain Date: Tue, 19 Sep 2017 22:48:46 +0100 Subject: [PATCH 1/8] Implemented SNS.SetSMSAttributes & SNS.GetSMSAttributes + Filtering TODO: AddPermission / RemovePermission CheckIfPhoneNumberIsOptedOut ConfirmSubscription ListPhoneNumbersOptedOut OptInPhoneNumber --- moto/sns/models.py | 4 ++ moto/sns/responses.py | 66 ++++++++++++++++++++++++++++++++ tests/test_sns/test_sms_boto3.py | 32 ++++++++++++++++ 3 files changed, 102 insertions(+) create mode 100644 tests/test_sns/test_sms_boto3.py diff --git a/moto/sns/models.py b/moto/sns/models.py index 009398407..bc80f9e41 100644 --- a/moto/sns/models.py +++ b/moto/sns/models.py @@ -172,12 +172,16 @@ class SNSBackend(BaseBackend): self.applications = {} self.platform_endpoints = {} self.region_name = region_name + self.sms_attributes = {} def reset(self): region_name = self.region_name self.__dict__ = {} self.__init__(region_name) + def update_sms_attributes(self, attrs): + self.sms_attributes.update(attrs) + def create_topic(self, name): topic = Topic(name, self) self.topics[topic.arn] = topic diff --git a/moto/sns/responses.py b/moto/sns/responses.py index 9c079b006..9ffe298af 100644 --- a/moto/sns/responses.py +++ b/moto/sns/responses.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals import json +import re +from collections import defaultdict from moto.core.responses import BaseResponse from moto.core.utils import camelcase_to_underscores @@ -459,6 +461,47 @@ class SNSResponse(BaseResponse): template = self.response_template(SET_SUBSCRIPTION_ATTRIBUTES_TEMPLATE) return template.render() + def set_sms_attributes(self): + attr_regex = re.compile(r'^attributes\.entry\.(?P\d+)\.(?Pkey|value)$') + + # attributes.entry.1.key + # attributes.entry.1.value + # to + # 1: {key:X, value:Y} + temp_dict = defaultdict(dict) + for key, value in self.querystring.items(): + match = attr_regex.match(key) + if match is not None: + temp_dict[match.group('index')][match.group('type')] = value[0] + + # 1: {key:X, value:Y} + # to + # X: Y + # All of this, just to take into account when people provide invalid stuff. + result = {} + for item in temp_dict.values(): + if 'key' in item and 'value' in item: + result[item['key']] = item['value'] + + self.backend.update_sms_attributes(result) + + template = self.response_template(SET_SMS_ATTRIBUTES_TEMPLATE) + return template.render() + + def get_sms_attributes(self): + filter_list = set() + for key, value in self.querystring.items(): + if key.startswith('attributes.member.1'): + filter_list.add(value[0]) + + if len(filter_list) > 0: + result = {k: v for k, v in self.backend.sms_attributes.items() if k in filter_list} + else: + result = self.backend.sms_attributes + + template = self.response_template(GET_SMS_ATTRIBUTES_TEMPLATE) + return template.render(attributes=result) + CREATE_TOPIC_TEMPLATE = """ @@ -758,3 +801,26 @@ SET_SUBSCRIPTION_ATTRIBUTES_TEMPLATE = """a8763b99-33a7-11df-a9b7-05d48da6f042 """ + +SET_SMS_ATTRIBUTES_TEMPLATE = """ + + + 26332069-c04a-5428-b829-72524b56a364 + +""" + +GET_SMS_ATTRIBUTES_TEMPLATE = """ + + + {% for name, value in attributes.items() %} + + {{ name }} + {{ value }} + + {% endfor %} + + + + 287f9554-8db3-5e66-8abc-c76f0186db7e + +""" diff --git a/tests/test_sns/test_sms_boto3.py b/tests/test_sns/test_sms_boto3.py new file mode 100644 index 000000000..220a7530a --- /dev/null +++ b/tests/test_sns/test_sms_boto3.py @@ -0,0 +1,32 @@ +from __future__ import unicode_literals +import boto3 +import sure # noqa + +from moto import mock_sns + + +@mock_sns +def test_set_sms_attributes(): + conn = boto3.client('sns', region_name='us-east-1') + + conn.set_sms_attributes(attributes={'DefaultSMSType': 'Transactional', 'test': 'test'}) + + response = conn.get_sms_attributes() + response.should.contain('attributes') + response['attributes'].should.contain('DefaultSMSType') + response['attributes'].should.contain('test') + response['attributes']['DefaultSMSType'].should.equal('Transactional') + response['attributes']['test'].should.equal('test') + + +@mock_sns +def test_get_sms_attributes_filtered(): + conn = boto3.client('sns', region_name='us-east-1') + + conn.set_sms_attributes(attributes={'DefaultSMSType': 'Transactional', 'test': 'test'}) + + response = conn.get_sms_attributes(attributes=['DefaultSMSType']) + response.should.contain('attributes') + response['attributes'].should.contain('DefaultSMSType') + response['attributes'].should_not.contain('test') + response['attributes']['DefaultSMSType'].should.equal('Transactional') From ba8a2ccfc5c2babbd3876be05fcd5796b7ee607c Mon Sep 17 00:00:00 2001 From: Terry Cain Date: Tue, 19 Sep 2017 23:54:13 +0100 Subject: [PATCH 2/8] Implemented CheckIfPhoneNumberIsOptedOut + Tests + Error code --- moto/sns/responses.py | 41 +++++++++++++++++++++++++++++--- tests/test_sns/test_sms_boto3.py | 29 ++++++++++++++++++++++ 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/moto/sns/responses.py b/moto/sns/responses.py index 9ffe298af..f06f10816 100644 --- a/moto/sns/responses.py +++ b/moto/sns/responses.py @@ -9,11 +9,17 @@ from .models import sns_backends class SNSResponse(BaseResponse): + SMS_ATTR_REGEX = re.compile(r'^attributes\.entry\.(?P\d+)\.(?Pkey|value)$') + OPT_OUT_PHONE_NUMBER_REGEX = re.compile(r'^\+?\d+$') @property def backend(self): return sns_backends[self.region] + def _error(self, code, message, sender='Sender'): + template = self.response_template(ERROR_RESPONSE) + return template.render(code=code, message=message, sender=sender) + def _get_attributes(self): attributes = self._get_list_prefix('Attributes.entry') return dict( @@ -462,15 +468,13 @@ class SNSResponse(BaseResponse): return template.render() def set_sms_attributes(self): - attr_regex = re.compile(r'^attributes\.entry\.(?P\d+)\.(?Pkey|value)$') - # attributes.entry.1.key # attributes.entry.1.value # to # 1: {key:X, value:Y} temp_dict = defaultdict(dict) for key, value in self.querystring.items(): - match = attr_regex.match(key) + match = self.SMS_ATTR_REGEX.match(key) if match is not None: temp_dict[match.group('index')][match.group('type')] = value[0] @@ -502,6 +506,19 @@ class SNSResponse(BaseResponse): template = self.response_template(GET_SMS_ATTRIBUTES_TEMPLATE) return template.render(attributes=result) + def check_if_phone_number_is_opted_out(self): + number = self._get_param('phoneNumber') + if self.OPT_OUT_PHONE_NUMBER_REGEX.match(number) is None: + error_response = self._error( + code='InvalidParameter', + message='Invalid parameter: PhoneNumber Reason: input incorrectly formatted' + ) + return error_response, dict(status=400) + + # There should be a nicer way to set if a nubmer has opted out + template = self.response_template(CHECK_IF_OPTED_OUT_TEMPLATE) + return template.render(opt_out=str(number.endswith('99')).lower()) + CREATE_TOPIC_TEMPLATE = """ @@ -824,3 +841,21 @@ GET_SMS_ATTRIBUTES_TEMPLATE = """ + + {{ opt_out }} + + + 287f9554-8db3-5e66-8abc-c76f0186db7e + +""" + +ERROR_RESPONSE = """ + + {{ sender }} + {{ code }} + {{ message }} + + 9dd01905-5012-5f99-8663-4b3ecd0dfaef +""" \ No newline at end of file diff --git a/tests/test_sns/test_sms_boto3.py b/tests/test_sns/test_sms_boto3.py index 220a7530a..185b3e43c 100644 --- a/tests/test_sns/test_sms_boto3.py +++ b/tests/test_sns/test_sms_boto3.py @@ -3,6 +3,8 @@ import boto3 import sure # noqa from moto import mock_sns +from botocore.exceptions import ClientError +from nose.tools import assert_raises @mock_sns @@ -30,3 +32,30 @@ def test_get_sms_attributes_filtered(): response['attributes'].should.contain('DefaultSMSType') response['attributes'].should_not.contain('test') response['attributes']['DefaultSMSType'].should.equal('Transactional') + + +@mock_sns +def test_check_not_opted_out(): + conn = boto3.client('sns', region_name='us-east-1') + response = conn.check_if_phone_number_is_opted_out(phoneNumber='+447428545375') + + response.should.contain('isOptedOut') + response['isOptedOut'].should.be(False) + + +@mock_sns +def test_check_opted_out(): # Ends in 99 so is opted out + conn = boto3.client('sns', region_name='us-east-1') + response = conn.check_if_phone_number_is_opted_out(phoneNumber='+447428545399') + + response.should.contain('isOptedOut') + response['isOptedOut'].should.be(True) + + +@mock_sns +def test_check_opted_out_invalid(): + conn = boto3.client('sns', region_name='us-east-1') + + # Invalid phone number + with assert_raises(ClientError): + conn.check_if_phone_number_is_opted_out(phoneNumber='+44742LALALA') From 1281ac86d51c0f5b33e012dd7fe999114c389126 Mon Sep 17 00:00:00 2001 From: Terry Cain Date: Wed, 20 Sep 2017 00:03:58 +0100 Subject: [PATCH 3/8] Implemented ListPhoneNumbersOptedOut + Tests --- moto/sns/responses.py | 19 ++++++++++++++++++- tests/test_sns/test_sms_boto3.py | 10 ++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/moto/sns/responses.py b/moto/sns/responses.py index f06f10816..93786e72e 100644 --- a/moto/sns/responses.py +++ b/moto/sns/responses.py @@ -519,6 +519,10 @@ class SNSResponse(BaseResponse): template = self.response_template(CHECK_IF_OPTED_OUT_TEMPLATE) return template.render(opt_out=str(number.endswith('99')).lower()) + def list_phone_numbers_opted_out(self): + template = self.response_template(LIST_OPTOUT_TEMPLATE) + return template.render(opt_outs=['+447420500600', '+447420505401']) + CREATE_TOPIC_TEMPLATE = """ @@ -858,4 +862,17 @@ ERROR_RESPONSE = """""" + +LIST_OPTOUT_TEMPLATE = """ + + + {% for item in opt_outs %} + {{ item }} + {% endfor %} + + + + 985e196d-a237-51b6-b33a-4b5601276b38 + +""" diff --git a/tests/test_sns/test_sms_boto3.py b/tests/test_sns/test_sms_boto3.py index 185b3e43c..a10f9d6dc 100644 --- a/tests/test_sns/test_sms_boto3.py +++ b/tests/test_sns/test_sms_boto3.py @@ -59,3 +59,13 @@ def test_check_opted_out_invalid(): # Invalid phone number with assert_raises(ClientError): conn.check_if_phone_number_is_opted_out(phoneNumber='+44742LALALA') + + +@mock_sns +def test_list_opted_out(): + conn = boto3.client('sns', region_name='us-east-1') + response = conn.list_phone_numbers_opted_out() + + response.should.contain('phoneNumbers') + response['phoneNumbers'].should.contain('+447420500600') + response['phoneNumbers'].should.contain('+447420505401') From f7f80293c7a21a6df859dfa0cc35ae1641dcba5e Mon Sep 17 00:00:00 2001 From: Terry Cain Date: Wed, 20 Sep 2017 20:56:37 +0100 Subject: [PATCH 4/8] Implemented OptInPhoneNumber + Tests --- moto/sns/models.py | 1 + moto/sns/responses.py | 22 +++++++++++++++++++++- tests/test_sns/test_sms_boto3.py | 17 +++++++++++++++-- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/moto/sns/models.py b/moto/sns/models.py index bc80f9e41..6f5ee54ac 100644 --- a/moto/sns/models.py +++ b/moto/sns/models.py @@ -173,6 +173,7 @@ class SNSBackend(BaseBackend): self.platform_endpoints = {} self.region_name = region_name self.sms_attributes = {} + self.opt_out_numbers = ['+447420500600', '+447420505401', '+447632960543', '+447632960028', '+447700900149', '+447700900550', '+447700900545', '+447700900907'] def reset(self): region_name = self.region_name diff --git a/moto/sns/responses.py b/moto/sns/responses.py index 93786e72e..5b1c34610 100644 --- a/moto/sns/responses.py +++ b/moto/sns/responses.py @@ -521,7 +521,19 @@ class SNSResponse(BaseResponse): def list_phone_numbers_opted_out(self): template = self.response_template(LIST_OPTOUT_TEMPLATE) - return template.render(opt_outs=['+447420500600', '+447420505401']) + return template.render(opt_outs=self.backend.opt_out_numbers) + + def opt_in_phone_number(self): + number = self._get_param('phoneNumber') + + try: + self.backend.opt_out_numbers.remove(number) + except ValueError: + pass + + template = self.response_template(OPT_IN_NUMBER_TEMPLATE) + return template.render() + CREATE_TOPIC_TEMPLATE = """ @@ -876,3 +888,11 @@ LIST_OPTOUT_TEMPLATE = """ + + + 4c61842c-0796-50ef-95ac-d610c0bc8cf8 + + +""" diff --git a/tests/test_sns/test_sms_boto3.py b/tests/test_sns/test_sms_boto3.py index a10f9d6dc..9fbc90ac8 100644 --- a/tests/test_sns/test_sms_boto3.py +++ b/tests/test_sns/test_sms_boto3.py @@ -67,5 +67,18 @@ def test_list_opted_out(): response = conn.list_phone_numbers_opted_out() response.should.contain('phoneNumbers') - response['phoneNumbers'].should.contain('+447420500600') - response['phoneNumbers'].should.contain('+447420505401') + len(response['phoneNumbers']).should.be.greater_than(0) + + +@mock_sns +def test_opt_in(): + conn = boto3.client('sns', region_name='us-east-1') + response = conn.list_phone_numbers_opted_out() + current_len = len(response['phoneNumbers']) + assert current_len > 0 + + conn.opt_in_phone_number(phoneNumber=response['phoneNumbers'][0]) + + response = conn.list_phone_numbers_opted_out() + len(response['phoneNumbers']).should.be.greater_than(0) + len(response['phoneNumbers']).should.be.lower_than(current_len) From ef8a97f6c35b63bfb6c7c9a0b10c6b9e94faffc2 Mon Sep 17 00:00:00 2001 From: Terry Cain Date: Wed, 20 Sep 2017 21:13:26 +0100 Subject: [PATCH 5/8] Implemented Add/RemovePermission + Tests --- moto/sns/models.py | 1 + moto/sns/responses.py | 39 ++++++++++++++++++++++++++++++-- tests/test_sns/test_sms_boto3.py | 16 +++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/moto/sns/models.py b/moto/sns/models.py index 6f5ee54ac..4a7cf7e7d 100644 --- a/moto/sns/models.py +++ b/moto/sns/models.py @@ -174,6 +174,7 @@ class SNSBackend(BaseBackend): self.region_name = region_name self.sms_attributes = {} self.opt_out_numbers = ['+447420500600', '+447420505401', '+447632960543', '+447632960028', '+447700900149', '+447700900550', '+447700900545', '+447700900907'] + self.permissions = {} def reset(self): region_name = self.region_name diff --git a/moto/sns/responses.py b/moto/sns/responses.py index 5b1c34610..97939d6b9 100644 --- a/moto/sns/responses.py +++ b/moto/sns/responses.py @@ -534,6 +534,30 @@ class SNSResponse(BaseResponse): template = self.response_template(OPT_IN_NUMBER_TEMPLATE) return template.render() + def add_permission(self): + arn = self._get_param('TopicArn') + label = self._get_param('Label') + accounts = self._get_multi_param('AWSAccountId.member.') + action = self._get_multi_param('ActionName.member.') + + key = (arn, label) + self.backend.permissions[key] = {'accounts': accounts, 'action': action} + + template = self.response_template(ADD_PERMISSION) + return template.render() + + def remove_permission(self): + arn = self._get_param('TopicArn') + label = self._get_param('Label') + + try: + key = (arn, label) + del self.backend.permissions[key] + except KeyError: + pass + + template = self.response_template(DEL_PERMISSION) + return template.render() CREATE_TOPIC_TEMPLATE = """ @@ -894,5 +918,16 @@ OPT_IN_NUMBER_TEMPLATE = """""" + +ADD_PERMISSION = """ + + c046e713-c5ff-5888-a7bc-b52f0e4f1299 + +""" + +DEL_PERMISSION = """ + + e767cc9f-314b-5e1b-b283-9ea3fd4e38a3 + +""" diff --git a/tests/test_sns/test_sms_boto3.py b/tests/test_sns/test_sms_boto3.py index 9fbc90ac8..beaa92d55 100644 --- a/tests/test_sns/test_sms_boto3.py +++ b/tests/test_sns/test_sms_boto3.py @@ -82,3 +82,19 @@ def test_opt_in(): response = conn.list_phone_numbers_opted_out() len(response['phoneNumbers']).should.be.greater_than(0) len(response['phoneNumbers']).should.be.lower_than(current_len) + + +@mock_sns +def test_add_remove_permissions(): + conn = boto3.client('sns', region_name='us-east-1') + + conn.add_permission( + TopicArn='arn:aws:sns:us-east-1:000000000000:terry_test', + Label='Test1234', + AWSAccountId=['999999999999'], + ActionName=['AddPermission'] + ) + conn.remove_permission( + TopicArn='arn:aws:sns:us-east-1:000000000000:terry_test', + Label='Test1234' + ) From 19074c535cee1f23f495042141395e9256d1e053 Mon Sep 17 00:00:00 2001 From: Terry Cain Date: Wed, 20 Sep 2017 21:47:02 +0100 Subject: [PATCH 6/8] Added ConfirmSubscription + Tests + checks For now subscriptions do nothing, but if we go the route of handing out subscribe tokens, I have layed the groundwork for validating that --- moto/sns/models.py | 1 + moto/sns/responses.py | 45 +++++++++++++++++++++++++++++--- tests/test_sns/test_sms_boto3.py | 17 ++++++++++-- 3 files changed, 57 insertions(+), 6 deletions(-) diff --git a/moto/sns/models.py b/moto/sns/models.py index 4a7cf7e7d..9feed0198 100644 --- a/moto/sns/models.py +++ b/moto/sns/models.py @@ -77,6 +77,7 @@ class Subscription(BaseModel): self.protocol = protocol self.arn = make_arn_for_subscription(self.topic.arn) self.attributes = {} + self.confirmed = False def publish(self, message, message_id): if self.protocol == 'sqs': diff --git a/moto/sns/responses.py b/moto/sns/responses.py index 97939d6b9..87d35ec17 100644 --- a/moto/sns/responses.py +++ b/moto/sns/responses.py @@ -540,25 +540,53 @@ class SNSResponse(BaseResponse): accounts = self._get_multi_param('AWSAccountId.member.') action = self._get_multi_param('ActionName.member.') + if arn not in self.backend.topics: + error_response = self._error('NotFound', 'Topic does not exist') + return error_response, dict(status=404) + key = (arn, label) self.backend.permissions[key] = {'accounts': accounts, 'action': action} - template = self.response_template(ADD_PERMISSION) + template = self.response_template(ADD_PERMISSION_TEMPLATE) return template.render() def remove_permission(self): arn = self._get_param('TopicArn') label = self._get_param('Label') + if arn not in self.backend.topics: + error_response = self._error('NotFound', 'Topic does not exist') + return error_response, dict(status=404) + try: key = (arn, label) del self.backend.permissions[key] except KeyError: pass - template = self.response_template(DEL_PERMISSION) + template = self.response_template(DEL_PERMISSION_TEMPLATE) return template.render() + def confirm_subscription(self): + arn = self._get_param('TopicArn') + + if arn not in self.backend.topics: + error_response = self._error('NotFound', 'Topic does not exist') + return error_response, dict(status=404) + + # Added other parts here for when they are needed + # token = self._get_param('Token') + # auth = self._get_param('AuthenticateOnUnsubscribe') + # if already_subscribed: + # error_response = self._error( + # code='AuthorizationError', + # message='Subscription already confirmed' + # ) + # return error_response, dict(status=400) + + template = self.response_template(CONFIRM_SUBSCRIPTION_TEMPLATE) + return template.render(sub_arn='{0}:68762e72-e9b1-410a-8b3b-903da69ee1d5'.format(arn)) + CREATE_TOPIC_TEMPLATE = """ @@ -920,14 +948,23 @@ OPT_IN_NUMBER_TEMPLATE = """ +ADD_PERMISSION_TEMPLATE = """ c046e713-c5ff-5888-a7bc-b52f0e4f1299 """ -DEL_PERMISSION = """ +DEL_PERMISSION_TEMPLATE = """ e767cc9f-314b-5e1b-b283-9ea3fd4e38a3 """ + +CONFIRM_SUBSCRIPTION_TEMPLATE = """ + + {{ sub_arn }} + + + 16eb4dde-7b3c-5b3e-a22a-1fe2a92d3293 + +""" diff --git a/tests/test_sns/test_sms_boto3.py b/tests/test_sns/test_sms_boto3.py index beaa92d55..ca6bfd22a 100644 --- a/tests/test_sns/test_sms_boto3.py +++ b/tests/test_sns/test_sms_boto3.py @@ -87,14 +87,27 @@ def test_opt_in(): @mock_sns def test_add_remove_permissions(): conn = boto3.client('sns', region_name='us-east-1') + response = conn.create_topic(Name='testpermissions') conn.add_permission( - TopicArn='arn:aws:sns:us-east-1:000000000000:terry_test', + TopicArn=response['TopicArn'], Label='Test1234', AWSAccountId=['999999999999'], ActionName=['AddPermission'] ) conn.remove_permission( - TopicArn='arn:aws:sns:us-east-1:000000000000:terry_test', + TopicArn=response['TopicArn'], Label='Test1234' ) + + +@mock_sns +def test_confirm_subscription(): + conn = boto3.client('sns', region_name='us-east-1') + response = conn.create_topic(Name='testconfirm') + + conn.confirm_subscription( + TopicArn=response['TopicArn'], + Token='2336412f37fb687f5d51e6e241d59b68c4e583a5cee0be6f95bbf97ab8d2441cf47b99e848408adaadf4c197e65f03473d53c4ba398f6abbf38ce2e8ebf7b4ceceb2cd817959bcde1357e58a2861b05288c535822eb88cac3db04f592285249971efc6484194fc4a4586147f16916692', + AuthenticateOnUnsubscribe='true' + ) From fec81fc6ea6093a2a39aa387d8db7c3b9a321237 Mon Sep 17 00:00:00 2001 From: Terry Cain Date: Wed, 20 Sep 2017 21:49:09 +0100 Subject: [PATCH 7/8] Updated README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4d5d2d7e6..cca50a16e 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ It gets even better! Moto isn't just for Python code and it isn't just for S3. L |------------------------------------------------------------------------------| | SES | @mock_ses | core endpoints done | |------------------------------------------------------------------------------| -| SNS | @mock_sns | core endpoints done | +| SNS | @mock_sns | all endpoints done | |------------------------------------------------------------------------------| | SQS | @mock_sqs | core endpoints done | |------------------------------------------------------------------------------| From 18cb0bce542823bf328cb53839246f94d3b94466 Mon Sep 17 00:00:00 2001 From: Terry Cain Date: Thu, 21 Sep 2017 21:16:00 +0100 Subject: [PATCH 8/8] General tidy up --- moto/sns/responses.py | 7 +- tests/test_sns/test_application_boto3.py | 27 +++++ tests/test_sns/test_sms_boto3.py | 113 --------------------- tests/test_sns/test_subscriptions.py | 2 + tests/test_sns/test_subscriptions_boto3.py | 66 ++++++++++++ tests/test_sns/test_topics_boto3.py | 17 ++++ 6 files changed, 118 insertions(+), 114 deletions(-) delete mode 100644 tests/test_sns/test_sms_boto3.py diff --git a/moto/sns/responses.py b/moto/sns/responses.py index 87d35ec17..92092dc42 100644 --- a/moto/sns/responses.py +++ b/moto/sns/responses.py @@ -574,7 +574,12 @@ class SNSResponse(BaseResponse): error_response = self._error('NotFound', 'Topic does not exist') return error_response, dict(status=404) - # Added other parts here for when they are needed + # Once Tokens are stored by the `subscribe` endpoint and distributed + # to the client somehow, then we can check validity of tokens + # presented to this method. The following code works, all thats + # needed is to perform a token check and assign that value to the + # `already_subscribed` variable. + # # token = self._get_param('Token') # auth = self._get_param('AuthenticateOnUnsubscribe') # if already_subscribed: diff --git a/tests/test_sns/test_application_boto3.py b/tests/test_sns/test_application_boto3.py index 99c378fe4..1c9695fea 100644 --- a/tests/test_sns/test_application_boto3.py +++ b/tests/test_sns/test_application_boto3.py @@ -321,3 +321,30 @@ def test_publish_to_disabled_platform_endpoint(): MessageStructure="json", TargetArn=endpoint_arn, ).should.throw(ClientError) + + +@mock_sns +def test_set_sms_attributes(): + conn = boto3.client('sns', region_name='us-east-1') + + conn.set_sms_attributes(attributes={'DefaultSMSType': 'Transactional', 'test': 'test'}) + + response = conn.get_sms_attributes() + response.should.contain('attributes') + response['attributes'].should.contain('DefaultSMSType') + response['attributes'].should.contain('test') + response['attributes']['DefaultSMSType'].should.equal('Transactional') + response['attributes']['test'].should.equal('test') + + +@mock_sns +def test_get_sms_attributes_filtered(): + conn = boto3.client('sns', region_name='us-east-1') + + conn.set_sms_attributes(attributes={'DefaultSMSType': 'Transactional', 'test': 'test'}) + + response = conn.get_sms_attributes(attributes=['DefaultSMSType']) + response.should.contain('attributes') + response['attributes'].should.contain('DefaultSMSType') + response['attributes'].should_not.contain('test') + response['attributes']['DefaultSMSType'].should.equal('Transactional') diff --git a/tests/test_sns/test_sms_boto3.py b/tests/test_sns/test_sms_boto3.py deleted file mode 100644 index ca6bfd22a..000000000 --- a/tests/test_sns/test_sms_boto3.py +++ /dev/null @@ -1,113 +0,0 @@ -from __future__ import unicode_literals -import boto3 -import sure # noqa - -from moto import mock_sns -from botocore.exceptions import ClientError -from nose.tools import assert_raises - - -@mock_sns -def test_set_sms_attributes(): - conn = boto3.client('sns', region_name='us-east-1') - - conn.set_sms_attributes(attributes={'DefaultSMSType': 'Transactional', 'test': 'test'}) - - response = conn.get_sms_attributes() - response.should.contain('attributes') - response['attributes'].should.contain('DefaultSMSType') - response['attributes'].should.contain('test') - response['attributes']['DefaultSMSType'].should.equal('Transactional') - response['attributes']['test'].should.equal('test') - - -@mock_sns -def test_get_sms_attributes_filtered(): - conn = boto3.client('sns', region_name='us-east-1') - - conn.set_sms_attributes(attributes={'DefaultSMSType': 'Transactional', 'test': 'test'}) - - response = conn.get_sms_attributes(attributes=['DefaultSMSType']) - response.should.contain('attributes') - response['attributes'].should.contain('DefaultSMSType') - response['attributes'].should_not.contain('test') - response['attributes']['DefaultSMSType'].should.equal('Transactional') - - -@mock_sns -def test_check_not_opted_out(): - conn = boto3.client('sns', region_name='us-east-1') - response = conn.check_if_phone_number_is_opted_out(phoneNumber='+447428545375') - - response.should.contain('isOptedOut') - response['isOptedOut'].should.be(False) - - -@mock_sns -def test_check_opted_out(): # Ends in 99 so is opted out - conn = boto3.client('sns', region_name='us-east-1') - response = conn.check_if_phone_number_is_opted_out(phoneNumber='+447428545399') - - response.should.contain('isOptedOut') - response['isOptedOut'].should.be(True) - - -@mock_sns -def test_check_opted_out_invalid(): - conn = boto3.client('sns', region_name='us-east-1') - - # Invalid phone number - with assert_raises(ClientError): - conn.check_if_phone_number_is_opted_out(phoneNumber='+44742LALALA') - - -@mock_sns -def test_list_opted_out(): - conn = boto3.client('sns', region_name='us-east-1') - response = conn.list_phone_numbers_opted_out() - - response.should.contain('phoneNumbers') - len(response['phoneNumbers']).should.be.greater_than(0) - - -@mock_sns -def test_opt_in(): - conn = boto3.client('sns', region_name='us-east-1') - response = conn.list_phone_numbers_opted_out() - current_len = len(response['phoneNumbers']) - assert current_len > 0 - - conn.opt_in_phone_number(phoneNumber=response['phoneNumbers'][0]) - - response = conn.list_phone_numbers_opted_out() - len(response['phoneNumbers']).should.be.greater_than(0) - len(response['phoneNumbers']).should.be.lower_than(current_len) - - -@mock_sns -def test_add_remove_permissions(): - conn = boto3.client('sns', region_name='us-east-1') - response = conn.create_topic(Name='testpermissions') - - conn.add_permission( - TopicArn=response['TopicArn'], - Label='Test1234', - AWSAccountId=['999999999999'], - ActionName=['AddPermission'] - ) - conn.remove_permission( - TopicArn=response['TopicArn'], - Label='Test1234' - ) - - -@mock_sns -def test_confirm_subscription(): - conn = boto3.client('sns', region_name='us-east-1') - response = conn.create_topic(Name='testconfirm') - - conn.confirm_subscription( - TopicArn=response['TopicArn'], - Token='2336412f37fb687f5d51e6e241d59b68c4e583a5cee0be6f95bbf97ab8d2441cf47b99e848408adaadf4c197e65f03473d53c4ba398f6abbf38ce2e8ebf7b4ceceb2cd817959bcde1357e58a2861b05288c535822eb88cac3db04f592285249971efc6484194fc4a4586147f16916692', - AuthenticateOnUnsubscribe='true' - ) diff --git a/tests/test_sns/test_subscriptions.py b/tests/test_sns/test_subscriptions.py index 292fd83c0..ba241ba44 100644 --- a/tests/test_sns/test_subscriptions.py +++ b/tests/test_sns/test_subscriptions.py @@ -34,6 +34,7 @@ def test_creating_subscription(): "ListSubscriptionsResult"]["Subscriptions"] subscriptions.should.have.length_of(0) + @mock_sns_deprecated def test_deleting_subscriptions_by_deleting_topic(): conn = boto.connect_sns() @@ -66,6 +67,7 @@ def test_deleting_subscriptions_by_deleting_topic(): "ListSubscriptionsResult"]["Subscriptions"] subscriptions.should.have.length_of(0) + @mock_sns_deprecated def test_getting_subscriptions_by_topic(): conn = boto.connect_sns() diff --git a/tests/test_sns/test_subscriptions_boto3.py b/tests/test_sns/test_subscriptions_boto3.py index 8cb5c1886..e600d6422 100644 --- a/tests/test_sns/test_subscriptions_boto3.py +++ b/tests/test_sns/test_subscriptions_boto3.py @@ -37,6 +37,7 @@ def test_creating_subscription(): subscriptions = conn.list_subscriptions()["Subscriptions"] subscriptions.should.have.length_of(0) + @mock_sns def test_deleting_subscriptions_by_deleting_topic(): conn = boto3.client('sns', region_name='us-east-1') @@ -68,6 +69,7 @@ def test_deleting_subscriptions_by_deleting_topic(): subscriptions = conn.list_subscriptions()["Subscriptions"] subscriptions.should.have.length_of(0) + @mock_sns def test_getting_subscriptions_by_topic(): conn = boto3.client('sns', region_name='us-east-1') @@ -197,3 +199,67 @@ def test_set_subscription_attributes(): AttributeName='InvalidName', AttributeValue='true' ) + + +@mock_sns +def test_check_not_opted_out(): + conn = boto3.client('sns', region_name='us-east-1') + response = conn.check_if_phone_number_is_opted_out(phoneNumber='+447428545375') + + response.should.contain('isOptedOut') + response['isOptedOut'].should.be(False) + + +@mock_sns +def test_check_opted_out(): + # Phone number ends in 99 so is hardcoded in the endpoint to return opted + # out status + conn = boto3.client('sns', region_name='us-east-1') + response = conn.check_if_phone_number_is_opted_out(phoneNumber='+447428545399') + + response.should.contain('isOptedOut') + response['isOptedOut'].should.be(True) + + +@mock_sns +def test_check_opted_out_invalid(): + conn = boto3.client('sns', region_name='us-east-1') + + # Invalid phone number + with assert_raises(ClientError): + conn.check_if_phone_number_is_opted_out(phoneNumber='+44742LALALA') + + +@mock_sns +def test_list_opted_out(): + conn = boto3.client('sns', region_name='us-east-1') + response = conn.list_phone_numbers_opted_out() + + response.should.contain('phoneNumbers') + len(response['phoneNumbers']).should.be.greater_than(0) + + +@mock_sns +def test_opt_in(): + conn = boto3.client('sns', region_name='us-east-1') + response = conn.list_phone_numbers_opted_out() + current_len = len(response['phoneNumbers']) + assert current_len > 0 + + conn.opt_in_phone_number(phoneNumber=response['phoneNumbers'][0]) + + response = conn.list_phone_numbers_opted_out() + len(response['phoneNumbers']).should.be.greater_than(0) + len(response['phoneNumbers']).should.be.lower_than(current_len) + + +@mock_sns +def test_confirm_subscription(): + conn = boto3.client('sns', region_name='us-east-1') + response = conn.create_topic(Name='testconfirm') + + conn.confirm_subscription( + TopicArn=response['TopicArn'], + Token='2336412f37fb687f5d51e6e241d59b68c4e583a5cee0be6f95bbf97ab8d2441cf47b99e848408adaadf4c197e65f03473d53c4ba398f6abbf38ce2e8ebf7b4ceceb2cd817959bcde1357e58a2861b05288c535822eb88cac3db04f592285249971efc6484194fc4a4586147f16916692', + AuthenticateOnUnsubscribe='true' + ) diff --git a/tests/test_sns/test_topics_boto3.py b/tests/test_sns/test_topics_boto3.py index 4702744c3..a9c2a2904 100644 --- a/tests/test_sns/test_topics_boto3.py +++ b/tests/test_sns/test_topics_boto3.py @@ -129,3 +129,20 @@ def test_topic_paging(): response.shouldnt.have("NextToken") topics_list.should.have.length_of(int(DEFAULT_PAGE_SIZE / 2)) + + +@mock_sns +def test_add_remove_permissions(): + conn = boto3.client('sns', region_name='us-east-1') + response = conn.create_topic(Name='testpermissions') + + conn.add_permission( + TopicArn=response['TopicArn'], + Label='Test1234', + AWSAccountId=['999999999999'], + ActionName=['AddPermission'] + ) + conn.remove_permission( + TopicArn=response['TopicArn'], + Label='Test1234' + )