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

Commit 9f9e8ea

Browse files
authored
Sns/switch v2 to default provider (#13699)
1 parent 13ed487 commit 9f9e8ea

16 files changed

Lines changed: 1229 additions & 3031 deletions

File tree

.github/workflows/aws-tests.yml

Lines changed: 0 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,6 @@ jobs:
211211
dynamodb-v2: ${{ steps.changes.outputs.dynamodb-v2 }}
212212
events-v1: ${{ steps.changes.outputs.events-v1 }}
213213
cloudformation-legacy: ${{ steps.changes.outputs.cloudformation-legacy }}
214-
sns-v2: ${{ steps.changes.outputs.sns-v2 }}
215214
steps:
216215
- name: Checkout
217216
uses: actions/checkout@v6
@@ -280,8 +279,6 @@ jobs:
280279
cloudformation-legacy:
281280
- 'localstack-core/localstack/services/cloudformation/**'
282281
- 'tests/aws/services/cloudformation/**'
283-
sns-v2:
284-
- 'tests/aws/services/sns/**' # todo: potentially add more locations (lambda/sqs tests?)
285282
286283
- name: Run Unit Tests
287284
timeout-minutes: 20
@@ -868,59 +865,6 @@ jobs:
868865
${{ env.JUNIT_REPORTS_FILE }}
869866
retention-days: 30
870867

871-
test-sns-v2:
872-
name: Test SNS V2
873-
if: ${{ !inputs.onlyAcceptanceTests && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || needs.test-preflight.outputs.sns-v2 == 'true') }}
874-
runs-on: ubuntu-latest
875-
needs:
876-
- test-preflight
877-
- build
878-
timeout-minutes: 60
879-
env:
880-
# Set job-specific environment variables for pytest-tinybird
881-
CI_JOB_NAME: ${{ github.job }}
882-
CI_JOB_ID: ${{ github.job }}
883-
outputs:
884-
# we need this output to conditionally execute the Publishing step
885-
job_status: "executed"
886-
steps:
887-
- name: Checkout
888-
uses: actions/checkout@v6
889-
890-
- name: Prepare Local Test Environment
891-
uses: ./.github/actions/setup-tests-env
892-
893-
- name: Download Test Selection
894-
if: ${{ env.TESTSELECTION_PYTEST_ARGS }}
895-
uses: actions/download-artifact@v7
896-
with:
897-
name: test-selection
898-
path: dist/testselection/
899-
900-
- name: Run SNS v2 Provider Tests
901-
timeout-minutes: 30
902-
env:
903-
# add the GitHub API token to avoid rate limit issues
904-
GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
905-
DEBUG: 1
906-
COVERAGE_FILE: ".coverage.sns_v2"
907-
TEST_PATH: "tests/aws/services/sns/"
908-
JUNIT_REPORTS_FILE: "pytest-junit-sns-v2.xml"
909-
PYTEST_ARGS: "${{ env.TINYBIRD_PYTEST_ARGS }}${{ env.TESTSELECTION_PYTEST_ARGS }} --reruns 3 -o junit_suite_name=sns_v2"
910-
PROVIDER_OVERRIDE_SNS: "v2"
911-
run: make test-coverage
912-
913-
- name: Archive Test Results
914-
uses: actions/upload-artifact@v6
915-
if: success() || failure()
916-
with:
917-
name: test-results-sns-v2
918-
include-hidden-files: true
919-
path: |
920-
pytest-junit-sns-v2.xml
921-
.coverage.sns_v2
922-
retention-days: 30
923-
924868
publish-alternative-provider-test-results:
925869
name: Publish Alternative Provider Test Results
926870
# execute on success or failure, but not if the workflow is cancelled or all of the dependencies has been skipped
@@ -930,7 +874,6 @@ jobs:
930874
- test-events-v1
931875
- test-ddb-v2
932876
- test-cloudwatch-v1
933-
- test-sns-v2
934877
runs-on: ubuntu-latest
935878
permissions:
936879
checks: write
@@ -958,11 +901,6 @@ jobs:
958901
with:
959902
pattern: test-results-cloudwatch-v1
960903

961-
- name: Download SNS v2 Artifacts
962-
uses: actions/download-artifact@v7
963-
with:
964-
pattern: test-results-sns-v2
965-
966904
- name: Publish Bootstrap and Integration Test Results
967905
uses: EnricoMi/publish-unit-test-result-action@v2
968906
if: success() || failure()

localstack-core/localstack/services/providers.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -311,17 +311,8 @@ def ses():
311311

312312
@aws_provider()
313313
def sns():
314-
from localstack.services.moto import MotoFallbackDispatcher
315314
from localstack.services.sns.provider import SnsProvider
316315

