Skip to content

tests(parameters): integration tests for SecretsProvider #1263

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Feb 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { Context } from 'aws-lambda';
import { SecretsProvider } from '../../lib/secrets';
import { TinyLogger } from '../helpers/tinyLogger';
import { SecretsGetOptionsInterface } from '../../lib/types';
import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager';
import { middleware } from '../helpers/sdkMiddlewareRequestCounter';

const logger = new TinyLogger();
const defaultProvider = new SecretsProvider();

const secretNamePlain = process.env.SECRET_NAME_PLAIN || '';
const secretNameObject = process.env.SECRET_NAME_OBJECT || '';
const secretNameBinary = process.env.SECRET_NAME_BINARY || '';
const secretNameObjectWithSuffix = process.env.SECRET_NAME_OBJECT_WITH_SUFFIX || '';
const secretNameBinaryWithSuffix = process.env.SECRET_NAME_BINARY_WITH_SUFFIX || '';
const secretNamePlainChached = process.env.SECRET_NAME_PLAIN_CACHED || '';

// Provider test 8, 9
const customClient = new SecretsManagerClient({});
customClient.middlewareStack.use(middleware);
const providerWithMiddleware = new SecretsProvider({
awsSdkV3Client: customClient
});

const _call_get = async (paramName: string, testName: string, options?: SecretsGetOptionsInterface, provider?: SecretsProvider,): Promise<void> => {
try {
// we might get a provider with specific sdk options, otherwise fallback to default
const currentProvider = provider ? provider : defaultProvider;

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<void> => {

// Test 1 get single param as plaintext
await _call_get(secretNamePlain, 'get-plain');

// Test 2 get single param with transform json
await _call_get(secretNameObject, 'get-transform-json', { transform: 'json' });

// Test 3 get single param with transform binary
await _call_get(secretNameBinary, 'get-transform-binary', { transform: 'binary' });

// Test 4 get single param with transform auto json
await _call_get(secretNameObjectWithSuffix, 'get-transform-auto-json', { transform: 'auto' });

// Test 5 get single param with transform auto binary
await _call_get(secretNameBinaryWithSuffix, 'get-transform-auto-binary', { transform: 'auto' });

// Test 6
// get parameter twice with middleware, which counts number of SDK requests, we check later if we only called SecretManager API once
try {
middleware.counter = 0;
await providerWithMiddleware.get(secretNamePlainChached);
await providerWithMiddleware.get(secretNamePlainChached);
logger.log({
test: 'get-plain-cached',
value: middleware.counter // should be 1
});
} catch (err) {
logger.log({
test: secretNamePlainChached,
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 {
middleware.counter = 0;
await providerWithMiddleware.get(secretNamePlainChached);
await providerWithMiddleware.get(secretNamePlainChached, { forceFetch: true });
logger.log({
test: 'get-plain-force',
value: middleware.counter // should be 2
});
} catch (err) {
logger.log({
test: secretNamePlainChached,
error: err.message
});
}

};
188 changes: 188 additions & 0 deletions packages/parameters/tests/e2e/secretsProvider.class.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/**
* Test SecretsPorovider class
*
* @group e2e/parameters/secrets/class
*/
import {
createStackWithLambdaFunction,
generateUniqueName,
invokeFunction,
isValidRuntimeKey
} from '../../../commons/tests/utils/e2eUtils';
import { RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, TEARDOWN_TIMEOUT, TEST_CASE_TIMEOUT } from './constants';
import { v4 } from 'uuid';
import { Tracing } from 'aws-cdk-lib/aws-lambda';
import { deployStack, destroyStack } from '../../../commons/tests/utils/cdk-cli';
import { App, Aspects, SecretValue, Stack } 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 || 'nodejs18x';

if (!isValidRuntimeKey(runtime)) {
throw new Error(`Invalid runtime key: ${runtime}`);
}

const uuid = v4();
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';

const secretNamePlain = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'testSecretPlain');
const secretNamePlainCached = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'testSecretPlainCached');
const secretNameObject = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'testSecretObject');
const secretNameBinary = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'testSecretBinary');
const secretNameObjectWithSuffix = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'testSecretObject.json');
const secretNameBinaryWithSuffix = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'testSecretObject.binary');

const invocationCount = 1;

const integTestApp = new App();
let stack: Stack;

