diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/SNSInteg.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/SNSInteg.assets.json index bb7fb8ce9227a..815adb808f6c1 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/SNSInteg.assets.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/SNSInteg.assets.json @@ -1,7 +1,7 @@ { - "version": "20.0.0", + "version": "35.0.0", "files": { - "abc29d4f06b918fce7d7beea18c92c833e7e6266f0863b256fd6927b69eb595f": { + "c917d5a2b2e16b9e3cd0679e1dee0539129223bcca595b66788045cc8fc23e20": { "source": { "path": "SNSInteg.template.json", "packaging": "file" @@ -9,7 +9,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "abc29d4f06b918fce7d7beea18c92c833e7e6266f0863b256fd6927b69eb595f.json", + "objectKey": "c917d5a2b2e16b9e3cd0679e1dee0539129223bcca595b66788045cc8fc23e20.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/SNSInteg.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/SNSInteg.template.json index 41dce82c5114a..69954cad9b6b7 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/SNSInteg.template.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/SNSInteg.template.json @@ -38,6 +38,24 @@ "MyTopic86869434": { "Type": "AWS::SNS::Topic", "Properties": { + "DeliveryStatusLogging": [ + { + "FailureFeedbackRoleArn": { + "Fn::GetAtt": [ + "FeedbackRoleCAF84E5C", + "Arn" + ] + }, + "Protocol": "http/s", + "SuccessFeedbackRoleArn": { + "Fn::GetAtt": [ + "FeedbackRoleCAF84E5C", + "Arn" + ] + }, + "SuccessFeedbackSampleRate": "50" + } + ], "DisplayName": "fooDisplayName", "KmsMasterKeyId": { "Fn::GetAtt": [ @@ -47,6 +65,51 @@ }, "TopicName": "fooTopic" } + }, + "FeedbackRoleCAF84E5C": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "sns.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "Policy23B91518": { + "Type": "AWS::IAM::ManagedPolicy", + "Properties": { + "Description": "", + "Path": "/", + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:PutMetricFilter", + "logs:PutRetentionPolicy" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "Roles": [ + { + "Ref": "FeedbackRoleCAF84E5C" + } + ] + } } }, "Parameters": { diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/cdk.out index 588d7b269d34f..c5cb2e5de6344 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/cdk.out +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/cdk.out @@ -1 +1 @@ -{"version":"20.0.0"} \ No newline at end of file +{"version":"35.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/integ.json index 4a167e58f13f1..d9fab2006a00e 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/integ.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/integ.json @@ -1,5 +1,5 @@ { - "version": "20.0.0", + "version": "35.0.0", "testCases": { "integ.sns": { "stacks": [ diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/manifest.json index ab09a23e85494..08016c12ab007 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/manifest.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/manifest.json @@ -1,12 +1,6 @@ { - "version": "20.0.0", + "version": "35.0.0", "artifacts": { - "Tree": { - "type": "cdk:tree", - "properties": { - "file": "tree.json" - } - }, "SNSInteg.assets": { "type": "cdk:asset-manifest", "properties": { @@ -20,10 +14,11 @@ "environment": "aws://unknown-account/unknown-region", "properties": { "templateFile": "SNSInteg.template.json", + "terminationProtection": false, "validateOnSynth": false, "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", - "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/abc29d4f06b918fce7d7beea18c92c833e7e6266f0863b256fd6927b69eb595f.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/c917d5a2b2e16b9e3cd0679e1dee0539129223bcca595b66788045cc8fc23e20.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -51,6 +46,18 @@ "data": "MyTopic86869434" } ], + "/SNSInteg/FeedbackRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "FeedbackRoleCAF84E5C" + } + ], + "/SNSInteg/Policy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Policy23B91518" + } + ], "/SNSInteg/BootstrapVersion": [ { "type": "aws:cdk:logicalId", @@ -65,6 +72,12 @@ ] }, "displayName": "SNSInteg" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } } } } \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/tree.json index 9b87706b97868..c7a2a6387aacc 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/tree.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/tree.json @@ -4,14 +4,6 @@ "id": "App", "path": "", "children": { - "Tree": { - "id": "Tree", - "path": "Tree", - "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.1.85" - } - }, "SNSInteg": { "id": "SNSInteg", "path": "SNSInteg", @@ -57,14 +49,14 @@ } }, "constructInfo": { - "fqn": "@aws-cdk/aws-kms.CfnKey", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.3.0" } } }, "constructInfo": { - "fqn": "@aws-cdk/aws-kms.Key", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.3.0" } }, "MyTopic": { @@ -77,6 +69,24 @@ "attributes": { "aws:cdk:cloudformation:type": "AWS::SNS::Topic", "aws:cdk:cloudformation:props": { + "deliveryStatusLogging": [ + { + "protocol": "http/s", + "failureFeedbackRoleArn": { + "Fn::GetAtt": [ + "FeedbackRoleCAF84E5C", + "Arn" + ] + }, + "successFeedbackRoleArn": { + "Fn::GetAtt": [ + "FeedbackRoleCAF84E5C", + "Arn" + ] + }, + "successFeedbackSampleRate": "50" + } + ], "displayName": "fooDisplayName", "kmsMasterKeyId": { "Fn::GetAtt": [ @@ -88,26 +98,147 @@ } }, "constructInfo": { - "fqn": "@aws-cdk/aws-sns.CfnTopic", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "FeedbackRole": { + "id": "FeedbackRole", + "path": "SNSInteg/FeedbackRole", + "children": { + "ImportFeedbackRole": { + "id": "ImportFeedbackRole", + "path": "SNSInteg/FeedbackRole/ImportFeedbackRole", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "Resource": { + "id": "Resource", + "path": "SNSInteg/FeedbackRole/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "sns.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "Policy": { + "id": "Policy", + "path": "SNSInteg/Policy", + "children": { + "ImportedPolicy": { + "id": "ImportedPolicy", + "path": "SNSInteg/Policy/ImportedPolicy", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "Resource": { + "id": "Resource", + "path": "SNSInteg/Policy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::ManagedPolicy", + "aws:cdk:cloudformation:props": { + "description": "", + "path": "/", + "policyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:PutMetricFilter", + "logs:PutRetentionPolicy" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "roles": [ + { + "Ref": "FeedbackRoleCAF84E5C" + } + ] + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" } } }, "constructInfo": { - "fqn": "@aws-cdk/aws-sns.Topic", - "version": "0.0.0" + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "SNSInteg/BootstrapVersion", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "SNSInteg/CheckBootstrapVersion", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" } } }, "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.85" + "version": "10.3.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" } } }, "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.85" + "version": "10.3.0" } } } \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.ts index ad9e6048dce91..5e1adcf34bc2e 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.ts +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.ts @@ -1,6 +1,7 @@ import { Key } from 'aws-cdk-lib/aws-kms'; import { App, Stack, StackProps } from 'aws-cdk-lib'; -import { Topic } from 'aws-cdk-lib/aws-sns'; +import { LoggingProtocol, Topic } from 'aws-cdk-lib/aws-sns'; +import { ManagedPolicy, PolicyDocument, PolicyStatement, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam'; class SNSInteg extends Stack { constructor(scope: App, id: string, props?: StackProps) { @@ -8,11 +9,37 @@ class SNSInteg extends Stack { const key = new Key(this, 'CustomKey'); - new Topic(this, 'MyTopic', { + const topic = new Topic(this, 'MyTopic', { topicName: 'fooTopic', displayName: 'fooDisplayName', masterKey: key, }); + + const feedbackRole = new Role(this, 'FeedbackRole', { + assumedBy: new ServicePrincipal('sns.amazonaws.com'), + }); + const deliveryLoggingPolicy = new ManagedPolicy(this, 'Policy', { + document: new PolicyDocument({ + statements: [new PolicyStatement({ + actions: [ + 'logs:CreateLogGroup', + 'logs:CreateLogStream', + 'logs:PutLogEvents', + 'logs:PutMetricFilter', + 'logs:PutRetentionPolicy', + ], + resources: ['*'], + })], + }), + }); + deliveryLoggingPolicy.attachToRole(feedbackRole); + + topic.addLoggingConfig({ + protocol: LoggingProtocol.HTTP, + failureFeedbackRole: feedbackRole, + successFeedbackRole: feedbackRole, + successFeedbackSampleRate: 50, + }); } } diff --git a/packages/aws-cdk-lib/aws-sns/README.md b/packages/aws-cdk-lib/aws-sns/README.md index 542482e50c3d8..dfde7e3312cba 100644 --- a/packages/aws-cdk-lib/aws-sns/README.md +++ b/packages/aws-cdk-lib/aws-sns/README.md @@ -206,3 +206,45 @@ const topicPolicy = new sns.TopicPolicy(this, 'Policy', { policyDocument, }); ``` + +## Delivery status logging + +Amazon SNS provides support to log the delivery status of notification messages sent to topics with the following Amazon SNS endpoints: + +- HTTP +- Amazon Kinesis Data Firehose +- AWS Lambda +- Platform application endpoint +- Amazon Simple Queue Service + +Example with a delivery status logging configuration for SQS: + +```ts +declare const role: iam.Role; +const topic = new sns.Topic(this, 'MyTopic', { + loggingConfigs: [ + { + protocol: sns.LoggingProtocol.SQS, + failureFeedbackRole: role, + successFeedbackRole: role, + successFeedbackSampleRate: 50, + }, + ], +}); +``` + +A delivery status logging configuration can also be added to your topic by `addLoggingConfig` method: + +```ts +declare const role: iam.Role; +const topic = new sns.Topic(this, 'MyTopic'); + +topic.addLoggingConfig({ + protocol: sns.LoggingProtocol.SQS, + failureFeedbackRole: role, + successFeedbackRole: role, + successFeedbackSampleRate: 50, +}); +``` + +Note that valid values for `successFeedbackSampleRate` are integer between 0-100. \ No newline at end of file diff --git a/packages/aws-cdk-lib/aws-sns/lib/topic.ts b/packages/aws-cdk-lib/aws-sns/lib/topic.ts index 7478c6ea0a415..15604b1cf2f40 100644 --- a/packages/aws-cdk-lib/aws-sns/lib/topic.ts +++ b/packages/aws-cdk-lib/aws-sns/lib/topic.ts @@ -1,8 +1,9 @@ import { Construct } from 'constructs'; import { CfnTopic } from './sns.generated'; import { ITopic, TopicBase } from './topic-base'; +import { IRole } from '../../aws-iam'; import { IKey } from '../../aws-kms'; -import { ArnFormat, Names, Stack } from '../../core'; +import { ArnFormat, Lazy, Names, Stack } from '../../core'; /** * Properties for a new SNS topic @@ -46,6 +47,80 @@ export interface TopicProps { * @default None */ readonly fifo?: boolean; + + /** + * The list of delivery status logging configurations for the topic. + * + * For more information, see https://docs.aws.amazon.com/sns/latest/dg/sns-topic-attributes.html. + * + * @default None + */ + readonly loggingConfigs?: LoggingConfig[]; +} + +/** + * A logging configuration for delivery status of messages sent from SNS topic to subscribed endpoints. + * + * For more information, see https://docs.aws.amazon.com/sns/latest/dg/sns-topic-attributes.html. + */ +export interface LoggingConfig { + /** + * Indicates one of the supported protocols for the SNS topic. + */ + readonly protocol: LoggingProtocol; + + /** + * The IAM role to be used when logging failed message deliveries in Amazon CloudWatch. + * + * @default None + */ + readonly failureFeedbackRole?: IRole; + + /** + * The IAM role to be used when logging successful message deliveries in Amazon CloudWatch. + * + * @default None + */ + readonly successFeedbackRole?: IRole; + + /** + * The percentage of successful message deliveries to be logged in Amazon CloudWatch. + * + * Valid values are integer between 0-100 + * + * @default None + */ + readonly successFeedbackSampleRate?: number; +} + +/** + * The type of supported protocol for delivery status logging. + */ +export enum LoggingProtocol { + /** + * HTTP + */ + HTTP = 'http/s', + + /** + * Amazon Simple Queue Service + */ + SQS = 'sqs', + + /** + * AWS Lambda + */ + LAMBDA = 'lambda', + + /** + * Amazon Kinesis Data Firehose + */ + FIREHOSE = 'firehose', + + /** + * Platform application endpoint + */ + APPLICATION = 'application', } /** @@ -81,6 +156,8 @@ export class Topic extends TopicBase { protected readonly autoCreatePolicy: boolean = true; + private readonly loggingConfigs: LoggingConfig[] = []; + constructor(scope: Construct, id: string, props: TopicProps = {}) { super(scope, id, { physicalName: props.topicName, @@ -90,6 +167,10 @@ export class Topic extends TopicBase { throw new Error('Content based deduplication can only be enabled for FIFO SNS topics.'); } + if (props.loggingConfigs) { + props.loggingConfigs.forEach(c => this.addLoggingConfig(c)); + } + let cfnTopicName: string; if (props.fifo && props.topicName && !props.topicName.endsWith('.fifo')) { cfnTopicName = this.physicalName + '.fifo'; @@ -110,6 +191,7 @@ export class Topic extends TopicBase { kmsMasterKeyId: props.masterKey && props.masterKey.keyArn, contentBasedDeduplication: props.contentBasedDeduplication, fifoTopic: props.fifo, + deliveryStatusLogging: Lazy.any({ produce: () => this.renderLoggingConfigs() }, { omitEmptyArray: true }), }); this.topicArn = this.getResourceArnAttribute(resource.ref, { @@ -120,4 +202,30 @@ export class Topic extends TopicBase { this.fifo = props.fifo || false; this.contentBasedDeduplication = props.contentBasedDeduplication || false; } + + private renderLoggingConfigs(): CfnTopic.LoggingConfigProperty[] { + return this.loggingConfigs.map(renderLoggingConfig); + + function renderLoggingConfig(spec: LoggingConfig): CfnTopic.LoggingConfigProperty { + if (spec.successFeedbackSampleRate !== undefined) { + const rate = spec.successFeedbackSampleRate; + if (!Number.isInteger(rate) || rate < 0 || rate > 100) { + throw new Error('Success feedback sample rate must be an integer between 0 and 100'); + } + } + return { + protocol: spec.protocol, + failureFeedbackRoleArn: spec.failureFeedbackRole?.roleArn, + successFeedbackRoleArn: spec.successFeedbackRole?.roleArn, + successFeedbackSampleRate: spec.successFeedbackSampleRate?.toString(), + }; + } + } + + /** + * Adds a delivery status logging configuration to the topic. + */ + public addLoggingConfig(config: LoggingConfig) { + this.loggingConfigs.push(config); + } } diff --git a/packages/aws-cdk-lib/aws-sns/test/sns.test.ts b/packages/aws-cdk-lib/aws-sns/test/sns.test.ts index 461575fb10038..45bb004e0deae 100644 --- a/packages/aws-cdk-lib/aws-sns/test/sns.test.ts +++ b/packages/aws-cdk-lib/aws-sns/test/sns.test.ts @@ -503,4 +503,109 @@ describe('Topic', () => { }); }); + + test('specify delivery status logging configuration through construct props', () => { + // GIVEN + const stack = new cdk.Stack(); + const feedbackRole = new iam.Role(stack, 'feedbackRole', { + assumedBy: new iam.ServicePrincipal('sns.amazonaws.com'), + }); + + // WHEN + new sns.Topic(stack, 'MyTopic', { + loggingConfigs: [ + { + protocol: sns.LoggingProtocol.SQS, + failureFeedbackRole: feedbackRole, + successFeedbackRole: feedbackRole, + successFeedbackSampleRate: 50, + }, + ], + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::SNS::Topic', { + 'DeliveryStatusLogging': [{ + 'Protocol': 'sqs', + 'SuccessFeedbackRoleArn': { 'Fn::GetAtt': ['feedbackRole2010903F', 'Arn'] }, + 'FailureFeedbackRoleArn': { 'Fn::GetAtt': ['feedbackRole2010903F', 'Arn'] }, + 'SuccessFeedbackSampleRate': '50', + }], + }); + }); + + test('add delivery status logging configuration to a topic', () => { + // GIVEN + const stack = new cdk.Stack(); + const feedbackRole = new iam.Role(stack, 'feedbackRole', { + assumedBy: new iam.ServicePrincipal('sns.amazonaws.com'), + }); + const topic = new sns.Topic(stack, 'MyTopic'); + + // WHEN + topic.addLoggingConfig({ + protocol: sns.LoggingProtocol.HTTP, + failureFeedbackRole: feedbackRole, + successFeedbackRole: feedbackRole, + successFeedbackSampleRate: 50, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::SNS::Topic', { + 'DeliveryStatusLogging': [{ + 'Protocol': 'http/s', + 'SuccessFeedbackRoleArn': { 'Fn::GetAtt': ['feedbackRole2010903F', 'Arn'] }, + 'FailureFeedbackRoleArn': { 'Fn::GetAtt': ['feedbackRole2010903F', 'Arn'] }, + 'SuccessFeedbackSampleRate': '50', + }], + }); + }); + + test('fails if success feedback sample rate is outside the appropriate range', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'my-stack'); + const feedbackRole = new iam.Role(stack, 'feedbackRole', { + assumedBy: new iam.ServicePrincipal('sns.amazonaws.com'), + }); + + // WHEN + new sns.Topic(stack, 'MyTopic', { + loggingConfigs: [ + { + protocol: sns.LoggingProtocol.SQS, + failureFeedbackRole: feedbackRole, + successFeedbackRole: feedbackRole, + successFeedbackSampleRate: 110, + }, + ], + }); + + // THEN + expect(() => app.synth()).toThrow(/Success feedback sample rate must be an integer between 0 and 100/); + }); + + test('fails if success feedback sample rate is decimal', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'my-stack'); + const feedbackRole = new iam.Role(stack, 'feedbackRole', { + assumedBy: new iam.ServicePrincipal('sns.amazonaws.com'), + }); + + // WHEN + new sns.Topic(stack, 'MyTopic', { + loggingConfigs: [ + { + protocol: sns.LoggingProtocol.SQS, + failureFeedbackRole: feedbackRole, + successFeedbackRole: feedbackRole, + successFeedbackSampleRate: 50.4, + }, + ], + }); + + // THEN + expect(() => app.synth()).toThrow(/Success feedback sample rate must be an integer between 0 and 100/); + }); });