Skip to content

Commit de02b3d

Browse files
author
Alexander Schueren
authored
tests(parameters): integration tests for SecretsProvider (#1263)
* change to more generic name * fix conditional * add provider to signature * add github issue to todo * add tests for caching and forceFetch
1 parent 64771fc commit de02b3d

File tree

3 files changed

+286
-5
lines changed

3 files changed

+286
-5
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { Context } from 'aws-lambda';
2+
import { SecretsProvider } from '../../lib/secrets';
3+
import { TinyLogger } from '../helpers/tinyLogger';
4+
import { SecretsGetOptionsInterface } from '../../lib/types';
5+
import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager';
6+
import { middleware } from '../helpers/sdkMiddlewareRequestCounter';
7+
8+
const logger = new TinyLogger();
9+
const defaultProvider = new SecretsProvider();
10+
11+
const secretNamePlain = process.env.SECRET_NAME_PLAIN || '';
12+
const secretNameObject = process.env.SECRET_NAME_OBJECT || '';
13+
const secretNameBinary = process.env.SECRET_NAME_BINARY || '';
14+
const secretNameObjectWithSuffix = process.env.SECRET_NAME_OBJECT_WITH_SUFFIX || '';
15+
const secretNameBinaryWithSuffix = process.env.SECRET_NAME_BINARY_WITH_SUFFIX || '';
16+
const secretNamePlainChached = process.env.SECRET_NAME_PLAIN_CACHED || '';
17+
18+
// Provider test 8, 9
19+
const customClient = new SecretsManagerClient({});
20+
customClient.middlewareStack.use(middleware);
21+
const providerWithMiddleware = new SecretsProvider({
22+
awsSdkV3Client: customClient
23+
});
24+
25+
const _call_get = async (paramName: string, testName: string, options?: SecretsGetOptionsInterface, provider?: SecretsProvider,): Promise<void> => {
26+
try {
27+
// we might get a provider with specific sdk options, otherwise fallback to default
28+
const currentProvider = provider ? provider : defaultProvider;
29+
30+
const parameterValue = await currentProvider.get(paramName, options);
31+
logger.log({
32+
test: testName,
33+
value: parameterValue
34+
});
35+
} catch (err) {
36+
logger.log({
37+
test: testName,
38+
error: err.message
39+
});
40+
}
41+
};
42+
43+
export const handler = async (_event: unknown, _context: Context): Promise<void> => {
44+
45+
// Test 1 get single param as plaintext
46+
await _call_get(secretNamePlain, 'get-plain');
47+
48+
// Test 2 get single param with transform json
49+
await _call_get(secretNameObject, 'get-transform-json', { transform: 'json' });
50+
51+
// Test 3 get single param with transform binary
52+
await _call_get(secretNameBinary, 'get-transform-binary', { transform: 'binary' });
53+
54+
// Test 4 get single param with transform auto json
55+
await _call_get(secretNameObjectWithSuffix, 'get-transform-auto-json', { transform: 'auto' });
56+
57+
// Test 5 get single param with transform auto binary
58+
await _call_get(secretNameBinaryWithSuffix, 'get-transform-auto-binary', { transform: 'auto' });
59+
60+
// Test 6
61+
// get parameter twice with middleware, which counts number of SDK requests, we check later if we only called SecretManager API once
62+
try {
63+
middleware.counter = 0;
64+
await providerWithMiddleware.get(secretNamePlainChached);
65+
await providerWithMiddleware.get(secretNamePlainChached);
66+
logger.log({
67+
test: 'get-plain-cached',
68+
value: middleware.counter // should be 1
69+
});
70+
} catch (err) {
71+
logger.log({
72+
test: secretNamePlainChached,
73+
error: err.message
74+
});
75+
}
76+
// Test 7
77+
// get parameter twice, but force fetch 2nd time, we count number of SDK requests and check that we made two API calls
78+
try {
79+
middleware.counter = 0;
80+
await providerWithMiddleware.get(secretNamePlainChached);
81+
await providerWithMiddleware.get(secretNamePlainChached, { forceFetch: true });
82+
logger.log({
83+
test: 'get-plain-force',
84+
value: middleware.counter // should be 2
85+
});
86+
} catch (err) {
87+
logger.log({
88+
test: secretNamePlainChached,
89+
error: err.message
90+
});
91+
}
92+
93+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
/**
2+
* Test SecretsPorovider class
3+
*
4+
* @group e2e/parameters/secrets/class
5+
*/
6+
import {
7+
createStackWithLambdaFunction,
8+
generateUniqueName,
9+
invokeFunction,
10+
isValidRuntimeKey
11+
} from '../../../commons/tests/utils/e2eUtils';
12+
import { RESOURCE_NAME_PREFIX, SETUP_TIMEOUT, TEARDOWN_TIMEOUT, TEST_CASE_TIMEOUT } from './constants';
13+
import { v4 } from 'uuid';
14+
import { Tracing } from 'aws-cdk-lib/aws-lambda';
15+
import { deployStack, destroyStack } from '../../../commons/tests/utils/cdk-cli';
16+
import { App, Aspects, SecretValue, Stack } from 'aws-cdk-lib';
17+
import path from 'path';
18+
import { Secret } from 'aws-cdk-lib/aws-secretsmanager';
19+
import { InvocationLogs } from '../../../commons/tests/utils/InvocationLogs';
20+
import { ResourceAccessGranter } from '../helpers/cdkAspectGrantAccess';
21+
22+
const runtime: string = process.env.RUNTIME || 'nodejs18x';
23+
24+
if (!isValidRuntimeKey(runtime)) {
25+
throw new Error(`Invalid runtime key: ${runtime}`);
26+
}
27+
28+
const uuid = v4();
29+
const stackName = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'secretsProvider');
30+
const functionName = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'secretsProvider');
31+
const lambdaFunctionCodeFile = 'secretsProvider.class.test.functionCode.ts';
32+
33+
const secretNamePlain = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'testSecretPlain');
34+
const secretNamePlainCached = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'testSecretPlainCached');
35+
const secretNameObject = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'testSecretObject');
36+
const secretNameBinary = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'testSecretBinary');
37+
const secretNameObjectWithSuffix = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'testSecretObject.json');
38+
const secretNameBinaryWithSuffix = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'testSecretObject.binary');
39+
40+
const invocationCount = 1;
41+
42+
const integTestApp = new App();
43+
let stack: Stack;
44+
45+
describe(`parameters E2E tests (SecretsProvider) for runtime: ${runtime}`, () => {
46+
47+
let invocationLogs: InvocationLogs[];
48+
49+
beforeAll(async () => {
50+
stack = createStackWithLambdaFunction({
51+
app: integTestApp,
52+
stackName: stackName,
53+
functionName: functionName,
54+
functionEntry: path.join(__dirname, lambdaFunctionCodeFile),
55+
tracing: Tracing.ACTIVE,
56+
environment: {
57+
UUID: uuid,
58+
SECRET_NAME_PLAIN: secretNamePlain,
59+
SECRET_NAME_OBJECT: secretNameObject,
60+
SECRET_NAME_BINARY: secretNameBinary,
61+
SECRET_NAME_OBJECT_WITH_SUFFIX: secretNameObjectWithSuffix,
62+
SECRET_NAME_BINARY_WITH_SUFFIX: secretNameBinaryWithSuffix,
63+
SECRET_NAME_PLAIN_CACHED: secretNamePlainCached,
64+
},
65+
runtime: runtime
66+
});
67+
68+
const secretString = new Secret(stack, 'testSecretPlain', {
69+
secretName: secretNamePlain,
70+
secretStringValue: SecretValue.unsafePlainText('foo')
71+
});
72+
73+
const secretObject = new Secret(stack, 'testSecretObject', {
74+
secretName: secretNameObject,
75+
secretObjectValue: {
76+
foo: SecretValue.unsafePlainText('bar'),
77+
}
78+
});
79+
80+
const secretBinary = new Secret(stack, 'testSecretBinary', {
81+
secretName: secretNameBinary,
82+
secretStringValue: SecretValue.unsafePlainText('Zm9v') // 'foo' encoded in base64
83+
});
84+
85+
const secretObjectWithSuffix = new Secret(stack, 'testSecretObjectWithSuffix', {
86+
secretName: secretNameObjectWithSuffix,
87+
secretObjectValue: {
88+
foo: SecretValue.unsafePlainText('bar')
89+
}
90+
});
91+
92+
const secretBinaryWithSuffix = new Secret(stack, 'testSecretBinaryWithSuffix', {
93+
secretName: secretNameBinaryWithSuffix,
94+
secretStringValue: SecretValue.unsafePlainText('Zm9v') // 'foo' encoded in base64
95+
});
96+
97+
const secretStringCached = new Secret(stack, 'testSecretStringCached', {
98+
secretName: secretNamePlainCached,
99+
secretStringValue: SecretValue.unsafePlainText('foo')
100+
});
101+
102+
Aspects.of(stack).add(new ResourceAccessGranter([ secretString, secretObject, secretBinary, secretObjectWithSuffix, secretBinaryWithSuffix, secretStringCached ]));
103+
104+
await deployStack(integTestApp, stack);
105+
106+
invocationLogs = await invokeFunction(functionName, invocationCount, 'SEQUENTIAL');
107+
108+
}, SETUP_TIMEOUT);
109+
110+
describe('SecretsProvider usage', () => {
111+
it('should retrieve a single parameter', async () => {
112+
113+
const logs = invocationLogs[0].getFunctionLogs();
114+
const testLog = InvocationLogs.parseFunctionLog(logs[0]);
115+
116+
expect(testLog).toStrictEqual({
117+
test: 'get-plain',
118+
value: 'foo'
119+
});
120+
}, TEST_CASE_TIMEOUT);
121+
122+
it('should retrieve a single parameter with transform json', async () => {
123+
const logs = invocationLogs[0].getFunctionLogs();
124+
const testLog = InvocationLogs.parseFunctionLog(logs[1]);
125+
126+
expect(testLog).toStrictEqual({
127+
test: 'get-transform-json',
128+
value: { foo: 'bar' }
129+
});
130+
}, TEST_CASE_TIMEOUT);
131+
132+
it('should retrieve single param with transform binary', async () => {
133+
const logs = invocationLogs[0].getFunctionLogs();
134+
const testLog = InvocationLogs.parseFunctionLog(logs[2]);
135+
136+
expect(testLog).toStrictEqual({
137+
test: 'get-transform-binary',
138+
value: 'foo'
139+
});
140+
}, TEST_CASE_TIMEOUT);
141+
});
142+
143+
it('should retrieve single param with transform auto json', async () => {
144+
const logs = invocationLogs[0].getFunctionLogs();
145+
const testLog = InvocationLogs.parseFunctionLog(logs[3]);
146+
147+
expect(testLog).toStrictEqual({
148+
test: 'get-transform-auto-json',
149+
value: { foo: 'bar' }
150+
});
151+
}, TEST_CASE_TIMEOUT);
152+
153+
it('should retrieve single param wit transform auto binary', async () => {
154+
const logs = invocationLogs[0].getFunctionLogs();
155+
const testLog = InvocationLogs.parseFunctionLog(logs[4]);
156+
157+
expect(testLog).toStrictEqual({
158+
test: 'get-transform-auto-binary',
159+
value: 'foo'
160+
});
161+
});
162+
163+
it('should retrieve single parameter cached', async () => {
164+
const logs = invocationLogs[0].getFunctionLogs();
165+
const testLogFirst = InvocationLogs.parseFunctionLog(logs[5]);
166+
167+
expect(testLogFirst).toStrictEqual({
168+
test: 'get-plain-cached',
169+
value: 1
170+
});
171+
});
172+
173+
it('should retrieve single parameter twice without caching', async () => {
174+
const logs = invocationLogs[0].getFunctionLogs();
175+
const testLogFirst = InvocationLogs.parseFunctionLog(logs[6]);
176+
177+
expect(testLogFirst).toStrictEqual({
178+
test: 'get-plain-force',
179+
value: 1
180+
});
181+
});
182+
183+
afterAll(async () => {
184+
if (!process.env.DISABLE_TEARDOWN) {
185+
await destroyStack(integTestApp, stack);
186+
}
187+
}, TEARDOWN_TIMEOUT);
188+
});

