Skip to content
This repository was archived by the owner on Mar 23, 2026. It is now read-only.

Commit 7528267

Browse files
authored
SNS: v2 tagging (#13254)
1 parent 2a0bcdb commit 7528267

4 files changed

Lines changed: 93 additions & 13 deletions

File tree

localstack-core/localstack/services/sns/v2/provider.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from localstack.aws.api import RequestContext
1010
from localstack.aws.api.sns import (
1111
ConfirmSubscriptionResponse,
12+
AmazonResourceName,
1213
CreateTopicResponse,
1314
GetSMSAttributesResponse,
1415
GetSubscriptionAttributesResponse,
@@ -17,6 +18,7 @@
1718
ListString,
1819
ListSubscriptionsByTopicResponse,
1920
ListSubscriptionsResponse,
21+
ListTagsForResourceResponse,
2022
ListTopicsResponse,
2123
MapStringToString,
2224
NotFoundException,
@@ -26,8 +28,11 @@
2628
SubscribeResponse,
2729
Subscription,
2830
SubscriptionAttributesMap,
31+
TagKeyList,
2932
TagList,
33+
TagResourceResponse,
3034
TopicAttributesMap,
35+
UntagResourceResponse,
3136
attributeName,
3237
attributeValue,
3338
authenticateOnUnsubscribe,
@@ -102,6 +107,11 @@ def create_topic(
102107
if not attrs.get(k) or not attrs.get(k) == v:
103108
# TODO:
104109
raise InvalidParameterException("Fix this Exception message and type")
110+
tag_resource_success = _check_matching_tags(topic_arn, tags, store)
111+
if not tag_resource_success:
112+
raise InvalidParameterException(
113+
"Invalid parameter: Tags Reason: Topic already exists with different tags"
114+
)
105115
return CreateTopicResponse(TopicArn=topic_arn)
106116

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

123133
topic = _create_topic(name=name, attributes=attributes, context=context)
124-
# todo: tags
134+
if tags:
135+
self.tag_resource(context=context, resource_arn=topic_arn, tags=tags)
125136

126137
store.topics[topic_arn] = topic
127138

@@ -546,6 +557,34 @@ def get_sms_attributes(
546557

547558
return GetSMSAttributesResponse(attributes=return_attributes)
548559

560+
def list_tags_for_resource(
561+
self, context: RequestContext, resource_arn: AmazonResourceName, **kwargs
562+
) -> ListTagsForResourceResponse:
563+
store = sns_stores[context.account_id][context.region]
564+
tags = store.TAGS.list_tags_for_resource(resource_arn)
565+
return ListTagsForResourceResponse(Tags=tags.get("Tags"))
566+
567+
def tag_resource(
568+
self, context: RequestContext, resource_arn: AmazonResourceName, tags: TagList, **kwargs
569+
) -> TagResourceResponse:
570+
unique_tag_keys = {tag["Key"] for tag in tags}
571+
if len(unique_tag_keys) < len(tags):
572+
raise InvalidParameterException("Invalid parameter: Duplicated keys are not allowed.")
573+
store = sns_stores[context.account_id][context.region]
574+
store.TAGS.tag_resource(resource_arn, tags)
575+
return TagResourceResponse()
576+
577+
def untag_resource(
578+
self,
579+
context: RequestContext,
580+
resource_arn: AmazonResourceName,
581+
tag_keys: TagKeyList,
582+
**kwargs,
583+
) -> UntagResourceResponse:
584+
store = sns_stores[context.account_id][context.region]
585+
store.TAGS.untag_resource(resource_arn, tag_keys)
586+
return UntagResourceResponse()
587+
549588
@staticmethod
550589
def get_store(account_id: str, region: str) -> SnsStore:
551590
return sns_stores[account_id][region]
@@ -649,3 +688,24 @@ def _validate_sms_attributes(attributes: dict) -> None:
649688
def _set_sms_attribute_default(store: SnsStore) -> None:
650689
# TODO: don't call this on every sms attribute crud api call
651690
store.sms_attributes.setdefault("MonthlySpendLimit", "1")
691+
692+
693+
def _check_matching_tags(topic_arn: str, tags: TagList | None, store: SnsStore) -> bool:
694+
"""
695+
Checks if a topic to be created doesn't already exist with different tags
696+
:param topic_arn: Arn of the topic
697+
:param tags: Tags to be checked
698+
:param store: Store object that holds the topics and tags
699+
:return: False if there is a mismatch in tags, True otherwise
700+
"""
701+
existing_tags = store.TAGS.list_tags_for_resource(topic_arn)["Tags"]
702+
# if this is none there is nothing to check
703+
if topic_arn in store.topics:
704+
if tags is None:
705+
tags = []
706+
for tag in tags:
707+
# this means topic already created with empty tags and when we try to create it
708+
# again with other tag value then it should fail according to aws documentation.
709+
if existing_tags is not None and tag not in existing_tags:
710+
return False
711+
return True

tests/aws/services/sns/test_sns.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,6 @@ def test_create_topic_with_attributes(self, sns_create_topic, snapshot, aws_clie
126126
snapshot.match("get-attrs-malformed-topic", e.value.response)
127127

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

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

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

239236
@markers.aws.validated
240-
@skip_if_sns_v2
241237
def test_create_topic_after_delete_with_new_tags(self, sns_create_topic, snapshot, aws_client):
242238
topic_name = f"test-{short_uid()}"
243239
topic = sns_create_topic(Name=topic_name, Tags=[{"Key": "Name", "Value": "pqr"}])

tests/aws/services/sns/test_sns.snapshot.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@
9090
}
9191
},
9292
"tests/aws/services/sns/test_sns.py::TestSNSTopicCrud::test_tags": {
93-
"recorded-date": "24-08-2023, 22:30:44",
93+
"recorded-date": "13-10-2025, 06:50:09",
9494
"recorded-content": {
9595
"duplicate-key-error": {
9696
"Error": {
@@ -235,7 +235,7 @@
235235
}
236236
},
237237
"tests/aws/services/sns/test_sns.py::TestSNSTopicCrud::test_create_duplicate_topic_with_more_tags": {
238-
"recorded-date": "24-08-2023, 22:30:46",
238+
"recorded-date": "13-10-2025, 06:53:55",
239239
"recorded-content": {
240240
"exception-duplicate": {
241241
"Error": {
@@ -251,7 +251,7 @@
251251
}
252252
},
253253
"tests/aws/services/sns/test_sns.py::TestSNSTopicCrud::test_create_duplicate_topic_check_idempotency": {
254-
"recorded-date": "24-08-2023, 22:30:47",
254+
"recorded-date": "13-10-2025, 07:07:09",
255255
"recorded-content": {
256256
"response-created": {
257257
"TopicArn": "arn:<partition>:sns:<region>:111111111111:<resource:1>",
@@ -284,7 +284,7 @@
284284
}
285285
},
286286
"tests/aws/services/sns/test_sns.py::TestSNSTopicCrud::test_create_topic_after_delete_with_new_tags": {
287-
"recorded-date": "24-08-2023, 22:30:48",
287+
"recorded-date": "13-10-2025, 07:59:08",
288288
"recorded-content": {
289289
"topic-0": {
290290
"TopicArn": "arn:<partition>:sns:<region>:111111111111:<resource:1>",

tests/aws/services/sns/test_sns.validation.json

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -564,13 +564,31 @@
564564
}
565565
},
566566
"tests/aws/services/sns/test_sns.py::TestSNSTopicCrud::test_create_duplicate_topic_check_idempotency": {
567-
"last_validated_date": "2023-08-24T20:30:47+00:00"
567+
"last_validated_date": "2025-10-13T07:07:09+00:00",
568+
"durations_in_seconds": {
569+
"setup": 1.26,
570+
"call": 2.02,
571+
"teardown": 1.08,
572+
"total": 4.36
573+
}
568574
},
569575
"tests/aws/services/sns/test_sns.py::TestSNSTopicCrud::test_create_duplicate_topic_with_more_tags": {
570-
"last_validated_date": "2023-08-24T20:30:46+00:00"
576+
"last_validated_date": "2025-10-13T06:53:55+00:00",
577+
"durations_in_seconds": {
578+
"setup": 1.09,
579+
"call": 1.57,
580+
"teardown": 0.31,
581+
"total": 2.97
582+
}
571583
},
572584
"tests/aws/services/sns/test_sns.py::TestSNSTopicCrud::test_create_topic_after_delete_with_new_tags": {
573-
"last_validated_date": "2023-08-24T20:30:48+00:00"
585+
"last_validated_date": "2025-10-13T07:59:08+00:00",
586+
"durations_in_seconds": {
587+
"setup": 0.88,
588+
"call": 1.32,
589+
"teardown": 0.58,
590+
"total": 2.78
591+
}
574592
},
575593
"tests/aws/services/sns/test_sns.py::TestSNSTopicCrud::test_create_topic_test_arn": {
576594
"last_validated_date": "2025-09-29T09:32:56+00:00",
@@ -600,7 +618,13 @@
600618
}
601619
},
602620
"tests/aws/services/sns/test_sns.py::TestSNSTopicCrud::test_tags": {
603-
"last_validated_date": "2023-08-24T20:30:44+00:00"
621+
"last_validated_date": "2025-10-13T06:50:09+00:00",
622+
"durations_in_seconds": {
623+
"setup": 0.86,
624+
"call": 2.28,
625+
"teardown": 0.34,
626+
"total": 3.48
627+
}
604628
},
605629
"tests/aws/services/sns/test_sns.py::TestSNSTopicCrud::test_topic_delivery_policy_crud": {
606630
"last_validated_date": "2024-10-03T21:46:17+00:00"

0 commit comments

Comments
 (0)