├── .eslintrc.json ├── .github └── ISSUE_TEMPLATE │ └── bug_report.md ├── .gitignore ├── .husky └── pre-commit ├── .npmignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bin └── cdk-integ-tests-demo.ts ├── cdk.json ├── images └── sample-architecture.png ├── integ-tests └── integ.sns-sqs-ddb.ts ├── jest.config.js ├── lib ├── cdk-integ-tests-demo-stack.ts └── functions │ └── sqs-ddb-function.ts ├── npm-audit.json ├── package-lock.json ├── package.json └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": "2020", 6 | "sourceType": "module", 7 | "project": "./tsconfig.json" 8 | }, 9 | "env": { 10 | "jest": true, 11 | "node": true 12 | }, 13 | "plugins": [ 14 | "@typescript-eslint", 15 | "awscdk", 16 | "import", 17 | "jest" 18 | ], 19 | "extends": [ 20 | "eslint:recommended", 21 | "plugin:@typescript-eslint/recommended", 22 | "plugin:awscdk/all", 23 | "plugin:jest/recommended" 24 | ], 25 | "settings": { 26 | "import/parsers": { 27 | "@typescript-eslint/parser": [ 28 | ".ts" 29 | ] 30 | }, 31 | "import/resolver": { 32 | "node": { 33 | "extensions": [".js", ".ts"] 34 | }, 35 | "typescript": { 36 | "directory": "./tsconfig.json" 37 | } 38 | } 39 | }, 40 | "ignorePatterns": [ 41 | "*.js", 42 | "*.d.ts", 43 | "node_modules/", 44 | "*.generated.ts" 45 | ], 46 | "rules": { 47 | "@typescript-eslint/no-require-imports": [ 48 | "error" 49 | ], 50 | "@typescript-eslint/indent": [ 51 | "error", 52 | 2, 53 | { 54 | "ignoreComments": false 55 | } 56 | ], 57 | "eol-last": ["error", "always"], 58 | "quotes": [ 59 | "error", 60 | "single", 61 | { 62 | "avoidEscape": true 63 | } 64 | ], 65 | "@typescript-eslint/no-non-null-assertion": "off", 66 | "comma-dangle": [ 67 | "error", 68 | "always-multiline" 69 | ], 70 | "comma-spacing": [ 71 | "error", 72 | { 73 | "before": false, 74 | "after": true 75 | } 76 | ], 77 | "no-multi-spaces": [ 78 | "error", 79 | { 80 | "ignoreEOLComments": false 81 | } 82 | ], 83 | "array-bracket-spacing": [ 84 | "error", 85 | "never" 86 | ], 87 | "arrow-spacing": [ 88 | "error", 89 | { 90 | "before": true, 91 | "after": true 92 | } 93 | ], 94 | "array-bracket-newline": [ 95 | "error", 96 | "consistent" 97 | ], 98 | "object-curly-spacing": [ 99 | "error", 100 | "always" 101 | ], 102 | "object-curly-newline": [ 103 | "error", 104 | { 105 | "multiline": true, 106 | "consistent": true 107 | } 108 | ], 109 | "object-property-newline": [ 110 | "error", 111 | { 112 | "allowAllPropertiesOnSameLine": true 113 | } 114 | ], 115 | "keyword-spacing": [ 116 | "error" 117 | ], 118 | "brace-style": [ 119 | "error", 120 | "1tbs", 121 | { 122 | "allowSingleLine": true 123 | } 124 | ], 125 | "space-before-blocks": "error", 126 | "curly": [ 127 | "error", 128 | "multi-line", 129 | "consistent" 130 | ], 131 | "import/no-unresolved": [ 132 | "error", 133 | { 134 | "ignore": [ 135 | "aws-lambda" 136 | ] 137 | } 138 | ], 139 | "no-duplicate-imports": [ 140 | "error" 141 | ], 142 | "no-shadow": [ 143 | "off" 144 | ], 145 | "@typescript-eslint/no-shadow": [ 146 | "error" 147 | ], 148 | "semi": [ 149 | "error", 150 | "always" 151 | ], 152 | "key-spacing": [ 153 | "error" 154 | ], 155 | "@typescript-eslint/no-unused-vars": ["error"], 156 | "no-multiple-empty-lines": [ 157 | "error" 158 | ], 159 | "max-len": [ 160 | "error", 161 | { 162 | "code": 150, 163 | "ignoreUrls": true, 164 | "ignoreStrings": true, 165 | "ignoreTemplateLiterals": true, 166 | "ignoreComments": true, 167 | "ignoreRegExpLiterals": true 168 | } 169 | ], 170 | "@typescript-eslint/no-floating-promises": [ 171 | "error" 172 | ], 173 | "no-return-await": "off", 174 | "@typescript-eslint/return-await": "error", 175 | "@typescript-eslint/type-annotation-spacing": [ 176 | "error" 177 | ], 178 | "no-trailing-spaces": [ 179 | "error" 180 | ], 181 | "dot-notation": [ 182 | "error" 183 | ], 184 | "no-bitwise": [ 185 | "error" 186 | ], 187 | "@typescript-eslint/naming-convention": [ 188 | "error", 189 | { 190 | "selector": "enumMember", 191 | "format": [ 192 | "PascalCase", 193 | "UPPER_CASE" 194 | ] 195 | }, 196 | { 197 | "selector": "variableLike", 198 | "format": [ 199 | "camelCase", 200 | "UPPER_CASE" 201 | ], 202 | "leadingUnderscore": "allow" 203 | }, 204 | { 205 | "selector": "typeLike", 206 | "format": [ 207 | "PascalCase" 208 | ], 209 | "leadingUnderscore": "allow" 210 | }, 211 | { 212 | "selector": "memberLike", 213 | "format": [ 214 | "camelCase", 215 | "PascalCase", 216 | "UPPER_CASE" 217 | ], 218 | "leadingUnderscore": "allow" 219 | } 220 | ], 221 | "@typescript-eslint/member-ordering": [ 222 | "error", 223 | { 224 | "default": [ 225 | "public-static-field", 226 | "public-static-method", 227 | "protected-static-field", 228 | "protected-static-method", 229 | "private-static-field", 230 | "private-static-method", 231 | "field", 232 | "constructor", 233 | "method" 234 | ] 235 | } 236 | ], 237 | "jest/expect-expect": "off", 238 | "jest/no-conditional-expect": "off", 239 | "jest/no-done-callback": "off", 240 | "jest/no-standalone-expect": "off", 241 | "jest/valid-expect": "off", 242 | "jest/valid-title": "off" 243 | } 244 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: iriskraja77 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Versions (please complete the following information):** 23 | - CDK version: 24 | - Integ runner version: 25 | - Integ tests version: 26 | - NPM version: 27 | - Node.js version: 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | 10 | .cache -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run lint 5 | npm run cdk synth 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Write AWS CDK Integration tests using CDK integ-test and CDK integ-runner constructs 2 | 3 | ## 4 | 5 | ![Stability: Stable](https://img.shields.io/badge/stability-Stable-success.svg?style=for-the-badge) 6 | 7 | > **This is a stable example. It should successfully build out of the box** 8 | > 9 | > This example uses the core CDK library, and does not have any infrastructure prerequisites to build. 10 | 11 | --- 12 | 13 | 14 | 15 | This example demonstrates how to write integration tests for your CDK applications using the [AWS CDK integ-test](https://docs.aws.amazon.com/cdk/api/v2/docs/integ-tests-alpha-readme.html) CDK construct and [integ-runner CLI Tool](https://github.com/aws/aws-cdk/tree/main/packages/%40aws-cdk/integ-runner). 16 | 17 | Our example application is a serverless data enrichment application with persistence shown in Figure 1. CDK integration tests are written for this application under the ```integ-tests/``` folder. When these tests are run, it creates a separate integration test stack (a copy of your operational application) and runs the test against this isolated environment. 18 | 19 | ![Figure 1](./images/sample-architecture.png) 20 | 21 | ## Prerequisites 22 | 23 | You should have a basic understanding of AWS CDK and event-driven architecture. 24 | 25 | - An AWS account 26 | - NodeJS and Npm are installed 27 | - Install AWS CDK version 2.73.0 or later 28 | - Clone this repository 29 | 30 | ## How to run 31 | 32 | Configure your AWS CLI credentials in your terminal: 33 | ```bash 34 | aws configure 35 | ``` 36 | 37 | Install the project dependencies: 38 | 39 | ```bash 40 | npm install 41 | ``` 42 | 43 | Build the TS application: 44 | 45 | ```bash 46 | npm run build 47 | ``` 48 | 49 | Run integration test: 50 | 51 | ```bash 52 | npm run integ-test 53 | ``` 54 | 55 | To clean the generated build filed in Javascript run: 56 | 57 | ```bash 58 | npm run clean 59 | ``` 60 | 61 | To lint the repository code according to the rules in .eslintrc.json run: 62 | 63 | ```bash 64 | npm run lint:fix 65 | ``` 66 | 67 | ## Helpful resources 68 | For information on how to get started with these constructs, please refer to [AWS CDK Integ Test documentation](https://docs.aws.amazon.com/cdk/api/v2/docs/integ-tests-alpha-readme.html). 69 | 70 | ## Security 71 | 72 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 73 | 74 | ## License 75 | 76 | This library is licensed under the MIT-0 License. See the LICENSE file. 77 | -------------------------------------------------------------------------------- /bin/cdk-integ-tests-demo.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import 'source-map-support/register'; 5 | import { Aspects, App } from 'aws-cdk-lib'; 6 | import { AwsSolutionsChecks } from 'cdk-nag'; 7 | import { CdkIntegTestsDemoStack } from '../lib/cdk-integ-tests-demo-stack'; 8 | 9 | const app = new App(); 10 | new CdkIntegTestsDemoStack(app, 'ApplicationStack', { 11 | setDestroyPolicyToAllResources: true, 12 | }); 13 | 14 | // Add the cdk-nag AwsSolutions Pack with extra verbose logging enabled. 15 | Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true })); 16 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/cdk-integ-tests-demo.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/core:checkSecretUsage": true, 22 | "@aws-cdk/core:target-partitions": [ 23 | "aws", 24 | "aws-cn" 25 | ], 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 32 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 33 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 34 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 35 | "@aws-cdk/core:enablePartitionLiterals": true, 36 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 37 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 38 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /images/sample-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/cdk-integ-tests-sample/7d2b0d4d0d4dc182f9340ff4330449e8f0c5f75d/images/sample-architecture.png -------------------------------------------------------------------------------- /integ-tests/integ.sns-sqs-ddb.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | /// !cdk-integ IntegrationTestStack 5 | import 'source-map-support/register'; 6 | import { IntegTest, ExpectedResult } from '@aws-cdk/integ-tests-alpha'; 7 | import { App, Duration, Aspects } from 'aws-cdk-lib'; 8 | import { CdkIntegTestsDemoStack } from '../lib/cdk-integ-tests-demo-stack'; 9 | import { AwsSolutionsChecks } from 'cdk-nag'; 10 | // CDK App for Integration Tests 11 | const app = new App(); 12 | // Add the cdk-nag AwsSolutions Pack with extra verbose logging enabled. 13 | Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true })); 14 | // Stack under test 15 | const stackUnderTest = new CdkIntegTestsDemoStack(app, 'IntegrationTestStack', { 16 | setDestroyPolicyToAllResources: true, 17 | description: 18 | "This stack includes the application's resources for integration testing.", 19 | }); 20 | 21 | // Initialize Integ Test construct 22 | const integ = new IntegTest(app, 'DataFlowTest', { 23 | testCases: [stackUnderTest], // Define a list of cases for this test 24 | cdkCommandOptions: { 25 | // Customize the integ-runner parameters 26 | destroy: { 27 | args: { 28 | force: true, 29 | }, 30 | }, 31 | }, 32 | regions: [stackUnderTest.region], 33 | }); 34 | 35 | /** 36 | * Assertion: 37 | * The application should handle single message and write the enriched item to the DynamoDB table. 38 | */ 39 | const id = 'test-id-1'; 40 | const message = 'This message should be validated'; 41 | /** 42 | * Publish a message to the SNS topic. 43 | * Note - SNS topic ARN is a member variable of the 44 | * application stack for testing purposes. 45 | */ 46 | const assertion = integ.assertions 47 | .awsApiCall('SNS', 'publish', { 48 | TopicArn: stackUnderTest.topicArn, 49 | Message: JSON.stringify({ 50 | id: id, 51 | message: message, 52 | }), 53 | }) 54 | /** 55 | * Validate that the DynamoDB table contains the enriched message. 56 | */ 57 | .next( 58 | integ.assertions 59 | .awsApiCall('DynamoDB', 'getItem', { 60 | TableName: stackUnderTest.tableName, 61 | Key: { id: { S: id } }, 62 | }) 63 | /** 64 | * Expect the enriched message to be returned. 65 | */ 66 | .expect( 67 | ExpectedResult.objectLike({ 68 | Item: { 69 | id: { 70 | S: id, 71 | }, 72 | message: { 73 | S: message, 74 | }, 75 | additionalAttr: { 76 | S: 'enriched', 77 | }, 78 | }, 79 | }), 80 | ) 81 | /** 82 | * Timeout and interval check for assertion to be true. 83 | * Note - Data may take some time to arrive in DynamoDB. 84 | * Iteratively executes API call at specified interval. 85 | */ 86 | .waitForAssertions({ 87 | totalTimeout: Duration.seconds(25), 88 | interval: Duration.seconds(3), 89 | }), 90 | ); 91 | 92 | // Add the required permissions to the api call 93 | assertion.provider.addToRolePolicy({ 94 | Effect: 'Allow', 95 | Action: [ 96 | 'kms:Encrypt', 97 | 'kms:ReEncrypt*', 98 | 'kms:GenerateDataKey*', 99 | 'kms:Decrypt', 100 | ], 101 | Resource: [stackUnderTest.kmsKeyArn], 102 | }); 103 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /lib/cdk-integ-tests-demo-stack.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import * as cdk from 'aws-cdk-lib'; 5 | import * as sns from 'aws-cdk-lib/aws-sns'; 6 | import * as logs from 'aws-cdk-lib/aws-logs'; 7 | import * as kms from 'aws-cdk-lib/aws-kms'; 8 | import * as sqs from 'aws-cdk-lib/aws-sqs'; 9 | import * as iam from 'aws-cdk-lib/aws-iam'; 10 | import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; 11 | import * as lambdaNodejs from 'aws-cdk-lib/aws-lambda-nodejs'; 12 | import * as lambda from 'aws-cdk-lib/aws-lambda'; 13 | import * as lambdaEventSources from 'aws-cdk-lib/aws-lambda-event-sources'; 14 | import * as snsSubscriptions from 'aws-cdk-lib/aws-sns-subscriptions'; 15 | import * as path from 'path'; 16 | import { Construct, IConstruct } from 'constructs'; 17 | import { CfnResource } from 'aws-cdk-lib'; 18 | 19 | interface CdkIntegTestsDemoStackProps extends cdk.StackProps { 20 | /** 21 | * Whether to set all removal policies to DESTROY 22 | */ 23 | setDestroyPolicyToAllResources?: boolean; 24 | } 25 | 26 | /** 27 | * The demo application stack defining a serverless data enrichment stack 28 | */ 29 | export class CdkIntegTestsDemoStack extends cdk.Stack { 30 | public readonly topicArn: string; 31 | public readonly tableName: string; 32 | public readonly kmsKeyArn: string; 33 | public readonly functionName: string; 34 | 35 | constructor( 36 | scope: Construct, 37 | id: string, 38 | props?: CdkIntegTestsDemoStackProps, 39 | ) { 40 | super(scope, id, props); 41 | 42 | // KMS key for server-side encryption 43 | const kmsKey = new kms.Key(this, 'KmsKey', { 44 | enableKeyRotation: true, 45 | }); 46 | this.kmsKeyArn = kmsKey.keyArn; 47 | 48 | // SNS topic 49 | const snsTopic = new sns.Topic(this, 'Topic', { 50 | masterKey: kmsKey, 51 | }); 52 | this.topicArn = snsTopic.topicArn; 53 | 54 | // SQS queue with a dead letter queue 55 | const dlq = new sqs.Queue(this, 'Dlq', { 56 | enforceSSL: true, 57 | }); 58 | 59 | const sqsQueue = new sqs.Queue(this, 'Queue', { 60 | enforceSSL: true, 61 | deadLetterQueue: { 62 | maxReceiveCount: 2, 63 | queue: dlq, 64 | }, 65 | }); 66 | 67 | // DynamoDB table 68 | const table = new dynamodb.Table(this, 'Table', { 69 | partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING }, 70 | billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, 71 | pointInTimeRecovery: true, 72 | }); 73 | this.tableName = table.tableName; 74 | 75 | // Lambda function 76 | const functionName = `integ-lambda-${this.stackName}`; 77 | 78 | // The lambda function's log group 79 | const logGroup = new logs.LogGroup(this, 'LogGroup', { 80 | logGroupName: `/aws/lambda/${functionName}`, 81 | }); 82 | 83 | // The Lambda function's role with logging permissions 84 | const lambdaRole = new iam.Role(this, 'Role', { 85 | assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), 86 | inlinePolicies: { 87 | logging: new iam.PolicyDocument({ 88 | statements: [ 89 | new iam.PolicyStatement({ 90 | actions: [ 91 | 'logs:CreateLogGroup', 92 | 'logs:CreateLogStream', 93 | 'logs:PutLogEvents', 94 | ], 95 | resources: [logGroup.logGroupArn], 96 | }), 97 | ], 98 | }), 99 | }, 100 | }); 101 | 102 | // The lambda enricher function 103 | const enricherFunction = new lambdaNodejs.NodejsFunction( 104 | this, 105 | 'EnricherLambda', 106 | { 107 | functionName: functionName, 108 | runtime: lambda.Runtime.NODEJS_18_X, 109 | entry: path.join(__dirname, './functions/sqs-ddb-function.ts'), 110 | timeout: cdk.Duration.seconds(30), 111 | environment: { 112 | TABLE_NAME: table.tableName, 113 | }, 114 | role: lambdaRole, 115 | }, 116 | ); 117 | this.functionName = enricherFunction.functionName; 118 | 119 | // Allow Lambda to write data to the DynamoDB table 120 | table.grantWriteData(enricherFunction); 121 | 122 | // SQS Queue subscribes to SNS 123 | snsTopic.addSubscription(new snsSubscriptions.SqsSubscription(sqsQueue)); 124 | 125 | // Lambda is triggered by SQS 126 | enricherFunction.addEventSource( 127 | new lambdaEventSources.SqsEventSource(sqsQueue), 128 | ); 129 | 130 | // If Destroy Policy Aspect is present: 131 | if (props?.setDestroyPolicyToAllResources) { 132 | cdk.Aspects.of(this).add(new ApplyDestroyPolicyAspect()); 133 | } 134 | } 135 | } 136 | /** 137 | * Aspect for setting all removal policies to DESTROY 138 | */ 139 | class ApplyDestroyPolicyAspect implements cdk.IAspect { 140 | public visit(node: IConstruct): void { 141 | if (node instanceof CfnResource) { 142 | node.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY); 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /lib/functions/sqs-ddb-function.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb'; 5 | import { SQSEvent } from 'aws-lambda'; 6 | 7 | /** 8 | * The DynamoDBClient to interact with DynamoDB 9 | */ 10 | const client = new DynamoDBClient({}); 11 | 12 | /** 13 | * Helper function for parsing data of type object or string 14 | * @param data - The data object to be parsed 15 | * @returns - The parsed data object based on its type 16 | */ 17 | function parseData(data: object | string) { 18 | if (typeof data === 'object') return data; 19 | if (typeof data === 'string') return JSON.parse(data); 20 | 21 | return data; 22 | } 23 | 24 | /** 25 | * Lambda function handler that parses the SQS event and writes the item to a DynamoDB table 26 | * @param event - The event expected to be sent from SQS 27 | * @returns - Status code and information about updated and failed records 28 | */ 29 | export async function handler(event: SQSEvent) { 30 | const updatedRecords: string[] = []; 31 | const failedRecords: string[] = []; 32 | for (const record of event.Records) { 33 | const body = parseData(record.body); 34 | const message = parseData(body.Message); 35 | 36 | const putItem = new PutItemCommand({ 37 | Item: { 38 | id: { S: message.id }, 39 | message: { S: message.message }, 40 | additionalAttr: { S: 'enriched' }, 41 | }, 42 | TableName: process.env.TABLE_NAME, 43 | }); 44 | 45 | try { 46 | await client.send(putItem); 47 | updatedRecords.push(message.id); 48 | } catch (e) { 49 | console.log(e); 50 | failedRecords.push(message.id); 51 | } 52 | } 53 | 54 | const statusCode = failedRecords.length == 0 ? 200 : 500; 55 | return { 56 | statusCode: statusCode, 57 | updatedRecords: JSON.stringify(updatedRecords), 58 | failedRecords: JSON.stringify(failedRecords), 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /npm-audit.json: -------------------------------------------------------------------------------- 1 | { 2 | "auditReportVersion": 2, 3 | "vulnerabilities": {}, 4 | "metadata": { 5 | "vulnerabilities": { 6 | "info": 0, 7 | "low": 0, 8 | "moderate": 0, 9 | "high": 0, 10 | "critical": 0, 11 | "total": 0 12 | }, 13 | "dependencies": { 14 | "prod": 106, 15 | "dev": 382, 16 | "optional": 23, 17 | "peer": 0, 18 | "peerOptional": 0, 19 | "total": 487 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cdk-integ-tests-sample", 3 | "version": "0.1.0", 4 | "bin": { 5 | "cdk-integ-tests-demo": "bin/cdk-integ-tests-demo.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "clean": "tsc --build --clean", 10 | "watch": "tsc -w", 11 | "cdk": "cdk", 12 | "lint": "eslint . && awslint", 13 | "lint:fix": "eslint . --fix", 14 | "prepare": "husky install", 15 | "integ-test": "integ-runner --directory ./integ-tests --parallel-regions us-east-1 --update-on-failed" 16 | }, 17 | "devDependencies": { 18 | "@aws-cdk/integ-runner": "^2.88.0-alpha.0", 19 | "@aws-cdk/integ-tests-alpha": "^2.88.0-alpha.0", 20 | "@types/aws-lambda": "^8.10.119", 21 | "@types/jest": "^29.5.3", 22 | "@types/node": "20.4.2", 23 | "@types/prettier": "2.7.3", 24 | "@typescript-eslint/eslint-plugin": "^6.1.0", 25 | "aws-cdk": "2.88.0", 26 | "awslint": "^2.72.1", 27 | "cdk-nag": "^2.27.75", 28 | "esbuild": "^0.18.15", 29 | "eslint-plugin-awscdk": "^0.0.65", 30 | "eslint-plugin-import": "^2.27.5", 31 | "eslint-plugin-jest": "^27.2.3", 32 | "husky": "^8.0.3", 33 | "jest": "^29.6.1", 34 | "ts-jest": "^29.1.1", 35 | "ts-node": "^10.9.1", 36 | "typescript": "~5.1.6" 37 | }, 38 | "dependencies": { 39 | "@aws-sdk/client-dynamodb": "^3.370.0", 40 | "aws-cdk-lib": "2.88.0", 41 | "constructs": "^10.2.69", 42 | "source-map-support": "^0.5.21" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2021" 7 | ], 8 | "declaration": true, 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "noImplicitThis": true, 13 | "alwaysStrict": true, 14 | "noUnusedLocals": false, 15 | "noUnusedParameters": false, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": false, 18 | "inlineSourceMap": true, 19 | "inlineSources": true, 20 | "experimentalDecorators": true, 21 | "strictPropertyInitialization": false, 22 | "skipLibCheck": true, 23 | "typeRoots": [ 24 | "./node_modules/@types" 25 | ] 26 | }, 27 | "exclude": [ 28 | "node_modules", 29 | "cdk.out" 30 | ] 31 | } 32 | --------------------------------------------------------------------------------