Diff for: packages/parameters/tests/helpers/cdkAspectGrantAccess.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,23 @@ import { Secret } from 'aws-cdk-lib/aws-secretsmanager';
66

77
/**
88
* An aspect that grants access to resources to a Lambda function.
9-
*
9+
*
1010
* In our integration tests, we dynamically generate AWS CDK stacks that contain a Lambda function.
1111
* We want to grant access to resources to the Lambda function, but we don't know the name of the
1212
* Lambda function at the time we create the resources. Additionally, we want to keep the code
1313
* that creates the stacks and functions as generic as possible.
14-
*
14+
*
1515
* This aspect allows us to grant access to specific resources to all Lambda functions in a stack
1616
* after the stack tree has been generated and before the stack is deployed. This aspect is
1717
* used to grant access to different resource types (DynamoDB tables, SSM parameters, etc.).
18-
*
18+
*
1919
* @see {@link https://docs.aws.amazon.com/cdk/v2/guide/aspects.html|CDK Docs - Aspects}
2020
*/
2121
export class ResourceAccessGranter implements IAspect {
2222
private readonly resources: Table[] | Secret[];
2323

24-
public constructor(tables: Table[] | Secret[]) {
25-
this.resources = tables;
24+
public constructor(resources: Table[] | Secret[]) {
25+
this.resources = resources;
2626
}
2727

2828
public visit(node: IConstruct): void {

0 commit comments

Comments
 (0)