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/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`, 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 7b4bab6988..a1a00c39ba 100644 --- a/layers/tests/e2e/layerPublisher.test.ts +++ b/layers/tests/e2e/layerPublisher.test.ts @@ -4,129 +4,119 @@ * @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 { + TestNodejsFunction, TestStack, - defaultRuntime, + TestInvocationLogs, + invokeFunctionOnce, + generateTestUniqueName, } from '@aws-lambda-powertools/testing-utils'; -import { - generateUniqueName, - invokeFunction, - isValidRuntimeKey, - createStackWithLambdaFunction, -} from '../../../packages/commons/tests/utils/e2eUtils'; 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; +/** + * 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: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'functionStack', + }, + }); -if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key: ${runtime}`); -} + let invocationLogs: TestInvocationLogs; -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, - runtime, - 'function' - ); - const ssmParameterLayerName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'parameter' + const ssmParameterLayerName = generateTestUniqueName({ + testPrefix: `${RESOURCE_NAME_PREFIX}`, + testName: 'parameter', + }); + + // Location of the lambda function code + const lambdaFunctionCodeFilePath = 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 testStack = new TestStack(stackNameFunction); const layerApp = new App(); - const layerStack = new LayerPublisherStack(layerApp, stackNameLayers, { - layerName, + const layerId = generateTestUniqueName({ + testPrefix: RESOURCE_NAME_PREFIX, + testName: 'layerStack', + }); + const layerStack = new LayerPublisherStack(layerApp, layerId, { + layerName: layerId, powertoolsPackageVersion: powerToolsPackageVersion, ssmParameterLayerArn: ssmParameterLayerName, + removeLayerVersion: true, + }); + 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('LatestLayerArn') ); - createStackWithLambdaFunction({ - stack: testStack.stack, - functionName: functionName, - functionEntry: path.join(__dirname, lambdaFunctionCodeFile), - tracing: Tracing.ACTIVE, - environment: { - UUID: uuid, - POWERTOOLS_PACKAGE_VERSION: powerToolsPackageVersion, - POWERTOOLS_SERVICE_NAME: 'LayerPublisherStack', - }, - runtime: runtime, - 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(); - invocationLogs = await invokeFunction( + const functionName = testStack.findAndGetStackOutputValue('testFn'); + + 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..1af54e589d 100644 --- a/packages/idempotency/tests/e2e/makeHandlerIdempotent.test.ts +++ b/packages/idempotency/tests/e2e/makeHandlerIdempotent.test.ts @@ -4,140 +4,116 @@ * @group e2e/idempotency/makeHandlerIdempotent */ import { - generateUniqueName, 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 { Duration } from 'aws-cdk-lib'; +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'; -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; - -if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key value: ${runtime}`); -} +const ddb = new DynamoDBClient({}); -const uuid = v4(); -const stackName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'makeFnIdempotent' -); -const makeHandlerIdempotentFile = 'makeHandlerIdempotent.test.FunctionCode.ts'; +describe(`Idempotency E2E tests, middy middleware usage`, () => { + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'makeHandlerIdempotent', + }, + }); -const ddb = new DynamoDBClient({}); -const testStack = new TestStack(stackName); + // Location of the lambda function code + const lambdaFunctionCodeFilePath = 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` -); -createIdempotencyResources( - testStack.stack, - runtime, - ddbTableNameDefault, - makeHandlerIdempotentFile, - functionNameDefault, - 'handler' -); + let functionNameDefault: string; + let tableNameDefault: string; + new IdempotencyTestNodejsFunctionAndDynamoTable( + testStack, + { + function: { + entry: lambdaFunctionCodeFilePath, + }, + }, + { + nameSuffix: 'default', + } + ); -const testDefaultParallel = 'default-parallel'; -const functionNameDefaultParallel = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - `${testDefaultParallel}-fn` -); -const ddbTableNameDefaultParallel = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - `${testDefaultParallel}-table` -); -createIdempotencyResources( - testStack.stack, - runtime, - ddbTableNameDefaultParallel, - makeHandlerIdempotentFile, - functionNameDefaultParallel, - 'handlerParallel' -); + let functionNameDefaultParallel: string; + let tableNameDefaultParallel: string; + new IdempotencyTestNodejsFunctionAndDynamoTable( + testStack, + { + function: { + entry: lambdaFunctionCodeFilePath, + handler: 'handlerParallel', + }, + }, + { + nameSuffix: 'defaultParallel', + } + ); -const testTimeout = 'timeout'; -const functionNameTimeout = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - `${testTimeout}-fn` -); -const ddbTableNameTimeout = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - `${testTimeout}-table` -); -createIdempotencyResources( - testStack.stack, - runtime, - ddbTableNameTimeout, - makeHandlerIdempotentFile, - functionNameTimeout, - 'handlerTimeout', - undefined, - 2 -); + let functionNameTimeout: string; + let tableNameTimeout: string; + new IdempotencyTestNodejsFunctionAndDynamoTable( + testStack, + { + function: { + entry: lambdaFunctionCodeFilePath, + handler: 'handlerTimeout', + timeout: Duration.seconds(2), + }, + }, + { + nameSuffix: 'timeout', + } + ); -const testExpired = 'expired'; -const functionNameExpired = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - `${testExpired}-fn` -); -const ddbTableNameExpired = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - `${testExpired}-table` -); -createIdempotencyResources( - testStack.stack, - runtime, - ddbTableNameExpired, - makeHandlerIdempotentFile, - functionNameExpired, - 'handlerExpired', - undefined, - 2 -); + 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 for runtime ${runtime}`, () => { 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( @@ -152,19 +128,18 @@ 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 const idempotencyRecords = await ddb.send( new ScanCommand({ - TableName: ddbTableNameDefault, + TableName: tableNameDefault, }) ); expect(idempotencyRecords.Items?.length).toEqual(1); @@ -178,7 +153,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,19 +178,18 @@ 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 const idempotencyRecords = await ddb.send( new ScanCommand({ - TableName: ddbTableNameDefaultParallel, + TableName: tableNameDefaultParallel, }) ); expect(idempotencyRecords.Items?.length).toEqual(1); @@ -262,19 +236,21 @@ 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 const idempotencyRecords = await ddb.send( new ScanCommand({ - TableName: ddbTableNameTimeout, + TableName: tableNameTimeout, }) ); expect(idempotencyRecords.Items?.length).toEqual(1); @@ -293,7 +269,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 +294,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()); @@ -345,7 +319,7 @@ describe(`Idempotency E2E tests, middy middleware usage for runtime ${runtime}`, // Assess const idempotencyRecords = await ddb.send( new ScanCommand({ - TableName: ddbTableNameExpired, + TableName: tableNameExpired, }) ); expect(idempotencyRecords.Items?.length).toEqual(1); @@ -360,7 +334,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 +344,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..ff7c890f59 100644 --- a/packages/idempotency/tests/e2e/makeIdempotent.test.ts +++ b/packages/idempotency/tests/e2e/makeIdempotent.test.ts @@ -4,114 +4,105 @@ * @group e2e/idempotency/makeIdempotent */ import { - generateUniqueName, invokeFunction, - isValidRuntimeKey, -} from '../../../commons/tests/utils/e2eUtils'; + TestInvocationLogs, + TestStack, +} from '@aws-lambda-powertools/testing-utils'; +import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; +import { ScanCommand } from '@aws-sdk/lib-dynamodb'; +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'; -import { v4 } from 'uuid'; -import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; -import { createHash } from 'node:crypto'; -import { - TestStack, - defaultRuntime, -} 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; +describe(`Idempotency E2E tests, wrapper function usage`, () => { + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'makeFnIdempotent', + }, + }); -if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key value: ${runtime}`); -} -const uuid = v4(); -const stackName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'makeFnIdempotent' -); -const makeFunctionIdempotentFile = 'makeIdempotent.test.FunctionCode.ts'; + // Location of the lambda function code + const lambdaFunctionCodeFilePath = join( + __dirname, + 'makeIdempotent.test.FunctionCode.ts' + ); -const ddb = new DynamoDBClient({ region: 'eu-west-1' }); -const testStack = new TestStack(stackName); + let functionNameDefault: string; + let tableNameDefault: string; + new IdempotencyTestNodejsFunctionAndDynamoTable( + testStack, + { + function: { + entry: lambdaFunctionCodeFilePath, + handler: 'handlerDefault', + }, + }, + { + nameSuffix: 'default', + } + ); -const testDefault = 'default'; -const functionNameDefault = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - `${testDefault}-fn` -); -const ddbTableNameDefault = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - `${testDefault}-table` -); -createIdempotencyResources( - testStack.stack, - runtime, - ddbTableNameDefault, - makeFunctionIdempotentFile, - functionNameDefault, - 'handlerDefault' -); + let functionNameCustomConfig: string; + let tableNameCustomConfig: string; + new IdempotencyTestNodejsFunctionAndDynamoTable( + testStack, + { + function: { + entry: lambdaFunctionCodeFilePath, + handler: 'handlerCustomized', + }, + table: { + partitionKey: { + name: 'customId', + type: AttributeType.STRING, + }, + }, + }, + { + nameSuffix: 'customConfig', + } + ); -const testCustomConfig = 'customConfig'; -const functionNameCustomConfig = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - `${testCustomConfig}-fn` -); -const ddbTableNameCustomConfig = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - `${testCustomConfig}-fn` -); -createIdempotencyResources( - testStack.stack, - runtime, - ddbTableNameCustomConfig, - makeFunctionIdempotentFile, - functionNameCustomConfig, - 'handlerCustomized', - 'customId' -); + let functionNameLambdaHandler: string; + let tableNameLambdaHandler: string; + new IdempotencyTestNodejsFunctionAndDynamoTable( + testStack, + { + function: { + entry: lambdaFunctionCodeFilePath, + handler: 'handlerLambda', + }, + }, + { + nameSuffix: 'handler', + } + ); -const testLambdaHandler = 'handler'; -const functionNameLambdaHandler = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - `${testLambdaHandler}-fn` -); -const ddbTableNameLambdaHandler = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - `${testLambdaHandler}-table` -); -createIdempotencyResources( - testStack.stack, - runtime, - ddbTableNameLambdaHandler, - makeFunctionIdempotentFile, - functionNameLambdaHandler, - 'handlerLambda' -); + const ddb = new DynamoDBClient({}); -describe(`Idempotency E2E tests, wrapper function usage for runtime`, () => { 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( @@ -130,19 +121,18 @@ 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 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 @@ -191,19 +181,18 @@ 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 const idempotencyRecords = await ddb.send( new ScanCommand({ - TableName: ddbTableNameCustomConfig, + TableName: tableNameCustomConfig, }) ); /** @@ -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,19 +275,18 @@ 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 const idempotencyRecords = await ddb.send( new ScanCommand({ - TableName: ddbTableNameLambdaHandler, + TableName: tableNameLambdaHandler, }) ); expect(idempotencyRecords.Items?.length).toEqual(1); @@ -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..efda071436 100644 --- a/packages/idempotency/tests/helpers/idempotencyUtils.ts +++ b/packages/idempotency/tests/helpers/idempotencyUtils.ts @@ -1,49 +1,4 @@ -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 { BasePersistenceLayer } from '../../src/persistence'; -import path from 'path'; -import { RetentionDays } from 'aws-cdk-lib/aws-logs'; - -export const createIdempotencyResources = ( - stack: Stack, - runtime: string, - ddbTableName: string, - pathToFunction: string, - functionName: string, - handler: string, - ddbPkId?: string, - timeout?: number -): void => { - const uniqueTableId = ddbTableName + v4().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 + v4().substring(0, 5); - const nodeJsFunction = new NodejsFunction(stack, uniqueFunctionId, { - runtime: TEST_RUNTIMES[runtime], - functionName: functionName, - entry: path.join(__dirname, `../e2e/${pathToFunction}`), - timeout: Duration.seconds(timeout || 30), - handler: handler, - environment: { - IDEMPOTENCY_TABLE_NAME: ddbTableName, - POWERTOOLS_LOGGER_LOG_EVENT: 'true', - }, - logRetention: RetentionDays.ONE_DAY, - }); - - 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.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..099053f538 100644 --- a/packages/logger/tests/e2e/basicFeatures.middy.test.ts +++ b/packages/logger/tests/e2e/basicFeatures.middy.test.ts @@ -3,20 +3,14 @@ * * @group e2e/logger/basicFeatures */ -import path from 'path'; -import { APIGatewayAuthorizerResult } from 'aws-lambda'; -import { v4 } from 'uuid'; import { - createStackWithLambdaFunction, - generateUniqueName, invokeFunction, - isValidRuntimeKey, -} from '../../../commons/tests/utils/e2eUtils'; -import { InvocationLogs } from '../../../commons/tests/utils/InvocationLogs'; -import { + TestInvocationLogs, TestStack, - defaultRuntime, } from '@aws-lambda-powertools/testing-utils'; +import type { APIGatewayAuthorizerResult } from 'aws-lambda'; +import { join } from 'node:path'; +import { LoggerTestNodejsFunction } from '../helpers/resources'; import { RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, @@ -24,87 +18,54 @@ import { TEARDOWN_TIMEOUT, TEST_CASE_TIMEOUT, XRAY_TRACE_ID_REGEX, + commonEnvironmentVars, } from './constants'; -const runtime: string = process.env.RUNTIME || defaultRuntime; - -if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key value: ${runtime}`); -} +describe(`Logger E2E tests, basic functionalities middy usage`, () => { + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'AllFeatures-Decorator', + }, + }); -const LEVEL = InvocationLogs.LEVEL; + // Location of the lambda function code + const lambdaFunctionCodeFilePath = join( + __dirname, + 'basicFeatures.middy.test.FunctionCode.ts' + ); -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'; - -const invocationCount = 3; - -// 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'; - -const testStack = new TestStack(stackName); -let logGroupName: string; // We do not know it until deployment - -describe(`logger E2E tests basic functionalities (middy) for runtime: ${runtime}`, () => { - let invocationLogs: InvocationLogs[]; + const invocationCount = 3; + 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 LoggerTestNodejsFunction( + testStack, + { + entry: lambdaFunctionCodeFilePath, }, - logGroupOutputKey: STACK_OUTPUT_LOG_GROUP, - runtime: runtime, - }); + { + 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( + invocationLogs = await invokeFunction({ functionName, - invocationCount, - 'SEQUENTIAL' - ); + times: invocationCount, + invocationMode: 'SEQUENTIAL', + payload: { + foo: 'bar', + }, + }); console.log('logGroupName', logGroupName); }, SETUP_TIMEOUT); @@ -115,7 +76,7 @@ describe(`logger E2E tests basic functionalities (middy) for runtime: ${runtime} async () => { 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 +94,7 @@ describe(`logger E2E tests basic functionalities (middy) for runtime: ${runtime} 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'); @@ -153,7 +114,7 @@ describe(`logger E2E tests basic functionalities (middy) for runtime: ${runtime} 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 { @@ -175,12 +136,12 @@ describe(`logger E2E tests basic functionalities (middy) for runtime: ${runtime} 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,15 +158,20 @@ describe(`logger E2E tests basic functionalities (middy) for runtime: ${runtime} it( 'should contain persistent value in every log', async () => { + 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 = 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); + expect(log).toHaveProperty(persistentKey); + expect(log[persistentKey]).toBe(persistentValue); } } }, @@ -215,20 +181,23 @@ describe(`logger E2E tests basic functionalities (middy) for runtime: ${runtime} it( 'should not contain persistent keys that were removed on runtime', async () => { + 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(); 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) { - 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); } } } @@ -239,21 +208,23 @@ describe(`logger E2E tests basic functionalities (middy) for runtime: ${runtime} it( 'should not leak any persistent keys added runtime since clearState is enabled', async () => { + const { RUNTIME_ADDED_KEY: runtimeAddedKey } = commonEnvironmentVars; + for (let i = 0; i < invocationCount; 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) { - 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); - // 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); + expect(log).toHaveProperty(runtimeAddedKey); + // Check that the value is the same for all logs + expect(log[runtimeAddedKey]).toEqual('bar'); } } } @@ -266,19 +237,24 @@ describe(`logger E2E tests basic functionalities (middy) for runtime: ${runtime} it( 'should log additional keys and value only once', async () => { + 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 = InvocationLogs.parseFunctionLog( + 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 @@ -289,21 +265,23 @@ describe(`logger E2E tests basic functionalities (middy) for runtime: ${runtime} it( 'should log error only once', async () => { + 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); // 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({ location: expect.any(String), name: 'Error', - message: ERROR_MSG, + message: errorMsg, stack: expect.anything(), }) ); @@ -317,22 +295,27 @@ describe(`logger E2E tests basic functionalities (middy) for runtime: ${runtime} it( 'should log additional arbitrary object only once', async () => { + 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); - 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[ - 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 +345,7 @@ describe(`logger E2E tests basic functionalities (middy) for runtime: ${runtime} // 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..454eab8a81 100644 --- a/packages/logger/tests/e2e/childLogger.manual.test.ts +++ b/packages/logger/tests/e2e/childLogger.manual.test.ts @@ -3,92 +3,62 @@ * * @group e2e/logger/childLogger */ -import path from 'path'; -import { v4 } from 'uuid'; import { - createStackWithLambdaFunction, - generateUniqueName, invokeFunction, - isValidRuntimeKey, -} from '../../../commons/tests/utils/e2eUtils'; -import { InvocationLogs } from '../../../commons/tests/utils/InvocationLogs'; -import { + TestInvocationLogs, TestStack, - defaultRuntime, } 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'; -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, - 'ChildLogger-Manual' -); -const functionName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'ChildLogger-Manual' -); -const lambdaFunctionCodeFile = 'childLogger.manual.test.FunctionCode.ts'; - -const invocationCount = 3; - -// 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; +describe(`Logger E2E tests, child logger`, () => { + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'ChildLogger-Manual', + }, + }); -const testStack = new TestStack(stackName); -let logGroupName: string; // We do not know it until deployment + // Location of the lambda function code + const lambdaFunctionCodeFilePath = join( + __dirname, + 'childLogger.manual.test.FunctionCode.ts' + ); -describe(`logger E2E tests child logger functionalities (manual) for runtime: ${runtime}`, () => { - let invocationLogs: InvocationLogs[]; + const invocationCount = 3; + let invocationLogs: TestInvocationLogs[]; + let logGroupName: string; 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 LoggerTestNodejsFunction( + testStack, + { + entry: lambdaFunctionCodeFilePath, }, - logGroupOutputKey: STACK_OUTPUT_LOG_GROUP, - runtime: runtime, - }); + { + 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, invocationCount); + invocationLogs = await invokeFunction({ + functionName, + invocationMode: 'SEQUENTIAL', + times: invocationCount, + }); console.log('logGroupName', logGroupName); }, SETUP_TIMEOUT); @@ -97,15 +67,18 @@ 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 () => { + 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); @@ -118,6 +91,7 @@ describe(`logger E2E tests child logger functionalities (manual) for runtime: ${ it( 'should log only level passed to a child', async () => { + const { CHILD_LOG_MSG: childLogMsg } = commonEnvironmentVars; for (let i = 0; i < invocationCount; i++) { // Get log messages of the invocation const logMessages = invocationLogs[i].getFunctionLogs(); @@ -125,8 +99,7 @@ describe(`logger E2E tests child logger functionalities (manual) for runtime: ${ // 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) @@ -140,18 +113,20 @@ describe(`logger E2E tests child logger functionalities (manual) for runtime: ${ it( 'should NOT inject context into the child logger', async () => { + 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 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,14 +140,16 @@ describe(`logger E2E tests child logger functionalities (manual) for runtime: ${ it( 'both logger instances should have the same persistent key/value', async () => { + 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 = InvocationLogs.parseFunctionLog(message); - expect(log).toHaveProperty(PERSISTENT_KEY); + const log = TestInvocationLogs.parseFunctionLog(message); + 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 9b4e10a727..5b87f0675f 100644 --- a/packages/logger/tests/e2e/logEventEnvVarSetting.middy.test.ts +++ b/packages/logger/tests/e2e/logEventEnvVarSetting.middy.test.ts @@ -3,84 +3,68 @@ * * @group e2e/logger/logEventEnvVarSetting */ -import path from 'path'; -import { v4 } from 'uuid'; import { - createStackWithLambdaFunction, - generateUniqueName, invokeFunction, - isValidRuntimeKey, -} from '../../../commons/tests/utils/e2eUtils'; -import { InvocationLogs } from '../../../commons/tests/utils/InvocationLogs'; -import { + TestInvocationLogs, TestStack, - defaultRuntime, } from '@aws-lambda-powertools/testing-utils'; +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'; -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, - 'LogEventEnvVarSetting-Middy' -); -const functionName = generateUniqueName( - RESOURCE_NAME_PREFIX, - uuid, - runtime, - 'LogEventEnvVarSetting-Middy' -); -const lambdaFunctionCodeFile = - 'logEventEnvVarSetting.middy.test.FunctionCode.ts'; - -const invocationCount = 3; +describe(`Logger E2E tests, log event via env var setting with middy`, () => { + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'LogEventFromEnv-Middy', + }, + }); -const testStack = new TestStack(stackName); -let logGroupName: string; // We do not know it until deployment + // Location of the lambda function code + const lambdaFunctionCodeFilePath = join( + __dirname, + 'logEventEnvVarSetting.middy.test.FunctionCode.ts' + ); -describe(`logger E2E tests log event via env var setting (middy) for runtime: ${runtime}`, () => { - let invocationLogs: InvocationLogs[]; + const invocationCount = 3; + let invocationLogs: TestInvocationLogs[]; + let logGroupName: string; 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, - - // Enabling the logger to log events via env var - POWERTOOLS_LOGGER_LOG_EVENT: 'true', + // Prepare + new LoggerTestNodejsFunction( + testStack, + { + entry: lambdaFunctionCodeFilePath, + environment: { + POWERTOOLS_LOGGER_LOG_EVENT: 'true', + }, }, - logGroupOutputKey: STACK_OUTPUT_LOG_GROUP, - runtime: runtime, - }); + { + 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( + invocationLogs = await invokeFunction({ functionName, - invocationCount, - 'SEQUENTIAL' - ); + invocationMode: 'SEQUENTIAL', + times: invocationCount, + payload: { + foo: 'bar', + }, + }); console.log('logGroupName', logGroupName); }, SETUP_TIMEOUT); @@ -94,12 +78,12 @@ describe(`logger E2E tests log event via env var setting (middy) for runtime: ${ 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..67c803dd58 100644 --- a/packages/logger/tests/e2e/sampleRate.decorator.test.ts +++ b/packages/logger/tests/e2e/sampleRate.decorator.test.ts @@ -3,86 +3,66 @@ * * @group e2e/logger/sampleRate */ -import path from 'path'; -import { v4 } from 'uuid'; import { - createStackWithLambdaFunction, - generateUniqueName, invokeFunction, - isValidRuntimeKey, -} from '../../../commons/tests/utils/e2eUtils'; -import { InvocationLogs } from '../../../commons/tests/utils/InvocationLogs'; -import { + TestInvocationLogs, TestStack, - defaultRuntime, } 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'; -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'; - -const invocationCount = 20; - -// 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; +describe(`Logger E2E tests, sample rate and injectLambdaContext()`, () => { + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'SampleRate-Decorator', + }, + }); -const testStack = new TestStack(stackName); -let logGroupName: string; // We do not know the exact name until deployment + // Location of the lambda function code + const lambdaFunctionCodeFilePath = join( + __dirname, + 'sampleRate.decorator.test.FunctionCode.ts' + ); -describe(`logger E2E tests sample rate and injectLambdaContext() for runtime: nodejs18x`, () => { - let invocationLogs: InvocationLogs[]; + const invocationCount = 20; + let invocationLogs: TestInvocationLogs[]; + let logGroupName: string; 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 LoggerTestNodejsFunction( + testStack, + { + entry: lambdaFunctionCodeFilePath, + environment: { + LOG_LEVEL: 'ERROR', + SAMPLE_RATE: '0.5', + LOG_MSG: `Log message ${randomUUID()}`, + }, }, - logGroupOutputKey: STACK_OUTPUT_LOG_GROUP, - runtime: runtime, - }); + { + 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, invocationCount); + invocationLogs = await invokeFunction({ + functionName, + times: invocationCount, + }); console.log('logGroupName', logGroupName); }, SETUP_TIMEOUT); @@ -99,10 +79,7 @@ describe(`logger E2E tests sample rate and injectLambdaContext() for runtime: no // 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++; @@ -114,7 +91,7 @@ describe(`logger E2E tests sample rate and injectLambdaContext() for runtime: no } } - // 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,11 +106,11 @@ describe(`logger E2E tests sample rate and injectLambdaContext() for runtime: no async () => { 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) { - 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/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 e2dbd76c3d..332636210e 100644 --- a/packages/metrics/tests/e2e/basicFeatures.decorators.test.ts +++ b/packages/metrics/tests/e2e/basicFeatures.decorators.test.ts @@ -3,116 +3,86 @@ * * @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, invokeFunction, -} from '../../../commons/tests/utils/e2eUtils'; -import { TestStack, - defaultRuntime, } from '@aws-lambda-powertools/testing-utils'; -import { MetricUnits } from '../../src'; import { + CloudWatchClient, + GetMetricStatisticsCommand, +} from '@aws-sdk/client-cloudwatch'; +import { join } from 'node:path'; +import { getMetrics } from '../helpers/metricsUtils'; +import { MetricsTestNodejsFunction } from '../helpers/resources'; +import { + commonEnvironmentVars, 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; - -if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key value: ${runtime}`); -} +describe(`Metrics E2E tests, basic features decorator usage`, () => { + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'BasicFeatures-Decorators', + }, + }); -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, + // Location of the lambda function code + const lambdaFunctionCodeFilePath = join( + __dirname, + 'basicFeatures.decorator.test.functionCode.ts' + ); + const startTime = new Date(); + + const expectedServiceName = 'e2eBasicFeatures'; + let fnNameBasicFeatures: string; + new MetricsTestNodejsFunction( + testStack, + { + entry: lambdaFunctionCodeFilePath, 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, }, - runtime: runtime, - }); + }, + { + nameSuffix: 'BasicFeatures', + } + ); + + const cloudwatchClient = new CloudWatchClient({}); + const invocations = 2; + beforeAll(async () => { + // Deploy the stack await testStack.deploy(); - // and invoked - await invokeFunction(functionName, invocationCount, 'SEQUENTIAL'); + // Get the actual function names from the stack outputs + fnNameBasicFeatures = testStack.findAndGetStackOutputValue('BasicFeatures'); + + // Act + await invokeFunction({ + functionName: fnNameBasicFeatures, + times: invocations, + invocationMode: 'SEQUENTIAL', + }); }, SETUP_TIMEOUT); + describe('ColdStart metrics', () => { 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: functionName }, + { Name: 'function_name', Value: fnNameBasicFeatures }, { Name: Object.keys(expectedDefaultDimensions)[0], Value: expectedDefaultDimensions.MyDimension, @@ -168,6 +138,14 @@ describe(`metrics E2E tests (decorator) for runtime: ${runtime}`, () => { 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, @@ -222,12 +200,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..b22861ab16 100644 --- a/packages/metrics/tests/e2e/basicFeatures.manual.test.ts +++ b/packages/metrics/tests/e2e/basicFeatures.manual.test.ts @@ -3,114 +3,79 @@ * * @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, invokeFunction, -} from '../../../commons/tests/utils/e2eUtils'; -import { TestStack, - defaultRuntime, } from '@aws-lambda-powertools/testing-utils'; -import { MetricUnits } from '../../src'; import { + CloudWatchClient, + GetMetricStatisticsCommand, +} from '@aws-sdk/client-cloudwatch'; +import { join } from 'node:path'; +import { getMetrics } from '../helpers/metricsUtils'; +import { MetricsTestNodejsFunction } from '../helpers/resources'; +import { + commonEnvironmentVars, 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; -if (!isValidRuntimeKey(runtime)) { - throw new Error(`Invalid runtime key value: ${runtime}`); -} +describe(`Metrics E2E tests, manual usage`, () => { + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'BasicFeatures-Manual', + }, + }); -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, + // Location of the lambda function code + const lambdaFunctionCodeFilePath = join( + __dirname, + 'basicFeatures.manual.test.functionCode.ts' + ); + const startTime = new Date(); + + const expectedServiceName = 'e2eManual'; + new MetricsTestNodejsFunction( + testStack, + { + entry: lambdaFunctionCodeFilePath, 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, }, - runtime: runtime, - }); + }, + { + nameSuffix: 'Manual', + } + ); + + const cloudwatchClient = new CloudWatchClient({}); + const invocations = 2; + + beforeAll(async () => { + // Deploy the stack await testStack.deploy(); - // and invoked - await invokeFunction(functionName, invocationCount, 'SEQUENTIAL'); + // Get the actual function names from the stack outputs + const functionName = testStack.findAndGetStackOutputValue('Manual'); + + // Act + await invokeFunction({ + functionName, + times: invocations, + invocationMode: 'SEQUENTIAL', + }); }, SETUP_TIMEOUT); describe('ColdStart metrics', () => { it( 'should capture ColdStart Metric', async () => { + const { EXPECTED_NAMESPACE: expectedNamespace } = commonEnvironmentVars; + // Check coldstart metric dimensions const coldStartMetrics = await getMetrics( cloudwatchClient, @@ -162,6 +127,14 @@ describe(`metrics E2E tests (manual) for runtime: ${runtime}`, () => { 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, @@ -216,7 +189,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..987e234b62 100644 --- a/packages/metrics/tests/e2e/constants.ts +++ b/packages/metrics/tests/e2e/constants.ts @@ -1,5 +1,31 @@ -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 { randomUUID } from 'node:crypto'; +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 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 { + RESOURCE_NAME_PREFIX, + ONE_MINUTE, + TEST_CASE_TIMEOUT, + SETUP_TIMEOUT, + TEARDOWN_TIMEOUT, + 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/parameters/tests/e2e/appConfigProvider.class.test.ts b/packages/parameters/tests/e2e/appConfigProvider.class.test.ts index ec662d8bb4..6d28b3ef04 100644 --- a/packages/parameters/tests/e2e/appConfigProvider.class.test.ts +++ b/packages/parameters/tests/e2e/appConfigProvider.class.test.ts @@ -3,123 +3,21 @@ * * @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, - isValidRuntimeKey, - createStackWithLambdaFunction, - invokeFunction, -} from '../../../commons/tests/utils/e2eUtils'; -import { InvocationLogs } from '../../../commons/tests/utils/InvocationLogs'; import { + invokeFunctionOnce, + TestInvocationLogs, + TestNodejsFunction, TestStack, - defaultRuntime, } from '@aws-lambda-powertools/testing-utils'; -import { ResourceAccessGranter } from '../helpers/cdkAspectGrantAccess'; +import { toBase64 } from '@aws-sdk/util-base64-node'; +import { join } from 'node:path'; +import { TestAppConfigWithProfiles } from '../helpers/resources'; 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,121 +71,117 @@ 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[]; - const encoder = new TextEncoder(); +describe(`Parameters E2E tests, AppConfig provider`, () => { + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'AppConfig', + }, + }); - beforeAll(async () => { - // Create a stack with a Lambda function - createStackWithLambdaFunction({ - stack: testStack.stack, - functionName, - functionEntry: path.join(__dirname, lambdaFunctionCodeFile), - environment: { - UUID: uuid, + // Location of the lambda function code + const lambdaFunctionCodeFilePath = join( + __dirname, + 'appConfigProvider.class.test.functionCode.ts' + ); - // Values(s) to be used by Parameters in the Lambda function - 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 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', }, - runtime, - }); - - // 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', + }, + values: { + myFeatureFlag: { + enabled: true, }, - }); + }, + }; - 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); + let invocationLogs: TestInvocationLogs; + const encoder = new TextEncoder(); - const freeFormBase64PlainText = createAppConfigConfigurationProfile({ - stack: testStack.stack, - application, - environment, - deploymentStrategy, - name: freeFormBase64PlainTextName, - type: 'AWS.Freeform', - content: { - content: freeFormBase64PlainTextValue, - contentType: 'text/plain', + beforeAll(async () => { + // Prepare + const testFunction = new TestNodejsFunction( + testStack, + { + entry: lambdaFunctionCodeFilePath, }, - }); - 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 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 +191,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 +203,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 +214,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 +228,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 +244,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 +261,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..59989d36bb 100644 --- a/packages/parameters/tests/e2e/dynamoDBProvider.class.test.ts +++ b/packages/parameters/tests/e2e/dynamoDBProvider.class.test.ts @@ -3,86 +3,21 @@ * * @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, - isValidRuntimeKey, - createStackWithLambdaFunction, - invokeFunction, -} from '../../../commons/tests/utils/e2eUtils'; -import { InvocationLogs } from '../../../commons/tests/utils/InvocationLogs'; import { + invokeFunctionOnce, + TestInvocationLogs, + TestNodejsFunction, TestStack, - defaultRuntime, } from '@aws-lambda-powertools/testing-utils'; -import { ResourceAccessGranter } from '../helpers/cdkAspectGrantAccess'; +import { AttributeType } from 'aws-cdk-lib/aws-dynamodb'; +import { join } from 'node:path'; +import { TestDynamodbTableWithItems } from '../helpers/resources'; 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,208 +95,179 @@ 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[]; - - beforeAll(async () => { - // Create a stack with a Lambda function - createStackWithLambdaFunction({ - stack: testStack.stack, - functionName, - functionEntry: path.join(__dirname, lambdaFunctionCodeFile), - 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, - TABLE_GET_MULTIPLE_CUSTOM_KEYS: tableGetMultipleCustomkeys, - KEY_ATTR: keyAttr, - SORT_ATTR: sortAttr, - VALUE_ATTR: valueAttr, - }, - runtime, - }); - - // 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, - }, - }); - - // Give the Lambda access to the DynamoDB tables - Aspects.of(testStack.stack).add( - new ResourceAccessGranter([ - ddbTableGet, - ddbTableGetMultiple, - ddbTableGetCustomKeys, - ddbTabelGetMultipleCustomKeys, - ]) - ); - - // Seed tables with test data - // Test 1 - putDynamoDBItem({ - stack: testStack.stack, - id: 'my-param-test1', - table: ddbTableGet, - item: { - id: 'my-param', - value: 'foo', - }, - }); +describe(`Parameters E2E tests, dynamoDB provider`, () => { + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'DynamoDBProvider', + }, + }); - // 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', - }, - }); + // Location of the lambda function code + const lambdaFunctionCodeFilePath = join( + __dirname, + 'dynamoDBProvider.class.test.functionCode.ts' + ); - // Test 3 - putDynamoDBItem({ - stack: testStack.stack, - id: 'my-param-test3', - table: ddbTableGetCustomKeys, - item: { - [keyAttr]: 'my-param', - [valueAttr]: 'foo', - }, - }); + const keyAttr = 'key'; + const sortAttr = 'sort'; + const valueAttr = 'val'; - // 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', - }, - }); + let invocationLogs: TestInvocationLogs; - // Test 5 - putDynamoDBItem({ - stack: testStack.stack, - id: 'my-param-test5', - table: ddbTableGet, - item: { - id: 'my-param-json', - value: JSON.stringify({ foo: 'bar' }), + beforeAll(async () => { + // Prepare + const testFunction = new TestNodejsFunction( + testStack, + { + entry: lambdaFunctionCodeFilePath, + environment: { + KEY_ATTR: keyAttr, + SORT_ATTR: sortAttr, + VALUE_ATTR: valueAttr, + }, }, - }); + { + nameSuffix: 'dynamoDBProvider', + } + ); - // 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 invokeFunction( + invocationLogs = await invokeFunctionOnce({ functionName, - invocationCount, - 'SEQUENTIAL' - ); + }); }, SETUP_TIMEOUT); describe('DynamoDBProvider usage', () => { @@ -369,8 +275,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 +290,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 +305,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 +320,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 +333,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 +344,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 +355,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 +369,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 +380,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..303aa37344 100644 --- a/packages/parameters/tests/e2e/secretsProvider.class.test.ts +++ b/packages/parameters/tests/e2e/secretsProvider.class.test.ts @@ -4,34 +4,21 @@ * @group e2e/parameters/secrets/class */ import { - createStackWithLambdaFunction, - generateUniqueName, - invokeFunction, - isValidRuntimeKey, -} from '../../../commons/tests/utils/e2eUtils'; + invokeFunctionOnce, + TestInvocationLogs, + TestNodejsFunction, + TestStack, +} from '@aws-lambda-powertools/testing-utils'; +import { SecretValue } from 'aws-cdk-lib'; +import { join } from 'node:path'; +import { TestSecret } from '../helpers/resources'; 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,139 +36,119 @@ 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, - runtime, - 'secretsProvider' - ); - const lambdaFunctionCodeFile = 'secretsProvider.class.test.functionCode.ts'; +describe(`Parameters E2E tests, Secrets Manager provider`, () => { + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'SecretsProvider', + }, + }); - const invocationCount = 1; + // Location of the lambda function code + const lambdaFunctionCodeFilePath = join( + __dirname, + 'secretsProvider.class.test.functionCode.ts' + ); - 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 testFunction = new TestNodejsFunction( + testStack, + { + entry: lambdaFunctionCodeFilePath, + }, + { + nameSuffix: 'secretsProvider', + } ); - // 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, - functionName: functionName, - functionEntry: path.join(__dirname, lambdaFunctionCodeFile), - tracing: Tracing.ACTIVE, - 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, + const secretString = new TestSecret( + testStack, + { + secretStringValue: SecretValue.unsafePlainText('foo'), }, - runtime: runtime, - }); - - const secretString = new Secret(testStack.stack, 'testSecretPlain', { - secretName: secretNamePlain, - 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(); - invocationLogs = await invokeFunction( + // Get the actual function names from the stack outputs + const functionName = + testStack.findAndGetStackOutputValue('secretsProvider'); + + 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 +161,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 +175,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 +188,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 +199,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..e8f0aae256 100644 --- a/packages/parameters/tests/e2e/ssmProvider.class.test.ts +++ b/packages/parameters/tests/e2e/ssmProvider.class.test.ts @@ -3,86 +3,23 @@ * * @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, - isValidRuntimeKey, - createStackWithLambdaFunction, - invokeFunction, -} from '../../../commons/tests/utils/e2eUtils'; -import { InvocationLogs } from '../../../commons/tests/utils/InvocationLogs'; import { + invokeFunctionOnce, + TestInvocationLogs, + TestNodejsFunction, TestStack, - defaultRuntime, } from '@aws-lambda-powertools/testing-utils'; -import { ResourceAccessGranter } from '../helpers/cdkAspectGrantAccess'; +import { join } from 'node:path'; +import { + TestSecureStringParameter, + TestStringParameter, +} from '../helpers/resources'; 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,70 +70,112 @@ 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 testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'AppConfig', + }, + }); - beforeAll(async () => { - // Create a stack with a Lambda function - createStackWithLambdaFunction({ - stack: testStack.stack, - functionName, - functionEntry: path.join(__dirname, lambdaFunctionCodeFile), - environment: { - UUID: uuid, + // Location of the lambda function code + const lambdaFunctionCodeFilePath = join( + __dirname, + 'ssmProvider.class.test.functionCode.ts' + ); - // 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, + // 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'; + + let invocationLogs: TestInvocationLogs; + + beforeAll(async () => { + // Prepare + const testFunction = new TestNodejsFunction( + testStack, + { + entry: lambdaFunctionCodeFilePath, }, - runtime, - }); + { + 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 invokeFunction( + invocationLogs = await invokeFunctionOnce({ functionName, - invocationCount, - 'SEQUENTIAL' - ); + }); }, SETUP_TIMEOUT); describe('SSMProvider usage', () => { @@ -204,8 +183,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 +198,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 +213,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 +239,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 +262,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 +286,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 +304,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 +324,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 +340,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/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 f351773f5b..0000000000 --- a/packages/parameters/tests/helpers/parametersUtils.ts +++ /dev/null @@ -1,222 +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, - }), - }); - - 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..59916cfb0e --- /dev/null +++ b/packages/parameters/tests/helpers/resources.ts @@ -0,0 +1,373 @@ +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 + ) { + 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, + }); + } +} + +/** + * A secret that can be used in tests. + */ +class TestSecret extends Secret { + public constructor( + testStack: TestStack, + props: Omit, + extraProps: ExtraTestProps + ) { + const secretId = concatenateResourceName({ + testName: testStack.testName, + resourceName: extraProps.nameSuffix, + }); + + super(testStack.stack, secretId, { + ...props, + secretName: `/${secretId}`, + }); + } +} + +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, +}; 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/TestStack.ts b/packages/testing/src/TestStack.ts index 8c84e86fb8..952f00a5a4 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 } 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,35 @@ 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, + 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 +92,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 +109,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 new file mode 100644 index 0000000000..4a737590b2 --- /dev/null +++ b/packages/testing/src/helpers.ts @@ -0,0 +1,84 @@ +import { randomUUID } from 'node:crypto'; +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. + * + * The maximum length of the name is 45 characters. + * + * @example + * ```ts + * process.env.RUNTIME = 'nodejs18x'; + * const testPrefix = 'E2E-TRACER'; + * const testName = 'someFeature'; + * const uniqueName = generateTestUniqueName({ testPrefix, testName }); + * // uniqueName = 'E2E-TRACER-node18-12345-someFeature' + * ``` + */ +const generateTestUniqueName = ({ + testPrefix, + testName, +}: { + testPrefix: string; + testName: string; +}): string => + [ + testPrefix, + getRuntimeKey().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); + +/** + * 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, + getRuntimeKey, + generateTestUniqueName, + concatenateResourceName, + findAndGetStackOutputValue, +}; 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/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 new file mode 100644 index 0000000000..d247551723 --- /dev/null +++ b/packages/testing/src/resources/TestNodejsFunction.ts @@ -0,0 +1,41 @@ +import { CfnOutput, Duration } from 'aws-cdk-lib'; +import { Tracing } from 'aws-cdk-lib/aws-lambda'; +import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; +import { RetentionDays } from 'aws-cdk-lib/aws-logs'; +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 outputs the function name. + */ +class TestNodejsFunction extends NodejsFunction { + public constructor( + stack: TestStack, + props: TestNodejsFunctionProps, + extraProps: ExtraTestProps + ) { + 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, + }); + + new CfnOutput(this, extraProps.nameSuffix, { + value: this.functionName, + }); + } +} + +export { ExtraTestProps, TestNodejsFunction, TestNodejsFunctionProps }; diff --git a/packages/testing/src/resources/index.ts b/packages/testing/src/resources/index.ts new file mode 100644 index 0000000000..ce4b5bbc26 --- /dev/null +++ b/packages/testing/src/resources/index.ts @@ -0,0 +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/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); }); }); diff --git a/packages/tracer/tests/e2e/allFeatures.decorator.test.ts b/packages/tracer/tests/e2e/allFeatures.decorator.test.ts index a6f9c0911d..9550628171 100644 --- a/packages/tracer/tests/e2e/allFeatures.decorator.test.ts +++ b/packages/tracer/tests/e2e/allFeatures.decorator.test.ts @@ -3,18 +3,18 @@ * * @group e2e/tracer/decorator */ -import path from 'path'; import { TestStack, - defaultRuntime, + TestDynamodbTable, } 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 { join } from 'node:path'; +import { TracerTestNodejsFunction } from '../helpers/resources'; +import { + assertAnnotation, + assertErrorAndFault, +} from '../helpers/traceAssertions'; import { - createTracerTestFunction, getFirstSubsegment, getFunctionArn, getInvocationSubsegment, @@ -23,191 +23,146 @@ import { splitSegmentsByName, } from '../helpers/tracesUtils'; import { - generateUniqueName, - isValidRuntimeKey, -} from '../../../commons/tests/utils/e2eUtils'; -import { - expectedCustomAnnotationKey, - expectedCustomAnnotationValue, - expectedCustomErrorMessage, - expectedCustomMetadataKey, - expectedCustomMetadataValue, - expectedCustomResponseValue, + commonEnvironmentVars, RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, 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; +describe(`Tracer E2E tests, all features with decorator instantiation`, () => { + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'AllFeatures-Decorator', + }, + }); -const testStack = new TestStack(stackName); + // Location of the lambda function code + const lambdaFunctionCodeFilePath = join( + __dirname, + 'allFeatures.decorator.test.functionCode.ts' + ); + const startTime = new Date(); + + /** + * Table used by all functions to make an SDK call + */ + const testTable = new TestDynamodbTable( + testStack, + {}, + { + nameSuffix: 'TestTable', + } + ); -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: { - name: 'id', - type: AttributeType.STRING, + /** + * Function #1 is with all flags enabled. + */ + let fnNameAllFlagsEnabled: string; + const fnAllFlagsEnabled = new TracerTestNodejsFunction( + testStack, + { + entry: lambdaFunctionCodeFilePath, + environment: { + TEST_TABLE_NAME: testTable.tableName, }, - billingMode: BillingMode.PAY_PER_REQUEST, - 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', + }, + { + nameSuffix: 'AllFlagsOn', + } + ); + testTable.grantWriteData(fnAllFlagsEnabled); + + /** + * Function #2 doesn't capture error or response + */ + let fnNameNoCaptureErrorOrResponse: string; + const fnNoCaptureErrorOrResponse = new TracerTestNodejsFunction( + testStack, + { + entry: lambdaFunctionCodeFilePath, + environment: { + TEST_TABLE_NAME: testTable.tableName, + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'false', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'false', }, - runtime, - }); - ddbTable.grantWriteData(functionWithAllFlagsEnabled); - - const functionThatDoesNotCapturesErrorAndResponse = - createTracerTestFunction({ - stack: testStack.stack, - functionName: functionNameWithNoCaptureErrorOrResponse, - entry, - expectedServiceName: serviceNameWithNoCaptureErrorOrResponse, - environmentParams: { - TEST_TABLE_NAME: ddbTableName, - POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'false', - POWERTOOLS_TRACER_CAPTURE_ERROR: 'false', - POWERTOOLS_TRACE_ENABLED: 'true', - }, - 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', + }, + { + nameSuffix: 'NoCaptureErrOrResp', + } + ); + testTable.grantWriteData(fnNoCaptureErrorOrResponse); + + /** + * Function #3 disables tracer + */ + let fnNameTracerDisabled: string; + const fnTracerDisabled = new TracerTestNodejsFunction( + testStack, + { + entry: lambdaFunctionCodeFilePath, + environment: { + TEST_TABLE_NAME: testTable.tableName, POWERTOOLS_TRACE_ENABLED: 'false', }, - runtime, - }); - ddbTable.grantWriteData(functionWithTracerDisabled); - - const functionWithCaptureResponseFalse = createTracerTestFunction({ - stack: testStack.stack, - functionName: functionNameWithCaptureResponseFalse, + }, + { + nameSuffix: 'TracerDisabled', + } + ); + testTable.grantWriteData(fnTracerDisabled); + + /** + * Function #4 disables capture response via decorator options + */ + let fnNameCaptureResponseOff: string; + const fnCaptureResponseOff = new TracerTestNodejsFunction( + testStack, + { + entry: lambdaFunctionCodeFilePath, handler: 'handlerWithCaptureResponseFalse', - entry, - expectedServiceName: serviceNameWithCaptureResponseFalse, - environmentParams: { - TEST_TABLE_NAME: ddbTableName, - POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', - POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', - POWERTOOLS_TRACE_ENABLED: 'true', + environment: { + TEST_TABLE_NAME: testTable.tableName, }, - runtime, - }); - ddbTable.grantWriteData(functionWithCaptureResponseFalse); + }, + { + nameSuffix: 'CaptureResponseOff', + } + ); + testTable.grantWriteData(fnCaptureResponseOff); + + const xrayClient = new XRayClient({}); + const invocationCount = 3; + beforeAll(async () => { + // Deploy the stack await testStack.deploy(); - // Act + // Get the actual function names from the stack outputs + fnNameAllFlagsEnabled = testStack.findAndGetStackOutputValue('AllFlagsOn'); + fnNameNoCaptureErrorOrResponse = + testStack.findAndGetStackOutputValue('NoCaptureErrOrResp'); + fnNameTracerDisabled = + testStack.findAndGetStackOutputValue('TracerDisabled'); + fnNameCaptureResponseOff = + testStack.findAndGetStackOutputValue('CaptureResponseOff'); + + // Invoke all functions await Promise.all([ - invokeAllTestCases(functionNameWithAllFlagsEnabled), - invokeAllTestCases(functionNameWithNoCaptureErrorOrResponse), - invokeAllTestCases(functionNameWithTracerDisabled), - invokeAllTestCases(functionNameWithCaptureResponseFalse), + invokeAllTestCases(fnNameAllFlagsEnabled, invocationCount), + invokeAllTestCases(fnNameNoCaptureErrorOrResponse, invocationCount), + invokeAllTestCases(fnNameTracerDisabled, invocationCount), + invokeAllTestCases(fnNameCaptureResponseOff, invocationCount), ]); }, SETUP_TIMEOUT); @@ -220,18 +175,21 @@ describe(`Tracer E2E tests, all features with decorator instantiation for runtim it( 'should generate all custom traces', async () => { + const { EXPECTED_CUSTOM_ERROR_MESSAGE: expectedCustomErrorMessage } = + commonEnvironmentVars; + const tracesWhenAllFlagsEnabled = await getTraces( xrayClient, startTime, - await getFunctionArn(stsClient, functionNameWithAllFlagsEnabled), - 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]; /** @@ -268,7 +226,7 @@ describe(`Tracer E2E tests, all features with decorator instantiation for runtim 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); } @@ -280,15 +238,23 @@ describe(`Tracer E2E tests, all features with decorator instantiation for runtim 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, - await getFunctionArn(stsClient, functionNameWithAllFlagsEnabled), - 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); @@ -298,7 +264,7 @@ describe(`Tracer E2E tests, all features with decorator instantiation for runtim assertAnnotation({ annotations, isColdStart, - expectedServiceName: serviceNameWithAllFlagsEnabled, + expectedServiceName: 'AllFlagsOn', expectedCustomAnnotationKey, expectedCustomAnnotationValue, }); @@ -306,16 +272,16 @@ describe(`Tracer E2E tests, all features with decorator instantiation for runtim if (!metadata) { fail('metadata is missing'); } - expect( - metadata[serviceNameWithAllFlagsEnabled][expectedCustomMetadataKey] - ).toEqual(expectedCustomMetadataValue); + expect(metadata['AllFlagsOn'][expectedCustomMetadataKey]).toEqual( + expectedCustomMetadataValue + ); - const shouldThrowAnError = i === invocations - 1; + const shouldThrowAnError = i === invocationCount - 1; if (!shouldThrowAnError) { // Assert that the metadata object contains the response - expect( - metadata[serviceNameWithAllFlagsEnabled]['index.handler response'] - ).toEqual(expectedCustomResponseValue); + expect(metadata['AllFlagsOn']['index.handler response']).toEqual( + expectedCustomResponseValue + ); } } }, @@ -328,18 +294,15 @@ describe(`Tracer E2E tests, all features with decorator instantiation for runtim const tracesWithNoCaptureErrorOrResponse = await getTraces( xrayClient, startTime, - await getFunctionArn( - stsClient, - functionNameWithNoCaptureErrorOrResponse - ), - 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]; /** @@ -376,7 +339,7 @@ describe(`Tracer E2E tests, all features with decorator instantiation for runtim 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); @@ -392,18 +355,21 @@ describe(`Tracer E2E tests, all features with decorator instantiation for runtim 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, - await getFunctionArn(stsClient, functionNameWithCaptureResponseFalse), - 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]; /** @@ -450,7 +416,7 @@ describe(`Tracer E2E tests, all features with decorator instantiation for runtim expect(myMethodSegment).toBeDefined(); expect(myMethodSegment).not.toHaveProperty('metadata'); - const shouldThrowAnError = i === invocations - 1; + const shouldThrowAnError = i === invocationCount - 1; if (shouldThrowAnError) { assertErrorAndFault(invocationSubsegment, expectedCustomErrorMessage); } @@ -466,15 +432,15 @@ describe(`Tracer E2E tests, all features with decorator instantiation for runtim const tracesWithTracerDisabled = await getTraces( xrayClient, startTime, - await getFunctionArn(stsClient, functionNameWithTracerDisabled), - 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); @@ -484,7 +450,7 @@ describe(`Tracer E2E tests, all features with decorator instantiation for runtim 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 9d8cbb7bed..e3d6c427f4 100644 --- a/packages/tracer/tests/e2e/allFeatures.manual.test.ts +++ b/packages/tracer/tests/e2e/allFeatures.manual.test.ts @@ -3,18 +3,18 @@ * * @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 { + TestDynamodbTable, TestStack, - defaultRuntime, } from '@aws-lambda-powertools/testing-utils'; +import { XRayClient } from '@aws-sdk/client-xray'; +import { join } from 'path'; +import { TracerTestNodejsFunction } from '../helpers/resources'; +import { + assertAnnotation, + assertErrorAndFault, +} from '../helpers/traceAssertions'; import { - createTracerTestFunction, getFirstSubsegment, getFunctionArn, getInvocationSubsegment, @@ -24,101 +24,74 @@ import { } from '../helpers/tracesUtils'; import type { ParsedTrace } from '../helpers/traceUtils.types'; import { - generateUniqueName, - isValidRuntimeKey, -} from '../../../commons/tests/utils/e2eUtils'; -import { - expectedCustomAnnotationKey, - expectedCustomAnnotationValue, - expectedCustomErrorMessage, - expectedCustomMetadataKey, - expectedCustomMetadataValue, - expectedCustomResponseValue, + commonEnvironmentVars, RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, 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 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 xrayClient = new XRayClient({}); -const stsClient = new STSClient({}); -const invocations = 3; -let sortedTraces: ParsedTrace[]; +describe(`Tracer E2E tests, all features with manual instantiation`, () => { + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'AllFeatures-Manual', + }, + }); -const testStack = new TestStack(stackName); + // Location of the lambda function code + const lambdaFunctionCodeFilePath = join( + __dirname, + 'allFeatures.manual.test.functionCode.ts' + ); + const startTime = new Date(); + + /** + * Table used by all functions to make an SDK call + */ + const testTable = new TestDynamodbTable( + testStack, + {}, + { + nameSuffix: 'TestTable', + } + ); -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: { - name: 'id', - type: AttributeType.STRING, + let fnNameAllFlagsEnabled: string; + const fnAllFlagsEnabled = new TracerTestNodejsFunction( + testStack, + { + entry: lambdaFunctionCodeFilePath, + environment: { + TEST_TABLE_NAME: testTable.tableName, }, - billingMode: BillingMode.PAY_PER_REQUEST, - removalPolicy: RemovalPolicy.DESTROY, - }); + }, + { + nameSuffix: 'AllFlagsOn', + } + ); + testTable.grantWriteData(fnAllFlagsEnabled); - ddbTable.grantWriteData(testFunction); + const xrayClient = new XRayClient({}); + const invocationCount = 3; + let sortedTraces: ParsedTrace[]; + beforeAll(async () => { + // Deploy the stack await testStack.deploy(); - // Act - await invokeAllTestCases(functionName); + // Get the actual function names from the stack outputs + fnNameAllFlagsEnabled = testStack.findAndGetStackOutputValue('AllFlagsOn'); + + // Invoke all test cases + await invokeAllTestCases(fnNameAllFlagsEnabled, invocationCount); // Retrieve traces from X-Ray for assertion - const lambdaFunctionArn = await getFunctionArn(stsClient, functionName); sortedTraces = await getTraces( xrayClient, startTime, - lambdaFunctionArn, - invocations, + await getFunctionArn(fnNameAllFlagsEnabled), + invocationCount, 4 ); }, SETUP_TIMEOUT); @@ -132,10 +105,13 @@ describe(`Tracer E2E tests, all features with manual instantiation for runtime: it( 'should generate all custom traces', async () => { - expect(sortedTraces.length).toBe(invocations); + const { EXPECTED_CUSTOM_ERROR_MESSAGE: expectedCustomErrorMessage } = + commonEnvironmentVars; + + expect(sortedTraces.length).toBe(invocationCount); // Assess - for (let i = 0; i < invocations; i++) { + for (let i = 0; i < invocationCount; i++) { const trace = sortedTraces[i]; /** @@ -169,7 +145,7 @@ describe(`Tracer E2E tests, all features with manual instantiation for runtime: 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); } @@ -181,7 +157,15 @@ describe(`Tracer E2E tests, all features with manual instantiation for runtime: it( 'should have correct annotations and metadata', async () => { - for (let i = 0; i < invocations; i++) { + 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); const handlerSubsegment = getFirstSubsegment(invocationSubsegment); @@ -191,7 +175,7 @@ describe(`Tracer E2E tests, all features with manual instantiation for runtime: assertAnnotation({ annotations, isColdStart, - expectedServiceName, + expectedServiceName: 'AllFlagsOn', expectedCustomAnnotationKey, expectedCustomAnnotationValue, }); @@ -199,16 +183,16 @@ describe(`Tracer E2E tests, all features with manual instantiation for runtime: if (!metadata) { fail('metadata is missing'); } - expect( - metadata[expectedServiceName][expectedCustomMetadataKey] - ).toEqual(expectedCustomMetadataValue); + expect(metadata['AllFlagsOn'][expectedCustomMetadataKey]).toEqual( + expectedCustomMetadataValue + ); - const shouldThrowAnError = i === invocations - 1; + const shouldThrowAnError = i === invocationCount - 1; if (!shouldThrowAnError) { // Assert that the metadata object contains the response - expect( - metadata[expectedServiceName]['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 7066a549db..7fbe843943 100644 --- a/packages/tracer/tests/e2e/allFeatures.middy.test.ts +++ b/packages/tracer/tests/e2e/allFeatures.middy.test.ts @@ -3,19 +3,18 @@ * * @group e2e/tracer/middy */ - -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, - defaultRuntime, + TestDynamodbTable, } from '@aws-lambda-powertools/testing-utils'; +import { XRayClient } from '@aws-sdk/client-xray'; +import { join } from 'node:path'; +import { TracerTestNodejsFunction } from '../helpers/resources'; +import { + assertAnnotation, + assertErrorAndFault, +} from '../helpers/traceAssertions'; import { - createTracerTestFunction, getFirstSubsegment, getFunctionArn, getInvocationSubsegment, @@ -24,195 +23,146 @@ import { splitSegmentsByName, } from '../helpers/tracesUtils'; import { - generateUniqueName, - isValidRuntimeKey, -} from '../../../commons/tests/utils/e2eUtils'; -import { - expectedCustomAnnotationKey, - expectedCustomAnnotationValue, - expectedCustomErrorMessage, - expectedCustomMetadataKey, - expectedCustomMetadataValue, - expectedCustomResponseValue, + commonEnvironmentVars, RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, 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 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; +describe(`Tracer E2E tests, all features with middy instantiation`, () => { + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'AllFeatures-Decorator', + }, + }); -const testStack = new TestStack(stackName); + // Location of the lambda function code + const lambdaFunctionCodeFilePath = join( + __dirname, + 'allFeatures.middy.test.functionCode.ts' + ); + const startTime = new Date(); + + /** + * Table used by all functions to make an SDK call + */ + const testTable = new TestDynamodbTable( + testStack, + {}, + { + nameSuffix: 'TestTable', + } + ); -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: { - name: 'id', - type: AttributeType.STRING, + /** + * Function #1 is with all flags enabled. + */ + let fnNameAllFlagsEnabled: string; + const fnAllFlagsEnabled = new TracerTestNodejsFunction( + testStack, + { + entry: lambdaFunctionCodeFilePath, + environment: { + TEST_TABLE_NAME: testTable.tableName, }, - billingMode: BillingMode.PAY_PER_REQUEST, - 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', + }, + { + nameSuffix: 'AllFlagsOn', + } + ); + testTable.grantWriteData(fnAllFlagsEnabled); + + /** + * Function #2 doesn't capture error or response + */ + let fnNameNoCaptureErrorOrResponse: string; + const fnNoCaptureErrorOrResponse = new TracerTestNodejsFunction( + testStack, + { + entry: lambdaFunctionCodeFilePath, + environment: { + TEST_TABLE_NAME: testTable.tableName, + POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'false', + POWERTOOLS_TRACER_CAPTURE_ERROR: 'false', }, - runtime, - }); - ddbTable.grantWriteData(functionWithAllFlagsEnabled); - - const functionThatDoesNotCapturesErrorAndResponse = - createTracerTestFunction({ - stack: testStack.stack, - functionName: functionNameWithNoCaptureErrorOrResponse, - entry, - expectedServiceName: serviceNameWithNoCaptureErrorOrResponse, - environmentParams: { - TEST_TABLE_NAME: ddbTableName, - POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'false', - POWERTOOLS_TRACER_CAPTURE_ERROR: 'false', - POWERTOOLS_TRACE_ENABLED: 'true', - }, - 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', + }, + { + nameSuffix: 'NoCaptureErrOrResp', + } + ); + testTable.grantWriteData(fnNoCaptureErrorOrResponse); + + /** + * Function #3 disables tracer + */ + let fnNameTracerDisabled: string; + const fnTracerDisabled = new TracerTestNodejsFunction( + testStack, + { + entry: lambdaFunctionCodeFilePath, + environment: { + TEST_TABLE_NAME: testTable.tableName, POWERTOOLS_TRACE_ENABLED: 'false', }, - runtime, - }); - ddbTable.grantWriteData(functionWithTracerDisabled); - - const functionThatDoesNotCaptureResponseViaMiddlewareOption = - createTracerTestFunction({ - stack: testStack.stack, - functionName: functionNameWithNoCaptureResponseViaMiddlewareOption, - entry, - handler: 'handlerWithNoCaptureResponseViaMiddlewareOption', - expectedServiceName: - serviceNameWithNoCaptureResponseViaMiddlewareOption, - environmentParams: { - TEST_TABLE_NAME: ddbTableName, - POWERTOOLS_TRACER_CAPTURE_RESPONSE: 'true', - POWERTOOLS_TRACER_CAPTURE_ERROR: 'true', - POWERTOOLS_TRACE_ENABLED: 'true', - }, - runtime, - }); - ddbTable.grantWriteData( - functionThatDoesNotCaptureResponseViaMiddlewareOption - ); + }, + { + nameSuffix: 'TracerDisabled', + } + ); + testTable.grantWriteData(fnTracerDisabled); + + /** + * Function #4 disables response capture via middleware option + */ + let fnNameCaptureResponseOff: string; + const fnCaptureResponseOff = new TracerTestNodejsFunction( + testStack, + { + entry: lambdaFunctionCodeFilePath, + handler: 'handlerWithNoCaptureResponseViaMiddlewareOption', + environment: { + TEST_TABLE_NAME: testTable.tableName, + }, + }, + { + nameSuffix: 'CaptureResponseOff', + } + ); + testTable.grantWriteData(fnCaptureResponseOff); + + const xrayClient = new XRayClient({}); + const invocationCount = 3; + beforeAll(async () => { + // Deploy the stack await testStack.deploy(); - // Act + // Get the actual function names from the stack outputs + fnNameAllFlagsEnabled = testStack.findAndGetStackOutputValue('AllFlagsOn'); + fnNameNoCaptureErrorOrResponse = + testStack.findAndGetStackOutputValue('NoCaptureErrOrResp'); + fnNameTracerDisabled = + testStack.findAndGetStackOutputValue('TracerDisabled'); + fnNameCaptureResponseOff = + testStack.findAndGetStackOutputValue('CaptureResponseOff'); + + // Invoke all functions await Promise.all([ - invokeAllTestCases(functionNameWithAllFlagsEnabled), - invokeAllTestCases(functionNameWithNoCaptureErrorOrResponse), - invokeAllTestCases(functionNameWithTracerDisabled), - invokeAllTestCases(functionNameWithNoCaptureResponseViaMiddlewareOption), + invokeAllTestCases(fnNameAllFlagsEnabled, invocationCount), + invokeAllTestCases(fnNameNoCaptureErrorOrResponse, invocationCount), + invokeAllTestCases(fnNameTracerDisabled, invocationCount), + invokeAllTestCases(fnNameCaptureResponseOff, invocationCount), ]); }, SETUP_TIMEOUT); @@ -225,18 +175,21 @@ describe(`Tracer E2E tests, all features with middy instantiation for runtime: $ it( 'should generate all custom traces', async () => { + const { EXPECTED_CUSTOM_ERROR_MESSAGE: expectedCustomErrorMessage } = + commonEnvironmentVars; + const tracesWhenAllFlagsEnabled = await getTraces( xrayClient, startTime, - await getFunctionArn(stsClient, functionNameWithAllFlagsEnabled), - 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]; /** @@ -270,7 +223,7 @@ describe(`Tracer E2E tests, all features with middy instantiation for runtime: $ 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); } @@ -282,15 +235,23 @@ describe(`Tracer E2E tests, all features with middy instantiation for runtime: $ 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, - await getFunctionArn(stsClient, functionNameWithAllFlagsEnabled), - 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); @@ -300,7 +261,7 @@ describe(`Tracer E2E tests, all features with middy instantiation for runtime: $ assertAnnotation({ annotations, isColdStart, - expectedServiceName: serviceNameWithAllFlagsEnabled, + expectedServiceName: 'AllFlagsOn', expectedCustomAnnotationKey, expectedCustomAnnotationValue, }); @@ -308,16 +269,16 @@ describe(`Tracer E2E tests, all features with middy instantiation for runtime: $ if (!metadata) { fail('metadata is missing'); } - expect( - metadata[serviceNameWithAllFlagsEnabled][expectedCustomMetadataKey] - ).toEqual(expectedCustomMetadataValue); + expect(metadata['AllFlagsOn'][expectedCustomMetadataKey]).toEqual( + expectedCustomMetadataValue + ); - const shouldThrowAnError = i === invocations - 1; + const shouldThrowAnError = i === invocationCount - 1; if (!shouldThrowAnError) { // Assert that the metadata object contains the response - expect( - metadata[serviceNameWithAllFlagsEnabled]['index.handler response'] - ).toEqual(expectedCustomResponseValue); + expect(metadata['AllFlagsOn']['index.handler response']).toEqual( + expectedCustomResponseValue + ); } } }, @@ -330,18 +291,15 @@ describe(`Tracer E2E tests, all features with middy instantiation for runtime: $ const tracesWithNoCaptureErrorOrResponse = await getTraces( xrayClient, startTime, - await getFunctionArn( - stsClient, - functionNameWithNoCaptureErrorOrResponse - ), - 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]; /** @@ -375,7 +333,7 @@ describe(`Tracer E2E tests, all features with middy instantiation for runtime: $ 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); @@ -391,21 +349,21 @@ describe(`Tracer E2E tests, all features with middy instantiation for runtime: $ 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, - await getFunctionArn( - stsClient, - functionNameWithNoCaptureResponseViaMiddlewareOption - ), - 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]; /** @@ -443,7 +401,7 @@ describe(`Tracer E2E tests, all features with middy instantiation for runtime: $ 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); } @@ -459,15 +417,15 @@ describe(`Tracer E2E tests, all features with middy instantiation for runtime: $ const tracesWithTracerDisabled = await getTraces( xrayClient, startTime, - await getFunctionArn(stsClient, functionNameWithTracerDisabled), - 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); @@ -477,7 +435,7 @@ describe(`Tracer E2E tests, all features with middy instantiation for runtime: $ 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 8d460ee0f1..a37f0ad34b 100644 --- a/packages/tracer/tests/e2e/asyncHandler.decorator.test.ts +++ b/packages/tracer/tests/e2e/asyncHandler.decorator.test.ts @@ -3,19 +3,18 @@ * * @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, - defaultRuntime, + TestDynamodbTable, } from '@aws-lambda-powertools/testing-utils'; +import { XRayClient } from '@aws-sdk/client-xray'; +import { join } from 'node:path'; +import { TracerTestNodejsFunction } from '../helpers/resources'; +import { + assertAnnotation, + assertErrorAndFault, +} from '../helpers/traceAssertions'; import { - createTracerTestFunction, getFirstSubsegment, getFunctionArn, getInvocationSubsegment, @@ -24,128 +23,93 @@ import { splitSegmentsByName, } from '../helpers/tracesUtils'; import { - generateUniqueName, - isValidRuntimeKey, -} from '../../../commons/tests/utils/e2eUtils'; -import { - expectedCustomAnnotationKey, - expectedCustomAnnotationValue, - expectedCustomErrorMessage, - expectedCustomMetadataKey, - expectedCustomMetadataValue, - expectedCustomResponseValue, - expectedCustomSubSegmentName, + commonEnvironmentVars, RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, 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; - -/** - * 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; -const xrayClient = new XRayClient({}); -const stsClient = new STSClient({}); -const invocations = 3; +describe(`Tracer E2E tests, async handler with decorator instantiation`, () => { + const testStack = new TestStack({ + stackNameProps: { + stackNamePrefix: RESOURCE_NAME_PREFIX, + testName: 'AllFeatures-Decorator', + }, + }); -const testStack = new TestStack(stackName); + // Location of the lambda function code + const lambdaFunctionCodeFilePath = join( + __dirname, + 'asyncHandler.decorator.test.functionCode.ts' + ); + const startTime = new Date(); + + /** + * Table used by all functions to make an SDK call + */ + const testTable = new TestDynamodbTable( + testStack, + {}, + { + nameSuffix: 'TestTable', + } + ); -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: { - name: 'id', - type: AttributeType.STRING, - }, - billingMode: BillingMode.PAY_PER_REQUEST, - 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', + /** + * Function #1 is with all flags enabled. + */ + let fnNameAllFlagsEnabled: string; + const fnAllFlagsEnabled = new TracerTestNodejsFunction( + testStack, + { + entry: lambdaFunctionCodeFilePath, + environment: { + TEST_TABLE_NAME: testTable.tableName, }, - runtime, - }); - ddbTable.grantWriteData(functionWithAllFlagsEnabled); - - const functionWithCustomSubsegmentNameInMethod = createTracerTestFunction({ - stack: testStack.stack, - functionName: functionNameWithCustomSubsegmentNameInMethod, + }, + { + nameSuffix: 'AllFlagsOn', + } + ); + testTable.grantWriteData(fnAllFlagsEnabled); + + /** + * Function #2 sets a custom subsegment name in the decorated method + */ + let fnNameCustomSubsegment: string; + const fnCustomSubsegmentName = new TracerTestNodejsFunction( + testStack, + { + entry: lambdaFunctionCodeFilePath, 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', + environment: { + TEST_TABLE_NAME: testTable.tableName, }, - runtime, - }); - ddbTable.grantWriteData(functionWithCustomSubsegmentNameInMethod); + }, + { + nameSuffix: 'CustomSubsegmentName', + } + ); + testTable.grantWriteData(fnCustomSubsegmentName); + + const xrayClient = new XRayClient({}); + const invocationsCount = 3; + beforeAll(async () => { + // Deploy the stack await testStack.deploy(); + // Get the actual function names from the stack outputs + fnNameAllFlagsEnabled = testStack.findAndGetStackOutputValue('AllFlagsOn'); + fnNameCustomSubsegment = testStack.findAndGetStackOutputValue( + 'CustomSubsegmentName' + ); + // Act await Promise.all([ - invokeAllTestCases(functionNameWithAllFlagsEnabled), - invokeAllTestCases(functionNameWithCustomSubsegmentNameInMethod), + invokeAllTestCases(fnNameAllFlagsEnabled, invocationsCount), + invokeAllTestCases(fnNameCustomSubsegment, invocationsCount), ]); }, SETUP_TIMEOUT); @@ -158,18 +122,21 @@ describe(`Tracer E2E tests, asynchronous handler with decorator instantiation fo it( 'should generate all custom traces', async () => { + const { EXPECTED_CUSTOM_ERROR_MESSAGE: expectedCustomErrorMessage } = + commonEnvironmentVars; + const tracesWhenAllFlagsEnabled = await getTraces( xrayClient, startTime, - await getFunctionArn(stsClient, functionNameWithAllFlagsEnabled), - 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]; /** @@ -206,7 +173,7 @@ describe(`Tracer E2E tests, asynchronous handler with decorator instantiation fo 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); } @@ -218,15 +185,23 @@ describe(`Tracer E2E tests, asynchronous handler with decorator instantiation fo 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, - await getFunctionArn(stsClient, functionNameWithAllFlagsEnabled), - 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); @@ -236,7 +211,7 @@ describe(`Tracer E2E tests, asynchronous handler with decorator instantiation fo assertAnnotation({ annotations, isColdStart, - expectedServiceName: serviceNameWithAllFlagsEnabled, + expectedServiceName: 'AllFlagsOn', expectedCustomAnnotationKey, expectedCustomAnnotationValue, }); @@ -244,16 +219,16 @@ describe(`Tracer E2E tests, asynchronous handler with decorator instantiation fo if (!metadata) { fail('metadata is missing'); } - expect( - metadata[serviceNameWithAllFlagsEnabled][expectedCustomMetadataKey] - ).toEqual(expectedCustomMetadataValue); + expect(metadata['AllFlagsOn'][expectedCustomMetadataKey]).toEqual( + expectedCustomMetadataValue + ); - const shouldThrowAnError = i === invocations - 1; + const shouldThrowAnError = i === invocationsCount - 1; if (!shouldThrowAnError) { // Assert that the metadata object contains the response - expect( - metadata[serviceNameWithAllFlagsEnabled]['index.handler response'] - ).toEqual(expectedCustomResponseValue); + expect(metadata['AllFlagsOn']['index.handler response']).toEqual( + expectedCustomResponseValue + ); } } }, @@ -263,21 +238,25 @@ describe(`Tracer E2E tests, asynchronous handler with decorator instantiation fo 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, - await getFunctionArn( - stsClient, - functionNameWithCustomSubsegmentNameInMethod - ), - 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]; /** @@ -316,7 +295,7 @@ describe(`Tracer E2E tests, asynchronous handler with decorator instantiation fo 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/e2e/constants.ts b/packages/tracer/tests/e2e/constants.ts index 7d27f4cde4..7e817e1aac 100644 --- a/packages/tracer/tests/e2e/constants.ts +++ b/packages/tracer/tests/e2e/constants.ts @@ -1,13 +1,30 @@ -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 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 { + RESOURCE_NAME_PREFIX, + ONE_MINUTE, + TEST_CASE_TIMEOUT, + SETUP_TIMEOUT, + TEARDOWN_TIMEOUT, + commonEnvironmentVars, +}; 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/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 }; 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..daa7d6cc70 100644 --- a/packages/tracer/tests/helpers/tracesUtils.ts +++ b/packages/tracer/tests/helpers/tracesUtils.ts @@ -1,33 +1,17 @@ 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, 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 { - expectedCustomAnnotationKey, - expectedCustomAnnotationValue, - expectedCustomErrorMessage, - expectedCustomMetadataKey, - expectedCustomMetadataValue, - expectedCustomResponseValue, -} from '../e2e/constants'; -import { - invokeFunction, - TEST_RUNTIMES, - TestRuntimesKey, -} from '../../../commons/tests/utils/e2eUtils'; +import { invokeFunction } 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 @@ -228,67 +212,36 @@ 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 invokeFunction(functionName, 1, 'SEQUENTIAL', { - invocation: 3, - throw: true, // only last invocation should throw - }); -}; - -const createTracerTestFunction = ( - params: TracerTestFunctionParams -): NodejsFunction => { - const { - stack, +const invokeAllTestCases = async ( + functionName: string, + times: number +): Promise => { + await invokeFunction({ 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, - }, - timeout: Duration.seconds(30), // Default value (3 seconds) will time out - runtime: TEST_RUNTIMES[runtime as TestRuntimesKey], + times, + invocationMode: 'SEQUENTIAL', + payload: [ + { + invocation: 1, + throw: false, + }, + { + invocation: 2, + throw: false, + }, + { + invocation: 3, + throw: true, // only last invocation should throw + }, + ], }); - - return func; }; 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; } @@ -303,6 +256,5 @@ export { getInvocationSubsegment, splitSegmentsByName, invokeAllTestCases, - createTracerTestFunction, getFunctionArn, };