Skip to content
This repository was archived by the owner on Mar 23, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 61 additions & 1 deletion localstack-core/localstack/services/sns/v2/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from localstack.aws.api import RequestContext
from localstack.aws.api.sns import (
ConfirmSubscriptionResponse,
AmazonResourceName,
CreateTopicResponse,
GetSMSAttributesResponse,
GetSubscriptionAttributesResponse,
Expand All @@ -17,6 +18,7 @@
ListString,
ListSubscriptionsByTopicResponse,
ListSubscriptionsResponse,
ListTagsForResourceResponse,
ListTopicsResponse,
MapStringToString,
NotFoundException,
Expand All @@ -26,8 +28,11 @@
SubscribeResponse,
Subscription,
SubscriptionAttributesMap,
TagKeyList,
TagList,
TagResourceResponse,
TopicAttributesMap,
UntagResourceResponse,
attributeName,
attributeValue,
authenticateOnUnsubscribe,
Expand Down Expand Up @@ -102,6 +107,11 @@ def create_topic(
if not attrs.get(k) or not attrs.get(k) == v:
# TODO:
raise InvalidParameterException("Fix this Exception message and type")
tag_resource_success = _check_matching_tags(topic_arn, tags, store)
if not tag_resource_success:
raise InvalidParameterException(
"Invalid parameter: Tags Reason: Topic already exists with different tags"
)
return CreateTopicResponse(TopicArn=topic_arn)

attributes = attributes or {}
Expand All @@ -121,7 +131,8 @@ def create_topic(
raise InvalidParameterException("Invalid parameter: Topic Name")

topic = _create_topic(name=name, attributes=attributes, context=context)
# todo: tags
if tags:
self.tag_resource(context=context, resource_arn=topic_arn, tags=tags)

store.topics[topic_arn] = topic

Expand Down Expand Up @@ -546,6 +557,34 @@ def get_sms_attributes(

return GetSMSAttributesResponse(attributes=return_attributes)

def list_tags_for_resource(
self, context: RequestContext, resource_arn: AmazonResourceName, **kwargs
) -> ListTagsForResourceResponse:
store = sns_stores[context.account_id][context.region]
tags = store.TAGS.list_tags_for_resource(resource_arn)
return ListTagsForResourceResponse(Tags=tags.get("Tags"))

def tag_resource(
self, context: RequestContext, resource_arn: AmazonResourceName, tags: TagList, **kwargs
) -> TagResourceResponse:
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.")
store = sns_stores[context.account_id][context.region]
store.TAGS.tag_resource(resource_arn, tags)
return TagResourceResponse()

def untag_resource(
self,
context: RequestContext,
resource_arn: AmazonResourceName,
tag_keys: TagKeyList,
**kwargs,
) -> UntagResourceResponse:
store = sns_stores[context.account_id][context.region]
store.TAGS.untag_resource(resource_arn, tag_keys)
return UntagResourceResponse()

@staticmethod
def get_store(account_id: str, region: str) -> SnsStore:
return sns_stores[account_id][region]
Expand Down Expand Up @@ -649,3 +688,24 @@ def _validate_sms_attributes(attributes: dict) -> None:
def _set_sms_attribute_default(store: SnsStore) -> None:
# TODO: don't call this on every sms attribute crud api call
store.sms_attributes.setdefault("MonthlySpendLimit", "1")


def _check_matching_tags(topic_arn: str, tags: TagList | None, store: SnsStore) -> bool:
"""
Checks if a topic to be created doesn't already exist with different tags
:param topic_arn: Arn of the topic
:param tags: Tags to be checked
:param store: Store object that holds the topics and tags
:return: False if there is a mismatch in tags, True otherwise
"""
existing_tags = store.TAGS.list_tags_for_resource(topic_arn)["Tags"]
# if this is none there is nothing to check
if topic_arn in store.topics:
if tags is None:
tags = []
for tag in tags:
# this means topic already created with empty tags and when we try to create it
# again with other tag value then it should fail according to aws documentation.
if existing_tags is not None and tag not in existing_tags:
return False
return True
4 changes: 0 additions & 4 deletions tests/aws/services/sns/test_sns.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ def test_create_topic_with_attributes(self, sns_create_topic, snapshot, aws_clie
snapshot.match("get-attrs-malformed-topic", e.value.response)

@markers.aws.validated
@skip_if_sns_v2
def test_tags(self, sns_create_topic, snapshot, aws_client):
topic_arn = sns_create_topic()["TopicArn"]
with pytest.raises(ClientError) as exc:
Expand Down Expand Up @@ -206,7 +205,6 @@ def test_delete_topic_idempotency(self, sns_create_topic, aws_client, snapshot):
snapshot.match("delete-topic-again", delete_topic)

@markers.aws.validated
@skip_if_sns_v2
def test_create_duplicate_topic_with_more_tags(self, sns_create_topic, snapshot, aws_client):
topic_name = "test-duplicated-topic-more-tags"
sns_create_topic(Name=topic_name)
Expand All @@ -217,7 +215,6 @@ def test_create_duplicate_topic_with_more_tags(self, sns_create_topic, snapshot,
snapshot.match("exception-duplicate", e.value.response)

@markers.aws.validated
@skip_if_sns_v2
def test_create_duplicate_topic_check_idempotency(self, sns_create_topic, snapshot):
topic_name = f"test-{short_uid()}"
tags = [{"Key": "a", "Value": "1"}, {"Key": "b", "Value": "2"}]
Expand All @@ -237,7 +234,6 @@ def test_create_duplicate_topic_check_idempotency(self, sns_create_topic, snapsh
snapshot.match(f"response-same-arn-{index}", response)

@markers.aws.validated
@skip_if_sns_v2
def test_create_topic_after_delete_with_new_tags(self, sns_create_topic, snapshot, aws_client):
topic_name = f"test-{short_uid()}"
topic = sns_create_topic(Name=topic_name, Tags=[{"Key": "Name", "Value": "pqr"}])
Expand Down
8 changes: 4 additions & 4 deletions tests/aws/services/sns/test_sns.snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
}
},
"tests/aws/services/sns/test_sns.py::TestSNSTopicCrud::test_tags": {
"recorded-date": "24-08-2023, 22:30:44",
"recorded-date": "13-10-2025, 06:50:09",
"recorded-content": {
"duplicate-key-error": {
"Error": {
Expand Down Expand Up @@ -235,7 +235,7 @@
}
},
"tests/aws/services/sns/test_sns.py::TestSNSTopicCrud::test_create_duplicate_topic_with_more_tags": {
"recorded-date": "24-08-2023, 22:30:46",
"recorded-date": "13-10-2025, 06:53:55",
"recorded-content": {
"exception-duplicate": {
"Error": {
Expand All @@ -251,7 +251,7 @@
}
},
"tests/aws/services/sns/test_sns.py::TestSNSTopicCrud::test_create_duplicate_topic_check_idempotency": {
"recorded-date": "24-08-2023, 22:30:47",
"recorded-date": "13-10-2025, 07:07:09",
"recorded-content": {
"response-created": {
"TopicArn": "arn:<partition>:sns:<region>:111111111111:<resource:1>",
Expand Down Expand Up @@ -284,7 +284,7 @@
}
},
"tests/aws/services/sns/test_sns.py::TestSNSTopicCrud::test_create_topic_after_delete_with_new_tags": {
"recorded-date": "24-08-2023, 22:30:48",
"recorded-date": "13-10-2025, 07:59:08",
"recorded-content": {
"topic-0": {
"TopicArn": "arn:<partition>:sns:<region>:111111111111:<resource:1>",
Expand Down
32 changes: 28 additions & 4 deletions tests/aws/services/sns/test_sns.validation.json
Original file line number Diff line number Diff line change
Expand Up @@ -564,13 +564,31 @@
}
},
"tests/aws/services/sns/test_sns.py::TestSNSTopicCrud::test_create_duplicate_topic_check_idempotency": {
"last_validated_date": "2023-08-24T20:30:47+00:00"
"last_validated_date": "2025-10-13T07:07:09+00:00",
"durations_in_seconds": {
"setup": 1.26,
"call": 2.02,
"teardown": 1.08,
"total": 4.36
}
},
"tests/aws/services/sns/test_sns.py::TestSNSTopicCrud::test_create_duplicate_topic_with_more_tags": {
"last_validated_date": "2023-08-24T20:30:46+00:00"
"last_validated_date": "2025-10-13T06:53:55+00:00",
"durations_in_seconds": {
"setup": 1.09,
"call": 1.57,
"teardown": 0.31,
"total": 2.97
}
},
"tests/aws/services/sns/test_sns.py::TestSNSTopicCrud::test_create_topic_after_delete_with_new_tags": {
"last_validated_date": "2023-08-24T20:30:48+00:00"
"last_validated_date": "2025-10-13T07:59:08+00:00",
"durations_in_seconds": {
"setup": 0.88,
"call": 1.32,
"teardown": 0.58,
"total": 2.78
}
},
"tests/aws/services/sns/test_sns.py::TestSNSTopicCrud::test_create_topic_test_arn": {
"last_validated_date": "2025-09-29T09:32:56+00:00",
Expand Down Expand Up @@ -600,7 +618,13 @@
}
},
"tests/aws/services/sns/test_sns.py::TestSNSTopicCrud::test_tags": {
"last_validated_date": "2023-08-24T20:30:44+00:00"
"last_validated_date": "2025-10-13T06:50:09+00:00",
"durations_in_seconds": {
"setup": 0.86,
"call": 2.28,
"teardown": 0.34,
"total": 3.48
}
},
"tests/aws/services/sns/test_sns.py::TestSNSTopicCrud::test_topic_delivery_policy_crud": {
"last_validated_date": "2024-10-03T21:46:17+00:00"
Expand Down
Loading