diff --git a/packages/parameters/package.json b/packages/parameters/package.json index bc1bf31e9b..32c9d56d69 100644 --- a/packages/parameters/package.json +++ b/packages/parameters/package.json @@ -13,9 +13,9 @@ "commit": "commit", "test": "npm run test:unit", "test:unit": "jest --group=unit --detectOpenHandles --coverage --verbose", - "test:e2e:nodejs14x": "echo \"Not implemented\"", - "test:e2e:nodejs16x": "echo \"Not implemented\"", - "test:e2e:nodejs18x": "echo \"Not implemented\"", + "test:e2e:nodejs14x": "RUNTIME=nodejs14x jest --group=e2e", + "test:e2e:nodejs16x": "RUNTIME=nodejs16x jest --group=e2e", + "test:e2e:nodejs18x": "RUNTIME=nodejs18x jest --group=e2e", "test:e2e": "echo \"Not implemented\"", "watch": "jest --watch", "build": "tsc", @@ -62,4 +62,4 @@ "dependencies": { "@aws-sdk/util-base64-node": "^3.209.0" } -} +} \ No newline at end of file diff --git a/packages/parameters/tests/e2e/constants.ts b/packages/parameters/tests/e2e/constants.ts new file mode 100644 index 0000000000..03fb4eed52 --- /dev/null +++ b/packages/parameters/tests/e2e/constants.ts @@ -0,0 +1,5 @@ +export const RESOURCE_NAME_PREFIX = 'Parameters-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; \ No newline at end of file diff --git a/packages/parameters/tests/e2e/dynamoDBProvider.class.test.functionCode.ts b/packages/parameters/tests/e2e/dynamoDBProvider.class.test.functionCode.ts new file mode 100644 index 0000000000..f8d80314ae --- /dev/null +++ b/packages/parameters/tests/e2e/dynamoDBProvider.class.test.functionCode.ts @@ -0,0 +1,201 @@ +import { Context } from 'aws-lambda'; +import { DynamoDBProvider } from '../../src/dynamodb'; +import { TinyLogger } from '../helpers/tinyLogger'; +// # TODO: Uncomment code below once #1222 is fixed +/* +import { middleware } from '../helpers/sdkMiddlewareRequestCounter'; +import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; +*/ + +const tableGet = process.env.TABLE_GET ?? 'my-table'; +const tableGetMultiple = process.env.TABLE_GET_MULTIPLE ?? 'my-table'; +const tableGetCustomkeys = process.env.TABLE_GET_CUSTOM_KEYS ?? 'my-table'; +const tableGetMultipleCustomkeys = process.env.TABLE_GET_MULTIPLE_CUSTOM_KEYS ?? 'my-table'; +const keyAttr = process.env.KEY_ATTR ?? 'id'; +const sortAttr = process.env.SORT_ATTR ?? 'sk'; +const valueAttr = process.env.VALUE_ATTR ?? 'value'; + +// We use a custom logger to log pure JSON objects to stdout +const logger = new TinyLogger(); + +// Provider test 1, 5, 6 +const providerGet = new DynamoDBProvider({ + tableName: tableGet, +}); +// Provider test 2, 7 +const providerGetMultiple = new DynamoDBProvider({ + tableName: tableGetMultiple, +}); +// Provider test 3 +const providerGetCustomKeys = new DynamoDBProvider({ + tableName: tableGetCustomkeys, + keyAttr, + valueAttr, +}); +// Provider 4 +const providerGetMultipleCustomKeys = new DynamoDBProvider({ + tableName: tableGetMultipleCustomkeys, + keyAttr, + sortAttr, + valueAttr, +}); +// # TODO: Uncomment code below once #1222 is fixed +/* +// Provider test 8, 9 +const customClient = new DynamoDBClient({}); +providerWithMiddleware.middlewareStack.use(middleware); +const providerWithMiddleware = new DynamoDBProvider({ + awsSdkV3Client: customClient +}); +*/ + +export const handler = async (_event: unknown, _context: Context): Promise => { + // Test 1 - get a single parameter with default options (keyAttr: 'id', valueAttr: 'value') + try { + const parameterValue = await providerGet.get('my-param'); + logger.log({ + test: 'get', + value: parameterValue + }); + } catch (err) { + logger.log({ + test: 'get', + error: err.message + }); + } + + // Test 2 - get multiple parameters with default options (keyAttr: 'id', sortAttr: 'sk', valueAttr: 'value') + try { + const parametersValues = await providerGetMultiple.getMultiple('my-params'); + logger.log({ + test: 'get-multiple', + value: parametersValues + }); + } catch (err) { + logger.log({ + test: 'get-multiple', + error: err.message + }); + } + + // Test 3 - get a single parameter with custom options (keyAttr: 'key', valueAttr: 'val') + try { + const parameterValueCustom = await providerGetCustomKeys.get('my-param'); + logger.log({ + test: 'get-custom', + value: parameterValueCustom + }); + } catch (err) { + logger.log({ + test: 'get-custom', + error: err.message + }); + } + + // Test 4 - get multiple parameters with custom options (keyAttr: 'key', sortAttr: 'sort', valueAttr: 'val') + try { + const parametersValuesCustom = await providerGetMultipleCustomKeys.getMultiple('my-params'); + logger.log({ + test: 'get-multiple-custom', + value: parametersValuesCustom + }); + } catch (err) { + logger.log({ + test: 'get-multiple-custom', + error: err.message + }); + } + + // Test 5 - get a single parameter with json transform + try { + const parameterValueJson = await providerGet.get('my-param-json', { + transform: 'json' + }); + logger.log({ + test: 'get-json-transform', + value: typeof parameterValueJson // should be object + }); + } catch (err) { + logger.log({ + test: 'get-json-transform', + error: err.message + }); + } + + // Test 6 - get a single parameter with binary transform + try { + const parameterValueBinary = await providerGet.get('my-param-binary', { + transform: 'binary' + }); + logger.log({ + test: 'get-binary-transform', + value: typeof parameterValueBinary // should be string + }); + } catch (err) { + logger.log({ + test: 'get-binary-transform', + error: err.message + }); + } + + // Test 7 - get multiple parameters with auto transform + try { + const parametersValuesAuto = await providerGetMultiple.getMultiple('my-encoded-params', { + transform: 'auto' + }); + if (!parametersValuesAuto) throw new Error('parametersValuesAuto is undefined'); + + logger.log({ + test: 'get-multiple-auto-transform', + value: + `${typeof parametersValuesAuto['config.json']},${typeof parametersValuesAuto['key.binary']}` // should be object,string + }); + } catch (err) { + logger.log({ + test: 'get-multiple-auto-transform', + error: err.message + }); + } + + // # TODO: Uncomment code below once #1222 is fixed + /** + * Test 8 - get a parameter twice, second time should be cached + * + * Should only make 1 request, we use middleware to count requests + */ + /* + try { + await providerWithMiddleware.get('my-param'); + await providerWithMiddleware.get('my-param'); + logger.log({ + test: 'get-cache-request-count', + value: middleware.requestCount + }); + } catch (err) { + logger.log({ + test: 'get-cache-request-count', + error: err.message + }); + } + */ + + /** + * Test 9 - get a parameter once more but with forceFetch = true + * + * Request count should increase to 2, we use middleware to count requests + */ + /* + try { + await providerWithMiddleware.get('my-param', { forceFetch: true }); + logger.log({ + test: 'get-force-fetch-request-count', + value: middleware.requestCount + }); + } catch (err) { + logger.log({ + test: 'get-force-fetch-request-count', + error: err.message + }); + } + */ +}; \ No newline at end of file diff --git a/packages/parameters/tests/e2e/dynamoDBProvider.class.test.ts b/packages/parameters/tests/e2e/dynamoDBProvider.class.test.ts new file mode 100644 index 0000000000..ce8f7f50d4 --- /dev/null +++ b/packages/parameters/tests/e2e/dynamoDBProvider.class.test.ts @@ -0,0 +1,419 @@ +/** + * Test DynamoDBProvider class + * + * @group e2e/parameters/dynamodb/class + */ +import path from 'path'; +import { Tracing } from 'aws-cdk-lib/aws-lambda'; +import { AttributeType } from 'aws-cdk-lib/aws-dynamodb'; +import { App, Stack, Aspects } from 'aws-cdk-lib'; +import { DynamoDBClient, PutItemCommand } from '@aws-sdk/client-dynamodb'; +import { marshall } from '@aws-sdk/util-dynamodb'; +import { v4 } from 'uuid'; +import { + generateUniqueName, + isValidRuntimeKey, + createStackWithLambdaFunction, + invokeFunction, +} from '../../../commons/tests/utils/e2eUtils'; +import { InvocationLogs } from '../../../commons/tests/utils/InvocationLogs'; +import { deployStack, destroyStack } from '../../../commons/tests/utils/cdk-cli'; +import { ResourceAccessGranter } from '../helpers/cdkAspectGrantAccess'; +import { + RESOURCE_NAME_PREFIX, + SETUP_TIMEOUT, + TEARDOWN_TIMEOUT, + TEST_CASE_TIMEOUT +} from './constants'; +import { createDynamoDBTable } from '../helpers/parametersUtils'; + +const runtime: string = process.env.RUNTIME || 'nodejs18x'; + +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 dynamoDBClient = new DynamoDBClient({}); + +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 integTestApp = new App(); +let stack: Stack; + +/** + * This test suite deploys a CDK stack with a Lambda function and a number of DynamoDB tables. + * The function code uses the Parameters utility to retrieve values from the DynamoDB tables. + * It then logs the values to CloudWatch Logs as JSON. + * + * Once the stack is deployed, the Lambda function is invoked and the CloudWatch Logs are retrieved. + * The logs are then parsed and the values are compared to the expected values in each test case. + * + * The tables are populated with data before the Lambda function is invoked. These tables and values + * allow to test the different use cases of the DynamoDBProvider class. + * + * The tables are: + * + * - Table-Get: a table with a single partition key (id) and attribute (value) + * +-----------------+----------------------+ + * | id | value | + * +-----------------+----------------------+ + * | my-param | foo | + * | my-param-json | "{\"foo\": \"bar\"}" | + * | my-param-binary | "YmF6" | + * +-----------------+----------------------+ + * + * - Table-GetMultiple: a table with a partition key (id) and a sort key (sk) and attribute (value) + * +-------------------+---------------+----------------------+ + * | id | sk | value | + * +-------------------+---------------+----------------------+ + * | my-params | config | bar | + * | my-params | key | baz | + * | my-encoded-params | config.json | "{\"foo\": \"bar\"}" | + * | my-encoded-params | config.binary | "YmF6" | + * +-------------------+---------------+----------------------+ + * + * - Table-GetCustomKeys: a table with a single partition key (key) and attribute (val) + * +-----------------+----------------------+ + * | key | val | + * +-----------------+----------------------+ + * | my-param | foo | + * +-----------------+----------------------+ + * + * - Table-GetMultipleCustomKeys: a table with a partition key (key) and a sort key (sort) and attribute (val) + * +-------------------+---------------+----------------------+ + * | key | sort | val | + * +-------------------+---------------+----------------------+ + * | my-params | config | bar | + * | my-params | key | baz | + * +-------------------+---------------+----------------------+ + * + * The tests are: + * + * Test 1 + * Get a single parameter with default options (keyAttr: 'id', valueAttr: 'value') from table Table-Get + * + * Test 2 + * Get multiple parameters with default options (keyAttr: 'id', sortAttr: 'sk', valueAttr: 'value') from table Table-GetMultiple + * + * Test 3 + * Get a single parameter with custom options (keyAttr: 'key', valueAttr: 'val') from table Table-GetCustomKeys + * + * Test 4 + * Get multiple parameters with custom options (keyAttr: 'key', sortAttr: 'sort', valueAttr: 'val') from table Table-GetMultipleCustomKeys + * + * Test 5 + * Get a single JSON parameter with default options (keyAttr: 'id', valueAttr: 'value') and transform from table Table-Get + * + * Test 6 + * Get a single binrary parameter with default options (keyAttr: 'id', valueAttr: 'value') and transform it from table Table-Get + * + * Test 7 + * Get multiple JSON and binary parameters with default options (keyAttr: 'id', sortAttr: 'sk', valueAttr: 'value') and transform them automatically from table Table-GetMultiple + * + * Test 8 + * Get a parameter twice and check that the value is cached. This uses a custom SDK client that counts the number of calls to DynamoDB. + * + * 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 + stack = createStackWithLambdaFunction({ + app: integTestApp, + stackName: stackName, + functionName: functionName, + functionEntry: path.join(__dirname, lambdaFunctionCodeFile), + tracing: Tracing.ACTIVE, + 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: runtime, + }); + + // Create the DynamoDB tables + const ddbTableGet = createDynamoDBTable({ + stack, + id: 'Table-get', + tableName: tableGet, + partitionKey: { + name: 'id', + type: AttributeType.STRING + }, + }); + const ddbTableGetMultiple = createDynamoDBTable({ + stack, + id: 'Table-getMultiple', + tableName: tableGetMultiple, + partitionKey: { + name: 'id', + type: AttributeType.STRING + }, + sortKey: { + name: 'sk', + type: AttributeType.STRING + } + }); + const ddbTableGetCustomKeys = createDynamoDBTable({ + stack, + id: 'Table-getCustomKeys', + tableName: tableGetCustomkeys, + partitionKey: { + name: keyAttr, + type: AttributeType.STRING + }, + }); + const ddbTabelGetMultipleCustomKeys = createDynamoDBTable({ + 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(stack).add(new ResourceAccessGranter([ + ddbTableGet, + ddbTableGetMultiple, + ddbTableGetCustomKeys, + ddbTabelGetMultipleCustomKeys, + ])); + + // Deploy the stack + await deployStack(integTestApp, stack); + + // Seed tables with test data + // Test 1 + await dynamoDBClient.send(new PutItemCommand({ + TableName: tableGet, + Item: marshall({ + id: 'my-param', + value: 'foo', + }), + })); + + // Test 2 + await dynamoDBClient.send(new PutItemCommand({ + TableName: tableGetMultiple, + Item: marshall({ + id: 'my-params', + sk: 'config', + value: 'bar', + }), + })); + await dynamoDBClient.send(new PutItemCommand({ + TableName: tableGetMultiple, + Item: marshall({ + id: 'my-params', + sk: 'key', + value: 'baz', + }), + })); + + // Test 3 + await dynamoDBClient.send(new PutItemCommand({ + TableName: tableGetCustomkeys, + Item: marshall({ + [keyAttr]: 'my-param', + [valueAttr]: 'foo', + }), + })); + + // Test 4 + await dynamoDBClient.send(new PutItemCommand({ + TableName: tableGetMultipleCustomkeys, + Item: marshall({ + [keyAttr]: 'my-params', + [sortAttr]: 'config', + [valueAttr]: 'bar', + }), + })); + await dynamoDBClient.send(new PutItemCommand({ + TableName: tableGetMultipleCustomkeys, + Item: marshall({ + [keyAttr]: 'my-params', + [sortAttr]: 'key', + [valueAttr]: 'baz', + }), + })); + + // Test 5 + await dynamoDBClient.send(new PutItemCommand({ + TableName: tableGet, + Item: marshall({ + id: 'my-param-json', + value: JSON.stringify({ foo: 'bar' }), + }), + })); + + // Test 6 + await dynamoDBClient.send(new PutItemCommand({ + TableName: tableGet, + Item: marshall({ + id: 'my-param-binary', + value: 'YmF6', // base64 encoded 'baz' + }), + })); + + // Test 7 + await dynamoDBClient.send(new PutItemCommand({ + TableName: tableGetMultiple, + Item: marshall({ + id: 'my-encoded-params', + sk: 'config.json', + value: JSON.stringify({ foo: 'bar' }), + }), + })); + await dynamoDBClient.send(new PutItemCommand({ + TableName: tableGetMultiple, + Item: marshall({ + id: 'my-encoded-params', + sk: 'key.binary', + value: 'YmF6', // base64 encoded 'baz' + }), + })); + + // Test 8 & 9 use the same items as Test 1 + + // and invoke the Lambda function + invocationLogs = await invokeFunction(functionName, invocationCount, 'SEQUENTIAL'); + + }, SETUP_TIMEOUT); + + describe('DynamoDBProvider usage', () => { + + // Test 1 - get a single parameter with default options (keyAttr: 'id', valueAttr: 'value') + it('should retrieve a single parameter', async () => { + + const logs = invocationLogs[0].getFunctionLogs(); + const testLog = InvocationLogs.parseFunctionLog(logs[0]); + + expect(testLog).toStrictEqual({ + test: 'get', + value: 'foo', + }); + + }, TEST_CASE_TIMEOUT); + + // Test 2 - get multiple parameters with default options (keyAttr: 'id', sortAttr: 'sk', valueAttr: 'value') + it('should retrieve multiple parameters', async () => { + + const logs = invocationLogs[0].getFunctionLogs(); + const testLog = InvocationLogs.parseFunctionLog(logs[1]); + + expect(testLog).toStrictEqual({ + test: 'get-multiple', + value: { config: 'bar', key: 'baz' }, + }); + + }, TEST_CASE_TIMEOUT); + + // Test 3 - get a single parameter with custom options (keyAttr: 'key', valueAttr: 'val') + it('should retrieve a single parameter', async () => { + + const logs = invocationLogs[0].getFunctionLogs(); + const testLog = InvocationLogs.parseFunctionLog(logs[2]); + + expect(testLog).toStrictEqual({ + test: 'get-custom', + value: 'foo', + }); + + }, TEST_CASE_TIMEOUT); + + // Test 4 - get multiple parameters with custom options (keyAttr: 'key', sortAttr: 'sort', valueAttr: 'val') + it('should retrieve multiple parameters', async () => { + + const logs = invocationLogs[0].getFunctionLogs(); + const testLog = InvocationLogs.parseFunctionLog(logs[3]); + + expect(testLog).toStrictEqual({ + test: 'get-multiple-custom', + value: { config: 'bar', key: 'baz' }, + }); + + }, TEST_CASE_TIMEOUT); + + // 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]); + + expect(testLog).toStrictEqual({ + test: 'get-json-transform', + value: 'object', + }); + + }); + + // 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]); + + expect(testLog).toStrictEqual({ + test: 'get-binary-transform', + value: 'string', // as opposed to Uint8Array + }); + + }); + + // 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]); + + expect(testLog).toStrictEqual({ + test: 'get-multiple-auto-transform', + value: 'object,string', + }); + + }); + + // TODO: implement tests for the following cases once #1222 is merged: + // Test 8 - get a parameter twice, second time should be cached + // Test 9 - get a parameter once more but with forceFetch = true + + }); + + afterAll(async () => { + if (!process.env.DISABLE_TEARDOWN) { + await destroyStack(integTestApp, stack); + } + }, TEARDOWN_TIMEOUT); +}); diff --git a/packages/parameters/tests/helpers/cdkAspectGrantAccess.ts b/packages/parameters/tests/helpers/cdkAspectGrantAccess.ts new file mode 100644 index 0000000000..22f8c09e6f --- /dev/null +++ b/packages/parameters/tests/helpers/cdkAspectGrantAccess.ts @@ -0,0 +1,44 @@ +import { IAspect } 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 { Secret } from 'aws-cdk-lib/aws-secretsmanager'; + +/** + * 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[]; + + public constructor(tables: Table[] | Secret[]) { + this.resources = tables; + } + + 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) => { + + if (resource instanceof Table) { + resource.grantReadData(node); + } else if (resource instanceof Secret) { + resource.grantRead(node); + } + + }); + } + } +} \ No newline at end of file diff --git a/packages/parameters/tests/helpers/parametersUtils.ts b/packages/parameters/tests/helpers/parametersUtils.ts new file mode 100644 index 0000000000..3768a1fe53 --- /dev/null +++ b/packages/parameters/tests/helpers/parametersUtils.ts @@ -0,0 +1,22 @@ +import { Stack, RemovalPolicy } from 'aws-cdk-lib'; +import { Table, TableProps, BillingMode } from 'aws-cdk-lib/aws-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 { + createDynamoDBTable +}; \ No newline at end of file diff --git a/packages/parameters/tests/helpers/sdkMiddlewareRequestCounter.ts b/packages/parameters/tests/helpers/sdkMiddlewareRequestCounter.ts new file mode 100644 index 0000000000..f71a6173eb --- /dev/null +++ b/packages/parameters/tests/helpers/sdkMiddlewareRequestCounter.ts @@ -0,0 +1,30 @@ +/** + * Middleware to count the number of API calls made by the SDK. + * + * The AWS SDK for JavaScript v3 uses a middleware stack to manage the execution of + * operations. Middleware can be added to the stack to perform custom tasks before + * or after an operation is executed. + * + * This middleware is added to the stack to count the number of API calls (`ROUND_TRIP`) made by the SDK. + * This allows us to verify that the SDK is making the expected number of API calls and thus test that + * caching or forcing a retrieval are working as expected. + * + * @see {@link https://aws.amazon.com/blogs/developer/middleware-stack-modular-aws-sdk-js/|AWS Blog - Middleware Stack} + */ +export const middleware = { + // + counter : 0, + applyToStack: (stack) => { + // Middleware added to mark start and end of an complete API call. + stack.add( + (next, _context) => async (args) => { + // Increment counter + middleware.counter++; + + // Call next middleware + return await next(args); + }, + { tags: ['ROUND_TRIP'] } + ); + }, +}; \ No newline at end of file diff --git a/packages/parameters/tests/helpers/tinyLogger.ts b/packages/parameters/tests/helpers/tinyLogger.ts new file mode 100644 index 0000000000..0effa723ae --- /dev/null +++ b/packages/parameters/tests/helpers/tinyLogger.ts @@ -0,0 +1,21 @@ +import { Console } from 'console'; + +/** + * A tiny logger that logs to stdout and stderr. + * + * This is used to log the results of the function code during the integration tests. + * We use this instead of the global console object because we want to log pure JSON objects. + * In Node.js runtimes, AWS Lambda usually patches the global console object to inject some + * metadata like the request ID. This is not desirable in our case because we want to log pure + * JSON objects to stdout and stderr. + * + * This allows us to get the logs when invoking the function and parse them to verify that + * the function code is working as expected. + */ +export class TinyLogger { + private console = new Console({ stdout: process.stdout, stderr: process.stderr }); + + public log(message: unknown): void { + this.console.log(JSON.stringify(message)); + } +} \ No newline at end of file