commit
e7f1ab3290
@ -5866,7 +5866,7 @@
|
|||||||
- [ ] update_job
|
- [ ] update_job
|
||||||
|
|
||||||
## sns
|
## sns
|
||||||
48% implemented
|
58% implemented
|
||||||
- [ ] add_permission
|
- [ ] add_permission
|
||||||
- [ ] check_if_phone_number_is_opted_out
|
- [ ] check_if_phone_number_is_opted_out
|
||||||
- [ ] confirm_subscription
|
- [ ] confirm_subscription
|
||||||
@ -5886,7 +5886,7 @@
|
|||||||
- [X] list_platform_applications
|
- [X] list_platform_applications
|
||||||
- [X] list_subscriptions
|
- [X] list_subscriptions
|
||||||
- [ ] list_subscriptions_by_topic
|
- [ ] list_subscriptions_by_topic
|
||||||
- [ ] list_tags_for_resource
|
- [x] list_tags_for_resource
|
||||||
- [X] list_topics
|
- [X] list_topics
|
||||||
- [ ] opt_in_phone_number
|
- [ ] opt_in_phone_number
|
||||||
- [X] publish
|
- [X] publish
|
||||||
@ -5897,9 +5897,9 @@
|
|||||||
- [X] set_subscription_attributes
|
- [X] set_subscription_attributes
|
||||||
- [ ] set_topic_attributes
|
- [ ] set_topic_attributes
|
||||||
- [X] subscribe
|
- [X] subscribe
|
||||||
- [ ] tag_resource
|
- [x] tag_resource
|
||||||
- [X] unsubscribe
|
- [X] unsubscribe
|
||||||
- [ ] untag_resource
|
- [x] untag_resource
|
||||||
|
|
||||||
## sqs
|
## sqs
|
||||||
65% implemented
|
65% implemented
|
||||||
|
@ -10,6 +10,14 @@ class SNSNotFoundError(RESTError):
|
|||||||
"NotFound", message)
|
"NotFound", message)
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceNotFoundError(RESTError):
|
||||||
|
code = 404
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(ResourceNotFoundError, self).__init__(
|
||||||
|
'ResourceNotFound', 'Resource does not exist')
|
||||||
|
|
||||||
|
|
||||||
class DuplicateSnsEndpointError(RESTError):
|
class DuplicateSnsEndpointError(RESTError):
|
||||||
code = 400
|
code = 400
|
||||||
|
|
||||||
@ -42,6 +50,14 @@ class InvalidParameterValue(RESTError):
|
|||||||
"InvalidParameterValue", message)
|
"InvalidParameterValue", message)
|
||||||
|
|
||||||
|
|
||||||
|
class TagLimitExceededError(RESTError):
|
||||||
|
code = 400
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(TagLimitExceededError, self).__init__(
|
||||||
|
'TagLimitExceeded', 'Could not complete request: tag quota of per resource exceeded')
|
||||||
|
|
||||||
|
|
||||||
class InternalError(RESTError):
|
class InternalError(RESTError):
|
||||||
code = 500
|
code = 500
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ from moto.awslambda import lambda_backends
|
|||||||
|
|
||||||
from .exceptions import (
|
from .exceptions import (
|
||||||
SNSNotFoundError, DuplicateSnsEndpointError, SnsEndpointDisabled, SNSInvalidParameter,
|
SNSNotFoundError, DuplicateSnsEndpointError, SnsEndpointDisabled, SNSInvalidParameter,
|
||||||
InvalidParameterValue, InternalError
|
InvalidParameterValue, InternalError, ResourceNotFoundError, TagLimitExceededError
|
||||||
)
|
)
|
||||||
from .utils import make_arn_for_topic, make_arn_for_subscription
|
from .utils import make_arn_for_topic, make_arn_for_subscription
|
||||||
|
|
||||||
@ -44,6 +44,8 @@ class Topic(BaseModel):
|
|||||||
self.subscriptions_confimed = 0
|
self.subscriptions_confimed = 0
|
||||||
self.subscriptions_deleted = 0
|
self.subscriptions_deleted = 0
|
||||||
|
|
||||||
|
self._tags = {}
|
||||||
|
|
||||||
def publish(self, message, subject=None, message_attributes=None):
|
def publish(self, message, subject=None, message_attributes=None):
|
||||||
message_id = six.text_type(uuid.uuid4())
|
message_id = six.text_type(uuid.uuid4())
|
||||||
subscriptions, _ = self.sns_backend.list_subscriptions(self.arn)
|
subscriptions, _ = self.sns_backend.list_subscriptions(self.arn)
|
||||||
@ -277,7 +279,7 @@ class SNSBackend(BaseBackend):
|
|||||||
def update_sms_attributes(self, attrs):
|
def update_sms_attributes(self, attrs):
|
||||||
self.sms_attributes.update(attrs)
|
self.sms_attributes.update(attrs)
|
||||||
|
|
||||||
def create_topic(self, name, attributes=None):
|
def create_topic(self, name, attributes=None, tags=None):
|
||||||
fails_constraints = not re.match(r'^[a-zA-Z0-9_-]{1,256}$', name)
|
fails_constraints = not re.match(r'^[a-zA-Z0-9_-]{1,256}$', name)
|
||||||
if fails_constraints:
|
if fails_constraints:
|
||||||
raise InvalidParameterValue("Topic names must be made up of only uppercase and lowercase ASCII letters, numbers, underscores, and hyphens, and must be between 1 and 256 characters long.")
|
raise InvalidParameterValue("Topic names must be made up of only uppercase and lowercase ASCII letters, numbers, underscores, and hyphens, and must be between 1 and 256 characters long.")
|
||||||
@ -285,6 +287,8 @@ class SNSBackend(BaseBackend):
|
|||||||
if attributes:
|
if attributes:
|
||||||
for attribute in attributes:
|
for attribute in attributes:
|
||||||
setattr(candidate_topic, camelcase_to_underscores(attribute), attributes[attribute])
|
setattr(candidate_topic, camelcase_to_underscores(attribute), attributes[attribute])
|
||||||
|
if tags:
|
||||||
|
candidate_topic._tags = tags
|
||||||
if candidate_topic.arn in self.topics:
|
if candidate_topic.arn in self.topics:
|
||||||
return self.topics[candidate_topic.arn]
|
return self.topics[candidate_topic.arn]
|
||||||
else:
|
else:
|
||||||
@ -499,6 +503,31 @@ class SNSBackend(BaseBackend):
|
|||||||
|
|
||||||
raise SNSInvalidParameter("Invalid parameter: FilterPolicy: Match value must be String, number, true, false, or null")
|
raise SNSInvalidParameter("Invalid parameter: FilterPolicy: Match value must be String, number, true, false, or null")
|
||||||
|
|
||||||
|
def list_tags_for_resource(self, resource_arn):
|
||||||
|
if resource_arn not in self.topics:
|
||||||
|
raise ResourceNotFoundError
|
||||||
|
|
||||||
|
return self.topics[resource_arn]._tags
|
||||||
|
|
||||||
|
def tag_resource(self, resource_arn, tags):
|
||||||
|
if resource_arn not in self.topics:
|
||||||
|
raise ResourceNotFoundError
|
||||||
|
|
||||||
|
updated_tags = self.topics[resource_arn]._tags.copy()
|
||||||
|
updated_tags.update(tags)
|
||||||
|
|
||||||
|
if len(updated_tags) > 50:
|
||||||
|
raise TagLimitExceededError
|
||||||
|
|
||||||
|
self.topics[resource_arn]._tags = updated_tags
|
||||||
|
|
||||||
|
def untag_resource(self, resource_arn, tag_keys):
|
||||||
|
if resource_arn not in self.topics:
|
||||||
|
raise ResourceNotFoundError
|
||||||
|
|
||||||
|
for key in tag_keys:
|
||||||
|
self.topics[resource_arn]._tags.pop(key, None)
|
||||||
|
|
||||||
|
|
||||||
sns_backends = {}
|
sns_backends = {}
|
||||||
for region in Session().get_available_regions('sns'):
|
for region in Session().get_available_regions('sns'):
|
||||||
|
@ -30,6 +30,10 @@ class SNSResponse(BaseResponse):
|
|||||||
in attributes
|
in attributes
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _get_tags(self):
|
||||||
|
tags = self._get_list_prefix('Tags.member')
|
||||||
|
return {tag['key']: tag['value'] for tag in tags}
|
||||||
|
|
||||||
def _parse_message_attributes(self, prefix='', value_namespace='Value.'):
|
def _parse_message_attributes(self, prefix='', value_namespace='Value.'):
|
||||||
message_attributes = self._get_object_map(
|
message_attributes = self._get_object_map(
|
||||||
'MessageAttributes.entry',
|
'MessageAttributes.entry',
|
||||||
@ -85,7 +89,8 @@ class SNSResponse(BaseResponse):
|
|||||||
def create_topic(self):
|
def create_topic(self):
|
||||||
name = self._get_param('Name')
|
name = self._get_param('Name')
|
||||||
attributes = self._get_attributes()
|
attributes = self._get_attributes()
|
||||||
topic = self.backend.create_topic(name, attributes)
|
tags = self._get_tags()
|
||||||
|
topic = self.backend.create_topic(name, attributes, tags)
|
||||||
|
|
||||||
if self.request_json:
|
if self.request_json:
|
||||||
return json.dumps({
|
return json.dumps({
|
||||||
@ -691,6 +696,30 @@ class SNSResponse(BaseResponse):
|
|||||||
template = self.response_template(CONFIRM_SUBSCRIPTION_TEMPLATE)
|
template = self.response_template(CONFIRM_SUBSCRIPTION_TEMPLATE)
|
||||||
return template.render(sub_arn='{0}:68762e72-e9b1-410a-8b3b-903da69ee1d5'.format(arn))
|
return template.render(sub_arn='{0}:68762e72-e9b1-410a-8b3b-903da69ee1d5'.format(arn))
|
||||||
|
|
||||||
|
def list_tags_for_resource(self):
|
||||||
|
arn = self._get_param('ResourceArn')
|
||||||
|
|
||||||
|
result = self.backend.list_tags_for_resource(arn)
|
||||||
|
|
||||||
|
template = self.response_template(LIST_TAGS_FOR_RESOURCE_TEMPLATE)
|
||||||
|
return template.render(tags=result)
|
||||||
|
|
||||||
|
def tag_resource(self):
|
||||||
|
arn = self._get_param('ResourceArn')
|
||||||
|
tags = self._get_tags()
|
||||||
|
|
||||||
|
self.backend.tag_resource(arn, tags)
|
||||||
|
|
||||||
|
return self.response_template(TAG_RESOURCE_TEMPLATE).render()
|
||||||
|
|
||||||
|
def untag_resource(self):
|
||||||
|
arn = self._get_param('ResourceArn')
|
||||||
|
tag_keys = self._get_multi_param('TagKeys.member')
|
||||||
|
|
||||||
|
self.backend.untag_resource(arn, tag_keys)
|
||||||
|
|
||||||
|
return self.response_template(UNTAG_RESOURCE_TEMPLATE).render()
|
||||||
|
|
||||||
|
|
||||||
CREATE_TOPIC_TEMPLATE = """<CreateTopicResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
|
CREATE_TOPIC_TEMPLATE = """<CreateTopicResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
|
||||||
<CreateTopicResult>
|
<CreateTopicResult>
|
||||||
@ -1072,3 +1101,33 @@ CONFIRM_SUBSCRIPTION_TEMPLATE = """<ConfirmSubscriptionResponse xmlns="http://sn
|
|||||||
<RequestId>16eb4dde-7b3c-5b3e-a22a-1fe2a92d3293</RequestId>
|
<RequestId>16eb4dde-7b3c-5b3e-a22a-1fe2a92d3293</RequestId>
|
||||||
</ResponseMetadata>
|
</ResponseMetadata>
|
||||||
</ConfirmSubscriptionResponse>"""
|
</ConfirmSubscriptionResponse>"""
|
||||||
|
|
||||||
|
LIST_TAGS_FOR_RESOURCE_TEMPLATE = """<ListTagsForResourceResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
|
||||||
|
<ListTagsForResourceResult>
|
||||||
|
<Tags>
|
||||||
|
{% for name, value in tags.items() %}
|
||||||
|
<member>
|
||||||
|
<Key>{{ name }}</Key>
|
||||||
|
<Value>{{ value }}</Value>
|
||||||
|
</member>
|
||||||
|
{% endfor %}
|
||||||
|
</Tags>
|
||||||
|
</ListTagsForResourceResult>
|
||||||
|
<ResponseMetadata>
|
||||||
|
<RequestId>97fa763f-861b-5223-a946-20251f2a42e2</RequestId>
|
||||||
|
</ResponseMetadata>
|
||||||
|
</ListTagsForResourceResponse>"""
|
||||||
|
|
||||||
|
TAG_RESOURCE_TEMPLATE = """<TagResourceResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
|
||||||
|
<TagResourceResult/>
|
||||||
|
<ResponseMetadata>
|
||||||
|
<RequestId>fd4ab1da-692f-50a7-95ad-e7c665877d98</RequestId>
|
||||||
|
</ResponseMetadata>
|
||||||
|
</TagResourceResponse>"""
|
||||||
|
|
||||||
|
UNTAG_RESOURCE_TEMPLATE = """<UntagResourceResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
|
||||||
|
<UntagResourceResult/>
|
||||||
|
<ResponseMetadata>
|
||||||
|
<RequestId>14eb7b1a-4cbd-5a56-80db-2d06412df769</RequestId>
|
||||||
|
</ResponseMetadata>
|
||||||
|
</UntagResourceResponse>"""
|
||||||
|
@ -44,6 +44,36 @@ def test_create_topic_with_attributes():
|
|||||||
attributes['DisplayName'].should.equal('test-topic')
|
attributes['DisplayName'].should.equal('test-topic')
|
||||||
|
|
||||||
|
|
||||||
|
@mock_sns
|
||||||
|
def test_create_topic_with_tags():
|
||||||
|
conn = boto3.client("sns", region_name="us-east-1")
|
||||||
|
response = conn.create_topic(
|
||||||
|
Name='some-topic-with-tags',
|
||||||
|
Tags=[
|
||||||
|
{
|
||||||
|
'Key': 'tag_key_1',
|
||||||
|
'Value': 'tag_value_1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'Key': 'tag_key_2',
|
||||||
|
'Value': 'tag_value_2'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
topic_arn = response['TopicArn']
|
||||||
|
|
||||||
|
conn.list_tags_for_resource(ResourceArn=topic_arn)['Tags'].should.equal([
|
||||||
|
{
|
||||||
|
'Key': 'tag_key_1',
|
||||||
|
'Value': 'tag_value_1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'Key': 'tag_key_2',
|
||||||
|
'Value': 'tag_value_2'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
@mock_sns
|
@mock_sns
|
||||||
def test_create_topic_should_be_indempodent():
|
def test_create_topic_should_be_indempodent():
|
||||||
conn = boto3.client("sns", region_name="us-east-1")
|
conn = boto3.client("sns", region_name="us-east-1")
|
||||||
@ -200,3 +230,204 @@ def test_add_remove_permissions():
|
|||||||
TopicArn=response['TopicArn'],
|
TopicArn=response['TopicArn'],
|
||||||
Label='Test1234'
|
Label='Test1234'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_sns
|
||||||
|
def test_tag_topic():
|
||||||
|
conn = boto3.client('sns', region_name='us-east-1')
|
||||||
|
response = conn.create_topic(
|
||||||
|
Name = 'some-topic-with-tags'
|
||||||
|
)
|
||||||
|
topic_arn = response['TopicArn']
|
||||||
|
|
||||||
|
conn.tag_resource(
|
||||||
|
ResourceArn=topic_arn,
|
||||||
|
Tags=[
|
||||||
|
{
|
||||||
|
'Key': 'tag_key_1',
|
||||||
|
'Value': 'tag_value_1'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
conn.list_tags_for_resource(ResourceArn = topic_arn)['Tags'].should.equal([
|
||||||
|
{
|
||||||
|
'Key': 'tag_key_1',
|
||||||
|
'Value': 'tag_value_1'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
conn.tag_resource(
|
||||||
|
ResourceArn=topic_arn,
|
||||||
|
Tags=[
|
||||||
|
{
|
||||||
|
'Key': 'tag_key_2',
|
||||||
|
'Value': 'tag_value_2'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
conn.list_tags_for_resource(ResourceArn = topic_arn)['Tags'].should.equal([
|
||||||
|
{
|
||||||
|
'Key': 'tag_key_1',
|
||||||
|
'Value': 'tag_value_1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'Key': 'tag_key_2',
|
||||||
|
'Value': 'tag_value_2'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
conn.tag_resource(
|
||||||
|
ResourceArn = topic_arn,
|
||||||
|
Tags = [
|
||||||
|
{
|
||||||
|
'Key': 'tag_key_1',
|
||||||
|
'Value': 'tag_value_X'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
conn.list_tags_for_resource(ResourceArn = topic_arn)['Tags'].should.equal([
|
||||||
|
{
|
||||||
|
'Key': 'tag_key_1',
|
||||||
|
'Value': 'tag_value_X'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'Key': 'tag_key_2',
|
||||||
|
'Value': 'tag_value_2'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
@mock_sns
|
||||||
|
def test_untag_topic():
|
||||||
|
conn = boto3.client('sns', region_name = 'us-east-1')
|
||||||
|
response = conn.create_topic(
|
||||||
|
Name = 'some-topic-with-tags',
|
||||||
|
Tags = [
|
||||||
|
{
|
||||||
|
'Key': 'tag_key_1',
|
||||||
|
'Value': 'tag_value_1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'Key': 'tag_key_2',
|
||||||
|
'Value': 'tag_value_2'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
topic_arn = response['TopicArn']
|
||||||
|
|
||||||
|
conn.untag_resource(
|
||||||
|
ResourceArn = topic_arn,
|
||||||
|
TagKeys = [
|
||||||
|
'tag_key_1'
|
||||||
|
]
|
||||||
|
)
|
||||||
|
conn.list_tags_for_resource(ResourceArn = topic_arn)['Tags'].should.equal([
|
||||||
|
{
|
||||||
|
'Key': 'tag_key_2',
|
||||||
|
'Value': 'tag_value_2'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
# removing a non existing tag should not raise any error
|
||||||
|
conn.untag_resource(
|
||||||
|
ResourceArn = topic_arn,
|
||||||
|
TagKeys = [
|
||||||
|
'not-existing-tag'
|
||||||
|
]
|
||||||
|
)
|
||||||
|
conn.list_tags_for_resource(ResourceArn = topic_arn)['Tags'].should.equal([
|
||||||
|
{
|
||||||
|
'Key': 'tag_key_2',
|
||||||
|
'Value': 'tag_value_2'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
@mock_sns
|
||||||
|
def test_list_tags_for_resource_error():
|
||||||
|
conn = boto3.client('sns', region_name = 'us-east-1')
|
||||||
|
conn.create_topic(
|
||||||
|
Name = 'some-topic-with-tags',
|
||||||
|
Tags = [
|
||||||
|
{
|
||||||
|
'Key': 'tag_key_1',
|
||||||
|
'Value': 'tag_value_X'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
conn.list_tags_for_resource.when.called_with(
|
||||||
|
ResourceArn = 'not-existing-topic'
|
||||||
|
).should.throw(
|
||||||
|
ClientError,
|
||||||
|
'Resource does not exist'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_sns
|
||||||
|
def test_tag_resource_errors():
|
||||||
|
conn = boto3.client('sns', region_name = 'us-east-1')
|
||||||
|
response = conn.create_topic(
|
||||||
|
Name = 'some-topic-with-tags',
|
||||||
|
Tags = [
|
||||||
|
{
|
||||||
|
'Key': 'tag_key_1',
|
||||||
|
'Value': 'tag_value_X'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
topic_arn = response['TopicArn']
|
||||||
|
|
||||||
|
conn.tag_resource.when.called_with(
|
||||||
|
ResourceArn = 'not-existing-topic',
|
||||||
|
Tags = [
|
||||||
|
{
|
||||||
|
'Key': 'tag_key_1',
|
||||||
|
'Value': 'tag_value_1'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
).should.throw(
|
||||||
|
ClientError,
|
||||||
|
'Resource does not exist'
|
||||||
|
)
|
||||||
|
|
||||||
|
too_many_tags = [{'Key': 'tag_key_{}'.format(i), 'Value': 'tag_value_{}'.format(i)} for i in range(51)]
|
||||||
|
conn.tag_resource.when.called_with(
|
||||||
|
ResourceArn = topic_arn,
|
||||||
|
Tags = too_many_tags
|
||||||
|
).should.throw(
|
||||||
|
ClientError,
|
||||||
|
'Could not complete request: tag quota of per resource exceeded'
|
||||||
|
)
|
||||||
|
|
||||||
|
# when the request fails, the tags should not be updated
|
||||||
|
conn.list_tags_for_resource(ResourceArn = topic_arn)['Tags'].should.equal([
|
||||||
|
{
|
||||||
|
'Key': 'tag_key_1',
|
||||||
|
'Value': 'tag_value_X'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
@mock_sns
|
||||||
|
def test_untag_resource_error():
|
||||||
|
conn = boto3.client('sns', region_name = 'us-east-1')
|
||||||
|
conn.create_topic(
|
||||||
|
Name = 'some-topic-with-tags',
|
||||||
|
Tags = [
|
||||||
|
{
|
||||||
|
'Key': 'tag_key_1',
|
||||||
|
'Value': 'tag_value_X'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
conn.untag_resource.when.called_with(
|
||||||
|
ResourceArn = 'not-existing-topic',
|
||||||
|
TagKeys = [
|
||||||
|
'tag_key_1'
|
||||||
|
]
|
||||||
|
).should.throw(
|
||||||
|
ClientError,
|
||||||
|
'Resource does not exist'
|
||||||
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user