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
1 change: 1 addition & 0 deletions localstack-core/localstack/services/sns/v2/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class Topic(TypedDict, total=True):
arn: str
name: str
attributes: TopicAttributesMap
data_protection_policy: str
subscriptions: list[str]


Expand Down
45 changes: 45 additions & 0 deletions localstack-core/localstack/services/sns/v2/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
DelegatesList,
Endpoint,
EndpointDisabledException,
GetDataProtectionPolicyResponse,
GetEndpointAttributesResponse,
GetPlatformApplicationAttributesResponse,
GetSMSAttributesResponse,
Expand Down Expand Up @@ -214,6 +215,8 @@ def create_topic(
if not name_match:
raise InvalidParameterException("Invalid parameter: Topic Name")

attributes["EffectiveDeliveryPolicy"] = _create_default_effective_delivery_policy()

topic = _create_topic(name=name, attributes=attributes, context=context)
if tags:
self.tag_resource(context=context, resource_arn=topic_arn, tags=tags)
Expand Down Expand Up @@ -1173,6 +1176,28 @@ def remove_permission(
policy["Statement"] = statements
topic["attributes"]["Policy"] = json.dumps(policy)

#
# Data Protection Policy operations
#

def get_data_protection_policy(
self, context: RequestContext, resource_arn: topicARN, **kwargs
) -> GetDataProtectionPolicyResponse:
topic = self._get_topic(resource_arn, context)
return GetDataProtectionPolicyResponse(
DataProtectionPolicy=topic.get("data_protection_policy")
)

def put_data_protection_policy(
self,
context: RequestContext,
resource_arn: topicARN,
data_protection_policy: attributeValue,
**kwargs,
) -> None:
topic = self._get_topic(resource_arn, context)
topic["data_protection_policy"] = data_protection_policy

def list_tags_for_resource(
self, context: RequestContext, resource_arn: AmazonResourceName, **kwargs
) -> ListTagsForResourceResponse:
Expand Down Expand Up @@ -1270,6 +1295,26 @@ def _default_attributes(topic: Topic, context: RequestContext) -> TopicAttribute
return default_attributes


def _create_default_effective_delivery_policy():

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is somewhat of the default: I think the behavior is somewhat different: the EffectiveDeliveryPolicy is a merged value between the default value and what would be set for DeliveryPolicy. Meaning if you set for example the defaultRequestPolicy in your DeliveryPolicy, you effective delivery policy will be a merged between the default value, and that value you set, effectively being the "Effective" value.

I believe we will need to implement this behavior, it would be a great addition.

Doesn't need to be implemented in this PR as we had an issue before already, but it would be nice to keep track of this 👍 (basically updating the effective policy when updating the delivery policy)

return json.dumps(
{
"http": {
"defaultHealthyRetryPolicy": {
"minDelayTarget": 20,
"maxDelayTarget": 20,
"numRetries": 3,
"numMaxDelayRetries": 0,
"numNoDelayRetries": 0,
"numMinDelayRetries": 0,
"backoffFunction": "linear",
},
"disableSubscriptionOverrides": False,
"defaultRequestPolicy": {"headerContentType": "text/plain; charset=UTF-8"},
}
}
)


def _create_default_topic_policy(topic: Topic, context: RequestContext) -> str:
return json.dumps(
{
Expand Down
78 changes: 56 additions & 22 deletions tests/aws/services/sns/test_sns.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@ class TestSNSTopicCrud:
paths=[
"$.get-topic-attrs.Attributes.DeliveryPolicy", # TODO: remove this with the v2 provider switch
"$.get-topic-attrs.Attributes.EffectiveDeliveryPolicy",
]
],
condition=is_sns_v1_provider,
)
def test_create_topic_with_attributes(self, sns_create_topic, snapshot, aws_client):
create_topic = sns_create_topic(
Expand Down Expand Up @@ -215,7 +216,8 @@ def test_tags(self, sns_create_topic, snapshot, aws_client):
paths=[
"$.get-topic-attrs.Attributes.DeliveryPolicy", # TODO: remove this with the v2 provider switch
"$.get-topic-attrs.Attributes.EffectiveDeliveryPolicy",
]
],
condition=is_sns_v1_provider,
)
def test_create_topic_test_arn(self, sns_create_topic, snapshot, aws_client, account_id):
topic_name = "topic-test-create"
Expand Down Expand Up @@ -379,16 +381,12 @@ def test_delete_non_existent_topic(self, snapshot, aws_client, account_id, regio
snapshot.match("delete-non-existent-topic", response)

@markers.aws.validated
@markers.snapshot.skip_snapshot_verify(
paths=[
"$..Attributes.EffectiveDeliveryPolicy",
]
)
@markers.snapshot.skip_snapshot_verify(
# skipped only for v1
condition=is_sns_v1_provider,
paths=[
"$..Attributes.DeliveryPolicy",
"$..Attributes.EffectiveDeliveryPolicy",
],
)
def test_create_topic_should_be_idempotent(self, snapshot, sns_create_topic, aws_client):
Expand Down Expand Up @@ -435,16 +433,12 @@ def test_create_topic_name_constraints(self, snapshot, sns_create_topic):
snapshot.match(f"name-invalid-char-{index}", e.value.response)

@markers.aws.validated
@markers.snapshot.skip_snapshot_verify(
paths=[
"$..Attributes.EffectiveDeliveryPolicy",
]
)
@markers.snapshot.skip_snapshot_verify(
# skipped only for v1
condition=is_sns_v1_provider,
paths=[
"$..Attributes.DeliveryPolicy",
"$..Attributes.EffectiveDeliveryPolicy",
],
)
def test_create_topic_in_multiple_regions(
Expand Down Expand Up @@ -497,11 +491,6 @@ def test_list_topic_paging(self, aws_client, sns_create_topic):

@pytest.mark.skipif(is_sns_v1_provider(), reason="not covered in v1")
@markers.aws.validated
@markers.snapshot.skip_snapshot_verify(
paths=[
"$..Attributes.EffectiveDeliveryPolicy",
]
)
def test_topic_get_attributes_with_fifo_false(self, sns_create_topic, aws_client, snapshot):
resp = sns_create_topic(
Name=f"standard-topic-{short_uid()}", Attributes={"FifoTopic": "false"}
Expand Down Expand Up @@ -618,6 +607,44 @@ def test_remove_permission_errors(self, snapshot, aws_client, account_id):

snapshot.match("topic-not-found", e.value.response)

@markers.aws.validated
@pytest.mark.skipif(condition=is_sns_v1_provider(), reason="Not implemented in moto")
def test_data_protection_policy_crud(self, snapshot, aws_client):
topic_name = f"topic-{short_uid()}"
topic_arn = aws_client.sns.create_topic(Name=topic_name)["TopicArn"]

policy_doc = {
"Name": "data_protection_policy",
"Description": "Test Policy",
"Version": "2021-06-01",
"Statement": [
{
"Sid": "test-statement",
"DataDirection": "Inbound",
"Principal": ["*"],
"DataIdentifier": [
"arn:aws:dataprotection:us-east-1::data-identifier/EmailAddress"
],
"Operation": {"Deny": {}},
}
],
}

response = aws_client.sns.get_topic_attributes(TopicArn=topic_arn)
snapshot.match("get-topic-attributes-before-policy", response)

response = aws_client.sns.put_data_protection_policy(
ResourceArn=topic_arn, DataProtectionPolicy=json.dumps(policy_doc)
)
snapshot.match("put-data-protection-policy", response)

response = aws_client.sns.get_data_protection_policy(ResourceArn=topic_arn)
snapshot.match("get-data-protection-policy", response)

# check if policy shows up in the attributes
response = aws_client.sns.get_topic_attributes(TopicArn=topic_arn)
snapshot.match("get-topic-attributes-after-policy", response)


class TestSNSPublishCrud:
"""
Expand Down Expand Up @@ -3158,7 +3185,8 @@ def test_validations_for_fifo(
"$.topic-attrs.Attributes.DeliveryPolicy",
"$.topic-attrs.Attributes.EffectiveDeliveryPolicy",
"$.topic-attrs.Attributes.Policy.Statement..Action", # SNS:Receive is added by moto but not returned in AWS
]
],
condition=is_sns_v1_provider,
)
@pytest.mark.parametrize("raw_message_delivery", [True, False])
def test_publish_fifo_messages_to_dlq(
Expand Down Expand Up @@ -3315,14 +3343,19 @@ def get_messages_from_dlq(amount_msg: int):
snapshot.match("messages-in-dlq", {"Messages": messages})

@markers.aws.validated
@markers.snapshot.skip_snapshot_verify(
paths=[
"$.republish-batch-response-fifo.Successful..MessageId", # TODO: SNS doesnt keep track of duplicate
"$.republish-batch-response-fifo.Successful..SequenceNumber", # TODO: SNS doesnt keep track of duplicate
],
)
@markers.snapshot.skip_snapshot_verify(
paths=[
"$.topic-attrs.Attributes.DeliveryPolicy",
"$.topic-attrs.Attributes.EffectiveDeliveryPolicy",
"$.topic-attrs.Attributes.Policy.Statement..Action", # SNS:Receive is added by moto but not returned in AWS
"$.republish-batch-response-fifo.Successful..MessageId", # TODO: SNS doesnt keep track of duplicate
"$.republish-batch-response-fifo.Successful..SequenceNumber", # TODO: SNS doesnt keep track of duplicate
]
],
condition=is_sns_v1_provider,
)
@pytest.mark.parametrize("content_based_deduplication", [True, False])
def test_publish_batch_messages_from_fifo_topic_to_fifo_queue(
Expand Down Expand Up @@ -6006,7 +6039,8 @@ class TestSNSPublishDelivery:
"$..Attributes.DeliveryPolicy",
"$..Attributes.EffectiveDeliveryPolicy",
"$..Attributes.Policy.Statement..Action", # SNS:Receive is added by moto but not returned in AWS
]
],
condition=is_sns_v1_provider,
)
@skip_if_sns_v2
def test_delivery_lambda(
Expand Down
Loading
Loading