describe(`parameters E2E tests (SecretsProvider) for runtime: ${runtime}`, () => {

let invocationLogs: InvocationLogs[];

beforeAll(async () => {
stack = createStackWithLambdaFunction({
app: integTestApp,
stackName: stackName,
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_OBJECT_WITH_SUFFIX: secretNameObjectWithSuffix,
SECRET_NAME_BINARY_WITH_SUFFIX: secretNameBinaryWithSuffix,
SECRET_NAME_PLAIN_CACHED: secretNamePlainCached,
},
runtime: runtime
});

const secretString = new Secret(stack, 'testSecretPlain', {
secretName: secretNamePlain,
secretStringValue: SecretValue.unsafePlainText('foo')
});

const secretObject = new Secret(stack, 'testSecretObject', {
secretName: secretNameObject,
secretObjectValue: {
foo: SecretValue.unsafePlainText('bar'),
}
});

const secretBinary = new Secret(stack, 'testSecretBinary', {
secretName: secretNameBinary,
secretStringValue: SecretValue.unsafePlainText('Zm9v') // 'foo' encoded in base64
});

const secretObjectWithSuffix = new Secret(stack, 'testSecretObjectWithSuffix', {
secretName: secretNameObjectWithSuffix,
secretObjectValue: {
foo: SecretValue.unsafePlainText('bar')
}
});

const secretBinaryWithSuffix = new Secret(stack, 'testSecretBinaryWithSuffix', {
secretName: secretNameBinaryWithSuffix,
secretStringValue: SecretValue.unsafePlainText('Zm9v') // 'foo' encoded in base64
});

const secretStringCached = new Secret(stack, 'testSecretStringCached', {
secretName: secretNamePlainCached,
secretStringValue: SecretValue.unsafePlainText('foo')
});

Aspects.of(stack).add(new ResourceAccessGranter([ secretString, secretObject, secretBinary, secretObjectWithSuffix, secretBinaryWithSuffix, secretStringCached ]));

await deployStack(integTestApp, stack);

invocationLogs = await invokeFunction(functionName, invocationCount, 'SEQUENTIAL');

}, SETUP_TIMEOUT);

describe('SecretsProvider usage', () => {
it('should retrieve a single parameter', async () => {

const logs = invocationLogs[0].getFunctionLogs();
const testLog = InvocationLogs.parseFunctionLog(logs[0]);

expect(testLog).toStrictEqual({
test: 'get-plain',
value: 'foo'
});
}, TEST_CASE_TIMEOUT);

it('should retrieve a single parameter with transform json', async () => {
const logs = invocationLogs[0].getFunctionLogs();
const testLog = InvocationLogs.parseFunctionLog(logs[1]);

expect(testLog).toStrictEqual({
test: 'get-transform-json',
value: { foo: 'bar' }
});
}, TEST_CASE_TIMEOUT);

it('should retrieve single param with transform binary', async () => {
const logs = invocationLogs[0].getFunctionLogs();
const testLog = InvocationLogs.parseFunctionLog(logs[2]);

expect(testLog).toStrictEqual({
test: 'get-transform-binary',
value: 'foo'
});
}, TEST_CASE_TIMEOUT);
});

it('should retrieve single param with transform auto json', async () => {
const logs = invocationLogs[0].getFunctionLogs();
const testLog = InvocationLogs.parseFunctionLog(logs[3]);

expect(testLog).toStrictEqual({
test: 'get-transform-auto-json',
value: { foo: 'bar' }
});
}, TEST_CASE_TIMEOUT);

it('should retrieve single param wit transform auto binary', async () => {
const logs = invocationLogs[0].getFunctionLogs();
const testLog = InvocationLogs.parseFunctionLog(logs[4]);

expect(testLog).toStrictEqual({
test: 'get-transform-auto-binary',
value: 'foo'
});
});

it('should retrieve single parameter cached', async () => {
const logs = invocationLogs[0].getFunctionLogs();
const testLogFirst = InvocationLogs.parseFunctionLog(logs[5]);

expect(testLogFirst).toStrictEqual({
test: 'get-plain-cached',
value: 1
});
});

it('should retrieve single parameter twice without caching', async () => {
const logs = invocationLogs[0].getFunctionLogs();
const testLogFirst = InvocationLogs.parseFunctionLog(logs[6]);

expect(testLogFirst).toStrictEqual({
test: 'get-plain-force',
value: 1
});
});

afterAll(async () => {
if (!process.env.DISABLE_TEARDOWN) {
await destroyStack(integTestApp, stack);
}
}, TEARDOWN_TIMEOUT);
});
10 changes: 5 additions & 5 deletions packages/parameters/tests/helpers/cdkAspectGrantAccess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,23 @@ 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 constructor(resources: Table[] | Secret[]) {
this.resources = resources;
}

public visit(node: IConstruct): void {
Expand Down