From 20ff5d9e47a76226d60b5fb32aeeccd38b395213 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Mon, 13 Feb 2023 18:59:01 +0100 Subject: [PATCH 1/6] tests: appconfigprovider e2e tests --- ...pConfigProvider.class.test.functionCode.ts | 117 +++++++ .../tests/e2e/appConfigProvider.class.test.ts | 331 ++++++++++++++++++ .../tests/helpers/cdkAspectGrantAccess.ts | 27 +- .../tests/helpers/parametersUtils.ts | 114 +++++- .../helpers/sdkMiddlewareRequestCounter.ts | 13 +- 5 files changed, 594 insertions(+), 8 deletions(-) create mode 100644 packages/parameters/tests/e2e/appConfigProvider.class.test.functionCode.ts create mode 100644 packages/parameters/tests/e2e/appConfigProvider.class.test.ts diff --git a/packages/parameters/tests/e2e/appConfigProvider.class.test.functionCode.ts b/packages/parameters/tests/e2e/appConfigProvider.class.test.functionCode.ts new file mode 100644 index 0000000000..1791e8c54e --- /dev/null +++ b/packages/parameters/tests/e2e/appConfigProvider.class.test.functionCode.ts @@ -0,0 +1,117 @@ +import { Context } from 'aws-lambda'; +import { + AppConfigProvider, +} from '../../src/appconfig'; +import { + AppConfigGetOptionsInterface, +} from '../../src/types'; +import { TinyLogger } from '../helpers/tinyLogger'; +import { middleware } from '../helpers/sdkMiddlewareRequestCounter'; +import { AppConfigDataClient } from '@aws-sdk/client-appconfigdata'; + +// We use a custom logger to log pure JSON objects to stdout +const logger = new TinyLogger(); + +const application = process.env.APPLICATION_NAME || 'my-app'; +const environment = process.env.ENVIRONMENT_NAME || 'my-env'; +const freeFormJsonName = process.env.FREEFORM_JSON_NAME || 'freeform-json'; +const freeFormYamlName = process.env.FREEFORM_YAML_NAME || 'freeform-yaml'; +const freeFormPlainTextName = process.env.FREEFORM_PLAIN_TEXT_NAME || 'freeform-plain-text'; +const featureFlagJsonName = process.env.FEATURE_FLAG_JSON_NAME || 'feature-flag-json'; + +const defaultProvider = new AppConfigProvider({ + application, + environment, +}); +// Provider test +const customClient = new AppConfigDataClient({}); +customClient.middlewareStack.use(middleware); +const providerWithMiddleware = new AppConfigProvider({ + awsSdkV3Client: customClient, + application, + environment, +}); + +// Use provider specified, or default to main one & return it with cache cleared +const resolveProvider = (provider?: AppConfigProvider): AppConfigProvider => { + const resolvedProvider = provider ? provider : defaultProvider; + resolvedProvider.clearCache(); + + return resolvedProvider; +}; + +// Helper function to call get() and log the result +const _call_get = async ( + paramName: string, + testName: string, + options?: AppConfigGetOptionsInterface, + provider?: AppConfigProvider, +): Promise => { + try { + const currentProvider = resolveProvider(provider); + + const parameterValue = await currentProvider.get(paramName, options); + logger.log({ + test: testName, + value: parameterValue + }); + } catch (err) { + logger.log({ + test: testName, + error: err.message + }); + } +}; + +export const handler = async (_event: unknown, _context: Context): Promise => { + // Test 1 - get a single parameter as-is (no transformation) + await _call_get(freeFormPlainTextName, 'get'); + + // Test 2 - get a free-form JSON and apply binary transformation (should return a stringified JSON) + await _call_get(freeFormJsonName, 'get-freeform-json-binary', { transform: 'binary' }); + + // Test 3 - get a free-form YAML and apply binary transformation (should return a string-encoded YAML) + await _call_get(freeFormYamlName, 'get-freeform-yaml-binary', { transform: 'binary' }); + + // Test 4 - get a free-form plain text and apply binary transformation (should return a string) + await _call_get(freeFormPlainTextName, 'get-freeform-plain-text-binary', { transform: 'binary' }); + + // Test 5 - get a feature flag JSON and apply binary transformation (should return a stringified JSON) + await _call_get(featureFlagJsonName, 'get-feature-flag-json-binary', { transform: 'binary' }); + + // Test 6 + // get parameter twice with middleware, which counts the number of requests, we check later if we only called AppConfig API once + try { + providerWithMiddleware.clearCache(); + middleware.counter = 0; + await providerWithMiddleware.get(freeFormPlainTextName); + await providerWithMiddleware.get(freeFormPlainTextName); + logger.log({ + test: 'get-cached', + value: middleware.counter // should be 1 + }); + } catch (err) { + logger.log({ + test: 'get-cached', + error: err.message + }); + } + + // Test 7 + // get parameter twice, but force fetch 2nd time, we count number of SDK requests and check that we made two API calls + try { + providerWithMiddleware.clearCache(); + middleware.counter = 0; + await providerWithMiddleware.get(freeFormPlainTextName); + await providerWithMiddleware.get(freeFormPlainTextName, { forceFetch: true }); + logger.log({ + test: 'get-forced', + value: middleware.counter // should be 2 + }); + } catch (err) { + logger.log({ + test: 'get-forced', + error: err.message + }); + } +}; \ No newline at end of file diff --git a/packages/parameters/tests/e2e/appConfigProvider.class.test.ts b/packages/parameters/tests/e2e/appConfigProvider.class.test.ts new file mode 100644 index 0000000000..39017d5b2f --- /dev/null +++ b/packages/parameters/tests/e2e/appConfigProvider.class.test.ts @@ -0,0 +1,331 @@ +/** + * Test AppConfigProvider class + * + * @group e2e/parameters/appconfig/class + */ +import path from 'path'; +import { App, Stack, 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 { 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 { + createBaseAppConfigResources, + createAppConfigConfigurationProfile, +} 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, '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 freeFormPlainTextName = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'freeFormPlainText'); +const featureFlagJsonName = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'featureFlagJson'); + +const freeFormJsonValue = { + foo: 'bar', +}; +const freeFormYamlValue = `foo: bar +`; +const freeFormPlainTextValue = 'foo'; +const featureFlagJsonValue = { + version: '1', + flags: { + myFeatureFlag: { + 'name': 'myFeatureFlag', + } + }, + values: { + myFeatureFlag: { + enabled: true, + } + } +}; + +const integTestApp = new App(); +let stack: Stack; + +/** + * This test suite deploys a CDK stack with a Lambda function and a number of AppConfig parameters. + * The function code uses the Parameters utility to retrieve the parameters. + * It then logs the values to CloudWatch Logs as JSON objects. + * + * 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 checked against the expected values for each test case. + * + * The stack creates an AppConfig application and environment, and then creates a number configuration + * profiles, each with a different type of parameter. + * + * The parameters created are: + * - Free-form JSON + * - Free-form YAML + * - Free-form plain text + * - Feature flag JSON + * + * These parameters allow to retrieve the values and test some transformations. + * + * The tests are: + * + * Test 1 + * get a single parameter as-is (no transformation) + * + * Test 2 + * get a free-form JSON and apply binary transformation (should return a stringified JSON) + * + * Test 3 + * get a free-form YAML and apply binary transformation (should return a string-encoded YAML) + * + * Test 4 + * get a free-form plain text and apply binary transformation (should return a string) + * + * Test 5 + * get a feature flag JSON and apply binary transformation (should return a stringified JSON) + * + * Test 6 + * get parameter twice with middleware, which counts the number of requests, + * we check later if we only called AppConfig API once + * + * Test 7 + * 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 (appConfigProvider) for runtime ${runtime}`, () => { + + let invocationLogs: InvocationLogs[]; + const encoder = new TextEncoder(); + + beforeAll(async () => { + // Create a stack with a Lambda function + stack = createStackWithLambdaFunction({ + app: integTestApp, + stackName, + functionName, + functionEntry: path.join(__dirname, lambdaFunctionCodeFile), + environment: { + UUID: uuid, + + // 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_PLAIN_TEXT_NAME: freeFormPlainTextName, + FEATURE_FLAG_JSON_NAME: featureFlagJsonName, + }, + runtime, + }); + + // Create the base resources for an AppConfig application. + const { + application, + environment, + deploymentStrategy + } = createBaseAppConfigResources({ + stack, + applicationName, + environmentName, + deploymentStrategyName, + }); + + // Create configuration profiles for tests. + const freeFormJson = createAppConfigConfigurationProfile({ + stack, + application, + environment, + deploymentStrategy, + name: freeFormJsonName, + type: 'AWS.Freeform', + content: { + content: JSON.stringify(freeFormJsonValue), + contentType: 'application/json', + } + }); + + const freeFormYaml = createAppConfigConfigurationProfile({ + stack, + application, + environment, + deploymentStrategy, + name: freeFormYamlName, + type: 'AWS.Freeform', + content: { + content: freeFormYamlValue, + contentType: 'application/x-yaml', + } + }); + + const freeFormPlainText = createAppConfigConfigurationProfile({ + stack, + application, + environment, + deploymentStrategy, + name: freeFormPlainTextName, + type: 'AWS.Freeform', + content: { + content: freeFormPlainTextValue, + contentType: 'text/plain', + } + }); + + const featureFlagJson = createAppConfigConfigurationProfile({ + stack, + application, + environment, + deploymentStrategy, + name: featureFlagJsonName, + type: 'AWS.AppConfig.FeatureFlags', + content: { + content: JSON.stringify(featureFlagJsonValue), + contentType: 'application/json', + } + }); + + // Grant access to the Lambda function to the AppConfig resources. + Aspects.of(stack).add(new ResourceAccessGranter([ + freeFormJson, + freeFormYaml, + freeFormPlainText, + featureFlagJson, + ])); + + // Deploy the stack + await deployStack(integTestApp, stack); + + // and invoke the Lambda function + invocationLogs = await invokeFunction(functionName, invocationCount, 'SEQUENTIAL'); + + }, SETUP_TIMEOUT); + + describe('AppConfigProvider usage', () => { + + // Test 1 - get a single parameter as-is (no transformation) + it('should retrieve single parameter', () => { + + const logs = invocationLogs[0].getFunctionLogs(); + const testLog = InvocationLogs.parseFunctionLog(logs[0]); + + expect(testLog).toStrictEqual({ + test: 'get', + value: encoder.encode(freeFormPlainTextName), + }); + + }); + + // Test 2 - get a free-form JSON and apply binary transformation + // (should return a stringified JSON) + it('should retrieve single free-form JSON parameter with binary transformation', () => { + + const logs = invocationLogs[0].getFunctionLogs(); + const testLog = InvocationLogs.parseFunctionLog(logs[1]); + + expect(testLog).toStrictEqual({ + test: 'get-freeform-json-binary', + value: JSON.stringify(freeFormJsonValue), + }); + + }); + + // Test 3 - get a free-form YAML and apply binary transformation + // (should return a string-encoded YAML) + it('should retrieve single free-form YAML parameter with binary transformation', () => { + + const logs = invocationLogs[0].getFunctionLogs(); + const testLog = InvocationLogs.parseFunctionLog(logs[2]); + + expect(testLog).toStrictEqual({ + test: 'get-freeform-yaml-binary', + value: freeFormYamlValue, + }); + + }); + + // Test 4 - get a free-form plain text and apply binary transformation + // (should return a string) + it('should retrieve single free-form plain text parameter with binary transformation', () => { + + const logs = invocationLogs[0].getFunctionLogs(); + const testLog = InvocationLogs.parseFunctionLog(logs[3]); + + expect(testLog).toStrictEqual({ + test: 'get-freeform-plain-text-binary', + value: freeFormPlainTextValue, + }); + + }); + + // Test 5 - get a feature flag JSON and apply binary transformation + // (should return a stringified JSON) + it('should retrieve single feature flag parameter with binary transformation', () => { + + const logs = invocationLogs[0].getFunctionLogs(); + const testLog = InvocationLogs.parseFunctionLog(logs[4]); + + expect(testLog).toStrictEqual({ + test: 'get-feature-flag-json-binary', + value: JSON.stringify(featureFlagJsonValue.values), + }); + + }); + + // Test 6 - get parameter twice with middleware, which counts the number + // of requests, we check later if we only called AppConfig API once + it('should retrieve single parameter cached', () => { + + const logs = invocationLogs[0].getFunctionLogs(); + const testLog = InvocationLogs.parseFunctionLog(logs[5]); + + expect(testLog).toStrictEqual({ + test: 'get-cached', + value: 1 + }); + + }, TEST_CASE_TIMEOUT); + + // Test 7 - get parameter twice, but force fetch 2nd time, + // we count number of SDK requests and check that we made two API calls + it('should retrieve single parameter twice without caching', async () => { + + const logs = invocationLogs[0].getFunctionLogs(); + const testLog = InvocationLogs.parseFunctionLog(logs[6]); + + expect(testLog).toStrictEqual({ + test: 'get-forced', + value: 2 + }); + + }, TEST_CASE_TIMEOUT); + + }); + + afterAll(async () => { + if (!process.env.DISABLE_TEARDOWN) { + await destroyStack(integTestApp, stack); + } + }, TEARDOWN_TIMEOUT); + +}); \ No newline at end of file diff --git a/packages/parameters/tests/helpers/cdkAspectGrantAccess.ts b/packages/parameters/tests/helpers/cdkAspectGrantAccess.ts index 57d99a393b..4248258d79 100644 --- a/packages/parameters/tests/helpers/cdkAspectGrantAccess.ts +++ b/packages/parameters/tests/helpers/cdkAspectGrantAccess.ts @@ -1,8 +1,10 @@ -import { IAspect } from 'aws-cdk-lib'; +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 } from 'aws-cdk-lib/aws-iam'; import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; +import { CfnDeployment } from 'aws-cdk-lib/aws-appconfig'; /** * An aspect that grants access to resources to a Lambda function. @@ -19,9 +21,9 @@ import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; * @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[]; + private readonly resources: Table[] | Secret[] | CfnDeployment[]; - public constructor(resources: Table[] | Secret[]) { + public constructor(resources: Table[] | Secret[] | CfnDeployment[]) { this.resources = resources; } @@ -30,12 +32,29 @@ export class ResourceAccessGranter implements IAspect { if (node instanceof NodejsFunction) { // Grant access to the resources - this.resources.forEach((resource: Table | Secret) => { + this.resources.forEach((resource: Table | Secret | CfnDeployment) => { if (resource instanceof Table) { resource.grantReadData(node); } else if (resource instanceof Secret) { resource.grantRead(node); + } 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({ + actions: [ + 'appconfig:StartConfigurationSession', + 'appconfig:GetLatestConfiguration', + ], + resources: [ + appConfigConfigurationArn, + ], + }), + ); } }); diff --git a/packages/parameters/tests/helpers/parametersUtils.ts b/packages/parameters/tests/helpers/parametersUtils.ts index 3768a1fe53..f121cb8e40 100644 --- a/packages/parameters/tests/helpers/parametersUtils.ts +++ b/packages/parameters/tests/helpers/parametersUtils.ts @@ -1,5 +1,13 @@ import { Stack, RemovalPolicy } from 'aws-cdk-lib'; import { Table, TableProps, BillingMode } from 'aws-cdk-lib/aws-dynamodb'; +import { + CfnApplication, + CfnConfigurationProfile, + CfnDeployment, + CfnDeploymentStrategy, + CfnEnvironment, + CfnHostedConfigurationVersion, +} from 'aws-cdk-lib/aws-appconfig'; export type CreateDynamoDBTableOptions = { stack: Stack @@ -17,6 +25,110 @@ const createDynamoDBTable = (options: CreateDynamoDBTableOptions): Table => { 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 { - createDynamoDBTable + createDynamoDBTable, + createBaseAppConfigResources, + createAppConfigConfigurationProfile, }; \ No newline at end of file diff --git a/packages/parameters/tests/helpers/sdkMiddlewareRequestCounter.ts b/packages/parameters/tests/helpers/sdkMiddlewareRequestCounter.ts index f71a6173eb..0fbff71084 100644 --- a/packages/parameters/tests/helpers/sdkMiddlewareRequestCounter.ts +++ b/packages/parameters/tests/helpers/sdkMiddlewareRequestCounter.ts @@ -17,9 +17,16 @@ export const middleware = { applyToStack: (stack) => { // Middleware added to mark start and end of an complete API call. stack.add( - (next, _context) => async (args) => { - // Increment counter - middleware.counter++; + (next, context) => async (args) => { + // We only want to count API calls to retrieve data, + // not the StartConfigurationSessionCommand + if ( + context.clientName !== 'AppConfigDataClient' || + context.commandName !== 'StartConfigurationSessionCommand' + ) { + // Increment counter + middleware.counter++; + } // Call next middleware return await next(args); From 320e23f51d357cf120fb988b75a791bda0b67c24 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 16 Feb 2023 11:04:19 +0100 Subject: [PATCH 2/6] tests: completed e2e tests --- ...pConfigProvider.class.test.functionCode.ts | 21 ++++--- .../tests/e2e/appConfigProvider.class.test.ts | 62 +++++++++++++------ 2 files changed, 54 insertions(+), 29 deletions(-) diff --git a/packages/parameters/tests/e2e/appConfigProvider.class.test.functionCode.ts b/packages/parameters/tests/e2e/appConfigProvider.class.test.functionCode.ts index 1791e8c54e..589fd475ed 100644 --- a/packages/parameters/tests/e2e/appConfigProvider.class.test.functionCode.ts +++ b/packages/parameters/tests/e2e/appConfigProvider.class.test.functionCode.ts @@ -16,8 +16,9 @@ const application = process.env.APPLICATION_NAME || 'my-app'; const environment = process.env.ENVIRONMENT_NAME || 'my-env'; const freeFormJsonName = process.env.FREEFORM_JSON_NAME || 'freeform-json'; const freeFormYamlName = process.env.FREEFORM_YAML_NAME || 'freeform-yaml'; -const freeFormPlainTextName = process.env.FREEFORM_PLAIN_TEXT_NAME || 'freeform-plain-text'; -const featureFlagJsonName = process.env.FEATURE_FLAG_JSON_NAME || 'feature-flag-json'; +const freeFormPlainTextNameA = process.env.FREEFORM_PLAIN_TEXT_NAME_A || 'freeform-plain-text'; +const freeFormPlainTextNameB = process.env.FREEFORM_PLAIN_TEXT_NAME_B || 'freeform-plain-text'; +const featureFlagName = process.env.FEATURE_FLAG_NAME || 'feature-flag'; const defaultProvider = new AppConfigProvider({ application, @@ -65,7 +66,7 @@ const _call_get = async ( export const handler = async (_event: unknown, _context: Context): Promise => { // Test 1 - get a single parameter as-is (no transformation) - await _call_get(freeFormPlainTextName, 'get'); + await _call_get(freeFormPlainTextNameA, 'get'); // Test 2 - get a free-form JSON and apply binary transformation (should return a stringified JSON) await _call_get(freeFormJsonName, 'get-freeform-json-binary', { transform: 'binary' }); @@ -74,18 +75,18 @@ export const handler = async (_event: unknown, _context: Context): Promise await _call_get(freeFormYamlName, 'get-freeform-yaml-binary', { transform: 'binary' }); // Test 4 - get a free-form plain text and apply binary transformation (should return a string) - await _call_get(freeFormPlainTextName, 'get-freeform-plain-text-binary', { transform: 'binary' }); + await _call_get(freeFormPlainTextNameB, 'get-freeform-plain-text-binary', { transform: 'binary' }); - // Test 5 - get a feature flag JSON and apply binary transformation (should return a stringified JSON) - await _call_get(featureFlagJsonName, 'get-feature-flag-json-binary', { transform: 'binary' }); + // Test 5 - get a feature flag and apply binary transformation (should return a stringified JSON) + await _call_get(featureFlagName, 'get-feature-flag-binary', { transform: 'binary' }); // Test 6 // get parameter twice with middleware, which counts the number of requests, we check later if we only called AppConfig API once try { providerWithMiddleware.clearCache(); middleware.counter = 0; - await providerWithMiddleware.get(freeFormPlainTextName); - await providerWithMiddleware.get(freeFormPlainTextName); + await providerWithMiddleware.get(freeFormPlainTextNameA); + await providerWithMiddleware.get(freeFormPlainTextNameA); logger.log({ test: 'get-cached', value: middleware.counter // should be 1 @@ -102,8 +103,8 @@ export const handler = async (_event: unknown, _context: Context): Promise try { providerWithMiddleware.clearCache(); middleware.counter = 0; - await providerWithMiddleware.get(freeFormPlainTextName); - await providerWithMiddleware.get(freeFormPlainTextName, { forceFetch: true }); + await providerWithMiddleware.get(freeFormPlainTextNameA); + await providerWithMiddleware.get(freeFormPlainTextNameA, { forceFetch: true }); logger.log({ test: 'get-forced', value: middleware.counter // should be 2 diff --git a/packages/parameters/tests/e2e/appConfigProvider.class.test.ts b/packages/parameters/tests/e2e/appConfigProvider.class.test.ts index 39017d5b2f..5e000fa755 100644 --- a/packages/parameters/tests/e2e/appConfigProvider.class.test.ts +++ b/packages/parameters/tests/e2e/appConfigProvider.class.test.ts @@ -44,8 +44,9 @@ const environmentName = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 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 freeFormPlainTextName = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'freeFormPlainText'); -const featureFlagJsonName = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'featureFlagJson'); +const freeFormPlainTextNameA = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'freeFormPlainTextA'); +const freeFormPlainTextNameB = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'freeFormPlainTextB'); +const featureFlagName = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'featureFlag'); const freeFormJsonValue = { foo: 'bar', @@ -53,7 +54,7 @@ const freeFormJsonValue = { const freeFormYamlValue = `foo: bar `; const freeFormPlainTextValue = 'foo'; -const featureFlagJsonValue = { +const featureFlagValue = { version: '1', flags: { myFeatureFlag: { @@ -84,8 +85,8 @@ let stack: Stack; * The parameters created are: * - Free-form JSON * - Free-form YAML - * - Free-form plain text - * - Feature flag JSON + * - 2x Free-form plain text + * - Feature flag * * These parameters allow to retrieve the values and test some transformations. * @@ -104,7 +105,7 @@ let stack: Stack; * get a free-form plain text and apply binary transformation (should return a string) * * Test 5 - * get a feature flag JSON and apply binary transformation (should return a stringified JSON) + * get a feature flag and apply binary transformation (should return a stringified JSON) * * Test 6 * get parameter twice with middleware, which counts the number of requests, @@ -134,8 +135,9 @@ describe(`parameters E2E tests (appConfigProvider) for runtime ${runtime}`, () = ENVIRONMENT_NAME: environmentName, FREEFORM_JSON_NAME: freeFormJsonName, FREEFORM_YAML_NAME: freeFormYamlName, - FREEFORM_PLAIN_TEXT_NAME: freeFormPlainTextName, - FEATURE_FLAG_JSON_NAME: featureFlagJsonName, + FREEFORM_PLAIN_TEXT_NAME_A: freeFormPlainTextNameA, + FREEFORM_PLAIN_TEXT_NAME_B: freeFormPlainTextNameB, + FEATURE_FLAG_NAME: featureFlagName, }, runtime, }); @@ -178,39 +180,57 @@ describe(`parameters E2E tests (appConfigProvider) for runtime ${runtime}`, () = contentType: 'application/x-yaml', } }); + freeFormYaml.node.addDependency(freeFormJson); - const freeFormPlainText = createAppConfigConfigurationProfile({ + const freeFormPlainTextA = createAppConfigConfigurationProfile({ stack, application, environment, deploymentStrategy, - name: freeFormPlainTextName, + name: freeFormPlainTextNameA, type: 'AWS.Freeform', content: { content: freeFormPlainTextValue, contentType: 'text/plain', } }); + freeFormPlainTextA.node.addDependency(freeFormYaml); + + const freeFormPlainTextB = createAppConfigConfigurationProfile({ + stack, + application, + environment, + deploymentStrategy, + name: freeFormPlainTextNameB, + type: 'AWS.Freeform', + content: { + content: freeFormPlainTextValue, + contentType: 'text/plain', + } + }); + freeFormPlainTextB.node.addDependency(freeFormPlainTextA); - const featureFlagJson = createAppConfigConfigurationProfile({ + const featureFlag = createAppConfigConfigurationProfile({ stack, application, environment, deploymentStrategy, - name: featureFlagJsonName, + name: featureFlagName, type: 'AWS.AppConfig.FeatureFlags', content: { - content: JSON.stringify(featureFlagJsonValue), + content: JSON.stringify(featureFlagValue), contentType: 'application/json', } }); + featureFlag.node.addDependency(freeFormPlainTextB); // Grant access to the Lambda function to the AppConfig resources. Aspects.of(stack).add(new ResourceAccessGranter([ freeFormJson, freeFormYaml, - freeFormPlainText, - featureFlagJson, + freeFormPlainTextA, + freeFormPlainTextB, + featureFlag, ])); // Deploy the stack @@ -231,7 +251,11 @@ describe(`parameters E2E tests (appConfigProvider) for runtime ${runtime}`, () = expect(testLog).toStrictEqual({ test: 'get', - value: encoder.encode(freeFormPlainTextName), + value: JSON.parse( + JSON.stringify( + encoder.encode(freeFormPlainTextValue) + ) + ), }); }); @@ -278,7 +302,7 @@ describe(`parameters E2E tests (appConfigProvider) for runtime ${runtime}`, () = }); - // Test 5 - get a feature flag JSON and apply binary transformation + // Test 5 - get a feature flag and apply binary transformation // (should return a stringified JSON) it('should retrieve single feature flag parameter with binary transformation', () => { @@ -286,8 +310,8 @@ describe(`parameters E2E tests (appConfigProvider) for runtime ${runtime}`, () = const testLog = InvocationLogs.parseFunctionLog(logs[4]); expect(testLog).toStrictEqual({ - test: 'get-feature-flag-json-binary', - value: JSON.stringify(featureFlagJsonValue.values), + test: 'get-feature-flag-binary', + value: JSON.stringify(featureFlagValue.values), }); }); From 0bacc9a62164db71bdcfd27aed5b0eadb63862af Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 16 Feb 2023 17:23:05 +0100 Subject: [PATCH 3/6] chore: added effect in policy --- packages/parameters/tests/helpers/cdkAspectGrantAccess.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/parameters/tests/helpers/cdkAspectGrantAccess.ts b/packages/parameters/tests/helpers/cdkAspectGrantAccess.ts index 4248258d79..57d6330884 100644 --- a/packages/parameters/tests/helpers/cdkAspectGrantAccess.ts +++ b/packages/parameters/tests/helpers/cdkAspectGrantAccess.ts @@ -2,7 +2,7 @@ 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 } from 'aws-cdk-lib/aws-iam'; +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'; @@ -46,6 +46,7 @@ export class ResourceAccessGranter implements IAspect { node.addToRolePolicy( new PolicyStatement({ + effect: Effect.ALLOW, actions: [ 'appconfig:StartConfigurationSession', 'appconfig:GetLatestConfiguration', From 425390df1a359edb28b320aec49e73c82945d86c Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Fri, 17 Feb 2023 17:23:21 +0100 Subject: [PATCH 4/6] chore: add missing semicolon --- packages/parameters/tests/helpers/parametersUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/parameters/tests/helpers/parametersUtils.ts b/packages/parameters/tests/helpers/parametersUtils.ts index f066514e82..3bd785bbed 100644 --- a/packages/parameters/tests/helpers/parametersUtils.ts +++ b/packages/parameters/tests/helpers/parametersUtils.ts @@ -131,7 +131,7 @@ const createAppConfigConfigurationProfile = (options: CreateAppConfigConfigurati deploymentStrategyId: deploymentStrategy.ref, environmentId: environment.ref, }); -} +}; export type CreateSecureStringProviderOptions = { stack: Stack @@ -207,4 +207,4 @@ export { createAppConfigConfigurationProfile, createSSMSecureString, createSecureStringProvider, -}; \ No newline at end of file +}; From 9052c5fb87c6cf75080217940155ed2b0220eb1a Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Fri, 17 Feb 2023 17:23:43 +0100 Subject: [PATCH 5/6] chore: fix indentation --- packages/parameters/tests/helpers/cdkAspectGrantAccess.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/parameters/tests/helpers/cdkAspectGrantAccess.ts b/packages/parameters/tests/helpers/cdkAspectGrantAccess.ts index e431c5734a..68f755a3fc 100644 --- a/packages/parameters/tests/helpers/cdkAspectGrantAccess.ts +++ b/packages/parameters/tests/helpers/cdkAspectGrantAccess.ts @@ -56,7 +56,7 @@ export class ResourceAccessGranter implements IAspect { ], resources: [ resource.parameterArn.split(':').slice(0, -1).join(':'), - ], + ], }), ); } else if (resource instanceof CfnDeployment) { @@ -82,4 +82,4 @@ export class ResourceAccessGranter implements IAspect { }); } } -} \ No newline at end of file +} From 78712a6de8877a57a45de7d1d2db6ddaed184331 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 21 Feb 2023 10:27:09 +0100 Subject: [PATCH 6/6] chore: documented explicit dependency --- .../parameters/tests/e2e/appConfigProvider.class.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/parameters/tests/e2e/appConfigProvider.class.test.ts b/packages/parameters/tests/e2e/appConfigProvider.class.test.ts index 5e000fa755..b32504444a 100644 --- a/packages/parameters/tests/e2e/appConfigProvider.class.test.ts +++ b/packages/parameters/tests/e2e/appConfigProvider.class.test.ts @@ -114,6 +114,11 @@ let stack: Stack; * Test 7 * get parameter twice, but force fetch 2nd time, we count number of SDK requests and * check that we made two API calls + * + * Note: To avoid race conditions, we add a dependency between each pair of configuration profiles. + * This allows us to influence the order of creation and ensure that each configuration profile + * 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}`, () => {