diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index 972031ad2..57f169b8a 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -5866,7 +5866,7 @@ - [ ] update_job ## sns -55% implemented +58% implemented - [ ] add_permission - [ ] check_if_phone_number_is_opted_out - [ ] confirm_subscription @@ -5899,7 +5899,7 @@ - [X] subscribe - [x] tag_resource - [X] unsubscribe -- [ ] untag_resource +- [x] untag_resource ## sqs 65% implemented diff --git a/moto/sns/models.py b/moto/sns/models.py index 55b243e45..51b5c2b2c 100644 --- a/moto/sns/models.py +++ b/moto/sns/models.py @@ -521,6 +521,13 @@ class SNSBackend(BaseBackend): 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 = {} for region in Session().get_available_regions('sns'): diff --git a/moto/sns/responses.py b/moto/sns/responses.py index 3b8383d1f..c315e2a87 100644 --- a/moto/sns/responses.py +++ b/moto/sns/responses.py @@ -712,6 +712,14 @@ class SNSResponse(BaseResponse): 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 = """ @@ -1116,3 +1124,10 @@ TAG_RESOURCE_TEMPLATE = """ + + + 14eb7b1a-4cbd-5a56-80db-2d06412df769 + +""" diff --git a/tests/test_sns/test_topics_boto3.py b/tests/test_sns/test_topics_boto3.py index ace825af5..05c8f74b4 100644 --- a/tests/test_sns/test_topics_boto3.py +++ b/tests/test_sns/test_topics_boto3.py @@ -297,6 +297,52 @@ def test_tag_topic(): ]) +@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') @@ -361,3 +407,27 @@ def test_tag_resource_errors(): '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' + )