Repository: localstack/localstack

Issue:
bug: Unable to provision FIFO SNS Topic via CDK
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Current Behavior

When attempting to provision a FIFO SNS topic in LocalStack via CDK using `cdklocal`, e.g.:

```typescript
const topic = new sns.Topic(this, 'FIFOTopic', {
  displayName: 'topic.fifo',
  fifo: true,
  contentBasedDeduplication: true,
});
```

The resulting topic created in LocalStack is not FIFO:

![Screenshot 2023-11-27 at 12 26 44 PM](https://github.com/localstack/localstack/assets/77160631/fe0079be-1197-4a8b-8405-8014f3d633f1)

This doesn't appear to be an issue with `cdklocal`, because the template output does appear to have the correct properties:

```json
{
 "Resources": {
  "FIFOTopic5C947601": {
   "Type": "AWS::SNS::Topic",
   "Properties": {
    "ContentBasedDeduplication": true,
    "DisplayName": "topic.fifo",
    "FifoTopic": true,
    "TopicName": "SNSStack-FIFOTopic-99AA2860.fifo"
   },
   ...
   
 }
```


### Expected Behavior

A FIFO SNS Topic would be provisioned when setting `fifo: true` on the CDK construct.

### How are you starting LocalStack?

With the `localstack` script

### Steps To Reproduce

I created this git repository to provide an example duplicating the issue: https://github.com/tbellerose-godaddy/ls-fifo-sns-cdk-bug

#### How are you starting localstack (e.g., `bin/localstack` command, arguments, or `docker-compose.yml`)

    localstack start -d

#### Client commands (e.g., AWS SDK code snippet, or sequence of "awslocal" commands)

    
    cdklocal bootstrap
    cdklocal deploy '*'

### Environment

```markdown
- OS: macOS Ventura 13.5.2
- LocalStack: 2.3.2
```


### Anything else?

Creating a FIFO SNS Topic via the `awslocal-cli` works as expected. This is only an issue when creating via CDK.


Discussion:
Welcome to LocalStack! Thanks for reporting your first issue and our team will be working towards fixing the issue for you or reach out for more background information. We recommend joining our [Slack Community](https://localstack.cloud/contact/) for real-time help and drop a message to LocalStack Pro Support if you are a Pro user! If you are willing to contribute towards fixing this issue, please have a look at our [contributing guidelines](https://github.com/localstack/.github/blob/main/CONTRIBUTING.md) and our [contributing guide](https://docs.localstack.cloud/contributing/).

Resolving patch:
diff --git a/localstack/services/sns/resource_providers/aws_sns_topic.py b/localstack/services/sns/resource_providers/aws_sns_topic.py
--- a/localstack/services/sns/resource_providers/aws_sns_topic.py
+++ b/localstack/services/sns/resource_providers/aws_sns_topic.py
@@ -11,7 +11,7 @@
     ResourceProvider,
     ResourceRequest,
 )
-from localstack.utils.strings import short_uid
+from localstack.utils.strings import canonicalize_bool_to_str, short_uid
 
 
 class SNSTopicProperties(TypedDict):
@@ -79,13 +79,11 @@ def create(
         attributes = {k: v for k, v in model.items() if v is not None if k != "TopicName"}
 
         # following attributes need to be str instead of bool for boto to work
-        if attributes.get("FifoTopic") is not None:
-            attributes["FifoTopic"] = str(attributes.get("FifoTopic"))
+        if (fifo_topic := attributes.get("FifoTopic")) is not None:
+            attributes["FifoTopic"] = canonicalize_bool_to_str(fifo_topic)
 
-        if attributes.get("ContentBasedDeduplication") is not None:
-            attributes["ContentBasedDeduplication"] = str(
-                attributes.get("ContentBasedDeduplication")
-            )
+        if (content_based_dedup := attributes.get("ContentBasedDeduplication")) is not None:
+            attributes["ContentBasedDeduplication"] = canonicalize_bool_to_str(content_based_dedup)
 
         subscriptions = []
         if attributes.get("Subscription") is not None:
@@ -99,7 +97,8 @@ def create(
 
         # in case cloudformation didn't provide topic name
         if model.get("TopicName") is None:
-            model["TopicName"] = f"topic-{short_uid()}"
+            name = f"topic-{short_uid()}" if not fifo_topic else f"topic-{short_uid()}.fifo"
+            model["TopicName"] = name
 
         create_sns_response = sns.create_topic(Name=model["TopicName"], Attributes=attributes)
         request.custom_context[REPEATED_INVOCATION] = True


Test patch:
diff --git a/tests/aws/services/cloudformation/resources/test_sns.py b/tests/aws/services/cloudformation/resources/test_sns.py
--- a/tests/aws/services/cloudformation/resources/test_sns.py
+++ b/tests/aws/services/cloudformation/resources/test_sns.py
@@ -8,7 +8,15 @@
 
 
 @markers.aws.validated
-def test_sns_topic_fifo_with_deduplication(deploy_cfn_template, aws_client):
+@markers.snapshot.skip_snapshot_verify(
+    paths=[
+        "$..Attributes.DeliveryPolicy",
+        "$..Attributes.EffectiveDeliveryPolicy",
+        "$..Attributes.Policy.Statement..Action",  # SNS:Receive is added by moto but not returned in AWS
+    ]
+)
+def test_sns_topic_fifo_with_deduplication(deploy_cfn_template, aws_client, snapshot):
+    snapshot.add_transformer(snapshot.transform.key_value("TopicArn"))
     topic_name = f"topic-{short_uid()}.fifo"
 
     deploy_cfn_template(
@@ -21,7 +29,12 @@ def test_sns_topic_fifo_with_deduplication(deploy_cfn_template, aws_client):
     topics = aws_client.sns.list_topics()["Topics"]
     topic_arns = [t["TopicArn"] for t in topics]
 
-    assert len([t for t in topic_arns if topic_name in t]) == 1
+    filtered_topics = [t for t in topic_arns if topic_name in t]
+    assert len(filtered_topics) == 1
+
+    # assert that the topic is properly created as Fifo
+    topic_attrs = aws_client.sns.get_topic_attributes(TopicArn=filtered_topics[0])
+    snapshot.match("get-topic-attrs", topic_attrs)
 
 
 @markers.aws.needs_fixing
diff --git a/tests/aws/services/cloudformation/resources/test_sns.snapshot.json b/tests/aws/services/cloudformation/resources/test_sns.snapshot.json
new file mode 100644
--- /dev/null
+++ b/tests/aws/services/cloudformation/resources/test_sns.snapshot.json
@@ -0,0 +1,69 @@
+{
+  "tests/aws/services/cloudformation/resources/test_sns.py::test_sns_topic_fifo_with_deduplication": {
+    "recorded-date": "27-11-2023, 21:27:29",
+    "recorded-content": {
+      "get-topic-attrs": {
+        "Attributes": {
+          "ContentBasedDeduplication": "true",
+          "DisplayName": "",
+          "EffectiveDeliveryPolicy": {
+            "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"
+              }
+            }
+          },
+          "FifoTopic": "true",
+          "Owner": "111111111111",
+          "Policy": {
+            "Version": "2008-10-17",
+            "Id": "__default_policy_ID",
+            "Statement": [
+              {
+                "Sid": "__default_statement_ID",
+                "Effect": "Allow",
+                "Principal": {
+                  "AWS": "*"
+                },
+                "Action": [
+                  "SNS:GetTopicAttributes",
+                  "SNS:SetTopicAttributes",
+                  "SNS:AddPermission",
+                  "SNS:RemovePermission",
+                  "SNS:DeleteTopic",
+                  "SNS:Subscribe",
+                  "SNS:ListSubscriptionsByTopic",
+                  "SNS:Publish"
+                ],
+                "Resource": "<topic-arn:1>",
+                "Condition": {
+                  "StringEquals": {
+                    "AWS:SourceOwner": "111111111111"
+                  }
+                }
+              }
+            ]
+          },
+          "SubscriptionsConfirmed": "0",
+          "SubscriptionsDeleted": "0",
+          "SubscriptionsPending": "0",
+          "TopicArn": "<topic-arn:1>"
+        },
+        "ResponseMetadata": {
+          "HTTPHeaders": {},
+          "HTTPStatusCode": 200
+        }
+      }
+    }
+  }
+}