From f60eeabe6f450991c2735b7143436c344049c215 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Mon, 24 Jul 2023 14:58:44 +0200 Subject: [PATCH 1/4] tests(ci): add testing package --- package-lock.json | 410 +++++++++++++++++- package.json | 3 +- packages/testing/jest.config.js | 28 ++ packages/testing/package.json | 49 +++ packages/testing/src/TestCase.ts | 211 +++++++++ packages/testing/src/TestStack.ts | 253 +++++++++++ packages/testing/src/constants.ts | 17 + packages/testing/src/factories.ts | 161 +++++++ packages/testing/src/index.ts | 1 + packages/testing/src/types/TestStack.ts | 39 ++ packages/testing/src/types/factories.ts | 242 +++++++++++ packages/testing/src/types/index.ts | 2 + .../helpers/populateEnvironmentVariables.ts | 16 + packages/testing/tsconfig.es.json | 31 ++ packages/testing/tsconfig.json | 32 ++ 15 files changed, 1493 insertions(+), 2 deletions(-) create mode 100644 packages/testing/jest.config.js create mode 100644 packages/testing/package.json create mode 100644 packages/testing/src/TestCase.ts create mode 100644 packages/testing/src/TestStack.ts create mode 100644 packages/testing/src/constants.ts create mode 100644 packages/testing/src/factories.ts create mode 100644 packages/testing/src/index.ts create mode 100644 packages/testing/src/types/TestStack.ts create mode 100644 packages/testing/src/types/factories.ts create mode 100644 packages/testing/src/types/index.ts create mode 100644 packages/testing/tests/helpers/populateEnvironmentVariables.ts create mode 100644 packages/testing/tsconfig.es.json create mode 100644 packages/testing/tsconfig.json diff --git a/package-lock.json b/package-lock.json index b0050f1e8e..266388962f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,8 @@ "docs/snippets", "layers", "examples/cdk", - "examples/sam" + "examples/sam", + "packages/testing" ], "devDependencies": { "@aws-cdk/cloudformation-diff": "^2.73.0", @@ -422,6 +423,15 @@ "md5": "^2.3.0" } }, + "node_modules/@aws-cdk/cli-lib-alpha": { + "version": "2.87.0-alpha.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cli-lib-alpha/-/cli-lib-alpha-2.87.0-alpha.0.tgz", + "integrity": "sha512-6hhu7YT4zn4ByPbN8R2pmLJTnYQUXqS5tqiBalzKy8luUnG9QIkvke9m8N/QnyLvjj7L4mWUjVNC4fGUjZsSBw==", + "dev": true, + "engines": { + "node": ">= 14.15.0" + } + }, "node_modules/@aws-cdk/cloud-assembly-schema": { "version": "2.73.0", "bundleDependencies": [ @@ -658,6 +668,10 @@ "resolved": "packages/parameters", "link": true }, + "node_modules/@aws-lambda-powertools/testing-utils": { + "resolved": "packages/testing", + "link": true + }, "node_modules/@aws-lambda-powertools/tracer": { "resolved": "packages/tracer", "link": true @@ -18463,6 +18477,400 @@ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, + "packages/testing": { + "name": "@aws-lambda-powertools/testing-utils", + "version": "1.11.1", + "license": "MIT-0", + "devDependencies": { + "@aws-cdk/cli-lib-alpha": "^2.87.0-alpha.0", + "aws-cdk-lib": "^2.73.0" + } + }, + "packages/testing/node_modules/aws-cdk-lib": { + "version": "2.73.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.73.0.tgz", + "integrity": "sha512-r9CUe3R7EThr9U0Eb7kQCK4Ee34TDeMH+bonvGD9rNRRTYDauvAgNCsx4DZYYksPrXLRzWjzVbuXAHaDDzWt+A==", + "bundleDependencies": [ + "@balena/dockerignore", + "case", + "fs-extra", + "ignore", + "jsonschema", + "minimatch", + "punycode", + "semver", + "table", + "yaml" + ], + "dev": true, + "dependencies": { + "@aws-cdk/asset-awscli-v1": "^2.2.97", + "@aws-cdk/asset-kubectl-v20": "^2.1.1", + "@aws-cdk/asset-node-proxy-agent-v5": "^2.0.77", + "@balena/dockerignore": "^1.0.2", + "case": "1.6.3", + "fs-extra": "^9.1.0", + "ignore": "^5.2.4", + "jsonschema": "^1.4.1", + "minimatch": "^3.1.2", + "punycode": "^2.3.0", + "semver": "^7.3.8", + "table": "^6.8.1", + "yaml": "1.10.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "constructs": "^10.0.0" + } + }, + "packages/testing/node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "Apache-2.0" + }, + "packages/testing/node_modules/aws-cdk-lib/node_modules/ajv": { + "version": "8.12.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "packages/testing/node_modules/aws-cdk-lib/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "packages/testing/node_modules/aws-cdk-lib/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "packages/testing/node_modules/aws-cdk-lib/node_modules/astral-regex": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "packages/testing/node_modules/aws-cdk-lib/node_modules/at-least-node": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "packages/testing/node_modules/aws-cdk-lib/node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "packages/testing/node_modules/aws-cdk-lib/node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "packages/testing/node_modules/aws-cdk-lib/node_modules/case": { + "version": "1.6.3", + "dev": true, + "inBundle": true, + "license": "(MIT OR GPL-3.0-or-later)", + "engines": { + "node": ">= 0.8.0" + } + }, + "packages/testing/node_modules/aws-cdk-lib/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "packages/testing/node_modules/aws-cdk-lib/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "packages/testing/node_modules/aws-cdk-lib/node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "packages/testing/node_modules/aws-cdk-lib/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "packages/testing/node_modules/aws-cdk-lib/node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "packages/testing/node_modules/aws-cdk-lib/node_modules/fs-extra": { + "version": "9.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "packages/testing/node_modules/aws-cdk-lib/node_modules/graceful-fs": { + "version": "4.2.10", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "packages/testing/node_modules/aws-cdk-lib/node_modules/ignore": { + "version": "5.2.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "packages/testing/node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "packages/testing/node_modules/aws-cdk-lib/node_modules/json-schema-traverse": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "packages/testing/node_modules/aws-cdk-lib/node_modules/jsonfile": { + "version": "6.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "packages/testing/node_modules/aws-cdk-lib/node_modules/jsonschema": { + "version": "1.4.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "packages/testing/node_modules/aws-cdk-lib/node_modules/lodash.truncate": { + "version": "4.4.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "packages/testing/node_modules/aws-cdk-lib/node_modules/lru-cache": { + "version": "6.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "packages/testing/node_modules/aws-cdk-lib/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "packages/testing/node_modules/aws-cdk-lib/node_modules/punycode": { + "version": "2.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "packages/testing/node_modules/aws-cdk-lib/node_modules/require-from-string": { + "version": "2.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "packages/testing/node_modules/aws-cdk-lib/node_modules/semver": { + "version": "7.3.8", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "packages/testing/node_modules/aws-cdk-lib/node_modules/slice-ansi": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "packages/testing/node_modules/aws-cdk-lib/node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "packages/testing/node_modules/aws-cdk-lib/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "packages/testing/node_modules/aws-cdk-lib/node_modules/table": { + "version": "6.8.1", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "packages/testing/node_modules/aws-cdk-lib/node_modules/universalify": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "packages/testing/node_modules/aws-cdk-lib/node_modules/uri-js": { + "version": "4.4.1", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "packages/testing/node_modules/aws-cdk-lib/node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "packages/testing/node_modules/aws-cdk-lib/node_modules/yaml": { + "version": "1.10.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "packages/tracer": { "name": "@aws-lambda-powertools/tracer", "version": "1.11.1", diff --git a/package.json b/package.json index 467365c53e..bf5968e93a 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "docs/snippets", "layers", "examples/cdk", - "examples/sam" + "examples/sam", + "packages/testing" ], "scripts": { "init-environment": "husky install", diff --git a/packages/testing/jest.config.js b/packages/testing/jest.config.js new file mode 100644 index 0000000000..6d6c2fecf2 --- /dev/null +++ b/packages/testing/jest.config.js @@ -0,0 +1,28 @@ +module.exports = { + displayName: { + name: 'Powertools for AWS Lambda (TypeScript) utility: TESTING', + color: 'blue', + }, + runner: 'groups', + preset: 'ts-jest', + transform: { + '^.+\\.ts?$': 'ts-jest', + }, + moduleFileExtensions: ['js', 'ts'], + collectCoverageFrom: ['**/src/**/*.ts', '!**/node_modules/**'], + testMatch: ['**/?(*.)+(spec|test).ts'], + roots: ['/src', '/tests'], + testPathIgnorePatterns: ['/node_modules/'], + testEnvironment: 'node', + coveragePathIgnorePatterns: ['/node_modules/', '/types/'], + coverageThreshold: { + global: { + statements: 100, + branches: 100, + functions: 100, + lines: 100, + }, + }, + coverageReporters: ['json-summary', 'text', 'lcov'], + setupFiles: ['/tests/helpers/populateEnvironmentVariables.ts'], +}; diff --git a/packages/testing/package.json b/packages/testing/package.json new file mode 100644 index 0000000000..5e9762219b --- /dev/null +++ b/packages/testing/package.json @@ -0,0 +1,49 @@ +{ + "name": "@aws-lambda-powertools/testing-utils", + "version": "1.11.1", + "description": "A package containing utilities to test your serverless workloads", + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com" + }, + "private": true, + "devDependencies": { + "@aws-cdk/cli-lib-alpha": "^2.87.0-alpha.0", + "aws-cdk-lib": "^2.73.0" + }, + "scripts": { + "test": "npm run test:unit", + "test:unit": "jest --group=unit --detectOpenHandles --coverage --verbose", + "test:e2e": "echo 'Not implemented'", + "watch": "jest --watch", + "build": "tsc", + "lint": "eslint --ext .ts,.js --no-error-on-unmatched-pattern .", + "lint-fix": "eslint --fix --ext .ts,.js --no-error-on-unmatched-pattern .", + "prebuild": "rimraf ./lib", + "prepack": "node ../../.github/scripts/release_patch_package_json.js ." + }, + "lint-staged": { + "*.{js,ts}": "npm run lint-fix" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/aws-powertools/powertools-lambda-typescript.git" + }, + "files": [ + "lib" + ], + "main": "./lib/index.js", + "types": "./lib/index.d.ts", + "keywords": [ + "aws", + "lambda", + "powertools", + "testing", + "serverless" + ], + "license": "MIT-0", + "bugs": { + "url": "https://github.com/aws-powertools/powertools-lambda-typescript/issues" + }, + "homepage": "https://github.com/aws-powertools/powertools-lambda-typescript/tree/main/packages/testing#readme" +} diff --git a/packages/testing/src/TestCase.ts b/packages/testing/src/TestCase.ts new file mode 100644 index 0000000000..d1692d790c --- /dev/null +++ b/packages/testing/src/TestCase.ts @@ -0,0 +1,211 @@ +import type { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; +import type { Table } from 'aws-cdk-lib/aws-dynamodb'; +import type { IStringParameter, StringParameter } from 'aws-cdk-lib/aws-ssm'; +import { PolicyStatement, Effect } from 'aws-cdk-lib/aws-iam'; + +class NodeJsFunction { + public functionName: string; + public ref: NodejsFunction; + + public constructor(name: string, ref: NodejsFunction) { + this.functionName = name; + this.ref = ref; + } +} + +class DynamoDBTable { + public ref: Table; + public tableName: string; + readonly #envVariableName?: string; + + public constructor(name: string, ref: Table, envVariableName?: string) { + this.tableName = name; + this.ref = ref; + this.#envVariableName = envVariableName; + } + + public get envVariableName(): string { + return this.#envVariableName || 'TABLE_NAME'; + } +} + +class SsmSecureString { + public parameterName: string; + public ref: IStringParameter; + readonly #envVariableName?: string; + + public constructor( + name: string, + ref: IStringParameter, + envVariableName?: string + ) { + this.parameterName = name; + this.ref = ref; + this.#envVariableName = envVariableName; + } + + public get envVariableName(): string { + return this.#envVariableName || 'SECURE_STRING_NAME'; + } +} + +class SsmString { + public parameterName: string; + public ref: StringParameter; + readonly #envVariableName?: string; + + public constructor( + name: string, + ref: StringParameter, + envVariableName?: string + ) { + this.parameterName = name; + this.ref = ref; + this.#envVariableName = envVariableName; + } + + public get envVariableName(): string { + return this.#envVariableName || 'SSM_STRING_NAME'; + } +} + +/** + * A test case that can be added to a test stack. + */ +class TestCase { + public testName: string; + #dynamodb?: DynamoDBTable; + #function?: NodeJsFunction; + #ssmSecureString?: SsmSecureString; + #ssmString?: SsmString; + + public constructor(testName: string) { + this.testName = testName; + } + + /** + * The NodejsFunction that is associated with this test case. + */ + public set function(fn: NodeJsFunction) { + if (this.#dynamodb) { + this.#grantAccessToDynamoDBTableAndSetEnv(fn, this.#dynamodb); + } + if (this.#ssmSecureString) { + this.#grantAccessToSsmeStringAndSetEnv(fn, this.#ssmSecureString); + } + if (this.#ssmString) { + this.#grantAccessToSsmeStringAndSetEnv(fn, this.#ssmString); + } + this.#function = fn; + } + + /** + * Get the NodejsFunction that is associated with this test case. + */ + public get function(): NodeJsFunction { + if (!this.#function) throw new Error('This test case has no function.'); + + return this.#function; + } + + /** + * The DynamoDB table that is associated with this test case. + */ + public set dynamodb(table: DynamoDBTable) { + if (this.#function) { + this.#grantAccessToDynamoDBTableAndSetEnv(this.#function, table); + } + this.#dynamodb = table; + } + + /** + * Get the DynamoDB table that is associated with this test case. + */ + public get dynamodb(): DynamoDBTable { + if (!this.#dynamodb) + throw new Error('This test case has no DynamoDB table.'); + + return this.#dynamodb; + } + + /** + * Grant access to the DynamoDB table and set the environment variable to + * the table name. + * + * @param fn - The function to grant access to the table and set the environment variable. + * @param table - The table to grant access to and identified by the environment variable. + */ + #grantAccessToDynamoDBTableAndSetEnv = ( + fn: NodeJsFunction, + table: DynamoDBTable + ): void => { + table.ref.grantReadWriteData(fn.ref); + fn.ref.addEnvironment(table.envVariableName, table.ref.tableName); + }; + + /** + * The SSM SecureString that is associated with this test case. + */ + public set ssmSecureString(parameter: SsmSecureString) { + if (this.#function) { + this.#grantAccessToSsmeStringAndSetEnv(this.#function, parameter); + } + this.#ssmSecureString = parameter; + } + + /** + * Get the SSM SecureString that is associated with this test case. + */ + public get ssmSecureString(): SsmSecureString { + if (!this.#ssmSecureString) + throw new Error('This test case has no SSM SecureString.'); + + return this.#ssmSecureString; + } + + /** + * The SSM String that is associated with this test case. + */ + public set ssmString(parameter: SsmString) { + if (this.#function) { + this.#grantAccessToSsmeStringAndSetEnv(this.#function, parameter); + } + this.#ssmString = parameter; + } + + /** + * Get the SSM String that is associated with this test case. + */ + public get ssmString(): SsmString { + if (!this.#ssmString) throw new Error('This test case has no SSM String.'); + + return this.#ssmString; + } + + /** + * Grant access to the SSM String and set the environment variable to + * the parameter name. + * + * @param fn - The function to grant access to the parameter and set the environment variable. + * @param parameter - The parameter to grant access to and identified by the environment variable. + */ + #grantAccessToSsmeStringAndSetEnv = ( + fn: NodeJsFunction, + parameter: SsmSecureString | SsmString + ): void => { + fn.ref.addEnvironment(parameter.envVariableName, parameter.parameterName); + // Grant access also to the path of the parameter + fn.ref.addToRolePolicy( + new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['ssm:GetParametersByPath'], + resources: [ + parameter.ref.parameterArn.split(':').slice(0, -1).join(':'), + ], + }) + ); + parameter.ref.grantRead(fn.ref); + }; +} + +export { TestCase, NodeJsFunction, DynamoDBTable, SsmSecureString, SsmString }; diff --git a/packages/testing/src/TestStack.ts b/packages/testing/src/TestStack.ts new file mode 100644 index 0000000000..34d963f250 --- /dev/null +++ b/packages/testing/src/TestStack.ts @@ -0,0 +1,253 @@ +import { App, Stack } from 'aws-cdk-lib'; +import { AwsCdkCli, RequireApproval } from '@aws-cdk/cli-lib-alpha'; +import { randomUUID } from 'node:crypto'; +import type { ICloudAssemblyDirectoryProducer } from '@aws-cdk/cli-lib-alpha'; +import { + dynamoDBTable, + dynamoDBItem, + nodejsFunction, + ssmSecureString, + ssmString, +} from './factories'; +import { TEST_RUNTIMES, defaultRuntime } from './constants'; +import { + NodeJsFunction, + DynamoDBTable, + SsmSecureString, + TestCase, + SsmString, +} from './TestCase'; +import type { + AddFunctionOptions, + AddDynamoDBTableOptions, + AddTestCaseOptions, + AddSsmSecureStringOptions, + AddSsmStringOptions, +} from './types'; + +class TestStack implements ICloudAssemblyDirectoryProducer { + public app: App; + public cli: AwsCdkCli; + public stack: Stack; + readonly #resourceNamePrefix; + readonly #runtime: keyof typeof TEST_RUNTIMES; + readonly #testCases = new Map(); + + public constructor(resourceNamePrefix: string, testName: string) { + this.#resourceNamePrefix = resourceNamePrefix; + const providedRuntime = process.env.RUNTIME || defaultRuntime; + if (!(providedRuntime in TEST_RUNTIMES)) { + throw new Error( + `Invalid runtime: ${providedRuntime}. Valid runtimes are: ${Object.keys( + TEST_RUNTIMES + ).join(', ')}` + ); + } + this.#runtime = providedRuntime as keyof typeof TEST_RUNTIMES; + this.app = new App(); + this.stack = new Stack(this.app, this.#generateUniqueName(testName)); + this.cli = AwsCdkCli.fromCloudAssemblyDirectoryProducer(this); + } + + /** + * Add a DynamoDB table to the test stack. + * + * @returns A reference to the DynamoDB table that was added to the stack. + */ + #addDynamoDBTable = ( + testCaseName: string, + options: AddDynamoDBTableOptions + ): DynamoDBTable => { + const resourceId = this.#generateUniqueName(`${testCaseName}-table`); + const name = options.name || resourceId; + const table = dynamoDBTable({ + ...options, + stack: this.stack, + resourceId, + name, + }); + + return new DynamoDBTable(name, table, options.envVariableName); + }; + + /** + * Add a NodejsFunction to the test stack. + * + * @returns A reference to the NodejsFunction that was added to the stack. + */ + #addFunction = ( + testCaseName: string, + options: AddFunctionOptions + ): NodeJsFunction => { + const resourceId = this.#generateUniqueName(`${testCaseName}-fn`); + const name = options.functionConfigs?.name || resourceId; + const fn = nodejsFunction({ + stack: this.stack, + resourceId, + runtime: this.#runtime, + functionConfigs: { + ...options.functionConfigs, + name, + }, + functionCode: options.functionCode, + }); + + return new NodeJsFunction(name, fn); + }; + + /** + * Add a SSM SecureString to the test stack. + */ + #addSsmSecureString = ( + testCaseName: string, + options: AddSsmSecureStringOptions + ): SsmSecureString => { + const resourceId = this.#generateUniqueName(`${testCaseName}-ssmSecure`); + const name = options.name || resourceId; + const ssmSecure = ssmSecureString({ + stack: this.stack, + resourceId, + name, + value: options.value, + }); + + return new SsmSecureString(name, ssmSecure, options.envVariableName); + }; + + /** + * Add a SSM String to the test stack. + */ + #addSsmString = ( + testCaseName: string, + options: AddSsmStringOptions + ): SsmString => { + const resourceId = this.#generateUniqueName(`${testCaseName}-ssmString`); + const name = options.name || resourceId; + const ssm = ssmString({ + stack: this.stack, + resourceId, + name, + value: options.value, + }); + + return new SsmString(name, ssm, options.envVariableName); + }; + + /** + * Create a TestCase and add it to the test stack. + * + * @param options - The options for creating a TestCase. + */ + public addTestCase = (options: AddTestCaseOptions): void => { + if (this.#testCases.has(options.testCaseName)) { + throw new Error( + `A test case with the name ${options.testCaseName} already exists.` + ); + } + // Initialize the test case + const test = new TestCase(options.testCaseName); + // Add the function to the test case + const fn = this.#addFunction(options.testCaseName, options.function); + test.function = fn; + // If the test case has a DynamoDB table, add it + if (options?.dynamodb) { + const table = this.#addDynamoDBTable( + options.testCaseName, + options.dynamodb + ); + // If the test case has items, add them to the table + if (options.dynamodb?.items) { + options.dynamodb.items.forEach((item, index) => { + dynamoDBItem({ + stack: this.stack, + resourceId: this.#generateUniqueName( + `${options.testCaseName}-item${index}` + ), + tableName: table.tableName, + tableArn: table.ref.tableArn, + item, + }); + }); + } + // Save a reference to the table in the test case + test.dynamodb = table; + } + // If the test case has a SSM SecureString, add it + if (options?.ssmSecureString) { + const ssmSecureString = this.#addSsmSecureString( + options.testCaseName, + options.ssmSecureString + ); + // Save a reference to the SSM SecureString in the test case + test.ssmSecureString = ssmSecureString; + } + // If the test case has a SSM String, add it + if (options?.ssmString) { + const ssmString = this.#addSsmString( + options.testCaseName, + options.ssmString + ); + // Save a reference to the SSM String in the test case + test.ssmString = ssmString; + } + + this.#testCases.set(options.testCaseName, test); + }; + + /** + * Deploy the test stack to the selected environment. + */ + public async deploy(): Promise { + await this.cli.deploy({ + stacks: [this.stack.stackName], + requireApproval: RequireApproval.NEVER, + }); + } + + /** + * Destroy the test stack. + */ + public async destroy(): Promise { + await this.cli.destroy({ + stacks: [this.stack.stackName], + requireApproval: false, + }); + } + + /** + * Get a test case from the test stack. + * + * If no test case with the provided name exists, an error is thrown. + * + * @param testCaseName - The name of the test case to get. + */ + public getTestCase = (testCaseName: string): TestCase => { + const testCase = this.#testCases.get(testCaseName); + if (!testCase) { + throw new Error(`No test case with name ${testCaseName} exists.`); + } + + return testCase; + }; + + public async produce(_context: Record): Promise { + return this.app.synth().directory; + } + + public async synth(): Promise { + await this.cli.synth({ + stacks: [this.stack.stackName], + }); + } + + #generateUniqueName = (resourceName: string): string => { + const uuid = randomUUID(); + + return `${this.#resourceNamePrefix}-${this.#runtime}-${uuid.substring( + 0, + 5 + )}-${resourceName}`.substring(0, 64); + }; +} + +export { TestStack }; diff --git a/packages/testing/src/constants.ts b/packages/testing/src/constants.ts new file mode 100644 index 0000000000..85d66e625a --- /dev/null +++ b/packages/testing/src/constants.ts @@ -0,0 +1,17 @@ +import { Runtime } from 'aws-cdk-lib/aws-lambda'; + +/** + * The default AWS Lambda runtime to use when none is provided. + */ +const defaultRuntime = 'nodejs18x'; + +/** + * The AWS Lambda runtimes that are supported by the project. + */ +const TEST_RUNTIMES = { + nodejs14x: Runtime.NODEJS_14_X, + nodejs16x: Runtime.NODEJS_16_X, + [defaultRuntime]: Runtime.NODEJS_18_X, +} as const; + +export { TEST_RUNTIMES, defaultRuntime }; diff --git a/packages/testing/src/factories.ts b/packages/testing/src/factories.ts new file mode 100644 index 0000000000..05c519fcc4 --- /dev/null +++ b/packages/testing/src/factories.ts @@ -0,0 +1,161 @@ +import { CfnOutput, Duration, RemovalPolicy } from 'aws-cdk-lib'; +import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; +import { Tracing, Architecture } from 'aws-cdk-lib/aws-lambda'; +import { RetentionDays } from 'aws-cdk-lib/aws-logs'; +import { PhysicalResourceId } from 'aws-cdk-lib/custom-resources'; +import { + AwsCustomResource, + AwsCustomResourcePolicy, +} from 'aws-cdk-lib/custom-resources'; +import { StringParameter } from 'aws-cdk-lib/aws-ssm'; +import { Table, BillingMode, AttributeType } from 'aws-cdk-lib/aws-dynamodb'; +import { TEST_RUNTIMES } from './constants'; +import { marshall } from '@aws-sdk/util-dynamodb'; +import type { IStringParameter } from 'aws-cdk-lib/aws-ssm'; +import type { + NodejsFunctionOptions, + DynamoDBTableOptions, + SsmSecureStringOptions, + DynamoDBItemOptions, + SsmStringOptions, +} from './types'; + +/** + * Add a NodejsFunction to the test stack. + * + * @param options - The options for creating a NodejsFunction. + */ +const nodejsFunction = (options: NodejsFunctionOptions): NodejsFunction => { + const nodeJsFunction = new NodejsFunction(options.stack, options.resourceId, { + runtime: TEST_RUNTIMES[options.runtime], + functionName: options.functionConfigs?.name, + timeout: Duration.seconds(options.functionConfigs?.timeout || 30), + entry: options?.functionCode?.path, + handler: options?.functionCode?.handler || 'handler', + environment: { + ...(options.functionConfigs?.environment || {}), + }, + tracing: options.functionConfigs?.tracing || Tracing.ACTIVE, + memorySize: options.functionConfigs?.memorySize || 256, + architecture: options.functionConfigs?.architecture || Architecture.X86_64, + logRetention: RetentionDays.ONE_DAY, + bundling: options?.functionCode?.bundling, + }); + + if (options.logGroupOutputKey) { + new CfnOutput(options.stack, options.logGroupOutputKey, { + value: nodeJsFunction.logGroup.logGroupName, + }); + } + + return nodeJsFunction; +}; + +/** + * Add a DynamoDB table to the test stack. + * + * @param options - The options for creating a DynamoDB table. + */ +const dynamoDBTable = (options: DynamoDBTableOptions): Table => { + const table = new Table(options.stack, options.resourceId, { + tableName: options.name, + partitionKey: { + name: options?.partitionKey?.name || 'id', + type: options?.partitionKey?.type || AttributeType.STRING, + }, + ...(options?.sortKey || {}), + billingMode: options.billingMode || BillingMode.PAY_PER_REQUEST, + removalPolicy: RemovalPolicy.DESTROY, + }); + + return table; +}; + +/** + * Add an item to a DynamoDB table in the test stack. + * + * @param options - The options for creating a DynamoDB item. + */ +const dynamoDBItem = (options: DynamoDBItemOptions): void => { + new AwsCustomResource(options.stack, options.resourceId, { + onCreate: { + service: 'DynamoDB', + action: 'putItem', + parameters: { + TableName: options.tableName, + Item: marshall(options.item), + }, + physicalResourceId: PhysicalResourceId.of(options.resourceId), + }, + policy: AwsCustomResourcePolicy.fromSdkCalls({ + resources: [options.tableArn], + }), + }); +}; + +/** + * Add a SSM String parameter to the test stack. + * + * @param options - The options for creating a SSM String parameter. + */ +const ssmString = (options: SsmStringOptions): StringParameter => { + const ssmString = new StringParameter(options.stack, options.resourceId, { + parameterName: options.name, + stringValue: options.value, + }); + + return ssmString; +}; + +/** + * Add a SSM SecureString parameter to the test stack. + * + * @param options - The options for creating a SSM SecureString parameter. + */ +const ssmSecureString = (options: SsmSecureStringOptions): IStringParameter => { + const resourceCreator = new AwsCustomResource( + options.stack, + `creator-${options.resourceId}`, + { + onCreate: { + service: 'SSM', + action: 'putParameter', + parameters: { + Name: options.name || options.resourceId, + Value: options.value, + Type: 'SecureString', + }, + physicalResourceId: PhysicalResourceId.of(options.resourceId), + }, + onDelete: { + service: 'SSM', + action: 'deleteParameter', + parameters: { + Name: options.name || options.resourceId, + }, + }, + policy: AwsCustomResourcePolicy.fromSdkCalls({ + resources: AwsCustomResourcePolicy.ANY_RESOURCE, + }), + } + ); + + const secureString = StringParameter.fromSecureStringParameterAttributes( + options.stack, + options.resourceId, + { + parameterName: options.name || options.resourceId, + } + ); + secureString.node.addDependency(resourceCreator); + + return secureString; +}; + +export { + nodejsFunction, + dynamoDBTable, + dynamoDBItem, + ssmSecureString, + ssmString, +}; diff --git a/packages/testing/src/index.ts b/packages/testing/src/index.ts new file mode 100644 index 0000000000..11d3cd4421 --- /dev/null +++ b/packages/testing/src/index.ts @@ -0,0 +1 @@ +export * from './TestStack'; diff --git a/packages/testing/src/types/TestStack.ts b/packages/testing/src/types/TestStack.ts new file mode 100644 index 0000000000..a93ff27dc3 --- /dev/null +++ b/packages/testing/src/types/TestStack.ts @@ -0,0 +1,39 @@ +import type { + NodejsFunctionOptions, + DynamoDBTableOptions, + SsmSecureStringOptions, + SsmStringOptions, +} from './factories'; + +type AddFunctionOptions = Omit< + NodejsFunctionOptions, + 'stack' | 'resourceId' | 'runtime' +>; + +type AddDynamoDBTableOptions = Omit< + DynamoDBTableOptions, + 'stack' | 'resourceId' +>; + +type AddSsmSecureStringOptions = Omit< + SsmSecureStringOptions, + 'stack' | 'resourceId' +>; + +type AddSsmStringOptions = Omit; + +type AddTestCaseOptions = { + testCaseName: string; + function: AddFunctionOptions; + dynamodb?: AddDynamoDBTableOptions; + ssmSecureString?: AddSsmSecureStringOptions; + ssmString?: AddSsmStringOptions; +}; + +export { + AddFunctionOptions, + AddDynamoDBTableOptions, + AddTestCaseOptions, + AddSsmSecureStringOptions, + AddSsmStringOptions, +}; diff --git a/packages/testing/src/types/factories.ts b/packages/testing/src/types/factories.ts new file mode 100644 index 0000000000..fbeeea984d --- /dev/null +++ b/packages/testing/src/types/factories.ts @@ -0,0 +1,242 @@ +import type { Stack } from 'aws-cdk-lib'; +import type { FunctionOptions } from 'aws-cdk-lib/aws-lambda'; +import type { NodejsFunctionProps } from 'aws-cdk-lib/aws-lambda-nodejs'; +import type { AttributeType, BillingMode } from 'aws-cdk-lib/aws-dynamodb'; +import { TEST_RUNTIMES } from '../constants'; + +/** + * Options for creating a NodejsFunction. + */ +type NodejsFunctionOptions = { + /** + * The stack to add the function to. + */ + stack: Stack; + /** + * The unique identifier for the function. + */ + resourceId: string; + /** + * The runtime for the function. + */ + runtime: keyof typeof TEST_RUNTIMES; + /** + * The function code. + */ + functionCode: { + /** + * The absolute path to the entry file. + * @default - None + */ + path: NodejsFunctionProps['entry']; + /** + * The name of the function handler. + * @default - handler + */ + handler?: NodejsFunctionProps['handler']; + /** + * Options for bundling with esbuild. + */ + bundling?: NodejsFunctionProps['bundling']; + }; + /** + * Function configurations. + */ + functionConfigs?: { + /** + * The amount of memory, in MB, that is allocated to your Lambda function. + * @default - The default value is 256 MB. + */ + memorySize?: FunctionOptions['memorySize']; + /** + * The function execution time (in seconds) after which Lambda terminates the function. + * @default - The default value is 30 seconds. + */ + timeout?: number; + /** + * The environment variables for the Lambda function service. + * @default - No environment variables. + */ + environment?: FunctionOptions['environment']; + /** + * A name for the function. + * @default - The utility will generate a unique name for the function. + */ + name?: FunctionOptions['functionName']; + /** + * Enable AWS X-Ray Tracing for Lambda Function. + * @default - Enabled + */ + tracing?: FunctionOptions['tracing']; + /** + * The Lambda runtime architecture to use. + * @default - x86_64 + */ + architecture?: FunctionOptions['architecture']; + }; + /** + * The name of the CloudFormation Output key to store the log group name. + * + * When this property is set, the log group name will be stored in the CloudFormation Output. + * + * @default - No output + */ + logGroupOutputKey?: string; +}; + +/** + * Options for creating a DynamoDB table. + */ +type DynamoDBTableOptions = { + /** + * The stack to add the table to. + */ + stack: Stack; + /** + * The unique identifier for the table. + */ + resourceId: string; + /** + * The name of the table. + */ + name?: string; + /** + * The partition key for the table. + * @default - id (string) + */ + partitionKey?: { + /** + * The name of the partition key. + * @default - id + */ + name?: string; + /** + * The type of the partition key. + * @default - AttributeType.STRING + */ + type?: AttributeType; + }; + /** + * The sort key for the table. + * @default - None + */ + sortKey?: { + /** + * The name of the sort key. + * @default - None + */ + name?: string; + /** + * The type of the sort key. + * @default - None + */ + type?: AttributeType; + }; + /** + * The billing mode for the table. + * @default - BillingMode.PAY_PER_REQUEST + */ + billingMode?: BillingMode; + /** + * The name of the environment variable that will store the table name + * and that will be applied to the function. + * @default - TABLE_NAME + */ + envVariableName?: string; + /** + * Items to add to the table. + */ + items?: Array; +}; + +/** + * Add an item to a DynamoDB table. + */ +type DynamoDBItemOptions = { + /** + * The stack to of the table to add the item to. + */ + stack: Stack; + /** + * The unique identifier for the item. + */ + resourceId: string; + /** + * The name of the table to add the item to. + */ + tableName: string; + /** + * The arn of the table to add the item to. + */ + tableArn: string; + /** + * The item to add to the table. + */ + item: Record; +}; + +/** + * Options for creating a SSM Secure String. + */ +type SsmSecureStringOptions = { + /** + * The stack to add the secure string to. + */ + stack: Stack; + /** + * The unique identifier for the secure string. + */ + resourceId: string; + /** + * The name of the secure string. + * @default - Same as resourceId + */ + name?: string; + /** + * The value of the secure string. + */ + value: string; + /** + * The name of the environment variable that will store the name + * of the string and that will be applied to the function. + * @default - SECURE_STRING_NAME + */ + envVariableName?: string; +}; + +/** + * Options for creating a SSM String. + */ +type SsmStringOptions = { + /** + * The stack to add the string to. + */ + stack: Stack; + /** + * The unique identifier for the string. + */ + resourceId: string; + /** + * The name of the string. + * @default - Same as resourceId + */ + name?: string; + /** + * The value of the string. + */ + value: string; + /** + * The name of the environment variable that will store the name + * of the string and that will be applied to the function. + * @default - SECURE_STRING_NAME + */ + envVariableName?: string; +}; + +export { + NodejsFunctionOptions, + DynamoDBTableOptions, + DynamoDBItemOptions, + SsmSecureStringOptions, + SsmStringOptions, +}; diff --git a/packages/testing/src/types/index.ts b/packages/testing/src/types/index.ts new file mode 100644 index 0000000000..71c096ea42 --- /dev/null +++ b/packages/testing/src/types/index.ts @@ -0,0 +1,2 @@ +export * from './factories'; +export * from './TestStack'; diff --git a/packages/testing/tests/helpers/populateEnvironmentVariables.ts b/packages/testing/tests/helpers/populateEnvironmentVariables.ts new file mode 100644 index 0000000000..4d4166743d --- /dev/null +++ b/packages/testing/tests/helpers/populateEnvironmentVariables.ts @@ -0,0 +1,16 @@ +// Reserved variables +process.env._X_AMZN_TRACE_ID = '1-abcdef12-3456abcdef123456abcdef12'; +process.env.AWS_LAMBDA_FUNCTION_NAME = 'my-lambda-function'; +process.env.AWS_EXECUTION_ENV = 'nodejs16.x'; +process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE = '128'; +if ( + process.env.AWS_REGION === undefined && + process.env.CDK_DEFAULT_REGION === undefined +) { + process.env.AWS_REGION = 'eu-west-1'; +} +process.env._HANDLER = 'index.handler'; + +// Powertools for AWS Lambda (TypeScript) variables +process.env.POWERTOOLS_SERVICE_NAME = 'hello-world'; +process.env.AWS_XRAY_LOGGING_LEVEL = 'silent'; diff --git a/packages/testing/tsconfig.es.json b/packages/testing/tsconfig.es.json new file mode 100644 index 0000000000..802f18e8f9 --- /dev/null +++ b/packages/testing/tsconfig.es.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "experimentalDecorators": true, + "noImplicitAny": true, + "target": "ES2020", + "module": "commonjs", + "declaration": true, + "declarationMap": true, + "outDir": "lib", + "removeComments": false, + "strict": true, + "inlineSourceMap": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "pretty": true, + "baseUrl": "src/", + "rootDirs": [ "src/" ] + }, + "include": [ "src/**/*", "tests/**/*" ], + "exclude": [ "./node_modules"], + "watchOptions": { + "watchFile": "useFsEvents", + "watchDirectory": "useFsEvents", + "fallbackPolling": "dynamicPriority" + }, + "lib": [ "es2020" ], + "types": [ + "jest", + "node" + ] +} \ No newline at end of file diff --git a/packages/testing/tsconfig.json b/packages/testing/tsconfig.json new file mode 100644 index 0000000000..8b93f8c299 --- /dev/null +++ b/packages/testing/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "experimentalDecorators": true, + "noImplicitAny": true, + "target": "ES2020", + "module": "commonjs", + "declaration": true, + "declarationMap": true, + "outDir": "lib", + "removeComments": false, + "strict": true, + "inlineSourceMap": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "pretty": true, + "baseUrl": "src/", + "rootDirs": [ "src/" ], + "esModuleInterop": true + }, + "include": [ "src/**/*" ], + "exclude": [ "./node_modules"], + "watchOptions": { + "watchFile": "useFsEvents", + "watchDirectory": "useFsEvents", + "fallbackPolling": "dynamicPriority" + }, + "lib": [ "es2020" ], + "types": [ + "jest", + "node" + ] +} \ No newline at end of file From 737a2b454001aa8aa8c3a2262be75074aa518870 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Mon, 24 Jul 2023 14:59:29 +0200 Subject: [PATCH 2/4] chore: add comments on types --- packages/testing/src/types/TestStack.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/testing/src/types/TestStack.ts b/packages/testing/src/types/TestStack.ts index a93ff27dc3..197cbb4f6c 100644 --- a/packages/testing/src/types/TestStack.ts +++ b/packages/testing/src/types/TestStack.ts @@ -5,23 +5,38 @@ import type { SsmStringOptions, } from './factories'; +/** + * Options to add a NodejsFunction to the test stack. + */ type AddFunctionOptions = Omit< NodejsFunctionOptions, 'stack' | 'resourceId' | 'runtime' >; +/** + * Options to add a DynamoDB table to the test stack. + */ type AddDynamoDBTableOptions = Omit< DynamoDBTableOptions, 'stack' | 'resourceId' >; +/** + * Options to add a SSM Secure String to the test stack. + */ type AddSsmSecureStringOptions = Omit< SsmSecureStringOptions, 'stack' | 'resourceId' >; +/** + * Options to add a SSM String to the test stack. + */ type AddSsmStringOptions = Omit; +/** + * Options to add a test case to the test stack. + */ type AddTestCaseOptions = { testCaseName: string; function: AddFunctionOptions; From 80fbe65fa7badcaed384a9b20944aecdd44ab7cd Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Mon, 24 Jul 2023 18:50:39 +0200 Subject: [PATCH 3/4] chore: improve code smells --- packages/testing/src/TestCase.ts | 36 +++++++++++++++++-------------- packages/testing/src/TestStack.ts | 6 +++--- packages/testing/src/factories.ts | 34 +++++++++++++++-------------- 3 files changed, 41 insertions(+), 35 deletions(-) diff --git a/packages/testing/src/TestCase.ts b/packages/testing/src/TestCase.ts index d1692d790c..51de2992b4 100644 --- a/packages/testing/src/TestCase.ts +++ b/packages/testing/src/TestCase.ts @@ -25,47 +25,51 @@ class DynamoDBTable { } public get envVariableName(): string { - return this.#envVariableName || 'TABLE_NAME'; + return this.#envVariableName ?? 'TABLE_NAME'; } } -class SsmSecureString { +abstract class SSMResource { public parameterName: string; - public ref: IStringParameter; - readonly #envVariableName?: string; + public ref: StringParameter | IStringParameter; + protected readonly envVarName?: string; public constructor( name: string, - ref: IStringParameter, + ref: StringParameter | IStringParameter, envVariableName?: string ) { this.parameterName = name; this.ref = ref; - this.#envVariableName = envVariableName; + this.envVarName = envVariableName; + } +} + +class SsmSecureString extends SSMResource { + public constructor( + name: string, + ref: IStringParameter, + envVariableName?: string + ) { + super(name, ref, envVariableName); } public get envVariableName(): string { - return this.#envVariableName || 'SECURE_STRING_NAME'; + return this.envVarName ?? 'SECURE_STRING_NAME'; } } -class SsmString { - public parameterName: string; - public ref: StringParameter; - readonly #envVariableName?: string; - +class SsmString extends SSMResource { public constructor( name: string, ref: StringParameter, envVariableName?: string ) { - this.parameterName = name; - this.ref = ref; - this.#envVariableName = envVariableName; + super(name, ref, envVariableName); } public get envVariableName(): string { - return this.#envVariableName || 'SSM_STRING_NAME'; + return this.envVarName ?? 'SSM_STRING_NAME'; } } diff --git a/packages/testing/src/TestStack.ts b/packages/testing/src/TestStack.ts index 34d963f250..798df92404 100644 --- a/packages/testing/src/TestStack.ts +++ b/packages/testing/src/TestStack.ts @@ -59,7 +59,7 @@ class TestStack implements ICloudAssemblyDirectoryProducer { options: AddDynamoDBTableOptions ): DynamoDBTable => { const resourceId = this.#generateUniqueName(`${testCaseName}-table`); - const name = options.name || resourceId; + const name = options.name ?? resourceId; const table = dynamoDBTable({ ...options, stack: this.stack, @@ -80,7 +80,7 @@ class TestStack implements ICloudAssemblyDirectoryProducer { options: AddFunctionOptions ): NodeJsFunction => { const resourceId = this.#generateUniqueName(`${testCaseName}-fn`); - const name = options.functionConfigs?.name || resourceId; + const name = options.functionConfigs?.name ?? resourceId; const fn = nodejsFunction({ stack: this.stack, resourceId, @@ -103,7 +103,7 @@ class TestStack implements ICloudAssemblyDirectoryProducer { options: AddSsmSecureStringOptions ): SsmSecureString => { const resourceId = this.#generateUniqueName(`${testCaseName}-ssmSecure`); - const name = options.name || resourceId; + const name = options.name ?? resourceId; const ssmSecure = ssmSecureString({ stack: this.stack, resourceId, diff --git a/packages/testing/src/factories.ts b/packages/testing/src/factories.ts index 05c519fcc4..a4322d4f39 100644 --- a/packages/testing/src/factories.ts +++ b/packages/testing/src/factories.ts @@ -2,8 +2,8 @@ import { CfnOutput, Duration, RemovalPolicy } from 'aws-cdk-lib'; import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; import { Tracing, Architecture } from 'aws-cdk-lib/aws-lambda'; import { RetentionDays } from 'aws-cdk-lib/aws-logs'; -import { PhysicalResourceId } from 'aws-cdk-lib/custom-resources'; import { + PhysicalResourceId, AwsCustomResource, AwsCustomResourcePolicy, } from 'aws-cdk-lib/custom-resources'; @@ -29,15 +29,15 @@ const nodejsFunction = (options: NodejsFunctionOptions): NodejsFunction => { const nodeJsFunction = new NodejsFunction(options.stack, options.resourceId, { runtime: TEST_RUNTIMES[options.runtime], functionName: options.functionConfigs?.name, - timeout: Duration.seconds(options.functionConfigs?.timeout || 30), + timeout: Duration.seconds(options.functionConfigs?.timeout ?? 30), entry: options?.functionCode?.path, - handler: options?.functionCode?.handler || 'handler', + handler: options?.functionCode?.handler ?? 'handler', environment: { - ...(options.functionConfigs?.environment || {}), + ...(options.functionConfigs?.environment ?? {}), }, - tracing: options.functionConfigs?.tracing || Tracing.ACTIVE, - memorySize: options.functionConfigs?.memorySize || 256, - architecture: options.functionConfigs?.architecture || Architecture.X86_64, + tracing: options.functionConfigs?.tracing ?? Tracing.ACTIVE, + memorySize: options.functionConfigs?.memorySize ?? 256, + architecture: options.functionConfigs?.architecture ?? Architecture.X86_64, logRetention: RetentionDays.ONE_DAY, bundling: options?.functionCode?.bundling, }); @@ -60,11 +60,11 @@ const dynamoDBTable = (options: DynamoDBTableOptions): Table => { const table = new Table(options.stack, options.resourceId, { tableName: options.name, partitionKey: { - name: options?.partitionKey?.name || 'id', - type: options?.partitionKey?.type || AttributeType.STRING, + name: options?.partitionKey?.name ?? 'id', + type: options?.partitionKey?.type ?? AttributeType.STRING, }, - ...(options?.sortKey || {}), - billingMode: options.billingMode || BillingMode.PAY_PER_REQUEST, + ...(options?.sortKey ?? {}), + billingMode: options.billingMode ?? BillingMode.PAY_PER_REQUEST, removalPolicy: RemovalPolicy.DESTROY, }); @@ -76,8 +76,8 @@ const dynamoDBTable = (options: DynamoDBTableOptions): Table => { * * @param options - The options for creating a DynamoDB item. */ -const dynamoDBItem = (options: DynamoDBItemOptions): void => { - new AwsCustomResource(options.stack, options.resourceId, { +const dynamoDBItem = (options: DynamoDBItemOptions): AwsCustomResource => { + const item = new AwsCustomResource(options.stack, options.resourceId, { onCreate: { service: 'DynamoDB', action: 'putItem', @@ -91,6 +91,8 @@ const dynamoDBItem = (options: DynamoDBItemOptions): void => { resources: [options.tableArn], }), }); + + return item; }; /** @@ -121,7 +123,7 @@ const ssmSecureString = (options: SsmSecureStringOptions): IStringParameter => { service: 'SSM', action: 'putParameter', parameters: { - Name: options.name || options.resourceId, + Name: options.name ?? options.resourceId, Value: options.value, Type: 'SecureString', }, @@ -131,7 +133,7 @@ const ssmSecureString = (options: SsmSecureStringOptions): IStringParameter => { service: 'SSM', action: 'deleteParameter', parameters: { - Name: options.name || options.resourceId, + Name: options.name ?? options.resourceId, }, }, policy: AwsCustomResourcePolicy.fromSdkCalls({ @@ -144,7 +146,7 @@ const ssmSecureString = (options: SsmSecureStringOptions): IStringParameter => { options.stack, options.resourceId, { - parameterName: options.name || options.resourceId, + parameterName: options.name ?? options.resourceId, } ); secureString.node.addDependency(resourceCreator); From d6ff220edf6869fdc83bc760897a9cfe40c39a2e Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Mon, 24 Jul 2023 18:52:05 +0200 Subject: [PATCH 4/4] chore: improve code smells --- packages/testing/src/TestStack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/testing/src/TestStack.ts b/packages/testing/src/TestStack.ts index 798df92404..7bc91e00aa 100644 --- a/packages/testing/src/TestStack.ts +++ b/packages/testing/src/TestStack.ts @@ -122,7 +122,7 @@ class TestStack implements ICloudAssemblyDirectoryProducer { options: AddSsmStringOptions ): SsmString => { const resourceId = this.#generateUniqueName(`${testCaseName}-ssmString`); - const name = options.name || resourceId; + const name = options.name ?? resourceId; const ssm = ssmString({ stack: this.stack, resourceId,