From c8aaf0783575996524c9946a19dbdec8d94b6710 Mon Sep 17 00:00:00 2001 From: Benjamin Simon Date: Mon, 1 Aug 2022 17:31:36 +0200 Subject: [PATCH 1/2] fix sns duplicated tags + validate against AWS --- localstack/services/sns/provider.py | 7 ++- tests/integration/test_sns.py | 66 ++++++++++++++---------- tests/integration/test_sns.snapshot.json | 56 ++++++++++++++++++++ 3 files changed, 101 insertions(+), 28 deletions(-) diff --git a/localstack/services/sns/provider.py b/localstack/services/sns/provider.py index 973758b5c5630..eb38f85297475 100644 --- a/localstack/services/sns/provider.py +++ b/localstack/services/sns/provider.py @@ -773,10 +773,15 @@ def tag_resource( self, context: RequestContext, resource_arn: AmazonResourceName, tags: TagList ) -> TagResourceResponse: # TODO: can this be used to tag any resource when using AWS? + # each tag key must be unique + # https://docs.aws.amazon.com/general/latest/gr/aws_tagging.html#tag-best-practices + unique_tag_keys = {tag["Key"] for tag in tags} + if len(unique_tag_keys) < len(tags): + raise InvalidParameterException("Invalid parameter: Duplicated keys are not allowed.") + call_moto(context) sns_backend = SNSBackend.get() existing_tags = sns_backend.sns_tags.get(resource_arn, []) - tags = [tag for idx, tag in enumerate(tags) if tag not in tags[:idx]] def existing_tag_index(item): for idx, tag in enumerate(existing_tags): diff --git a/tests/integration/test_sns.py b/tests/integration/test_sns.py index bf9229a19f18e..8853e20c45c26 100644 --- a/tests/integration/test_sns.py +++ b/tests/integration/test_sns.py @@ -3,6 +3,7 @@ import queue import random from base64 import b64encode +from operator import itemgetter import pytest import requests @@ -25,6 +26,7 @@ from .awslambda.functions import lambda_integration from .awslambda.test_lambda import ( LAMBDA_RUNTIME_PYTHON36, + LAMBDA_RUNTIME_PYTHON37, TEST_LAMBDA_FUNCTION_PREFIX, TEST_LAMBDA_LIBS, TEST_LAMBDA_PYTHON, @@ -36,6 +38,7 @@ class TestSNSSubscription: + @pytest.mark.aws_validated def test_python_lambda_subscribe_sns_topic( self, create_lambda_function, @@ -56,7 +59,7 @@ def test_python_lambda_subscribe_sns_topic( lambda_creation_response = create_lambda_function( func_name=function_name, handler_file=TEST_LAMBDA_PYTHON_ECHO, - runtime=LAMBDA_RUNTIME_PYTHON36, + runtime=LAMBDA_RUNTIME_PYTHON37, role=lambda_su_role, ) lambda_arn = lambda_creation_response["CreateFunctionResponse"]["FunctionArn"] @@ -125,6 +128,7 @@ def test_publish_unicode_chars( msg_received = msg_received["Message"] assert message == msg_received + @pytest.mark.aws_validated def test_subscribe_with_invalid_protocol(self, sns_client, sns_create_topic, sns_subscription): topic_arn = sns_create_topic()["TopicArn"] @@ -398,6 +402,7 @@ def test_subscribe_sqs_queue( message_body = json.loads(message["Body"]) assert message_body["MessageAttributes"]["attr1"]["Value"] == "99.12" + @pytest.mark.only_localstack def test_subscribe_platform_endpoint( self, sns_client, sqs_create_queue, sns_create_topic, sns_subscription ): @@ -439,8 +444,13 @@ def check_message(): sns_client.delete_endpoint(EndpointArn=platform_arn) sns_client.delete_platform_application(PlatformApplicationArn=app_arn) - def test_unknown_topic_publish(self, sns_client): - fake_arn = "arn:aws:sns:us-east-1:123456789012:i_dont_exist" + @pytest.mark.aws_validated + def test_unknown_topic_publish(self, sns_client, sns_create_topic): + # create topic to get the basic arn structure + # otherwise you get InvalidClientTokenId exception because of account id + topic_arn = sns_create_topic()["TopicArn"] + # append to get an unknown topic + fake_arn = f"{topic_arn}-fake" message = "This is a test message" with pytest.raises(ClientError) as e: @@ -450,6 +460,7 @@ def test_unknown_topic_publish(self, sns_client): assert e.value.response["Error"]["Message"] == "Topic does not exist" assert e.value.response["ResponseMetadata"]["HTTPStatusCode"] == 404 + @pytest.mark.only_localstack def test_publish_sms(self, sns_client): response = sns_client.publish(PhoneNumber="+33000000000", Message="This is a SMS") assert "MessageId" in response @@ -464,43 +475,44 @@ def test_publish_non_existent_target(self, sns_client): assert ex.value.response["Error"]["Code"] == "InvalidClientTokenId" - def test_tags(self, sns_client, sns_create_topic): + # todo: the message key is added to the error response body, but not in AWS + # check with serializer? + @pytest.mark.skip_snapshot_verify(paths=["$..message"]) + def test_tags(self, sns_client, sns_create_topic, snapshot): topic_arn = sns_create_topic()["TopicArn"] + with pytest.raises(ClientError) as exc: + sns_client.tag_resource( + ResourceArn=topic_arn, + Tags=[ + {"Key": "k1", "Value": "v1"}, + {"Key": "k2", "Value": "v2"}, + {"Key": "k2", "Value": "v2"}, + ], + ) + snapshot.match("duplicate-key-error", exc.value.response) + sns_client.tag_resource( ResourceArn=topic_arn, Tags=[ - {"Key": "123", "Value": "abc"}, - {"Key": "456", "Value": "def"}, - {"Key": "456", "Value": "def"}, + {"Key": "k1", "Value": "v1"}, + {"Key": "k2", "Value": "v2"}, ], ) tags = sns_client.list_tags_for_resource(ResourceArn=topic_arn) - distinct_tags = [ - tag for idx, tag in enumerate(tags["Tags"]) if tag not in tags["Tags"][:idx] - ] - # test for duplicate tags - assert len(tags["Tags"]) == len(distinct_tags) - assert len(tags["Tags"]) == 2 - assert tags["Tags"][0]["Key"] == "123" - assert tags["Tags"][0]["Value"] == "abc" - assert tags["Tags"][1]["Key"] == "456" - assert tags["Tags"][1]["Value"] == "def" - - sns_client.untag_resource(ResourceArn=topic_arn, TagKeys=["123"]) + # could not figure out the logic for tag order in AWS, so resorting to sorting it manually in place + tags["Tags"].sort(key=itemgetter("Key")) + snapshot.match("list-created-tags", tags) + sns_client.untag_resource(ResourceArn=topic_arn, TagKeys=["k1"]) tags = sns_client.list_tags_for_resource(ResourceArn=topic_arn) - assert len(tags["Tags"]) == 1 - assert tags["Tags"][0]["Key"] == "456" - assert tags["Tags"][0]["Value"] == "def" - - sns_client.tag_resource(ResourceArn=topic_arn, Tags=[{"Key": "456", "Value": "pqr"}]) + snapshot.match("list-after-delete-tags", tags) + # test update tag + sns_client.tag_resource(ResourceArn=topic_arn, Tags=[{"Key": "k2", "Value": "v2b"}]) tags = sns_client.list_tags_for_resource(ResourceArn=topic_arn) - assert len(tags["Tags"]) == 1 - assert tags["Tags"][0]["Key"] == "456" - assert tags["Tags"][0]["Value"] == "pqr" + snapshot.match("list-after-update-tags", tags) def test_topic_subscription(self, sns_client, sns_create_topic, sns_subscription): topic_arn = sns_create_topic()["TopicArn"] diff --git a/tests/integration/test_sns.snapshot.json b/tests/integration/test_sns.snapshot.json index f17af59c6802e..3dae389aaaa4d 100644 --- a/tests/integration/test_sns.snapshot.json +++ b/tests/integration/test_sns.snapshot.json @@ -56,5 +56,61 @@ } } } + }, + "tests/integration/test_sns.py::TestSNSProvider::test_tags": { + "recorded-date": "01-08-2022, 17:10:09", + "recorded-content": { + "duplicate-key-error": { + "Error": { + "Type": "Sender", + "Code": "InvalidParameter", + "Message": "Invalid parameter: Duplicated keys are not allowed." + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "list-created-tags": { + "Tags": [ + { + "Key": "k1", + "Value": "v1" + }, + { + "Key": "k2", + "Value": "v2" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-after-delete-tags": { + "Tags": [ + { + "Key": "k2", + "Value": "v2" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-after-update-tags": { + "Tags": [ + { + "Key": "k2", + "Value": "v2b" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } From 6370d1ffb583171a43153a4d41821d75495dced5 Mon Sep 17 00:00:00 2001 From: Benjamin Simon Date: Mon, 1 Aug 2022 17:39:01 +0200 Subject: [PATCH 2/2] add aws_validated marker --- tests/integration/test_sns.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/test_sns.py b/tests/integration/test_sns.py index 8853e20c45c26..5dc1ffeadcf51 100644 --- a/tests/integration/test_sns.py +++ b/tests/integration/test_sns.py @@ -477,6 +477,7 @@ def test_publish_non_existent_target(self, sns_client): # todo: the message key is added to the error response body, but not in AWS # check with serializer? + @pytest.mark.aws_validated @pytest.mark.skip_snapshot_verify(paths=["$..message"]) def test_tags(self, sns_client, sns_create_topic, snapshot):