Repository: getmoto/moto
Pull request #6838 (2023-09-20 22:27:57)

Feature: s3 sns notification
Implement missing s3 sns notifications.

- [x] Feature is added
- [x] The linter is happy
- [x] Tests are added
- [ ] All tests pass, existing and new

I created a new test file because I couldn't find any tests that test the notifications themselves (the ones in test_s3.py test the configuration options, but not the implementation of send_event).
I'm not sure if need to update the function docs from models.py and the s3.rst, just edited both to be sure.


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/6838?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
> Merging [#6838](https://app.codecov.io/gh/getmoto/moto/pull/6838?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) (3dbd0e5) into [master](https://app.codecov.io/gh/getmoto/moto/commit/179a2287411a8347f902d97fe67ec70bb32a68d5?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) (179a228) will **decrease** coverage by `0.01%`.
> The diff coverage is `84.61%`.

```diff
@@            Coverage Diff             @@
##           master    #6838      +/-   ##
==========================================
- Coverage   96.20%   96.20%   -0.01%     
==========================================
  Files         816      816              
  Lines       80076    80089      +13     
==========================================
+ Hits        77037    77049      +12     
- Misses       3039     3040       +1     
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/6838/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Coverage Δ | |
|---|---|---|
| [servertests](https://app.codecov.io/gh/getmoto/moto/pull/6838/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `37.00% <7.69%> (-0.01%)` | :arrow_down: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/6838/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `96.14% <84.61%> (+<0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Files Changed](https://app.codecov.io/gh/getmoto/moto/pull/6838?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Coverage Δ | |
|---|---|---|
| [moto/s3/models.py](https://app.codecov.io/gh/getmoto/moto/pull/6838?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#diff-bW90by9zMy9tb2RlbHMucHk=) | `97.01% <ø> (ø)` | |
| [moto/s3/notifications.py](https://app.codecov.io/gh/getmoto/moto/pull/6838?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#diff-bW90by9zMy9ub3RpZmljYXRpb25zLnB5) | `96.92% <84.61%> (-3.08%)` | :arrow_down: |

... and [1 file with indirect coverage changes](https://app.codecov.io/gh/getmoto/moto/pull/6838/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto)


maederm: I'm not sure what my changes have to do with terraform failing.

```
=== CONT  TestAccAPIGatewayV2Authorizer_HTTPAPILambdaRequestAuthorizer_initialMissingCacheTTL
=== NAME  TestAccAPIGatewayV2Authorizer_HTTPAPILambdaRequestAuthorizer_initialZeroCacheTTL
    testing_new.go:88: Error running post-test destroy, there may be dangling resources: exit status 1
        
        Error: waiting for EC2 VPC (vpc-7ffe1a55) delete: operation error EC2: DescribeVpcs, https response error StatusCode: 400, RequestID: , decomposing response: read tcp [::1]:50438->[::1]:4566: use of closed network connection

```

Patch:
diff --git a/.github/workflows/data-update_config-managed-rules.yml b/.github/workflows/data-update_config-managed-rules.yml
new file mode 100644
index 000000000..8ea4ae6aa
--- /dev/null
+++ b/.github/workflows/data-update_config-managed-rules.yml
@@ -0,0 +1,43 @@
+# Data Update:
+#   Config Managed Rules
+#
+# This Github Action:
+#   - executes the script that updates the Config Managed Rules that come bundled with Moto
+#   - creates a PR
+#
+name: "DataUpdate_ConfigManagedRules"
+
+on:
+  schedule:
+    - cron: '00 6 1 * *'
+  workflow_dispatch:
+
+jobs:
+  update:
+    name: Update Config Managed Rules
+    runs-on: ubuntu-latest
+    if: ${{ github.ref == 'refs/heads/master' && github.repository == 'getmoto/moto' }}
+    permissions:
+      id-token: write
+      contents: write
+      pull-requests: write
+
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@v4
+
+    - name: Set up Python 3.9
+      uses: actions/setup-python@v4
+      with:
+        python-version: 3.9
+
+    - name: Pull Config managed rules from AWS
+      run: |
+        pip install boto3
+        scripts/pull_down_aws_managed_rules.py
+    - name: Create PR
+      uses: peter-evans/create-pull-request@v5
+      with:
+        branch: 'chore-update-config-managed-rules-${{ github.run_number }}'
+        title: 'chore: update Config Managed Rules'
+        commit-message: 'chore: update Config Managed Rules'
diff --git a/docs/docs/services/s3.rst b/docs/docs/services/s3.rst
index aa7351335..963946db5 100644
--- a/docs/docs/services/s3.rst
+++ b/docs/docs/services/s3.rst
@@ -113,6 +113,7 @@ s3
         The configuration can be persisted, but at the moment we only send notifications to the following targets:
 
          - AWSLambda
+         - SNS
          - SQS
 
         For the following events:
diff --git a/moto/elbv2/responses.py b/moto/elbv2/responses.py
index 443d92fbb..c65754e0c 100644
--- a/moto/elbv2/responses.py
+++ b/moto/elbv2/responses.py
@@ -131,6 +131,23 @@ SSL_POLICIES = [
             {"name": "ECDHE-RSA-AES256-GCM-SHA384", "priority": 4},
         ],
     },
+    {
+        "name": "ELBSecurityPolicy-TLS13-1-2-2021-06",
+        "ssl_protocols": ["TLSv1.2", "TLSv1.3"],
+        "ciphers": [
+            {"name": "TLS_AES_128_GCM_SHA256", "priority": 1},
+            {"name": "TLS_AES_256_GCM_SHA384", "priority": 2},
+            {"name": "TLS_CHACHA20_POLY1305_SHA256", "priority": 3},
+            {"name": "ECDHE-ECDSA-AES128-GCM-SHA256", "priority": 4},
+            {"name": "ECDHE-RSA-AES128-GCM-SHA256", "priority": 5},
+            {"name": "ECDHE-ECDSA-AES128-SHA256", "priority": 6},
+            {"name": "ECDHE-RSA-AES128-SHA256", "priority": 7},
+            {"name": "ECDHE-ECDSA-AES256-GCM-SHA384", "priority": 8},
+            {"name": "ECDHE-RSA-AES256-GCM-SHA384", "priority": 9},
+            {"name": "ECDHE-ECDSA-AES256-SHA384", "priority": 10},
+            {"name": "ECDHE-RSA-AES256-SHA384", "priority": 11},
+        ],
+    },
 ]
 
 
diff --git a/moto/s3/models.py b/moto/s3/models.py
index b88cced02..ac73475f0 100644
--- a/moto/s3/models.py
+++ b/moto/s3/models.py
@@ -2202,6 +2202,7 @@ class S3Backend(BaseBackend, CloudWatchMetricProvider):
         The configuration can be persisted, but at the moment we only send notifications to the following targets:
 
          - AWSLambda
+         - SNS
          - SQS
 
         For the following events:
diff --git a/moto/s3/notifications.py b/moto/s3/notifications.py
index 5258b4f11..b43ab1908 100644
--- a/moto/s3/notifications.py
+++ b/moto/s3/notifications.py
@@ -60,6 +60,14 @@ def send_event(account_id: str, event_name: str, bucket: Any, key: Any) -> None:
 
             _send_sqs_message(account_id, event_body, queue_name, region_name)
 
+    for notification in bucket.notification_configuration.topic:
+        if notification.matches(event_name, key.name):
+            event_body = _get_s3_event(event_name, bucket, key, notification.id)
+            region_name = _get_region_from_arn(notification.arn)
+            topic_arn = notification.arn
+
+            _send_sns_message(account_id, event_body, topic_arn, region_name)
+
 
 def _send_sqs_message(
     account_id: str, event_body: Any, queue_name: str, region_name: str
@@ -79,6 +87,22 @@ def _send_sqs_message(
         pass
 
 
+def _send_sns_message(
+    account_id: str, event_body: Any, topic_arn: str, region_name: str
+) -> None:
+    try:
+        from moto.sns.models import sns_backends
+
+        sns_backend = sns_backends[account_id][region_name]
+        sns_backend.publish(arn=topic_arn, message=json.dumps(event_body))
+    except:  # noqa
+        # This is an async action in AWS.
+        # Even if this part fails, the calling function should pass, so catch all errors
+        # Possible exceptions that could be thrown:
+        # - Topic does not exist
+        pass
+
+
 def _invoke_awslambda(
     account_id: str, event_body: Any, fn_arn: str, region_name: str
 ) -> None:
@@ -113,3 +137,9 @@ def send_test_event(account_id: str, bucket: Any) -> None:
         queue_name = arn.split(":")[-1]
         message_body = _get_test_event(bucket.name)
         _send_sqs_message(account_id, message_body, queue_name, region_name)
+
+    arns = [n.arn for n in bucket.notification_configuration.topic]
+    for arn in set(arns):
+        region_name = _get_region_from_arn(arn)
+        message_body = _get_test_event(bucket.name)
+        _send_sns_message(account_id, message_body, arn, region_name)


Test patch:
diff --git a/tests/test_elbv2/test_elbv2.py b/tests/test_elbv2/test_elbv2.py
index 67c398187..84fcf9675 100644
--- a/tests/test_elbv2/test_elbv2.py
+++ b/tests/test_elbv2/test_elbv2.py
@@ -1120,7 +1120,7 @@ def test_describe_ssl_policies():
     client = boto3.client("elbv2", region_name="eu-central-1")
 
     resp = client.describe_ssl_policies()
-    assert len(resp["SslPolicies"]) == 6
+    assert len(resp["SslPolicies"]) == 7
 
     resp = client.describe_ssl_policies(
         Names=["ELBSecurityPolicy-TLS-1-2-2017-01", "ELBSecurityPolicy-2016-08"]
diff --git a/tests/test_s3/test_s3_lambda_integration.py b/tests/test_s3/test_s3_lambda_integration.py
index 3df1a2fea..301af5f49 100644
--- a/tests/test_s3/test_s3_lambda_integration.py
+++ b/tests/test_s3/test_s3_lambda_integration.py
@@ -4,7 +4,7 @@ from uuid import uuid4
 import boto3
 import pytest
 
-from moto import mock_lambda, mock_logs, mock_s3, mock_sqs
+from moto import mock_lambda, mock_logs, mock_s3, mock_sns, mock_sqs
 from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
 from tests.markers import requires_docker
 from tests.test_awslambda.utilities import (
@@ -285,3 +285,101 @@ def test_object_put__sends_to_queue__using_filter():
     messages = queue.receive_messages()
     assert not messages
     _ = [m.delete() for m in messages]
+
+
+@mock_s3
+@mock_sns
+@mock_sqs
+def test_put_bucket_notification_sns_sqs():
+    s3_client = boto3.client("s3", region_name=REGION_NAME)
+    s3_client.create_bucket(Bucket="bucket")
+
+    sqs_client = boto3.client("sqs", region_name=REGION_NAME)
+    sqs_queue = sqs_client.create_queue(QueueName="queue")
+    sqs_queue_arn = sqs_client.get_queue_attributes(
+        QueueUrl=sqs_queue["QueueUrl"], AttributeNames=["QueueArn"]
+    )
+
+    sns_client = boto3.client("sns", region_name=REGION_NAME)
+    sns_topic = sns_client.create_topic(Name="topic")
+
+    # Subscribe SQS queue to SNS topic
+    sns_client.subscribe(
+        TopicArn=sns_topic["TopicArn"],
+        Protocol="sqs",
+        Endpoint=sqs_queue_arn["Attributes"]["QueueArn"],
+    )
+
+    # Set S3 to send ObjectCreated to SNS
+    s3_client.put_bucket_notification_configuration(
+        Bucket="bucket",
+        NotificationConfiguration={
+            "TopicConfigurations": [
+                {
+                    "Id": "SomeID",
+                    "TopicArn": sns_topic["TopicArn"],
+                    "Events": ["s3:ObjectCreated:*"],
+                }
+            ]
+        },
+    )
+
+    # We should receive a test message
+    messages = sqs_client.receive_message(
+        QueueUrl=sqs_queue["QueueUrl"], MaxNumberOfMessages=10
+    )
+    assert len(messages["Messages"]) == 1
+
+    sqs_client.delete_message(
+        QueueUrl=sqs_queue["QueueUrl"],
+        ReceiptHandle=messages["Messages"][0]["ReceiptHandle"],
+    )
+
+    message_body = messages["Messages"][0]["Body"]
+    sns_message = json.loads(message_body)
+    assert sns_message["Type"] == "Notification"
+
+    # Get S3 notification from SNS message
+    s3_message_body = json.loads(sns_message["Message"])
+    assert s3_message_body["Event"] == "s3:TestEvent"
+
+    # Upload file to trigger notification
+    s3_client.put_object(Bucket="bucket", Key="myfile", Body=b"asdf1324")
+
+    # Verify queue not empty
+    messages = sqs_client.receive_message(
+        QueueUrl=sqs_queue["QueueUrl"], MaxNumberOfMessages=10
+    )
+    assert len(messages["Messages"]) == 1
+
+    # Get SNS message from SQS
+    message_body = messages["Messages"][0]["Body"]
+    sns_message = json.loads(message_body)
+    assert sns_message["Type"] == "Notification"
+
+    # Get S3 notification from SNS message
+    s3_message_body = json.loads(sns_message["Message"])
+    assert s3_message_body["Records"][0]["eventName"] == "ObjectCreated:Put"
+
+
+@mock_s3
+def test_put_bucket_notification_sns_error():
+    s3_client = boto3.client("s3", region_name=REGION_NAME)
+    s3_client.create_bucket(Bucket="bucket")
+
+    # Set S3 to send ObjectCreated to SNS
+    s3_client.put_bucket_notification_configuration(
+        Bucket="bucket",
+        NotificationConfiguration={
+            "TopicConfigurations": [
+                {
+                    "Id": "SomeID",
+                    "TopicArn": "arn:aws:sns:us-east-1:012345678910:notexistingtopic",
+                    "Events": ["s3:ObjectCreated:*"],
+                }
+            ]
+        },
+    )
+
+    # This should not throw an exception
+    s3_client.put_object(Bucket="bucket", Key="myfile", Body=b"asdf1324")