317-
provider = SnsProvider()
318-
return Service.for_provider(provider, dispatch_table_factory=MotoFallbackDispatcher)
319-
320-
321-
@aws_provider(api="sns", name="v2")
322-
def sns_v2():
323-
from localstack.services.sns.v2.provider import SnsProvider
324-
325316
provider = SnsProvider()
326317
return Service.for_provider(provider)
327318

localstack-core/localstack/services/sns/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from string import ascii_letters, digits
33
from typing import get_args
44

5-
from localstack.services.sns.v2.models import SnsApplicationPlatforms
5+
from localstack.services.sns.models import SnsApplicationPlatforms
66

77
SNS_PROTOCOLS = [
88
"http",

localstack-core/localstack/services/sns/models.py

Lines changed: 81 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,33 @@
55
from typing import Literal, TypedDict
66

77
from localstack.aws.api.sns import (
8+
Endpoint,
89
MessageAttributeMap,
10+
PhoneNumber,
11+
PlatformApplication,
912
PublishBatchRequestEntry,
13+
TopicAttributesMap,
1014
subscriptionARN,
1115
topicARN,
1216
)
13-
from localstack.services.stores import AccountRegionBundle, BaseStore, LocalAttribute
14-
from localstack.utils.aws.arns import parse_arn
17+
from localstack.services.stores import (
18+
AccountRegionBundle,
19+
BaseStore,
20+
CrossRegionAttribute,
21+
LocalAttribute,
22+
)
1523
from localstack.utils.objects import singleton_factory
1624
from localstack.utils.strings import long_uid
25+
from localstack.utils.tagging import TaggingService
26+
27+
28+
class Topic(TypedDict, total=True):
29+
arn: str
30+
name: str
31+
attributes: TopicAttributesMap
32+
data_protection_policy: str
33+
subscriptions: list[str]
34+
1735

1836
SnsProtocols = Literal[
1937
"http", "https", "email", "email-json", "sms", "sqs", "application", "lambda", "firehose"
@@ -23,39 +41,47 @@
2341
"APNS", "APNS_SANDBOX", "ADM", "FCM", "Baidu", "GCM", "MPNS", "WNS"
2442
]
2543

44+
45+
class EndpointAttributeNames(StrEnum):
46+
CUSTOM_USER_DATA = "CustomUserData"
47+
Token = "Token"
48+
ENABLED = "Enabled"
49+
50+
51+
SMS_ATTRIBUTE_NAMES = [
52+
"DeliveryStatusIAMRole",
53+
"DeliveryStatusSuccessSamplingRate",
54+
"DefaultSenderID",
55+
"DefaultSMSType",
56+
"UsageReportS3Bucket",
57+
]
58+
SMS_TYPES = ["Promotional", "Transactional"]
59+
SMS_DEFAULT_SENDER_REGEX = r"^(?=[A-Za-z0-9]{1,11}$)(?=.*[A-Za-z])[A-Za-z0-9]+$"
2660
SnsMessageProtocols = Literal[SnsProtocols, SnsApplicationPlatforms]
2761

2862

29-
def create_default_sns_topic_policy(topic_arn: str) -> dict:
63+
class SnsSubscription(TypedDict, total=False):
3064
"""
31-
Creates the default SNS topic policy for the given topic ARN.
32-
33-
:param topic_arn: The topic arn
34-
:return: A policy document
65+
In SNS, Subscription can be represented with only TopicArn, Endpoint, Protocol, SubscriptionArn and Owner, for
66+
example in ListSubscriptions. However, when getting a subscription with GetSubscriptionAttributes, it will return
67+
the Subscription object merged with its own attributes.
68+
This represents this merged object, for internal use and in GetSubscriptionAttributes
69+
https://docs.aws.amazon.com/cli/latest/reference/sns/get-subscription-attributes.html
3570
"""
36-
return {
37-
"Version": "2008-10-17",
38-
"Id": "__default_policy_ID",
39-
"Statement": [
40-
{
41-
"Sid": "__default_statement_ID",
42-
"Effect": "Allow",
43-
"Principal": {"AWS": "*"},
44-
"Action": [
45-
"SNS:GetTopicAttributes",
46-
"SNS:SetTopicAttributes",
47-
"SNS:AddPermission",
48-
"SNS:RemovePermission",
49-
"SNS:DeleteTopic",
50-
"SNS:Subscribe",
51-
"SNS:ListSubscriptionsByTopic",
52-
"SNS:Publish",
53-
],
54-
"Resource": topic_arn,
55-
"Condition": {"StringEquals": {"AWS:SourceOwner": parse_arn(topic_arn)["account"]}},
56-
}
57-
],
58-
}
71+
72+
TopicArn: topicARN
73+
Endpoint: str
74+
Protocol: SnsProtocols
75+
SubscriptionArn: subscriptionARN
76+
PendingConfirmation: Literal["true", "false"]
77+
Owner: str | None
78+
SubscriptionPrincipal: str | None
79+
FilterPolicy: str | None
80+
FilterPolicyScope: Literal["MessageAttributes", "MessageBody"]
81+
RawMessageDelivery: Literal["true", "false"]
82+
ConfirmationWasAuthenticated: Literal["true", "false"]
83+
SubscriptionRoleArn: str | None
84+
DeliveryPolicy: str | None
5985

6086

6187
@singleton_factory
@@ -126,60 +152,50 @@ def from_batch_entry(cls, entry: PublishBatchRequestEntry, is_fifo=False) -> "Sn
126152
)
127153

