From ba71813086fc835568ef21ad725b8239d9d7d53a Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 29 Aug 2023 14:59:40 +0200 Subject: [PATCH 01/18] chore: move e2e utils into testing --- layers/src/canary-stack.ts | 4 +- layers/tests/e2e/layerPublisher.test.ts | 108 +++--- package-lock.json | 12 +- package.json | 3 +- packages/commons/tests/utils/e2eUtils.ts | 134 -------- ...makeHandlerIdempotent.test.FunctionCode.ts | 8 +- .../tests/e2e/makeHandlerIdempotent.test.ts | 206 +++++------ .../tests/e2e/makeIdempotent.test.ts | 146 ++++---- .../tests/helpers/idempotencyUtils.ts | 21 +- .../basicFeatures.middy.test.FunctionCode.ts | 2 +- .../tests/e2e/basicFeatures.middy.test.ts | 201 +++++------ .../tests/e2e/childLogger.manual.test.ts | 137 ++++---- .../e2e/logEventEnvVarSetting.middy.test.ts | 118 ++++--- .../tests/e2e/sampleRate.decorator.test.ts | 118 +++---- .../e2e/basicFeatures.decorators.test.ts | 146 ++++---- .../tests/e2e/basicFeatures.manual.test.ts | 142 ++++---- packages/metrics/tests/e2e/constants.ts | 53 ++- .../tests/e2e/appConfigProvider.class.test.ts | 254 +++++++------- .../tests/e2e/dynamoDBProvider.class.test.ts | 190 +++++----- .../tests/e2e/secretsProvider.class.test.ts | 162 ++++----- .../tests/e2e/ssmProvider.class.test.ts | 191 +++++----- .../tests/helpers/parametersUtils.ts | 1 + .../src/TestInvocationLogs.ts} | 38 +- packages/testing/src/helpers.ts | 53 +++ packages/testing/src/index.ts | 4 + packages/testing/src/invokeTestFunction.ts | 94 +++++ .../src/resources/TestNodejsFunction.ts | 40 +++ packages/testing/src/resources/index.ts | 1 + .../tests/e2e/allFeatures.decorator.test.ts | 325 +++++++++--------- .../tests/e2e/allFeatures.manual.test.ts | 144 ++++---- .../tests/e2e/allFeatures.middy.test.ts | 309 ++++++++--------- .../tests/e2e/asyncHandler.decorator.test.ts | 214 ++++++------ packages/tracer/tests/e2e/constants.ts | 51 ++- .../helpers/populateEnvironmentVariables.ts | 1 + .../tracer/tests/helpers/traceUtils.types.ts | 20 +- packages/tracer/tests/helpers/tracesUtils.ts | 83 +---- 36 files changed, 1845 insertions(+), 1889 deletions(-) delete mode 100644 packages/commons/tests/utils/e2eUtils.ts rename packages/{commons/tests/utils/InvocationLogs.ts => testing/src/TestInvocationLogs.ts} (85%) create mode 100644 packages/testing/src/helpers.ts create mode 100644 packages/testing/src/invokeTestFunction.ts create mode 100644 packages/testing/src/resources/TestNodejsFunction.ts create mode 100644 packages/testing/src/resources/index.ts diff --git a/layers/src/canary-stack.ts b/layers/src/canary-stack.ts index 17f0af14f3..476f83246f 100644 --- a/layers/src/canary-stack.ts +++ b/layers/src/canary-stack.ts @@ -2,7 +2,7 @@ import { CustomResource, Duration, Stack, StackProps } from 'aws-cdk-lib'; import { Construct } from 'constructs'; import { LayerVersion, Runtime } from 'aws-cdk-lib/aws-lambda'; import { RetentionDays } from 'aws-cdk-lib/aws-logs'; -import { v4 } from 'uuid'; +import { randomUUID } from 'node:crypto'; import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam'; import { Provider } from 'aws-cdk-lib/custom-resources'; import { StringParameter } from 'aws-cdk-lib/aws-ssm'; @@ -20,7 +20,7 @@ export class CanaryStack extends Stack { super(scope, id, props); const { layerName, powertoolsPackageVersion } = props; - const suffix = v4().substring(0, 5); + const suffix = randomUUID().substring(0, 5); const layerArn = StringParameter.fromStringParameterAttributes( this, diff --git a/layers/tests/e2e/layerPublisher.test.ts b/layers/tests/e2e/layerPublisher.test.ts index 7b4bab6988..664c04afb9 100644 --- a/layers/tests/e2e/layerPublisher.test.ts +++ b/layers/tests/e2e/layerPublisher.test.ts @@ -4,30 +4,26 @@ * @group e2e/layers/all */ import { App } from 'aws-cdk-lib'; -import { LayerVersion, Tracing } from 'aws-cdk-lib/aws-lambda'; +import { LayerVersion } from 'aws-cdk-lib/aws-lambda'; import { LayerPublisherStack } from '../../src/layer-publisher-stack'; import { - TestStack, + concatenateResourceName, defaultRuntime, -} from '@aws-lambda-powertools/testing-utils'; -import { - generateUniqueName, - invokeFunction, + generateTestUniqueName, isValidRuntimeKey, - createStackWithLambdaFunction, -} from '../../../packages/commons/tests/utils/e2eUtils'; + TestNodejsFunction, + TestStack, + TEST_RUNTIMES, + TestInvocationLogs, + invokeFunctionOnce, +} from '@aws-lambda-powertools/testing-utils'; import { RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, TEARDOWN_TIMEOUT, TEST_CASE_TIMEOUT, } from './constants'; -import { - LEVEL, - InvocationLogs, -} from '../../../packages/commons/tests/utils/InvocationLogs'; -import { v4 } from 'uuid'; -import path from 'path'; +import { join } from 'node:path'; import packageJson from '../../package.json'; const runtime: string = process.env.RUNTIME || defaultRuntime; @@ -37,42 +33,41 @@ if (!isValidRuntimeKey(runtime)) { } describe(`layers E2E tests (LayerPublisherStack) for runtime: ${runtime}`, () => { - const uuid = v4(); - let invocationLogs: InvocationLogs[]; - const stackNameLayers = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'layerStack' - ); - const stackNameFunction = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'functionStack' - ); - const functionName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, + let invocationLogs: TestInvocationLogs; + + const stackNameLayers = generateTestUniqueName({ + testPrefix: RESOURCE_NAME_PREFIX, runtime, - 'function' - ); - const ssmParameterLayerName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, + testName: 'layerStack', + }); + + const stackNameFunction = generateTestUniqueName({ + testPrefix: RESOURCE_NAME_PREFIX, runtime, - 'parameter' + testName: 'functionStack', + }); + + const functionName = concatenateResourceName({ + testName: stackNameFunction, + resourceName: 'function', + }); + + const ssmParameterLayerName = concatenateResourceName({ + testName: stackNameFunction, + resourceName: 'parameter', + }); + + // Location of the lambda function code + const lambdaFunctionCodeFile = join( + __dirname, + 'layerPublisher.class.test.functionCode.ts' ); - const lambdaFunctionCodeFile = 'layerPublisher.class.test.functionCode.ts'; - const invocationCount = 1; const powerToolsPackageVersion = packageJson.version; - const layerName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'layer' - ); + const layerName = concatenateResourceName({ + testName: stackNameLayers, + resourceName: 'layer', + }); const testStack = new TestStack(stackNameFunction); const layerApp = new App(); @@ -91,17 +86,14 @@ describe(`layers E2E tests (LayerPublisherStack) for runtime: ${runtime}`, () => 'LayerVersionArnReference', outputs['LatestLayerArn'] ); - createStackWithLambdaFunction({ - stack: testStack.stack, + new TestNodejsFunction(testStack.stack, functionName, { functionName: functionName, - functionEntry: path.join(__dirname, lambdaFunctionCodeFile), - tracing: Tracing.ACTIVE, + entry: lambdaFunctionCodeFile, + runtime: TEST_RUNTIMES[runtime], environment: { - UUID: uuid, POWERTOOLS_PACKAGE_VERSION: powerToolsPackageVersion, POWERTOOLS_SERVICE_NAME: 'LayerPublisherStack', }, - runtime: runtime, bundling: { externalModules: [ '@aws-lambda-powertools/commons', @@ -115,18 +107,16 @@ describe(`layers E2E tests (LayerPublisherStack) for runtime: ${runtime}`, () => await testStack.deploy(); - invocationLogs = await invokeFunction( + invocationLogs = await invokeFunctionOnce({ functionName, - invocationCount, - 'SEQUENTIAL' - ); + }); }, SETUP_TIMEOUT); describe('LayerPublisherStack usage', () => { it( 'should have no errors in the logs, which indicates the pacakges version matches the expected one', () => { - const logs = invocationLogs[0].getFunctionLogs(LEVEL.ERROR); + const logs = invocationLogs.getFunctionLogs('ERROR'); expect(logs.length).toBe(0); }, @@ -136,7 +126,7 @@ describe(`layers E2E tests (LayerPublisherStack) for runtime: ${runtime}`, () => it( 'should have one warning related to missing Metrics namespace', () => { - const logs = invocationLogs[0].getFunctionLogs(LEVEL.WARN); + const logs = invocationLogs.getFunctionLogs('WARN'); expect(logs.length).toBe(1); expect(logs[0]).toContain('Namespace should be defined, default used'); @@ -147,7 +137,7 @@ describe(`layers E2E tests (LayerPublisherStack) for runtime: ${runtime}`, () => it( 'should have one info log related to coldstart metric', () => { - const logs = invocationLogs[0].getFunctionLogs(LEVEL.INFO); + const logs = invocationLogs.getFunctionLogs('INFO'); expect(logs.length).toBe(1); expect(logs[0]).toContain('ColdStart'); @@ -158,7 +148,7 @@ describe(`layers E2E tests (LayerPublisherStack) for runtime: ${runtime}`, () => it( 'should have one debug log that says Hello World!', () => { - const logs = invocationLogs[0].getFunctionLogs(LEVEL.DEBUG); + const logs = invocationLogs.getFunctionLogs('DEBUG'); expect(logs.length).toBe(1); expect(logs[0]).toContain('Hello World!'); diff --git a/package-lock.json b/package-lock.json index 9e61c13363..5db333ef67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,8 +47,7 @@ "ts-node": "^10.9.1", "typedoc": "^0.24.7", "typedoc-plugin-missing-exports": "^2.0.0", - "typescript": "^4.9.4", - "uuid": "^9.0.0" + "typescript": "^4.9.4" }, "engines": { "node": ">=14" @@ -17327,15 +17326,6 @@ "dev": true, "license": "MIT" }, - "node_modules/uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", - "dev": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/v8-compile-cache": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", diff --git a/package.json b/package.json index b9186a7128..c8867af64c 100644 --- a/package.json +++ b/package.json @@ -70,8 +70,7 @@ "ts-node": "^10.9.1", "typedoc": "^0.24.7", "typedoc-plugin-missing-exports": "^2.0.0", - "typescript": "^4.9.4", - "uuid": "^9.0.0" + "typescript": "^4.9.4" }, "engines": { "node": ">=14" diff --git a/packages/commons/tests/utils/e2eUtils.ts b/packages/commons/tests/utils/e2eUtils.ts deleted file mode 100644 index 2bf5fdbdc1..0000000000 --- a/packages/commons/tests/utils/e2eUtils.ts +++ /dev/null @@ -1,134 +0,0 @@ -/** - * E2E utils is used by e2e tests. They are helper function that calls either CDK or SDK - * to interact with services. - */ -import { CfnOutput, Duration, Stack } from 'aws-cdk-lib'; -import { - NodejsFunction, - NodejsFunctionProps, -} from 'aws-cdk-lib/aws-lambda-nodejs'; -import { Runtime, Tracing } from 'aws-cdk-lib/aws-lambda'; -import { RetentionDays } from 'aws-cdk-lib/aws-logs'; -import { InvokeCommand, LambdaClient } from '@aws-sdk/client-lambda'; -import { fromUtf8 } from '@aws-sdk/util-utf8-node'; - -import { InvocationLogs } from './InvocationLogs'; - -const lambdaClient = new LambdaClient({}); - -const testRuntimeKeys = ['nodejs14x', 'nodejs16x', 'nodejs18x']; -export type TestRuntimesKey = (typeof testRuntimeKeys)[number]; -export const TEST_RUNTIMES: Record = { - nodejs14x: Runtime.NODEJS_14_X, - nodejs16x: Runtime.NODEJS_16_X, - nodejs18x: Runtime.NODEJS_18_X, -}; - -export type StackWithLambdaFunctionOptions = { - stack: Stack; - functionName: string; - functionEntry: string; - tracing?: Tracing; - environment: { [key: string]: string }; - logGroupOutputKey?: string; - runtime: string; - bundling?: NodejsFunctionProps['bundling']; - layers?: NodejsFunctionProps['layers']; - timeout?: Duration; -}; - -type FunctionPayload = { - [key: string]: string | boolean | number | Array>; -}; - -export const isValidRuntimeKey = ( - runtime: string -): runtime is TestRuntimesKey => testRuntimeKeys.includes(runtime); - -export const createStackWithLambdaFunction = ( - params: StackWithLambdaFunctionOptions -): void => { - const testFunction = new NodejsFunction(params.stack, `testFunction`, { - functionName: params.functionName, - entry: params.functionEntry, - tracing: params.tracing, - environment: params.environment, - runtime: TEST_RUNTIMES[params.runtime as TestRuntimesKey], - bundling: params.bundling, - layers: params.layers, - logRetention: RetentionDays.ONE_DAY, - timeout: params.timeout, - }); - - if (params.logGroupOutputKey) { - new CfnOutput(params.stack, params.logGroupOutputKey, { - value: testFunction.logGroup.logGroupName, - }); - } -}; - -export const generateUniqueName = ( - name_prefix: string, - uuid: string, - runtime: string, - testName: string -): string => - `${name_prefix}-${runtime}-${uuid.substring(0, 5)}-${testName}`.substring( - 0, - 64 - ); - -export const invokeFunction = async ( - functionName: string, - times = 1, - invocationMode: 'PARALLEL' | 'SEQUENTIAL' = 'PARALLEL', - payload: FunctionPayload = {}, - includeIndex = true -): Promise => { - const invocationLogs: InvocationLogs[] = []; - - const promiseFactory = (index?: number): Promise => { - // in some cases we need to send a payload without the index, i.e. idempotency tests - const payloadToSend = includeIndex - ? { invocation: index, ...payload } - : { ...payload }; - const invokePromise = lambdaClient - .send( - new InvokeCommand({ - FunctionName: functionName, - InvocationType: 'RequestResponse', - LogType: 'Tail', // Wait until execution completes and return all logs - Payload: fromUtf8(JSON.stringify(payloadToSend)), - }) - ) - .then((response) => { - if (response?.LogResult) { - invocationLogs.push(new InvocationLogs(response?.LogResult)); - } else { - throw new Error( - 'No LogResult field returned in the response of Lambda invocation. This should not happen.' - ); - } - }); - - return invokePromise; - }; - - const promiseFactories = Array.from({ length: times }, () => promiseFactory); - - const invocation = - invocationMode == 'PARALLEL' - ? Promise.all(promiseFactories.map((factory, index) => factory(index))) - : chainPromises(promiseFactories); - await invocation; - - return invocationLogs; -}; - -const chainPromises = async ( - promiseFactories: ((index?: number) => Promise)[] -): Promise => { - for (let index = 0; index < promiseFactories.length; index++) { - await promiseFactories[index](index); - } -}; diff --git a/packages/idempotency/tests/e2e/makeHandlerIdempotent.test.FunctionCode.ts b/packages/idempotency/tests/e2e/makeHandlerIdempotent.test.FunctionCode.ts index 510dcd399e..499eda7cca 100644 --- a/packages/idempotency/tests/e2e/makeHandlerIdempotent.test.FunctionCode.ts +++ b/packages/idempotency/tests/e2e/makeHandlerIdempotent.test.FunctionCode.ts @@ -56,16 +56,16 @@ export const handlerParallel = middy( * Test handler with timeout and JMESPath expression to extract the * idempotency key. * - * We put a 0.5s delay in the handler to ensure that it will timeout - * (timeout is set to 1s). By the time the second call is made, the - * second call is made, the first idempotency record has expired. + * We put a 2s delay in the handler to ensure that it will timeout + * (timeout is set to 2s). By the time the second call is made, the + * first idempotency record has expired. */ export const handlerTimeout = middy( async (event: { foo: string; invocation: number }, context: Context) => { logger.addContext(context); if (event.invocation === 0) { - await new Promise((resolve) => setTimeout(resolve, 2000)); + await new Promise((resolve) => setTimeout(resolve, 4000)); } logger.info('Processed event', { diff --git a/packages/idempotency/tests/e2e/makeHandlerIdempotent.test.ts b/packages/idempotency/tests/e2e/makeHandlerIdempotent.test.ts index 5d4ccd82c7..bf433cc6b1 100644 --- a/packages/idempotency/tests/e2e/makeHandlerIdempotent.test.ts +++ b/packages/idempotency/tests/e2e/makeHandlerIdempotent.test.ts @@ -4,26 +4,25 @@ * @group e2e/idempotency/makeHandlerIdempotent */ import { - generateUniqueName, + concatenateResourceName, + defaultRuntime, + generateTestUniqueName, invokeFunction, isValidRuntimeKey, -} from '../../../commons/tests/utils/e2eUtils'; -import { InvocationLogs } from '../../../commons/tests/utils/InvocationLogs'; + TestInvocationLogs, + TestStack, +} from '@aws-lambda-powertools/testing-utils'; +import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; +import { ScanCommand } from '@aws-sdk/lib-dynamodb'; +import { createHash } from 'node:crypto'; +import { join } from 'node:path'; +import { createIdempotencyResources } from '../helpers/idempotencyUtils'; import { RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, TEARDOWN_TIMEOUT, TEST_CASE_TIMEOUT, } from './constants'; -import { - TestStack, - defaultRuntime, -} from '@aws-lambda-powertools/testing-utils'; -import { v4 } from 'uuid'; -import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; -import { createHash } from 'node:crypto'; -import { ScanCommand } from '@aws-sdk/lib-dynamodb'; -import { createIdempotencyResources } from '../helpers/idempotencyUtils'; const runtime: string = process.env.RUNTIME || defaultRuntime; @@ -31,80 +30,69 @@ if (!isValidRuntimeKey(runtime)) { throw new Error(`Invalid runtime key value: ${runtime}`); } -const uuid = v4(); -const stackName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, +const testName = generateTestUniqueName({ + testPrefix: RESOURCE_NAME_PREFIX, runtime, - 'makeFnIdempotent' -); -const makeHandlerIdempotentFile = 'makeHandlerIdempotent.test.FunctionCode.ts'; + testName: 'makeHandlerIdempotent', +}); +const testStack = new TestStack(testName); -const ddb = new DynamoDBClient({}); -const testStack = new TestStack(stackName); +// Location of the lambda function code +const lambdaFunctionCodeFile = join( + __dirname, + 'makeHandlerIdempotent.test.FunctionCode.ts' +); const testDefault = 'default-sequential'; -const functionNameDefault = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - `${testDefault}-fn` -); -const ddbTableNameDefault = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - `${testDefault}-table` -); +const functionNameDefault = concatenateResourceName({ + testName, + resourceName: `${testDefault}-fn`, +}); +const ddbTableNameDefault = concatenateResourceName({ + testName, + resourceName: `${testDefault}-table`, +}); createIdempotencyResources( testStack.stack, runtime, ddbTableNameDefault, - makeHandlerIdempotentFile, + lambdaFunctionCodeFile, functionNameDefault, 'handler' ); const testDefaultParallel = 'default-parallel'; -const functionNameDefaultParallel = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - `${testDefaultParallel}-fn` -); -const ddbTableNameDefaultParallel = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - `${testDefaultParallel}-table` -); +const functionNameDefaultParallel = concatenateResourceName({ + testName, + resourceName: `${testDefaultParallel}-fn`, +}); +const ddbTableNameDefaultParallel = concatenateResourceName({ + testName, + resourceName: `${testDefaultParallel}-table`, +}); createIdempotencyResources( testStack.stack, runtime, ddbTableNameDefaultParallel, - makeHandlerIdempotentFile, + lambdaFunctionCodeFile, functionNameDefaultParallel, 'handlerParallel' ); const testTimeout = 'timeout'; -const functionNameTimeout = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - `${testTimeout}-fn` -); -const ddbTableNameTimeout = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - `${testTimeout}-table` -); +const functionNameTimeout = concatenateResourceName({ + testName, + resourceName: `${testTimeout}-fn`, +}); +const ddbTableNameTimeout = concatenateResourceName({ + testName, + resourceName: `${testTimeout}-table`, +}); createIdempotencyResources( testStack.stack, runtime, ddbTableNameTimeout, - makeHandlerIdempotentFile, + lambdaFunctionCodeFile, functionNameTimeout, 'handlerTimeout', undefined, @@ -112,30 +100,28 @@ createIdempotencyResources( ); const testExpired = 'expired'; -const functionNameExpired = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - `${testExpired}-fn` -); -const ddbTableNameExpired = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - `${testExpired}-table` -); +const functionNameExpired = concatenateResourceName({ + testName, + resourceName: `${testExpired}-fn`, +}); +const ddbTableNameExpired = concatenateResourceName({ + testName, + resourceName: `${testExpired}-table`, +}); createIdempotencyResources( testStack.stack, runtime, ddbTableNameExpired, - makeHandlerIdempotentFile, + lambdaFunctionCodeFile, functionNameExpired, 'handlerExpired', undefined, 2 ); -describe(`Idempotency E2E tests, middy middleware usage for runtime ${runtime}`, () => { +const ddb = new DynamoDBClient({}); + +describe(`Idempotency E2E tests, middy middleware usage`, () => { beforeAll(async () => { await testStack.deploy(); }, SETUP_TIMEOUT); @@ -152,13 +138,12 @@ describe(`Idempotency E2E tests, middy middleware usage for runtime ${runtime}`, .digest('base64'); // Act - const logs = await invokeFunction( - functionNameDefault, - 2, - 'SEQUENTIAL', + const logs = await invokeFunction({ + functionName: functionNameDefault, + times: 2, + invocationMode: 'SEQUENTIAL', payload, - false - ); + }); const functionLogs = logs.map((log) => log.getFunctionLogs()); // Assess @@ -178,7 +163,7 @@ describe(`Idempotency E2E tests, middy middleware usage for runtime ${runtime}`, expect(functionLogs[0]).toHaveLength(1); // We test the content of the log as well as the presence of fields from the context, this // ensures that the all the arguments are passed to the handler when made idempotent - expect(InvocationLogs.parseFunctionLog(functionLogs[0][0])).toEqual( + expect(TestInvocationLogs.parseFunctionLog(functionLogs[0][0])).toEqual( expect.objectContaining({ message: 'foo', details: 'bar', @@ -203,13 +188,12 @@ describe(`Idempotency E2E tests, middy middleware usage for runtime ${runtime}`, .digest('base64'); // Act - const logs = await invokeFunction( - functionNameDefaultParallel, - 2, - 'PARALLEL', + const logs = await invokeFunction({ + functionName: functionNameDefaultParallel, + times: 2, + invocationMode: 'PARALLEL', payload, - false - ); + }); const functionLogs = logs.map((log) => log.getFunctionLogs()); // Assess @@ -262,13 +246,15 @@ describe(`Idempotency E2E tests, middy middleware usage for runtime ${runtime}`, .digest('base64'); // Act - const logs = await invokeFunction( - functionNameTimeout, - 2, - 'SEQUENTIAL', - payload, - true - ); + const logs = await invokeFunction({ + functionName: functionNameTimeout, + times: 2, + invocationMode: 'SEQUENTIAL', + payload: Array.from({ length: 2 }, (_, index) => ({ + ...payload, + invocation: index, + })), + }); const functionLogs = logs.map((log) => log.getFunctionLogs()); // Assess @@ -293,7 +279,7 @@ describe(`Idempotency E2E tests, middy middleware usage for runtime ${runtime}`, // During the second invocation the handler should be called and complete, so the logs should // contain 1 log expect(functionLogs[1]).toHaveLength(1); - expect(InvocationLogs.parseFunctionLog(functionLogs[1][0])).toEqual( + expect(TestInvocationLogs.parseFunctionLog(functionLogs[1][0])).toEqual( expect.objectContaining({ message: 'Processed event', details: 'bar', @@ -318,26 +304,24 @@ describe(`Idempotency E2E tests, middy middleware usage for runtime ${runtime}`, // Act const logs = [ ( - await invokeFunction( - functionNameExpired, - 1, - 'SEQUENTIAL', - { ...payload, invocation: 0 }, - false - ) + await invokeFunction({ + functionName: functionNameExpired, + times: 1, + invocationMode: 'SEQUENTIAL', + payload: { ...payload, invocation: 0 }, + }) )[0], ]; // Wait for the idempotency record to expire await new Promise((resolve) => setTimeout(resolve, 2000)); logs.push( ( - await invokeFunction( - functionNameExpired, - 1, - 'SEQUENTIAL', - { ...payload, invocation: 1 }, - false - ) + await invokeFunction({ + functionName: functionNameExpired, + times: 1, + invocationMode: 'SEQUENTIAL', + payload: { ...payload, invocation: 1 }, + }) )[0] ); const functionLogs = logs.map((log) => log.getFunctionLogs()); @@ -360,7 +344,7 @@ describe(`Idempotency E2E tests, middy middleware usage for runtime ${runtime}`, // Both invocations should be successful and the logs should contain 1 log each expect(functionLogs[0]).toHaveLength(1); - expect(InvocationLogs.parseFunctionLog(functionLogs[1][0])).toEqual( + expect(TestInvocationLogs.parseFunctionLog(functionLogs[1][0])).toEqual( expect.objectContaining({ message: 'Processed event', details: 'bar', @@ -370,7 +354,7 @@ describe(`Idempotency E2E tests, middy middleware usage for runtime ${runtime}`, // During the second invocation the handler should be called and complete, so the logs should // contain 1 log expect(functionLogs[1]).toHaveLength(1); - expect(InvocationLogs.parseFunctionLog(functionLogs[1][0])).toEqual( + expect(TestInvocationLogs.parseFunctionLog(functionLogs[1][0])).toEqual( expect.objectContaining({ message: 'Processed event', details: 'bar', diff --git a/packages/idempotency/tests/e2e/makeIdempotent.test.ts b/packages/idempotency/tests/e2e/makeIdempotent.test.ts index 3feaefb3f1..3dd801ec78 100644 --- a/packages/idempotency/tests/e2e/makeIdempotent.test.ts +++ b/packages/idempotency/tests/e2e/makeIdempotent.test.ts @@ -3,113 +3,104 @@ * * @group e2e/idempotency/makeIdempotent */ -import { - generateUniqueName, - invokeFunction, - isValidRuntimeKey, -} from '../../../commons/tests/utils/e2eUtils'; import { RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, TEARDOWN_TIMEOUT, TEST_CASE_TIMEOUT, } from './constants'; -import { v4 } from 'uuid'; +import { join } from 'node:path'; import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; import { createHash } from 'node:crypto'; import { - TestStack, + concatenateResourceName, defaultRuntime, + generateTestUniqueName, + invokeFunction, + isValidRuntimeKey, + TestInvocationLogs, + TestStack, } from '@aws-lambda-powertools/testing-utils'; import { ScanCommand } from '@aws-sdk/lib-dynamodb'; import { createIdempotencyResources } from '../helpers/idempotencyUtils'; -import { InvocationLogs } from '@aws-lambda-powertools/commons/tests/utils/InvocationLogs'; const runtime: string = process.env.RUNTIME || defaultRuntime; if (!isValidRuntimeKey(runtime)) { throw new Error(`Invalid runtime key value: ${runtime}`); } -const uuid = v4(); -const stackName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, + +const testName = generateTestUniqueName({ + testPrefix: RESOURCE_NAME_PREFIX, runtime, - 'makeFnIdempotent' -); -const makeFunctionIdempotentFile = 'makeIdempotent.test.FunctionCode.ts'; + testName: 'makeFnIdempotent', +}); +const testStack = new TestStack(testName); -const ddb = new DynamoDBClient({ region: 'eu-west-1' }); -const testStack = new TestStack(stackName); +// Location of the lambda function code +const lambdaFunctionCodeFile = join( + __dirname, + 'makeIdempotent.test.FunctionCode.ts' +); const testDefault = 'default'; -const functionNameDefault = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - `${testDefault}-fn` -); -const ddbTableNameDefault = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - `${testDefault}-table` -); +const functionNameDefault = concatenateResourceName({ + testName, + resourceName: `${testDefault}-fn`, +}); +const ddbTableNameDefault = concatenateResourceName({ + testName, + resourceName: `${testDefault}-table`, +}); createIdempotencyResources( testStack.stack, runtime, ddbTableNameDefault, - makeFunctionIdempotentFile, + lambdaFunctionCodeFile, functionNameDefault, 'handlerDefault' ); const testCustomConfig = 'customConfig'; -const functionNameCustomConfig = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - `${testCustomConfig}-fn` -); -const ddbTableNameCustomConfig = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - `${testCustomConfig}-fn` -); +const functionNameCustomConfig = concatenateResourceName({ + testName, + resourceName: `${testCustomConfig}-fn`, +}); +const ddbTableNameCustomConfig = concatenateResourceName({ + testName, + resourceName: `${testCustomConfig}-table`, +}); createIdempotencyResources( testStack.stack, runtime, ddbTableNameCustomConfig, - makeFunctionIdempotentFile, + lambdaFunctionCodeFile, functionNameCustomConfig, 'handlerCustomized', 'customId' ); const testLambdaHandler = 'handler'; -const functionNameLambdaHandler = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - `${testLambdaHandler}-fn` -); -const ddbTableNameLambdaHandler = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - `${testLambdaHandler}-table` -); +const functionNameLambdaHandler = concatenateResourceName({ + testName, + resourceName: `${testLambdaHandler}-fn`, +}); +const ddbTableNameLambdaHandler = concatenateResourceName({ + testName, + resourceName: `${testLambdaHandler}-table`, +}); createIdempotencyResources( testStack.stack, runtime, ddbTableNameLambdaHandler, - makeFunctionIdempotentFile, + lambdaFunctionCodeFile, functionNameLambdaHandler, 'handlerLambda' ); -describe(`Idempotency E2E tests, wrapper function usage for runtime`, () => { +const ddb = new DynamoDBClient({}); + +describe(`Idempotency E2E tests, wrapper function usage`, () => { beforeAll(async () => { await testStack.deploy(); }, SETUP_TIMEOUT); @@ -130,13 +121,12 @@ describe(`Idempotency E2E tests, wrapper function usage for runtime`, () => { ); // Act - const logs = await invokeFunction( - functionNameDefault, - 2, - 'SEQUENTIAL', + const logs = await invokeFunction({ + functionName: functionNameDefault, + times: 2, + invocationMode: 'SEQUENTIAL', payload, - false - ); + }); const functionLogs = logs.map((log) => log.getFunctionLogs()); // Assess @@ -191,13 +181,12 @@ describe(`Idempotency E2E tests, wrapper function usage for runtime`, () => { ); // Act - const logs = await invokeFunction( - functionNameCustomConfig, - 2, - 'SEQUENTIAL', + const logs = await invokeFunction({ + functionName: functionNameCustomConfig, + times: 2, + invocationMode: 'SEQUENTIAL', payload, - false - ); + }); const functionLogs = logs.map((log) => log.getFunctionLogs()); // Assess @@ -246,21 +235,21 @@ describe(`Idempotency E2E tests, wrapper function usage for runtime`, () => { // During the first invocation, the processing function should have been called 3 times (once for each record) expect(functionLogs[0]).toHaveLength(3); - expect(InvocationLogs.parseFunctionLog(functionLogs[0][0])).toEqual( + expect(TestInvocationLogs.parseFunctionLog(functionLogs[0][0])).toEqual( expect.objectContaining({ baz: 0, // index of recursion in handler, assess that all function arguments are preserved record: payload.records[0], message: 'Got test event', }) ); - expect(InvocationLogs.parseFunctionLog(functionLogs[0][1])).toEqual( + expect(TestInvocationLogs.parseFunctionLog(functionLogs[0][1])).toEqual( expect.objectContaining({ baz: 1, record: payload.records[1], message: 'Got test event', }) ); - expect(InvocationLogs.parseFunctionLog(functionLogs[0][2])).toEqual( + expect(TestInvocationLogs.parseFunctionLog(functionLogs[0][2])).toEqual( expect.objectContaining({ baz: 2, record: payload.records[2], @@ -286,13 +275,12 @@ describe(`Idempotency E2E tests, wrapper function usage for runtime`, () => { .digest('base64'); // Act - const logs = await invokeFunction( - functionNameLambdaHandler, - 2, - 'SEQUENTIAL', + const logs = await invokeFunction({ + functionName: functionNameLambdaHandler, + times: 2, + invocationMode: 'SEQUENTIAL', payload, - true - ); + }); const functionLogs = logs.map((log) => log.getFunctionLogs()); // Assess @@ -312,7 +300,7 @@ describe(`Idempotency E2E tests, wrapper function usage for runtime`, () => { expect(functionLogs[0]).toHaveLength(1); // We test the content of the log as well as the presence of fields from the context, this // ensures that the all the arguments are passed to the handler when made idempotent - expect(InvocationLogs.parseFunctionLog(functionLogs[0][0])).toEqual( + expect(TestInvocationLogs.parseFunctionLog(functionLogs[0][0])).toEqual( expect.objectContaining({ message: 'foo', details: 'bar', diff --git a/packages/idempotency/tests/helpers/idempotencyUtils.ts b/packages/idempotency/tests/helpers/idempotencyUtils.ts index aa3612d853..cef4073572 100644 --- a/packages/idempotency/tests/helpers/idempotencyUtils.ts +++ b/packages/idempotency/tests/helpers/idempotencyUtils.ts @@ -1,11 +1,11 @@ +import { + TestNodejsFunction, + TEST_RUNTIMES, +} from '@aws-lambda-powertools/testing-utils'; import { Duration, RemovalPolicy, Stack } from 'aws-cdk-lib'; -import { v4 } from 'uuid'; import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb'; -import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; -import { TEST_RUNTIMES } from '../../../commons/tests/utils/e2eUtils'; +import { randomUUID } from 'node:crypto'; import { BasePersistenceLayer } from '../../src/persistence'; -import path from 'path'; -import { RetentionDays } from 'aws-cdk-lib/aws-logs'; export const createIdempotencyResources = ( stack: Stack, @@ -17,7 +17,7 @@ export const createIdempotencyResources = ( ddbPkId?: string, timeout?: number ): void => { - const uniqueTableId = ddbTableName + v4().substring(0, 5); + const uniqueTableId = ddbTableName + randomUUID().substring(0, 5); const ddbTable = new Table(stack, uniqueTableId, { tableName: ddbTableName, partitionKey: { @@ -28,18 +28,17 @@ export const createIdempotencyResources = ( removalPolicy: RemovalPolicy.DESTROY, }); - const uniqueFunctionId = functionName + v4().substring(0, 5); - const nodeJsFunction = new NodejsFunction(stack, uniqueFunctionId, { - runtime: TEST_RUNTIMES[runtime], + const uniqueFunctionId = functionName + randomUUID().substring(0, 5); + const nodeJsFunction = new TestNodejsFunction(stack, uniqueFunctionId, { functionName: functionName, - entry: path.join(__dirname, `../e2e/${pathToFunction}`), + entry: pathToFunction, + runtime: TEST_RUNTIMES[runtime as keyof typeof TEST_RUNTIMES], timeout: Duration.seconds(timeout || 30), handler: handler, environment: { IDEMPOTENCY_TABLE_NAME: ddbTableName, POWERTOOLS_LOGGER_LOG_EVENT: 'true', }, - logRetention: RetentionDays.ONE_DAY, }); ddbTable.grantReadWriteData(nodeJsFunction); diff --git a/packages/logger/tests/e2e/basicFeatures.middy.test.FunctionCode.ts b/packages/logger/tests/e2e/basicFeatures.middy.test.FunctionCode.ts index 6be9c265cb..9107bf5453 100644 --- a/packages/logger/tests/e2e/basicFeatures.middy.test.FunctionCode.ts +++ b/packages/logger/tests/e2e/basicFeatures.middy.test.FunctionCode.ts @@ -35,7 +35,7 @@ const testFunction = async (event: TestEvent, context: Context): TestOutput => { logger.removeKeys([REMOVABLE_KEY]); // This key should not appear in any log (except the event log) logger.appendKeys({ // This key-value pair should appear in every log (except the event log) - [RUNTIME_ADDED_KEY]: event.invocation, + [RUNTIME_ADDED_KEY]: 'bar', }); // Test feature 5: One-time additional log keys and values diff --git a/packages/logger/tests/e2e/basicFeatures.middy.test.ts b/packages/logger/tests/e2e/basicFeatures.middy.test.ts index 971050b5db..422ed6c186 100644 --- a/packages/logger/tests/e2e/basicFeatures.middy.test.ts +++ b/packages/logger/tests/e2e/basicFeatures.middy.test.ts @@ -3,20 +3,20 @@ * * @group e2e/logger/basicFeatures */ -import path from 'path'; -import { APIGatewayAuthorizerResult } from 'aws-lambda'; -import { v4 } from 'uuid'; import { - createStackWithLambdaFunction, - generateUniqueName, + concatenateResourceName, + defaultRuntime, + generateTestUniqueName, invokeFunction, isValidRuntimeKey, -} from '../../../commons/tests/utils/e2eUtils'; -import { InvocationLogs } from '../../../commons/tests/utils/InvocationLogs'; -import { + TestInvocationLogs, + TestNodejsFunction, TestStack, - defaultRuntime, + TEST_RUNTIMES, } from '@aws-lambda-powertools/testing-utils'; +import type { APIGatewayAuthorizerResult } from 'aws-lambda'; +import { randomUUID } from 'node:crypto'; +import { join } from 'node:path'; import { RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, @@ -26,85 +26,90 @@ import { XRAY_TRACE_ID_REGEX, } from './constants'; -const runtime: string = process.env.RUNTIME || defaultRuntime; +describe(`Logger E2E tests, basic functionalities middy usage`, () => { + const runtime: string = process.env.RUNTIME || defaultRuntime; -if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key value: ${runtime}`); -} + if (!isValidRuntimeKey(runtime)) { + throw new Error(`Invalid runtime key value: ${runtime}`); + } -const LEVEL = InvocationLogs.LEVEL; + const testName = generateTestUniqueName({ + testPrefix: RESOURCE_NAME_PREFIX, + runtime, + testName: 'AllFeatures-Decorator', + }); + const testStack = new TestStack(testName); -const uuid = v4(); -const stackName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'BasicFeatures-Middy' -); -const functionName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'BasicFeatures-Middy' -); -const lambdaFunctionCodeFile = 'basicFeatures.middy.test.FunctionCode.ts'; + // Location of the lambda function code + const lambdaFunctionCodeFile = join( + __dirname, + 'basicFeatures.middy.test.FunctionCode.ts' + ); -const invocationCount = 3; + const fnNameBasicFeatures = concatenateResourceName({ + testName, + resourceName: 'BasicFeatures', + }); -// Text to be used by Logger in the Lambda function -const PERSISTENT_KEY = 'persistentKey'; -const RUNTIME_ADDED_KEY = 'invocation'; -const PERSISTENT_VALUE = uuid; -const REMOVABLE_KEY = 'removableKey'; -const REMOVABLE_VALUE = 'removedValue'; -const SINGLE_LOG_ITEM_KEY = 'singleKey'; -const SINGLE_LOG_ITEM_VALUE = 'singleValue'; -const ERROR_MSG = 'error'; -const ARBITRARY_OBJECT_KEY = 'arbitraryObjectKey'; -const ARBITRARY_OBJECT_DATA = 'arbitraryObjectData'; + // Text to be used by Logger in the Lambda function + const PERSISTENT_KEY = 'persistentKey'; + const RUNTIME_ADDED_KEY = 'foo'; + const PERSISTENT_VALUE = randomUUID(); + const REMOVABLE_KEY = 'removableKey'; + const REMOVABLE_VALUE = 'removedValue'; + const SINGLE_LOG_ITEM_KEY = 'singleKey'; + const SINGLE_LOG_ITEM_VALUE = 'singleValue'; + const ERROR_MSG = 'error'; + const ARBITRARY_OBJECT_KEY = 'arbitraryObjectKey'; + const ARBITRARY_OBJECT_DATA = 'arbitraryObjectData'; + const LEVEL = TestInvocationLogs.LEVEL; + const invocations = 3; -const testStack = new TestStack(stackName); -let logGroupName: string; // We do not know it until deployment + let logGroupName: string; // We do not know it until deployment -describe(`logger E2E tests basic functionalities (middy) for runtime: ${runtime}`, () => { - let invocationLogs: InvocationLogs[]; + let invocationLogs: TestInvocationLogs[]; beforeAll(async () => { - // Create and deploy a stack with AWS CDK - createStackWithLambdaFunction({ - stack: testStack.stack, - functionName: functionName, - functionEntry: path.join(__dirname, lambdaFunctionCodeFile), - environment: { - LOG_LEVEL: 'INFO', - POWERTOOLS_SERVICE_NAME: 'logger-e2e-testing', - UUID: uuid, - - // Text to be used by Logger in the Lambda function - PERSISTENT_KEY, - PERSISTENT_VALUE, - RUNTIME_ADDED_KEY, - REMOVABLE_KEY, - REMOVABLE_VALUE, - SINGLE_LOG_ITEM_KEY, - SINGLE_LOG_ITEM_VALUE, - ERROR_MSG, - ARBITRARY_OBJECT_KEY, - ARBITRARY_OBJECT_DATA, + // Prepare + new TestNodejsFunction( + testStack.stack, + fnNameBasicFeatures, + { + functionName: fnNameBasicFeatures, + entry: lambdaFunctionCodeFile, + runtime: TEST_RUNTIMES[runtime], + environment: { + LOG_LEVEL: 'INFO', + POWERTOOLS_SERVICE_NAME: 'logger-e2e-testing', + PERSISTENT_KEY, + PERSISTENT_VALUE, + RUNTIME_ADDED_KEY, + REMOVABLE_KEY, + REMOVABLE_VALUE, + SINGLE_LOG_ITEM_KEY, + SINGLE_LOG_ITEM_VALUE, + ERROR_MSG, + ARBITRARY_OBJECT_KEY, + ARBITRARY_OBJECT_DATA, + }, }, - logGroupOutputKey: STACK_OUTPUT_LOG_GROUP, - runtime: runtime, - }); + { + logGroupOutputKey: STACK_OUTPUT_LOG_GROUP, + } + ); const result = await testStack.deploy(); logGroupName = result[STACK_OUTPUT_LOG_GROUP]; // Invoke the function three time (one for cold start, then two for warm start) - invocationLogs = await invokeFunction( - functionName, - invocationCount, - 'SEQUENTIAL' - ); + invocationLogs = await invokeFunction({ + functionName: fnNameBasicFeatures, + times: invocations, + invocationMode: 'SEQUENTIAL', + payload: { + foo: 'bar', + }, + }); console.log('logGroupName', logGroupName); }, SETUP_TIMEOUT); @@ -113,7 +118,7 @@ describe(`logger E2E tests basic functionalities (middy) for runtime: ${runtime} it( 'should filter log based on LOG_LEVEL (INFO) environment variable in Lambda', async () => { - for (let i = 0; i < invocationCount; i++) { + for (let i = 0; i < invocations; i++) { // Get log messages of the invocation and filter by level const debugLogs = invocationLogs[i].getFunctionLogs(LEVEL.DEBUG); // Check that no log message below INFO level is logged @@ -128,12 +133,12 @@ describe(`logger E2E tests basic functionalities (middy) for runtime: ${runtime} it( 'should inject context info in each log', async () => { - for (let i = 0; i < invocationCount; i++) { + for (let i = 0; i < invocations; i++) { // Get log messages of the invocation const logMessages = invocationLogs[i].getFunctionLogs(); // Check that the context is logged on every log for (const message of logMessages) { - const log = InvocationLogs.parseFunctionLog(message); + const log = TestInvocationLogs.parseFunctionLog(message); expect(log).toHaveProperty('function_arn'); expect(log).toHaveProperty('function_memory_size'); expect(log).toHaveProperty('function_name'); @@ -148,12 +153,12 @@ describe(`logger E2E tests basic functionalities (middy) for runtime: ${runtime} it( 'should include coldStart equal to TRUE only on the first invocation, FALSE otherwise', async () => { - for (let i = 0; i < invocationCount; i++) { + for (let i = 0; i < invocations; i++) { // Get log messages of the invocation const logMessages = invocationLogs[i].getFunctionLogs(); // Check that cold start is logged correctly on every log for (const message of logMessages) { - const log = InvocationLogs.parseFunctionLog(message); + const log = TestInvocationLogs.parseFunctionLog(message); if (i === 0) { expect(log.cold_start).toBe(true); } else { @@ -170,17 +175,17 @@ describe(`logger E2E tests basic functionalities (middy) for runtime: ${runtime} it( 'should log the event as the first log of each invocation only', async () => { - for (let i = 0; i < invocationCount; i++) { + for (let i = 0; i < invocations; i++) { // Get log messages of the invocation const logMessages = invocationLogs[i].getFunctionLogs(); for (const [index, message] of logMessages.entries()) { - const log = InvocationLogs.parseFunctionLog(message); + const log = TestInvocationLogs.parseFunctionLog(message); // Check that the event is logged on the first log if (index === 0) { expect(log).toHaveProperty('event'); expect(log.event).toStrictEqual( - expect.objectContaining({ invocation: i }) + expect.objectContaining({ foo: 'bar' }) ); // Check that the event is not logged again on the rest of the logs } else { @@ -197,12 +202,12 @@ describe(`logger E2E tests basic functionalities (middy) for runtime: ${runtime} it( 'should contain persistent value in every log', async () => { - for (let i = 0; i < invocationCount; i++) { + for (let i = 0; i < invocations; i++) { // Get log messages of the invocation const logMessages = invocationLogs[i].getFunctionLogs(); for (const message of logMessages) { - const log = InvocationLogs.parseFunctionLog(message); + const log = TestInvocationLogs.parseFunctionLog(message); // Check that the persistent key is present in every log expect(log).toHaveProperty(PERSISTENT_KEY); expect(log[PERSISTENT_KEY]).toBe(PERSISTENT_VALUE); @@ -215,12 +220,12 @@ describe(`logger E2E tests basic functionalities (middy) for runtime: ${runtime} it( 'should not contain persistent keys that were removed on runtime', async () => { - for (let i = 0; i < invocationCount; i++) { + for (let i = 0; i < invocations; i++) { // Get log messages of the invocation const logMessages = invocationLogs[i].getFunctionLogs(); for (const [index, message] of logMessages.entries()) { - const log = InvocationLogs.parseFunctionLog(message); + const log = TestInvocationLogs.parseFunctionLog(message); // Check that at the time of logging the event, which happens before the handler, // the key was still present if (index === 0) { @@ -239,12 +244,12 @@ describe(`logger E2E tests basic functionalities (middy) for runtime: ${runtime} it( 'should not leak any persistent keys added runtime since clearState is enabled', async () => { - for (let i = 0; i < invocationCount; i++) { + for (let i = 0; i < invocations; i++) { // Get log messages of the invocation const logMessages = invocationLogs[i].getFunctionLogs(); for (const [index, message] of logMessages.entries()) { - const log = InvocationLogs.parseFunctionLog(message); + const log = TestInvocationLogs.parseFunctionLog(message); // Check that at the time of logging the event, which happens before the handler, // the key is NOT present if (index === 0) { @@ -252,8 +257,8 @@ describe(`logger E2E tests basic functionalities (middy) for runtime: ${runtime} } else { // Check that all other logs that happen at runtime do contain the key expect(log).toHaveProperty(RUNTIME_ADDED_KEY); - // Check that the value is the same for all logs (it should be the index of the invocation) - expect(log[RUNTIME_ADDED_KEY]).toEqual(i); + // Check that the value is the same for all logs + expect(log[RUNTIME_ADDED_KEY]).toEqual('bar'); } } } @@ -266,7 +271,7 @@ describe(`logger E2E tests basic functionalities (middy) for runtime: ${runtime} it( 'should log additional keys and value only once', async () => { - for (let i = 0; i < invocationCount; i++) { + for (let i = 0; i < invocations; i++) { // Get log messages of the invocation const logMessages = invocationLogs[i].getFunctionLogs(); // Check that the additional log is logged only once @@ -275,7 +280,7 @@ describe(`logger E2E tests basic functionalities (middy) for runtime: ${runtime} ); expect(logMessagesWithAdditionalLog).toHaveLength(1); // Check that the additional log is logged correctly - const parsedLog = InvocationLogs.parseFunctionLog( + const parsedLog = TestInvocationLogs.parseFunctionLog( logMessagesWithAdditionalLog[0] ); expect(parsedLog[SINGLE_LOG_ITEM_KEY]).toBe(SINGLE_LOG_ITEM_VALUE); @@ -289,7 +294,7 @@ describe(`logger E2E tests basic functionalities (middy) for runtime: ${runtime} it( 'should log error only once', async () => { - for (let i = 0; i < invocationCount; i++) { + for (let i = 0; i < invocations; i++) { // Get log messages of the invocation filtered by error level const logMessages = invocationLogs[i].getFunctionLogs(LEVEL.ERROR); @@ -297,7 +302,7 @@ describe(`logger E2E tests basic functionalities (middy) for runtime: ${runtime} expect(logMessages).toHaveLength(1); // Check that the error is logged correctly - const errorLog = InvocationLogs.parseFunctionLog(logMessages[0]); + const errorLog = TestInvocationLogs.parseFunctionLog(logMessages[0]); expect(errorLog).toHaveProperty('error'); expect(errorLog.error).toStrictEqual( expect.objectContaining({ @@ -317,7 +322,7 @@ describe(`logger E2E tests basic functionalities (middy) for runtime: ${runtime} it( 'should log additional arbitrary object only once', async () => { - for (let i = 0; i < invocationCount; i++) { + for (let i = 0; i < invocations; i++) { // Get log messages of the invocation const logMessages = invocationLogs[i].getFunctionLogs(); // Get the log messages that contains the arbitrary object @@ -326,7 +331,9 @@ describe(`logger E2E tests basic functionalities (middy) for runtime: ${runtime} ); // Check that the arbitrary object is logged only once expect(filteredLogs).toHaveLength(1); - const logObject = InvocationLogs.parseFunctionLog(filteredLogs[0]); + const logObject = TestInvocationLogs.parseFunctionLog( + filteredLogs[0] + ); // Check that the arbitrary object is logged correctly expect(logObject).toHaveProperty(ARBITRARY_OBJECT_KEY); const arbitrary = logObject[ @@ -355,14 +362,14 @@ describe(`logger E2E tests basic functionalities (middy) for runtime: ${runtime} it( 'should inject & parse the X-Ray Trace ID of the current invocation into every log', async () => { - for (let i = 0; i < invocationCount; i++) { + for (let i = 0; i < invocations; i++) { // Get log messages of the invocation const logMessages = invocationLogs[i].getFunctionLogs(); // Check that the X-Ray Trace ID is logged on every log const traceIds: string[] = []; for (const message of logMessages) { - const log = InvocationLogs.parseFunctionLog(message); + const log = TestInvocationLogs.parseFunctionLog(message); expect(log).toHaveProperty('xray_trace_id'); expect(log.xray_trace_id).toMatch(XRAY_TRACE_ID_REGEX); traceIds.push(log.xray_trace_id as string); diff --git a/packages/logger/tests/e2e/childLogger.manual.test.ts b/packages/logger/tests/e2e/childLogger.manual.test.ts index b3ec427f77..d07757a786 100644 --- a/packages/logger/tests/e2e/childLogger.manual.test.ts +++ b/packages/logger/tests/e2e/childLogger.manual.test.ts @@ -3,18 +3,17 @@ * * @group e2e/logger/childLogger */ -import path from 'path'; -import { v4 } from 'uuid'; +import { join } from 'node:path'; import { - createStackWithLambdaFunction, - generateUniqueName, - invokeFunction, + concatenateResourceName, + defaultRuntime, + generateTestUniqueName, isValidRuntimeKey, -} from '../../../commons/tests/utils/e2eUtils'; -import { InvocationLogs } from '../../../commons/tests/utils/InvocationLogs'; -import { + TestNodejsFunction, TestStack, - defaultRuntime, + TEST_RUNTIMES, + TestInvocationLogs, + invokeFunction, } from '@aws-lambda-powertools/testing-utils'; import { RESOURCE_NAME_PREFIX, @@ -24,71 +23,75 @@ import { TEARDOWN_TIMEOUT, } from './constants'; -const runtime: string = process.env.RUNTIME || defaultRuntime; - -if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key value: ${runtime}`); -} - -const LEVEL = InvocationLogs.LEVEL; +describe(`Logger E2E tests, child logger`, () => { + const runtime: string = process.env.RUNTIME || defaultRuntime; -const uuid = v4(); -const stackName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'ChildLogger-Manual' -); -const functionName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'ChildLogger-Manual' -); -const lambdaFunctionCodeFile = 'childLogger.manual.test.FunctionCode.ts'; + if (!isValidRuntimeKey(runtime)) { + throw new Error(`Invalid runtime key value: ${runtime}`); + } -const invocationCount = 3; + const testName = generateTestUniqueName({ + testPrefix: RESOURCE_NAME_PREFIX, + runtime, + testName: 'ChildLogger-Manual', + }); + const testStack = new TestStack(testName); -// Parameters to be used by Logger in the Lambda function -const PERSISTENT_KEY = 'persistentKey'; -const PERSISTENT_VALUE = 'persistentValue'; -const PARENT_LOG_MSG = 'parent-only-log-msg'; -const CHILD_LOG_MSG = 'child-only-log-msg'; -const CHILD_LOG_LEVEL = LEVEL.ERROR; + // Location of the lambda function code + const lambdaFunctionCodeFile = join( + __dirname, + 'childLogger.manual.test.FunctionCode.ts' + ); -const testStack = new TestStack(stackName); -let logGroupName: string; // We do not know it until deployment + const fnNameChildLogger = concatenateResourceName({ + testName, + resourceName: 'ChildLogger', + }); -describe(`logger E2E tests child logger functionalities (manual) for runtime: ${runtime}`, () => { - let invocationLogs: InvocationLogs[]; + // Parameters to be used by Logger in the Lambda function + const PERSISTENT_KEY = 'persistentKey'; + const PERSISTENT_VALUE = 'persistentValue'; + const PARENT_LOG_MSG = 'parent-only-log-msg'; + const CHILD_LOG_MSG = 'child-only-log-msg'; + const LEVEL = TestInvocationLogs.LEVEL; + const CHILD_LOG_LEVEL = LEVEL.ERROR; + let logGroupName: string; // We do not know it until deployment + let invocationLogs: TestInvocationLogs[]; + const invocations = 3; beforeAll(async () => { - // Create and deploy a stack with AWS CDK - createStackWithLambdaFunction({ - stack: testStack.stack, - functionName: functionName, - functionEntry: path.join(__dirname, lambdaFunctionCodeFile), - environment: { - LOG_LEVEL: 'INFO', - POWERTOOLS_SERVICE_NAME: 'logger-e2e-testing', - UUID: uuid, - - // Text to be used by Logger in the Lambda function - PERSISTENT_KEY, - PERSISTENT_VALUE, - PARENT_LOG_MSG, - CHILD_LOG_MSG, - CHILD_LOG_LEVEL, + // Prepare + new TestNodejsFunction( + testStack.stack, + fnNameChildLogger, + { + functionName: fnNameChildLogger, + entry: lambdaFunctionCodeFile, + runtime: TEST_RUNTIMES[runtime], + environment: { + LOG_LEVEL: 'INFO', + POWERTOOLS_SERVICE_NAME: 'logger-e2e-testing', + PERSISTENT_KEY, + PERSISTENT_VALUE, + PARENT_LOG_MSG, + CHILD_LOG_MSG, + CHILD_LOG_LEVEL, + }, }, - logGroupOutputKey: STACK_OUTPUT_LOG_GROUP, - runtime: runtime, - }); + { + logGroupOutputKey: STACK_OUTPUT_LOG_GROUP, + } + ); const result = await testStack.deploy(); logGroupName = result[STACK_OUTPUT_LOG_GROUP]; // Invoke the function three time (one for cold start, then two for warm start) - invocationLogs = await invokeFunction(functionName, invocationCount); + invocationLogs = await invokeFunction({ + functionName: fnNameChildLogger, + invocationMode: 'SEQUENTIAL', + times: invocations, + }); console.log('logGroupName', logGroupName); }, SETUP_TIMEOUT); @@ -97,7 +100,7 @@ describe(`logger E2E tests child logger functionalities (manual) for runtime: ${ it( 'should not log at same level of parent because of its own logLevel', async () => { - for (let i = 0; i < invocationCount; i++) { + for (let i = 0; i < invocations; i++) { // Get log messages of the invocation and filter by level const infoLogs = invocationLogs[i].getFunctionLogs(LEVEL.INFO); @@ -118,7 +121,7 @@ describe(`logger E2E tests child logger functionalities (manual) for runtime: ${ it( 'should log only level passed to a child', async () => { - for (let i = 0; i < invocationCount; i++) { + for (let i = 0; i < invocations; i++) { // Get log messages of the invocation const logMessages = invocationLogs[i].getFunctionLogs(); @@ -140,7 +143,7 @@ describe(`logger E2E tests child logger functionalities (manual) for runtime: ${ it( 'should NOT inject context into the child logger', async () => { - for (let i = 0; i < invocationCount; i++) { + for (let i = 0; i < invocations; i++) { // Get log messages of the invocation const logMessages = invocationLogs[i].getFunctionLogs(); @@ -151,7 +154,7 @@ describe(`logger E2E tests child logger functionalities (manual) for runtime: ${ // Check that the context is not present in any of the child logs for (const message of childLogMessages) { - const log = InvocationLogs.parseFunctionLog(message); + const log = TestInvocationLogs.parseFunctionLog(message); expect(log).not.toHaveProperty('function_arn'); expect(log).not.toHaveProperty('function_memory_size'); expect(log).not.toHaveProperty('function_name'); @@ -165,13 +168,13 @@ describe(`logger E2E tests child logger functionalities (manual) for runtime: ${ it( 'both logger instances should have the same persistent key/value', async () => { - for (let i = 0; i < invocationCount; i++) { + for (let i = 0; i < invocations; i++) { // Get log messages of the invocation const logMessages = invocationLogs[i].getFunctionLogs(); // Check that all logs have the persistent key/value for (const message of logMessages) { - const log = InvocationLogs.parseFunctionLog(message); + const log = TestInvocationLogs.parseFunctionLog(message); expect(log).toHaveProperty(PERSISTENT_KEY); } } diff --git a/packages/logger/tests/e2e/logEventEnvVarSetting.middy.test.ts b/packages/logger/tests/e2e/logEventEnvVarSetting.middy.test.ts index 9b4e10a727..ed0f2a67e6 100644 --- a/packages/logger/tests/e2e/logEventEnvVarSetting.middy.test.ts +++ b/packages/logger/tests/e2e/logEventEnvVarSetting.middy.test.ts @@ -3,84 +3,88 @@ * * @group e2e/logger/logEventEnvVarSetting */ -import path from 'path'; -import { v4 } from 'uuid'; import { - createStackWithLambdaFunction, - generateUniqueName, + concatenateResourceName, + defaultRuntime, + generateTestUniqueName, invokeFunction, isValidRuntimeKey, -} from '../../../commons/tests/utils/e2eUtils'; -import { InvocationLogs } from '../../../commons/tests/utils/InvocationLogs'; -import { + TestInvocationLogs, + TestNodejsFunction, TestStack, - defaultRuntime, + TEST_RUNTIMES, } from '@aws-lambda-powertools/testing-utils'; +import { join } from 'node:path'; import { RESOURCE_NAME_PREFIX, - STACK_OUTPUT_LOG_GROUP, SETUP_TIMEOUT, - TEST_CASE_TIMEOUT, + STACK_OUTPUT_LOG_GROUP, TEARDOWN_TIMEOUT, + TEST_CASE_TIMEOUT, } from './constants'; -const runtime: string = process.env.RUNTIME || defaultRuntime; +describe(`Logger E2E tests, log event via env var setting with middy`, () => { + const runtime: string = process.env.RUNTIME || defaultRuntime; -if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key value: ${runtime}`); -} + if (!isValidRuntimeKey(runtime)) { + throw new Error(`Invalid runtime key value: ${runtime}`); + } -const uuid = v4(); -const stackName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'LogEventEnvVarSetting-Middy' -); -const functionName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'LogEventEnvVarSetting-Middy' -); -const lambdaFunctionCodeFile = - 'logEventEnvVarSetting.middy.test.FunctionCode.ts'; + const testName = generateTestUniqueName({ + testPrefix: RESOURCE_NAME_PREFIX, + runtime, + testName: 'LogEventEnvVarSetting-Middy', + }); + const testStack = new TestStack(testName); -const invocationCount = 3; + // Location of the lambda function code + const lambdaFunctionCodeFile = join( + __dirname, + 'logEventEnvVarSetting.middy.test.FunctionCode.ts' + ); -const testStack = new TestStack(stackName); -let logGroupName: string; // We do not know it until deployment + const fnNameLogEventEnvVar = concatenateResourceName({ + testName, + resourceName: 'LogEvent', + }); -describe(`logger E2E tests log event via env var setting (middy) for runtime: ${runtime}`, () => { - let invocationLogs: InvocationLogs[]; + let logGroupName: string; // We do not know it until deployment - beforeAll(async () => { - // Create and deploy a stack with AWS CDK - createStackWithLambdaFunction({ - stack: testStack.stack, - functionName: functionName, - functionEntry: path.join(__dirname, lambdaFunctionCodeFile), - environment: { - LOG_LEVEL: 'INFO', - POWERTOOLS_SERVICE_NAME: 'logger-e2e-testing', - UUID: uuid, + let invocationLogs: TestInvocationLogs[]; + const invocations = 3; - // Enabling the logger to log events via env var - POWERTOOLS_LOGGER_LOG_EVENT: 'true', + beforeAll(async () => { + // Prepare + new TestNodejsFunction( + testStack.stack, + fnNameLogEventEnvVar, + { + functionName: fnNameLogEventEnvVar, + entry: lambdaFunctionCodeFile, + runtime: TEST_RUNTIMES[runtime], + environment: { + LOG_LEVEL: 'INFO', + POWERTOOLS_SERVICE_NAME: 'logger-e2e-testing', + POWERTOOLS_LOGGER_LOG_EVENT: 'true', + }, }, - logGroupOutputKey: STACK_OUTPUT_LOG_GROUP, - runtime: runtime, - }); + { + logGroupOutputKey: STACK_OUTPUT_LOG_GROUP, + } + ); const result = await testStack.deploy(); logGroupName = result[STACK_OUTPUT_LOG_GROUP]; // Invoke the function three time (one for cold start, then two for warm start) - invocationLogs = await invokeFunction( - functionName, - invocationCount, - 'SEQUENTIAL' - ); + invocationLogs = await invokeFunction({ + functionName: fnNameLogEventEnvVar, + invocationMode: 'SEQUENTIAL', + times: invocations, + payload: { + foo: 'bar', + }, + }); console.log('logGroupName', logGroupName); }, SETUP_TIMEOUT); @@ -89,17 +93,17 @@ describe(`logger E2E tests log event via env var setting (middy) for runtime: ${ it( 'should log the event as the first log of each invocation only', async () => { - for (let i = 0; i < invocationCount; i++) { + for (let i = 0; i < invocations; i++) { // Get log messages of the invocation const logMessages = invocationLogs[i].getFunctionLogs(); for (const [index, message] of logMessages.entries()) { - const log = InvocationLogs.parseFunctionLog(message); + const log = TestInvocationLogs.parseFunctionLog(message); // Check that the event is logged on the first log if (index === 0) { expect(log).toHaveProperty('event'); expect(log.event).toStrictEqual( - expect.objectContaining({ invocation: i }) + expect.objectContaining({ foo: 'bar' }) ); // Check that the event is not logged again on the rest of the logs } else { diff --git a/packages/logger/tests/e2e/sampleRate.decorator.test.ts b/packages/logger/tests/e2e/sampleRate.decorator.test.ts index 53f6401aee..424d4fa7a5 100644 --- a/packages/logger/tests/e2e/sampleRate.decorator.test.ts +++ b/packages/logger/tests/e2e/sampleRate.decorator.test.ts @@ -3,18 +3,17 @@ * * @group e2e/logger/sampleRate */ -import path from 'path'; -import { v4 } from 'uuid'; +import { join } from 'node:path'; import { - createStackWithLambdaFunction, - generateUniqueName, + concatenateResourceName, + defaultRuntime, + generateTestUniqueName, invokeFunction, isValidRuntimeKey, -} from '../../../commons/tests/utils/e2eUtils'; -import { InvocationLogs } from '../../../commons/tests/utils/InvocationLogs'; -import { + TestNodejsFunction, TestStack, - defaultRuntime, + TEST_RUNTIMES, + TestInvocationLogs, } from '@aws-lambda-powertools/testing-utils'; import { RESOURCE_NAME_PREFIX, @@ -24,65 +23,68 @@ import { TEARDOWN_TIMEOUT, } from './constants'; -const runtime: string = process.env.RUNTIME || defaultRuntime; +describe(`Logger E2E tests, sample rate and injectLambdaContext()`, () => { + const runtime: string = process.env.RUNTIME || defaultRuntime; -if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key value: ${runtime}`); -} - -const LEVEL = InvocationLogs.LEVEL; - -const uuid = v4(); -const stackName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'SampleRate-Decorator' -); -const functionName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'SampleRate-Decorator' -); -const lambdaFunctionCodeFile = 'sampleRate.decorator.test.FunctionCode.ts'; + if (!isValidRuntimeKey(runtime)) { + throw new Error(`Invalid runtime key value: ${runtime}`); + } -const invocationCount = 20; + const testName = generateTestUniqueName({ + testPrefix: RESOURCE_NAME_PREFIX, + runtime, + testName: 'SampleRate-Decorator', + }); + const testStack = new TestStack(testName); -// Parameters to be used by Logger in the Lambda function -const LOG_MSG = `Log message ${uuid}`; -const SAMPLE_RATE = '0.5'; -const LOG_LEVEL = LEVEL.ERROR; + // Location of the lambda function code + const lambdaFunctionCodeFile = join( + __dirname, + 'sampleRate.decorator.test.FunctionCode.ts' + ); -const testStack = new TestStack(stackName); -let logGroupName: string; // We do not know the exact name until deployment + const fnNameSampleRate = concatenateResourceName({ + testName, + resourceName: 'SampleRate', + }); -describe(`logger E2E tests sample rate and injectLambdaContext() for runtime: nodejs18x`, () => { - let invocationLogs: InvocationLogs[]; + // Parameters to be used by Logger in the Lambda function + const LOG_MSG = `Log message ${fnNameSampleRate}`; + const SAMPLE_RATE = '0.5'; + const LEVEL = TestInvocationLogs.LEVEL; + const LOG_LEVEL = LEVEL.ERROR; + let logGroupName: string; // We do not know the exact name until deployment + let invocationLogs: TestInvocationLogs[]; + const invocations = 20; beforeAll(async () => { - // Create and deploy a stack with AWS CDK - createStackWithLambdaFunction({ - stack: testStack.stack, - functionName: functionName, - functionEntry: path.join(__dirname, lambdaFunctionCodeFile), - environment: { - LOG_LEVEL: LOG_LEVEL, - POWERTOOLS_SERVICE_NAME: 'logger-e2e-testing', - UUID: uuid, - - // Parameter(s) to be used by Logger in the Lambda function - LOG_MSG, - SAMPLE_RATE, + // Prepare + new TestNodejsFunction( + testStack.stack, + fnNameSampleRate, + { + functionName: fnNameSampleRate, + entry: lambdaFunctionCodeFile, + runtime: TEST_RUNTIMES[runtime], + environment: { + LOG_LEVEL: LOG_LEVEL, + POWERTOOLS_SERVICE_NAME: 'logger-e2e-testing', + LOG_MSG, + SAMPLE_RATE, + }, }, - logGroupOutputKey: STACK_OUTPUT_LOG_GROUP, - runtime: runtime, - }); + { + logGroupOutputKey: STACK_OUTPUT_LOG_GROUP, + } + ); const result = await testStack.deploy(); logGroupName = result[STACK_OUTPUT_LOG_GROUP]; - invocationLogs = await invokeFunction(functionName, invocationCount); + invocationLogs = await invokeFunction({ + functionName: fnNameSampleRate, + times: invocations, + }); console.log('logGroupName', logGroupName); }, SETUP_TIMEOUT); @@ -95,7 +97,7 @@ describe(`logger E2E tests sample rate and injectLambdaContext() for runtime: no let countSampled = 0; let countNotSampled = 0; - for (let i = 0; i < invocationCount; i++) { + for (let i = 0; i < invocations; i++) { // Get log messages of the invocation const logMessages = invocationLogs[i].getFunctionLogs(); @@ -127,13 +129,13 @@ describe(`logger E2E tests sample rate and injectLambdaContext() for runtime: no it( 'should inject Lambda context into every log emitted', async () => { - for (let i = 0; i < invocationCount; i++) { + for (let i = 0; i < invocations; i++) { // Get log messages of the invocation const logMessages = invocationLogs[i].getFunctionLogs(LEVEL.ERROR); // Check that the context is logged on every log for (const message of logMessages) { - const log = InvocationLogs.parseFunctionLog(message); + const log = TestInvocationLogs.parseFunctionLog(message); expect(log).toHaveProperty('function_arn'); expect(log).toHaveProperty('function_memory_size'); expect(log).toHaveProperty('function_name'); diff --git a/packages/metrics/tests/e2e/basicFeatures.decorators.test.ts b/packages/metrics/tests/e2e/basicFeatures.decorators.test.ts index e2dbd76c3d..981b6ed9ba 100644 --- a/packages/metrics/tests/e2e/basicFeatures.decorators.test.ts +++ b/packages/metrics/tests/e2e/basicFeatures.decorators.test.ts @@ -3,116 +3,101 @@ * * @group e2e/metrics/decorator */ -import path from 'path'; -import { Tracing } from 'aws-cdk-lib/aws-lambda'; import { - CloudWatchClient, - GetMetricStatisticsCommand, -} from '@aws-sdk/client-cloudwatch'; -import { v4 } from 'uuid'; -import { - generateUniqueName, - isValidRuntimeKey, - createStackWithLambdaFunction, + concatenateResourceName, + defaultRuntime, + generateTestUniqueName, invokeFunction, -} from '../../../commons/tests/utils/e2eUtils'; -import { + isValidRuntimeKey, + TestNodejsFunction, TestStack, - defaultRuntime, + TEST_RUNTIMES, } from '@aws-lambda-powertools/testing-utils'; -import { MetricUnits } from '../../src'; import { + CloudWatchClient, + GetMetricStatisticsCommand, +} from '@aws-sdk/client-cloudwatch'; +import { randomUUID } from 'node:crypto'; +import { join } from 'node:path'; +import { getMetrics } from '../helpers/metricsUtils'; +import { + commonEnvironmentVariables, + expectedDefaultDimensions, + expectedExtraDimension, + expectedMetricName, + expectedMetricValue, ONE_MINUTE, RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, TEARDOWN_TIMEOUT, TEST_CASE_TIMEOUT, } from './constants'; -import { getMetrics } from '../helpers/metricsUtils'; -const runtime: string = process.env.RUNTIME || defaultRuntime; +describe(`Metrics E2E tests, basic features decorator usage`, () => { + const runtime: string = process.env.RUNTIME || defaultRuntime; -if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key value: ${runtime}`); -} + if (!isValidRuntimeKey(runtime)) { + throw new Error(`Invalid runtime key value: ${runtime}`); + } + + const testName = generateTestUniqueName({ + testPrefix: RESOURCE_NAME_PREFIX, + runtime, + testName: 'BasicFeatures-Decorator', + }); + const testStack = new TestStack(testName); + const startTime = new Date(); + + // Location of the lambda function code + const lambdaFunctionCodeFile = join( + __dirname, + 'basicFeatures.decorator.test.functionCode.ts' + ); + + const fnNameBasicFeatures = concatenateResourceName({ + testName, + resourceName: 'BasicFeatures', + }); + + const cloudwatchClient = new CloudWatchClient({}); + + const invocations = 2; + + // Parameters to be used by Metrics in the Lambda function + const expectedNamespace = randomUUID(); // to easily find metrics back at assert phase + const expectedServiceName = fnNameBasicFeatures; -const uuid = v4(); -const stackName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'decorator' -); -const functionName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'decorator' -); -const lambdaFunctionCodeFile = 'basicFeatures.decorator.test.functionCode.ts'; - -const cloudwatchClient = new CloudWatchClient({}); - -const invocationCount = 2; -const startTime = new Date(); - -// Parameters to be used by Metrics in the Lambda function -const expectedNamespace = uuid; // to easily find metrics back at assert phase -const expectedServiceName = 'e2eDecorator'; -const expectedMetricName = 'MyMetric'; -const expectedMetricUnit = MetricUnits.Count; -const expectedMetricValue = '1'; -const expectedDefaultDimensions = { MyDimension: 'MyValue' }; -const expectedExtraDimension = { MyExtraDimension: 'MyExtraValue' }; -const expectedSingleMetricDimension = { MySingleMetricDim: 'MySingleValue' }; -const expectedSingleMetricName = 'MySingleMetric'; -const expectedSingleMetricUnit = MetricUnits.Percent; -const expectedSingleMetricValue = '2'; - -const testStack = new TestStack(stackName); - -describe(`metrics E2E tests (decorator) for runtime: ${runtime}`, () => { beforeAll(async () => { - // GIVEN a stack - createStackWithLambdaFunction({ - stack: testStack.stack, - functionName: functionName, - functionEntry: path.join(__dirname, lambdaFunctionCodeFile), - tracing: Tracing.ACTIVE, + // Prepare + new TestNodejsFunction(testStack.stack, fnNameBasicFeatures, { + functionName: fnNameBasicFeatures, + entry: lambdaFunctionCodeFile, + runtime: TEST_RUNTIMES[runtime], environment: { POWERTOOLS_SERVICE_NAME: 'metrics-e2e-testing', - UUID: uuid, - - // Parameter(s) to be used by Metrics in the Lambda function EXPECTED_NAMESPACE: expectedNamespace, EXPECTED_SERVICE_NAME: expectedServiceName, - EXPECTED_METRIC_NAME: expectedMetricName, - EXPECTED_METRIC_UNIT: expectedMetricUnit, - EXPECTED_METRIC_VALUE: expectedMetricValue, - EXPECTED_DEFAULT_DIMENSIONS: JSON.stringify(expectedDefaultDimensions), - EXPECTED_EXTRA_DIMENSION: JSON.stringify(expectedExtraDimension), - EXPECTED_SINGLE_METRIC_DIMENSION: JSON.stringify( - expectedSingleMetricDimension - ), - EXPECTED_SINGLE_METRIC_NAME: expectedSingleMetricName, - EXPECTED_SINGLE_METRIC_UNIT: expectedSingleMetricUnit, - EXPECTED_SINGLE_METRIC_VALUE: expectedSingleMetricValue, + ...commonEnvironmentVariables, }, - runtime: runtime, }); await testStack.deploy(); - // and invoked - await invokeFunction(functionName, invocationCount, 'SEQUENTIAL'); + // Act + await invokeFunction({ + functionName: fnNameBasicFeatures, + times: invocations, + invocationMode: 'SEQUENTIAL', + }); }, SETUP_TIMEOUT); + describe('ColdStart metrics', () => { it( 'should capture ColdStart Metric', async () => { const expectedDimensions = [ { Name: 'service', Value: expectedServiceName }, - { Name: 'function_name', Value: functionName }, + { Name: 'function_name', Value: fnNameBasicFeatures }, { Name: Object.keys(expectedDefaultDimensions)[0], Value: expectedDefaultDimensions.MyDimension, @@ -222,12 +207,13 @@ describe(`metrics E2E tests (decorator) for runtime: ${runtime}`, () => { ? metricStat.Datapoints[0] : {}; expect(singleDataPoint?.Sum).toBeGreaterThanOrEqual( - parseInt(expectedMetricValue) * invocationCount + parseInt(expectedMetricValue) * invocations ); }, TEST_CASE_TIMEOUT ); }); + afterAll(async () => { if (!process.env.DISABLE_TEARDOWN) { await testStack.destroy(); diff --git a/packages/metrics/tests/e2e/basicFeatures.manual.test.ts b/packages/metrics/tests/e2e/basicFeatures.manual.test.ts index 4bde10e7f3..fde4479a29 100644 --- a/packages/metrics/tests/e2e/basicFeatures.manual.test.ts +++ b/packages/metrics/tests/e2e/basicFeatures.manual.test.ts @@ -3,108 +3,90 @@ * * @group e2e/metrics/standardFunctions */ - -import path from 'path'; -import { Tracing } from 'aws-cdk-lib/aws-lambda'; import { - CloudWatchClient, - GetMetricStatisticsCommand, -} from '@aws-sdk/client-cloudwatch'; -import { v4 } from 'uuid'; -import { - generateUniqueName, - isValidRuntimeKey, - createStackWithLambdaFunction, + concatenateResourceName, + defaultRuntime, + generateTestUniqueName, invokeFunction, -} from '../../../commons/tests/utils/e2eUtils'; -import { + isValidRuntimeKey, + TestNodejsFunction, TestStack, - defaultRuntime, + TEST_RUNTIMES, } from '@aws-lambda-powertools/testing-utils'; -import { MetricUnits } from '../../src'; import { + CloudWatchClient, + GetMetricStatisticsCommand, +} from '@aws-sdk/client-cloudwatch'; +import { randomUUID } from 'node:crypto'; +import { join } from 'node:path'; +import { getMetrics } from '../helpers/metricsUtils'; +import { + commonEnvironmentVariables, + expectedDefaultDimensions, + expectedExtraDimension, + expectedMetricName, + expectedMetricValue, ONE_MINUTE, RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, TEARDOWN_TIMEOUT, TEST_CASE_TIMEOUT, } from './constants'; -import { getMetrics } from '../helpers/metricsUtils'; -const runtime: string = process.env.RUNTIME || defaultRuntime; +describe(`Metrics E2E tests, manual usage`, () => { + const runtime: string = process.env.RUNTIME || defaultRuntime; -if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key value: ${runtime}`); -} + if (!isValidRuntimeKey(runtime)) { + throw new Error(`Invalid runtime key value: ${runtime}`); + } + + const testName = generateTestUniqueName({ + testPrefix: RESOURCE_NAME_PREFIX, + runtime, + testName: 'BasicFeatures-Manual', + }); + const testStack = new TestStack(testName); + + // Location of the lambda function code + const lambdaFunctionCodeFile = join( + __dirname, + 'basicFeatures.manual.test.functionCode.ts' + ); + const startTime = new Date(); + + const fnNameManual = concatenateResourceName({ + testName, + resourceName: 'Manual', + }); + + // Parameters to be used by Metrics in the Lambda function + const expectedNamespace = randomUUID(); // to easily find metrics back at assert phase + const expectedServiceName = 'e2eManual'; + const cloudwatchClient = new CloudWatchClient({}); + const invocations = 2; -const uuid = v4(); -const stackName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'manual' -); -const functionName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'manual' -); -const lambdaFunctionCodeFile = 'basicFeatures.manual.test.functionCode.ts'; - -const cloudwatchClient = new CloudWatchClient({}); - -const invocationCount = 2; -const startTime = new Date(); - -// Parameters to be used by Metrics in the Lambda function -const expectedNamespace = uuid; // to easily find metrics back at assert phase -const expectedServiceName = 'e2eManual'; -const expectedMetricName = 'MyMetric'; -const expectedMetricUnit = MetricUnits.Count; -const expectedMetricValue = '1'; -const expectedDefaultDimensions = { MyDimension: 'MyValue' }; -const expectedExtraDimension = { MyExtraDimension: 'MyExtraValue' }; -const expectedSingleMetricDimension = { MySingleMetricDim: 'MySingleValue' }; -const expectedSingleMetricName = 'MySingleMetric'; -const expectedSingleMetricUnit = MetricUnits.Percent; -const expectedSingleMetricValue = '2'; - -const testStack = new TestStack(stackName); - -describe(`metrics E2E tests (manual) for runtime: ${runtime}`, () => { beforeAll(async () => { - // GIVEN a stack - createStackWithLambdaFunction({ - stack: testStack.stack, - functionName: functionName, - functionEntry: path.join(__dirname, lambdaFunctionCodeFile), - tracing: Tracing.ACTIVE, + // Prepare + new TestNodejsFunction(testStack.stack, fnNameManual, { + functionName: fnNameManual, + entry: lambdaFunctionCodeFile, + runtime: TEST_RUNTIMES[runtime], environment: { POWERTOOLS_SERVICE_NAME: 'metrics-e2e-testing', - UUID: uuid, - - // Parameter(s) to be used by Metrics in the Lambda function EXPECTED_NAMESPACE: expectedNamespace, EXPECTED_SERVICE_NAME: expectedServiceName, - EXPECTED_METRIC_NAME: expectedMetricName, - EXPECTED_METRIC_UNIT: expectedMetricUnit, - EXPECTED_METRIC_VALUE: expectedMetricValue, - EXPECTED_DEFAULT_DIMENSIONS: JSON.stringify(expectedDefaultDimensions), - EXPECTED_EXTRA_DIMENSION: JSON.stringify(expectedExtraDimension), - EXPECTED_SINGLE_METRIC_DIMENSION: JSON.stringify( - expectedSingleMetricDimension - ), - EXPECTED_SINGLE_METRIC_NAME: expectedSingleMetricName, - EXPECTED_SINGLE_METRIC_UNIT: expectedSingleMetricUnit, - EXPECTED_SINGLE_METRIC_VALUE: expectedSingleMetricValue, + ...commonEnvironmentVariables, }, - runtime: runtime, }); + await testStack.deploy(); - // and invoked - await invokeFunction(functionName, invocationCount, 'SEQUENTIAL'); + // Act + await invokeFunction({ + functionName: fnNameManual, + times: invocations, + invocationMode: 'SEQUENTIAL', + }); }, SETUP_TIMEOUT); describe('ColdStart metrics', () => { @@ -216,7 +198,7 @@ describe(`metrics E2E tests (manual) for runtime: ${runtime}`, () => { ? metricStat.Datapoints[0] : {}; expect(singleDataPoint.Sum).toBeGreaterThanOrEqual( - parseInt(expectedMetricValue) * invocationCount + parseInt(expectedMetricValue) * invocations ); }, TEST_CASE_TIMEOUT diff --git a/packages/metrics/tests/e2e/constants.ts b/packages/metrics/tests/e2e/constants.ts index 9eddc39ebe..3a6343e22a 100644 --- a/packages/metrics/tests/e2e/constants.ts +++ b/packages/metrics/tests/e2e/constants.ts @@ -1,5 +1,48 @@ -export const RESOURCE_NAME_PREFIX = 'Metrics-E2E'; -export const ONE_MINUTE = 60 * 1000; -export const TEST_CASE_TIMEOUT = 3 * ONE_MINUTE; -export const SETUP_TIMEOUT = 5 * ONE_MINUTE; -export const TEARDOWN_TIMEOUT = 5 * ONE_MINUTE; +import { MetricUnits } from '../../src'; + +const RESOURCE_NAME_PREFIX = 'Metrics-E2E'; +const ONE_MINUTE = 60 * 1000; +const TEST_CASE_TIMEOUT = 3 * ONE_MINUTE; +const SETUP_TIMEOUT = 5 * ONE_MINUTE; +const TEARDOWN_TIMEOUT = 5 * ONE_MINUTE; + +const expectedMetricName = 'MyMetric'; +const expectedMetricUnit = MetricUnits.Count; +const expectedMetricValue = '1'; +const expectedDefaultDimensions = { MyDimension: 'MyValue' }; +const expectedExtraDimension = { MyExtraDimension: 'MyExtraValue' }; +const expectedSingleMetricDimension = { MySingleMetricDim: 'MySingleValue' }; +const expectedSingleMetricName = 'MySingleMetric'; +const expectedSingleMetricUnit = MetricUnits.Percent; +const expectedSingleMetricValue = '2'; +const commonEnvironmentVariables = { + EXPECTED_METRIC_NAME: expectedMetricName, + EXPECTED_METRIC_UNIT: expectedMetricUnit, + EXPECTED_METRIC_VALUE: expectedMetricValue, + EXPECTED_DEFAULT_DIMENSIONS: JSON.stringify(expectedDefaultDimensions), + EXPECTED_EXTRA_DIMENSION: JSON.stringify(expectedExtraDimension), + EXPECTED_SINGLE_METRIC_DIMENSION: JSON.stringify( + expectedSingleMetricDimension + ), + EXPECTED_SINGLE_METRIC_NAME: expectedSingleMetricName, + EXPECTED_SINGLE_METRIC_UNIT: expectedSingleMetricUnit, + EXPECTED_SINGLE_METRIC_VALUE: expectedSingleMetricValue, +}; + +export { + RESOURCE_NAME_PREFIX, + ONE_MINUTE, + TEST_CASE_TIMEOUT, + SETUP_TIMEOUT, + TEARDOWN_TIMEOUT, + expectedMetricName, + expectedMetricUnit, + expectedMetricValue, + expectedDefaultDimensions, + expectedExtraDimension, + expectedSingleMetricDimension, + expectedSingleMetricName, + expectedSingleMetricUnit, + expectedSingleMetricValue, + commonEnvironmentVariables, +}; diff --git a/packages/parameters/tests/e2e/appConfigProvider.class.test.ts b/packages/parameters/tests/e2e/appConfigProvider.class.test.ts index ec662d8bb4..250ebd28ee 100644 --- a/packages/parameters/tests/e2e/appConfigProvider.class.test.ts +++ b/packages/parameters/tests/e2e/appConfigProvider.class.test.ts @@ -3,123 +3,31 @@ * * @group e2e/parameters/appconfig/class */ -import path from 'path'; -import { Aspects } from 'aws-cdk-lib'; -import { toBase64 } from '@aws-sdk/util-base64-node'; -import { v4 } from 'uuid'; import { - generateUniqueName, + concatenateResourceName, + defaultRuntime, + generateTestUniqueName, + invokeFunctionOnce, isValidRuntimeKey, - createStackWithLambdaFunction, - invokeFunction, -} from '../../../commons/tests/utils/e2eUtils'; -import { InvocationLogs } from '../../../commons/tests/utils/InvocationLogs'; -import { + TestInvocationLogs, + TestNodejsFunction, TestStack, - defaultRuntime, + TEST_RUNTIMES, } from '@aws-lambda-powertools/testing-utils'; +import { toBase64 } from '@aws-sdk/util-base64-node'; +import { Aspects } from 'aws-cdk-lib'; +import { join } from 'node:path'; import { ResourceAccessGranter } from '../helpers/cdkAspectGrantAccess'; +import { + createAppConfigConfigurationProfile, + createBaseAppConfigResources, +} from '../helpers/parametersUtils'; import { RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, TEARDOWN_TIMEOUT, TEST_CASE_TIMEOUT, } from './constants'; -import { - createBaseAppConfigResources, - createAppConfigConfigurationProfile, -} from '../helpers/parametersUtils'; - -const runtime: string = process.env.RUNTIME || defaultRuntime; - -if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key value: ${runtime}`); -} - -const uuid = v4(); -const stackName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'appConfigProvider' -); -const functionName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'appConfigProvider' -); -const lambdaFunctionCodeFile = 'appConfigProvider.class.test.functionCode.ts'; - -const invocationCount = 1; - -const applicationName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'app' -); -const environmentName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'env' -); -const deploymentStrategyName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'immediate' -); -const freeFormJsonName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'freeFormJson' -); -const freeFormYamlName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'freeFormYaml' -); -const freeFormBase64PlainTextName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'freeFormBase64PlainText' -); -const featureFlagName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'featureFlag' -); - -const freeFormJsonValue = { - foo: 'bar', -}; -const freeFormYamlValue = `foo: bar -`; -const freeFormPlainTextValue = 'foo'; -const freeFormBase64PlainTextValue = toBase64( - new TextEncoder().encode(freeFormPlainTextValue) -); -const featureFlagValue = { - version: '1', - flags: { - myFeatureFlag: { - name: 'myFeatureFlag', - }, - }, - values: { - myFeatureFlag: { - enabled: true, - }, - }, -}; - -const testStack = new TestStack(stackName); /** * This test suite deploys a CDK stack with a Lambda function and a number of AppConfig parameters. @@ -173,20 +81,99 @@ const testStack = new TestStack(stackName); * is created after the previous one. This is necessary because we share the same AppConfig * application and environment for all tests. */ -describe(`parameters E2E tests (appConfigProvider) for runtime ${runtime}`, () => { - let invocationLogs: InvocationLogs[]; +describe(`Parameters E2E tests, AppConfig provider`, () => { + const runtime: string = process.env.RUNTIME || defaultRuntime; + + if (!isValidRuntimeKey(runtime)) { + throw new Error(`Invalid runtime key value: ${runtime}`); + } + + const testName = generateTestUniqueName({ + testPrefix: RESOURCE_NAME_PREFIX, + runtime, + testName: 'AppConfig', + }); + const testStack = new TestStack(testName); + + // Location of the lambda function code + const lambdaFunctionCodeFile = join( + __dirname, + 'appConfigProvider.class.test.functionCode.ts' + ); + + const functionName = concatenateResourceName({ + testName, + resourceName: 'appConfigProvider', + }); + + const applicationName = concatenateResourceName({ + testName, + resourceName: 'app', + }); + + const environmentName = concatenateResourceName({ + testName, + resourceName: 'env', + }); + + const deploymentStrategyName = concatenateResourceName({ + testName, + resourceName: 'immediate', + }); + + const freeFormJsonName = concatenateResourceName({ + testName, + resourceName: 'freeFormJson', + }); + + const freeFormYamlName = concatenateResourceName({ + testName, + resourceName: 'freeFormYaml', + }); + + const freeFormBase64PlainTextName = concatenateResourceName({ + testName, + resourceName: 'freeFormBase64PlainText', + }); + + const featureFlagName = concatenateResourceName({ + testName, + resourceName: 'featureFlag', + }); + + const freeFormJsonValue = { + foo: 'bar', + }; + const freeFormYamlValue = `foo: bar +`; + const freeFormPlainTextValue = 'foo'; + const freeFormBase64PlainTextValue = toBase64( + new TextEncoder().encode(freeFormPlainTextValue) + ); + const featureFlagValue = { + version: '1', + flags: { + myFeatureFlag: { + name: 'myFeatureFlag', + }, + }, + values: { + myFeatureFlag: { + enabled: true, + }, + }, + }; + + let invocationLogs: TestInvocationLogs; const encoder = new TextEncoder(); beforeAll(async () => { - // Create a stack with a Lambda function - createStackWithLambdaFunction({ - stack: testStack.stack, - functionName, - functionEntry: path.join(__dirname, lambdaFunctionCodeFile), + // Prepare + new TestNodejsFunction(testStack.stack, functionName, { + functionName: functionName, + entry: lambdaFunctionCodeFile, + runtime: TEST_RUNTIMES[runtime], environment: { - UUID: uuid, - - // Values(s) to be used by Parameters in the Lambda function APPLICATION_NAME: applicationName, ENVIRONMENT_NAME: environmentName, FREEFORM_JSON_NAME: freeFormJsonName, @@ -194,7 +181,6 @@ describe(`parameters E2E tests (appConfigProvider) for runtime ${runtime}`, () = FREEFORM_BASE64_ENCODED_PLAIN_TEXT_NAME: freeFormBase64PlainTextName, FEATURE_FLAG_NAME: featureFlagName, }, - runtime, }); // Create the base resources for an AppConfig application. @@ -276,18 +262,16 @@ describe(`parameters E2E tests (appConfigProvider) for runtime ${runtime}`, () = await testStack.deploy(); // and invoke the Lambda function - invocationLogs = await invokeFunction( + invocationLogs = await invokeFunctionOnce({ functionName, - invocationCount, - 'SEQUENTIAL' - ); + }); }, SETUP_TIMEOUT); describe('AppConfigProvider usage', () => { // Test 1 - get a single parameter as-is (no transformation - should return an Uint8Array) it('should retrieve single parameter as-is', () => { - const logs = invocationLogs[0].getFunctionLogs(); - const testLog = InvocationLogs.parseFunctionLog(logs[0]); + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[0]); expect(testLog).toStrictEqual({ test: 'get', @@ -297,8 +281,8 @@ describe(`parameters E2E tests (appConfigProvider) for runtime ${runtime}`, () = // Test 2 - get a free-form JSON and apply json transformation (should return an object) it('should retrieve a free-form JSON parameter with JSON transformation', () => { - const logs = invocationLogs[0].getFunctionLogs(); - const testLog = InvocationLogs.parseFunctionLog(logs[1]); + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[1]); expect(testLog).toStrictEqual({ test: 'get-freeform-json-binary', @@ -309,8 +293,8 @@ describe(`parameters E2E tests (appConfigProvider) for runtime ${runtime}`, () = // Test 3 - get a free-form base64-encoded plain text and apply binary transformation // (should return a decoded string) it('should retrieve a base64-encoded plain text parameter with binary transformation', () => { - const logs = invocationLogs[0].getFunctionLogs(); - const testLog = InvocationLogs.parseFunctionLog(logs[2]); + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[2]); expect(testLog).toStrictEqual({ test: 'get-freeform-base64-plaintext-binary', @@ -320,8 +304,8 @@ describe(`parameters E2E tests (appConfigProvider) for runtime ${runtime}`, () = // Test 4 - get a feature flag and apply json transformation (should return an object) it('should retrieve a feature flag parameter with JSON transformation', () => { - const logs = invocationLogs[0].getFunctionLogs(); - const testLog = InvocationLogs.parseFunctionLog(logs[3]); + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[3]); expect(testLog).toStrictEqual({ test: 'get-feature-flag-binary', @@ -334,8 +318,8 @@ describe(`parameters E2E tests (appConfigProvider) for runtime ${runtime}`, () = it( 'should retrieve single parameter cached', () => { - const logs = invocationLogs[0].getFunctionLogs(); - const testLog = InvocationLogs.parseFunctionLog(logs[4]); + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[4]); expect(testLog).toStrictEqual({ test: 'get-cached', @@ -350,8 +334,8 @@ describe(`parameters E2E tests (appConfigProvider) for runtime ${runtime}`, () = it( 'should retrieve single parameter twice without caching', async () => { - const logs = invocationLogs[0].getFunctionLogs(); - const testLog = InvocationLogs.parseFunctionLog(logs[5]); + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[5]); expect(testLog).toStrictEqual({ test: 'get-forced', @@ -367,8 +351,8 @@ describe(`parameters E2E tests (appConfigProvider) for runtime ${runtime}`, () = it( 'should retrieve single parameter twice, with expiration between and matching values', async () => { - const logs = invocationLogs[0].getFunctionLogs(); - const testLog = InvocationLogs.parseFunctionLog(logs[6]); + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[6]); const result = freeFormPlainTextValue; expect(testLog).toStrictEqual({ diff --git a/packages/parameters/tests/e2e/dynamoDBProvider.class.test.ts b/packages/parameters/tests/e2e/dynamoDBProvider.class.test.ts index c47a006e30..e86bc6c9c0 100644 --- a/packages/parameters/tests/e2e/dynamoDBProvider.class.test.ts +++ b/packages/parameters/tests/e2e/dynamoDBProvider.class.test.ts @@ -3,86 +3,31 @@ * * @group e2e/parameters/dynamodb/class */ -import path from 'path'; -import { AttributeType } from 'aws-cdk-lib/aws-dynamodb'; -import { Aspects } from 'aws-cdk-lib'; -import { v4 } from 'uuid'; import { - generateUniqueName, + concatenateResourceName, + defaultRuntime, + generateTestUniqueName, + invokeFunctionOnce, isValidRuntimeKey, - createStackWithLambdaFunction, - invokeFunction, -} from '../../../commons/tests/utils/e2eUtils'; -import { InvocationLogs } from '../../../commons/tests/utils/InvocationLogs'; -import { + TestInvocationLogs, + TestNodejsFunction, TestStack, - defaultRuntime, + TEST_RUNTIMES, } from '@aws-lambda-powertools/testing-utils'; +import { Aspects } from 'aws-cdk-lib'; +import { AttributeType } from 'aws-cdk-lib/aws-dynamodb'; +import { join } from 'node:path'; import { ResourceAccessGranter } from '../helpers/cdkAspectGrantAccess'; +import { + createDynamoDBTable, + putDynamoDBItem, +} from '../helpers/parametersUtils'; import { RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, TEARDOWN_TIMEOUT, TEST_CASE_TIMEOUT, } from './constants'; -import { - createDynamoDBTable, - putDynamoDBItem, -} from '../helpers/parametersUtils'; - -const runtime: string = process.env.RUNTIME || defaultRuntime; - -if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key value: ${runtime}`); -} - -const uuid = v4(); -const stackName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'dynamoDBProvider' -); -const functionName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'dynamoDBProvider' -); -const lambdaFunctionCodeFile = 'dynamoDBProvider.class.test.functionCode.ts'; - -const invocationCount = 1; - -// Parameters to be used by Parameters in the Lambda function -const tableGet = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'Table-Get' -); -const tableGetMultiple = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'Table-GetMultiple' -); -const tableGetCustomkeys = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'Table-GetCustomKeys' -); -const tableGetMultipleCustomkeys = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'Table-GetMultipleCustomKeys' -); -const keyAttr = 'key'; -const sortAttr = 'sort'; -const valueAttr = 'val'; - -const testStack = new TestStack(stackName); /** * This test suite deploys a CDK stack with a Lambda function and a number of DynamoDB tables. @@ -160,19 +105,65 @@ const testStack = new TestStack(stackName); * Test 9 * Get a cached parameter and force retrieval. This also uses the same custom SDK client that counts the number of calls to DynamoDB. */ -describe(`parameters E2E tests (dynamoDBProvider) for runtime: ${runtime}`, () => { - let invocationLogs: InvocationLogs[]; +describe(`Parameters E2E tests, dynamoDB provider`, () => { + const runtime: string = process.env.RUNTIME || defaultRuntime; + + if (!isValidRuntimeKey(runtime)) { + throw new Error(`Invalid runtime key value: ${runtime}`); + } + + const testName = generateTestUniqueName({ + testPrefix: RESOURCE_NAME_PREFIX, + runtime, + testName: 'AllFeatures-Decorator', + }); + const testStack = new TestStack(testName); + + // Location of the lambda function code + const lambdaFunctionCodeFile = join( + __dirname, + 'dynamoDBProvider.class.test.functionCode.ts' + ); + + const functionName = concatenateResourceName({ + testName, + resourceName: 'dynamoDBProvider', + }); + + // Parameters to be used by Parameters in the Lambda function + const tableGet = concatenateResourceName({ + testName, + resourceName: 'Table-Get', + }); + + const tableGetMultiple = concatenateResourceName({ + testName, + resourceName: 'Table-GetMultiple', + }); + + const tableGetCustomkeys = concatenateResourceName({ + testName, + resourceName: 'Table-GetCustomKeys', + }); + + const tableGetMultipleCustomkeys = concatenateResourceName({ + testName, + resourceName: 'Table-GetMultipleCustomKeys', + }); + + const keyAttr = 'key'; + const sortAttr = 'sort'; + const valueAttr = 'val'; + + let invocationLogs: TestInvocationLogs; beforeAll(async () => { - // Create a stack with a Lambda function - createStackWithLambdaFunction({ - stack: testStack.stack, + // Prepare + new TestNodejsFunction(testStack.stack, functionName, { functionName, - functionEntry: path.join(__dirname, lambdaFunctionCodeFile), + entry: lambdaFunctionCodeFile, + runtime: TEST_RUNTIMES[runtime], environment: { - UUID: uuid, - - // Values(s) to be used by Parameters in the Lambda function TABLE_GET: tableGet, TABLE_GET_MULTIPLE: tableGetMultiple, TABLE_GET_CUSTOM_KEYS: tableGetCustomkeys, @@ -181,7 +172,6 @@ describe(`parameters E2E tests (dynamoDBProvider) for runtime: ${runtime}`, () = SORT_ATTR: sortAttr, VALUE_ATTR: valueAttr, }, - runtime, }); // Create the DynamoDB tables @@ -357,11 +347,9 @@ describe(`parameters E2E tests (dynamoDBProvider) for runtime: ${runtime}`, () = await testStack.deploy(); // and invoke the Lambda function - invocationLogs = await invokeFunction( + invocationLogs = await invokeFunctionOnce({ functionName, - invocationCount, - 'SEQUENTIAL' - ); + }); }, SETUP_TIMEOUT); describe('DynamoDBProvider usage', () => { @@ -369,8 +357,8 @@ describe(`parameters E2E tests (dynamoDBProvider) for runtime: ${runtime}`, () = it( 'should retrieve a single parameter', async () => { - const logs = invocationLogs[0].getFunctionLogs(); - const testLog = InvocationLogs.parseFunctionLog(logs[0]); + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[0]); expect(testLog).toStrictEqual({ test: 'get', @@ -384,8 +372,8 @@ describe(`parameters E2E tests (dynamoDBProvider) for runtime: ${runtime}`, () = it( 'should retrieve multiple parameters', async () => { - const logs = invocationLogs[0].getFunctionLogs(); - const testLog = InvocationLogs.parseFunctionLog(logs[1]); + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[1]); expect(testLog).toStrictEqual({ test: 'get-multiple', @@ -399,8 +387,8 @@ describe(`parameters E2E tests (dynamoDBProvider) for runtime: ${runtime}`, () = it( 'should retrieve a single parameter', async () => { - const logs = invocationLogs[0].getFunctionLogs(); - const testLog = InvocationLogs.parseFunctionLog(logs[2]); + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[2]); expect(testLog).toStrictEqual({ test: 'get-custom', @@ -414,8 +402,8 @@ describe(`parameters E2E tests (dynamoDBProvider) for runtime: ${runtime}`, () = it( 'should retrieve multiple parameters', async () => { - const logs = invocationLogs[0].getFunctionLogs(); - const testLog = InvocationLogs.parseFunctionLog(logs[3]); + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[3]); expect(testLog).toStrictEqual({ test: 'get-multiple-custom', @@ -427,8 +415,8 @@ describe(`parameters E2E tests (dynamoDBProvider) for runtime: ${runtime}`, () = // Test 5 - get a single parameter with json transform it('should retrieve a single parameter with json transform', async () => { - const logs = invocationLogs[0].getFunctionLogs(); - const testLog = InvocationLogs.parseFunctionLog(logs[4]); + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[4]); expect(testLog).toStrictEqual({ test: 'get-json-transform', @@ -438,8 +426,8 @@ describe(`parameters E2E tests (dynamoDBProvider) for runtime: ${runtime}`, () = // Test 6 - get a single parameter with binary transform it('should retrieve a single parameter with binary transform', async () => { - const logs = invocationLogs[0].getFunctionLogs(); - const testLog = InvocationLogs.parseFunctionLog(logs[5]); + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[5]); expect(testLog).toStrictEqual({ test: 'get-binary-transform', @@ -449,8 +437,8 @@ describe(`parameters E2E tests (dynamoDBProvider) for runtime: ${runtime}`, () = // Test 7 - get multiple parameters with auto transforms (json and binary) it('should retrieve multiple parameters with auto transforms', async () => { - const logs = invocationLogs[0].getFunctionLogs(); - const testLog = InvocationLogs.parseFunctionLog(logs[6]); + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[6]); expect(testLog).toStrictEqual({ test: 'get-multiple-auto-transform', @@ -463,8 +451,8 @@ describe(`parameters E2E tests (dynamoDBProvider) for runtime: ${runtime}`, () = // Test 8 - Get a parameter twice and check that the value is cached. it('should retrieve multiple parameters with auto transforms', async () => { - const logs = invocationLogs[0].getFunctionLogs(); - const testLog = InvocationLogs.parseFunctionLog(logs[7]); + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[7]); expect(testLog).toStrictEqual({ test: 'get-cached', @@ -474,8 +462,8 @@ describe(`parameters E2E tests (dynamoDBProvider) for runtime: ${runtime}`, () = // Test 9 - Get a cached parameter and force retrieval. it('should retrieve multiple parameters with auto transforms', async () => { - const logs = invocationLogs[0].getFunctionLogs(); - const testLog = InvocationLogs.parseFunctionLog(logs[8]); + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[8]); expect(testLog).toStrictEqual({ test: 'get-forced', diff --git a/packages/parameters/tests/e2e/secretsProvider.class.test.ts b/packages/parameters/tests/e2e/secretsProvider.class.test.ts index d43dd768e5..47ceb0620e 100644 --- a/packages/parameters/tests/e2e/secretsProvider.class.test.ts +++ b/packages/parameters/tests/e2e/secretsProvider.class.test.ts @@ -4,34 +4,27 @@ * @group e2e/parameters/secrets/class */ import { - createStackWithLambdaFunction, - generateUniqueName, - invokeFunction, + concatenateResourceName, + defaultRuntime, + generateTestUniqueName, + invokeFunctionOnce, isValidRuntimeKey, -} from '../../../commons/tests/utils/e2eUtils'; + TestInvocationLogs, + TestNodejsFunction, + TestStack, + TEST_RUNTIMES, +} from '@aws-lambda-powertools/testing-utils'; +import { Aspects, SecretValue } from 'aws-cdk-lib'; +import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; +import { join } from 'node:path'; +import { ResourceAccessGranter } from '../helpers/cdkAspectGrantAccess'; import { RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, TEARDOWN_TIMEOUT, TEST_CASE_TIMEOUT, } from './constants'; -import { v4 } from 'uuid'; -import { Tracing } from 'aws-cdk-lib/aws-lambda'; -import { - TestStack, - defaultRuntime, -} from '@aws-lambda-powertools/testing-utils'; -import { Aspects, SecretValue } from 'aws-cdk-lib'; -import path from 'path'; -import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; -import { InvocationLogs } from '../../../commons/tests/utils/InvocationLogs'; -import { ResourceAccessGranter } from '../helpers/cdkAspectGrantAccess'; -const runtime: string = process.env.RUNTIME || defaultRuntime; - -if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key: ${runtime}`); -} /** * Collection of e2e tests for SecretsProvider utility. * @@ -49,76 +42,71 @@ if (!isValidRuntimeKey(runtime)) { * Make sure to add the right permissions to the lambda function to access the resources. We use our `ResourceAccessGranter` to add permissions. * */ -describe(`parameters E2E tests (SecretsProvider) for runtime: ${runtime}`, () => { - const uuid = v4(); - let invocationLogs: InvocationLogs[]; - const stackName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'secretsProvider' - ); - const functionName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, +describe(`Parameters E2E tests, Secrets Manager provider`, () => { + const runtime: string = process.env.RUNTIME || defaultRuntime; + + if (!isValidRuntimeKey(runtime)) { + throw new Error(`Invalid runtime key: ${runtime}`); + } + + const testName = generateTestUniqueName({ + testPrefix: RESOURCE_NAME_PREFIX, runtime, - 'secretsProvider' + testName: 'SecretsProvider', + }); + const testStack = new TestStack(testName); + + // Location of the lambda function code + const lambdaFunctionCodeFile = join( + __dirname, + 'secretsProvider.class.test.functionCode.ts' ); - const lambdaFunctionCodeFile = 'secretsProvider.class.test.functionCode.ts'; - const invocationCount = 1; + const functionName = concatenateResourceName({ + testName, + resourceName: 'secretsProvider', + }); - const testStack = new TestStack(stackName); + let invocationLogs: TestInvocationLogs; beforeAll(async () => { // use unique names for each test to keep a clean state - const secretNamePlain = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'testSecretPlain' - ); - const secretNameObject = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'testSecretObject' - ); - const secretNameBinary = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'testSecretBinary' - ); - const secretNamePlainCached = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'testSecretPlainCached' - ); - const secretNamePlainForceFetch = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'testSecretPlainForceFetch' - ); + const secretNamePlain = concatenateResourceName({ + testName, + resourceName: 'testSecretPlain', + }); + + const secretNameObject = concatenateResourceName({ + testName, + resourceName: 'testSecretObject', + }); + + const secretNameBinary = concatenateResourceName({ + testName, + resourceName: 'testSecretBinary', + }); + + const secretNamePlainCached = concatenateResourceName({ + testName, + resourceName: 'testSecretPlainCached', + }); - // creates the test fuction that uses Powertools for AWS Lambda (TypeScript) secret provider we want to test - // pass env vars with secret names we want to fetch - createStackWithLambdaFunction({ - stack: testStack.stack, + const secretNamePlainForceFetch = concatenateResourceName({ + testName, + resourceName: 'testSecretPlainForceFetch', + }); + + new TestNodejsFunction(testStack.stack, functionName, { functionName: functionName, - functionEntry: path.join(__dirname, lambdaFunctionCodeFile), - tracing: Tracing.ACTIVE, + entry: lambdaFunctionCodeFile, + runtime: TEST_RUNTIMES[runtime], environment: { - UUID: uuid, SECRET_NAME_PLAIN: secretNamePlain, SECRET_NAME_OBJECT: secretNameObject, SECRET_NAME_BINARY: secretNameBinary, SECRET_NAME_PLAIN_CACHED: secretNamePlainCached, SECRET_NAME_PLAIN_FORCE_FETCH: secretNamePlainForceFetch, }, - runtime: runtime, }); const secretString = new Secret(testStack.stack, 'testSecretPlain', { @@ -169,19 +157,17 @@ describe(`parameters E2E tests (SecretsProvider) for runtime: ${runtime}`, () => await testStack.deploy(); - invocationLogs = await invokeFunction( + invocationLogs = await invokeFunctionOnce({ functionName, - invocationCount, - 'SEQUENTIAL' - ); + }); }, SETUP_TIMEOUT); describe('SecretsProvider usage', () => { it( 'should retrieve a secret as plain string', async () => { - const logs = invocationLogs[0].getFunctionLogs(); - const testLog = InvocationLogs.parseFunctionLog(logs[0]); + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[0]); expect(testLog).toStrictEqual({ test: 'get-plain', @@ -194,8 +180,8 @@ describe(`parameters E2E tests (SecretsProvider) for runtime: ${runtime}`, () => it( 'should retrieve a secret using transform json option', async () => { - const logs = invocationLogs[0].getFunctionLogs(); - const testLog = InvocationLogs.parseFunctionLog(logs[1]); + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[1]); expect(testLog).toStrictEqual({ test: 'get-transform-json', @@ -208,8 +194,8 @@ describe(`parameters E2E tests (SecretsProvider) for runtime: ${runtime}`, () => it( 'should retrieve a secret using transform binary option', async () => { - const logs = invocationLogs[0].getFunctionLogs(); - const testLog = InvocationLogs.parseFunctionLog(logs[2]); + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[2]); expect(testLog).toStrictEqual({ test: 'get-transform-binary', @@ -221,8 +207,8 @@ describe(`parameters E2E tests (SecretsProvider) for runtime: ${runtime}`, () => }); it('should retrieve a secret twice with cached value', async () => { - const logs = invocationLogs[0].getFunctionLogs(); - const testLogFirst = InvocationLogs.parseFunctionLog(logs[3]); + const logs = invocationLogs.getFunctionLogs(); + const testLogFirst = TestInvocationLogs.parseFunctionLog(logs[3]); // we fetch twice, but we expect to make an API call only once expect(testLogFirst).toStrictEqual({ @@ -232,8 +218,8 @@ describe(`parameters E2E tests (SecretsProvider) for runtime: ${runtime}`, () => }); it('should retrieve a secret twice with forceFetch second time', async () => { - const logs = invocationLogs[0].getFunctionLogs(); - const testLogFirst = InvocationLogs.parseFunctionLog(logs[4]); + const logs = invocationLogs.getFunctionLogs(); + const testLogFirst = TestInvocationLogs.parseFunctionLog(logs[4]); // we fetch twice, 2nd time with forceFetch: true flag, we expect two api calls expect(testLogFirst).toStrictEqual({ diff --git a/packages/parameters/tests/e2e/ssmProvider.class.test.ts b/packages/parameters/tests/e2e/ssmProvider.class.test.ts index a685a988f5..be0bc323c7 100644 --- a/packages/parameters/tests/e2e/ssmProvider.class.test.ts +++ b/packages/parameters/tests/e2e/ssmProvider.class.test.ts @@ -3,86 +3,28 @@ * * @group e2e/parameters/ssm/class */ -import path from 'path'; -import { Aspects } from 'aws-cdk-lib'; -import { StringParameter } from 'aws-cdk-lib/aws-ssm'; -import { v4 } from 'uuid'; import { - generateUniqueName, + concatenateResourceName, + defaultRuntime, + generateTestUniqueName, + invokeFunctionOnce, isValidRuntimeKey, - createStackWithLambdaFunction, - invokeFunction, -} from '../../../commons/tests/utils/e2eUtils'; -import { InvocationLogs } from '../../../commons/tests/utils/InvocationLogs'; -import { + TestInvocationLogs, + TestNodejsFunction, TestStack, - defaultRuntime, + TEST_RUNTIMES, } from '@aws-lambda-powertools/testing-utils'; +import { Aspects } from 'aws-cdk-lib'; +import { StringParameter } from 'aws-cdk-lib/aws-ssm'; +import { join } from 'node:path'; import { ResourceAccessGranter } from '../helpers/cdkAspectGrantAccess'; +import { createSSMSecureString } from '../helpers/parametersUtils'; import { RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, TEARDOWN_TIMEOUT, TEST_CASE_TIMEOUT, } from './constants'; -import { createSSMSecureString } from '../helpers/parametersUtils'; - -const runtime: string = process.env.RUNTIME || defaultRuntime; - -if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key value: ${runtime}`); -} - -const uuid = v4(); -const stackName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'ssmProvider' -); -const functionName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'ssmProvider' -); -const lambdaFunctionCodeFile = 'ssmProvider.class.test.functionCode.ts'; - -const invocationCount = 1; - -// Parameter names to be used by Parameters in the Lambda function -const paramA = generateUniqueName( - `/${RESOURCE_NAME_PREFIX}`, - uuid, - runtime, - 'param/a' -); -const paramB = generateUniqueName( - `/${RESOURCE_NAME_PREFIX}`, - uuid, - runtime, - 'param/b' -); -const paramEncryptedA = generateUniqueName( - `/${RESOURCE_NAME_PREFIX}`, - uuid, - runtime, - 'param-encrypted/a' -); -const paramEncryptedB = generateUniqueName( - `/${RESOURCE_NAME_PREFIX}`, - uuid, - runtime, - 'param-encrypted/b' -); - -// Parameters values -const paramAValue = 'foo'; -const paramBValue = 'bar'; -const paramEncryptedAValue = 'foo-encrypted'; -const paramEncryptedBValue = 'bar-encrypted'; - -const testStack = new TestStack(stackName); /** * This test suite deploys a CDK stack with a Lambda function and a number of SSM parameters. @@ -133,25 +75,72 @@ const testStack = new TestStack(stackName); * get parameter twice, but force fetch 2nd time, we count number of SDK requests and * check that we made two API calls */ -describe(`parameters E2E tests (ssmProvider) for runtime: ${runtime}`, () => { - let invocationLogs: InvocationLogs[]; +describe(`Parameters E2E tests, SSM provider`, () => { + const runtime: string = process.env.RUNTIME || defaultRuntime; + + if (!isValidRuntimeKey(runtime)) { + throw new Error(`Invalid runtime key value: ${runtime}`); + } + + const testName = generateTestUniqueName({ + testPrefix: RESOURCE_NAME_PREFIX, + runtime, + testName: 'SSMProvider', + }); + const testStack = new TestStack(testName); + + // Location of the lambda function code + const lambdaFunctionCodeFile = join( + __dirname, + 'ssmProvider.class.test.functionCode.ts' + ); + + const functionName = concatenateResourceName({ + testName, + resourceName: 'ssmProvider', + }); + + // Parameter names to be used by Parameters in the Lambda function + const paramA = `/${concatenateResourceName({ + testName, + resourceName: 'param/a', + })}`; + + const paramB = `/${concatenateResourceName({ + testName, + resourceName: 'param/b', + })}`; + + const paramEncryptedA = `/${concatenateResourceName({ + testName, + resourceName: 'param-encrypted/a', + })}`; + + const paramEncryptedB = `/${concatenateResourceName({ + testName, + resourceName: 'param-encrypted/b', + })}`; + + // Parameters values + const paramAValue = 'foo'; + const paramBValue = 'bar'; + const paramEncryptedAValue = 'foo-encrypted'; + const paramEncryptedBValue = 'bar-encrypted'; + + let invocationLogs: TestInvocationLogs; beforeAll(async () => { - // Create a stack with a Lambda function - createStackWithLambdaFunction({ - stack: testStack.stack, - functionName, - functionEntry: path.join(__dirname, lambdaFunctionCodeFile), + // Prepare + new TestNodejsFunction(testStack.stack, functionName, { + functionName: functionName, + entry: lambdaFunctionCodeFile, + runtime: TEST_RUNTIMES[runtime], environment: { - UUID: uuid, - - // Values(s) to be used by Parameters in the Lambda function PARAM_A: paramA, PARAM_B: paramB, PARAM_ENCRYPTED_A: paramEncryptedA, PARAM_ENCRYPTED_B: paramEncryptedB, }, - runtime, }); // Create SSM parameters @@ -192,11 +181,9 @@ describe(`parameters E2E tests (ssmProvider) for runtime: ${runtime}`, () => { await testStack.deploy(); // and invoke the Lambda function - invocationLogs = await invokeFunction( + invocationLogs = await invokeFunctionOnce({ functionName, - invocationCount, - 'SEQUENTIAL' - ); + }); }, SETUP_TIMEOUT); describe('SSMProvider usage', () => { @@ -204,8 +191,8 @@ describe(`parameters E2E tests (ssmProvider) for runtime: ${runtime}`, () => { it( 'should retrieve a single parameter', async () => { - const logs = invocationLogs[0].getFunctionLogs(); - const testLog = InvocationLogs.parseFunctionLog(logs[0]); + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[0]); expect(testLog).toStrictEqual({ test: 'get', @@ -219,8 +206,8 @@ describe(`parameters E2E tests (ssmProvider) for runtime: ${runtime}`, () => { it( 'should retrieve a single parameter with decryption', async () => { - const logs = invocationLogs[0].getFunctionLogs(); - const testLog = InvocationLogs.parseFunctionLog(logs[1]); + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[1]); expect(testLog).toStrictEqual({ test: 'get-decrypt', @@ -234,8 +221,8 @@ describe(`parameters E2E tests (ssmProvider) for runtime: ${runtime}`, () => { it( 'should retrieve multiple parameters', async () => { - const logs = invocationLogs[0].getFunctionLogs(); - const testLog = InvocationLogs.parseFunctionLog(logs[2]); + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[2]); const expectedParameterNameA = paramA.substring( paramA.lastIndexOf('/') + 1 ); @@ -260,8 +247,8 @@ describe(`parameters E2E tests (ssmProvider) for runtime: ${runtime}`, () => { it( 'should retrieve multiple parameters recursively', async () => { - const logs = invocationLogs[0].getFunctionLogs(); - const testLog = InvocationLogs.parseFunctionLog(logs[3]); + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[3]); const expectedParameterNameA = paramA.substring( paramA.lastIndexOf('/') + 1 ); @@ -283,8 +270,8 @@ describe(`parameters E2E tests (ssmProvider) for runtime: ${runtime}`, () => { it( 'should retrieve multiple parameters with decryption', async () => { - const logs = invocationLogs[0].getFunctionLogs(); - const testLog = InvocationLogs.parseFunctionLog(logs[4]); + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[4]); const expectedParameterNameA = paramEncryptedA.substring( paramEncryptedA.lastIndexOf('/') + 1 ); @@ -307,8 +294,8 @@ describe(`parameters E2E tests (ssmProvider) for runtime: ${runtime}`, () => { it( 'should retrieve multiple parameters by name', async () => { - const logs = invocationLogs[0].getFunctionLogs(); - const testLog = InvocationLogs.parseFunctionLog(logs[5]); + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[5]); expect(testLog).toStrictEqual({ test: 'get-multiple-by-name', @@ -325,8 +312,8 @@ describe(`parameters E2E tests (ssmProvider) for runtime: ${runtime}`, () => { it( 'should retrieve multiple parameters by name with mixed decryption', async () => { - const logs = invocationLogs[0].getFunctionLogs(); - const testLog = InvocationLogs.parseFunctionLog(logs[6]); + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[6]); expect(testLog).toStrictEqual({ test: 'get-multiple-by-name-mixed-decrypt', @@ -345,8 +332,8 @@ describe(`parameters E2E tests (ssmProvider) for runtime: ${runtime}`, () => { it( 'should retrieve single parameter cached', async () => { - const logs = invocationLogs[0].getFunctionLogs(); - const testLog = InvocationLogs.parseFunctionLog(logs[7]); + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[7]); expect(testLog).toStrictEqual({ test: 'get-cached', @@ -361,8 +348,8 @@ describe(`parameters E2E tests (ssmProvider) for runtime: ${runtime}`, () => { it( 'should retrieve single parameter twice without caching', async () => { - const logs = invocationLogs[0].getFunctionLogs(); - const testLog = InvocationLogs.parseFunctionLog(logs[8]); + const logs = invocationLogs.getFunctionLogs(); + const testLog = TestInvocationLogs.parseFunctionLog(logs[8]); expect(testLog).toStrictEqual({ test: 'get-forced', diff --git a/packages/parameters/tests/helpers/parametersUtils.ts b/packages/parameters/tests/helpers/parametersUtils.ts index f351773f5b..14d7a14c64 100644 --- a/packages/parameters/tests/helpers/parametersUtils.ts +++ b/packages/parameters/tests/helpers/parametersUtils.ts @@ -175,6 +175,7 @@ const createSSMSecureString = ( policy: AwsCustomResourcePolicy.fromSdkCalls({ resources: AwsCustomResourcePolicy.ANY_RESOURCE, }), + installLatestAwsSdk: false, }); const param = StringParameter.fromSecureStringParameterAttributes(stack, id, { diff --git a/packages/commons/tests/utils/InvocationLogs.ts b/packages/testing/src/TestInvocationLogs.ts similarity index 85% rename from packages/commons/tests/utils/InvocationLogs.ts rename to packages/testing/src/TestInvocationLogs.ts index 00d3cc03ce..fa396c98eb 100644 --- a/packages/commons/tests/utils/InvocationLogs.ts +++ b/packages/testing/src/TestInvocationLogs.ts @@ -1,26 +1,26 @@ /** * Log level. used for filtering the log */ -export enum LEVEL { - DEBUG = 'DEBUG', - INFO = 'INFO', - WARN = 'WARN', - ERROR = 'ERROR', -} +const Level = { + DEBUG: 'DEBUG', + INFO: 'INFO', + WARN: 'WARN', + ERROR: 'ERROR', +} as const; -export type ErrorField = { +type ErrorField = { name: string; message: string; stack: string; }; -export type FunctionLog = { - level: LEVEL; +type FunctionLog = { + level: keyof typeof Level; error: ErrorField; } & { [key: string]: unknown }; -export class InvocationLogs { - public static LEVEL = LEVEL; +class TestInvocationLogs { + public static LEVEL = Level; /** * Array of logs from invocation. @@ -51,7 +51,7 @@ export class InvocationLogs { */ public doesAnyFunctionLogsContains( text: string, - levelToFilter?: LEVEL + levelToFilter?: keyof typeof Level ): boolean { const filteredLogs = this.getFunctionLogs(levelToFilter).filter((log) => log.includes(text) @@ -81,23 +81,23 @@ export class InvocationLogs { * Return only logs from function, exclude START, END, REPORT, * and X-Ray log generated by the Lambda service. * - * @param {LEVEL} [levelToFilter] - Level to filter the logs + * @param {typeof Level} [levelToFilter] - Level to filter the logs * @returns Array of function logs, filtered by level if provided */ - public getFunctionLogs(levelToFilter?: LEVEL): string[] { - const startLogIndex = InvocationLogs.getStartLogIndex(this.logs); - const endLogIndex = InvocationLogs.getEndLogIndex(this.logs); + public getFunctionLogs(levelToFilter?: keyof typeof Level): string[] { + const startLogIndex = TestInvocationLogs.getStartLogIndex(this.logs); + const endLogIndex = TestInvocationLogs.getEndLogIndex(this.logs); let filteredLogs = this.logs.slice(startLogIndex + 1, endLogIndex); if (levelToFilter) { filteredLogs = filteredLogs.filter((log) => { try { - const parsedLog = InvocationLogs.parseFunctionLog(log); + const parsedLog = TestInvocationLogs.parseFunctionLog(log); return parsedLog.level == levelToFilter; } catch (error) { // If log is not from structured logging : such as metrics one. - return log.split('\t')[2] == levelToFilter; + return (log.split('\t')[2] as keyof typeof Level) === levelToFilter; } }); } @@ -117,3 +117,5 @@ export class InvocationLogs { return JSON.parse(log); } } + +export { TestInvocationLogs }; diff --git a/packages/testing/src/helpers.ts b/packages/testing/src/helpers.ts new file mode 100644 index 0000000000..8e8e5eac21 --- /dev/null +++ b/packages/testing/src/helpers.ts @@ -0,0 +1,53 @@ +import { randomUUID } from 'node:crypto'; +import { TEST_RUNTIMES } from './constants'; + +const isValidRuntimeKey = ( + runtime: string +): runtime is keyof typeof TEST_RUNTIMES => runtime in TEST_RUNTIMES; + +/** + * Generate a unique name for a test. + * + * The maximum length of the name is 45 characters. + * + * @example + * ```ts + * const testPrefix = 'E2E-TRACER'; + * const runtime = 'nodejs18x'; + * const testName = 'someFeature'; + * const uniqueName = generateTestUniqueName({ testPrefix, runtime, testName }); + * // uniqueName = 'E2E-TRACER-node18-12345-someFeature' + * ``` + */ +const generateTestUniqueName = ({ + testPrefix, + runtime, + testName, +}: { + testPrefix: string; + runtime: string; + testName: string; +}): string => + [ + testPrefix, + runtime.replace(/[jsx]/g, ''), + randomUUID().toString().substring(0, 5), + testName, + ] + .join('-') + .substring(0, 45); + +/** + * Given a test name and a resource name, generate a unique name for the resource. + * + * The maximum length of the name is 64 characters. + */ +const concatenateResourceName = ({ + testName, + resourceName, +}: { + testName: string; + resourceName: string; +}): string => `${testName}-${resourceName}`.substring(0, 64); + +export { isValidRuntimeKey, generateTestUniqueName, concatenateResourceName }; diff --git a/packages/testing/src/index.ts b/packages/testing/src/index.ts index 6ba0fd1d5a..7c8276c774 100644 --- a/packages/testing/src/index.ts +++ b/packages/testing/src/index.ts @@ -1,2 +1,6 @@ export * from './TestStack'; export * from './constants'; +export * from './helpers'; +export * from './resources'; +export * from './invokeTestFunction'; +export * from './TestInvocationLogs'; diff --git a/packages/testing/src/invokeTestFunction.ts b/packages/testing/src/invokeTestFunction.ts new file mode 100644 index 0000000000..46a54d9553 --- /dev/null +++ b/packages/testing/src/invokeTestFunction.ts @@ -0,0 +1,94 @@ +import { InvokeCommand, LambdaClient } from '@aws-sdk/client-lambda'; +import { fromUtf8 } from '@aws-sdk/util-utf8-node'; +import { TestInvocationLogs } from './TestInvocationLogs'; + +type InvokeTestFunctionOptions = { + functionName: string; + times?: number; + invocationMode?: 'PARALLEL' | 'SEQUENTIAL'; + payload?: Record | Array>; +}; + +const lambdaClient = new LambdaClient({}); + +/** + * Invoke a Lambda function once and return the logs + */ +const invokeFunctionOnce = async ({ + functionName, + payload = {}, +}: Omit< + InvokeTestFunctionOptions, + 'times' | 'invocationMode' +>): Promise => { + const result = await lambdaClient.send( + new InvokeCommand({ + FunctionName: functionName, + InvocationType: 'RequestResponse', + LogType: 'Tail', // Wait until execution completes and return all logs + Payload: fromUtf8(JSON.stringify(payload)), + }) + ); + + if (result?.LogResult) { + return new TestInvocationLogs(result?.LogResult); + } else { + throw new Error( + 'No LogResult field returned in the response of Lambda invocation. This should not happen.' + ); + } +}; + +/** + * Invoke a Lambda function multiple times and return the logs + * + * When specifying a payload, you can either pass a single object that will be used for all invocations, + * or an array of objects that will be used for each invocation. If you pass an array, the length of the + * array must be the same as the times parameter. + */ +const invokeFunction = async ({ + functionName, + times = 1, + invocationMode = 'PARALLEL', + payload = {}, +}: InvokeTestFunctionOptions): Promise => { + const invocationLogs: TestInvocationLogs[] = []; + + if (payload && Array.isArray(payload) && payload.length !== times) { + throw new Error( + `The payload array must have the same length as the times parameter.` + ); + } + + if (invocationMode == 'PARALLEL') { + const invocationPromises = Array.from( + { length: times }, + () => invokeFunctionOnce + ); + + invocationLogs.push( + ...(await Promise.all( + invocationPromises.map((invoke, index) => { + const invocationPayload = Array.isArray(payload) + ? payload[index] + : payload; + + return invoke({ functionName, payload: invocationPayload }); + }) + )) + ); + } else { + for (let index = 0; index < times; index++) { + const invocationPayload = Array.isArray(payload) + ? payload[index] + : payload; + invocationLogs.push( + await invokeFunctionOnce({ functionName, payload: invocationPayload }) + ); + } + } + + return invocationLogs; +}; + +export { invokeFunctionOnce, invokeFunction }; diff --git a/packages/testing/src/resources/TestNodejsFunction.ts b/packages/testing/src/resources/TestNodejsFunction.ts new file mode 100644 index 0000000000..006689c442 --- /dev/null +++ b/packages/testing/src/resources/TestNodejsFunction.ts @@ -0,0 +1,40 @@ +import { CfnOutput, Duration } from 'aws-cdk-lib'; +import { Tracing } from 'aws-cdk-lib/aws-lambda'; +import type { NodejsFunctionProps } from 'aws-cdk-lib/aws-lambda-nodejs'; +import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; +import { RetentionDays } from 'aws-cdk-lib/aws-logs'; +import type { Construct } from 'constructs'; + +interface ExtraTestProps { + logGroupOutputKey?: string; +} + +/** + * A NodejsFunction that can be used in tests. + * + * It includes some default props and can optionally output the log group name. + */ +class TestNodejsFunction extends NodejsFunction { + public constructor( + scope: Construct, + id: string, + props: NodejsFunctionProps, + extraProps: ExtraTestProps = {} + ) { + super(scope, id, { + timeout: Duration.seconds(30), + memorySize: 256, + tracing: Tracing.ACTIVE, + ...props, + logRetention: RetentionDays.ONE_DAY, + }); + + if (extraProps.logGroupOutputKey) { + new CfnOutput(this, extraProps.logGroupOutputKey, { + value: this.logGroup.logGroupName, + }); + } + } +} + +export { TestNodejsFunction }; diff --git a/packages/testing/src/resources/index.ts b/packages/testing/src/resources/index.ts new file mode 100644 index 0000000000..d7ad6e35c9 --- /dev/null +++ b/packages/testing/src/resources/index.ts @@ -0,0 +1 @@ +export * from './TestNodejsFunction'; diff --git a/packages/tracer/tests/e2e/allFeatures.decorator.test.ts b/packages/tracer/tests/e2e/allFeatures.decorator.test.ts index a6f9c0911d..b1ba14fa2f 100644 --- a/packages/tracer/tests/e2e/allFeatures.decorator.test.ts +++ b/packages/tracer/tests/e2e/allFeatures.decorator.test.ts @@ -3,18 +3,25 @@ * * @group e2e/tracer/decorator */ -import path from 'path'; import { - TestStack, + concatenateResourceName, defaultRuntime, + generateTestUniqueName, + isValidRuntimeKey, + TestNodejsFunction, + TestStack, + TEST_RUNTIMES, } from '@aws-lambda-powertools/testing-utils'; -import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb'; -import { RemovalPolicy } from 'aws-cdk-lib'; -import { XRayClient } from '@aws-sdk/client-xray'; import { STSClient } from '@aws-sdk/client-sts'; -import { v4 } from 'uuid'; +import { XRayClient } from '@aws-sdk/client-xray'; +import { RemovalPolicy } from 'aws-cdk-lib'; +import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb'; +import { join } from 'node:path'; +import { + assertAnnotation, + assertErrorAndFault, +} from '../helpers/traceAssertions'; import { - createTracerTestFunction, getFirstSubsegment, getFunctionArn, getInvocationSubsegment, @@ -23,10 +30,7 @@ import { splitSegmentsByName, } from '../helpers/tracesUtils'; import { - generateUniqueName, - isValidRuntimeKey, -} from '../../../commons/tests/utils/e2eUtils'; -import { + commonEnvironmentVariables, expectedCustomAnnotationKey, expectedCustomAnnotationValue, expectedCustomErrorMessage, @@ -38,95 +42,83 @@ import { TEARDOWN_TIMEOUT, TEST_CASE_TIMEOUT, } from './constants'; -import { - assertAnnotation, - assertErrorAndFault, -} from '../helpers/traceAssertions'; - -const runtime: string = process.env.RUNTIME || defaultRuntime; - -if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key value: ${runtime}`); -} /** - * We will create a stack with 3 Lambda functions: + * The test includes one stack with 4 Lambda functions that correspond to the following test cases: * 1. With all flags enabled (capture both response and error) * 2. Do not capture error or response * 3. Do not enable tracer + * 4. Disable capture response via decorator options * Each stack must use a unique `serviceName` as it's used to for retrieving the trace. * Using the same one will result in traces from different test cases mixing up. */ -const stackName = generateUniqueName( - RESOURCE_NAME_PREFIX, - v4(), - runtime, - 'AllFeatures-Decorator' -); -const lambdaFunctionCodeFile = 'allFeatures.decorator.test.functionCode.ts'; -let startTime: Date; - -/** - * Function #1 is with all flags enabled. - */ -const uuidFunction1 = v4(); -const functionNameWithAllFlagsEnabled = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuidFunction1, - runtime, - 'AllFeatures-Decorator-AllFlagsEnabled' -); -const serviceNameWithAllFlagsEnabled = functionNameWithAllFlagsEnabled; - -/** - * Function #2 doesn't capture error or response - */ -const uuidFunction2 = v4(); -const functionNameWithNoCaptureErrorOrResponse = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuidFunction2, - runtime, - 'AllFeatures-Decorator-NoCaptureErrorOrResponse' -); -const serviceNameWithNoCaptureErrorOrResponse = - functionNameWithNoCaptureErrorOrResponse; -/** - * Function #3 disables tracer - */ -const uuidFunction3 = v4(); -const functionNameWithTracerDisabled = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuidFunction3, - runtime, - 'AllFeatures-Decorator-TracerDisabled' -); -const serviceNameWithTracerDisabled = functionNameWithNoCaptureErrorOrResponse; - -/** - * Function #4 disables capture response via decorator options - */ -const uuidFunction4 = v4(); -const functionNameWithCaptureResponseFalse = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuidFunction4, - runtime, - 'AllFeatures-Decorator-CaptureResponseFalse' -); -const serviceNameWithCaptureResponseFalse = - functionNameWithCaptureResponseFalse; - -const xrayClient = new XRayClient({}); -const stsClient = new STSClient({}); -const invocations = 3; - -const testStack = new TestStack(stackName); +describe(`Tracer E2E tests, all features with decorator instantiation`, () => { + const runtime: string = process.env.RUNTIME || defaultRuntime; + + if (!isValidRuntimeKey(runtime)) { + throw new Error(`Invalid runtime key value: ${runtime}`); + } + + const testName = generateTestUniqueName({ + testPrefix: RESOURCE_NAME_PREFIX, + runtime, + testName: 'AllFeatures-Decorator', + }); + const testStack = new TestStack(testName); + + // Location of the lambda function code + const lambdaFunctionCodeFile = join( + __dirname, + 'allFeatures.decorator.test.functionCode.ts' + ); + const startTime = new Date(); + + /** + * Function #1 is with all flags enabled. + */ + const fnNameAllFlagsEnabled = concatenateResourceName({ + testName, + resourceName: 'AllFlagsOn', + }); + + /** + * Function #2 doesn't capture error or response + */ + const fnNameNoCaptureErrorOrResponse = concatenateResourceName({ + testName, + resourceName: 'NoCaptureErrOrResp', + }); + + /** + * Function #3 disables tracer + */ + const fnNameTracerDisabled = concatenateResourceName({ + testName, + resourceName: 'TracerDisabled', + }); + + /** + * Function #4 disables capture response via decorator options + */ + const fnNameCaptureResponseOff = concatenateResourceName({ + testName, + resourceName: 'CaptureRespOff', + }); + + /** + * Table used by all functions to make an SDK call + */ + const ddbTableName = concatenateResourceName({ + testName, + resourceName: 'TestTable', + }); + + const xrayClient = new XRayClient({}); + const stsClient = new STSClient({}); + const invocations = 3; -describe(`Tracer E2E tests, all features with decorator instantiation for runtime: ${runtime}`, () => { beforeAll(async () => { // Prepare - startTime = new Date(); - const ddbTableName = stackName + '-table'; - const ddbTable = new Table(testStack.stack, 'Table', { tableName: ddbTableName, partitionKey: { @@ -137,77 +129,91 @@ describe(`Tracer E2E tests, all features with decorator instantiation for runtim removalPolicy: RemovalPolicy.DESTROY, }); - const entry = path.join(__dirname, lambdaFunctionCodeFile); - const functionWithAllFlagsEnabled = createTracerTestFunction({ - stack: testStack.stack, - functionName: functionNameWithAllFlagsEnabled, - entry, - expectedServiceName: serviceNameWithAllFlagsEnabled, - environmentParams: { - TEST_TABLE_NAME: ddbTableName, - POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', - POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', - POWERTOOLS_TRACE_ENABLED: 'true', - }, - runtime, - }); - ddbTable.grantWriteData(functionWithAllFlagsEnabled); - - const functionThatDoesNotCapturesErrorAndResponse = - createTracerTestFunction({ - stack: testStack.stack, - functionName: functionNameWithNoCaptureErrorOrResponse, - entry, - expectedServiceName: serviceNameWithNoCaptureErrorOrResponse, - environmentParams: { + const fnWithAllFlagsEnabled = new TestNodejsFunction( + testStack.stack, + fnNameAllFlagsEnabled, + { + functionName: fnNameAllFlagsEnabled, + entry: lambdaFunctionCodeFile, + runtime: TEST_RUNTIMES[runtime], + environment: { + TEST_TABLE_NAME: ddbTableName, + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', + POWERTOOLS_TRACE_ENABLED: 'true', + EXPECTED_SERVICE_NAME: fnNameAllFlagsEnabled, + ...commonEnvironmentVariables, + }, + } + ); + ddbTable.grantWriteData(fnWithAllFlagsEnabled); + + const fnThatDoesNotCapturesErrorAndResponse = new TestNodejsFunction( + testStack.stack, + fnNameNoCaptureErrorOrResponse, + { + functionName: fnNameNoCaptureErrorOrResponse, + entry: lambdaFunctionCodeFile, + runtime: TEST_RUNTIMES[runtime], + environment: { TEST_TABLE_NAME: ddbTableName, POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'false', POWERTOOLS_TRACER_CAPTURE_ERROR: 'false', POWERTOOLS_TRACE_ENABLED: 'true', + EXPECTED_SERVICE_NAME: fnNameNoCaptureErrorOrResponse, + ...commonEnvironmentVariables, }, - runtime, - }); - ddbTable.grantWriteData(functionThatDoesNotCapturesErrorAndResponse); - - const functionWithTracerDisabled = createTracerTestFunction({ - stack: testStack.stack, - functionName: functionNameWithTracerDisabled, - entry, - expectedServiceName: serviceNameWithTracerDisabled, - environmentParams: { - TEST_TABLE_NAME: ddbTableName, - POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', - POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', - POWERTOOLS_TRACE_ENABLED: 'false', - }, - runtime, - }); - ddbTable.grantWriteData(functionWithTracerDisabled); - - const functionWithCaptureResponseFalse = createTracerTestFunction({ - stack: testStack.stack, - functionName: functionNameWithCaptureResponseFalse, - handler: 'handlerWithCaptureResponseFalse', - entry, - expectedServiceName: serviceNameWithCaptureResponseFalse, - environmentParams: { - TEST_TABLE_NAME: ddbTableName, - POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', - POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', - POWERTOOLS_TRACE_ENABLED: 'true', - }, - runtime, - }); - ddbTable.grantWriteData(functionWithCaptureResponseFalse); + } + ); + ddbTable.grantWriteData(fnThatDoesNotCapturesErrorAndResponse); + + const fnWithTracerDisabled = new TestNodejsFunction( + testStack.stack, + fnNameTracerDisabled, + { + functionName: fnNameTracerDisabled, + entry: lambdaFunctionCodeFile, + runtime: TEST_RUNTIMES[runtime], + environment: { + TEST_TABLE_NAME: ddbTableName, + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', + POWERTOOLS_TRACE_ENABLED: 'false', + EXPECTED_SERVICE_NAME: fnNameTracerDisabled, + ...commonEnvironmentVariables, + }, + } + ); + ddbTable.grantWriteData(fnWithTracerDisabled); + + const fnWithCaptureResponseFalse = new TestNodejsFunction( + testStack.stack, + fnNameCaptureResponseOff, + { + functionName: fnNameCaptureResponseOff, + handler: 'handlerWithCaptureResponseFalse', + entry: lambdaFunctionCodeFile, + runtime: TEST_RUNTIMES[runtime], + environment: { + TEST_TABLE_NAME: ddbTableName, + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', + POWERTOOLS_TRACE_ENABLED: 'true', + EXPECTED_SERVICE_NAME: fnNameCaptureResponseOff, + ...commonEnvironmentVariables, + }, + } + ); + ddbTable.grantWriteData(fnWithCaptureResponseFalse); await testStack.deploy(); // Act await Promise.all([ - invokeAllTestCases(functionNameWithAllFlagsEnabled), - invokeAllTestCases(functionNameWithNoCaptureErrorOrResponse), - invokeAllTestCases(functionNameWithTracerDisabled), - invokeAllTestCases(functionNameWithCaptureResponseFalse), + invokeAllTestCases(fnNameAllFlagsEnabled), + invokeAllTestCases(fnNameNoCaptureErrorOrResponse), + invokeAllTestCases(fnNameTracerDisabled), + invokeAllTestCases(fnNameCaptureResponseOff), ]); }, SETUP_TIMEOUT); @@ -223,7 +229,7 @@ describe(`Tracer E2E tests, all features with decorator instantiation for runtim const tracesWhenAllFlagsEnabled = await getTraces( xrayClient, startTime, - await getFunctionArn(stsClient, functionNameWithAllFlagsEnabled), + await getFunctionArn(stsClient, fnNameAllFlagsEnabled), invocations, 4 ); @@ -283,7 +289,7 @@ describe(`Tracer E2E tests, all features with decorator instantiation for runtim const tracesWhenAllFlagsEnabled = await getTraces( xrayClient, startTime, - await getFunctionArn(stsClient, functionNameWithAllFlagsEnabled), + await getFunctionArn(stsClient, fnNameAllFlagsEnabled), invocations, 4 ); @@ -298,7 +304,7 @@ describe(`Tracer E2E tests, all features with decorator instantiation for runtim assertAnnotation({ annotations, isColdStart, - expectedServiceName: serviceNameWithAllFlagsEnabled, + expectedServiceName: fnNameAllFlagsEnabled, expectedCustomAnnotationKey, expectedCustomAnnotationValue, }); @@ -307,14 +313,14 @@ describe(`Tracer E2E tests, all features with decorator instantiation for runtim fail('metadata is missing'); } expect( - metadata[serviceNameWithAllFlagsEnabled][expectedCustomMetadataKey] + metadata[fnNameAllFlagsEnabled][expectedCustomMetadataKey] ).toEqual(expectedCustomMetadataValue); const shouldThrowAnError = i === invocations - 1; if (!shouldThrowAnError) { // Assert that the metadata object contains the response expect( - metadata[serviceNameWithAllFlagsEnabled]['index.handler response'] + metadata[fnNameAllFlagsEnabled]['index.handler response'] ).toEqual(expectedCustomResponseValue); } } @@ -328,10 +334,7 @@ describe(`Tracer E2E tests, all features with decorator instantiation for runtim const tracesWithNoCaptureErrorOrResponse = await getTraces( xrayClient, startTime, - await getFunctionArn( - stsClient, - functionNameWithNoCaptureErrorOrResponse - ), + await getFunctionArn(stsClient, fnNameNoCaptureErrorOrResponse), invocations, 4 ); @@ -395,7 +398,7 @@ describe(`Tracer E2E tests, all features with decorator instantiation for runtim const tracesWithCaptureResponseFalse = await getTraces( xrayClient, startTime, - await getFunctionArn(stsClient, functionNameWithCaptureResponseFalse), + await getFunctionArn(stsClient, fnNameCaptureResponseOff), invocations, 4 ); @@ -466,7 +469,7 @@ describe(`Tracer E2E tests, all features with decorator instantiation for runtim const tracesWithTracerDisabled = await getTraces( xrayClient, startTime, - await getFunctionArn(stsClient, functionNameWithTracerDisabled), + await getFunctionArn(stsClient, fnNameTracerDisabled), invocations, expectedNoOfTraces ); diff --git a/packages/tracer/tests/e2e/allFeatures.manual.test.ts b/packages/tracer/tests/e2e/allFeatures.manual.test.ts index 9d8cbb7bed..e5599b5dc8 100644 --- a/packages/tracer/tests/e2e/allFeatures.manual.test.ts +++ b/packages/tracer/tests/e2e/allFeatures.manual.test.ts @@ -3,18 +3,25 @@ * * @group e2e/tracer/manual */ -import path from 'path'; -import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb'; -import { RemovalPolicy } from 'aws-cdk-lib'; -import { XRayClient } from '@aws-sdk/client-xray'; -import { STSClient } from '@aws-sdk/client-sts'; -import { v4 } from 'uuid'; import { - TestStack, + concatenateResourceName, defaultRuntime, + generateTestUniqueName, + isValidRuntimeKey, + TestNodejsFunction, + TestStack, + TEST_RUNTIMES, } from '@aws-lambda-powertools/testing-utils'; +import { STSClient } from '@aws-sdk/client-sts'; +import { XRayClient } from '@aws-sdk/client-xray'; +import { RemovalPolicy } from 'aws-cdk-lib'; +import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb'; +import { join } from 'path'; +import { + assertAnnotation, + assertErrorAndFault, +} from '../helpers/traceAssertions'; import { - createTracerTestFunction, getFirstSubsegment, getFunctionArn, getInvocationSubsegment, @@ -24,10 +31,7 @@ import { } from '../helpers/tracesUtils'; import type { ParsedTrace } from '../helpers/traceUtils.types'; import { - generateUniqueName, - isValidRuntimeKey, -} from '../../../commons/tests/utils/e2eUtils'; -import { + commonEnvironmentVariables, expectedCustomAnnotationKey, expectedCustomAnnotationValue, expectedCustomErrorMessage, @@ -39,62 +43,48 @@ import { TEARDOWN_TIMEOUT, TEST_CASE_TIMEOUT, } from './constants'; -import { - assertAnnotation, - assertErrorAndFault, -} from '../helpers/traceAssertions'; -const runtime: string = process.env.RUNTIME || defaultRuntime; +describe(`Tracer E2E tests, all features with manual instantiation`, () => { + const runtime: string = process.env.RUNTIME || defaultRuntime; -if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key value: ${runtime}`); -} + if (!isValidRuntimeKey(runtime)) { + throw new Error(`Invalid runtime key value: ${runtime}`); + } -const uuid = v4(); -const stackName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'AllFeatures-Manual' -); -const functionName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'AllFeatures-Manual' -); -const lambdaFunctionCodeFile = 'allFeatures.manual.test.functionCode.ts'; -const expectedServiceName = functionName; + const testName = generateTestUniqueName({ + testPrefix: RESOURCE_NAME_PREFIX, + runtime, + testName: 'AllFeatures-Decorator', + }); + const testStack = new TestStack(testName); -const xrayClient = new XRayClient({}); -const stsClient = new STSClient({}); -const invocations = 3; -let sortedTraces: ParsedTrace[]; - -const testStack = new TestStack(stackName); + // Location of the lambda function code + const lambdaFunctionCodeFile = join( + __dirname, + 'allFeatures.manual.test.functionCode.ts' + ); + const startTime = new Date(); + + const fnNameAllFlagsEnabled = concatenateResourceName({ + testName, + resourceName: 'AllFlagsOn', + }); + + /** + * Table used by all functions to make an SDK call + */ + const ddbTableName = concatenateResourceName({ + testName, + resourceName: 'TestTable', + }); + + const xrayClient = new XRayClient({}); + const stsClient = new STSClient({}); + const invocations = 3; + let sortedTraces: ParsedTrace[]; -describe(`Tracer E2E tests, all features with manual instantiation for runtime: ${runtime}`, () => { beforeAll(async () => { // Prepare - const startTime = new Date(); - const ddbTableName = stackName + '-table'; - - const entry = path.join(__dirname, lambdaFunctionCodeFile); - const environmentParams = { - TEST_TABLE_NAME: ddbTableName, - POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', - POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', - POWERTOOLS_TRACE_ENABLED: 'true', - }; - const testFunction = createTracerTestFunction({ - stack: testStack.stack, - functionName, - entry, - expectedServiceName, - environmentParams, - runtime, - }); - const ddbTable = new Table(testStack.stack, 'Table', { tableName: ddbTableName, partitionKey: { @@ -105,15 +95,35 @@ describe(`Tracer E2E tests, all features with manual instantiation for runtime: removalPolicy: RemovalPolicy.DESTROY, }); - ddbTable.grantWriteData(testFunction); + const fnWithAllFlagsEnabled = new TestNodejsFunction( + testStack.stack, + fnNameAllFlagsEnabled, + { + functionName: fnNameAllFlagsEnabled, + entry: lambdaFunctionCodeFile, + runtime: TEST_RUNTIMES[runtime], + environment: { + TEST_TABLE_NAME: ddbTableName, + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', + POWERTOOLS_TRACE_ENABLED: 'true', + EXPECTED_SERVICE_NAME: fnNameAllFlagsEnabled, + ...commonEnvironmentVariables, + }, + } + ); + ddbTable.grantWriteData(fnWithAllFlagsEnabled); await testStack.deploy(); // Act - await invokeAllTestCases(functionName); + await invokeAllTestCases(fnNameAllFlagsEnabled); // Retrieve traces from X-Ray for assertion - const lambdaFunctionArn = await getFunctionArn(stsClient, functionName); + const lambdaFunctionArn = await getFunctionArn( + stsClient, + fnNameAllFlagsEnabled + ); sortedTraces = await getTraces( xrayClient, startTime, @@ -191,7 +201,7 @@ describe(`Tracer E2E tests, all features with manual instantiation for runtime: assertAnnotation({ annotations, isColdStart, - expectedServiceName, + expectedServiceName: fnNameAllFlagsEnabled, expectedCustomAnnotationKey, expectedCustomAnnotationValue, }); @@ -200,14 +210,14 @@ describe(`Tracer E2E tests, all features with manual instantiation for runtime: fail('metadata is missing'); } expect( - metadata[expectedServiceName][expectedCustomMetadataKey] + metadata[fnNameAllFlagsEnabled][expectedCustomMetadataKey] ).toEqual(expectedCustomMetadataValue); const shouldThrowAnError = i === invocations - 1; if (!shouldThrowAnError) { // Assert that the metadata object contains the response expect( - metadata[expectedServiceName]['index.handler response'] + metadata[fnNameAllFlagsEnabled]['index.handler response'] ).toEqual(expectedCustomResponseValue); } } diff --git a/packages/tracer/tests/e2e/allFeatures.middy.test.ts b/packages/tracer/tests/e2e/allFeatures.middy.test.ts index 7066a549db..f63326f107 100644 --- a/packages/tracer/tests/e2e/allFeatures.middy.test.ts +++ b/packages/tracer/tests/e2e/allFeatures.middy.test.ts @@ -3,19 +3,21 @@ * * @group e2e/tracer/middy */ - -import path from 'path'; +import { join } from 'node:path'; +import { + concatenateResourceName, + defaultRuntime, + generateTestUniqueName, + isValidRuntimeKey, + TestNodejsFunction, + TestStack, + TEST_RUNTIMES, +} from '@aws-lambda-powertools/testing-utils'; import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb'; import { RemovalPolicy } from 'aws-cdk-lib'; import { XRayClient } from '@aws-sdk/client-xray'; import { STSClient } from '@aws-sdk/client-sts'; -import { v4 } from 'uuid'; -import { - TestStack, - defaultRuntime, -} from '@aws-lambda-powertools/testing-utils'; import { - createTracerTestFunction, getFirstSubsegment, getFunctionArn, getInvocationSubsegment, @@ -24,10 +26,7 @@ import { splitSegmentsByName, } from '../helpers/tracesUtils'; import { - generateUniqueName, - isValidRuntimeKey, -} from '../../../commons/tests/utils/e2eUtils'; -import { + commonEnvironmentVariables, expectedCustomAnnotationKey, expectedCustomAnnotationValue, expectedCustomErrorMessage, @@ -44,90 +43,82 @@ import { assertErrorAndFault, } from '../helpers/traceAssertions'; -const runtime: string = process.env.RUNTIME || defaultRuntime; - -if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key value: ${runtime}`); -} - /** - * We will create a stack with 3 Lambda functions: + * The test includes one stack with 4 Lambda functions that correspond to the following test cases: * 1. With all flags enabled (capture both response and error) * 2. Do not capture error or response * 3. Do not enable tracer + * 4. Disable response capture via middleware option * Each stack must use a unique `serviceName` as it's used to for retrieving the trace. * Using the same one will result in traces from different test cases mixing up. */ -const stackName = generateUniqueName( - RESOURCE_NAME_PREFIX, - v4(), - runtime, - 'AllFeatures-Middy' -); -const lambdaFunctionCodeFile = 'allFeatures.middy.test.functionCode.ts'; -let startTime: Date; - -/** - * Function #1 is with all flags enabled. - */ -const uuidFunction1 = v4(); -const functionNameWithAllFlagsEnabled = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuidFunction1, - runtime, - 'AllFeatures-Middy-AllFlagsEnabled' -); -const serviceNameWithAllFlagsEnabled = functionNameWithAllFlagsEnabled; - -/** - * Function #2 doesn't capture error or response - */ -const uuidFunction2 = v4(); -const functionNameWithNoCaptureErrorOrResponse = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuidFunction2, - runtime, - 'AllFeatures-Middy-NoCaptureErrorOrResponse' -); -const serviceNameWithNoCaptureErrorOrResponse = - functionNameWithNoCaptureErrorOrResponse; -/** - * Function #3 disables tracer - */ -const uuidFunction3 = v4(); -const functionNameWithTracerDisabled = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuidFunction3, - runtime, - 'AllFeatures-Middy-TracerDisabled' -); -const serviceNameWithTracerDisabled = functionNameWithNoCaptureErrorOrResponse; - -/** - * Function #4 doesn't capture response - */ -const uuidFunction4 = v4(); -const functionNameWithNoCaptureResponseViaMiddlewareOption = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuidFunction4, - runtime, - 'AllFeatures-Middy-NoCaptureResponse2' -); -const serviceNameWithNoCaptureResponseViaMiddlewareOption = - functionNameWithNoCaptureResponseViaMiddlewareOption; - -const xrayClient = new XRayClient({}); -const stsClient = new STSClient({}); -const invocations = 3; - -const testStack = new TestStack(stackName); +describe(`Tracer E2E tests, all features with middy instantiation`, () => { + const runtime: string = process.env.RUNTIME || defaultRuntime; + + if (!isValidRuntimeKey(runtime)) { + throw new Error(`Invalid runtime key value: ${runtime}`); + } + + const testName = generateTestUniqueName({ + testPrefix: RESOURCE_NAME_PREFIX, + runtime, + testName: 'AllFeatures-Middy', + }); + const testStack = new TestStack(testName); + + // Location of the lambda function code + const lambdaFunctionCodeFile = join( + __dirname, + 'allFeatures.middy.test.functionCode.ts' + ); + const startTime = new Date(); + + /** + * Function #1 is with all flags enabled. + */ + const fnNameAllFlagsEnabled = concatenateResourceName({ + testName, + resourceName: 'AllFlagsOn', + }); + + /** + * Function #2 doesn't capture error or response + */ + const fnNameNoCaptureErrorOrResponse = concatenateResourceName({ + testName, + resourceName: 'NoCaptureErrOrResp', + }); + + /** + * Function #3 disables tracer + */ + const fnNameTracerDisabled = concatenateResourceName({ + testName, + resourceName: 'TracerDisabled', + }); + + /** + * Function #4 disables response capture via middleware option + */ + const fnNameCaptureResponseOff = concatenateResourceName({ + testName, + resourceName: 'CaptureResponseOff', + }); + + /** + * Table used by all functions to make an SDK call + */ + const ddbTableName = concatenateResourceName({ + testName, + resourceName: 'TestTable', + }); + + const xrayClient = new XRayClient({}); + const stsClient = new STSClient({}); + const invocations = 3; -describe(`Tracer E2E tests, all features with middy instantiation for runtime: ${runtime}`, () => { beforeAll(async () => { // Prepare - startTime = new Date(); - const ddbTableName = stackName + '-table'; - const ddbTable = new Table(testStack.stack, 'Table', { tableName: ddbTableName, partitionKey: { @@ -138,81 +129,91 @@ describe(`Tracer E2E tests, all features with middy instantiation for runtime: $ removalPolicy: RemovalPolicy.DESTROY, }); - const entry = path.join(__dirname, lambdaFunctionCodeFile); - const functionWithAllFlagsEnabled = createTracerTestFunction({ - stack: testStack.stack, - functionName: functionNameWithAllFlagsEnabled, - entry, - expectedServiceName: serviceNameWithAllFlagsEnabled, - environmentParams: { - TEST_TABLE_NAME: ddbTableName, - POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', - POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', - POWERTOOLS_TRACE_ENABLED: 'true', - }, - runtime, - }); - ddbTable.grantWriteData(functionWithAllFlagsEnabled); - - const functionThatDoesNotCapturesErrorAndResponse = - createTracerTestFunction({ - stack: testStack.stack, - functionName: functionNameWithNoCaptureErrorOrResponse, - entry, - expectedServiceName: serviceNameWithNoCaptureErrorOrResponse, - environmentParams: { + const fnWithAllFlagsEnabled = new TestNodejsFunction( + testStack.stack, + fnNameAllFlagsEnabled, + { + functionName: fnNameAllFlagsEnabled, + entry: lambdaFunctionCodeFile, + runtime: TEST_RUNTIMES[runtime], + environment: { + TEST_TABLE_NAME: ddbTableName, + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', + POWERTOOLS_TRACE_ENABLED: 'true', + EXPECTED_SERVICE_NAME: fnNameAllFlagsEnabled, + ...commonEnvironmentVariables, + }, + } + ); + ddbTable.grantWriteData(fnWithAllFlagsEnabled); + + const fnThatDoesNotCapturesErrorAndResponse = new TestNodejsFunction( + testStack.stack, + fnNameNoCaptureErrorOrResponse, + { + functionName: fnNameNoCaptureErrorOrResponse, + entry: lambdaFunctionCodeFile, + runtime: TEST_RUNTIMES[runtime], + environment: { TEST_TABLE_NAME: ddbTableName, POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'false', POWERTOOLS_TRACER_CAPTURE_ERROR: 'false', POWERTOOLS_TRACE_ENABLED: 'true', + EXPECTED_SERVICE_NAME: fnNameNoCaptureErrorOrResponse, + ...commonEnvironmentVariables, }, - runtime, - }); - ddbTable.grantWriteData(functionThatDoesNotCapturesErrorAndResponse); - - const functionWithTracerDisabled = createTracerTestFunction({ - stack: testStack.stack, - functionName: functionNameWithTracerDisabled, - entry, - expectedServiceName: serviceNameWithTracerDisabled, - environmentParams: { - TEST_TABLE_NAME: ddbTableName, - POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', - POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', - POWERTOOLS_TRACE_ENABLED: 'false', - }, - runtime, - }); - ddbTable.grantWriteData(functionWithTracerDisabled); + } + ); + ddbTable.grantWriteData(fnThatDoesNotCapturesErrorAndResponse); + + const fnWithTracerDisabled = new TestNodejsFunction( + testStack.stack, + fnNameTracerDisabled, + { + functionName: fnNameTracerDisabled, + entry: lambdaFunctionCodeFile, + runtime: TEST_RUNTIMES[runtime], + environment: { + TEST_TABLE_NAME: ddbTableName, + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', + POWERTOOLS_TRACE_ENABLED: 'false', + EXPECTED_SERVICE_NAME: fnNameTracerDisabled, + ...commonEnvironmentVariables, + }, + } + ); + ddbTable.grantWriteData(fnWithTracerDisabled); - const functionThatDoesNotCaptureResponseViaMiddlewareOption = - createTracerTestFunction({ - stack: testStack.stack, - functionName: functionNameWithNoCaptureResponseViaMiddlewareOption, - entry, + const fnWithCaptureResponseFalse = new TestNodejsFunction( + testStack.stack, + fnNameCaptureResponseOff, + { + functionName: fnNameCaptureResponseOff, handler: 'handlerWithNoCaptureResponseViaMiddlewareOption', - expectedServiceName: - serviceNameWithNoCaptureResponseViaMiddlewareOption, - environmentParams: { + entry: lambdaFunctionCodeFile, + runtime: TEST_RUNTIMES[runtime], + environment: { TEST_TABLE_NAME: ddbTableName, POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', POWERTOOLS_TRACE_ENABLED: 'true', + EXPECTED_SERVICE_NAME: fnNameCaptureResponseOff, + ...commonEnvironmentVariables, }, - runtime, - }); - ddbTable.grantWriteData( - functionThatDoesNotCaptureResponseViaMiddlewareOption + } ); + ddbTable.grantWriteData(fnWithCaptureResponseFalse); await testStack.deploy(); // Act await Promise.all([ - invokeAllTestCases(functionNameWithAllFlagsEnabled), - invokeAllTestCases(functionNameWithNoCaptureErrorOrResponse), - invokeAllTestCases(functionNameWithTracerDisabled), - invokeAllTestCases(functionNameWithNoCaptureResponseViaMiddlewareOption), + invokeAllTestCases(fnNameAllFlagsEnabled), + invokeAllTestCases(fnNameNoCaptureErrorOrResponse), + invokeAllTestCases(fnNameTracerDisabled), + invokeAllTestCases(fnNameCaptureResponseOff), ]); }, SETUP_TIMEOUT); @@ -228,7 +229,7 @@ describe(`Tracer E2E tests, all features with middy instantiation for runtime: $ const tracesWhenAllFlagsEnabled = await getTraces( xrayClient, startTime, - await getFunctionArn(stsClient, functionNameWithAllFlagsEnabled), + await getFunctionArn(stsClient, fnNameAllFlagsEnabled), invocations, 4 ); @@ -285,7 +286,7 @@ describe(`Tracer E2E tests, all features with middy instantiation for runtime: $ const tracesWhenAllFlagsEnabled = await getTraces( xrayClient, startTime, - await getFunctionArn(stsClient, functionNameWithAllFlagsEnabled), + await getFunctionArn(stsClient, fnNameAllFlagsEnabled), invocations, 4 ); @@ -300,7 +301,7 @@ describe(`Tracer E2E tests, all features with middy instantiation for runtime: $ assertAnnotation({ annotations, isColdStart, - expectedServiceName: serviceNameWithAllFlagsEnabled, + expectedServiceName: fnNameAllFlagsEnabled, expectedCustomAnnotationKey, expectedCustomAnnotationValue, }); @@ -309,14 +310,14 @@ describe(`Tracer E2E tests, all features with middy instantiation for runtime: $ fail('metadata is missing'); } expect( - metadata[serviceNameWithAllFlagsEnabled][expectedCustomMetadataKey] + metadata[fnNameAllFlagsEnabled][expectedCustomMetadataKey] ).toEqual(expectedCustomMetadataValue); const shouldThrowAnError = i === invocations - 1; if (!shouldThrowAnError) { // Assert that the metadata object contains the response expect( - metadata[serviceNameWithAllFlagsEnabled]['index.handler response'] + metadata[fnNameAllFlagsEnabled]['index.handler response'] ).toEqual(expectedCustomResponseValue); } } @@ -330,10 +331,7 @@ describe(`Tracer E2E tests, all features with middy instantiation for runtime: $ const tracesWithNoCaptureErrorOrResponse = await getTraces( xrayClient, startTime, - await getFunctionArn( - stsClient, - functionNameWithNoCaptureErrorOrResponse - ), + await getFunctionArn(stsClient, fnNameNoCaptureErrorOrResponse), invocations, 4 ); @@ -394,10 +392,7 @@ describe(`Tracer E2E tests, all features with middy instantiation for runtime: $ const tracesWithNoCaptureResponse = await getTraces( xrayClient, startTime, - await getFunctionArn( - stsClient, - functionNameWithNoCaptureResponseViaMiddlewareOption - ), + await getFunctionArn(stsClient, fnNameCaptureResponseOff), invocations, 4 ); @@ -459,7 +454,7 @@ describe(`Tracer E2E tests, all features with middy instantiation for runtime: $ const tracesWithTracerDisabled = await getTraces( xrayClient, startTime, - await getFunctionArn(stsClient, functionNameWithTracerDisabled), + await getFunctionArn(stsClient, fnNameTracerDisabled), invocations, expectedNoOfTraces ); diff --git a/packages/tracer/tests/e2e/asyncHandler.decorator.test.ts b/packages/tracer/tests/e2e/asyncHandler.decorator.test.ts index 8d460ee0f1..336b6d1817 100644 --- a/packages/tracer/tests/e2e/asyncHandler.decorator.test.ts +++ b/packages/tracer/tests/e2e/asyncHandler.decorator.test.ts @@ -3,19 +3,25 @@ * * @group e2e/tracer/decorator-async-handler */ - -import path from 'path'; -import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb'; -import { RemovalPolicy } from 'aws-cdk-lib'; -import { XRayClient } from '@aws-sdk/client-xray'; -import { STSClient } from '@aws-sdk/client-sts'; -import { v4 } from 'uuid'; import { - TestStack, + concatenateResourceName, defaultRuntime, + generateTestUniqueName, + isValidRuntimeKey, + TestNodejsFunction, + TestStack, + TEST_RUNTIMES, } from '@aws-lambda-powertools/testing-utils'; +import { STSClient } from '@aws-sdk/client-sts'; +import { XRayClient } from '@aws-sdk/client-xray'; +import { RemovalPolicy } from 'aws-cdk-lib'; +import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb'; +import { join } from 'node:path'; +import { + assertAnnotation, + assertErrorAndFault, +} from '../helpers/traceAssertions'; import { - createTracerTestFunction, getFirstSubsegment, getFunctionArn, getInvocationSubsegment, @@ -24,10 +30,7 @@ import { splitSegmentsByName, } from '../helpers/tracesUtils'; import { - generateUniqueName, - isValidRuntimeKey, -} from '../../../commons/tests/utils/e2eUtils'; -import { + commonEnvironmentVariables, expectedCustomAnnotationKey, expectedCustomAnnotationValue, expectedCustomErrorMessage, @@ -40,63 +43,58 @@ import { TEARDOWN_TIMEOUT, TEST_CASE_TIMEOUT, } from './constants'; -import { - assertAnnotation, - assertErrorAndFault, -} from '../helpers/traceAssertions'; - -const runtime: string = process.env.RUNTIME || defaultRuntime; - -if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key value: ${runtime}`); -} - -const stackName = generateUniqueName( - RESOURCE_NAME_PREFIX, - v4(), - runtime, - 'AllFeatures-Decorator' -); -const lambdaFunctionCodeFile = 'asyncHandler.decorator.test.functionCode.ts'; -let startTime: Date; -/** - * Function #1 is with all flags enabled. - */ -const uuidFunction1 = v4(); -const functionNameWithAllFlagsEnabled = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuidFunction1, - runtime, - 'AllFeatures-Decorator-Async-AllFlagsEnabled' -); -const serviceNameWithAllFlagsEnabled = functionNameWithAllFlagsEnabled; +describe(`Tracer E2E tests, async handler with decorator instantiation`, () => { + const runtime: string = process.env.RUNTIME || defaultRuntime; -/** - * Function #2 sets a custom subsegment name in the decorated method - */ -const uuidFunction2 = v4(); -const functionNameWithCustomSubsegmentNameInMethod = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuidFunction2, - runtime, - 'AllFeatures-Decorator-Async-CustomSubsegmentNameInMethod' -); -const serviceNameWithCustomSubsegmentNameInMethod = - functionNameWithCustomSubsegmentNameInMethod; + if (!isValidRuntimeKey(runtime)) { + throw new Error(`Invalid runtime key value: ${runtime}`); + } -const xrayClient = new XRayClient({}); -const stsClient = new STSClient({}); -const invocations = 3; + const testName = generateTestUniqueName({ + testPrefix: RESOURCE_NAME_PREFIX, + runtime, + testName: 'Async-Decorator', + }); + const testStack = new TestStack(testName); -const testStack = new TestStack(stackName); + // Location of the lambda function code + const lambdaFunctionCodeFile = join( + __dirname, + 'asyncHandler.decorator.test.functionCode.ts' + ); + const startTime = new Date(); + + /** + * Function #1 is with all flags enabled. + */ + const fnNameAllFlagsEnabled = concatenateResourceName({ + testName, + resourceName: 'AllFlagsOn', + }); + + /** + * Function #2 sets a custom subsegment name in the decorated method + */ + const fnNameCustomSubsegment = concatenateResourceName({ + testName, + resourceName: 'CustomSubsegmentName', + }); + + /** + * Table used by all functions to make an SDK call + */ + const ddbTableName = concatenateResourceName({ + testName, + resourceName: 'TestTable', + }); + + const xrayClient = new XRayClient({}); + const stsClient = new STSClient({}); + const invocations = 3; -describe(`Tracer E2E tests, asynchronous handler with decorator instantiation for runtime: ${runtime}`, () => { beforeAll(async () => { // Prepare - startTime = new Date(); - const ddbTableName = stackName + '-table'; - const ddbTable = new Table(testStack.stack, 'Table', { tableName: ddbTableName, partitionKey: { @@ -107,45 +105,52 @@ describe(`Tracer E2E tests, asynchronous handler with decorator instantiation fo removalPolicy: RemovalPolicy.DESTROY, }); - const entry = path.join(__dirname, lambdaFunctionCodeFile); - const functionWithAllFlagsEnabled = createTracerTestFunction({ - stack: testStack.stack, - functionName: functionNameWithAllFlagsEnabled, - entry, - expectedServiceName: serviceNameWithAllFlagsEnabled, - environmentParams: { - TEST_TABLE_NAME: ddbTableName, - POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', - POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', - POWERTOOLS_TRACE_ENABLED: 'true', - }, - runtime, - }); - ddbTable.grantWriteData(functionWithAllFlagsEnabled); - - const functionWithCustomSubsegmentNameInMethod = createTracerTestFunction({ - stack: testStack.stack, - functionName: functionNameWithCustomSubsegmentNameInMethod, - handler: 'handlerWithCustomSubsegmentNameInMethod', - entry, - expectedServiceName: serviceNameWithCustomSubsegmentNameInMethod, - environmentParams: { - TEST_TABLE_NAME: ddbTableName, - EXPECTED_CUSTOM_SUBSEGMENT_NAME: expectedCustomSubSegmentName, - POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', - POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', - POWERTOOLS_TRACE_ENABLED: 'true', - }, - runtime, - }); - ddbTable.grantWriteData(functionWithCustomSubsegmentNameInMethod); + const fnWithAllFlagsEnabled = new TestNodejsFunction( + testStack.stack, + fnNameAllFlagsEnabled, + { + functionName: fnNameAllFlagsEnabled, + entry: lambdaFunctionCodeFile, + runtime: TEST_RUNTIMES[runtime], + environment: { + TEST_TABLE_NAME: ddbTableName, + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', + POWERTOOLS_TRACE_ENABLED: 'true', + EXPECTED_SERVICE_NAME: fnNameAllFlagsEnabled, + ...commonEnvironmentVariables, + }, + } + ); + ddbTable.grantWriteData(fnWithAllFlagsEnabled); + + const fnWithCustomSubsegmentName = new TestNodejsFunction( + testStack.stack, + fnNameCustomSubsegment, + { + functionName: fnNameCustomSubsegment, + entry: lambdaFunctionCodeFile, + runtime: TEST_RUNTIMES[runtime], + handler: 'handlerWithCustomSubsegmentNameInMethod', + environment: { + TEST_TABLE_NAME: ddbTableName, + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', + POWERTOOLS_TRACE_ENABLED: 'true', + EXPECTED_SERVICE_NAME: fnNameCustomSubsegment, + EXPECTED_CUSTOM_SUBSEGMENT_NAME: expectedCustomSubSegmentName, + ...commonEnvironmentVariables, + }, + } + ); + ddbTable.grantWriteData(fnWithCustomSubsegmentName); await testStack.deploy(); // Act await Promise.all([ - invokeAllTestCases(functionNameWithAllFlagsEnabled), - invokeAllTestCases(functionNameWithCustomSubsegmentNameInMethod), + invokeAllTestCases(fnNameAllFlagsEnabled), + invokeAllTestCases(fnNameCustomSubsegment), ]); }, SETUP_TIMEOUT); @@ -161,7 +166,7 @@ describe(`Tracer E2E tests, asynchronous handler with decorator instantiation fo const tracesWhenAllFlagsEnabled = await getTraces( xrayClient, startTime, - await getFunctionArn(stsClient, functionNameWithAllFlagsEnabled), + await getFunctionArn(stsClient, fnNameAllFlagsEnabled), invocations, 4 ); @@ -221,7 +226,7 @@ describe(`Tracer E2E tests, asynchronous handler with decorator instantiation fo const traces = await getTraces( xrayClient, startTime, - await getFunctionArn(stsClient, functionNameWithAllFlagsEnabled), + await getFunctionArn(stsClient, fnNameAllFlagsEnabled), invocations, 4 ); @@ -236,7 +241,7 @@ describe(`Tracer E2E tests, asynchronous handler with decorator instantiation fo assertAnnotation({ annotations, isColdStart, - expectedServiceName: serviceNameWithAllFlagsEnabled, + expectedServiceName: fnNameAllFlagsEnabled, expectedCustomAnnotationKey, expectedCustomAnnotationValue, }); @@ -245,14 +250,14 @@ describe(`Tracer E2E tests, asynchronous handler with decorator instantiation fo fail('metadata is missing'); } expect( - metadata[serviceNameWithAllFlagsEnabled][expectedCustomMetadataKey] + metadata[fnNameAllFlagsEnabled][expectedCustomMetadataKey] ).toEqual(expectedCustomMetadataValue); const shouldThrowAnError = i === invocations - 1; if (!shouldThrowAnError) { // Assert that the metadata object contains the response expect( - metadata[serviceNameWithAllFlagsEnabled]['index.handler response'] + metadata[fnNameAllFlagsEnabled]['index.handler response'] ).toEqual(expectedCustomResponseValue); } } @@ -266,10 +271,7 @@ describe(`Tracer E2E tests, asynchronous handler with decorator instantiation fo const tracesWhenCustomSubsegmentNameInMethod = await getTraces( xrayClient, startTime, - await getFunctionArn( - stsClient, - functionNameWithCustomSubsegmentNameInMethod - ), + await getFunctionArn(stsClient, fnNameCustomSubsegment), invocations, 4 ); diff --git a/packages/tracer/tests/e2e/constants.ts b/packages/tracer/tests/e2e/constants.ts index 7d27f4cde4..e303978585 100644 --- a/packages/tracer/tests/e2e/constants.ts +++ b/packages/tracer/tests/e2e/constants.ts @@ -1,13 +1,40 @@ -export const RESOURCE_NAME_PREFIX = 'Tracer-E2E'; -export const ONE_MINUTE = 60 * 1_000; -export const TEST_CASE_TIMEOUT = 5 * ONE_MINUTE; -export const SETUP_TIMEOUT = 5 * ONE_MINUTE; -export const TEARDOWN_TIMEOUT = 5 * ONE_MINUTE; +// Prefix for all resources created by the E2E tests +const RESOURCE_NAME_PREFIX = 'Tracer-E2E'; +// Constants relating time to be used in the tests +const ONE_MINUTE = 60 * 1_000; +const TEST_CASE_TIMEOUT = 5 * ONE_MINUTE; +const SETUP_TIMEOUT = 5 * ONE_MINUTE; +const TEARDOWN_TIMEOUT = 5 * ONE_MINUTE; -export const expectedCustomAnnotationKey = 'myAnnotation'; -export const expectedCustomAnnotationValue = 'myValue'; -export const expectedCustomMetadataKey = 'myMetadata'; -export const expectedCustomMetadataValue = { bar: 'baz' }; -export const expectedCustomResponseValue = { foo: 'bar' }; -export const expectedCustomErrorMessage = 'An error has occurred'; -export const expectedCustomSubSegmentName = 'mySubsegment'; +// Expected values for custom annotations, metadata, and response +const expectedCustomAnnotationKey = 'myAnnotation'; +const expectedCustomAnnotationValue = 'myValue'; +const expectedCustomMetadataKey = 'myMetadata'; +const expectedCustomMetadataValue = { bar: 'baz' }; +const expectedCustomResponseValue = { foo: 'bar' }; +const expectedCustomErrorMessage = 'An error has occurred'; +const expectedCustomSubSegmentName = 'mySubsegment'; +const commonEnvironmentVariables = { + EXPECTED_CUSTOM_ANNOTATION_KEY: expectedCustomAnnotationKey, + EXPECTED_CUSTOM_ANNOTATION_VALUE: expectedCustomAnnotationValue, + EXPECTED_CUSTOM_METADATA_KEY: expectedCustomMetadataKey, + EXPECTED_CUSTOM_METADATA_VALUE: JSON.stringify(expectedCustomMetadataValue), + EXPECTED_CUSTOM_RESPONSE_VALUE: JSON.stringify(expectedCustomResponseValue), + EXPECTED_CUSTOM_ERROR_MESSAGE: expectedCustomErrorMessage, +}; + +export { + RESOURCE_NAME_PREFIX, + ONE_MINUTE, + TEST_CASE_TIMEOUT, + SETUP_TIMEOUT, + TEARDOWN_TIMEOUT, + expectedCustomAnnotationKey, + expectedCustomAnnotationValue, + expectedCustomMetadataKey, + expectedCustomMetadataValue, + expectedCustomResponseValue, + expectedCustomErrorMessage, + expectedCustomSubSegmentName, + commonEnvironmentVariables, +}; diff --git a/packages/tracer/tests/helpers/populateEnvironmentVariables.ts b/packages/tracer/tests/helpers/populateEnvironmentVariables.ts index 4d4166743d..096b3c67be 100644 --- a/packages/tracer/tests/helpers/populateEnvironmentVariables.ts +++ b/packages/tracer/tests/helpers/populateEnvironmentVariables.ts @@ -10,6 +10,7 @@ if ( process.env.AWS_REGION = 'eu-west-1'; } process.env._HANDLER = 'index.handler'; +process.env.AWS_NODEJS_CONNECTION_REUSE_ENABLED = '1'; // Powertools for AWS Lambda (TypeScript) variables process.env.POWERTOOLS_SERVICE_NAME = 'hello-world'; diff --git a/packages/tracer/tests/helpers/traceUtils.types.ts b/packages/tracer/tests/helpers/traceUtils.types.ts index 6b537deced..fc2c8cac05 100644 --- a/packages/tracer/tests/helpers/traceUtils.types.ts +++ b/packages/tracer/tests/helpers/traceUtils.types.ts @@ -1,5 +1,3 @@ -import type { Stack } from 'aws-cdk-lib'; - interface ParsedDocument { name: string; id: string; @@ -61,16 +59,6 @@ interface ParsedTrace { Segments: ParsedSegment[]; } -interface TracerTestFunctionParams { - stack: Stack; - functionName: string; - handler?: string; - entry: string; - expectedServiceName: string; - environmentParams: { [key: string]: string }; - runtime: string; -} - interface AssertAnnotationParams { annotations: ParsedDocument['annotations']; isColdStart: boolean; @@ -79,10 +67,4 @@ interface AssertAnnotationParams { expectedCustomAnnotationValue: string | number | boolean; } -export { - ParsedDocument, - ParsedSegment, - ParsedTrace, - TracerTestFunctionParams, - AssertAnnotationParams, -}; +export { ParsedDocument, ParsedSegment, ParsedTrace, AssertAnnotationParams }; diff --git a/packages/tracer/tests/helpers/tracesUtils.ts b/packages/tracer/tests/helpers/tracesUtils.ts index 3bda9c551c..3bbd13a022 100644 --- a/packages/tracer/tests/helpers/tracesUtils.ts +++ b/packages/tracer/tests/helpers/tracesUtils.ts @@ -1,7 +1,4 @@ import promiseRetry from 'promise-retry'; -import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; -import { Duration } from 'aws-cdk-lib'; -import { Architecture, Tracing } from 'aws-cdk-lib/aws-lambda'; import type { XRayClient } from '@aws-sdk/client-xray'; import { BatchGetTracesCommand, @@ -9,25 +6,12 @@ import { } from '@aws-sdk/client-xray'; import type { STSClient } from '@aws-sdk/client-sts'; import { GetCallerIdentityCommand } from '@aws-sdk/client-sts'; -import { - expectedCustomAnnotationKey, - expectedCustomAnnotationValue, - expectedCustomErrorMessage, - expectedCustomMetadataKey, - expectedCustomMetadataValue, - expectedCustomResponseValue, -} from '../e2e/constants'; -import { - invokeFunction, - TEST_RUNTIMES, - TestRuntimesKey, -} from '../../../commons/tests/utils/e2eUtils'; +import { invokeFunctionOnce } from '@aws-lambda-powertools/testing-utils'; import { FunctionSegmentNotDefinedError } from './FunctionSegmentNotDefinedError'; import type { ParsedDocument, ParsedSegment, ParsedTrace, - TracerTestFunctionParams, } from './traceUtils.types'; const getTraces = async ( @@ -43,9 +27,9 @@ const getTraces = async ( maxTimeout: 10_000, factor: 1.25, }; + const endTime = new Date(); return promiseRetry(async (retry: (err?: Error) => never, _: number) => { - const endTime = new Date(); console.log( `Manual query: aws xray get-trace-summaries --start-time ${Math.floor( startTime.getTime() / 1000 @@ -229,57 +213,27 @@ const splitSegmentsByName = ( * @param functionName */ const invokeAllTestCases = async (functionName: string): Promise => { - await invokeFunction(functionName, 1, 'SEQUENTIAL', { - invocation: 1, - throw: false, - }); - await invokeFunction(functionName, 1, 'SEQUENTIAL', { - invocation: 2, - throw: false, + await invokeFunctionOnce({ + functionName, + payload: { + invocation: 1, + throw: false, + }, }); - await invokeFunction(functionName, 1, 'SEQUENTIAL', { - invocation: 3, - throw: true, // only last invocation should throw + await invokeFunctionOnce({ + functionName, + payload: { + invocation: 2, + throw: false, + }, }); -}; - -const createTracerTestFunction = ( - params: TracerTestFunctionParams -): NodejsFunction => { - const { - stack, + await invokeFunctionOnce({ functionName, - entry, - expectedServiceName, - environmentParams, - runtime, - } = params; - const func = new NodejsFunction(stack, functionName, { - entry: entry, - functionName: functionName, - handler: params.handler ?? 'handler', - tracing: Tracing.ACTIVE, - architecture: Architecture.X86_64, - memorySize: 256, // Default value (128) will take too long to process - environment: { - EXPECTED_SERVICE_NAME: expectedServiceName, - EXPECTED_CUSTOM_ANNOTATION_KEY: expectedCustomAnnotationKey, - EXPECTED_CUSTOM_ANNOTATION_VALUE: expectedCustomAnnotationValue, - EXPECTED_CUSTOM_METADATA_KEY: expectedCustomMetadataKey, - EXPECTED_CUSTOM_METADATA_VALUE: JSON.stringify( - expectedCustomMetadataValue - ), - EXPECTED_CUSTOM_RESPONSE_VALUE: JSON.stringify( - expectedCustomResponseValue - ), - EXPECTED_CUSTOM_ERROR_MESSAGE: expectedCustomErrorMessage, - ...environmentParams, + payload: { + invocation: 3, + throw: true, // only last invocation should throw }, - timeout: Duration.seconds(30), // Default value (3 seconds) will time out - runtime: TEST_RUNTIMES[runtime as TestRuntimesKey], }); - - return func; }; let account: string | undefined; @@ -303,6 +257,5 @@ export { getInvocationSubsegment, splitSegmentsByName, invokeAllTestCases, - createTracerTestFunction, getFunctionArn, }; From c6771beb1c070676a0cc9bbc041f7127dde81990 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 29 Aug 2023 15:12:16 +0200 Subject: [PATCH 02/18] chore: move test to correct folder --- .../tests/unit/TestInvocationLogs.test.ts} | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) rename packages/{commons/tests/unit/InvocationLogs.test.ts => testing/tests/unit/TestInvocationLogs.test.ts} (89%) diff --git a/packages/commons/tests/unit/InvocationLogs.test.ts b/packages/testing/tests/unit/TestInvocationLogs.test.ts similarity index 89% rename from packages/commons/tests/unit/InvocationLogs.test.ts rename to packages/testing/tests/unit/TestInvocationLogs.test.ts index 5c457a6dbe..c81a05b0b0 100644 --- a/packages/commons/tests/unit/InvocationLogs.test.ts +++ b/packages/testing/tests/unit/TestInvocationLogs.test.ts @@ -5,7 +5,7 @@ * */ -import { InvocationLogs, LEVEL } from '../utils/InvocationLogs'; +import { TestInvocationLogs } from '../../src/TestInvocationLogs'; const exampleLogs = `START RequestId: c6af9ac6-7b61-11e6-9a41-93e812345678 Version: $LATEST {"cold_start":true,"function_arn":"arn:aws:lambda:eu-west-1:561912387782:function:loggerMiddyStandardFeatures-c555a2ec-1121-4586-9c04-185ab36ea34c","function_memory_size":128,"function_name":"loggerMiddyStandardFeatures-c555a2ec-1121-4586-9c04-185ab36ea34c","function_request_id":"7f586697-238a-4c3b-9250-a5f057c1119c","level":"DEBUG","message":"This is a DEBUG log but contains the word INFO some context and persistent key","service":"logger-e2e-testing","timestamp":"2022-01-27T16:04:39.323Z","persistentKey":"works"} @@ -17,20 +17,20 @@ REPORT RequestId: c6af9ac6-7b61-11e6-9a41-93e812345678\tDuration: 2.16 ms\tBille describe('Constructor', () => { test('it should parse base64 text correctly', () => { - const invocationLogs = new InvocationLogs( + const invocationLogs = new TestInvocationLogs( Buffer.from(exampleLogs).toString('base64') ); - expect(invocationLogs.getFunctionLogs(LEVEL.DEBUG).length).toBe(1); - expect(invocationLogs.getFunctionLogs(LEVEL.INFO).length).toBe(2); - expect(invocationLogs.getFunctionLogs(LEVEL.ERROR).length).toBe(1); + expect(invocationLogs.getFunctionLogs('DEBUG').length).toBe(1); + expect(invocationLogs.getFunctionLogs('INFO').length).toBe(2); + expect(invocationLogs.getFunctionLogs('ERROR').length).toBe(1); }); }); describe('doesAnyFunctionLogsContains()', () => { - let invocationLogs: InvocationLogs; + let invocationLogs: TestInvocationLogs; beforeEach(() => { - invocationLogs = new InvocationLogs( + invocationLogs = new TestInvocationLogs( Buffer.from(exampleLogs).toString('base64') ); }); @@ -74,49 +74,49 @@ describe('doesAnyFunctionLogsContains()', () => { test('it should apply filter log based on the given level', () => { const debugLogHasWordINFO = invocationLogs.doesAnyFunctionLogsContains( 'INFO', - LEVEL.DEBUG + 'DEBUG' ); expect(debugLogHasWordINFO).toBe(true); const infoLogHasWordINFO = invocationLogs.doesAnyFunctionLogsContains( 'INFO', - LEVEL.INFO + 'INFO' ); expect(infoLogHasWordINFO).toBe(true); const errorLogHasWordINFO = invocationLogs.doesAnyFunctionLogsContains( 'INFO', - LEVEL.ERROR + 'ERROR' ); expect(errorLogHasWordINFO).toBe(false); }); }); describe('getFunctionLogs()', () => { - let invocationLogs: InvocationLogs; + let invocationLogs: TestInvocationLogs; beforeEach(() => { - invocationLogs = new InvocationLogs( + invocationLogs = new TestInvocationLogs( Buffer.from(exampleLogs).toString('base64') ); }); test('it should retrive logs of the given level only', () => { - const infoLogs = invocationLogs.getFunctionLogs(LEVEL.INFO); + const infoLogs = invocationLogs.getFunctionLogs('INFO'); expect(infoLogs.length).toBe(2); expect(infoLogs[0].includes('INFO')).toBe(true); expect(infoLogs[1].includes('INFO')).toBe(true); expect(infoLogs[0].includes('ERROR')).toBe(false); expect(infoLogs[1].includes('ERROR')).toBe(false); - const errorLogs = invocationLogs.getFunctionLogs(LEVEL.ERROR); + const errorLogs = invocationLogs.getFunctionLogs('ERROR'); expect(errorLogs.length).toBe(1); expect(errorLogs[0].includes('INFO')).toBe(false); expect(errorLogs[0].includes('ERROR')).toBe(true); }); test('it should NOT return logs generated by Lambda service (e.g. START, END, and REPORT)', () => { - const errorLogs = invocationLogs.getFunctionLogs(LEVEL.ERROR); + const errorLogs = invocationLogs.getFunctionLogs('ERROR'); expect(errorLogs.length).toBe(1); expect(errorLogs[0].includes('START')).toBe(false); expect(errorLogs[0].includes('END')).toBe(false); @@ -129,7 +129,7 @@ describe('parseFunctionLog()', () => { const rawLogStr = '{"cold_start":true,"function_arn":"arn:aws:lambda:eu-west-1:561912387782:function:loggerMiddyStandardFeatures-c555a2ec-1121-4586-9c04-185ab36ea34c","function_memory_size":128,"function_name":"loggerMiddyStandardFeatures-c555a2ec-1121-4586-9c04-185ab36ea34c","function_request_id":"7f586697-238a-4c3b-9250-a5f057c1119c","level":"DEBUG","message":"This is a DEBUG log but contains the word INFO some context and persistent key","service":"logger-e2e-testing","timestamp":"2022-01-27T16:04:39.323Z","persistentKey":"works"}'; - const logObj = InvocationLogs.parseFunctionLog(rawLogStr); + const logObj = TestInvocationLogs.parseFunctionLog(rawLogStr); expect(logObj).toStrictEqual({ cold_start: true, function_arn: @@ -150,7 +150,7 @@ describe('parseFunctionLog()', () => { test('it should throw an error if receive incorrect formatted raw log string', () => { const notJSONstring = 'not-json-string'; expect(() => { - InvocationLogs.parseFunctionLog(notJSONstring); + TestInvocationLogs.parseFunctionLog(notJSONstring); }).toThrow(Error); }); }); From 4db2b5993161757e8b75259362aabae9f17b5959 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 29 Aug 2023 19:12:12 +0200 Subject: [PATCH 03/18] chore: reduced duplication --- packages/testing/src/helpers.ts | 24 +- .../src/resources/TestNodejsFunction.ts | 7 + .../tests/e2e/allFeatures.decorator.test.ts | 235 +++++++---------- .../tests/e2e/allFeatures.manual.test.ts | 94 +++---- .../tests/e2e/allFeatures.middy.test.ts | 245 +++++++----------- .../tests/e2e/asyncHandler.decorator.test.ts | 142 ++++------ packages/tracer/tests/helpers/factories.ts | 80 ++++++ packages/tracer/tests/helpers/tracesUtils.ts | 51 ++-- 8 files changed, 419 insertions(+), 459 deletions(-) create mode 100644 packages/tracer/tests/helpers/factories.ts diff --git a/packages/testing/src/helpers.ts b/packages/testing/src/helpers.ts index 8e8e5eac21..e697e2e671 100644 --- a/packages/testing/src/helpers.ts +++ b/packages/testing/src/helpers.ts @@ -50,4 +50,26 @@ const concatenateResourceName = ({ resourceName: string; }): string => `${testName}-${resourceName}`.substring(0, 64); -export { isValidRuntimeKey, generateTestUniqueName, concatenateResourceName }; +/** + * Find and get the value of a StackOutput by its key. + */ +const findAndGetStackOutputValue = ( + outputs: Record, + key: string +): string => { + const value = Object.keys(outputs).find((outputKey) => + outputKey.includes(key) + ); + if (!value) { + throw new Error(`Cannot find output for ${key}`); + } + + return outputs[value]; +}; + +export { + isValidRuntimeKey, + generateTestUniqueName, + concatenateResourceName, + findAndGetStackOutputValue, +}; diff --git a/packages/testing/src/resources/TestNodejsFunction.ts b/packages/testing/src/resources/TestNodejsFunction.ts index 006689c442..c95040d906 100644 --- a/packages/testing/src/resources/TestNodejsFunction.ts +++ b/packages/testing/src/resources/TestNodejsFunction.ts @@ -7,6 +7,7 @@ import type { Construct } from 'constructs'; interface ExtraTestProps { logGroupOutputKey?: string; + fnOutputKey?: string; } /** @@ -34,6 +35,12 @@ class TestNodejsFunction extends NodejsFunction { value: this.logGroup.logGroupName, }); } + + if (extraProps.fnOutputKey) { + new CfnOutput(this, extraProps.fnOutputKey, { + value: this.functionName, + }); + } } } diff --git a/packages/tracer/tests/e2e/allFeatures.decorator.test.ts b/packages/tracer/tests/e2e/allFeatures.decorator.test.ts index b1ba14fa2f..c4a1a904f0 100644 --- a/packages/tracer/tests/e2e/allFeatures.decorator.test.ts +++ b/packages/tracer/tests/e2e/allFeatures.decorator.test.ts @@ -4,19 +4,15 @@ * @group e2e/tracer/decorator */ import { - concatenateResourceName, defaultRuntime, + findAndGetStackOutputValue, generateTestUniqueName, isValidRuntimeKey, - TestNodejsFunction, TestStack, - TEST_RUNTIMES, } from '@aws-lambda-powertools/testing-utils'; -import { STSClient } from '@aws-sdk/client-sts'; import { XRayClient } from '@aws-sdk/client-xray'; -import { RemovalPolicy } from 'aws-cdk-lib'; -import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb'; import { join } from 'node:path'; +import { functionFactory, tableFactory } from '../helpers/factories'; import { assertAnnotation, assertErrorAndFault, @@ -30,7 +26,6 @@ import { splitSegmentsByName, } from '../helpers/tracesUtils'; import { - commonEnvironmentVariables, expectedCustomAnnotationKey, expectedCustomAnnotationValue, expectedCustomErrorMessage, @@ -67,153 +62,113 @@ describe(`Tracer E2E tests, all features with decorator instantiation`, () => { const testStack = new TestStack(testName); // Location of the lambda function code - const lambdaFunctionCodeFile = join( + const lambdaFunctionCodeFilePath = join( __dirname, 'allFeatures.decorator.test.functionCode.ts' ); const startTime = new Date(); /** - * Function #1 is with all flags enabled. + * Table used by all functions to make an SDK call */ - const fnNameAllFlagsEnabled = concatenateResourceName({ + const testTable = tableFactory({ + testStack, testName, - resourceName: 'AllFlagsOn', + tableSuffix: 'TestTable', }); /** - * Function #2 doesn't capture error or response + * Function #1 is with all flags enabled. */ - const fnNameNoCaptureErrorOrResponse = concatenateResourceName({ + let fnNameAllFlagsEnabled: string; + const fnAllFlagsEnabled = functionFactory({ + testStack, testName, - resourceName: 'NoCaptureErrOrResp', + functionSuffix: 'AllFlagsOn', + lambdaFunctionCodeFilePath, + environment: { + TEST_TABLE_NAME: testTable.tableName, + }, }); + testTable.grantWriteData(fnAllFlagsEnabled); /** - * Function #3 disables tracer + * Function #2 doesn't capture error or response */ - const fnNameTracerDisabled = concatenateResourceName({ + let fnNameNoCaptureErrorOrResponse: string; + const fnNoCaptureErrorOrResponse = functionFactory({ + testStack, testName, - resourceName: 'TracerDisabled', + functionSuffix: 'NoCaptureErrOrResp', + lambdaFunctionCodeFilePath, + environment: { + TEST_TABLE_NAME: testTable.tableName, + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'false', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'false', + }, }); + testTable.grantWriteData(fnNoCaptureErrorOrResponse); /** - * Function #4 disables capture response via decorator options + * Function #3 disables tracer */ - const fnNameCaptureResponseOff = concatenateResourceName({ + let fnNameTracerDisabled: string; + const fnTracerDisabled = functionFactory({ + testStack, testName, - resourceName: 'CaptureRespOff', + functionSuffix: 'TracerDisabled', + lambdaFunctionCodeFilePath, + environment: { + TEST_TABLE_NAME: testTable.tableName, + POWERTOOLS_TRACE_ENABLED: 'false', + }, }); + testTable.grantWriteData(fnTracerDisabled); /** - * Table used by all functions to make an SDK call + * Function #4 disables capture response via decorator options */ - const ddbTableName = concatenateResourceName({ + let fnNameCaptureResponseOff: string; + const fnCaptureResponseOff = functionFactory({ + testStack, testName, - resourceName: 'TestTable', + functionSuffix: 'CaptureResponseOff', + lambdaFunctionCodeFilePath, + handler: 'handlerWithCaptureResponseFalse', + environment: { + TEST_TABLE_NAME: testTable.tableName, + }, }); + testTable.grantWriteData(fnCaptureResponseOff); const xrayClient = new XRayClient({}); - const stsClient = new STSClient({}); - const invocations = 3; + const invocationCount = 3; beforeAll(async () => { - // Prepare - const ddbTable = new Table(testStack.stack, 'Table', { - tableName: ddbTableName, - partitionKey: { - name: 'id', - type: AttributeType.STRING, - }, - billingMode: BillingMode.PAY_PER_REQUEST, - removalPolicy: RemovalPolicy.DESTROY, - }); - - const fnWithAllFlagsEnabled = new TestNodejsFunction( - testStack.stack, - fnNameAllFlagsEnabled, - { - functionName: fnNameAllFlagsEnabled, - entry: lambdaFunctionCodeFile, - runtime: TEST_RUNTIMES[runtime], - environment: { - TEST_TABLE_NAME: ddbTableName, - POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', - POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', - POWERTOOLS_TRACE_ENABLED: 'true', - EXPECTED_SERVICE_NAME: fnNameAllFlagsEnabled, - ...commonEnvironmentVariables, - }, - } + // Deploy the stack + const outputs = await testStack.deploy(); + + // Get the actual function names from the stack outputs + fnNameAllFlagsEnabled = findAndGetStackOutputValue(outputs, 'AllFlagsOn'); + fnNameNoCaptureErrorOrResponse = findAndGetStackOutputValue( + outputs, + 'NoCaptureErrOrResp' ); - ddbTable.grantWriteData(fnWithAllFlagsEnabled); - - const fnThatDoesNotCapturesErrorAndResponse = new TestNodejsFunction( - testStack.stack, - fnNameNoCaptureErrorOrResponse, - { - functionName: fnNameNoCaptureErrorOrResponse, - entry: lambdaFunctionCodeFile, - runtime: TEST_RUNTIMES[runtime], - environment: { - TEST_TABLE_NAME: ddbTableName, - POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'false', - POWERTOOLS_TRACER_CAPTURE_ERROR: 'false', - POWERTOOLS_TRACE_ENABLED: 'true', - EXPECTED_SERVICE_NAME: fnNameNoCaptureErrorOrResponse, - ...commonEnvironmentVariables, - }, - } + fnNameTracerDisabled = findAndGetStackOutputValue( + outputs, + 'TracerDisabled' ); - ddbTable.grantWriteData(fnThatDoesNotCapturesErrorAndResponse); - - const fnWithTracerDisabled = new TestNodejsFunction( - testStack.stack, - fnNameTracerDisabled, - { - functionName: fnNameTracerDisabled, - entry: lambdaFunctionCodeFile, - runtime: TEST_RUNTIMES[runtime], - environment: { - TEST_TABLE_NAME: ddbTableName, - POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', - POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', - POWERTOOLS_TRACE_ENABLED: 'false', - EXPECTED_SERVICE_NAME: fnNameTracerDisabled, - ...commonEnvironmentVariables, - }, - } + fnNameCaptureResponseOff = findAndGetStackOutputValue( + outputs, + 'CaptureResponseOff' ); - ddbTable.grantWriteData(fnWithTracerDisabled); - - const fnWithCaptureResponseFalse = new TestNodejsFunction( - testStack.stack, - fnNameCaptureResponseOff, - { - functionName: fnNameCaptureResponseOff, - handler: 'handlerWithCaptureResponseFalse', - entry: lambdaFunctionCodeFile, - runtime: TEST_RUNTIMES[runtime], - environment: { - TEST_TABLE_NAME: ddbTableName, - POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', - POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', - POWERTOOLS_TRACE_ENABLED: 'true', - EXPECTED_SERVICE_NAME: fnNameCaptureResponseOff, - ...commonEnvironmentVariables, - }, - } - ); - ddbTable.grantWriteData(fnWithCaptureResponseFalse); - - await testStack.deploy(); - // Act + // Invoke all functions await Promise.all([ - invokeAllTestCases(fnNameAllFlagsEnabled), - invokeAllTestCases(fnNameNoCaptureErrorOrResponse), - invokeAllTestCases(fnNameTracerDisabled), - invokeAllTestCases(fnNameCaptureResponseOff), + invokeAllTestCases(fnNameAllFlagsEnabled, invocationCount), + invokeAllTestCases(fnNameNoCaptureErrorOrResponse, invocationCount), + invokeAllTestCases(fnNameTracerDisabled, invocationCount), + invokeAllTestCases(fnNameCaptureResponseOff, invocationCount), ]); }, SETUP_TIMEOUT); @@ -229,15 +184,15 @@ describe(`Tracer E2E tests, all features with decorator instantiation`, () => { const tracesWhenAllFlagsEnabled = await getTraces( xrayClient, startTime, - await getFunctionArn(stsClient, fnNameAllFlagsEnabled), - invocations, + await getFunctionArn(fnNameAllFlagsEnabled), + invocationCount, 4 ); - expect(tracesWhenAllFlagsEnabled.length).toBe(invocations); + expect(tracesWhenAllFlagsEnabled.length).toBe(invocationCount); // Assess - for (let i = 0; i < invocations; i++) { + for (let i = 0; i < invocationCount; i++) { const trace = tracesWhenAllFlagsEnabled[i]; /** @@ -274,7 +229,7 @@ describe(`Tracer E2E tests, all features with decorator instantiation`, () => { expect(subsegments.get('### myMethod')?.length).toBe(1); expect(subsegments.get('other')?.length).toBe(0); - const shouldThrowAnError = i === invocations - 1; + const shouldThrowAnError = i === invocationCount - 1; if (shouldThrowAnError) { assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage); } @@ -289,12 +244,12 @@ describe(`Tracer E2E tests, all features with decorator instantiation`, () => { const tracesWhenAllFlagsEnabled = await getTraces( xrayClient, startTime, - await getFunctionArn(stsClient, fnNameAllFlagsEnabled), - invocations, + await getFunctionArn(fnNameAllFlagsEnabled), + invocationCount, 4 ); - for (let i = 0; i < invocations; i++) { + for (let i = 0; i < invocationCount; i++) { const trace = tracesWhenAllFlagsEnabled[i]; const invocationSubsegment = getInvocationSubsegment(trace); const handlerSubsegment = getFirstSubsegment(invocationSubsegment); @@ -316,7 +271,7 @@ describe(`Tracer E2E tests, all features with decorator instantiation`, () => { metadata[fnNameAllFlagsEnabled][expectedCustomMetadataKey] ).toEqual(expectedCustomMetadataValue); - const shouldThrowAnError = i === invocations - 1; + const shouldThrowAnError = i === invocationCount - 1; if (!shouldThrowAnError) { // Assert that the metadata object contains the response expect( @@ -334,15 +289,15 @@ describe(`Tracer E2E tests, all features with decorator instantiation`, () => { const tracesWithNoCaptureErrorOrResponse = await getTraces( xrayClient, startTime, - await getFunctionArn(stsClient, fnNameNoCaptureErrorOrResponse), - invocations, + await getFunctionArn(fnNameNoCaptureErrorOrResponse), + invocationCount, 4 ); - expect(tracesWithNoCaptureErrorOrResponse.length).toBe(invocations); + expect(tracesWithNoCaptureErrorOrResponse.length).toBe(invocationCount); // Assess - for (let i = 0; i < invocations; i++) { + for (let i = 0; i < invocationCount; i++) { const trace = tracesWithNoCaptureErrorOrResponse[i]; /** @@ -379,7 +334,7 @@ describe(`Tracer E2E tests, all features with decorator instantiation`, () => { expect(subsegments.get('### myMethod')?.length).toBe(1); expect(subsegments.get('other')?.length).toBe(0); - const shouldThrowAnError = i === invocations - 1; + const shouldThrowAnError = i === invocationCount - 1; if (shouldThrowAnError) { // Assert that the subsegment has the expected fault expect(invocationSubsegment.error).toBe(true); @@ -398,15 +353,15 @@ describe(`Tracer E2E tests, all features with decorator instantiation`, () => { const tracesWithCaptureResponseFalse = await getTraces( xrayClient, startTime, - await getFunctionArn(stsClient, fnNameCaptureResponseOff), - invocations, + await getFunctionArn(fnNameCaptureResponseOff), + invocationCount, 4 ); - expect(tracesWithCaptureResponseFalse.length).toBe(invocations); + expect(tracesWithCaptureResponseFalse.length).toBe(invocationCount); // Assess - for (let i = 0; i < invocations; i++) { + for (let i = 0; i < invocationCount; i++) { const trace = tracesWithCaptureResponseFalse[i]; /** @@ -453,7 +408,7 @@ describe(`Tracer E2E tests, all features with decorator instantiation`, () => { expect(myMethodSegment).toBeDefined(); expect(myMethodSegment).not.toHaveProperty('metadata'); - const shouldThrowAnError = i === invocations - 1; + const shouldThrowAnError = i === invocationCount - 1; if (shouldThrowAnError) { assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage); } @@ -469,15 +424,15 @@ describe(`Tracer E2E tests, all features with decorator instantiation`, () => { const tracesWithTracerDisabled = await getTraces( xrayClient, startTime, - await getFunctionArn(stsClient, fnNameTracerDisabled), - invocations, + await getFunctionArn(fnNameTracerDisabled), + invocationCount, expectedNoOfTraces ); - expect(tracesWithTracerDisabled.length).toBe(invocations); + expect(tracesWithTracerDisabled.length).toBe(invocationCount); // Assess - for (let i = 0; i < invocations; i++) { + for (let i = 0; i < invocationCount; i++) { const trace = tracesWithTracerDisabled[i]; expect(trace.Segments.length).toBe(2); @@ -487,7 +442,7 @@ describe(`Tracer E2E tests, all features with decorator instantiation`, () => { const invocationSubsegment = getInvocationSubsegment(trace); expect(invocationSubsegment?.subsegments).toBeUndefined(); - const shouldThrowAnError = i === invocations - 1; + const shouldThrowAnError = i === invocationCount - 1; if (shouldThrowAnError) { expect(invocationSubsegment.error).toBe(true); } diff --git a/packages/tracer/tests/e2e/allFeatures.manual.test.ts b/packages/tracer/tests/e2e/allFeatures.manual.test.ts index e5599b5dc8..e167cde677 100644 --- a/packages/tracer/tests/e2e/allFeatures.manual.test.ts +++ b/packages/tracer/tests/e2e/allFeatures.manual.test.ts @@ -4,19 +4,15 @@ * @group e2e/tracer/manual */ import { - concatenateResourceName, defaultRuntime, + findAndGetStackOutputValue, generateTestUniqueName, isValidRuntimeKey, - TestNodejsFunction, TestStack, - TEST_RUNTIMES, } from '@aws-lambda-powertools/testing-utils'; -import { STSClient } from '@aws-sdk/client-sts'; import { XRayClient } from '@aws-sdk/client-xray'; -import { RemovalPolicy } from 'aws-cdk-lib'; -import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb'; import { join } from 'path'; +import { functionFactory, tableFactory } from '../helpers/factories'; import { assertAnnotation, assertErrorAndFault, @@ -31,7 +27,6 @@ import { } from '../helpers/tracesUtils'; import type { ParsedTrace } from '../helpers/traceUtils.types'; import { - commonEnvironmentVariables, expectedCustomAnnotationKey, expectedCustomAnnotationValue, expectedCustomErrorMessage, @@ -54,81 +49,58 @@ describe(`Tracer E2E tests, all features with manual instantiation`, () => { const testName = generateTestUniqueName({ testPrefix: RESOURCE_NAME_PREFIX, runtime, - testName: 'AllFeatures-Decorator', + testName: 'AllFeatures-Manual', }); const testStack = new TestStack(testName); // Location of the lambda function code - const lambdaFunctionCodeFile = join( + const lambdaFunctionCodeFilePath = join( __dirname, 'allFeatures.manual.test.functionCode.ts' ); const startTime = new Date(); - const fnNameAllFlagsEnabled = concatenateResourceName({ - testName, - resourceName: 'AllFlagsOn', - }); - /** * Table used by all functions to make an SDK call */ - const ddbTableName = concatenateResourceName({ + const testTable = tableFactory({ + testStack, + testName, + tableSuffix: 'TestTable', + }); + + let fnNameAllFlagsEnabled: string; + const fnAllFlagsEnabled = functionFactory({ + testStack, testName, - resourceName: 'TestTable', + functionSuffix: 'AllFlagsOn', + lambdaFunctionCodeFilePath, + environment: { + TEST_TABLE_NAME: testTable.tableName, + }, }); + testTable.grantWriteData(fnAllFlagsEnabled); const xrayClient = new XRayClient({}); - const stsClient = new STSClient({}); - const invocations = 3; + const invocationCount = 3; let sortedTraces: ParsedTrace[]; beforeAll(async () => { - // Prepare - const ddbTable = new Table(testStack.stack, 'Table', { - tableName: ddbTableName, - partitionKey: { - name: 'id', - type: AttributeType.STRING, - }, - billingMode: BillingMode.PAY_PER_REQUEST, - removalPolicy: RemovalPolicy.DESTROY, - }); - - const fnWithAllFlagsEnabled = new TestNodejsFunction( - testStack.stack, - fnNameAllFlagsEnabled, - { - functionName: fnNameAllFlagsEnabled, - entry: lambdaFunctionCodeFile, - runtime: TEST_RUNTIMES[runtime], - environment: { - TEST_TABLE_NAME: ddbTableName, - POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', - POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', - POWERTOOLS_TRACE_ENABLED: 'true', - EXPECTED_SERVICE_NAME: fnNameAllFlagsEnabled, - ...commonEnvironmentVariables, - }, - } - ); - ddbTable.grantWriteData(fnWithAllFlagsEnabled); + // Deploy the stack + const outputs = await testStack.deploy(); - await testStack.deploy(); + // Get the actual function names from the stack outputs + fnNameAllFlagsEnabled = findAndGetStackOutputValue(outputs, 'AllFlagsOn'); - // Act - await invokeAllTestCases(fnNameAllFlagsEnabled); + // Invoke all test cases + await invokeAllTestCases(fnNameAllFlagsEnabled, invocationCount); // Retrieve traces from X-Ray for assertion - const lambdaFunctionArn = await getFunctionArn( - stsClient, - fnNameAllFlagsEnabled - ); sortedTraces = await getTraces( xrayClient, startTime, - lambdaFunctionArn, - invocations, + await getFunctionArn(fnNameAllFlagsEnabled), + invocationCount, 4 ); }, SETUP_TIMEOUT); @@ -142,10 +114,10 @@ describe(`Tracer E2E tests, all features with manual instantiation`, () => { it( 'should generate all custom traces', async () => { - expect(sortedTraces.length).toBe(invocations); + expect(sortedTraces.length).toBe(invocationCount); // Assess - for (let i = 0; i < invocations; i++) { + for (let i = 0; i < invocationCount; i++) { const trace = sortedTraces[i]; /** @@ -179,7 +151,7 @@ describe(`Tracer E2E tests, all features with manual instantiation`, () => { expect(subsegments.get('docs.powertools.aws.dev')?.length).toBe(1); expect(subsegments.get('other')?.length).toBe(0); - const shouldThrowAnError = i === invocations - 1; + const shouldThrowAnError = i === invocationCount - 1; if (shouldThrowAnError) { assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage); } @@ -191,7 +163,7 @@ describe(`Tracer E2E tests, all features with manual instantiation`, () => { it( 'should have correct annotations and metadata', async () => { - for (let i = 0; i < invocations; i++) { + for (let i = 0; i < invocationCount; i++) { const trace = sortedTraces[i]; const invocationSubsegment = getInvocationSubsegment(trace); const handlerSubsegment = getFirstSubsegment(invocationSubsegment); @@ -213,7 +185,7 @@ describe(`Tracer E2E tests, all features with manual instantiation`, () => { metadata[fnNameAllFlagsEnabled][expectedCustomMetadataKey] ).toEqual(expectedCustomMetadataValue); - const shouldThrowAnError = i === invocations - 1; + const shouldThrowAnError = i === invocationCount - 1; if (!shouldThrowAnError) { // Assert that the metadata object contains the response expect( diff --git a/packages/tracer/tests/e2e/allFeatures.middy.test.ts b/packages/tracer/tests/e2e/allFeatures.middy.test.ts index f63326f107..6dba0ebeb6 100644 --- a/packages/tracer/tests/e2e/allFeatures.middy.test.ts +++ b/packages/tracer/tests/e2e/allFeatures.middy.test.ts @@ -3,20 +3,20 @@ * * @group e2e/tracer/middy */ -import { join } from 'node:path'; import { - concatenateResourceName, defaultRuntime, generateTestUniqueName, isValidRuntimeKey, - TestNodejsFunction, TestStack, - TEST_RUNTIMES, + findAndGetStackOutputValue, } from '@aws-lambda-powertools/testing-utils'; -import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb'; -import { RemovalPolicy } from 'aws-cdk-lib'; import { XRayClient } from '@aws-sdk/client-xray'; -import { STSClient } from '@aws-sdk/client-sts'; +import { join } from 'node:path'; +import { functionFactory, tableFactory } from '../helpers/factories'; +import { + assertAnnotation, + assertErrorAndFault, +} from '../helpers/traceAssertions'; import { getFirstSubsegment, getFunctionArn, @@ -26,7 +26,6 @@ import { splitSegmentsByName, } from '../helpers/tracesUtils'; import { - commonEnvironmentVariables, expectedCustomAnnotationKey, expectedCustomAnnotationValue, expectedCustomErrorMessage, @@ -38,10 +37,6 @@ import { TEARDOWN_TIMEOUT, TEST_CASE_TIMEOUT, } from './constants'; -import { - assertAnnotation, - assertErrorAndFault, -} from '../helpers/traceAssertions'; /** * The test includes one stack with 4 Lambda functions that correspond to the following test cases: @@ -67,153 +62,113 @@ describe(`Tracer E2E tests, all features with middy instantiation`, () => { const testStack = new TestStack(testName); // Location of the lambda function code - const lambdaFunctionCodeFile = join( + const lambdaFunctionCodeFilePath = join( __dirname, 'allFeatures.middy.test.functionCode.ts' ); const startTime = new Date(); /** - * Function #1 is with all flags enabled. + * Table used by all functions to make an SDK call */ - const fnNameAllFlagsEnabled = concatenateResourceName({ + const testTable = tableFactory({ + testStack, testName, - resourceName: 'AllFlagsOn', + tableSuffix: 'TestTable', }); /** - * Function #2 doesn't capture error or response + * Function #1 is with all flags enabled. */ - const fnNameNoCaptureErrorOrResponse = concatenateResourceName({ + let fnNameAllFlagsEnabled: string; + const fnAllFlagsEnabled = functionFactory({ + testStack, testName, - resourceName: 'NoCaptureErrOrResp', + functionSuffix: 'AllFlagsOn', + lambdaFunctionCodeFilePath, + environment: { + TEST_TABLE_NAME: testTable.tableName, + }, }); + testTable.grantWriteData(fnAllFlagsEnabled); /** - * Function #3 disables tracer + * Function #2 doesn't capture error or response */ - const fnNameTracerDisabled = concatenateResourceName({ + let fnNameNoCaptureErrorOrResponse: string; + const fnNoCaptureErrorOrResponse = functionFactory({ + testStack, testName, - resourceName: 'TracerDisabled', + functionSuffix: 'NoCaptureErrOrResp', + lambdaFunctionCodeFilePath, + environment: { + TEST_TABLE_NAME: testTable.tableName, + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'false', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'false', + }, }); + testTable.grantWriteData(fnNoCaptureErrorOrResponse); /** - * Function #4 disables response capture via middleware option + * Function #3 disables tracer */ - const fnNameCaptureResponseOff = concatenateResourceName({ + let fnNameTracerDisabled: string; + const fnTracerDisabled = functionFactory({ + testStack, testName, - resourceName: 'CaptureResponseOff', + functionSuffix: 'TracerDisabled', + lambdaFunctionCodeFilePath, + environment: { + TEST_TABLE_NAME: testTable.tableName, + POWERTOOLS_TRACE_ENABLED: 'false', + }, }); + testTable.grantWriteData(fnTracerDisabled); /** - * Table used by all functions to make an SDK call + * Function #4 disables response capture via middleware option */ - const ddbTableName = concatenateResourceName({ + let fnNameCaptureResponseOff: string; + const fnCaptureResponseOff = functionFactory({ + testStack, testName, - resourceName: 'TestTable', + functionSuffix: 'CaptureResponseOff', + lambdaFunctionCodeFilePath, + handler: 'handlerWithNoCaptureResponseViaMiddlewareOption', + environment: { + TEST_TABLE_NAME: testTable.tableName, + }, }); + testTable.grantWriteData(fnCaptureResponseOff); const xrayClient = new XRayClient({}); - const stsClient = new STSClient({}); - const invocations = 3; + const invocationCount = 3; beforeAll(async () => { - // Prepare - const ddbTable = new Table(testStack.stack, 'Table', { - tableName: ddbTableName, - partitionKey: { - name: 'id', - type: AttributeType.STRING, - }, - billingMode: BillingMode.PAY_PER_REQUEST, - removalPolicy: RemovalPolicy.DESTROY, - }); - - const fnWithAllFlagsEnabled = new TestNodejsFunction( - testStack.stack, - fnNameAllFlagsEnabled, - { - functionName: fnNameAllFlagsEnabled, - entry: lambdaFunctionCodeFile, - runtime: TEST_RUNTIMES[runtime], - environment: { - TEST_TABLE_NAME: ddbTableName, - POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', - POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', - POWERTOOLS_TRACE_ENABLED: 'true', - EXPECTED_SERVICE_NAME: fnNameAllFlagsEnabled, - ...commonEnvironmentVariables, - }, - } + // Deploy the stack + const outputs = await testStack.deploy(); + + // Get the actual function names from the stack outputs + fnNameAllFlagsEnabled = findAndGetStackOutputValue(outputs, 'AllFlagsOn'); + fnNameNoCaptureErrorOrResponse = findAndGetStackOutputValue( + outputs, + 'NoCaptureErrOrResp' ); - ddbTable.grantWriteData(fnWithAllFlagsEnabled); - - const fnThatDoesNotCapturesErrorAndResponse = new TestNodejsFunction( - testStack.stack, - fnNameNoCaptureErrorOrResponse, - { - functionName: fnNameNoCaptureErrorOrResponse, - entry: lambdaFunctionCodeFile, - runtime: TEST_RUNTIMES[runtime], - environment: { - TEST_TABLE_NAME: ddbTableName, - POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'false', - POWERTOOLS_TRACER_CAPTURE_ERROR: 'false', - POWERTOOLS_TRACE_ENABLED: 'true', - EXPECTED_SERVICE_NAME: fnNameNoCaptureErrorOrResponse, - ...commonEnvironmentVariables, - }, - } + fnNameTracerDisabled = findAndGetStackOutputValue( + outputs, + 'TracerDisabled' ); - ddbTable.grantWriteData(fnThatDoesNotCapturesErrorAndResponse); - - const fnWithTracerDisabled = new TestNodejsFunction( - testStack.stack, - fnNameTracerDisabled, - { - functionName: fnNameTracerDisabled, - entry: lambdaFunctionCodeFile, - runtime: TEST_RUNTIMES[runtime], - environment: { - TEST_TABLE_NAME: ddbTableName, - POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', - POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', - POWERTOOLS_TRACE_ENABLED: 'false', - EXPECTED_SERVICE_NAME: fnNameTracerDisabled, - ...commonEnvironmentVariables, - }, - } + fnNameCaptureResponseOff = findAndGetStackOutputValue( + outputs, + 'CaptureResponseOff' ); - ddbTable.grantWriteData(fnWithTracerDisabled); - - const fnWithCaptureResponseFalse = new TestNodejsFunction( - testStack.stack, - fnNameCaptureResponseOff, - { - functionName: fnNameCaptureResponseOff, - handler: 'handlerWithNoCaptureResponseViaMiddlewareOption', - entry: lambdaFunctionCodeFile, - runtime: TEST_RUNTIMES[runtime], - environment: { - TEST_TABLE_NAME: ddbTableName, - POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', - POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', - POWERTOOLS_TRACE_ENABLED: 'true', - EXPECTED_SERVICE_NAME: fnNameCaptureResponseOff, - ...commonEnvironmentVariables, - }, - } - ); - ddbTable.grantWriteData(fnWithCaptureResponseFalse); - - await testStack.deploy(); - // Act + // Invoke all functions await Promise.all([ - invokeAllTestCases(fnNameAllFlagsEnabled), - invokeAllTestCases(fnNameNoCaptureErrorOrResponse), - invokeAllTestCases(fnNameTracerDisabled), - invokeAllTestCases(fnNameCaptureResponseOff), + invokeAllTestCases(fnNameAllFlagsEnabled, invocationCount), + invokeAllTestCases(fnNameNoCaptureErrorOrResponse, invocationCount), + invokeAllTestCases(fnNameTracerDisabled, invocationCount), + invokeAllTestCases(fnNameCaptureResponseOff, invocationCount), ]); }, SETUP_TIMEOUT); @@ -229,15 +184,15 @@ describe(`Tracer E2E tests, all features with middy instantiation`, () => { const tracesWhenAllFlagsEnabled = await getTraces( xrayClient, startTime, - await getFunctionArn(stsClient, fnNameAllFlagsEnabled), - invocations, + await getFunctionArn(fnNameAllFlagsEnabled), + invocationCount, 4 ); - expect(tracesWhenAllFlagsEnabled.length).toBe(invocations); + expect(tracesWhenAllFlagsEnabled.length).toBe(invocationCount); // Assess - for (let i = 0; i < invocations; i++) { + for (let i = 0; i < invocationCount; i++) { const trace = tracesWhenAllFlagsEnabled[i]; /** @@ -271,7 +226,7 @@ describe(`Tracer E2E tests, all features with middy instantiation`, () => { expect(subsegments.get('docs.powertools.aws.dev')?.length).toBe(1); expect(subsegments.get('other')?.length).toBe(0); - const shouldThrowAnError = i === invocations - 1; + const shouldThrowAnError = i === invocationCount - 1; if (shouldThrowAnError) { assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage); } @@ -286,12 +241,12 @@ describe(`Tracer E2E tests, all features with middy instantiation`, () => { const tracesWhenAllFlagsEnabled = await getTraces( xrayClient, startTime, - await getFunctionArn(stsClient, fnNameAllFlagsEnabled), - invocations, + await getFunctionArn(fnNameAllFlagsEnabled), + invocationCount, 4 ); - for (let i = 0; i < invocations; i++) { + for (let i = 0; i < invocationCount; i++) { const trace = tracesWhenAllFlagsEnabled[i]; const invocationSubsegment = getInvocationSubsegment(trace); const handlerSubsegment = getFirstSubsegment(invocationSubsegment); @@ -313,7 +268,7 @@ describe(`Tracer E2E tests, all features with middy instantiation`, () => { metadata[fnNameAllFlagsEnabled][expectedCustomMetadataKey] ).toEqual(expectedCustomMetadataValue); - const shouldThrowAnError = i === invocations - 1; + const shouldThrowAnError = i === invocationCount - 1; if (!shouldThrowAnError) { // Assert that the metadata object contains the response expect( @@ -331,15 +286,15 @@ describe(`Tracer E2E tests, all features with middy instantiation`, () => { const tracesWithNoCaptureErrorOrResponse = await getTraces( xrayClient, startTime, - await getFunctionArn(stsClient, fnNameNoCaptureErrorOrResponse), - invocations, + await getFunctionArn(fnNameNoCaptureErrorOrResponse), + invocationCount, 4 ); - expect(tracesWithNoCaptureErrorOrResponse.length).toBe(invocations); + expect(tracesWithNoCaptureErrorOrResponse.length).toBe(invocationCount); // Assess - for (let i = 0; i < invocations; i++) { + for (let i = 0; i < invocationCount; i++) { const trace = tracesWithNoCaptureErrorOrResponse[i]; /** @@ -373,7 +328,7 @@ describe(`Tracer E2E tests, all features with middy instantiation`, () => { expect(subsegments.get('docs.powertools.aws.dev')?.length).toBe(1); expect(subsegments.get('other')?.length).toBe(0); - const shouldThrowAnError = i === invocations - 1; + const shouldThrowAnError = i === invocationCount - 1; if (shouldThrowAnError) { // Assert that the subsegment has the expected fault expect(invocationSubsegment.error).toBe(true); @@ -392,15 +347,15 @@ describe(`Tracer E2E tests, all features with middy instantiation`, () => { const tracesWithNoCaptureResponse = await getTraces( xrayClient, startTime, - await getFunctionArn(stsClient, fnNameCaptureResponseOff), - invocations, + await getFunctionArn(fnNameCaptureResponseOff), + invocationCount, 4 ); - expect(tracesWithNoCaptureResponse.length).toBe(invocations); + expect(tracesWithNoCaptureResponse.length).toBe(invocationCount); // Assess - for (let i = 0; i < invocations; i++) { + for (let i = 0; i < invocationCount; i++) { const trace = tracesWithNoCaptureResponse[i]; /** @@ -438,7 +393,7 @@ describe(`Tracer E2E tests, all features with middy instantiation`, () => { expect(subsegments.get('docs.powertools.aws.dev')?.length).toBe(1); expect(subsegments.get('other')?.length).toBe(0); - const shouldThrowAnError = i === invocations - 1; + const shouldThrowAnError = i === invocationCount - 1; if (shouldThrowAnError) { assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage); } @@ -454,15 +409,15 @@ describe(`Tracer E2E tests, all features with middy instantiation`, () => { const tracesWithTracerDisabled = await getTraces( xrayClient, startTime, - await getFunctionArn(stsClient, fnNameTracerDisabled), - invocations, + await getFunctionArn(fnNameTracerDisabled), + invocationCount, expectedNoOfTraces ); - expect(tracesWithTracerDisabled.length).toBe(invocations); + expect(tracesWithTracerDisabled.length).toBe(invocationCount); // Assess - for (let i = 0; i < invocations; i++) { + for (let i = 0; i < invocationCount; i++) { const trace = tracesWithTracerDisabled[i]; expect(trace.Segments.length).toBe(2); @@ -472,7 +427,7 @@ describe(`Tracer E2E tests, all features with middy instantiation`, () => { const invocationSubsegment = getInvocationSubsegment(trace); expect(invocationSubsegment?.subsegments).toBeUndefined(); - const shouldThrowAnError = i === invocations - 1; + const shouldThrowAnError = i === invocationCount - 1; if (shouldThrowAnError) { expect(invocationSubsegment.error).toBe(true); } diff --git a/packages/tracer/tests/e2e/asyncHandler.decorator.test.ts b/packages/tracer/tests/e2e/asyncHandler.decorator.test.ts index 336b6d1817..8b2c49c13b 100644 --- a/packages/tracer/tests/e2e/asyncHandler.decorator.test.ts +++ b/packages/tracer/tests/e2e/asyncHandler.decorator.test.ts @@ -4,19 +4,15 @@ * @group e2e/tracer/decorator-async-handler */ import { - concatenateResourceName, defaultRuntime, + findAndGetStackOutputValue, generateTestUniqueName, isValidRuntimeKey, - TestNodejsFunction, TestStack, - TEST_RUNTIMES, } from '@aws-lambda-powertools/testing-utils'; -import { STSClient } from '@aws-sdk/client-sts'; import { XRayClient } from '@aws-sdk/client-xray'; -import { RemovalPolicy } from 'aws-cdk-lib'; -import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb'; import { join } from 'node:path'; +import { functionFactory, tableFactory } from '../helpers/factories'; import { assertAnnotation, assertErrorAndFault, @@ -30,7 +26,6 @@ import { splitSegmentsByName, } from '../helpers/tracesUtils'; import { - commonEnvironmentVariables, expectedCustomAnnotationKey, expectedCustomAnnotationValue, expectedCustomErrorMessage, @@ -59,98 +54,71 @@ describe(`Tracer E2E tests, async handler with decorator instantiation`, () => { const testStack = new TestStack(testName); // Location of the lambda function code - const lambdaFunctionCodeFile = join( + const lambdaFunctionCodeFilePath = join( __dirname, 'asyncHandler.decorator.test.functionCode.ts' ); const startTime = new Date(); /** - * Function #1 is with all flags enabled. + * Table used by all functions to make an SDK call */ - const fnNameAllFlagsEnabled = concatenateResourceName({ + const testTable = tableFactory({ + testStack, testName, - resourceName: 'AllFlagsOn', + tableSuffix: 'TestTable', }); /** - * Function #2 sets a custom subsegment name in the decorated method + * Function #1 is with all flags enabled. */ - const fnNameCustomSubsegment = concatenateResourceName({ + let fnNameAllFlagsEnabled: string; + const fnAllFlagsEnabled = functionFactory({ + testStack, testName, - resourceName: 'CustomSubsegmentName', + functionSuffix: 'AllFlagsOn', + lambdaFunctionCodeFilePath, + environment: { + TEST_TABLE_NAME: testTable.tableName, + }, }); + testTable.grantWriteData(fnAllFlagsEnabled); /** - * Table used by all functions to make an SDK call + * Function #2 sets a custom subsegment name in the decorated method */ - const ddbTableName = concatenateResourceName({ + let fnNameCustomSubsegment: string; + const fnCustomSubsegmentName = functionFactory({ + testStack, testName, - resourceName: 'TestTable', + functionSuffix: 'CustomSubsegmentName', + lambdaFunctionCodeFilePath, + handler: 'handlerWithCustomSubsegmentNameInMethod', + environment: { + TEST_TABLE_NAME: testTable.tableName, + EXPECTED_CUSTOM_SUBSEGMENT_NAME: expectedCustomSubSegmentName, + }, }); + testTable.grantWriteData(fnCustomSubsegmentName); const xrayClient = new XRayClient({}); - const stsClient = new STSClient({}); - const invocations = 3; + const invocationsCount = 3; beforeAll(async () => { - // Prepare - const ddbTable = new Table(testStack.stack, 'Table', { - tableName: ddbTableName, - partitionKey: { - name: 'id', - type: AttributeType.STRING, - }, - billingMode: BillingMode.PAY_PER_REQUEST, - removalPolicy: RemovalPolicy.DESTROY, - }); - - const fnWithAllFlagsEnabled = new TestNodejsFunction( - testStack.stack, - fnNameAllFlagsEnabled, - { - functionName: fnNameAllFlagsEnabled, - entry: lambdaFunctionCodeFile, - runtime: TEST_RUNTIMES[runtime], - environment: { - TEST_TABLE_NAME: ddbTableName, - POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', - POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', - POWERTOOLS_TRACE_ENABLED: 'true', - EXPECTED_SERVICE_NAME: fnNameAllFlagsEnabled, - ...commonEnvironmentVariables, - }, - } + // Deploy the stack + const outputs = await testStack.deploy(); + + // Get the actual function names from the stack outputs + fnNameAllFlagsEnabled = findAndGetStackOutputValue(outputs, 'AllFlagsOn'); + fnNameCustomSubsegment = findAndGetStackOutputValue( + outputs, + 'CustomSubsegmentName' ); - ddbTable.grantWriteData(fnWithAllFlagsEnabled); - - const fnWithCustomSubsegmentName = new TestNodejsFunction( - testStack.stack, - fnNameCustomSubsegment, - { - functionName: fnNameCustomSubsegment, - entry: lambdaFunctionCodeFile, - runtime: TEST_RUNTIMES[runtime], - handler: 'handlerWithCustomSubsegmentNameInMethod', - environment: { - TEST_TABLE_NAME: ddbTableName, - POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', - POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', - POWERTOOLS_TRACE_ENABLED: 'true', - EXPECTED_SERVICE_NAME: fnNameCustomSubsegment, - EXPECTED_CUSTOM_SUBSEGMENT_NAME: expectedCustomSubSegmentName, - ...commonEnvironmentVariables, - }, - } - ); - ddbTable.grantWriteData(fnWithCustomSubsegmentName); - - await testStack.deploy(); // Act await Promise.all([ - invokeAllTestCases(fnNameAllFlagsEnabled), - invokeAllTestCases(fnNameCustomSubsegment), + invokeAllTestCases(fnNameAllFlagsEnabled, invocationsCount), + invokeAllTestCases(fnNameCustomSubsegment, invocationsCount), ]); }, SETUP_TIMEOUT); @@ -166,15 +134,15 @@ describe(`Tracer E2E tests, async handler with decorator instantiation`, () => { const tracesWhenAllFlagsEnabled = await getTraces( xrayClient, startTime, - await getFunctionArn(stsClient, fnNameAllFlagsEnabled), - invocations, + await getFunctionArn(fnNameAllFlagsEnabled), + invocationsCount, 4 ); - expect(tracesWhenAllFlagsEnabled.length).toBe(invocations); + expect(tracesWhenAllFlagsEnabled.length).toBe(invocationsCount); // Assess - for (let i = 0; i < invocations; i++) { + for (let i = 0; i < invocationsCount; i++) { const trace = tracesWhenAllFlagsEnabled[i]; /** @@ -211,7 +179,7 @@ describe(`Tracer E2E tests, async handler with decorator instantiation`, () => { expect(subsegments.get('### myMethod')?.length).toBe(1); expect(subsegments.get('other')?.length).toBe(0); - const shouldThrowAnError = i === invocations - 1; + const shouldThrowAnError = i === invocationsCount - 1; if (shouldThrowAnError) { assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage); } @@ -226,12 +194,12 @@ describe(`Tracer E2E tests, async handler with decorator instantiation`, () => { const traces = await getTraces( xrayClient, startTime, - await getFunctionArn(stsClient, fnNameAllFlagsEnabled), - invocations, + await getFunctionArn(fnNameAllFlagsEnabled), + invocationsCount, 4 ); - for (let i = 0; i < invocations; i++) { + for (let i = 0; i < invocationsCount; i++) { const trace = traces[i]; const invocationSubsegment = getInvocationSubsegment(trace); const handlerSubsegment = getFirstSubsegment(invocationSubsegment); @@ -253,7 +221,7 @@ describe(`Tracer E2E tests, async handler with decorator instantiation`, () => { metadata[fnNameAllFlagsEnabled][expectedCustomMetadataKey] ).toEqual(expectedCustomMetadataValue); - const shouldThrowAnError = i === invocations - 1; + const shouldThrowAnError = i === invocationsCount - 1; if (!shouldThrowAnError) { // Assert that the metadata object contains the response expect( @@ -271,15 +239,17 @@ describe(`Tracer E2E tests, async handler with decorator instantiation`, () => { const tracesWhenCustomSubsegmentNameInMethod = await getTraces( xrayClient, startTime, - await getFunctionArn(stsClient, fnNameCustomSubsegment), - invocations, + await getFunctionArn(fnNameCustomSubsegment), + invocationsCount, 4 ); - expect(tracesWhenCustomSubsegmentNameInMethod.length).toBe(invocations); + expect(tracesWhenCustomSubsegmentNameInMethod.length).toBe( + invocationsCount + ); // Assess - for (let i = 0; i < invocations; i++) { + for (let i = 0; i < invocationsCount; i++) { const trace = tracesWhenCustomSubsegmentNameInMethod[i]; /** @@ -318,7 +288,7 @@ describe(`Tracer E2E tests, async handler with decorator instantiation`, () => { expect(subsegments.get(expectedCustomSubSegmentName)?.length).toBe(1); expect(subsegments.get('other')?.length).toBe(0); - const shouldThrowAnError = i === invocations - 1; + const shouldThrowAnError = i === invocationsCount - 1; if (shouldThrowAnError) { assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage); } diff --git a/packages/tracer/tests/helpers/factories.ts b/packages/tracer/tests/helpers/factories.ts new file mode 100644 index 0000000000..90ea11da99 --- /dev/null +++ b/packages/tracer/tests/helpers/factories.ts @@ -0,0 +1,80 @@ +import type { TestStack } from '@aws-lambda-powertools/testing-utils'; +import { + concatenateResourceName, + defaultRuntime, + TestNodejsFunction, + TEST_RUNTIMES, +} from '@aws-lambda-powertools/testing-utils'; +import { RemovalPolicy } from 'aws-cdk-lib'; +import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb'; +import { randomUUID } from 'node:crypto'; +import { commonEnvironmentVariables } from '../e2e/constants'; + +const functionFactory = ({ + testStack, + testName, + functionSuffix, + lambdaFunctionCodeFilePath, + handler, + environment = {}, +}: { + testStack: TestStack; + testName: string; + functionSuffix: string; + lambdaFunctionCodeFilePath: string; + handler?: string; + environment?: Record; +}): TestNodejsFunction => { + const runtime: string = process.env.RUNTIME || defaultRuntime; + + const functionName = concatenateResourceName({ + testName, + resourceName: functionSuffix, + }); + + return new TestNodejsFunction( + testStack.stack, + `fn-${randomUUID().substring(0, 5)}`, + { + functionName, + entry: lambdaFunctionCodeFilePath, + runtime: TEST_RUNTIMES[runtime as keyof typeof TEST_RUNTIMES], + handler, + environment: { + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', + POWERTOOLS_TRACE_ENABLED: 'true', + EXPECTED_SERVICE_NAME: functionName, + ...commonEnvironmentVariables, + ...environment, + }, + }, + { + fnOutputKey: functionSuffix, + } + ); +}; + +const tableFactory = ({ + testStack, + testName, + tableSuffix, +}: { + testStack: TestStack; + testName: string; + tableSuffix: string; +}): Table => + new Table(testStack.stack, `table-${randomUUID().substring(0, 5)}`, { + tableName: concatenateResourceName({ + testName, + resourceName: tableSuffix, + }), + partitionKey: { + name: 'id', + type: AttributeType.STRING, + }, + billingMode: BillingMode.PAY_PER_REQUEST, + removalPolicy: RemovalPolicy.DESTROY, + }); + +export { functionFactory, tableFactory }; diff --git a/packages/tracer/tests/helpers/tracesUtils.ts b/packages/tracer/tests/helpers/tracesUtils.ts index 3bbd13a022..daa7d6cc70 100644 --- a/packages/tracer/tests/helpers/tracesUtils.ts +++ b/packages/tracer/tests/helpers/tracesUtils.ts @@ -4,9 +4,9 @@ import { BatchGetTracesCommand, GetTraceSummariesCommand, } from '@aws-sdk/client-xray'; -import type { STSClient } from '@aws-sdk/client-sts'; +import { STSClient } from '@aws-sdk/client-sts'; import { GetCallerIdentityCommand } from '@aws-sdk/client-sts'; -import { invokeFunctionOnce } from '@aws-lambda-powertools/testing-utils'; +import { invokeFunction } from '@aws-lambda-powertools/testing-utils'; import { FunctionSegmentNotDefinedError } from './FunctionSegmentNotDefinedError'; import type { ParsedDocument, @@ -212,37 +212,36 @@ const splitSegmentsByName = ( * * @param functionName */ -const invokeAllTestCases = async (functionName: string): Promise => { - await invokeFunctionOnce({ +const invokeAllTestCases = async ( + functionName: string, + times: number +): Promise => { + await invokeFunction({ functionName, - payload: { - invocation: 1, - throw: false, - }, - }); - await invokeFunctionOnce({ - functionName, - payload: { - invocation: 2, - throw: false, - }, - }); - await invokeFunctionOnce({ - functionName, - payload: { - invocation: 3, - throw: true, // only last invocation should throw - }, + times, + invocationMode: 'SEQUENTIAL', + payload: [ + { + invocation: 1, + throw: false, + }, + { + invocation: 2, + throw: false, + }, + { + invocation: 3, + throw: true, // only last invocation should throw + }, + ], }); }; let account: string | undefined; -const getFunctionArn = async ( - stsClient: STSClient, - functionName: string -): Promise => { +const getFunctionArn = async (functionName: string): Promise => { const region = process.env.AWS_REGION; if (!account) { + const stsClient = new STSClient({}); const identity = await stsClient.send(new GetCallerIdentityCommand({})); account = identity.Account; } From d6aa2e2e000ae44e426b804d54370196d73e35fc Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 30 Aug 2023 10:00:22 +0200 Subject: [PATCH 04/18] chore: add sonarcloud properties temp --- .sonarcloud.properties | 1 + 1 file changed, 1 insertion(+) create mode 100644 .sonarcloud.properties diff --git a/.sonarcloud.properties b/.sonarcloud.properties new file mode 100644 index 0000000000..04ef15d77e --- /dev/null +++ b/.sonarcloud.properties @@ -0,0 +1 @@ +sonar.cpd.exclusions=examples/**/*,packages/*/tests/**/* \ No newline at end of file From 3fba31ef8eed560ef73945b5b5b46a4c46489e96 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 30 Aug 2023 10:10:21 +0200 Subject: [PATCH 05/18] chore: add sonarcloud properties temp --- .sonarcloud.properties | 2 +- packages/testing/src/resources/TestNodejsFunction.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.sonarcloud.properties b/.sonarcloud.properties index 04ef15d77e..ae1170e220 100644 --- a/.sonarcloud.properties +++ b/.sonarcloud.properties @@ -1 +1 @@ -sonar.cpd.exclusions=examples/**/*,packages/*/tests/**/* \ No newline at end of file +sonar.cpd.exclusions=examples/**/*,packages/*/tests/**/*,layers/tests/**/* \ No newline at end of file diff --git a/packages/testing/src/resources/TestNodejsFunction.ts b/packages/testing/src/resources/TestNodejsFunction.ts index c95040d906..d65c70de03 100644 --- a/packages/testing/src/resources/TestNodejsFunction.ts +++ b/packages/testing/src/resources/TestNodejsFunction.ts @@ -41,6 +41,9 @@ class TestNodejsFunction extends NodejsFunction { value: this.functionName, }); } + new CfnOutput(this, 'fnName', { + value: this.functionName, + }); } } From 57de11ba5fc92ffc58f799d156955820a7be0ea8 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 30 Aug 2023 11:14:44 +0200 Subject: [PATCH 06/18] chore: remove sonarcloud properties --- .sonarcloud.properties | 1 - packages/testing/src/resources/TestNodejsFunction.ts | 3 --- 2 files changed, 4 deletions(-) delete mode 100644 .sonarcloud.properties diff --git a/.sonarcloud.properties b/.sonarcloud.properties deleted file mode 100644 index ae1170e220..0000000000 --- a/.sonarcloud.properties +++ /dev/null @@ -1 +0,0 @@ -sonar.cpd.exclusions=examples/**/*,packages/*/tests/**/*,layers/tests/**/* \ No newline at end of file diff --git a/packages/testing/src/resources/TestNodejsFunction.ts b/packages/testing/src/resources/TestNodejsFunction.ts index d65c70de03..c95040d906 100644 --- a/packages/testing/src/resources/TestNodejsFunction.ts +++ b/packages/testing/src/resources/TestNodejsFunction.ts @@ -41,9 +41,6 @@ class TestNodejsFunction extends NodejsFunction { value: this.functionName, }); } - new CfnOutput(this, 'fnName', { - value: this.functionName, - }); } } From 2e5fbc6040db02d4496fab4ddc2d999887df9dfc Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Wed, 30 Aug 2023 20:13:48 +0200 Subject: [PATCH 07/18] improv: refactor tests to remove duplication --- .../tests/e2e/makeHandlerIdempotent.test.ts | 190 +++++++++--------- .../tests/e2e/makeIdempotent.test.ts | 176 ++++++++-------- .../tests/helpers/idempotencyUtils.ts | 44 ---- .../idempotency/tests/helpers/resources.ts | 54 +++++ .../tests/e2e/basicFeatures.middy.test.ts | 164 +++++++-------- .../tests/e2e/childLogger.manual.test.ts | 110 ++++------ packages/logger/tests/e2e/constants.ts | 45 ++++- .../e2e/logEventEnvVarSetting.middy.test.ts | 60 ++---- .../tests/e2e/sampleRate.decorator.test.ts | 87 +++----- packages/logger/tests/helpers/resources.ts | 40 ++++ .../e2e/basicFeatures.decorators.test.ts | 87 ++++---- .../tests/e2e/basicFeatures.manual.test.ts | 81 ++++---- packages/metrics/tests/e2e/constants.ts | 45 ++--- packages/metrics/tests/helpers/resources.ts | 38 ++++ packages/testing/src/TestStack.ts | 67 +++++- packages/testing/src/helpers.ts | 13 +- .../src/resources/TestDynamodbTable.ts | 39 ++++ .../src/resources/TestNodejsFunction.ts | 44 ++-- packages/testing/src/resources/index.ts | 1 + packages/testing/src/resources/types.ts | 31 +++ .../tests/e2e/allFeatures.decorator.test.ts | 166 +++++++-------- .../tests/e2e/allFeatures.manual.test.ts | 86 ++++---- .../tests/e2e/allFeatures.middy.test.ts | 166 +++++++-------- .../tests/e2e/asyncHandler.decorator.test.ts | 115 ++++++----- packages/tracer/tests/e2e/constants.ts | 34 ++-- packages/tracer/tests/helpers/factories.ts | 80 -------- packages/tracer/tests/helpers/resources.ts | 36 ++++ 27 files changed, 1094 insertions(+), 1005 deletions(-) create mode 100644 packages/idempotency/tests/helpers/resources.ts create mode 100644 packages/logger/tests/helpers/resources.ts create mode 100644 packages/metrics/tests/helpers/resources.ts create mode 100644 packages/testing/src/resources/TestDynamodbTable.ts create mode 100644 packages/testing/src/resources/types.ts delete mode 100644 packages/tracer/tests/helpers/factories.ts create mode 100644 packages/tracer/tests/helpers/resources.ts diff --git a/packages/idempotency/tests/e2e/makeHandlerIdempotent.test.ts b/packages/idempotency/tests/e2e/makeHandlerIdempotent.test.ts index bf433cc6b1..1af54e589d 100644 --- a/packages/idempotency/tests/e2e/makeHandlerIdempotent.test.ts +++ b/packages/idempotency/tests/e2e/makeHandlerIdempotent.test.ts @@ -4,19 +4,16 @@ * @group e2e/idempotency/makeHandlerIdempotent */ import { - concatenateResourceName, - defaultRuntime, - generateTestUniqueName, invokeFunction, - isValidRuntimeKey, TestInvocationLogs, TestStack, } from '@aws-lambda-powertools/testing-utils'; import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; import { ScanCommand } from '@aws-sdk/lib-dynamodb'; +import { Duration } from 'aws-cdk-lib'; import { createHash } from 'node:crypto'; import { join } from 'node:path'; -import { createIdempotencyResources } from '../helpers/idempotencyUtils'; +import { IdempotencyTestNodejsFunctionAndDynamoTable } from '../helpers/resources'; import { RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, @@ -24,106 +21,99 @@ import { TEST_CASE_TIMEOUT, } from './constants'; -const runtime: string = process.env.RUNTIME || defaultRuntime; - -if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key value: ${runtime}`); -} - -const testName = generateTestUniqueName({ - testPrefix: RESOURCE_NAME_PREFIX, - runtime, - testName: 'makeHandlerIdempotent', -}); -const testStack = new TestStack(testName); +const ddb = new DynamoDBClient({}); -// Location of the lambda function code -const lambdaFunctionCodeFile = join( - __dirname, - 'makeHandlerIdempotent.test.FunctionCode.ts' -); +describe(`Idempotency E2E tests, middy middleware usage`, () => { + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'makeHandlerIdempotent', + }, + }); -const testDefault = 'default-sequential'; -const functionNameDefault = concatenateResourceName({ - testName, - resourceName: `${testDefault}-fn`, -}); -const ddbTableNameDefault = concatenateResourceName({ - testName, - resourceName: `${testDefault}-table`, -}); -createIdempotencyResources( - testStack.stack, - runtime, - ddbTableNameDefault, - lambdaFunctionCodeFile, - functionNameDefault, - 'handler' -); + // Location of the lambda function code + const lambdaFunctionCodeFilePath = join( + __dirname, + 'makeHandlerIdempotent.test.FunctionCode.ts' + ); -const testDefaultParallel = 'default-parallel'; -const functionNameDefaultParallel = concatenateResourceName({ - testName, - resourceName: `${testDefaultParallel}-fn`, -}); -const ddbTableNameDefaultParallel = concatenateResourceName({ - testName, - resourceName: `${testDefaultParallel}-table`, -}); -createIdempotencyResources( - testStack.stack, - runtime, - ddbTableNameDefaultParallel, - lambdaFunctionCodeFile, - functionNameDefaultParallel, - 'handlerParallel' -); + let functionNameDefault: string; + let tableNameDefault: string; + new IdempotencyTestNodejsFunctionAndDynamoTable( + testStack, + { + function: { + entry: lambdaFunctionCodeFilePath, + }, + }, + { + nameSuffix: 'default', + } + ); -const testTimeout = 'timeout'; -const functionNameTimeout = concatenateResourceName({ - testName, - resourceName: `${testTimeout}-fn`, -}); -const ddbTableNameTimeout = concatenateResourceName({ - testName, - resourceName: `${testTimeout}-table`, -}); -createIdempotencyResources( - testStack.stack, - runtime, - ddbTableNameTimeout, - lambdaFunctionCodeFile, - functionNameTimeout, - 'handlerTimeout', - undefined, - 2 -); + let functionNameDefaultParallel: string; + let tableNameDefaultParallel: string; + new IdempotencyTestNodejsFunctionAndDynamoTable( + testStack, + { + function: { + entry: lambdaFunctionCodeFilePath, + handler: 'handlerParallel', + }, + }, + { + nameSuffix: 'defaultParallel', + } + ); -const testExpired = 'expired'; -const functionNameExpired = concatenateResourceName({ - testName, - resourceName: `${testExpired}-fn`, -}); -const ddbTableNameExpired = concatenateResourceName({ - testName, - resourceName: `${testExpired}-table`, -}); -createIdempotencyResources( - testStack.stack, - runtime, - ddbTableNameExpired, - lambdaFunctionCodeFile, - functionNameExpired, - 'handlerExpired', - undefined, - 2 -); + let functionNameTimeout: string; + let tableNameTimeout: string; + new IdempotencyTestNodejsFunctionAndDynamoTable( + testStack, + { + function: { + entry: lambdaFunctionCodeFilePath, + handler: 'handlerTimeout', + timeout: Duration.seconds(2), + }, + }, + { + nameSuffix: 'timeout', + } + ); -const ddb = new DynamoDBClient({}); + let functionNameExpired: string; + let tableNameExpired: string; + new IdempotencyTestNodejsFunctionAndDynamoTable( + testStack, + { + function: { + entry: lambdaFunctionCodeFilePath, + handler: 'handlerExpired', + timeout: Duration.seconds(2), + }, + }, + { + nameSuffix: 'expired', + } + ); -describe(`Idempotency E2E tests, middy middleware usage`, () => { beforeAll(async () => { + // Deploy the stack await testStack.deploy(); + + // Get the actual function names from the stack outputs + functionNameDefault = testStack.findAndGetStackOutputValue('defaultFn'); + tableNameDefault = testStack.findAndGetStackOutputValue('defaultTable'); + functionNameDefaultParallel = + testStack.findAndGetStackOutputValue('defaultParallelFn'); + tableNameDefaultParallel = testStack.findAndGetStackOutputValue( + 'defaultParallelTable' + ); + functionNameTimeout = testStack.findAndGetStackOutputValue('timeoutFn'); + tableNameTimeout = testStack.findAndGetStackOutputValue('timeoutTable'); + functionNameExpired = testStack.findAndGetStackOutputValue('expiredFn'); + tableNameExpired = testStack.findAndGetStackOutputValue('expiredTable'); }, SETUP_TIMEOUT); test( @@ -149,7 +139,7 @@ describe(`Idempotency E2E tests, middy middleware usage`, () => { // Assess const idempotencyRecords = await ddb.send( new ScanCommand({ - TableName: ddbTableNameDefault, + TableName: tableNameDefault, }) ); expect(idempotencyRecords.Items?.length).toEqual(1); @@ -199,7 +189,7 @@ describe(`Idempotency E2E tests, middy middleware usage`, () => { // Assess const idempotencyRecords = await ddb.send( new ScanCommand({ - TableName: ddbTableNameDefaultParallel, + TableName: tableNameDefaultParallel, }) ); expect(idempotencyRecords.Items?.length).toEqual(1); @@ -260,7 +250,7 @@ describe(`Idempotency E2E tests, middy middleware usage`, () => { // Assess const idempotencyRecords = await ddb.send( new ScanCommand({ - TableName: ddbTableNameTimeout, + TableName: tableNameTimeout, }) ); expect(idempotencyRecords.Items?.length).toEqual(1); @@ -329,7 +319,7 @@ describe(`Idempotency E2E tests, middy middleware usage`, () => { // Assess const idempotencyRecords = await ddb.send( new ScanCommand({ - TableName: ddbTableNameExpired, + TableName: tableNameExpired, }) ); expect(idempotencyRecords.Items?.length).toEqual(1); diff --git a/packages/idempotency/tests/e2e/makeIdempotent.test.ts b/packages/idempotency/tests/e2e/makeIdempotent.test.ts index 3dd801ec78..ff7c890f59 100644 --- a/packages/idempotency/tests/e2e/makeIdempotent.test.ts +++ b/packages/idempotency/tests/e2e/makeIdempotent.test.ts @@ -4,105 +4,105 @@ * @group e2e/idempotency/makeIdempotent */ import { - RESOURCE_NAME_PREFIX, - SETUP_TIMEOUT, - TEARDOWN_TIMEOUT, - TEST_CASE_TIMEOUT, -} from './constants'; -import { join } from 'node:path'; -import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; -import { createHash } from 'node:crypto'; -import { - concatenateResourceName, - defaultRuntime, - generateTestUniqueName, invokeFunction, - isValidRuntimeKey, TestInvocationLogs, TestStack, } from '@aws-lambda-powertools/testing-utils'; +import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; import { ScanCommand } from '@aws-sdk/lib-dynamodb'; -import { createIdempotencyResources } from '../helpers/idempotencyUtils'; - -const runtime: string = process.env.RUNTIME || defaultRuntime; - -if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key value: ${runtime}`); -} +import { AttributeType } from 'aws-cdk-lib/aws-dynamodb'; +import { createHash } from 'node:crypto'; +import { join } from 'node:path'; +import { IdempotencyTestNodejsFunctionAndDynamoTable } from '../helpers/resources'; +import { + RESOURCE_NAME_PREFIX, + SETUP_TIMEOUT, + TEARDOWN_TIMEOUT, + TEST_CASE_TIMEOUT, +} from './constants'; -const testName = generateTestUniqueName({ - testPrefix: RESOURCE_NAME_PREFIX, - runtime, - testName: 'makeFnIdempotent', -}); -const testStack = new TestStack(testName); +describe(`Idempotency E2E tests, wrapper function usage`, () => { + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'makeFnIdempotent', + }, + }); -// Location of the lambda function code -const lambdaFunctionCodeFile = join( - __dirname, - 'makeIdempotent.test.FunctionCode.ts' -); + // Location of the lambda function code + const lambdaFunctionCodeFilePath = join( + __dirname, + 'makeIdempotent.test.FunctionCode.ts' + ); -const testDefault = 'default'; -const functionNameDefault = concatenateResourceName({ - testName, - resourceName: `${testDefault}-fn`, -}); -const ddbTableNameDefault = concatenateResourceName({ - testName, - resourceName: `${testDefault}-table`, -}); -createIdempotencyResources( - testStack.stack, - runtime, - ddbTableNameDefault, - lambdaFunctionCodeFile, - functionNameDefault, - 'handlerDefault' -); + let functionNameDefault: string; + let tableNameDefault: string; + new IdempotencyTestNodejsFunctionAndDynamoTable( + testStack, + { + function: { + entry: lambdaFunctionCodeFilePath, + handler: 'handlerDefault', + }, + }, + { + nameSuffix: 'default', + } + ); -const testCustomConfig = 'customConfig'; -const functionNameCustomConfig = concatenateResourceName({ - testName, - resourceName: `${testCustomConfig}-fn`, -}); -const ddbTableNameCustomConfig = concatenateResourceName({ - testName, - resourceName: `${testCustomConfig}-table`, -}); -createIdempotencyResources( - testStack.stack, - runtime, - ddbTableNameCustomConfig, - lambdaFunctionCodeFile, - functionNameCustomConfig, - 'handlerCustomized', - 'customId' -); + let functionNameCustomConfig: string; + let tableNameCustomConfig: string; + new IdempotencyTestNodejsFunctionAndDynamoTable( + testStack, + { + function: { + entry: lambdaFunctionCodeFilePath, + handler: 'handlerCustomized', + }, + table: { + partitionKey: { + name: 'customId', + type: AttributeType.STRING, + }, + }, + }, + { + nameSuffix: 'customConfig', + } + ); -const testLambdaHandler = 'handler'; -const functionNameLambdaHandler = concatenateResourceName({ - testName, - resourceName: `${testLambdaHandler}-fn`, -}); -const ddbTableNameLambdaHandler = concatenateResourceName({ - testName, - resourceName: `${testLambdaHandler}-table`, -}); -createIdempotencyResources( - testStack.stack, - runtime, - ddbTableNameLambdaHandler, - lambdaFunctionCodeFile, - functionNameLambdaHandler, - 'handlerLambda' -); + let functionNameLambdaHandler: string; + let tableNameLambdaHandler: string; + new IdempotencyTestNodejsFunctionAndDynamoTable( + testStack, + { + function: { + entry: lambdaFunctionCodeFilePath, + handler: 'handlerLambda', + }, + }, + { + nameSuffix: 'handler', + } + ); -const ddb = new DynamoDBClient({}); + const ddb = new DynamoDBClient({}); -describe(`Idempotency E2E tests, wrapper function usage`, () => { beforeAll(async () => { + // Deploy the stack await testStack.deploy(); + + // Get the actual function names from the stack outputs + functionNameDefault = testStack.findAndGetStackOutputValue('defaultFn'); + tableNameDefault = testStack.findAndGetStackOutputValue('defaultTable'); + functionNameCustomConfig = + testStack.findAndGetStackOutputValue('customConfigFn'); + tableNameCustomConfig = + testStack.findAndGetStackOutputValue('customConfigTable'); + functionNameLambdaHandler = + testStack.findAndGetStackOutputValue('handlerFn'); + tableNameLambdaHandler = + testStack.findAndGetStackOutputValue('handlerTable'); }, SETUP_TIMEOUT); it( @@ -132,7 +132,7 @@ describe(`Idempotency E2E tests, wrapper function usage`, () => { // Assess const idempotencyRecords = await ddb.send( new ScanCommand({ - TableName: ddbTableNameDefault, + TableName: tableNameDefault, }) ); // Since records 1 and 3 have the same payload, only 2 records should be created @@ -192,7 +192,7 @@ describe(`Idempotency E2E tests, wrapper function usage`, () => { // Assess const idempotencyRecords = await ddb.send( new ScanCommand({ - TableName: ddbTableNameCustomConfig, + TableName: tableNameCustomConfig, }) ); /** @@ -286,7 +286,7 @@ describe(`Idempotency E2E tests, wrapper function usage`, () => { // Assess const idempotencyRecords = await ddb.send( new ScanCommand({ - TableName: ddbTableNameLambdaHandler, + TableName: tableNameLambdaHandler, }) ); expect(idempotencyRecords.Items?.length).toEqual(1); diff --git a/packages/idempotency/tests/helpers/idempotencyUtils.ts b/packages/idempotency/tests/helpers/idempotencyUtils.ts index cef4073572..efda071436 100644 --- a/packages/idempotency/tests/helpers/idempotencyUtils.ts +++ b/packages/idempotency/tests/helpers/idempotencyUtils.ts @@ -1,49 +1,5 @@ -import { - TestNodejsFunction, - TEST_RUNTIMES, -} from '@aws-lambda-powertools/testing-utils'; -import { Duration, RemovalPolicy, Stack } from 'aws-cdk-lib'; -import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb'; -import { randomUUID } from 'node:crypto'; import { BasePersistenceLayer } from '../../src/persistence'; -export const createIdempotencyResources = ( - stack: Stack, - runtime: string, - ddbTableName: string, - pathToFunction: string, - functionName: string, - handler: string, - ddbPkId?: string, - timeout?: number -): void => { - const uniqueTableId = ddbTableName + randomUUID().substring(0, 5); - const ddbTable = new Table(stack, uniqueTableId, { - tableName: ddbTableName, - partitionKey: { - name: ddbPkId ? ddbPkId : 'id', - type: AttributeType.STRING, - }, - billingMode: BillingMode.PAY_PER_REQUEST, - removalPolicy: RemovalPolicy.DESTROY, - }); - - const uniqueFunctionId = functionName + randomUUID().substring(0, 5); - const nodeJsFunction = new TestNodejsFunction(stack, uniqueFunctionId, { - functionName: functionName, - entry: pathToFunction, - runtime: TEST_RUNTIMES[runtime as keyof typeof TEST_RUNTIMES], - timeout: Duration.seconds(timeout || 30), - handler: handler, - environment: { - IDEMPOTENCY_TABLE_NAME: ddbTableName, - POWERTOOLS_LOGGER_LOG_EVENT: 'true', - }, - }); - - ddbTable.grantReadWriteData(nodeJsFunction); -}; - /** * Dummy class to test the abstract class BasePersistenceLayer. * diff --git a/packages/idempotency/tests/helpers/resources.ts b/packages/idempotency/tests/helpers/resources.ts new file mode 100644 index 0000000000..75bc4276ed --- /dev/null +++ b/packages/idempotency/tests/helpers/resources.ts @@ -0,0 +1,54 @@ +import type { + ExtraTestProps, + TestDynamodbTableProps, + TestNodejsFunctionProps, + TestStack, +} from '@aws-lambda-powertools/testing-utils'; +import { + concatenateResourceName, + TestDynamodbTable, + TestNodejsFunction, +} from '@aws-lambda-powertools/testing-utils'; +import { Construct } from 'constructs'; +import { randomUUID } from 'node:crypto'; + +class IdempotencyTestNodejsFunctionAndDynamoTable extends Construct { + public constructor( + testStack: TestStack, + props: { + function: TestNodejsFunctionProps; + table?: TestDynamodbTableProps; + }, + extraProps: ExtraTestProps + ) { + super( + testStack.stack, + concatenateResourceName({ + testName: testStack.testName, + resourceName: randomUUID(), + }) + ); + + const table = new TestDynamodbTable(testStack, props.table || {}, { + nameSuffix: `${extraProps.nameSuffix}Table`, + }); + + const fn = new TestNodejsFunction( + testStack, + { + ...props.function, + environment: { + IDEMPOTENCY_TABLE_NAME: table.tableName, + POWERTOOLS_LOGGER_LOG_EVENT: 'true', + }, + }, + { + nameSuffix: `${extraProps.nameSuffix}Fn`, + } + ); + + table.grantReadWriteData(fn); + } +} + +export { IdempotencyTestNodejsFunctionAndDynamoTable }; diff --git a/packages/logger/tests/e2e/basicFeatures.middy.test.ts b/packages/logger/tests/e2e/basicFeatures.middy.test.ts index 422ed6c186..099053f538 100644 --- a/packages/logger/tests/e2e/basicFeatures.middy.test.ts +++ b/packages/logger/tests/e2e/basicFeatures.middy.test.ts @@ -4,19 +4,13 @@ * @group e2e/logger/basicFeatures */ import { - concatenateResourceName, - defaultRuntime, - generateTestUniqueName, invokeFunction, - isValidRuntimeKey, TestInvocationLogs, - TestNodejsFunction, TestStack, - TEST_RUNTIMES, } from '@aws-lambda-powertools/testing-utils'; import type { APIGatewayAuthorizerResult } from 'aws-lambda'; -import { randomUUID } from 'node:crypto'; import { join } from 'node:path'; +import { LoggerTestNodejsFunction } from '../helpers/resources'; import { RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, @@ -24,87 +18,49 @@ import { TEARDOWN_TIMEOUT, TEST_CASE_TIMEOUT, XRAY_TRACE_ID_REGEX, + commonEnvironmentVars, } from './constants'; describe(`Logger E2E tests, basic functionalities middy usage`, () => { - const runtime: string = process.env.RUNTIME || defaultRuntime; - - if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key value: ${runtime}`); - } - - const testName = generateTestUniqueName({ - testPrefix: RESOURCE_NAME_PREFIX, - runtime, - testName: 'AllFeatures-Decorator', + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'AllFeatures-Decorator', + }, }); - const testStack = new TestStack(testName); // Location of the lambda function code - const lambdaFunctionCodeFile = join( + const lambdaFunctionCodeFilePath = join( __dirname, 'basicFeatures.middy.test.FunctionCode.ts' ); - const fnNameBasicFeatures = concatenateResourceName({ - testName, - resourceName: 'BasicFeatures', - }); - - // Text to be used by Logger in the Lambda function - const PERSISTENT_KEY = 'persistentKey'; - const RUNTIME_ADDED_KEY = 'foo'; - const PERSISTENT_VALUE = randomUUID(); - const REMOVABLE_KEY = 'removableKey'; - const REMOVABLE_VALUE = 'removedValue'; - const SINGLE_LOG_ITEM_KEY = 'singleKey'; - const SINGLE_LOG_ITEM_VALUE = 'singleValue'; - const ERROR_MSG = 'error'; - const ARBITRARY_OBJECT_KEY = 'arbitraryObjectKey'; - const ARBITRARY_OBJECT_DATA = 'arbitraryObjectData'; - const LEVEL = TestInvocationLogs.LEVEL; - const invocations = 3; - - let logGroupName: string; // We do not know it until deployment - + const invocationCount = 3; let invocationLogs: TestInvocationLogs[]; beforeAll(async () => { // Prepare - new TestNodejsFunction( - testStack.stack, - fnNameBasicFeatures, + new LoggerTestNodejsFunction( + testStack, { - functionName: fnNameBasicFeatures, - entry: lambdaFunctionCodeFile, - runtime: TEST_RUNTIMES[runtime], - environment: { - LOG_LEVEL: 'INFO', - POWERTOOLS_SERVICE_NAME: 'logger-e2e-testing', - PERSISTENT_KEY, - PERSISTENT_VALUE, - RUNTIME_ADDED_KEY, - REMOVABLE_KEY, - REMOVABLE_VALUE, - SINGLE_LOG_ITEM_KEY, - SINGLE_LOG_ITEM_VALUE, - ERROR_MSG, - ARBITRARY_OBJECT_KEY, - ARBITRARY_OBJECT_DATA, - }, + entry: lambdaFunctionCodeFilePath, }, { logGroupOutputKey: STACK_OUTPUT_LOG_GROUP, + nameSuffix: 'BasicFeatures', } ); - const result = await testStack.deploy(); - logGroupName = result[STACK_OUTPUT_LOG_GROUP]; + await testStack.deploy(); + const logGroupName = testStack.findAndGetStackOutputValue( + STACK_OUTPUT_LOG_GROUP + ); + const functionName = testStack.findAndGetStackOutputValue('BasicFeatures'); // Invoke the function three time (one for cold start, then two for warm start) invocationLogs = await invokeFunction({ - functionName: fnNameBasicFeatures, - times: invocations, + functionName, + times: invocationCount, invocationMode: 'SEQUENTIAL', payload: { foo: 'bar', @@ -118,9 +74,9 @@ describe(`Logger E2E tests, basic functionalities middy usage`, () => { it( 'should filter log based on LOG_LEVEL (INFO) environment variable in Lambda', async () => { - for (let i = 0; i < invocations; i++) { + for (let i = 0; i < invocationCount; i++) { // Get log messages of the invocation and filter by level - const debugLogs = invocationLogs[i].getFunctionLogs(LEVEL.DEBUG); + const debugLogs = invocationLogs[i].getFunctionLogs('DEBUG'); // Check that no log message below INFO level is logged expect(debugLogs.length).toBe(0); } @@ -133,7 +89,7 @@ describe(`Logger E2E tests, basic functionalities middy usage`, () => { it( 'should inject context info in each log', async () => { - for (let i = 0; i < invocations; i++) { + for (let i = 0; i < invocationCount; i++) { // Get log messages of the invocation const logMessages = invocationLogs[i].getFunctionLogs(); // Check that the context is logged on every log @@ -153,7 +109,7 @@ describe(`Logger E2E tests, basic functionalities middy usage`, () => { it( 'should include coldStart equal to TRUE only on the first invocation, FALSE otherwise', async () => { - for (let i = 0; i < invocations; i++) { + for (let i = 0; i < invocationCount; i++) { // Get log messages of the invocation const logMessages = invocationLogs[i].getFunctionLogs(); // Check that cold start is logged correctly on every log @@ -175,7 +131,7 @@ describe(`Logger E2E tests, basic functionalities middy usage`, () => { it( 'should log the event as the first log of each invocation only', async () => { - for (let i = 0; i < invocations; i++) { + for (let i = 0; i < invocationCount; i++) { // Get log messages of the invocation const logMessages = invocationLogs[i].getFunctionLogs(); @@ -202,15 +158,20 @@ describe(`Logger E2E tests, basic functionalities middy usage`, () => { it( 'should contain persistent value in every log', async () => { - for (let i = 0; i < invocations; i++) { + const { + PERSISTENT_KEY: persistentKey, + PERSISTENT_VALUE: persistentValue, + } = commonEnvironmentVars; + + for (let i = 0; i < invocationCount; i++) { // Get log messages of the invocation const logMessages = invocationLogs[i].getFunctionLogs(); for (const message of logMessages) { const log = TestInvocationLogs.parseFunctionLog(message); // Check that the persistent key is present in every log - expect(log).toHaveProperty(PERSISTENT_KEY); - expect(log[PERSISTENT_KEY]).toBe(PERSISTENT_VALUE); + expect(log).toHaveProperty(persistentKey); + expect(log[persistentKey]).toBe(persistentValue); } } }, @@ -220,7 +181,10 @@ describe(`Logger E2E tests, basic functionalities middy usage`, () => { it( 'should not contain persistent keys that were removed on runtime', async () => { - for (let i = 0; i < invocations; i++) { + const { REMOVABLE_KEY: removableKey, REMOVABLE_VALUE: removableValue } = + commonEnvironmentVars; + + for (let i = 0; i < invocationCount; i++) { // Get log messages of the invocation const logMessages = invocationLogs[i].getFunctionLogs(); @@ -229,11 +193,11 @@ describe(`Logger E2E tests, basic functionalities middy usage`, () => { // Check that at the time of logging the event, which happens before the handler, // the key was still present if (index === 0) { - expect(log).toHaveProperty(REMOVABLE_KEY); - expect(log[REMOVABLE_KEY]).toBe(REMOVABLE_VALUE); + expect(log).toHaveProperty(removableKey); + expect(log[removableKey]).toBe(removableValue); // Check that all other logs that happen at runtime do not contain the key } else { - expect(log).not.toHaveProperty(REMOVABLE_KEY); + expect(log).not.toHaveProperty(removableValue); } } } @@ -244,7 +208,9 @@ describe(`Logger E2E tests, basic functionalities middy usage`, () => { it( 'should not leak any persistent keys added runtime since clearState is enabled', async () => { - for (let i = 0; i < invocations; i++) { + const { RUNTIME_ADDED_KEY: runtimeAddedKey } = commonEnvironmentVars; + + for (let i = 0; i < invocationCount; i++) { // Get log messages of the invocation const logMessages = invocationLogs[i].getFunctionLogs(); @@ -253,12 +219,12 @@ describe(`Logger E2E tests, basic functionalities middy usage`, () => { // Check that at the time of logging the event, which happens before the handler, // the key is NOT present if (index === 0) { - expect(log).not.toHaveProperty(RUNTIME_ADDED_KEY); + expect(log).not.toHaveProperty(runtimeAddedKey); } else { // Check that all other logs that happen at runtime do contain the key - expect(log).toHaveProperty(RUNTIME_ADDED_KEY); + expect(log).toHaveProperty(runtimeAddedKey); // Check that the value is the same for all logs - expect(log[RUNTIME_ADDED_KEY]).toEqual('bar'); + expect(log[runtimeAddedKey]).toEqual('bar'); } } } @@ -271,19 +237,24 @@ describe(`Logger E2E tests, basic functionalities middy usage`, () => { it( 'should log additional keys and value only once', async () => { - for (let i = 0; i < invocations; i++) { + const { + SINGLE_LOG_ITEM_KEY: singleLogItemKey, + SINGLE_LOG_ITEM_VALUE: singleLogItemValue, + } = commonEnvironmentVars; + + for (let i = 0; i < invocationCount; i++) { // Get log messages of the invocation const logMessages = invocationLogs[i].getFunctionLogs(); // Check that the additional log is logged only once const logMessagesWithAdditionalLog = logMessages.filter((log) => - log.includes(SINGLE_LOG_ITEM_KEY) + log.includes(singleLogItemKey) ); expect(logMessagesWithAdditionalLog).toHaveLength(1); // Check that the additional log is logged correctly const parsedLog = TestInvocationLogs.parseFunctionLog( logMessagesWithAdditionalLog[0] ); - expect(parsedLog[SINGLE_LOG_ITEM_KEY]).toBe(SINGLE_LOG_ITEM_VALUE); + expect(parsedLog[singleLogItemKey]).toBe(singleLogItemValue); } }, TEST_CASE_TIMEOUT @@ -294,9 +265,11 @@ describe(`Logger E2E tests, basic functionalities middy usage`, () => { it( 'should log error only once', async () => { - for (let i = 0; i < invocations; i++) { + const { ERROR_MSG: errorMsg } = commonEnvironmentVars; + + for (let i = 0; i < invocationCount; i++) { // Get log messages of the invocation filtered by error level - const logMessages = invocationLogs[i].getFunctionLogs(LEVEL.ERROR); + const logMessages = invocationLogs[i].getFunctionLogs('ERROR'); // Check that the error is logged only once expect(logMessages).toHaveLength(1); @@ -308,7 +281,7 @@ describe(`Logger E2E tests, basic functionalities middy usage`, () => { expect.objectContaining({ location: expect.any(String), name: 'Error', - message: ERROR_MSG, + message: errorMsg, stack: expect.anything(), }) ); @@ -322,12 +295,17 @@ describe(`Logger E2E tests, basic functionalities middy usage`, () => { it( 'should log additional arbitrary object only once', async () => { - for (let i = 0; i < invocations; i++) { + const { + ARBITRARY_OBJECT_KEY: objectKey, + ARBITRARY_OBJECT_DATA: objectData, + } = commonEnvironmentVars; + + for (let i = 0; i < invocationCount; i++) { // Get log messages of the invocation const logMessages = invocationLogs[i].getFunctionLogs(); // Get the log messages that contains the arbitrary object const filteredLogs = logMessages.filter((log) => - log.includes(ARBITRARY_OBJECT_DATA) + log.includes(objectData) ); // Check that the arbitrary object is logged only once expect(filteredLogs).toHaveLength(1); @@ -335,11 +313,9 @@ describe(`Logger E2E tests, basic functionalities middy usage`, () => { filteredLogs[0] ); // Check that the arbitrary object is logged correctly - expect(logObject).toHaveProperty(ARBITRARY_OBJECT_KEY); - const arbitrary = logObject[ - ARBITRARY_OBJECT_KEY - ] as APIGatewayAuthorizerResult; - expect(arbitrary.principalId).toBe(ARBITRARY_OBJECT_DATA); + expect(logObject).toHaveProperty(objectKey); + const arbitrary = logObject[objectKey] as APIGatewayAuthorizerResult; + expect(arbitrary.principalId).toBe(objectData); expect(arbitrary.policyDocument).toEqual( expect.objectContaining({ Version: 'Version 1', @@ -362,7 +338,7 @@ describe(`Logger E2E tests, basic functionalities middy usage`, () => { it( 'should inject & parse the X-Ray Trace ID of the current invocation into every log', async () => { - for (let i = 0; i < invocations; i++) { + for (let i = 0; i < invocationCount; i++) { // Get log messages of the invocation const logMessages = invocationLogs[i].getFunctionLogs(); diff --git a/packages/logger/tests/e2e/childLogger.manual.test.ts b/packages/logger/tests/e2e/childLogger.manual.test.ts index d07757a786..454eab8a81 100644 --- a/packages/logger/tests/e2e/childLogger.manual.test.ts +++ b/packages/logger/tests/e2e/childLogger.manual.test.ts @@ -3,94 +3,61 @@ * * @group e2e/logger/childLogger */ -import { join } from 'node:path'; import { - concatenateResourceName, - defaultRuntime, - generateTestUniqueName, - isValidRuntimeKey, - TestNodejsFunction, - TestStack, - TEST_RUNTIMES, - TestInvocationLogs, invokeFunction, + TestInvocationLogs, + TestStack, } from '@aws-lambda-powertools/testing-utils'; +import { join } from 'node:path'; +import { LoggerTestNodejsFunction } from '../helpers/resources'; import { + commonEnvironmentVars, RESOURCE_NAME_PREFIX, - STACK_OUTPUT_LOG_GROUP, SETUP_TIMEOUT, - TEST_CASE_TIMEOUT, + STACK_OUTPUT_LOG_GROUP, TEARDOWN_TIMEOUT, + TEST_CASE_TIMEOUT, } from './constants'; describe(`Logger E2E tests, child logger`, () => { - const runtime: string = process.env.RUNTIME || defaultRuntime; - - if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key value: ${runtime}`); - } - - const testName = generateTestUniqueName({ - testPrefix: RESOURCE_NAME_PREFIX, - runtime, - testName: 'ChildLogger-Manual', + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'ChildLogger-Manual', + }, }); - const testStack = new TestStack(testName); // Location of the lambda function code - const lambdaFunctionCodeFile = join( + const lambdaFunctionCodeFilePath = join( __dirname, 'childLogger.manual.test.FunctionCode.ts' ); - const fnNameChildLogger = concatenateResourceName({ - testName, - resourceName: 'ChildLogger', - }); - - // Parameters to be used by Logger in the Lambda function - const PERSISTENT_KEY = 'persistentKey'; - const PERSISTENT_VALUE = 'persistentValue'; - const PARENT_LOG_MSG = 'parent-only-log-msg'; - const CHILD_LOG_MSG = 'child-only-log-msg'; - const LEVEL = TestInvocationLogs.LEVEL; - const CHILD_LOG_LEVEL = LEVEL.ERROR; - let logGroupName: string; // We do not know it until deployment + const invocationCount = 3; let invocationLogs: TestInvocationLogs[]; - const invocations = 3; + let logGroupName: string; beforeAll(async () => { // Prepare - new TestNodejsFunction( - testStack.stack, - fnNameChildLogger, + new LoggerTestNodejsFunction( + testStack, { - functionName: fnNameChildLogger, - entry: lambdaFunctionCodeFile, - runtime: TEST_RUNTIMES[runtime], - environment: { - LOG_LEVEL: 'INFO', - POWERTOOLS_SERVICE_NAME: 'logger-e2e-testing', - PERSISTENT_KEY, - PERSISTENT_VALUE, - PARENT_LOG_MSG, - CHILD_LOG_MSG, - CHILD_LOG_LEVEL, - }, + entry: lambdaFunctionCodeFilePath, }, { logGroupOutputKey: STACK_OUTPUT_LOG_GROUP, + nameSuffix: 'ChildLogger', } ); - const result = await testStack.deploy(); - logGroupName = result[STACK_OUTPUT_LOG_GROUP]; + await testStack.deploy(); + logGroupName = testStack.findAndGetStackOutputValue(STACK_OUTPUT_LOG_GROUP); + const functionName = testStack.findAndGetStackOutputValue('ChildLogger'); - // Invoke the function three time (one for cold start, then two for warm start) invocationLogs = await invokeFunction({ - functionName: fnNameChildLogger, + functionName, invocationMode: 'SEQUENTIAL', - times: invocations, + times: invocationCount, }); console.log('logGroupName', logGroupName); @@ -100,15 +67,18 @@ describe(`Logger E2E tests, child logger`, () => { it( 'should not log at same level of parent because of its own logLevel', async () => { - for (let i = 0; i < invocations; i++) { + const { PARENT_LOG_MSG: parentLogMsg, CHILD_LOG_MSG: childLogMsg } = + commonEnvironmentVars; + + for (let i = 0; i < invocationCount; i++) { // Get log messages of the invocation and filter by level - const infoLogs = invocationLogs[i].getFunctionLogs(LEVEL.INFO); + const infoLogs = invocationLogs[i].getFunctionLogs('INFO'); const parentInfoLogs = infoLogs.filter((message) => - message.includes(PARENT_LOG_MSG) + message.includes(parentLogMsg) ); const childInfoLogs = infoLogs.filter((message) => - message.includes(CHILD_LOG_MSG) + message.includes(childLogMsg) ); expect(parentInfoLogs).toHaveLength(infoLogs.length); @@ -121,15 +91,15 @@ describe(`Logger E2E tests, child logger`, () => { it( 'should log only level passed to a child', async () => { - for (let i = 0; i < invocations; i++) { + const { CHILD_LOG_MSG: childLogMsg } = commonEnvironmentVars; + for (let i = 0; i < invocationCount; i++) { // Get log messages of the invocation const logMessages = invocationLogs[i].getFunctionLogs(); // Filter child logs by level const errorChildLogs = logMessages.filter( (message) => - message.includes(LEVEL.ERROR.toString()) && - message.includes(CHILD_LOG_MSG) + message.includes('ERROR') && message.includes(childLogMsg) ); // Check that the child logger only logged once (the other) @@ -143,13 +113,15 @@ describe(`Logger E2E tests, child logger`, () => { it( 'should NOT inject context into the child logger', async () => { - for (let i = 0; i < invocations; i++) { + const { CHILD_LOG_MSG: childLogMsg } = commonEnvironmentVars; + + for (let i = 0; i < invocationCount; i++) { // Get log messages of the invocation const logMessages = invocationLogs[i].getFunctionLogs(); // Filter child logs by level const childLogMessages = logMessages.filter((message) => - message.includes(CHILD_LOG_MSG) + message.includes(childLogMsg) ); // Check that the context is not present in any of the child logs @@ -168,14 +140,16 @@ describe(`Logger E2E tests, child logger`, () => { it( 'both logger instances should have the same persistent key/value', async () => { - for (let i = 0; i < invocations; i++) { + const { PERSISTENT_KEY: persistentKey } = commonEnvironmentVars; + + for (let i = 0; i < invocationCount; i++) { // Get log messages of the invocation const logMessages = invocationLogs[i].getFunctionLogs(); // Check that all logs have the persistent key/value for (const message of logMessages) { const log = TestInvocationLogs.parseFunctionLog(message); - expect(log).toHaveProperty(PERSISTENT_KEY); + expect(log).toHaveProperty(persistentKey); } } }, diff --git a/packages/logger/tests/e2e/constants.ts b/packages/logger/tests/e2e/constants.ts index 1c482b8819..11ac263ae4 100644 --- a/packages/logger/tests/e2e/constants.ts +++ b/packages/logger/tests/e2e/constants.ts @@ -1,7 +1,38 @@ -export const RESOURCE_NAME_PREFIX = 'Logger-E2E'; -export const ONE_MINUTE = 60 * 1000; -export const TEST_CASE_TIMEOUT = ONE_MINUTE; -export const SETUP_TIMEOUT = 5 * ONE_MINUTE; -export const TEARDOWN_TIMEOUT = 5 * ONE_MINUTE; -export const STACK_OUTPUT_LOG_GROUP = 'LogGroupName'; -export const XRAY_TRACE_ID_REGEX = /^1-[0-9a-f]{8}-[0-9a-f]{24}$/; +import { randomUUID } from 'node:crypto'; + +const RESOURCE_NAME_PREFIX = 'Logger-E2E'; +const ONE_MINUTE = 60 * 1000; +const TEST_CASE_TIMEOUT = ONE_MINUTE; +const SETUP_TIMEOUT = 5 * ONE_MINUTE; +const TEARDOWN_TIMEOUT = 5 * ONE_MINUTE; +const STACK_OUTPUT_LOG_GROUP = 'LogGroupName'; +const XRAY_TRACE_ID_REGEX = /^1-[0-9a-f]{8}-[0-9a-f]{24}$/; + +const commonEnvironmentVars = { + PERSISTENT_KEY: 'persistentKey', + RUNTIME_ADDED_KEY: 'foo', + PERSISTENT_VALUE: randomUUID(), + REMOVABLE_KEY: 'removableKey', + REMOVABLE_VALUE: 'removedValue', + SINGLE_LOG_ITEM_KEY: 'singleKey', + SINGLE_LOG_ITEM_VALUE: 'singleValue', + ERROR_MSG: 'error', + ARBITRARY_OBJECT_KEY: 'arbitraryObjectKey', + ARBITRARY_OBJECT_DATA: 'arbitraryObjectData', + PARENT_LOG_MSG: 'parent-only-log-msg', + CHILD_LOG_MSG: 'child-only-log-msg', + CHILD_LOG_LEVEL: 'ERROR', + POWERTOOLS_SERVICE_NAME: 'logger-e2e-testing', + LOG_LEVEL: 'INFO', +}; + +export { + RESOURCE_NAME_PREFIX, + ONE_MINUTE, + TEST_CASE_TIMEOUT, + SETUP_TIMEOUT, + TEARDOWN_TIMEOUT, + STACK_OUTPUT_LOG_GROUP, + XRAY_TRACE_ID_REGEX, + commonEnvironmentVars, +}; diff --git a/packages/logger/tests/e2e/logEventEnvVarSetting.middy.test.ts b/packages/logger/tests/e2e/logEventEnvVarSetting.middy.test.ts index ed0f2a67e6..5b87f0675f 100644 --- a/packages/logger/tests/e2e/logEventEnvVarSetting.middy.test.ts +++ b/packages/logger/tests/e2e/logEventEnvVarSetting.middy.test.ts @@ -4,17 +4,12 @@ * @group e2e/logger/logEventEnvVarSetting */ import { - concatenateResourceName, - defaultRuntime, - generateTestUniqueName, invokeFunction, - isValidRuntimeKey, TestInvocationLogs, - TestNodejsFunction, TestStack, - TEST_RUNTIMES, } from '@aws-lambda-powertools/testing-utils'; import { join } from 'node:path'; +import { LoggerTestNodejsFunction } from '../helpers/resources'; import { RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, @@ -24,63 +19,48 @@ import { } from './constants'; describe(`Logger E2E tests, log event via env var setting with middy`, () => { - const runtime: string = process.env.RUNTIME || defaultRuntime; - - if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key value: ${runtime}`); - } - - const testName = generateTestUniqueName({ - testPrefix: RESOURCE_NAME_PREFIX, - runtime, - testName: 'LogEventEnvVarSetting-Middy', + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'LogEventFromEnv-Middy', + }, }); - const testStack = new TestStack(testName); // Location of the lambda function code - const lambdaFunctionCodeFile = join( + const lambdaFunctionCodeFilePath = join( __dirname, 'logEventEnvVarSetting.middy.test.FunctionCode.ts' ); - const fnNameLogEventEnvVar = concatenateResourceName({ - testName, - resourceName: 'LogEvent', - }); - - let logGroupName: string; // We do not know it until deployment - + const invocationCount = 3; let invocationLogs: TestInvocationLogs[]; - const invocations = 3; + let logGroupName: string; beforeAll(async () => { // Prepare - new TestNodejsFunction( - testStack.stack, - fnNameLogEventEnvVar, + new LoggerTestNodejsFunction( + testStack, { - functionName: fnNameLogEventEnvVar, - entry: lambdaFunctionCodeFile, - runtime: TEST_RUNTIMES[runtime], + entry: lambdaFunctionCodeFilePath, environment: { - LOG_LEVEL: 'INFO', - POWERTOOLS_SERVICE_NAME: 'logger-e2e-testing', POWERTOOLS_LOGGER_LOG_EVENT: 'true', }, }, { logGroupOutputKey: STACK_OUTPUT_LOG_GROUP, + nameSuffix: 'LogEventFromEnv', } ); - const result = await testStack.deploy(); - logGroupName = result[STACK_OUTPUT_LOG_GROUP]; + await testStack.deploy(); + logGroupName = testStack.findAndGetStackOutputValue(STACK_OUTPUT_LOG_GROUP); + const functionName = + testStack.findAndGetStackOutputValue('LogEventFromEnv'); - // Invoke the function three time (one for cold start, then two for warm start) invocationLogs = await invokeFunction({ - functionName: fnNameLogEventEnvVar, + functionName, invocationMode: 'SEQUENTIAL', - times: invocations, + times: invocationCount, payload: { foo: 'bar', }, @@ -93,7 +73,7 @@ describe(`Logger E2E tests, log event via env var setting with middy`, () => { it( 'should log the event as the first log of each invocation only', async () => { - for (let i = 0; i < invocations; i++) { + for (let i = 0; i < invocationCount; i++) { // Get log messages of the invocation const logMessages = invocationLogs[i].getFunctionLogs(); diff --git a/packages/logger/tests/e2e/sampleRate.decorator.test.ts b/packages/logger/tests/e2e/sampleRate.decorator.test.ts index 424d4fa7a5..67c803dd58 100644 --- a/packages/logger/tests/e2e/sampleRate.decorator.test.ts +++ b/packages/logger/tests/e2e/sampleRate.decorator.test.ts @@ -3,87 +3,65 @@ * * @group e2e/logger/sampleRate */ -import { join } from 'node:path'; import { - concatenateResourceName, - defaultRuntime, - generateTestUniqueName, invokeFunction, - isValidRuntimeKey, - TestNodejsFunction, - TestStack, - TEST_RUNTIMES, TestInvocationLogs, + TestStack, } from '@aws-lambda-powertools/testing-utils'; +import { randomUUID } from 'node:crypto'; +import { join } from 'node:path'; +import { LoggerTestNodejsFunction } from '../helpers/resources'; import { RESOURCE_NAME_PREFIX, - STACK_OUTPUT_LOG_GROUP, SETUP_TIMEOUT, - TEST_CASE_TIMEOUT, + STACK_OUTPUT_LOG_GROUP, TEARDOWN_TIMEOUT, + TEST_CASE_TIMEOUT, } from './constants'; describe(`Logger E2E tests, sample rate and injectLambdaContext()`, () => { - const runtime: string = process.env.RUNTIME || defaultRuntime; - - if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key value: ${runtime}`); - } - - const testName = generateTestUniqueName({ - testPrefix: RESOURCE_NAME_PREFIX, - runtime, - testName: 'SampleRate-Decorator', + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'SampleRate-Decorator', + }, }); - const testStack = new TestStack(testName); // Location of the lambda function code - const lambdaFunctionCodeFile = join( + const lambdaFunctionCodeFilePath = join( __dirname, 'sampleRate.decorator.test.FunctionCode.ts' ); - const fnNameSampleRate = concatenateResourceName({ - testName, - resourceName: 'SampleRate', - }); - - // Parameters to be used by Logger in the Lambda function - const LOG_MSG = `Log message ${fnNameSampleRate}`; - const SAMPLE_RATE = '0.5'; - const LEVEL = TestInvocationLogs.LEVEL; - const LOG_LEVEL = LEVEL.ERROR; - let logGroupName: string; // We do not know the exact name until deployment + const invocationCount = 20; let invocationLogs: TestInvocationLogs[]; - const invocations = 20; + let logGroupName: string; beforeAll(async () => { // Prepare - new TestNodejsFunction( - testStack.stack, - fnNameSampleRate, + new LoggerTestNodejsFunction( + testStack, { - functionName: fnNameSampleRate, - entry: lambdaFunctionCodeFile, - runtime: TEST_RUNTIMES[runtime], + entry: lambdaFunctionCodeFilePath, environment: { - LOG_LEVEL: LOG_LEVEL, - POWERTOOLS_SERVICE_NAME: 'logger-e2e-testing', - LOG_MSG, - SAMPLE_RATE, + LOG_LEVEL: 'ERROR', + SAMPLE_RATE: '0.5', + LOG_MSG: `Log message ${randomUUID()}`, }, }, { logGroupOutputKey: STACK_OUTPUT_LOG_GROUP, + nameSuffix: 'BasicFeatures', } ); - const result = await testStack.deploy(); - logGroupName = result[STACK_OUTPUT_LOG_GROUP]; + await testStack.deploy(); + logGroupName = testStack.findAndGetStackOutputValue(STACK_OUTPUT_LOG_GROUP); + const functionName = testStack.findAndGetStackOutputValue('BasicFeatures'); invocationLogs = await invokeFunction({ - functionName: fnNameSampleRate, - times: invocations, + functionName, + times: invocationCount, }); console.log('logGroupName', logGroupName); @@ -97,14 +75,11 @@ describe(`Logger E2E tests, sample rate and injectLambdaContext()`, () => { let countSampled = 0; let countNotSampled = 0; - for (let i = 0; i < invocations; i++) { + for (let i = 0; i < invocationCount; i++) { // Get log messages of the invocation const logMessages = invocationLogs[i].getFunctionLogs(); - if ( - logMessages.length === 1 && - logMessages[0].includes(LEVEL.ERROR) - ) { + if (logMessages.length === 1 && logMessages[0].includes('ERROR')) { countNotSampled++; } else if (logMessages.length === 4) { countSampled++; @@ -116,7 +91,7 @@ describe(`Logger E2E tests, sample rate and injectLambdaContext()`, () => { } } - // Given that we set rate to 0.5. The chance that we get all invocations sampled + // Given that we set rate to 0.5. The chance that we get all invocationCount sampled // (or not sampled) is less than 0.5^20 expect(countSampled).toBeGreaterThan(0); expect(countNotSampled).toBeGreaterThan(0); @@ -129,9 +104,9 @@ describe(`Logger E2E tests, sample rate and injectLambdaContext()`, () => { it( 'should inject Lambda context into every log emitted', async () => { - for (let i = 0; i < invocations; i++) { + for (let i = 0; i < invocationCount; i++) { // Get log messages of the invocation - const logMessages = invocationLogs[i].getFunctionLogs(LEVEL.ERROR); + const logMessages = invocationLogs[i].getFunctionLogs('ERROR'); // Check that the context is logged on every log for (const message of logMessages) { diff --git a/packages/logger/tests/helpers/resources.ts b/packages/logger/tests/helpers/resources.ts new file mode 100644 index 0000000000..95e7a34074 --- /dev/null +++ b/packages/logger/tests/helpers/resources.ts @@ -0,0 +1,40 @@ +import { TestNodejsFunction } from '@aws-lambda-powertools/testing-utils'; +import type { TestStack } from '@aws-lambda-powertools/testing-utils'; +import { CfnOutput } from 'aws-cdk-lib'; +import type { + TestNodejsFunctionProps, + ExtraTestProps, +} from '@aws-lambda-powertools/testing-utils'; +import { commonEnvironmentVars } from '../e2e/constants'; + +interface LoggerExtraTestProps extends ExtraTestProps { + logGroupOutputKey?: string; +} + +class LoggerTestNodejsFunction extends TestNodejsFunction { + public constructor( + scope: TestStack, + props: TestNodejsFunctionProps, + extraProps: LoggerExtraTestProps + ) { + super( + scope, + { + ...props, + environment: { + ...commonEnvironmentVars, + ...props.environment, + }, + }, + extraProps + ); + + if (extraProps.logGroupOutputKey) { + new CfnOutput(this, extraProps.logGroupOutputKey, { + value: this.logGroup.logGroupName, + }); + } + } +} + +export { LoggerTestNodejsFunction }; diff --git a/packages/metrics/tests/e2e/basicFeatures.decorators.test.ts b/packages/metrics/tests/e2e/basicFeatures.decorators.test.ts index 981b6ed9ba..332636210e 100644 --- a/packages/metrics/tests/e2e/basicFeatures.decorators.test.ts +++ b/packages/metrics/tests/e2e/basicFeatures.decorators.test.ts @@ -4,28 +4,18 @@ * @group e2e/metrics/decorator */ import { - concatenateResourceName, - defaultRuntime, - generateTestUniqueName, invokeFunction, - isValidRuntimeKey, - TestNodejsFunction, TestStack, - TEST_RUNTIMES, } from '@aws-lambda-powertools/testing-utils'; import { CloudWatchClient, GetMetricStatisticsCommand, } from '@aws-sdk/client-cloudwatch'; -import { randomUUID } from 'node:crypto'; import { join } from 'node:path'; import { getMetrics } from '../helpers/metricsUtils'; +import { MetricsTestNodejsFunction } from '../helpers/resources'; import { - commonEnvironmentVariables, - expectedDefaultDimensions, - expectedExtraDimension, - expectedMetricName, - expectedMetricValue, + commonEnvironmentVars, ONE_MINUTE, RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, @@ -34,55 +24,45 @@ import { } from './constants'; describe(`Metrics E2E tests, basic features decorator usage`, () => { - const runtime: string = process.env.RUNTIME || defaultRuntime; - - if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key value: ${runtime}`); - } - - const testName = generateTestUniqueName({ - testPrefix: RESOURCE_NAME_PREFIX, - runtime, - testName: 'BasicFeatures-Decorator', + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'BasicFeatures-Decorators', + }, }); - const testStack = new TestStack(testName); - const startTime = new Date(); // Location of the lambda function code - const lambdaFunctionCodeFile = join( + const lambdaFunctionCodeFilePath = join( __dirname, 'basicFeatures.decorator.test.functionCode.ts' ); + const startTime = new Date(); - const fnNameBasicFeatures = concatenateResourceName({ - testName, - resourceName: 'BasicFeatures', - }); + const expectedServiceName = 'e2eBasicFeatures'; + let fnNameBasicFeatures: string; + new MetricsTestNodejsFunction( + testStack, + { + entry: lambdaFunctionCodeFilePath, + environment: { + EXPECTED_SERVICE_NAME: expectedServiceName, + }, + }, + { + nameSuffix: 'BasicFeatures', + } + ); const cloudwatchClient = new CloudWatchClient({}); - const invocations = 2; - // Parameters to be used by Metrics in the Lambda function - const expectedNamespace = randomUUID(); // to easily find metrics back at assert phase - const expectedServiceName = fnNameBasicFeatures; - beforeAll(async () => { - // Prepare - new TestNodejsFunction(testStack.stack, fnNameBasicFeatures, { - functionName: fnNameBasicFeatures, - entry: lambdaFunctionCodeFile, - runtime: TEST_RUNTIMES[runtime], - environment: { - POWERTOOLS_SERVICE_NAME: 'metrics-e2e-testing', - EXPECTED_NAMESPACE: expectedNamespace, - EXPECTED_SERVICE_NAME: expectedServiceName, - ...commonEnvironmentVariables, - }, - }); - + // Deploy the stack await testStack.deploy(); + // Get the actual function names from the stack outputs + fnNameBasicFeatures = testStack.findAndGetStackOutputValue('BasicFeatures'); + // Act await invokeFunction({ functionName: fnNameBasicFeatures, @@ -95,6 +75,11 @@ describe(`Metrics E2E tests, basic features decorator usage`, () => { it( 'should capture ColdStart Metric', async () => { + const { + EXPECTED_NAMESPACE: expectedNamespace, + EXPECTED_DEFAULT_DIMENSIONS: expectedDefaultDimensions, + } = commonEnvironmentVars; + const expectedDimensions = [ { Name: 'service', Value: expectedServiceName }, { Name: 'function_name', Value: fnNameBasicFeatures }, @@ -153,6 +138,14 @@ describe(`Metrics E2E tests, basic features decorator usage`, () => { it( 'should produce a Metric with the default and extra one dimensions', async () => { + const { + EXPECTED_NAMESPACE: expectedNamespace, + EXPECTED_METRIC_NAME: expectedMetricName, + EXPECTED_METRIC_VALUE: expectedMetricValue, + EXPECTED_DEFAULT_DIMENSIONS: expectedDefaultDimensions, + EXPECTED_EXTRA_DIMENSION: expectedExtraDimension, + } = commonEnvironmentVars; + // Check metric dimensions const metrics = await getMetrics( cloudwatchClient, diff --git a/packages/metrics/tests/e2e/basicFeatures.manual.test.ts b/packages/metrics/tests/e2e/basicFeatures.manual.test.ts index fde4479a29..b22861ab16 100644 --- a/packages/metrics/tests/e2e/basicFeatures.manual.test.ts +++ b/packages/metrics/tests/e2e/basicFeatures.manual.test.ts @@ -4,28 +4,18 @@ * @group e2e/metrics/standardFunctions */ import { - concatenateResourceName, - defaultRuntime, - generateTestUniqueName, invokeFunction, - isValidRuntimeKey, - TestNodejsFunction, TestStack, - TEST_RUNTIMES, } from '@aws-lambda-powertools/testing-utils'; import { CloudWatchClient, GetMetricStatisticsCommand, } from '@aws-sdk/client-cloudwatch'; -import { randomUUID } from 'node:crypto'; import { join } from 'node:path'; import { getMetrics } from '../helpers/metricsUtils'; +import { MetricsTestNodejsFunction } from '../helpers/resources'; import { - commonEnvironmentVariables, - expectedDefaultDimensions, - expectedExtraDimension, - expectedMetricName, - expectedMetricValue, + commonEnvironmentVars, ONE_MINUTE, RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, @@ -34,56 +24,47 @@ import { } from './constants'; describe(`Metrics E2E tests, manual usage`, () => { - const runtime: string = process.env.RUNTIME || defaultRuntime; - - if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key value: ${runtime}`); - } - - const testName = generateTestUniqueName({ - testPrefix: RESOURCE_NAME_PREFIX, - runtime, - testName: 'BasicFeatures-Manual', + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'BasicFeatures-Manual', + }, }); - const testStack = new TestStack(testName); // Location of the lambda function code - const lambdaFunctionCodeFile = join( + const lambdaFunctionCodeFilePath = join( __dirname, 'basicFeatures.manual.test.functionCode.ts' ); const startTime = new Date(); - const fnNameManual = concatenateResourceName({ - testName, - resourceName: 'Manual', - }); - - // Parameters to be used by Metrics in the Lambda function - const expectedNamespace = randomUUID(); // to easily find metrics back at assert phase const expectedServiceName = 'e2eManual'; - const cloudwatchClient = new CloudWatchClient({}); - const invocations = 2; - - beforeAll(async () => { - // Prepare - new TestNodejsFunction(testStack.stack, fnNameManual, { - functionName: fnNameManual, - entry: lambdaFunctionCodeFile, - runtime: TEST_RUNTIMES[runtime], + new MetricsTestNodejsFunction( + testStack, + { + entry: lambdaFunctionCodeFilePath, environment: { - POWERTOOLS_SERVICE_NAME: 'metrics-e2e-testing', - EXPECTED_NAMESPACE: expectedNamespace, EXPECTED_SERVICE_NAME: expectedServiceName, - ...commonEnvironmentVariables, }, - }); + }, + { + nameSuffix: 'Manual', + } + ); + + const cloudwatchClient = new CloudWatchClient({}); + const invocations = 2; + beforeAll(async () => { + // Deploy the stack await testStack.deploy(); + // Get the actual function names from the stack outputs + const functionName = testStack.findAndGetStackOutputValue('Manual'); + // Act await invokeFunction({ - functionName: fnNameManual, + functionName, times: invocations, invocationMode: 'SEQUENTIAL', }); @@ -93,6 +74,8 @@ describe(`Metrics E2E tests, manual usage`, () => { it( 'should capture ColdStart Metric', async () => { + const { EXPECTED_NAMESPACE: expectedNamespace } = commonEnvironmentVars; + // Check coldstart metric dimensions const coldStartMetrics = await getMetrics( cloudwatchClient, @@ -144,6 +127,14 @@ describe(`Metrics E2E tests, manual usage`, () => { it( 'should produce a Metric with the default and extra one dimensions', async () => { + const { + EXPECTED_NAMESPACE: expectedNamespace, + EXPECTED_METRIC_NAME: expectedMetricName, + EXPECTED_METRIC_VALUE: expectedMetricValue, + EXPECTED_DEFAULT_DIMENSIONS: expectedDefaultDimensions, + EXPECTED_EXTRA_DIMENSION: expectedExtraDimension, + } = commonEnvironmentVars; + // Check metric dimensions const metrics = await getMetrics( cloudwatchClient, diff --git a/packages/metrics/tests/e2e/constants.ts b/packages/metrics/tests/e2e/constants.ts index 3a6343e22a..987e234b62 100644 --- a/packages/metrics/tests/e2e/constants.ts +++ b/packages/metrics/tests/e2e/constants.ts @@ -1,3 +1,4 @@ +import { randomUUID } from 'node:crypto'; import { MetricUnits } from '../../src'; const RESOURCE_NAME_PREFIX = 'Metrics-E2E'; @@ -6,27 +7,18 @@ const TEST_CASE_TIMEOUT = 3 * ONE_MINUTE; const SETUP_TIMEOUT = 5 * ONE_MINUTE; const TEARDOWN_TIMEOUT = 5 * ONE_MINUTE; -const expectedMetricName = 'MyMetric'; -const expectedMetricUnit = MetricUnits.Count; -const expectedMetricValue = '1'; -const expectedDefaultDimensions = { MyDimension: 'MyValue' }; -const expectedExtraDimension = { MyExtraDimension: 'MyExtraValue' }; -const expectedSingleMetricDimension = { MySingleMetricDim: 'MySingleValue' }; -const expectedSingleMetricName = 'MySingleMetric'; -const expectedSingleMetricUnit = MetricUnits.Percent; -const expectedSingleMetricValue = '2'; -const commonEnvironmentVariables = { - EXPECTED_METRIC_NAME: expectedMetricName, - EXPECTED_METRIC_UNIT: expectedMetricUnit, - EXPECTED_METRIC_VALUE: expectedMetricValue, - EXPECTED_DEFAULT_DIMENSIONS: JSON.stringify(expectedDefaultDimensions), - EXPECTED_EXTRA_DIMENSION: JSON.stringify(expectedExtraDimension), - EXPECTED_SINGLE_METRIC_DIMENSION: JSON.stringify( - expectedSingleMetricDimension - ), - EXPECTED_SINGLE_METRIC_NAME: expectedSingleMetricName, - EXPECTED_SINGLE_METRIC_UNIT: expectedSingleMetricUnit, - EXPECTED_SINGLE_METRIC_VALUE: expectedSingleMetricValue, +const commonEnvironmentVars = { + EXPECTED_METRIC_NAME: 'MyMetric', + EXPECTED_METRIC_UNIT: MetricUnits.Count, + EXPECTED_METRIC_VALUE: '1', + EXPECTED_NAMESPACE: randomUUID(), + EXPECTED_DEFAULT_DIMENSIONS: { MyDimension: 'MyValue' }, + EXPECTED_EXTRA_DIMENSION: { MyExtraDimension: 'MyExtraValue' }, + EXPECTED_SINGLE_METRIC_DIMENSION: { MySingleMetricDim: 'MySingleValue' }, + EXPECTED_SINGLE_METRIC_NAME: 'MySingleMetric', + EXPECTED_SINGLE_METRIC_UNIT: MetricUnits.Percent, + EXPECTED_SINGLE_METRIC_VALUE: '2', + POWERTOOLS_SERVICE_NAME: 'metrics-e2e-testing', }; export { @@ -35,14 +27,5 @@ export { TEST_CASE_TIMEOUT, SETUP_TIMEOUT, TEARDOWN_TIMEOUT, - expectedMetricName, - expectedMetricUnit, - expectedMetricValue, - expectedDefaultDimensions, - expectedExtraDimension, - expectedSingleMetricDimension, - expectedSingleMetricName, - expectedSingleMetricUnit, - expectedSingleMetricValue, - commonEnvironmentVariables, + commonEnvironmentVars, }; diff --git a/packages/metrics/tests/helpers/resources.ts b/packages/metrics/tests/helpers/resources.ts new file mode 100644 index 0000000000..ebb111984b --- /dev/null +++ b/packages/metrics/tests/helpers/resources.ts @@ -0,0 +1,38 @@ +import type { + ExtraTestProps, + TestNodejsFunctionProps, + TestStack, +} from '@aws-lambda-powertools/testing-utils'; +import { TestNodejsFunction } from '@aws-lambda-powertools/testing-utils'; +import { commonEnvironmentVars } from '../e2e/constants'; + +class MetricsTestNodejsFunction extends TestNodejsFunction { + public constructor( + scope: TestStack, + props: TestNodejsFunctionProps, + extraProps: ExtraTestProps + ) { + super( + scope, + { + ...props, + environment: { + ...commonEnvironmentVars, + EXPECTED_DEFAULT_DIMENSIONS: JSON.stringify( + commonEnvironmentVars.EXPECTED_DEFAULT_DIMENSIONS + ), + EXPECTED_EXTRA_DIMENSION: JSON.stringify( + commonEnvironmentVars.EXPECTED_EXTRA_DIMENSION + ), + EXPECTED_SINGLE_METRIC_DIMENSION: JSON.stringify( + commonEnvironmentVars.EXPECTED_SINGLE_METRIC_DIMENSION + ), + ...props.environment, + }, + }, + extraProps + ); + } +} + +export { MetricsTestNodejsFunction }; diff --git a/packages/testing/src/TestStack.ts b/packages/testing/src/TestStack.ts index 8c84e86fb8..410cf38fbb 100644 --- a/packages/testing/src/TestStack.ts +++ b/packages/testing/src/TestStack.ts @@ -4,6 +4,35 @@ import { readFile } from 'node:fs/promises'; import { App, Stack } from 'aws-cdk-lib'; import { AwsCdkCli, RequireApproval } from '@aws-cdk/cli-lib-alpha'; import type { ICloudAssemblyDirectoryProducer } from '@aws-cdk/cli-lib-alpha'; +import { generateTestUniqueName, getRuntimeKey } from './helpers'; + +type StackNameProps = { + /** + * Prefix for the stack name. + */ + stackNamePrefix: string; + /** + * Name of the test. + */ + testName: string; +}; + +interface TestStackProps { + /** + * Name of the test stack. + */ + stackNameProps: StackNameProps; + /** + * Reference to the AWS CDK App object. + * @default new App() + */ + app?: App; + /** + * Reference to the AWS CDK Stack object. + * @default new Stack(this.app, stackName) + */ + stack?: Stack; +} /** * Test stack that can be deployed to the selected environment. @@ -14,20 +43,36 @@ class TestStack implements ICloudAssemblyDirectoryProducer { * @default new App() */ public app: App; + /** + * Outputs of the deployed stack. + */ + public outputs: Record = {}; /** * Reference to the AWS CDK Stack object. * @default new Stack(this.app, stackName) */ public stack: Stack; + /** + * Name of the test stack. + * @example + * Logger-E2E-node18-12345-someFeature + */ + public testName: string; + /** * @internal * Reference to the AWS CDK CLI object. */ #cli: AwsCdkCli; - public constructor(stackName: string, app?: App, stack?: Stack) { + public constructor({ stackNameProps, app, stack }: TestStackProps) { + this.testName = generateTestUniqueName({ + testName: stackNameProps.testName, + runtime: getRuntimeKey(), + testPrefix: stackNameProps.stackNamePrefix, + }); this.app = app ?? new App(); - this.stack = stack ?? new Stack(this.app, stackName); + this.stack = stack ?? new Stack(this.app, this.testName); this.#cli = AwsCdkCli.fromCloudAssemblyDirectoryProducer(this); } @@ -48,9 +93,11 @@ class TestStack implements ICloudAssemblyDirectoryProducer { outputsFile: outputFilePath, }); - return JSON.parse(await readFile(outputFilePath, 'utf-8'))[ + this.outputs = JSON.parse(await readFile(outputFilePath, 'utf-8'))[ this.stack.stackName ]; + + return this.outputs; } /** @@ -63,6 +110,20 @@ class TestStack implements ICloudAssemblyDirectoryProducer { }); } + /** + * Find and get the value of a StackOutput by its key. + */ + public findAndGetStackOutputValue = (key: string): string => { + const value = Object.keys(this.outputs).find((outputKey) => + outputKey.includes(key) + ); + if (!value) { + throw new Error(`Cannot find output for ${key}`); + } + + return this.outputs[value]; + }; + /** * Produce the Cloud Assembly directory. */ diff --git a/packages/testing/src/helpers.ts b/packages/testing/src/helpers.ts index e697e2e671..ec452701ab 100644 --- a/packages/testing/src/helpers.ts +++ b/packages/testing/src/helpers.ts @@ -1,10 +1,20 @@ import { randomUUID } from 'node:crypto'; -import { TEST_RUNTIMES } from './constants'; +import { TEST_RUNTIMES, defaultRuntime } from './constants'; const isValidRuntimeKey = ( runtime: string ): runtime is keyof typeof TEST_RUNTIMES => runtime in TEST_RUNTIMES; +const getRuntimeKey = (): keyof typeof TEST_RUNTIMES => { + const runtime: string = process.env.RUNTIME || defaultRuntime; + + if (!isValidRuntimeKey(runtime)) { + throw new Error(`Invalid runtime key value: ${runtime}`); + } + + return runtime; +}; + /** * Generate a unique name for a test. * @@ -69,6 +79,7 @@ const findAndGetStackOutputValue = ( export { isValidRuntimeKey, + getRuntimeKey, generateTestUniqueName, concatenateResourceName, findAndGetStackOutputValue, diff --git a/packages/testing/src/resources/TestDynamodbTable.ts b/packages/testing/src/resources/TestDynamodbTable.ts new file mode 100644 index 0000000000..2aa663639a --- /dev/null +++ b/packages/testing/src/resources/TestDynamodbTable.ts @@ -0,0 +1,39 @@ +import { CfnOutput, RemovalPolicy } from 'aws-cdk-lib'; +import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb'; +import { randomUUID } from 'node:crypto'; +import { concatenateResourceName } from '../helpers'; +import type { TestStack } from '../TestStack'; +import type { TestDynamodbTableProps, ExtraTestProps } from './types'; + +/** + * A DynamoDB Table that can be used in tests. + * + * It includes some default props and outputs the table name. + */ +class TestDynamodbTable extends Table { + public constructor( + stack: TestStack, + props: TestDynamodbTableProps, + extraProps: ExtraTestProps + ) { + super(stack.stack, `table-${randomUUID().substring(0, 5)}`, { + partitionKey: { + name: 'id', + type: AttributeType.STRING, + }, + ...props, + tableName: concatenateResourceName({ + testName: stack.testName, + resourceName: extraProps.nameSuffix, + }), + billingMode: BillingMode.PAY_PER_REQUEST, + removalPolicy: RemovalPolicy.DESTROY, + }); + + new CfnOutput(this, extraProps.nameSuffix, { + value: this.tableName, + }); + } +} + +export { TestDynamodbTable, TestDynamodbTableProps }; diff --git a/packages/testing/src/resources/TestNodejsFunction.ts b/packages/testing/src/resources/TestNodejsFunction.ts index c95040d906..d247551723 100644 --- a/packages/testing/src/resources/TestNodejsFunction.ts +++ b/packages/testing/src/resources/TestNodejsFunction.ts @@ -1,47 +1,41 @@ import { CfnOutput, Duration } from 'aws-cdk-lib'; import { Tracing } from 'aws-cdk-lib/aws-lambda'; -import type { NodejsFunctionProps } from 'aws-cdk-lib/aws-lambda-nodejs'; import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; import { RetentionDays } from 'aws-cdk-lib/aws-logs'; -import type { Construct } from 'constructs'; - -interface ExtraTestProps { - logGroupOutputKey?: string; - fnOutputKey?: string; -} +import { randomUUID } from 'node:crypto'; +import { TEST_RUNTIMES } from '../constants'; +import { concatenateResourceName, getRuntimeKey } from '../helpers'; +import type { TestStack } from '../TestStack'; +import type { ExtraTestProps, TestNodejsFunctionProps } from './types'; /** * A NodejsFunction that can be used in tests. * - * It includes some default props and can optionally output the log group name. + * It includes some default props and outputs the function name. */ class TestNodejsFunction extends NodejsFunction { public constructor( - scope: Construct, - id: string, - props: NodejsFunctionProps, - extraProps: ExtraTestProps = {} + stack: TestStack, + props: TestNodejsFunctionProps, + extraProps: ExtraTestProps ) { - super(scope, id, { + super(stack.stack, `fn-${randomUUID().substring(0, 5)}`, { timeout: Duration.seconds(30), memorySize: 256, tracing: Tracing.ACTIVE, ...props, + functionName: concatenateResourceName({ + testName: stack.testName, + resourceName: extraProps.nameSuffix, + }), + runtime: TEST_RUNTIMES[getRuntimeKey()], logRetention: RetentionDays.ONE_DAY, }); - if (extraProps.logGroupOutputKey) { - new CfnOutput(this, extraProps.logGroupOutputKey, { - value: this.logGroup.logGroupName, - }); - } - - if (extraProps.fnOutputKey) { - new CfnOutput(this, extraProps.fnOutputKey, { - value: this.functionName, - }); - } + new CfnOutput(this, extraProps.nameSuffix, { + value: this.functionName, + }); } } -export { TestNodejsFunction }; +export { ExtraTestProps, TestNodejsFunction, TestNodejsFunctionProps }; diff --git a/packages/testing/src/resources/index.ts b/packages/testing/src/resources/index.ts index d7ad6e35c9..ce4b5bbc26 100644 --- a/packages/testing/src/resources/index.ts +++ b/packages/testing/src/resources/index.ts @@ -1 +1,2 @@ export * from './TestNodejsFunction'; +export * from './TestDynamodbTable'; diff --git a/packages/testing/src/resources/types.ts b/packages/testing/src/resources/types.ts new file mode 100644 index 0000000000..5596a08f31 --- /dev/null +++ b/packages/testing/src/resources/types.ts @@ -0,0 +1,31 @@ +import type { TableProps, AttributeType } from 'aws-cdk-lib/aws-dynamodb'; +import type { NodejsFunctionProps } from 'aws-cdk-lib/aws-lambda-nodejs'; + +interface ExtraTestProps { + /** + * The suffix to be added to the resource name. + * + * For example, if the resource name is `fn-12345` and the suffix is `BasicFeatures`, + * the output will be `fn-12345-BasicFeatures`. + * + * Note that the maximum length of the name is 64 characters, so the suffix might be truncated. + */ + nameSuffix: string; +} + +type TestDynamodbTableProps = Omit< + TableProps, + 'removalPolicy' | 'tableName' | 'billingMode' | 'partitionKey' +> & { + partitionKey?: { + name: string; + type: AttributeType; + }; +}; + +type TestNodejsFunctionProps = Omit< + NodejsFunctionProps, + 'logRetention' | 'runtime' | 'functionName' +>; + +export { ExtraTestProps, TestDynamodbTableProps, TestNodejsFunctionProps }; diff --git a/packages/tracer/tests/e2e/allFeatures.decorator.test.ts b/packages/tracer/tests/e2e/allFeatures.decorator.test.ts index c4a1a904f0..9550628171 100644 --- a/packages/tracer/tests/e2e/allFeatures.decorator.test.ts +++ b/packages/tracer/tests/e2e/allFeatures.decorator.test.ts @@ -4,15 +4,12 @@ * @group e2e/tracer/decorator */ import { - defaultRuntime, - findAndGetStackOutputValue, - generateTestUniqueName, - isValidRuntimeKey, TestStack, + TestDynamodbTable, } from '@aws-lambda-powertools/testing-utils'; import { XRayClient } from '@aws-sdk/client-xray'; import { join } from 'node:path'; -import { functionFactory, tableFactory } from '../helpers/factories'; +import { TracerTestNodejsFunction } from '../helpers/resources'; import { assertAnnotation, assertErrorAndFault, @@ -26,12 +23,7 @@ import { splitSegmentsByName, } from '../helpers/tracesUtils'; import { - expectedCustomAnnotationKey, - expectedCustomAnnotationValue, - expectedCustomErrorMessage, - expectedCustomMetadataKey, - expectedCustomMetadataValue, - expectedCustomResponseValue, + commonEnvironmentVars, RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, TEARDOWN_TIMEOUT, @@ -48,18 +40,12 @@ import { * Using the same one will result in traces from different test cases mixing up. */ describe(`Tracer E2E tests, all features with decorator instantiation`, () => { - const runtime: string = process.env.RUNTIME || defaultRuntime; - - if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key value: ${runtime}`); - } - - const testName = generateTestUniqueName({ - testPrefix: RESOURCE_NAME_PREFIX, - runtime, - testName: 'AllFeatures-Decorator', + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'AllFeatures-Decorator', + }, }); - const testStack = new TestStack(testName); // Location of the lambda function code const lambdaFunctionCodeFilePath = join( @@ -71,74 +57,88 @@ describe(`Tracer E2E tests, all features with decorator instantiation`, () => { /** * Table used by all functions to make an SDK call */ - const testTable = tableFactory({ + const testTable = new TestDynamodbTable( testStack, - testName, - tableSuffix: 'TestTable', - }); + {}, + { + nameSuffix: 'TestTable', + } + ); /** * Function #1 is with all flags enabled. */ let fnNameAllFlagsEnabled: string; - const fnAllFlagsEnabled = functionFactory({ + const fnAllFlagsEnabled = new TracerTestNodejsFunction( testStack, - testName, - functionSuffix: 'AllFlagsOn', - lambdaFunctionCodeFilePath, - environment: { - TEST_TABLE_NAME: testTable.tableName, + { + entry: lambdaFunctionCodeFilePath, + environment: { + TEST_TABLE_NAME: testTable.tableName, + }, }, - }); + { + nameSuffix: 'AllFlagsOn', + } + ); testTable.grantWriteData(fnAllFlagsEnabled); /** * Function #2 doesn't capture error or response */ let fnNameNoCaptureErrorOrResponse: string; - const fnNoCaptureErrorOrResponse = functionFactory({ + const fnNoCaptureErrorOrResponse = new TracerTestNodejsFunction( testStack, - testName, - functionSuffix: 'NoCaptureErrOrResp', - lambdaFunctionCodeFilePath, - environment: { - TEST_TABLE_NAME: testTable.tableName, - POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'false', - POWERTOOLS_TRACER_CAPTURE_ERROR: 'false', + { + entry: lambdaFunctionCodeFilePath, + environment: { + TEST_TABLE_NAME: testTable.tableName, + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'false', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'false', + }, }, - }); + { + nameSuffix: 'NoCaptureErrOrResp', + } + ); testTable.grantWriteData(fnNoCaptureErrorOrResponse); /** * Function #3 disables tracer */ let fnNameTracerDisabled: string; - const fnTracerDisabled = functionFactory({ + const fnTracerDisabled = new TracerTestNodejsFunction( testStack, - testName, - functionSuffix: 'TracerDisabled', - lambdaFunctionCodeFilePath, - environment: { - TEST_TABLE_NAME: testTable.tableName, - POWERTOOLS_TRACE_ENABLED: 'false', + { + entry: lambdaFunctionCodeFilePath, + environment: { + TEST_TABLE_NAME: testTable.tableName, + POWERTOOLS_TRACE_ENABLED: 'false', + }, }, - }); + { + nameSuffix: 'TracerDisabled', + } + ); testTable.grantWriteData(fnTracerDisabled); /** * Function #4 disables capture response via decorator options */ let fnNameCaptureResponseOff: string; - const fnCaptureResponseOff = functionFactory({ + const fnCaptureResponseOff = new TracerTestNodejsFunction( testStack, - testName, - functionSuffix: 'CaptureResponseOff', - lambdaFunctionCodeFilePath, - handler: 'handlerWithCaptureResponseFalse', - environment: { - TEST_TABLE_NAME: testTable.tableName, + { + entry: lambdaFunctionCodeFilePath, + handler: 'handlerWithCaptureResponseFalse', + environment: { + TEST_TABLE_NAME: testTable.tableName, + }, }, - }); + { + nameSuffix: 'CaptureResponseOff', + } + ); testTable.grantWriteData(fnCaptureResponseOff); const xrayClient = new XRayClient({}); @@ -146,22 +146,16 @@ describe(`Tracer E2E tests, all features with decorator instantiation`, () => { beforeAll(async () => { // Deploy the stack - const outputs = await testStack.deploy(); + await testStack.deploy(); // Get the actual function names from the stack outputs - fnNameAllFlagsEnabled = findAndGetStackOutputValue(outputs, 'AllFlagsOn'); - fnNameNoCaptureErrorOrResponse = findAndGetStackOutputValue( - outputs, - 'NoCaptureErrOrResp' - ); - fnNameTracerDisabled = findAndGetStackOutputValue( - outputs, - 'TracerDisabled' - ); - fnNameCaptureResponseOff = findAndGetStackOutputValue( - outputs, - 'CaptureResponseOff' - ); + fnNameAllFlagsEnabled = testStack.findAndGetStackOutputValue('AllFlagsOn'); + fnNameNoCaptureErrorOrResponse = + testStack.findAndGetStackOutputValue('NoCaptureErrOrResp'); + fnNameTracerDisabled = + testStack.findAndGetStackOutputValue('TracerDisabled'); + fnNameCaptureResponseOff = + testStack.findAndGetStackOutputValue('CaptureResponseOff'); // Invoke all functions await Promise.all([ @@ -181,6 +175,9 @@ describe(`Tracer E2E tests, all features with decorator instantiation`, () => { it( 'should generate all custom traces', async () => { + const { EXPECTED_CUSTOM_ERROR_MESSAGE: expectedCustomErrorMessage } = + commonEnvironmentVars; + const tracesWhenAllFlagsEnabled = await getTraces( xrayClient, startTime, @@ -241,6 +238,14 @@ describe(`Tracer E2E tests, all features with decorator instantiation`, () => { it( 'should have correct annotations and metadata', async () => { + const { + EXPECTED_CUSTOM_ANNOTATION_KEY: expectedCustomAnnotationKey, + EXPECTED_CUSTOM_ANNOTATION_VALUE: expectedCustomAnnotationValue, + EXPECTED_CUSTOM_METADATA_KEY: expectedCustomMetadataKey, + EXPECTED_CUSTOM_METADATA_VALUE: expectedCustomMetadataValue, + EXPECTED_CUSTOM_RESPONSE_VALUE: expectedCustomResponseValue, + } = commonEnvironmentVars; + const tracesWhenAllFlagsEnabled = await getTraces( xrayClient, startTime, @@ -259,7 +264,7 @@ describe(`Tracer E2E tests, all features with decorator instantiation`, () => { assertAnnotation({ annotations, isColdStart, - expectedServiceName: fnNameAllFlagsEnabled, + expectedServiceName: 'AllFlagsOn', expectedCustomAnnotationKey, expectedCustomAnnotationValue, }); @@ -267,16 +272,16 @@ describe(`Tracer E2E tests, all features with decorator instantiation`, () => { if (!metadata) { fail('metadata is missing'); } - expect( - metadata[fnNameAllFlagsEnabled][expectedCustomMetadataKey] - ).toEqual(expectedCustomMetadataValue); + expect(metadata['AllFlagsOn'][expectedCustomMetadataKey]).toEqual( + expectedCustomMetadataValue + ); const shouldThrowAnError = i === invocationCount - 1; if (!shouldThrowAnError) { // Assert that the metadata object contains the response - expect( - metadata[fnNameAllFlagsEnabled]['index.handler response'] - ).toEqual(expectedCustomResponseValue); + expect(metadata['AllFlagsOn']['index.handler response']).toEqual( + expectedCustomResponseValue + ); } } }, @@ -350,6 +355,9 @@ describe(`Tracer E2E tests, all features with decorator instantiation`, () => { it( 'should not capture response when captureResponse is set to false', async () => { + const { EXPECTED_CUSTOM_ERROR_MESSAGE: expectedCustomErrorMessage } = + commonEnvironmentVars; + const tracesWithCaptureResponseFalse = await getTraces( xrayClient, startTime, diff --git a/packages/tracer/tests/e2e/allFeatures.manual.test.ts b/packages/tracer/tests/e2e/allFeatures.manual.test.ts index e167cde677..e3d6c427f4 100644 --- a/packages/tracer/tests/e2e/allFeatures.manual.test.ts +++ b/packages/tracer/tests/e2e/allFeatures.manual.test.ts @@ -4,15 +4,12 @@ * @group e2e/tracer/manual */ import { - defaultRuntime, - findAndGetStackOutputValue, - generateTestUniqueName, - isValidRuntimeKey, + TestDynamodbTable, TestStack, } from '@aws-lambda-powertools/testing-utils'; import { XRayClient } from '@aws-sdk/client-xray'; import { join } from 'path'; -import { functionFactory, tableFactory } from '../helpers/factories'; +import { TracerTestNodejsFunction } from '../helpers/resources'; import { assertAnnotation, assertErrorAndFault, @@ -27,12 +24,7 @@ import { } from '../helpers/tracesUtils'; import type { ParsedTrace } from '../helpers/traceUtils.types'; import { - expectedCustomAnnotationKey, - expectedCustomAnnotationValue, - expectedCustomErrorMessage, - expectedCustomMetadataKey, - expectedCustomMetadataValue, - expectedCustomResponseValue, + commonEnvironmentVars, RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, TEARDOWN_TIMEOUT, @@ -40,18 +32,12 @@ import { } from './constants'; describe(`Tracer E2E tests, all features with manual instantiation`, () => { - const runtime: string = process.env.RUNTIME || defaultRuntime; - - if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key value: ${runtime}`); - } - - const testName = generateTestUniqueName({ - testPrefix: RESOURCE_NAME_PREFIX, - runtime, - testName: 'AllFeatures-Manual', + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'AllFeatures-Manual', + }, }); - const testStack = new TestStack(testName); // Location of the lambda function code const lambdaFunctionCodeFilePath = join( @@ -63,22 +49,27 @@ describe(`Tracer E2E tests, all features with manual instantiation`, () => { /** * Table used by all functions to make an SDK call */ - const testTable = tableFactory({ + const testTable = new TestDynamodbTable( testStack, - testName, - tableSuffix: 'TestTable', - }); + {}, + { + nameSuffix: 'TestTable', + } + ); let fnNameAllFlagsEnabled: string; - const fnAllFlagsEnabled = functionFactory({ + const fnAllFlagsEnabled = new TracerTestNodejsFunction( testStack, - testName, - functionSuffix: 'AllFlagsOn', - lambdaFunctionCodeFilePath, - environment: { - TEST_TABLE_NAME: testTable.tableName, + { + entry: lambdaFunctionCodeFilePath, + environment: { + TEST_TABLE_NAME: testTable.tableName, + }, }, - }); + { + nameSuffix: 'AllFlagsOn', + } + ); testTable.grantWriteData(fnAllFlagsEnabled); const xrayClient = new XRayClient({}); @@ -87,10 +78,10 @@ describe(`Tracer E2E tests, all features with manual instantiation`, () => { beforeAll(async () => { // Deploy the stack - const outputs = await testStack.deploy(); + await testStack.deploy(); // Get the actual function names from the stack outputs - fnNameAllFlagsEnabled = findAndGetStackOutputValue(outputs, 'AllFlagsOn'); + fnNameAllFlagsEnabled = testStack.findAndGetStackOutputValue('AllFlagsOn'); // Invoke all test cases await invokeAllTestCases(fnNameAllFlagsEnabled, invocationCount); @@ -114,6 +105,9 @@ describe(`Tracer E2E tests, all features with manual instantiation`, () => { it( 'should generate all custom traces', async () => { + const { EXPECTED_CUSTOM_ERROR_MESSAGE: expectedCustomErrorMessage } = + commonEnvironmentVars; + expect(sortedTraces.length).toBe(invocationCount); // Assess @@ -163,6 +157,14 @@ describe(`Tracer E2E tests, all features with manual instantiation`, () => { it( 'should have correct annotations and metadata', async () => { + const { + EXPECTED_CUSTOM_ANNOTATION_KEY: expectedCustomAnnotationKey, + EXPECTED_CUSTOM_ANNOTATION_VALUE: expectedCustomAnnotationValue, + EXPECTED_CUSTOM_METADATA_KEY: expectedCustomMetadataKey, + EXPECTED_CUSTOM_METADATA_VALUE: expectedCustomMetadataValue, + EXPECTED_CUSTOM_RESPONSE_VALUE: expectedCustomResponseValue, + } = commonEnvironmentVars; + for (let i = 0; i < invocationCount; i++) { const trace = sortedTraces[i]; const invocationSubsegment = getInvocationSubsegment(trace); @@ -173,7 +175,7 @@ describe(`Tracer E2E tests, all features with manual instantiation`, () => { assertAnnotation({ annotations, isColdStart, - expectedServiceName: fnNameAllFlagsEnabled, + expectedServiceName: 'AllFlagsOn', expectedCustomAnnotationKey, expectedCustomAnnotationValue, }); @@ -181,16 +183,16 @@ describe(`Tracer E2E tests, all features with manual instantiation`, () => { if (!metadata) { fail('metadata is missing'); } - expect( - metadata[fnNameAllFlagsEnabled][expectedCustomMetadataKey] - ).toEqual(expectedCustomMetadataValue); + expect(metadata['AllFlagsOn'][expectedCustomMetadataKey]).toEqual( + expectedCustomMetadataValue + ); const shouldThrowAnError = i === invocationCount - 1; if (!shouldThrowAnError) { // Assert that the metadata object contains the response - expect( - metadata[fnNameAllFlagsEnabled]['index.handler response'] - ).toEqual(expectedCustomResponseValue); + expect(metadata['AllFlagsOn']['index.handler response']).toEqual( + expectedCustomResponseValue + ); } } }, diff --git a/packages/tracer/tests/e2e/allFeatures.middy.test.ts b/packages/tracer/tests/e2e/allFeatures.middy.test.ts index 6dba0ebeb6..7fbe843943 100644 --- a/packages/tracer/tests/e2e/allFeatures.middy.test.ts +++ b/packages/tracer/tests/e2e/allFeatures.middy.test.ts @@ -4,15 +4,12 @@ * @group e2e/tracer/middy */ import { - defaultRuntime, - generateTestUniqueName, - isValidRuntimeKey, TestStack, - findAndGetStackOutputValue, + TestDynamodbTable, } from '@aws-lambda-powertools/testing-utils'; import { XRayClient } from '@aws-sdk/client-xray'; import { join } from 'node:path'; -import { functionFactory, tableFactory } from '../helpers/factories'; +import { TracerTestNodejsFunction } from '../helpers/resources'; import { assertAnnotation, assertErrorAndFault, @@ -26,12 +23,7 @@ import { splitSegmentsByName, } from '../helpers/tracesUtils'; import { - expectedCustomAnnotationKey, - expectedCustomAnnotationValue, - expectedCustomErrorMessage, - expectedCustomMetadataKey, - expectedCustomMetadataValue, - expectedCustomResponseValue, + commonEnvironmentVars, RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, TEARDOWN_TIMEOUT, @@ -48,18 +40,12 @@ import { * Using the same one will result in traces from different test cases mixing up. */ describe(`Tracer E2E tests, all features with middy instantiation`, () => { - const runtime: string = process.env.RUNTIME || defaultRuntime; - - if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key value: ${runtime}`); - } - - const testName = generateTestUniqueName({ - testPrefix: RESOURCE_NAME_PREFIX, - runtime, - testName: 'AllFeatures-Middy', + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'AllFeatures-Decorator', + }, }); - const testStack = new TestStack(testName); // Location of the lambda function code const lambdaFunctionCodeFilePath = join( @@ -71,74 +57,88 @@ describe(`Tracer E2E tests, all features with middy instantiation`, () => { /** * Table used by all functions to make an SDK call */ - const testTable = tableFactory({ + const testTable = new TestDynamodbTable( testStack, - testName, - tableSuffix: 'TestTable', - }); + {}, + { + nameSuffix: 'TestTable', + } + ); /** * Function #1 is with all flags enabled. */ let fnNameAllFlagsEnabled: string; - const fnAllFlagsEnabled = functionFactory({ + const fnAllFlagsEnabled = new TracerTestNodejsFunction( testStack, - testName, - functionSuffix: 'AllFlagsOn', - lambdaFunctionCodeFilePath, - environment: { - TEST_TABLE_NAME: testTable.tableName, + { + entry: lambdaFunctionCodeFilePath, + environment: { + TEST_TABLE_NAME: testTable.tableName, + }, }, - }); + { + nameSuffix: 'AllFlagsOn', + } + ); testTable.grantWriteData(fnAllFlagsEnabled); /** * Function #2 doesn't capture error or response */ let fnNameNoCaptureErrorOrResponse: string; - const fnNoCaptureErrorOrResponse = functionFactory({ + const fnNoCaptureErrorOrResponse = new TracerTestNodejsFunction( testStack, - testName, - functionSuffix: 'NoCaptureErrOrResp', - lambdaFunctionCodeFilePath, - environment: { - TEST_TABLE_NAME: testTable.tableName, - POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'false', - POWERTOOLS_TRACER_CAPTURE_ERROR: 'false', + { + entry: lambdaFunctionCodeFilePath, + environment: { + TEST_TABLE_NAME: testTable.tableName, + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'false', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'false', + }, }, - }); + { + nameSuffix: 'NoCaptureErrOrResp', + } + ); testTable.grantWriteData(fnNoCaptureErrorOrResponse); /** * Function #3 disables tracer */ let fnNameTracerDisabled: string; - const fnTracerDisabled = functionFactory({ + const fnTracerDisabled = new TracerTestNodejsFunction( testStack, - testName, - functionSuffix: 'TracerDisabled', - lambdaFunctionCodeFilePath, - environment: { - TEST_TABLE_NAME: testTable.tableName, - POWERTOOLS_TRACE_ENABLED: 'false', + { + entry: lambdaFunctionCodeFilePath, + environment: { + TEST_TABLE_NAME: testTable.tableName, + POWERTOOLS_TRACE_ENABLED: 'false', + }, }, - }); + { + nameSuffix: 'TracerDisabled', + } + ); testTable.grantWriteData(fnTracerDisabled); /** * Function #4 disables response capture via middleware option */ let fnNameCaptureResponseOff: string; - const fnCaptureResponseOff = functionFactory({ + const fnCaptureResponseOff = new TracerTestNodejsFunction( testStack, - testName, - functionSuffix: 'CaptureResponseOff', - lambdaFunctionCodeFilePath, - handler: 'handlerWithNoCaptureResponseViaMiddlewareOption', - environment: { - TEST_TABLE_NAME: testTable.tableName, + { + entry: lambdaFunctionCodeFilePath, + handler: 'handlerWithNoCaptureResponseViaMiddlewareOption', + environment: { + TEST_TABLE_NAME: testTable.tableName, + }, }, - }); + { + nameSuffix: 'CaptureResponseOff', + } + ); testTable.grantWriteData(fnCaptureResponseOff); const xrayClient = new XRayClient({}); @@ -146,22 +146,16 @@ describe(`Tracer E2E tests, all features with middy instantiation`, () => { beforeAll(async () => { // Deploy the stack - const outputs = await testStack.deploy(); + await testStack.deploy(); // Get the actual function names from the stack outputs - fnNameAllFlagsEnabled = findAndGetStackOutputValue(outputs, 'AllFlagsOn'); - fnNameNoCaptureErrorOrResponse = findAndGetStackOutputValue( - outputs, - 'NoCaptureErrOrResp' - ); - fnNameTracerDisabled = findAndGetStackOutputValue( - outputs, - 'TracerDisabled' - ); - fnNameCaptureResponseOff = findAndGetStackOutputValue( - outputs, - 'CaptureResponseOff' - ); + fnNameAllFlagsEnabled = testStack.findAndGetStackOutputValue('AllFlagsOn'); + fnNameNoCaptureErrorOrResponse = + testStack.findAndGetStackOutputValue('NoCaptureErrOrResp'); + fnNameTracerDisabled = + testStack.findAndGetStackOutputValue('TracerDisabled'); + fnNameCaptureResponseOff = + testStack.findAndGetStackOutputValue('CaptureResponseOff'); // Invoke all functions await Promise.all([ @@ -181,6 +175,9 @@ describe(`Tracer E2E tests, all features with middy instantiation`, () => { it( 'should generate all custom traces', async () => { + const { EXPECTED_CUSTOM_ERROR_MESSAGE: expectedCustomErrorMessage } = + commonEnvironmentVars; + const tracesWhenAllFlagsEnabled = await getTraces( xrayClient, startTime, @@ -238,6 +235,14 @@ describe(`Tracer E2E tests, all features with middy instantiation`, () => { it( 'should have correct annotations and metadata', async () => { + const { + EXPECTED_CUSTOM_ANNOTATION_KEY: expectedCustomAnnotationKey, + EXPECTED_CUSTOM_ANNOTATION_VALUE: expectedCustomAnnotationValue, + EXPECTED_CUSTOM_METADATA_KEY: expectedCustomMetadataKey, + EXPECTED_CUSTOM_METADATA_VALUE: expectedCustomMetadataValue, + EXPECTED_CUSTOM_RESPONSE_VALUE: expectedCustomResponseValue, + } = commonEnvironmentVars; + const tracesWhenAllFlagsEnabled = await getTraces( xrayClient, startTime, @@ -256,7 +261,7 @@ describe(`Tracer E2E tests, all features with middy instantiation`, () => { assertAnnotation({ annotations, isColdStart, - expectedServiceName: fnNameAllFlagsEnabled, + expectedServiceName: 'AllFlagsOn', expectedCustomAnnotationKey, expectedCustomAnnotationValue, }); @@ -264,16 +269,16 @@ describe(`Tracer E2E tests, all features with middy instantiation`, () => { if (!metadata) { fail('metadata is missing'); } - expect( - metadata[fnNameAllFlagsEnabled][expectedCustomMetadataKey] - ).toEqual(expectedCustomMetadataValue); + expect(metadata['AllFlagsOn'][expectedCustomMetadataKey]).toEqual( + expectedCustomMetadataValue + ); const shouldThrowAnError = i === invocationCount - 1; if (!shouldThrowAnError) { // Assert that the metadata object contains the response - expect( - metadata[fnNameAllFlagsEnabled]['index.handler response'] - ).toEqual(expectedCustomResponseValue); + expect(metadata['AllFlagsOn']['index.handler response']).toEqual( + expectedCustomResponseValue + ); } } }, @@ -344,6 +349,9 @@ describe(`Tracer E2E tests, all features with middy instantiation`, () => { it( 'should not capture response when captureResponse is set to false', async () => { + const { EXPECTED_CUSTOM_ERROR_MESSAGE: expectedCustomErrorMessage } = + commonEnvironmentVars; + const tracesWithNoCaptureResponse = await getTraces( xrayClient, startTime, diff --git a/packages/tracer/tests/e2e/asyncHandler.decorator.test.ts b/packages/tracer/tests/e2e/asyncHandler.decorator.test.ts index 8b2c49c13b..a37f0ad34b 100644 --- a/packages/tracer/tests/e2e/asyncHandler.decorator.test.ts +++ b/packages/tracer/tests/e2e/asyncHandler.decorator.test.ts @@ -4,15 +4,12 @@ * @group e2e/tracer/decorator-async-handler */ import { - defaultRuntime, - findAndGetStackOutputValue, - generateTestUniqueName, - isValidRuntimeKey, TestStack, + TestDynamodbTable, } from '@aws-lambda-powertools/testing-utils'; import { XRayClient } from '@aws-sdk/client-xray'; import { join } from 'node:path'; -import { functionFactory, tableFactory } from '../helpers/factories'; +import { TracerTestNodejsFunction } from '../helpers/resources'; import { assertAnnotation, assertErrorAndFault, @@ -26,13 +23,7 @@ import { splitSegmentsByName, } from '../helpers/tracesUtils'; import { - expectedCustomAnnotationKey, - expectedCustomAnnotationValue, - expectedCustomErrorMessage, - expectedCustomMetadataKey, - expectedCustomMetadataValue, - expectedCustomResponseValue, - expectedCustomSubSegmentName, + commonEnvironmentVars, RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, TEARDOWN_TIMEOUT, @@ -40,18 +31,12 @@ import { } from './constants'; describe(`Tracer E2E tests, async handler with decorator instantiation`, () => { - const runtime: string = process.env.RUNTIME || defaultRuntime; - - if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key value: ${runtime}`); - } - - const testName = generateTestUniqueName({ - testPrefix: RESOURCE_NAME_PREFIX, - runtime, - testName: 'Async-Decorator', + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'AllFeatures-Decorator', + }, }); - const testStack = new TestStack(testName); // Location of the lambda function code const lambdaFunctionCodeFilePath = join( @@ -63,42 +48,49 @@ describe(`Tracer E2E tests, async handler with decorator instantiation`, () => { /** * Table used by all functions to make an SDK call */ - const testTable = tableFactory({ + const testTable = new TestDynamodbTable( testStack, - testName, - tableSuffix: 'TestTable', - }); + {}, + { + nameSuffix: 'TestTable', + } + ); /** * Function #1 is with all flags enabled. */ let fnNameAllFlagsEnabled: string; - const fnAllFlagsEnabled = functionFactory({ + const fnAllFlagsEnabled = new TracerTestNodejsFunction( testStack, - testName, - functionSuffix: 'AllFlagsOn', - lambdaFunctionCodeFilePath, - environment: { - TEST_TABLE_NAME: testTable.tableName, + { + entry: lambdaFunctionCodeFilePath, + environment: { + TEST_TABLE_NAME: testTable.tableName, + }, }, - }); + { + nameSuffix: 'AllFlagsOn', + } + ); testTable.grantWriteData(fnAllFlagsEnabled); /** * Function #2 sets a custom subsegment name in the decorated method */ let fnNameCustomSubsegment: string; - const fnCustomSubsegmentName = functionFactory({ + const fnCustomSubsegmentName = new TracerTestNodejsFunction( testStack, - testName, - functionSuffix: 'CustomSubsegmentName', - lambdaFunctionCodeFilePath, - handler: 'handlerWithCustomSubsegmentNameInMethod', - environment: { - TEST_TABLE_NAME: testTable.tableName, - EXPECTED_CUSTOM_SUBSEGMENT_NAME: expectedCustomSubSegmentName, + { + entry: lambdaFunctionCodeFilePath, + handler: 'handlerWithCustomSubsegmentNameInMethod', + environment: { + TEST_TABLE_NAME: testTable.tableName, + }, }, - }); + { + nameSuffix: 'CustomSubsegmentName', + } + ); testTable.grantWriteData(fnCustomSubsegmentName); const xrayClient = new XRayClient({}); @@ -106,12 +98,11 @@ describe(`Tracer E2E tests, async handler with decorator instantiation`, () => { beforeAll(async () => { // Deploy the stack - const outputs = await testStack.deploy(); + await testStack.deploy(); // Get the actual function names from the stack outputs - fnNameAllFlagsEnabled = findAndGetStackOutputValue(outputs, 'AllFlagsOn'); - fnNameCustomSubsegment = findAndGetStackOutputValue( - outputs, + fnNameAllFlagsEnabled = testStack.findAndGetStackOutputValue('AllFlagsOn'); + fnNameCustomSubsegment = testStack.findAndGetStackOutputValue( 'CustomSubsegmentName' ); @@ -131,6 +122,9 @@ describe(`Tracer E2E tests, async handler with decorator instantiation`, () => { it( 'should generate all custom traces', async () => { + const { EXPECTED_CUSTOM_ERROR_MESSAGE: expectedCustomErrorMessage } = + commonEnvironmentVars; + const tracesWhenAllFlagsEnabled = await getTraces( xrayClient, startTime, @@ -191,6 +185,14 @@ describe(`Tracer E2E tests, async handler with decorator instantiation`, () => { it( 'should have correct annotations and metadata', async () => { + const { + EXPECTED_CUSTOM_ANNOTATION_KEY: expectedCustomAnnotationKey, + EXPECTED_CUSTOM_ANNOTATION_VALUE: expectedCustomAnnotationValue, + EXPECTED_CUSTOM_METADATA_KEY: expectedCustomMetadataKey, + EXPECTED_CUSTOM_METADATA_VALUE: expectedCustomMetadataValue, + EXPECTED_CUSTOM_RESPONSE_VALUE: expectedCustomResponseValue, + } = commonEnvironmentVars; + const traces = await getTraces( xrayClient, startTime, @@ -209,7 +211,7 @@ describe(`Tracer E2E tests, async handler with decorator instantiation`, () => { assertAnnotation({ annotations, isColdStart, - expectedServiceName: fnNameAllFlagsEnabled, + expectedServiceName: 'AllFlagsOn', expectedCustomAnnotationKey, expectedCustomAnnotationValue, }); @@ -217,16 +219,16 @@ describe(`Tracer E2E tests, async handler with decorator instantiation`, () => { if (!metadata) { fail('metadata is missing'); } - expect( - metadata[fnNameAllFlagsEnabled][expectedCustomMetadataKey] - ).toEqual(expectedCustomMetadataValue); + expect(metadata['AllFlagsOn'][expectedCustomMetadataKey]).toEqual( + expectedCustomMetadataValue + ); const shouldThrowAnError = i === invocationsCount - 1; if (!shouldThrowAnError) { // Assert that the metadata object contains the response - expect( - metadata[fnNameAllFlagsEnabled]['index.handler response'] - ).toEqual(expectedCustomResponseValue); + expect(metadata['AllFlagsOn']['index.handler response']).toEqual( + expectedCustomResponseValue + ); } } }, @@ -236,6 +238,11 @@ describe(`Tracer E2E tests, async handler with decorator instantiation`, () => { it( 'should have a custom name as the subsegment name for the decorated method', async () => { + const { + EXPECTED_CUSTOM_ERROR_MESSAGE: expectedCustomErrorMessage, + EXPECTED_CUSTOM_SUBSEGMENT_NAME: expectedCustomSubSegmentName, + } = commonEnvironmentVars; + const tracesWhenCustomSubsegmentNameInMethod = await getTraces( xrayClient, startTime, diff --git a/packages/tracer/tests/e2e/constants.ts b/packages/tracer/tests/e2e/constants.ts index e303978585..7e817e1aac 100644 --- a/packages/tracer/tests/e2e/constants.ts +++ b/packages/tracer/tests/e2e/constants.ts @@ -7,20 +7,17 @@ const SETUP_TIMEOUT = 5 * ONE_MINUTE; const TEARDOWN_TIMEOUT = 5 * ONE_MINUTE; // Expected values for custom annotations, metadata, and response -const expectedCustomAnnotationKey = 'myAnnotation'; -const expectedCustomAnnotationValue = 'myValue'; -const expectedCustomMetadataKey = 'myMetadata'; -const expectedCustomMetadataValue = { bar: 'baz' }; -const expectedCustomResponseValue = { foo: 'bar' }; -const expectedCustomErrorMessage = 'An error has occurred'; -const expectedCustomSubSegmentName = 'mySubsegment'; -const commonEnvironmentVariables = { - EXPECTED_CUSTOM_ANNOTATION_KEY: expectedCustomAnnotationKey, - EXPECTED_CUSTOM_ANNOTATION_VALUE: expectedCustomAnnotationValue, - EXPECTED_CUSTOM_METADATA_KEY: expectedCustomMetadataKey, - EXPECTED_CUSTOM_METADATA_VALUE: JSON.stringify(expectedCustomMetadataValue), - EXPECTED_CUSTOM_RESPONSE_VALUE: JSON.stringify(expectedCustomResponseValue), - EXPECTED_CUSTOM_ERROR_MESSAGE: expectedCustomErrorMessage, +const commonEnvironmentVars = { + EXPECTED_CUSTOM_ANNOTATION_KEY: 'myAnnotation', + EXPECTED_CUSTOM_ANNOTATION_VALUE: 'myValue', + EXPECTED_CUSTOM_METADATA_KEY: 'myMetadata', + EXPECTED_CUSTOM_METADATA_VALUE: { bar: 'baz' }, + EXPECTED_CUSTOM_RESPONSE_VALUE: { foo: 'bar' }, + EXPECTED_CUSTOM_ERROR_MESSAGE: 'An error has occurred', + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', + POWERTOOLS_TRACE_ENABLED: 'true', + EXPECTED_CUSTOM_SUBSEGMENT_NAME: 'mySubsegment', }; export { @@ -29,12 +26,5 @@ export { TEST_CASE_TIMEOUT, SETUP_TIMEOUT, TEARDOWN_TIMEOUT, - expectedCustomAnnotationKey, - expectedCustomAnnotationValue, - expectedCustomMetadataKey, - expectedCustomMetadataValue, - expectedCustomResponseValue, - expectedCustomErrorMessage, - expectedCustomSubSegmentName, - commonEnvironmentVariables, + commonEnvironmentVars, }; diff --git a/packages/tracer/tests/helpers/factories.ts b/packages/tracer/tests/helpers/factories.ts deleted file mode 100644 index 90ea11da99..0000000000 --- a/packages/tracer/tests/helpers/factories.ts +++ /dev/null @@ -1,80 +0,0 @@ -import type { TestStack } from '@aws-lambda-powertools/testing-utils'; -import { - concatenateResourceName, - defaultRuntime, - TestNodejsFunction, - TEST_RUNTIMES, -} from '@aws-lambda-powertools/testing-utils'; -import { RemovalPolicy } from 'aws-cdk-lib'; -import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb'; -import { randomUUID } from 'node:crypto'; -import { commonEnvironmentVariables } from '../e2e/constants'; - -const functionFactory = ({ - testStack, - testName, - functionSuffix, - lambdaFunctionCodeFilePath, - handler, - environment = {}, -}: { - testStack: TestStack; - testName: string; - functionSuffix: string; - lambdaFunctionCodeFilePath: string; - handler?: string; - environment?: Record; -}): TestNodejsFunction => { - const runtime: string = process.env.RUNTIME || defaultRuntime; - - const functionName = concatenateResourceName({ - testName, - resourceName: functionSuffix, - }); - - return new TestNodejsFunction( - testStack.stack, - `fn-${randomUUID().substring(0, 5)}`, - { - functionName, - entry: lambdaFunctionCodeFilePath, - runtime: TEST_RUNTIMES[runtime as keyof typeof TEST_RUNTIMES], - handler, - environment: { - POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', - POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', - POWERTOOLS_TRACE_ENABLED: 'true', - EXPECTED_SERVICE_NAME: functionName, - ...commonEnvironmentVariables, - ...environment, - }, - }, - { - fnOutputKey: functionSuffix, - } - ); -}; - -const tableFactory = ({ - testStack, - testName, - tableSuffix, -}: { - testStack: TestStack; - testName: string; - tableSuffix: string; -}): Table => - new Table(testStack.stack, `table-${randomUUID().substring(0, 5)}`, { - tableName: concatenateResourceName({ - testName, - resourceName: tableSuffix, - }), - partitionKey: { - name: 'id', - type: AttributeType.STRING, - }, - billingMode: BillingMode.PAY_PER_REQUEST, - removalPolicy: RemovalPolicy.DESTROY, - }); - -export { functionFactory, tableFactory }; diff --git a/packages/tracer/tests/helpers/resources.ts b/packages/tracer/tests/helpers/resources.ts new file mode 100644 index 0000000000..6f46cb98d6 --- /dev/null +++ b/packages/tracer/tests/helpers/resources.ts @@ -0,0 +1,36 @@ +import type { + ExtraTestProps, + TestNodejsFunctionProps, + TestStack, +} from '@aws-lambda-powertools/testing-utils'; +import { TestNodejsFunction } from '@aws-lambda-powertools/testing-utils'; +import { commonEnvironmentVars } from '../e2e/constants'; + +class TracerTestNodejsFunction extends TestNodejsFunction { + public constructor( + scope: TestStack, + props: TestNodejsFunctionProps, + extraProps: ExtraTestProps + ) { + super( + scope, + { + ...props, + environment: { + ...commonEnvironmentVars, + EXPECTED_SERVICE_NAME: extraProps.nameSuffix, + EXPECTED_CUSTOM_METADATA_VALUE: JSON.stringify( + commonEnvironmentVars.EXPECTED_CUSTOM_METADATA_VALUE + ), + EXPECTED_CUSTOM_RESPONSE_VALUE: JSON.stringify( + commonEnvironmentVars.EXPECTED_CUSTOM_RESPONSE_VALUE + ), + ...props.environment, + }, + }, + extraProps + ); + } +} + +export { TracerTestNodejsFunction }; From e4d71733b8f781971c7fd815e7fe3d898cda8562 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 31 Aug 2023 00:44:18 +0200 Subject: [PATCH 08/18] improv: refactor tests to remove duplication --- .../tests/e2e/appConfigProvider.class.test.ts | 206 +++------- .../tests/e2e/dynamoDBProvider.class.test.ts | 366 +++++++---------- .../tests/e2e/secretsProvider.class.test.ts | 167 ++++---- .../tests/e2e/ssmProvider.class.test.ts | 164 ++++---- .../tests/helpers/cdkAspectGrantAccess.ts | 99 ----- .../tests/helpers/parametersUtils.ts | 223 ----------- .../parameters/tests/helpers/resources.ts | 377 ++++++++++++++++++ 7 files changed, 729 insertions(+), 873 deletions(-) delete mode 100644 packages/parameters/tests/helpers/cdkAspectGrantAccess.ts delete mode 100644 packages/parameters/tests/helpers/parametersUtils.ts create mode 100644 packages/parameters/tests/helpers/resources.ts diff --git a/packages/parameters/tests/e2e/appConfigProvider.class.test.ts b/packages/parameters/tests/e2e/appConfigProvider.class.test.ts index 250ebd28ee..6d28b3ef04 100644 --- a/packages/parameters/tests/e2e/appConfigProvider.class.test.ts +++ b/packages/parameters/tests/e2e/appConfigProvider.class.test.ts @@ -4,24 +4,14 @@ * @group e2e/parameters/appconfig/class */ import { - concatenateResourceName, - defaultRuntime, - generateTestUniqueName, invokeFunctionOnce, - isValidRuntimeKey, TestInvocationLogs, TestNodejsFunction, TestStack, - TEST_RUNTIMES, } from '@aws-lambda-powertools/testing-utils'; import { toBase64 } from '@aws-sdk/util-base64-node'; -import { Aspects } from 'aws-cdk-lib'; import { join } from 'node:path'; -import { ResourceAccessGranter } from '../helpers/cdkAspectGrantAccess'; -import { - createAppConfigConfigurationProfile, - createBaseAppConfigResources, -} from '../helpers/parametersUtils'; +import { TestAppConfigWithProfiles } from '../helpers/resources'; import { RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, @@ -82,65 +72,19 @@ import { * application and environment for all tests. */ describe(`Parameters E2E tests, AppConfig provider`, () => { - const runtime: string = process.env.RUNTIME || defaultRuntime; - - if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key value: ${runtime}`); - } - - const testName = generateTestUniqueName({ - testPrefix: RESOURCE_NAME_PREFIX, - runtime, - testName: 'AppConfig', + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'AppConfig', + }, }); - const testStack = new TestStack(testName); // Location of the lambda function code - const lambdaFunctionCodeFile = join( + const lambdaFunctionCodeFilePath = join( __dirname, 'appConfigProvider.class.test.functionCode.ts' ); - const functionName = concatenateResourceName({ - testName, - resourceName: 'appConfigProvider', - }); - - const applicationName = concatenateResourceName({ - testName, - resourceName: 'app', - }); - - const environmentName = concatenateResourceName({ - testName, - resourceName: 'env', - }); - - const deploymentStrategyName = concatenateResourceName({ - testName, - resourceName: 'immediate', - }); - - const freeFormJsonName = concatenateResourceName({ - testName, - resourceName: 'freeFormJson', - }); - - const freeFormYamlName = concatenateResourceName({ - testName, - resourceName: 'freeFormYaml', - }); - - const freeFormBase64PlainTextName = concatenateResourceName({ - testName, - resourceName: 'freeFormBase64PlainText', - }); - - const featureFlagName = concatenateResourceName({ - testName, - resourceName: 'featureFlag', - }); - const freeFormJsonValue = { foo: 'bar', }; @@ -169,98 +113,64 @@ describe(`Parameters E2E tests, AppConfig provider`, () => { beforeAll(async () => { // Prepare - new TestNodejsFunction(testStack.stack, functionName, { - functionName: functionName, - entry: lambdaFunctionCodeFile, - runtime: TEST_RUNTIMES[runtime], - environment: { - APPLICATION_NAME: applicationName, - ENVIRONMENT_NAME: environmentName, - FREEFORM_JSON_NAME: freeFormJsonName, - FREEFORM_YAML_NAME: freeFormYamlName, - FREEFORM_BASE64_ENCODED_PLAIN_TEXT_NAME: freeFormBase64PlainTextName, - FEATURE_FLAG_NAME: featureFlagName, + const testFunction = new TestNodejsFunction( + testStack, + { + entry: lambdaFunctionCodeFilePath, }, - }); - - // Create the base resources for an AppConfig application. - const { application, environment, deploymentStrategy } = - createBaseAppConfigResources({ - stack: testStack.stack, - applicationName, - environmentName, - deploymentStrategyName, - }); - - // Create configuration profiles for tests. - const freeFormJson = createAppConfigConfigurationProfile({ - stack: testStack.stack, - application, - environment, - deploymentStrategy, - name: freeFormJsonName, - type: 'AWS.Freeform', - content: { - content: JSON.stringify(freeFormJsonValue), - contentType: 'application/json', - }, - }); - - const freeFormYaml = createAppConfigConfigurationProfile({ - stack: testStack.stack, - application, - environment, - deploymentStrategy, - name: freeFormYamlName, - type: 'AWS.Freeform', - content: { - content: freeFormYamlValue, - contentType: 'application/x-yaml', - }, - }); - freeFormYaml.node.addDependency(freeFormJson); - - const freeFormBase64PlainText = createAppConfigConfigurationProfile({ - stack: testStack.stack, - application, - environment, - deploymentStrategy, - name: freeFormBase64PlainTextName, - type: 'AWS.Freeform', - content: { - content: freeFormBase64PlainTextValue, - contentType: 'text/plain', - }, - }); - freeFormBase64PlainText.node.addDependency(freeFormYaml); + { + nameSuffix: 'appConfigProvider', + } + ); - const featureFlag = createAppConfigConfigurationProfile({ - stack: testStack.stack, - application, - environment, - deploymentStrategy, - name: featureFlagName, - type: 'AWS.AppConfig.FeatureFlags', - content: { - content: JSON.stringify(featureFlagValue), - contentType: 'application/json', - }, + const appConfigResource = new TestAppConfigWithProfiles(testStack, { + profiles: [ + { + nameSuffix: 'freeFormJson', + type: 'AWS.Freeform', + content: { + content: JSON.stringify(freeFormJsonValue), + contentType: 'application/json', + }, + }, + { + nameSuffix: 'freeFormYaml', + type: 'AWS.Freeform', + content: { + content: freeFormYamlValue, + contentType: 'application/x-yaml', + }, + }, + { + nameSuffix: 'freeFormB64Plain', + type: 'AWS.Freeform', + content: { + content: freeFormBase64PlainTextValue, + contentType: 'text/plain', + }, + }, + { + nameSuffix: 'featureFlag', + type: 'AWS.AppConfig.FeatureFlags', + content: { + content: JSON.stringify(featureFlagValue), + contentType: 'application/json', + }, + }, + ], }); - featureFlag.node.addDependency(freeFormBase64PlainText); - - // Grant access to the Lambda function to the AppConfig resources. - Aspects.of(testStack.stack).add( - new ResourceAccessGranter([ - freeFormJson, - freeFormYaml, - freeFormBase64PlainText, - featureFlag, - ]) - ); + // Grant read permissions to the function + appConfigResource.grantReadData(testFunction); + // Add environment variables containing the resource names to the function + appConfigResource.addEnvVariablesToFunction(testFunction); // Deploy the stack await testStack.deploy(); + // Get the actual function names from the stack outputs + const functionName = + testStack.findAndGetStackOutputValue('appConfigProvider'); + // and invoke the Lambda function invocationLogs = await invokeFunctionOnce({ functionName, diff --git a/packages/parameters/tests/e2e/dynamoDBProvider.class.test.ts b/packages/parameters/tests/e2e/dynamoDBProvider.class.test.ts index e86bc6c9c0..59989d36bb 100644 --- a/packages/parameters/tests/e2e/dynamoDBProvider.class.test.ts +++ b/packages/parameters/tests/e2e/dynamoDBProvider.class.test.ts @@ -4,24 +4,14 @@ * @group e2e/parameters/dynamodb/class */ import { - concatenateResourceName, - defaultRuntime, - generateTestUniqueName, invokeFunctionOnce, - isValidRuntimeKey, TestInvocationLogs, TestNodejsFunction, TestStack, - TEST_RUNTIMES, } from '@aws-lambda-powertools/testing-utils'; -import { Aspects } from 'aws-cdk-lib'; import { AttributeType } from 'aws-cdk-lib/aws-dynamodb'; import { join } from 'node:path'; -import { ResourceAccessGranter } from '../helpers/cdkAspectGrantAccess'; -import { - createDynamoDBTable, - putDynamoDBItem, -} from '../helpers/parametersUtils'; +import { TestDynamodbTableWithItems } from '../helpers/resources'; import { RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, @@ -106,51 +96,19 @@ import { * Get a cached parameter and force retrieval. This also uses the same custom SDK client that counts the number of calls to DynamoDB. */ describe(`Parameters E2E tests, dynamoDB provider`, () => { - const runtime: string = process.env.RUNTIME || defaultRuntime; - - if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key value: ${runtime}`); - } - - const testName = generateTestUniqueName({ - testPrefix: RESOURCE_NAME_PREFIX, - runtime, - testName: 'AllFeatures-Decorator', + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'DynamoDBProvider', + }, }); - const testStack = new TestStack(testName); // Location of the lambda function code - const lambdaFunctionCodeFile = join( + const lambdaFunctionCodeFilePath = join( __dirname, 'dynamoDBProvider.class.test.functionCode.ts' ); - const functionName = concatenateResourceName({ - testName, - resourceName: 'dynamoDBProvider', - }); - - // Parameters to be used by Parameters in the Lambda function - const tableGet = concatenateResourceName({ - testName, - resourceName: 'Table-Get', - }); - - const tableGetMultiple = concatenateResourceName({ - testName, - resourceName: 'Table-GetMultiple', - }); - - const tableGetCustomkeys = concatenateResourceName({ - testName, - resourceName: 'Table-GetCustomKeys', - }); - - const tableGetMultipleCustomkeys = concatenateResourceName({ - testName, - resourceName: 'Table-GetMultipleCustomKeys', - }); - const keyAttr = 'key'; const sortAttr = 'sort'; const valueAttr = 'val'; @@ -159,193 +117,153 @@ describe(`Parameters E2E tests, dynamoDB provider`, () => { beforeAll(async () => { // Prepare - new TestNodejsFunction(testStack.stack, functionName, { - functionName, - entry: lambdaFunctionCodeFile, - runtime: TEST_RUNTIMES[runtime], - environment: { - TABLE_GET: tableGet, - TABLE_GET_MULTIPLE: tableGetMultiple, - TABLE_GET_CUSTOM_KEYS: tableGetCustomkeys, - TABLE_GET_MULTIPLE_CUSTOM_KEYS: tableGetMultipleCustomkeys, - KEY_ATTR: keyAttr, - SORT_ATTR: sortAttr, - VALUE_ATTR: valueAttr, - }, - }); - - // Create the DynamoDB tables - const ddbTableGet = createDynamoDBTable({ - stack: testStack.stack, - id: 'Table-get', - tableName: tableGet, - partitionKey: { - name: 'id', - type: AttributeType.STRING, - }, - }); - const ddbTableGetMultiple = createDynamoDBTable({ - stack: testStack.stack, - id: 'Table-getMultiple', - tableName: tableGetMultiple, - partitionKey: { - name: 'id', - type: AttributeType.STRING, - }, - sortKey: { - name: 'sk', - type: AttributeType.STRING, - }, - }); - const ddbTableGetCustomKeys = createDynamoDBTable({ - stack: testStack.stack, - id: 'Table-getCustomKeys', - tableName: tableGetCustomkeys, - partitionKey: { - name: keyAttr, - type: AttributeType.STRING, - }, - }); - const ddbTabelGetMultipleCustomKeys = createDynamoDBTable({ - stack: testStack.stack, - id: 'Table-getMultipleCustomKeys', - tableName: tableGetMultipleCustomkeys, - partitionKey: { - name: keyAttr, - type: AttributeType.STRING, - }, - sortKey: { - name: sortAttr, - type: AttributeType.STRING, + const testFunction = new TestNodejsFunction( + testStack, + { + entry: lambdaFunctionCodeFilePath, + environment: { + KEY_ATTR: keyAttr, + SORT_ATTR: sortAttr, + VALUE_ATTR: valueAttr, + }, }, - }); - - // Give the Lambda access to the DynamoDB tables - Aspects.of(testStack.stack).add( - new ResourceAccessGranter([ - ddbTableGet, - ddbTableGetMultiple, - ddbTableGetCustomKeys, - ddbTabelGetMultipleCustomKeys, - ]) + { + nameSuffix: 'dynamoDBProvider', + } ); - // Seed tables with test data - // Test 1 - putDynamoDBItem({ - stack: testStack.stack, - id: 'my-param-test1', - table: ddbTableGet, - item: { - id: 'my-param', - value: 'foo', - }, - }); - - // Test 2 - putDynamoDBItem({ - stack: testStack.stack, - id: 'my-param-test2-a', - table: ddbTableGetMultiple, - item: { - id: 'my-params', - sk: 'config', - value: 'bar', - }, - }); - putDynamoDBItem({ - stack: testStack.stack, - id: 'my-param-test2-b', - table: ddbTableGetMultiple, - item: { - id: 'my-params', - sk: 'key', - value: 'baz', - }, - }); - - // Test 3 - putDynamoDBItem({ - stack: testStack.stack, - id: 'my-param-test3', - table: ddbTableGetCustomKeys, - item: { - [keyAttr]: 'my-param', - [valueAttr]: 'foo', - }, - }); - - // Test 4 - putDynamoDBItem({ - stack: testStack.stack, - id: 'my-param-test4-a', - table: ddbTabelGetMultipleCustomKeys, - item: { - [keyAttr]: 'my-params', - [sortAttr]: 'config', - [valueAttr]: 'bar', - }, - }); - putDynamoDBItem({ - stack: testStack.stack, - id: 'my-param-test4-b', - table: ddbTabelGetMultipleCustomKeys, - item: { - [keyAttr]: 'my-params', - [sortAttr]: 'key', - [valueAttr]: 'baz', - }, - }); - - // Test 5 - putDynamoDBItem({ - stack: testStack.stack, - id: 'my-param-test5', - table: ddbTableGet, - item: { - id: 'my-param-json', - value: JSON.stringify({ foo: 'bar' }), - }, - }); - - // Test 6 - putDynamoDBItem({ - stack: testStack.stack, - id: 'my-param-test6', - table: ddbTableGet, - item: { - id: 'my-param-binary', - value: 'YmF6', // base64 encoded 'baz' + // Table for Test 1, 5, 6 + const tableGet = new TestDynamodbTableWithItems( + testStack, + {}, + { + nameSuffix: 'Table-Get', + items: [ + { + id: 'my-param', + value: 'foo', + }, + { + id: 'my-param-json', + value: JSON.stringify({ foo: 'bar' }), + }, + { + id: 'my-param-binary', + value: 'YmF6', // base64 encoded 'baz' + }, + ], + } + ); + tableGet.grantReadData(testFunction); + testFunction.addEnvironment('TABLE_GET', tableGet.tableName); + // Table for Test 2, 7 + const tableGetMultiple = new TestDynamodbTableWithItems( + testStack, + { + sortKey: { + name: 'sk', + type: AttributeType.STRING, + }, }, - }); - - // Test 7 - putDynamoDBItem({ - stack: testStack.stack, - id: 'my-param-test7-a', - table: ddbTableGetMultiple, - item: { - id: 'my-encoded-params', - sk: 'config.json', - value: JSON.stringify({ foo: 'bar' }), + { + nameSuffix: 'Table-GetMultiple', + items: [ + { + id: 'my-params', + sk: 'config', + value: 'bar', + }, + { + id: 'my-params', + sk: 'key', + value: 'baz', + }, + { + id: 'my-encoded-params', + sk: 'config.json', + value: JSON.stringify({ foo: 'bar' }), + }, + { + id: 'my-encoded-params', + sk: 'key.binary', + value: 'YmF6', // base64 encoded 'baz' + }, + ], + } + ); + tableGetMultiple.grantReadData(testFunction); + testFunction.addEnvironment( + 'TABLE_GET_MULTIPLE', + tableGetMultiple.tableName + ); + // Table for Test 3 + const tableGetCustomkeys = new TestDynamodbTableWithItems( + testStack, + { + partitionKey: { + name: keyAttr, + type: AttributeType.STRING, + }, }, - }); - putDynamoDBItem({ - stack: testStack.stack, - id: 'my-param-test7-b', - table: ddbTableGetMultiple, - item: { - id: 'my-encoded-params', - sk: 'key.binary', - value: 'YmF6', // base64 encoded 'baz' + { + nameSuffix: 'Table-GetCustomKeys', + items: [ + { + [keyAttr]: 'my-param', + [valueAttr]: 'foo', + }, + ], + } + ); + tableGetCustomkeys.grantReadData(testFunction); + testFunction.addEnvironment( + 'TABLE_GET_CUSTOM_KEYS', + tableGetCustomkeys.tableName + ); + // Table for Test 4 + const tableGetMultipleCustomkeys = new TestDynamodbTableWithItems( + testStack, + { + partitionKey: { + name: keyAttr, + type: AttributeType.STRING, + }, + sortKey: { + name: sortAttr, + type: AttributeType.STRING, + }, }, - }); + { + nameSuffix: 'Table-GetMultipleCustomKeys', + items: [ + { + [keyAttr]: 'my-params', + [sortAttr]: 'config', + [valueAttr]: 'bar', + }, + { + [keyAttr]: 'my-params', + [sortAttr]: 'key', + [valueAttr]: 'baz', + }, + ], + } + ); + tableGetMultipleCustomkeys.grantReadData(testFunction); + testFunction.addEnvironment( + 'TABLE_GET_MULTIPLE_CUSTOM_KEYS', + tableGetMultipleCustomkeys.tableName + ); // Test 8 & 9 use the same items as Test 1 // Deploy the stack await testStack.deploy(); + // Get the actual function names from the stack outputs + const functionName = + testStack.findAndGetStackOutputValue('dynamoDBProvider'); + // and invoke the Lambda function invocationLogs = await invokeFunctionOnce({ functionName, diff --git a/packages/parameters/tests/e2e/secretsProvider.class.test.ts b/packages/parameters/tests/e2e/secretsProvider.class.test.ts index 47ceb0620e..303aa37344 100644 --- a/packages/parameters/tests/e2e/secretsProvider.class.test.ts +++ b/packages/parameters/tests/e2e/secretsProvider.class.test.ts @@ -4,20 +4,14 @@ * @group e2e/parameters/secrets/class */ import { - concatenateResourceName, - defaultRuntime, - generateTestUniqueName, invokeFunctionOnce, - isValidRuntimeKey, TestInvocationLogs, TestNodejsFunction, TestStack, - TEST_RUNTIMES, } from '@aws-lambda-powertools/testing-utils'; -import { Aspects, SecretValue } from 'aws-cdk-lib'; -import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; +import { SecretValue } from 'aws-cdk-lib'; import { join } from 'node:path'; -import { ResourceAccessGranter } from '../helpers/cdkAspectGrantAccess'; +import { TestSecret } from '../helpers/resources'; import { RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, @@ -43,120 +37,107 @@ import { * */ describe(`Parameters E2E tests, Secrets Manager provider`, () => { - const runtime: string = process.env.RUNTIME || defaultRuntime; - - if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key: ${runtime}`); - } - - const testName = generateTestUniqueName({ - testPrefix: RESOURCE_NAME_PREFIX, - runtime, - testName: 'SecretsProvider', + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'SecretsProvider', + }, }); - const testStack = new TestStack(testName); // Location of the lambda function code - const lambdaFunctionCodeFile = join( + const lambdaFunctionCodeFilePath = join( __dirname, 'secretsProvider.class.test.functionCode.ts' ); - const functionName = concatenateResourceName({ - testName, - resourceName: 'secretsProvider', - }); - let invocationLogs: TestInvocationLogs; beforeAll(async () => { - // use unique names for each test to keep a clean state - const secretNamePlain = concatenateResourceName({ - testName, - resourceName: 'testSecretPlain', - }); - - const secretNameObject = concatenateResourceName({ - testName, - resourceName: 'testSecretObject', - }); - - const secretNameBinary = concatenateResourceName({ - testName, - resourceName: 'testSecretBinary', - }); - - const secretNamePlainCached = concatenateResourceName({ - testName, - resourceName: 'testSecretPlainCached', - }); - - const secretNamePlainForceFetch = concatenateResourceName({ - testName, - resourceName: 'testSecretPlainForceFetch', - }); - - new TestNodejsFunction(testStack.stack, functionName, { - functionName: functionName, - entry: lambdaFunctionCodeFile, - runtime: TEST_RUNTIMES[runtime], - environment: { - SECRET_NAME_PLAIN: secretNamePlain, - SECRET_NAME_OBJECT: secretNameObject, - SECRET_NAME_BINARY: secretNameBinary, - SECRET_NAME_PLAIN_CACHED: secretNamePlainCached, - SECRET_NAME_PLAIN_FORCE_FETCH: secretNamePlainForceFetch, + const testFunction = new TestNodejsFunction( + testStack, + { + entry: lambdaFunctionCodeFilePath, }, - }); + { + nameSuffix: 'secretsProvider', + } + ); - const secretString = new Secret(testStack.stack, 'testSecretPlain', { - secretName: secretNamePlain, - secretStringValue: SecretValue.unsafePlainText('foo'), - }); + const secretString = new TestSecret( + testStack, + { + secretStringValue: SecretValue.unsafePlainText('foo'), + }, + { + nameSuffix: 'testSecretPlain', + } + ); + secretString.grantRead(testFunction); + testFunction.addEnvironment('SECRET_NAME_PLAIN', secretString.secretName); - const secretObject = new Secret(testStack.stack, 'testSecretObject', { - secretName: secretNameObject, - secretObjectValue: { - foo: SecretValue.unsafePlainText('bar'), + const secretObject = new TestSecret( + testStack, + { + secretObjectValue: { + foo: SecretValue.unsafePlainText('bar'), + }, }, - }); + { + nameSuffix: 'testSecretObject', + } + ); + secretObject.grantRead(testFunction); + testFunction.addEnvironment('SECRET_NAME_OBJECT', secretObject.secretName); - const secretBinary = new Secret(testStack.stack, 'testSecretBinary', { - secretName: secretNameBinary, - secretStringValue: SecretValue.unsafePlainText('Zm9v'), // 'foo' encoded in base64 - }); + const secretBinary = new TestSecret( + testStack, + { + secretStringValue: SecretValue.unsafePlainText('Zm9v'), // 'foo' encoded in base64 + }, + { + nameSuffix: 'testSecretBinary', + } + ); + secretBinary.grantRead(testFunction); + testFunction.addEnvironment('SECRET_NAME_BINARY', secretBinary.secretName); - const secretStringCached = new Secret( - testStack.stack, - 'testSecretStringCached', + const secretStringCached = new TestSecret( + testStack, { - secretName: secretNamePlainCached, secretStringValue: SecretValue.unsafePlainText('foo'), + }, + { + nameSuffix: 'testSecretPlainCached', } ); + secretStringCached.grantRead(testFunction); + testFunction.addEnvironment( + 'SECRET_NAME_PLAIN_CACHED', + secretStringCached.secretName + ); - const secretStringForceFetch = new Secret( - testStack.stack, - 'testSecretStringForceFetch', + const secretStringForceFetch = new TestSecret( + testStack, { - secretName: secretNamePlainForceFetch, secretStringValue: SecretValue.unsafePlainText('foo'), + }, + { + nameSuffix: 'testSecretPlainForceFetch', } ); - - // add secrets here to grant lambda permisisons to access secrets - Aspects.of(testStack.stack).add( - new ResourceAccessGranter([ - secretString, - secretObject, - secretBinary, - secretStringCached, - secretStringForceFetch, - ]) + secretStringForceFetch.grantRead(testFunction); + testFunction.addEnvironment( + 'SECRET_NAME_PLAIN_FORCE_FETCH', + secretStringForceFetch.secretName ); + // Deploy the stack await testStack.deploy(); + // Get the actual function names from the stack outputs + const functionName = + testStack.findAndGetStackOutputValue('secretsProvider'); + invocationLogs = await invokeFunctionOnce({ functionName, }); diff --git a/packages/parameters/tests/e2e/ssmProvider.class.test.ts b/packages/parameters/tests/e2e/ssmProvider.class.test.ts index be0bc323c7..17dcc1dee3 100644 --- a/packages/parameters/tests/e2e/ssmProvider.class.test.ts +++ b/packages/parameters/tests/e2e/ssmProvider.class.test.ts @@ -4,21 +4,16 @@ * @group e2e/parameters/ssm/class */ import { - concatenateResourceName, - defaultRuntime, - generateTestUniqueName, invokeFunctionOnce, - isValidRuntimeKey, TestInvocationLogs, TestNodejsFunction, TestStack, - TEST_RUNTIMES, } from '@aws-lambda-powertools/testing-utils'; -import { Aspects } from 'aws-cdk-lib'; -import { StringParameter } from 'aws-cdk-lib/aws-ssm'; import { join } from 'node:path'; -import { ResourceAccessGranter } from '../helpers/cdkAspectGrantAccess'; -import { createSSMSecureString } from '../helpers/parametersUtils'; +import { + TestSecureStringParameter, + TestStringParameter, +} from '../helpers/resources'; import { RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, @@ -76,54 +71,26 @@ import { * check that we made two API calls */ describe(`Parameters E2E tests, SSM provider`, () => { - const runtime: string = process.env.RUNTIME || defaultRuntime; - - if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key value: ${runtime}`); - } - - const testName = generateTestUniqueName({ - testPrefix: RESOURCE_NAME_PREFIX, - runtime, - testName: 'SSMProvider', + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'AppConfig', + }, }); - const testStack = new TestStack(testName); // Location of the lambda function code - const lambdaFunctionCodeFile = join( + const lambdaFunctionCodeFilePath = join( __dirname, 'ssmProvider.class.test.functionCode.ts' ); - const functionName = concatenateResourceName({ - testName, - resourceName: 'ssmProvider', - }); - - // Parameter names to be used by Parameters in the Lambda function - const paramA = `/${concatenateResourceName({ - testName, - resourceName: 'param/a', - })}`; - - const paramB = `/${concatenateResourceName({ - testName, - resourceName: 'param/b', - })}`; - - const paramEncryptedA = `/${concatenateResourceName({ - testName, - resourceName: 'param-encrypted/a', - })}`; - - const paramEncryptedB = `/${concatenateResourceName({ - testName, - resourceName: 'param-encrypted/b', - })}`; - // Parameters values + let paramA: string; + let paramB: string; const paramAValue = 'foo'; const paramBValue = 'bar'; + let paramEncryptedA: string; + let paramEncryptedB: string; const paramEncryptedAValue = 'foo-encrypted'; const paramEncryptedBValue = 'bar-encrypted'; @@ -131,55 +98,80 @@ describe(`Parameters E2E tests, SSM provider`, () => { beforeAll(async () => { // Prepare - new TestNodejsFunction(testStack.stack, functionName, { - functionName: functionName, - entry: lambdaFunctionCodeFile, - runtime: TEST_RUNTIMES[runtime], - environment: { - PARAM_A: paramA, - PARAM_B: paramB, - PARAM_ENCRYPTED_A: paramEncryptedA, - PARAM_ENCRYPTED_B: paramEncryptedB, + const testFunction = new TestNodejsFunction( + testStack, + { + entry: lambdaFunctionCodeFilePath, }, - }); + { + nameSuffix: 'SsmProvider', + } + ); // Create SSM parameters - const parameterGetA = new StringParameter(testStack.stack, 'Param-a', { - parameterName: paramA, - stringValue: paramAValue, - }); - const parameterGetB = new StringParameter(testStack.stack, 'Param-b', { - parameterName: paramB, - stringValue: paramBValue, - }); - - const parameterEncryptedA = createSSMSecureString({ - stack: testStack.stack, - id: 'Param-encrypted-a', - name: paramEncryptedA, - value: paramEncryptedAValue, - }); + const parameterGetA = new TestStringParameter( + testStack, + { + stringValue: paramAValue, + }, + { + nameSuffix: 'get/a', + } + ); + parameterGetA.grantRead(testFunction); + testFunction.addEnvironment('PARAM_A', parameterGetA.parameterName); + const parameterGetB = new TestStringParameter( + testStack, + { + stringValue: paramBValue, + }, + { + nameSuffix: 'get/b', + } + ); + parameterGetB.grantRead(testFunction); + testFunction.addEnvironment('PARAM_B', parameterGetB.parameterName); - const parameterEncryptedB = createSSMSecureString({ - stack: testStack.stack, - id: 'Param-encrypted-b', - name: paramEncryptedB, - value: paramEncryptedBValue, - }); + const parameterEncryptedA = new TestSecureStringParameter( + testStack, + { + value: paramEncryptedAValue, + }, + { + nameSuffix: 'secure/a', + } + ); + parameterEncryptedA.grantReadData(testFunction); + testFunction.addEnvironment( + 'PARAM_ENCRYPTED_A', + parameterEncryptedA.parameterName + ); - // Give the Lambda function access to the SSM parameters - Aspects.of(testStack.stack).add( - new ResourceAccessGranter([ - parameterGetA, - parameterGetB, - parameterEncryptedA, - parameterEncryptedB, - ]) + const parameterEncryptedB = new TestSecureStringParameter( + testStack, + { + value: paramEncryptedBValue, + }, + { + nameSuffix: 'secure/b', + } + ); + parameterEncryptedB.grantReadData(testFunction); + testFunction.addEnvironment( + 'PARAM_ENCRYPTED_B', + parameterEncryptedB.parameterName ); // Deploy the stack await testStack.deploy(); + // Get the actual function names from the stack outputs + const functionName = testStack.findAndGetStackOutputValue('ssmProvider'); + paramA = testStack.findAndGetStackOutputValue('getaStr'); + paramB = testStack.findAndGetStackOutputValue('getbStr'); + paramEncryptedA = testStack.findAndGetStackOutputValue('secureaSecStr'); + paramEncryptedB = testStack.findAndGetStackOutputValue('securebSecStr'); + // and invoke the Lambda function invocationLogs = await invokeFunctionOnce({ functionName, diff --git a/packages/parameters/tests/helpers/cdkAspectGrantAccess.ts b/packages/parameters/tests/helpers/cdkAspectGrantAccess.ts deleted file mode 100644 index 5d832509df..0000000000 --- a/packages/parameters/tests/helpers/cdkAspectGrantAccess.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { IAspect, Stack } from 'aws-cdk-lib'; -import { IConstruct } from 'constructs'; -import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; -import { Table } from 'aws-cdk-lib/aws-dynamodb'; -import { PolicyStatement, Effect } from 'aws-cdk-lib/aws-iam'; -import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; -import { CfnDeployment } from 'aws-cdk-lib/aws-appconfig'; -import { StringParameter, IStringParameter } from 'aws-cdk-lib/aws-ssm'; - -const isStringParameterGeneric = ( - parameter: IConstruct -): parameter is StringParameter | IStringParameter => - parameter.hasOwnProperty('parameterArn'); - -/** - * An aspect that grants access to resources to a Lambda function. - * - * In our integration tests, we dynamically generate AWS CDK stacks that contain a Lambda function. - * We want to grant access to resources to the Lambda function, but we don't know the name of the - * Lambda function at the time we create the resources. Additionally, we want to keep the code - * that creates the stacks and functions as generic as possible. - * - * This aspect allows us to grant access to specific resources to all Lambda functions in a stack - * after the stack tree has been generated and before the stack is deployed. This aspect is - * used to grant access to different resource types (DynamoDB tables, SSM parameters, etc.). - * - * @see {@link https://docs.aws.amazon.com/cdk/v2/guide/aspects.html CDK Docs - Aspects} - */ -export class ResourceAccessGranter implements IAspect { - private readonly resources: - | Table[] - | Secret[] - | StringParameter[] - | IStringParameter[] - | CfnDeployment[]; - - public constructor( - resources: - | Table[] - | Secret[] - | StringParameter[] - | IStringParameter[] - | CfnDeployment[] - ) { - this.resources = resources; - } - - public visit(node: IConstruct): void { - // See that we're dealing with a Function - if (node instanceof NodejsFunction) { - // Grant access to the resources - this.resources.forEach( - ( - resource: - | Table - | Secret - | StringParameter - | IStringParameter - | CfnDeployment - ) => { - if (resource instanceof Table) { - resource.grantReadData(node); - } else if (resource instanceof Secret) { - resource.grantRead(node); - } else if (isStringParameterGeneric(resource)) { - resource.grantRead(node); - - // Grant access also to the path of the parameter - node.addToRolePolicy( - new PolicyStatement({ - effect: Effect.ALLOW, - actions: ['ssm:GetParametersByPath'], - resources: [ - resource.parameterArn.split(':').slice(0, -1).join(':'), - ], - }) - ); - } else if (resource instanceof CfnDeployment) { - const appConfigConfigurationArn = Stack.of(node).formatArn({ - service: 'appconfig', - resource: `application/${resource.applicationId}/environment/${resource.environmentId}/configuration/${resource.configurationProfileId}`, - }); - - node.addToRolePolicy( - new PolicyStatement({ - effect: Effect.ALLOW, - actions: [ - 'appconfig:StartConfigurationSession', - 'appconfig:GetLatestConfiguration', - ], - resources: [appConfigConfigurationArn], - }) - ); - } - } - ); - } - } -} diff --git a/packages/parameters/tests/helpers/parametersUtils.ts b/packages/parameters/tests/helpers/parametersUtils.ts deleted file mode 100644 index 14d7a14c64..0000000000 --- a/packages/parameters/tests/helpers/parametersUtils.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { Stack, RemovalPolicy } from 'aws-cdk-lib'; -import { PhysicalResourceId } from 'aws-cdk-lib/custom-resources'; -import { StringParameter, IStringParameter } from 'aws-cdk-lib/aws-ssm'; -import { Table, TableProps, BillingMode } from 'aws-cdk-lib/aws-dynamodb'; -import { - CfnApplication, - CfnConfigurationProfile, - CfnDeployment, - CfnDeploymentStrategy, - CfnEnvironment, - CfnHostedConfigurationVersion, -} from 'aws-cdk-lib/aws-appconfig'; -import { - AwsCustomResource, - AwsCustomResourcePolicy, -} from 'aws-cdk-lib/custom-resources'; -import { marshall } from '@aws-sdk/util-dynamodb'; - -export type CreateDynamoDBTableOptions = { - stack: Stack; - id: string; -} & TableProps; - -const createDynamoDBTable = (options: CreateDynamoDBTableOptions): Table => { - const { stack, id, ...tableProps } = options; - const props = { - billingMode: BillingMode.PAY_PER_REQUEST, - removalPolicy: RemovalPolicy.DESTROY, - ...tableProps, - }; - - return new Table(stack, id, props); -}; - -export type AppConfigResourcesOptions = { - stack: Stack; - applicationName: string; - environmentName: string; - deploymentStrategyName: string; -}; - -type AppConfigResourcesOutput = { - application: CfnApplication; - environment: CfnEnvironment; - deploymentStrategy: CfnDeploymentStrategy; -}; - -/** - * Utility function to create the base resources for an AppConfig application. - */ -const createBaseAppConfigResources = ( - options: AppConfigResourcesOptions -): AppConfigResourcesOutput => { - const { stack, applicationName, environmentName, deploymentStrategyName } = - options; - - // create a new app config application. - const application = new CfnApplication(stack, 'application', { - name: applicationName, - }); - - const environment = new CfnEnvironment(stack, 'environment', { - name: environmentName, - applicationId: application.ref, - }); - - const deploymentStrategy = new CfnDeploymentStrategy( - stack, - 'deploymentStrategy', - { - name: deploymentStrategyName, - deploymentDurationInMinutes: 0, - growthFactor: 100, - replicateTo: 'NONE', - finalBakeTimeInMinutes: 0, - } - ); - - return { - application, - environment, - deploymentStrategy, - }; -}; - -export type CreateAppConfigConfigurationProfileOptions = { - stack: Stack; - name: string; - application: CfnApplication; - environment: CfnEnvironment; - deploymentStrategy: CfnDeploymentStrategy; - type: 'AWS.Freeform' | 'AWS.AppConfig.FeatureFlags'; - content: { - contentType: 'application/json' | 'application/x-yaml' | 'text/plain'; - content: string; - }; -}; - -/** - * Utility function to create an AppConfig configuration profile and deployment. - */ -const createAppConfigConfigurationProfile = ( - options: CreateAppConfigConfigurationProfileOptions -): CfnDeployment => { - const { - stack, - name, - application, - environment, - deploymentStrategy, - type, - content, - } = options; - - const configProfile = new CfnConfigurationProfile( - stack, - `${name}-configProfile`, - { - name, - applicationId: application.ref, - locationUri: 'hosted', - type, - } - ); - - const configVersion = new CfnHostedConfigurationVersion( - stack, - `${name}-configVersion`, - { - applicationId: application.ref, - configurationProfileId: configProfile.ref, - ...content, - } - ); - - return new CfnDeployment(stack, `${name}-deployment`, { - applicationId: application.ref, - configurationProfileId: configProfile.ref, - configurationVersion: configVersion.ref, - deploymentStrategyId: deploymentStrategy.ref, - environmentId: environment.ref, - }); -}; - -export type CreateSSMSecureStringOptions = { - stack: Stack; - id: string; - name: string; - value: string; -}; - -const createSSMSecureString = ( - options: CreateSSMSecureStringOptions -): IStringParameter => { - const { stack, id, name, value } = options; - - const paramCreator = new AwsCustomResource(stack, `create-${id}`, { - onCreate: { - service: 'SSM', - action: 'putParameter', - parameters: { - Name: name, - Value: value, - Type: 'SecureString', - }, - physicalResourceId: PhysicalResourceId.of(id), - }, - onDelete: { - service: 'SSM', - action: 'deleteParameter', - parameters: { - Name: name, - }, - }, - policy: AwsCustomResourcePolicy.fromSdkCalls({ - resources: AwsCustomResourcePolicy.ANY_RESOURCE, - }), - installLatestAwsSdk: false, - }); - - const param = StringParameter.fromSecureStringParameterAttributes(stack, id, { - parameterName: name, - }); - param.node.addDependency(paramCreator); - - return param; -}; - -export type PutDynamoDBItemOptions = { - stack: Stack; - id: string; - table: Table; - item: Record; -}; - -const putDynamoDBItem = async ( - options: PutDynamoDBItemOptions -): Promise => { - const { stack, id, table, item } = options; - - new AwsCustomResource(stack, id, { - onCreate: { - service: 'DynamoDB', - action: 'putItem', - parameters: { - TableName: table.tableName, - Item: marshall(item), - }, - physicalResourceId: PhysicalResourceId.of(id), - }, - policy: AwsCustomResourcePolicy.fromSdkCalls({ - resources: [table.tableArn], - }), - }); -}; - -export { - createDynamoDBTable, - createBaseAppConfigResources, - createAppConfigConfigurationProfile, - createSSMSecureString, - putDynamoDBItem, -}; diff --git a/packages/parameters/tests/helpers/resources.ts b/packages/parameters/tests/helpers/resources.ts new file mode 100644 index 0000000000..87ca5800c6 --- /dev/null +++ b/packages/parameters/tests/helpers/resources.ts @@ -0,0 +1,377 @@ +import type { + ExtraTestProps, + TestDynamodbTableProps, + TestStack, +} from '@aws-lambda-powertools/testing-utils'; +import { + concatenateResourceName, + TestDynamodbTable, + TestNodejsFunction, +} from '@aws-lambda-powertools/testing-utils'; +import { marshall } from '@aws-sdk/util-dynamodb'; +import { CfnOutput, Stack } from 'aws-cdk-lib'; +import { + CfnApplication, + CfnConfigurationProfile, + CfnDeployment, + CfnDeploymentStrategy, + CfnEnvironment, + CfnHostedConfigurationVersion, +} from 'aws-cdk-lib/aws-appconfig'; +import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam'; +import type { SecretProps } from 'aws-cdk-lib/aws-secretsmanager'; +import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; +import type { StringParameterProps } from 'aws-cdk-lib/aws-ssm'; +import { IStringParameter, StringParameter } from 'aws-cdk-lib/aws-ssm'; +import { + AwsCustomResource, + AwsCustomResourcePolicy, + PhysicalResourceId, +} from 'aws-cdk-lib/custom-resources'; +import { Construct } from 'constructs'; +import { randomUUID } from 'node:crypto'; + +/** + * A secure string parameter that can be used in tests. + * + * It includes some default props and outputs the parameter name. + */ +class TestSecureStringParameter extends Construct { + public readonly parameterName: string; + public readonly secureString: IStringParameter; + + public constructor( + testStack: TestStack, + props: { + value: string; + }, + extraProps: ExtraTestProps + ) { + super( + testStack.stack, + concatenateResourceName({ + testName: testStack.testName, + resourceName: randomUUID(), + }) + ); + + const { value } = props; + + const name = `/secure/${randomUUID()}`; + + const secureStringCreator = new AwsCustomResource( + testStack.stack, + `create-${randomUUID()}`, + { + onCreate: { + service: 'SSM', + action: 'putParameter', + parameters: { + Name: name, + Value: value, + Type: 'SecureString', + }, + physicalResourceId: PhysicalResourceId.of(name), + }, + onDelete: { + service: 'SSM', + action: 'deleteParameter', + parameters: { + Name: name, + }, + }, + policy: AwsCustomResourcePolicy.fromSdkCalls({ + resources: AwsCustomResourcePolicy.ANY_RESOURCE, + }), + installLatestAwsSdk: false, + } + ); + + this.secureString = StringParameter.fromSecureStringParameterAttributes( + testStack.stack, + randomUUID(), + { + parameterName: name, + } + ); + this.secureString.node.addDependency(secureStringCreator); + + this.parameterName = this.secureString.parameterName; + + new CfnOutput(this, `${extraProps.nameSuffix.replace('/', '')}SecStr`, { + value: name, + }); + } + + /** + * Grant read access to the secure string to a function. + * + * @param fn The function to grant access to the secure string + */ + public grantReadData(fn: TestNodejsFunction): void { + this.secureString.grantRead(fn); + + // Grant access also to the path of the parameter + fn.addToRolePolicy( + new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['ssm:GetParametersByPath'], + resources: [ + this.secureString.parameterArn.split(':').slice(0, -1).join(':'), + ], + }) + ); + } +} + +/** + * A string parameter that can be used in tests. + */ +class TestStringParameter extends StringParameter { + public constructor( + testStack: TestStack, + props: Omit, + extraProps: ExtraTestProps + ) { + super( + testStack.stack, + concatenateResourceName({ + testName: testStack.testName, + resourceName: extraProps.nameSuffix, + }), + { + ...props, + parameterName: `/${extraProps.nameSuffix}`, + } + ); + + new CfnOutput(this, `${extraProps.nameSuffix.replace('/', '')}Str`, { + value: this.parameterName, + }); + } +} + +/** + * A secret that can be used in tests. + */ +class TestSecret extends Secret { + public constructor( + testStack: TestStack, + props: Omit, + extraProps: ExtraTestProps + ) { + super( + testStack.stack, + concatenateResourceName({ + testName: testStack.testName, + resourceName: extraProps.nameSuffix, + }), + { + ...props, + secretName: `/${extraProps.nameSuffix}`, + } + ); + } +} + +class TestDynamodbTableWithItems extends TestDynamodbTable { + public constructor( + testStack: TestStack, + props: TestDynamodbTableProps, + extraProps: ExtraTestProps & { + items: Record[]; + } + ) { + super(testStack, props, extraProps); + + const { items } = extraProps; + + const id = `putItems-${randomUUID()}`; + + new AwsCustomResource(testStack.stack, id, { + onCreate: { + service: 'DynamoDB', + action: 'batchWriteItem', + parameters: { + RequestItems: { + [this.tableName]: items.map((item) => ({ + PutRequest: { + Item: marshall(item), + }, + })), + }, + }, + physicalResourceId: PhysicalResourceId.of(id), + }, + policy: AwsCustomResourcePolicy.fromSdkCalls({ + resources: [this.tableArn], + }), + }); + } +} + +/** + * A set of AppConfig resources that can be used in tests. + */ +class TestAppConfigWithProfiles extends Construct { + private readonly application: CfnApplication; + private readonly deploymentStrategy: CfnDeploymentStrategy; + private readonly environment: CfnEnvironment; + private readonly profiles: CfnDeployment[] = []; + + public constructor( + testStack: TestStack, + props: { + profiles: { + nameSuffix: string; + type: 'AWS.Freeform' | 'AWS.AppConfig.FeatureFlags'; + content: { + contentType: 'application/json' | 'application/x-yaml' | 'text/plain'; + content: string; + }; + }[]; + } + ) { + super( + testStack.stack, + concatenateResourceName({ + testName: testStack.testName, + resourceName: randomUUID(), + }) + ); + + const { profiles } = props; + + this.application = new CfnApplication( + testStack.stack, + `app-${randomUUID()}`, + { + name: randomUUID(), + } + ); + + this.deploymentStrategy = new CfnDeploymentStrategy( + testStack.stack, + `de-${randomUUID()}`, + { + name: randomUUID(), + deploymentDurationInMinutes: 0, + growthFactor: 100, + replicateTo: 'NONE', + finalBakeTimeInMinutes: 0, + } + ); + + this.environment = new CfnEnvironment( + testStack.stack, + `ce-${randomUUID()}`, + { + name: randomUUID(), + applicationId: this.application.ref, + } + ); + + profiles.forEach((profile, index) => { + const configProfile = new CfnConfigurationProfile( + testStack.stack, + `cp-${randomUUID()}`, + { + name: randomUUID(), + applicationId: this.application.ref, + locationUri: 'hosted', + type: profile.type, + } + ); + + const configVersion = new CfnHostedConfigurationVersion( + testStack.stack, + `cv-${randomUUID()}`, + { + applicationId: this.application.ref, + configurationProfileId: configProfile.ref, + ...profile.content, + } + ); + + const deployment = new CfnDeployment( + testStack.stack, + concatenateResourceName({ + testName: testStack.testName, + resourceName: profile.nameSuffix, + }), + { + applicationId: this.application.ref, + configurationProfileId: configProfile.ref, + configurationVersion: configVersion.ref, + deploymentStrategyId: this.deploymentStrategy.ref, + environmentId: this.environment.ref, + } + ); + + if (index > 0 && this.profiles) { + deployment.node.addDependency(this.profiles[index - 1]); + } + + this.profiles.push(deployment); + }); + } + + /** + * Add the names of the AppConfig resources to the function as environment variables. + * + * @param fn The function to add the environment variables to + */ + public addEnvVariablesToFunction(fn: TestNodejsFunction): void { + fn.addEnvironment('APPLICATION_NAME', this.application.name); + fn.addEnvironment('ENVIRONMENT_NAME', this.environment.name); + fn.addEnvironment( + 'FREEFORM_JSON_NAME', + this.profiles[0].configurationProfileId + ); + fn.addEnvironment( + 'FREEFORM_YAML_NAME', + this.profiles[1].configurationProfileId + ); + fn.addEnvironment( + 'FREEFORM_BASE64_ENCODED_PLAIN_TEXT_NAME', + this.profiles[2].configurationProfileId + ); + fn.addEnvironment( + 'FEATURE_FLAG_NAME', + this.profiles[3].configurationProfileId + ); + } + + /** + * Grant access to all the profiles to a function. + * + * @param fn The function to grant access to the profiles + */ + public grantReadData(fn: TestNodejsFunction): void { + this.profiles.forEach((profile) => { + const appConfigConfigurationArn = Stack.of(fn).formatArn({ + service: 'appconfig', + resource: `application/${profile.applicationId}/environment/${profile.environmentId}/configuration/${profile.configurationProfileId}`, + }); + + fn.addToRolePolicy( + new PolicyStatement({ + effect: Effect.ALLOW, + actions: [ + 'appconfig:StartConfigurationSession', + 'appconfig:GetLatestConfiguration', + ], + resources: [appConfigConfigurationArn], + }) + ); + }); + } +} + +export { + TestSecureStringParameter, + TestStringParameter, + TestSecret, + TestDynamodbTableWithItems, + TestAppConfigWithProfiles, +}; From 3a8fd3c48961fbb2d49db6878047b1fde809673d Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 31 Aug 2023 00:54:45 +0200 Subject: [PATCH 09/18] improv: refactor tests to remove duplication --- layers/src/layer-publisher-stack.ts | 4 +- layers/tests/e2e/layerPublisher.test.ts | 111 +++++++++++------------- 2 files changed, 55 insertions(+), 60 deletions(-) diff --git a/layers/src/layer-publisher-stack.ts b/layers/src/layer-publisher-stack.ts index d8c738010f..7ad1f1f27e 100644 --- a/layers/src/layer-publisher-stack.ts +++ b/layers/src/layer-publisher-stack.ts @@ -11,7 +11,7 @@ import { StringParameter } from 'aws-cdk-lib/aws-ssm'; export interface LayerPublisherStackProps extends StackProps { readonly layerName?: string; readonly powertoolsPackageVersion?: string; - readonly ssmParameterLayerArn: string; + readonly ssmParameterLayerName: string; } export class LayerPublisherStack extends Stack { @@ -57,7 +57,7 @@ export class LayerPublisherStack extends Stack { this.lambdaLayerVersion.applyRemovalPolicy(RemovalPolicy.RETAIN); new StringParameter(this, 'VersionArn', { - parameterName: props.ssmParameterLayerArn, + parameterName: props.ssmParameterLayerName, stringValue: this.lambdaLayerVersion.layerVersionArn, }); new CfnOutput(this, 'LatestLayerArn', { diff --git a/layers/tests/e2e/layerPublisher.test.ts b/layers/tests/e2e/layerPublisher.test.ts index 664c04afb9..be3b6c51f9 100644 --- a/layers/tests/e2e/layerPublisher.test.ts +++ b/layers/tests/e2e/layerPublisher.test.ts @@ -8,12 +8,8 @@ import { LayerVersion } from 'aws-cdk-lib/aws-lambda'; import { LayerPublisherStack } from '../../src/layer-publisher-stack'; import { concatenateResourceName, - defaultRuntime, - generateTestUniqueName, - isValidRuntimeKey, TestNodejsFunction, TestStack, - TEST_RUNTIMES, TestInvocationLogs, invokeFunctionOnce, } from '@aws-lambda-powertools/testing-utils'; @@ -26,87 +22,86 @@ import { import { join } from 'node:path'; import packageJson from '../../package.json'; -const runtime: string = process.env.RUNTIME || defaultRuntime; - -if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key: ${runtime}`); -} - -describe(`layers E2E tests (LayerPublisherStack) for runtime: ${runtime}`, () => { - let invocationLogs: TestInvocationLogs; - - const stackNameLayers = generateTestUniqueName({ - testPrefix: RESOURCE_NAME_PREFIX, - runtime, - testName: 'layerStack', +describe(`Layers E2E tests, publisher stack`, () => { + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'functionStack', + }, }); - const stackNameFunction = generateTestUniqueName({ - testPrefix: RESOURCE_NAME_PREFIX, - runtime, - testName: 'functionStack', - }); - - const functionName = concatenateResourceName({ - testName: stackNameFunction, - resourceName: 'function', - }); + let invocationLogs: TestInvocationLogs; const ssmParameterLayerName = concatenateResourceName({ - testName: stackNameFunction, + testName: `${RESOURCE_NAME_PREFIX}-layer`, resourceName: 'parameter', }); // Location of the lambda function code - const lambdaFunctionCodeFile = join( + const lambdaFunctionCodeFilePath = join( __dirname, 'layerPublisher.class.test.functionCode.ts' ); const powerToolsPackageVersion = packageJson.version; - const layerName = concatenateResourceName({ - testName: stackNameLayers, - resourceName: 'layer', - }); - const testStack = new TestStack(stackNameFunction); const layerApp = new App(); - const layerStack = new LayerPublisherStack(layerApp, stackNameLayers, { - layerName, - powertoolsPackageVersion: powerToolsPackageVersion, - ssmParameterLayerArn: ssmParameterLayerName, + const layerStack = new LayerPublisherStack( + layerApp, + `${RESOURCE_NAME_PREFIX}-layer`, + { + layerName: concatenateResourceName({ + testName: RESOURCE_NAME_PREFIX, + resourceName: 'layer', + }), + powertoolsPackageVersion: powerToolsPackageVersion, + ssmParameterLayerName, + } + ); + const testLayerStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'layerStack', + }, + app: layerApp, + stack: layerStack, }); - const testLayerStack = new TestStack(stackNameLayers, layerApp, layerStack); beforeAll(async () => { - const outputs = await testLayerStack.deploy(); + await testLayerStack.deploy(); const layerVersion = LayerVersion.fromLayerVersionArn( testStack.stack, 'LayerVersionArnReference', - outputs['LatestLayerArn'] + testLayerStack.findAndGetStackOutputValue('LayerVersionArn') ); - new TestNodejsFunction(testStack.stack, functionName, { - functionName: functionName, - entry: lambdaFunctionCodeFile, - runtime: TEST_RUNTIMES[runtime], - environment: { - POWERTOOLS_PACKAGE_VERSION: powerToolsPackageVersion, - POWERTOOLS_SERVICE_NAME: 'LayerPublisherStack', - }, - bundling: { - externalModules: [ - '@aws-lambda-powertools/commons', - '@aws-lambda-powertools/logger', - '@aws-lambda-powertools/metrics', - '@aws-lambda-powertools/tracer', - ], + new TestNodejsFunction( + testStack, + { + entry: lambdaFunctionCodeFilePath, + environment: { + POWERTOOLS_PACKAGE_VERSION: powerToolsPackageVersion, + POWERTOOLS_SERVICE_NAME: 'LayerPublisherStack', + }, + bundling: { + externalModules: [ + '@aws-lambda-powertools/commons', + '@aws-lambda-powertools/logger', + '@aws-lambda-powertools/metrics', + '@aws-lambda-powertools/tracer', + ], + }, + layers: [layerVersion], }, - layers: [layerVersion], - }); + { + nameSuffix: 'testFn', + } + ); await testStack.deploy(); + const functionName = testStack.findAndGetStackOutputValue('testFn'); + invocationLogs = await invokeFunctionOnce({ functionName, }); From b4869ca5bbd9cb2de3a7ae30e92bbd9365116114 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 31 Aug 2023 23:54:04 +0200 Subject: [PATCH 10/18] fix: typo --- layers/src/layer-publisher-stack.ts | 2 +- layers/tests/e2e/layerPublisher.class.test.functionCode.ts | 1 + layers/tests/e2e/layerPublisher.test.ts | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/layers/src/layer-publisher-stack.ts b/layers/src/layer-publisher-stack.ts index 7ad1f1f27e..0ac25df02c 100644 --- a/layers/src/layer-publisher-stack.ts +++ b/layers/src/layer-publisher-stack.ts @@ -11,7 +11,7 @@ import { StringParameter } from 'aws-cdk-lib/aws-ssm'; export interface LayerPublisherStackProps extends StackProps { readonly layerName?: string; readonly powertoolsPackageVersion?: string; - readonly ssmParameterLayerName: string; + readonly ssmParameterLayerArn: string; } export class LayerPublisherStack extends Stack { diff --git a/layers/tests/e2e/layerPublisher.class.test.functionCode.ts b/layers/tests/e2e/layerPublisher.class.test.functionCode.ts index a2cdcd8d42..59fd1834c3 100644 --- a/layers/tests/e2e/layerPublisher.class.test.functionCode.ts +++ b/layers/tests/e2e/layerPublisher.class.test.functionCode.ts @@ -35,6 +35,7 @@ export const handler = (): void => { // Check that the tracer is working const segment = tracer.getSegment(); + if (!segment) throw new Error('Segment not found'); const handlerSegment = segment.addNewSubsegment('### index.handler'); tracer.setSegment(handlerSegment); tracer.annotateColdStart(); diff --git a/layers/tests/e2e/layerPublisher.test.ts b/layers/tests/e2e/layerPublisher.test.ts index be3b6c51f9..6c4e112ba6 100644 --- a/layers/tests/e2e/layerPublisher.test.ts +++ b/layers/tests/e2e/layerPublisher.test.ts @@ -55,7 +55,7 @@ describe(`Layers E2E tests, publisher stack`, () => { resourceName: 'layer', }), powertoolsPackageVersion: powerToolsPackageVersion, - ssmParameterLayerName, + ssmParameterLayerArn: ssmParameterLayerName, } ); const testLayerStack = new TestStack({ From f0ba7f913d29ca9186ec49317fa84a40dfafbcaf Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 31 Aug 2023 23:56:55 +0200 Subject: [PATCH 11/18] fix: typo --- layers/src/layer-publisher-stack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/layers/src/layer-publisher-stack.ts b/layers/src/layer-publisher-stack.ts index 0ac25df02c..d8c738010f 100644 --- a/layers/src/layer-publisher-stack.ts +++ b/layers/src/layer-publisher-stack.ts @@ -57,7 +57,7 @@ export class LayerPublisherStack extends Stack { this.lambdaLayerVersion.applyRemovalPolicy(RemovalPolicy.RETAIN); new StringParameter(this, 'VersionArn', { - parameterName: props.ssmParameterLayerName, + parameterName: props.ssmParameterLayerArn, stringValue: this.lambdaLayerVersion.layerVersionArn, }); new CfnOutput(this, 'LatestLayerArn', { From 4e9c4b494a93b5e9c68a27ddaf189fe82732eaa0 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Fri, 1 Sep 2023 17:16:27 +0200 Subject: [PATCH 12/18] chore: fix parameters test --- .../tests/e2e/ssmProvider.class.test.ts | 2 +- .../parameters/tests/helpers/resources.ts | 20 +++++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/parameters/tests/e2e/ssmProvider.class.test.ts b/packages/parameters/tests/e2e/ssmProvider.class.test.ts index 17dcc1dee3..e8f0aae256 100644 --- a/packages/parameters/tests/e2e/ssmProvider.class.test.ts +++ b/packages/parameters/tests/e2e/ssmProvider.class.test.ts @@ -166,7 +166,7 @@ describe(`Parameters E2E tests, SSM provider`, () => { await testStack.deploy(); // Get the actual function names from the stack outputs - const functionName = testStack.findAndGetStackOutputValue('ssmProvider'); + const functionName = testStack.findAndGetStackOutputValue('SsmProvider'); paramA = testStack.findAndGetStackOutputValue('getaStr'); paramB = testStack.findAndGetStackOutputValue('getbStr'); paramEncryptedA = testStack.findAndGetStackOutputValue('secureaSecStr'); diff --git a/packages/parameters/tests/helpers/resources.ts b/packages/parameters/tests/helpers/resources.ts index 87ca5800c6..2ac73d0cee 100644 --- a/packages/parameters/tests/helpers/resources.ts +++ b/packages/parameters/tests/helpers/resources.ts @@ -160,17 +160,15 @@ class TestSecret extends Secret { props: Omit, extraProps: ExtraTestProps ) { - super( - testStack.stack, - concatenateResourceName({ - testName: testStack.testName, - resourceName: extraProps.nameSuffix, - }), - { - ...props, - secretName: `/${extraProps.nameSuffix}`, - } - ); + const secretId = concatenateResourceName({ + testName: testStack.testName, + resourceName: extraProps.nameSuffix, + }); + + super(testStack.stack, secretId, { + ...props, + secretName: `/${secretId}`, + }); } } From 18412be9c127c27e658b10af374e9d1dfaf47f1c Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Fri, 1 Sep 2023 17:27:14 +0200 Subject: [PATCH 13/18] fix: layer arn retrieval --- layers/tests/e2e/layerPublisher.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/layers/tests/e2e/layerPublisher.test.ts b/layers/tests/e2e/layerPublisher.test.ts index 6c4e112ba6..58bf9204ad 100644 --- a/layers/tests/e2e/layerPublisher.test.ts +++ b/layers/tests/e2e/layerPublisher.test.ts @@ -73,7 +73,7 @@ describe(`Layers E2E tests, publisher stack`, () => { const layerVersion = LayerVersion.fromLayerVersionArn( testStack.stack, 'LayerVersionArnReference', - testLayerStack.findAndGetStackOutputValue('LayerVersionArn') + testLayerStack.findAndGetStackOutputValue('LatestLayerArn') ); new TestNodejsFunction( testStack, From 36c5881d991b402d2ff969e2124e6553d121ba2a Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Fri, 1 Sep 2023 17:56:17 +0200 Subject: [PATCH 14/18] chore: make ssm parameter name unique --- .../parameters/tests/helpers/resources.ts | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/parameters/tests/helpers/resources.ts b/packages/parameters/tests/helpers/resources.ts index 2ac73d0cee..59916cfb0e 100644 --- a/packages/parameters/tests/helpers/resources.ts +++ b/packages/parameters/tests/helpers/resources.ts @@ -133,17 +133,15 @@ class TestStringParameter extends StringParameter { props: Omit, extraProps: ExtraTestProps ) { - super( - testStack.stack, - concatenateResourceName({ - testName: testStack.testName, - resourceName: extraProps.nameSuffix, - }), - { - ...props, - parameterName: `/${extraProps.nameSuffix}`, - } - ); + const parameterId = concatenateResourceName({ + testName: testStack.testName, + resourceName: extraProps.nameSuffix, + }); + + super(testStack.stack, parameterId, { + ...props, + parameterName: `/${parameterId}`, + }); new CfnOutput(this, `${extraProps.nameSuffix.replace('/', '')}Str`, { value: this.parameterName, From e511b718016d7dd984ec3bc8a332574d9660859e Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Fri, 1 Sep 2023 18:40:54 +0200 Subject: [PATCH 15/18] docs: explain layers tests --- layers/tests/e2e/layerPublisher.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/layers/tests/e2e/layerPublisher.test.ts b/layers/tests/e2e/layerPublisher.test.ts index 58bf9204ad..a53e6ba2e3 100644 --- a/layers/tests/e2e/layerPublisher.test.ts +++ b/layers/tests/e2e/layerPublisher.test.ts @@ -22,6 +22,13 @@ import { import { join } from 'node:path'; import packageJson from '../../package.json'; +/** + * This test has two stacks: + * 1. LayerPublisherStack - publishes a layer version using the LayerPublisher construct and containing the Powertools utilities from the repo + * 2. TestStack - uses the layer published in the first stack and contains a lambda function that uses the Powertools utilities from the layer + * + * The lambda function is invoked once and the logs are collected. The goal of the test is to verify that the layer creation and usage works as expected. + */ describe(`Layers E2E tests, publisher stack`, () => { const testStack = new TestStack({ stackNameProps: { From dbfd9f87dd4f01342658f1fab5863f0339a2a33e Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Fri, 1 Sep 2023 19:07:22 +0200 Subject: [PATCH 16/18] chore: make layer id unique --- layers/tests/e2e/layerPublisher.test.ts | 22 ++++++++++------------ packages/testing/src/TestStack.ts | 3 +-- packages/testing/src/helpers.ts | 8 +++----- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/layers/tests/e2e/layerPublisher.test.ts b/layers/tests/e2e/layerPublisher.test.ts index a53e6ba2e3..5af3452bd4 100644 --- a/layers/tests/e2e/layerPublisher.test.ts +++ b/layers/tests/e2e/layerPublisher.test.ts @@ -12,6 +12,7 @@ import { TestStack, TestInvocationLogs, invokeFunctionOnce, + generateTestUniqueName, } from '@aws-lambda-powertools/testing-utils'; import { RESOURCE_NAME_PREFIX, @@ -53,18 +54,15 @@ describe(`Layers E2E tests, publisher stack`, () => { const powerToolsPackageVersion = packageJson.version; const layerApp = new App(); - const layerStack = new LayerPublisherStack( - layerApp, - `${RESOURCE_NAME_PREFIX}-layer`, - { - layerName: concatenateResourceName({ - testName: RESOURCE_NAME_PREFIX, - resourceName: 'layer', - }), - powertoolsPackageVersion: powerToolsPackageVersion, - ssmParameterLayerArn: ssmParameterLayerName, - } - ); + const layerId = generateTestUniqueName({ + testPrefix: RESOURCE_NAME_PREFIX, + testName: 'layerStack', + }); + const layerStack = new LayerPublisherStack(layerApp, layerId, { + layerName: layerId, + powertoolsPackageVersion: powerToolsPackageVersion, + ssmParameterLayerArn: ssmParameterLayerName, + }); const testLayerStack = new TestStack({ stackNameProps: { stackNamePrefix: RESOURCE_NAME_PREFIX, diff --git a/packages/testing/src/TestStack.ts b/packages/testing/src/TestStack.ts index 410cf38fbb..952f00a5a4 100644 --- a/packages/testing/src/TestStack.ts +++ b/packages/testing/src/TestStack.ts @@ -4,7 +4,7 @@ import { readFile } from 'node:fs/promises'; import { App, Stack } from 'aws-cdk-lib'; import { AwsCdkCli, RequireApproval } from '@aws-cdk/cli-lib-alpha'; import type { ICloudAssemblyDirectoryProducer } from '@aws-cdk/cli-lib-alpha'; -import { generateTestUniqueName, getRuntimeKey } from './helpers'; +import { generateTestUniqueName } from './helpers'; type StackNameProps = { /** @@ -68,7 +68,6 @@ class TestStack implements ICloudAssemblyDirectoryProducer { public constructor({ stackNameProps, app, stack }: TestStackProps) { this.testName = generateTestUniqueName({ testName: stackNameProps.testName, - runtime: getRuntimeKey(), testPrefix: stackNameProps.stackNamePrefix, }); this.app = app ?? new App(); diff --git a/packages/testing/src/helpers.ts b/packages/testing/src/helpers.ts index ec452701ab..4a737590b2 100644 --- a/packages/testing/src/helpers.ts +++ b/packages/testing/src/helpers.ts @@ -22,25 +22,23 @@ const getRuntimeKey = (): keyof typeof TEST_RUNTIMES => { * * @example * ```ts + * process.env.RUNTIME = 'nodejs18x'; * const testPrefix = 'E2E-TRACER'; - * const runtime = 'nodejs18x'; * const testName = 'someFeature'; - * const uniqueName = generateTestUniqueName({ testPrefix, runtime, testName }); + * const uniqueName = generateTestUniqueName({ testPrefix, testName }); * // uniqueName = 'E2E-TRACER-node18-12345-someFeature' * ``` */ const generateTestUniqueName = ({ testPrefix, - runtime, testName, }: { testPrefix: string; - runtime: string; testName: string; }): string => [ testPrefix, - runtime.replace(/[jsx]/g, ''), + getRuntimeKey().replace(/[jsx]/g, ''), randomUUID().toString().substring(0, 5), testName, ] From 6995d6cb282849a42421fd778c2e13fc90b55db9 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Fri, 1 Sep 2023 19:53:40 +0200 Subject: [PATCH 17/18] chore: added removal policy --- layers/src/layer-publisher-stack.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/layers/src/layer-publisher-stack.ts b/layers/src/layer-publisher-stack.ts index d8c738010f..6be087a7b0 100644 --- a/layers/src/layer-publisher-stack.ts +++ b/layers/src/layer-publisher-stack.ts @@ -7,11 +7,13 @@ import { CfnLayerVersionPermission, } from 'aws-cdk-lib/aws-lambda'; import { StringParameter } from 'aws-cdk-lib/aws-ssm'; +import { resolve } from 'node:path'; export interface LayerPublisherStackProps extends StackProps { readonly layerName?: string; readonly powertoolsPackageVersion?: string; readonly ssmParameterLayerArn: string; + readonly removeLayerVersion?: boolean; } export class LayerPublisherStack extends Stack { @@ -23,7 +25,7 @@ export class LayerPublisherStack extends Stack { ) { super(scope, id, props); - const { layerName, powertoolsPackageVersion } = props; + const { layerName, powertoolsPackageVersion, removeLayerVersion } = props; console.log( `publishing layer ${layerName} version : ${powertoolsPackageVersion}` @@ -40,7 +42,11 @@ export class LayerPublisherStack extends Stack { license: 'MIT-0', // This is needed because the following regions do not support the compatibleArchitectures property #1400 // ...(![ 'eu-south-2', 'eu-central-2', 'ap-southeast-4' ].includes(Stack.of(this).region) ? { compatibleArchitectures: [Architecture.X86_64] } : {}), - code: Code.fromAsset('../tmp'), + code: Code.fromAsset(resolve(__dirname, '..', '..', 'tmp')), + removalPolicy: + removeLayerVersion === true + ? RemovalPolicy.DESTROY + : RemovalPolicy.RETAIN, }); const layerPermission = new CfnLayerVersionPermission( @@ -60,6 +66,7 @@ export class LayerPublisherStack extends Stack { parameterName: props.ssmParameterLayerArn, stringValue: this.lambdaLayerVersion.layerVersionArn, }); + new CfnOutput(this, 'LatestLayerArn', { value: this.lambdaLayerVersion.layerVersionArn, exportName: props?.layerName ?? `LambdaPowerToolsForTypeScriptLayerARN`, From d3c04dea8ed232b005add45774819890efcb110f Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Fri, 1 Sep 2023 19:59:28 +0200 Subject: [PATCH 18/18] chore: make parameter name unique --- layers/tests/e2e/layerPublisher.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/layers/tests/e2e/layerPublisher.test.ts b/layers/tests/e2e/layerPublisher.test.ts index 5af3452bd4..a1a00c39ba 100644 --- a/layers/tests/e2e/layerPublisher.test.ts +++ b/layers/tests/e2e/layerPublisher.test.ts @@ -7,7 +7,6 @@ import { App } from 'aws-cdk-lib'; import { LayerVersion } from 'aws-cdk-lib/aws-lambda'; import { LayerPublisherStack } from '../../src/layer-publisher-stack'; import { - concatenateResourceName, TestNodejsFunction, TestStack, TestInvocationLogs, @@ -40,9 +39,9 @@ describe(`Layers E2E tests, publisher stack`, () => { let invocationLogs: TestInvocationLogs; - const ssmParameterLayerName = concatenateResourceName({ - testName: `${RESOURCE_NAME_PREFIX}-layer`, - resourceName: 'parameter', + const ssmParameterLayerName = generateTestUniqueName({ + testPrefix: `${RESOURCE_NAME_PREFIX}`, + testName: 'parameter', }); // Location of the lambda function code @@ -62,6 +61,7 @@ describe(`Layers E2E tests, publisher stack`, () => { layerName: layerId, powertoolsPackageVersion: powerToolsPackageVersion, ssmParameterLayerArn: ssmParameterLayerName, + removeLayerVersion: true, }); const testLayerStack = new TestStack({ stackNameProps: {