128154

129-
class SnsSubscription(TypedDict, total=False):
130-
"""
131-
In SNS, Subscription can be represented with only TopicArn, Endpoint, Protocol, SubscriptionArn and Owner, for
132-
example in ListSubscriptions. However, when getting a subscription with GetSubscriptionAttributes, it will return
133-
the Subscription object merged with its own attributes.
134-
This represents this merged object, for internal use and in GetSubscriptionAttributes
135-
https://docs.aws.amazon.com/cli/latest/reference/sns/get-subscription-attributes.html
136-
"""
155+
@dataclass
156+
class PlatformEndpoint:
157+
platform_application_arn: str
158+
platform_endpoint: Endpoint
137159

138-
TopicArn: topicARN
139-
Endpoint: str
140-
Protocol: SnsProtocols
141-
SubscriptionArn: subscriptionARN
142-
PendingConfirmation: Literal["true", "false"]
143-
Owner: str | None
144-
SubscriptionPrincipal: str | None
145-
FilterPolicy: str | None
146-
FilterPolicyScope: Literal["MessageAttributes", "MessageBody"]
147-
RawMessageDelivery: Literal["true", "false"]
148-
ConfirmationWasAuthenticated: Literal["true", "false"]
149-
SubscriptionRoleArn: str | None
150-
DeliveryPolicy: str | None
160+
161+
@dataclass
162+
class PlatformApplicationDetails:
163+
platform_application: PlatformApplication
164+
# maps all Endpoints of the PlatformApplication, from their Token to their ARN
165+
platform_endpoints: dict[str, str]
151166

152167

153168
class SnsStore(BaseStore):
154-
# maps topic ARN to subscriptions ARN
155-
topic_subscriptions: dict[str, list[str]] = LocalAttribute(default=dict)
169+
# maps topic ARN to Topic
170+
topics: dict[str, Topic] = LocalAttribute(default=dict)
156171

157172
# maps subscription ARN to SnsSubscription
158173
subscriptions: dict[str, SnsSubscription] = LocalAttribute(default=dict)
159174

175+
# filter policy are stored as JSON string in subscriptions, store the decoded result Dict
176+
subscription_filter_policy: dict[subscriptionARN, dict] = LocalAttribute(default=dict)
177+
160178
# maps confirmation token to subscription ARN
161179
subscription_tokens: dict[str, str] = LocalAttribute(default=dict)
162180

163-
# maps topic ARN to list of tags
164-
sns_tags: dict[str, list[dict]] = LocalAttribute(default=dict)
181+
# maps platform application arns to platform applications
182+
platform_applications: dict[str, PlatformApplicationDetails] = LocalAttribute(default=dict)
183+
184+
# maps endpoint arns to platform endpoints
185+
platform_endpoints: dict[str, PlatformEndpoint] = LocalAttribute(default=dict)
165186

166187
# cache of topic ARN to platform endpoint messages (used primarily for testing)
167188
platform_endpoint_messages: dict[str, list[dict]] = LocalAttribute(default=dict)
168189

190+
# topic/subscription independent default values for sending sms messages
191+
sms_attributes: dict[str, str] = LocalAttribute(default=dict)
192+
169193
# list of sent SMS messages
170194
sms_messages: list[dict] = LocalAttribute(default=list)
171195

172-
# filter policy are stored as JSON string in subscriptions, store the decoded result Dict
173-
subscription_filter_policy: dict[subscriptionARN, dict] = LocalAttribute(default=dict)
196+
TAGS: TaggingService = CrossRegionAttribute(default=TaggingService)
174197

175-
def get_topic_subscriptions(self, topic_arn: str) -> list[SnsSubscription]:
176-
topic_subscriptions = self.topic_subscriptions.get(topic_arn, [])
177-
subscriptions = [
178-
subscription
179-
for subscription_arn in topic_subscriptions
180-
if (subscription := self.subscriptions.get(subscription_arn))
181-
]
182-
return subscriptions
198+
PHONE_NUMBERS_OPTED_OUT: set[PhoneNumber] = CrossRegionAttribute(default=set)
183199

184200

185201
sns_stores = AccountRegionBundle("sns", SnsStore)

0 commit comments

Comments
 (0)