Skip to content

Commit bb50c04

Browse files
authored
fix(parameters): handle base64/binaries in transformer (#1326)
1 parent 4530be2 commit bb50c04

File tree

5 files changed

+63
-104
lines changed

5 files changed

+63
-104
lines changed

Diff for: packages/parameters/src/BaseProvider.ts

+9-7
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,11 @@ abstract class BaseProvider implements BaseProviderInterface {
131131
const transformValue = (value: string | Uint8Array | undefined, transform: TransformOptions, throwOnTransformError: boolean, key: string): string | Record<string, unknown> | undefined => {
132132
try {
133133
const normalizedTransform = transform.toLowerCase();
134+
135+
if (value instanceof Uint8Array) {
136+
value = new TextDecoder('utf-8').decode(value);
137+
}
138+
134139
if (
135140
(normalizedTransform === TRANSFORM_METHOD_JSON ||
136141
(normalizedTransform === 'auto' && key.toLowerCase().endsWith(`.${TRANSFORM_METHOD_JSON}`))) &&
@@ -139,15 +144,12 @@ const transformValue = (value: string | Uint8Array | undefined, transform: Trans
139144
return JSON.parse(value) as Record<string, unknown>;
140145
} else if (
141146
(normalizedTransform === TRANSFORM_METHOD_BINARY ||
142-
(normalizedTransform === 'auto' && key.toLowerCase().endsWith(`.${TRANSFORM_METHOD_BINARY}`)))
147+
(normalizedTransform === 'auto' && key.toLowerCase().endsWith(`.${TRANSFORM_METHOD_BINARY}`))) &&
148+
typeof value === 'string'
143149
) {
144-
if (typeof value === 'string') {
145-
return new TextDecoder('utf-8').decode(fromBase64(value));
146-
} else {
147-
return new TextDecoder('utf-8').decode(value);
148-
}
150+
return new TextDecoder('utf-8').decode(fromBase64(value));
149151
} else {
150-
return value as string;
152+
return value;
151153
}
152154
} catch (error) {
153155
if (throwOnTransformError)

Diff for: packages/parameters/tests/e2e/appConfigProvider.class.test.functionCode.ts

+13-17
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ const application = process.env.APPLICATION_NAME || 'my-app';
1616
const environment = process.env.ENVIRONMENT_NAME || 'my-env';
1717
const freeFormJsonName = process.env.FREEFORM_JSON_NAME || 'freeform-json';
1818
const freeFormYamlName = process.env.FREEFORM_YAML_NAME || 'freeform-yaml';
19-
const freeFormPlainTextNameA = process.env.FREEFORM_PLAIN_TEXT_NAME_A || 'freeform-plain-text';
20-
const freeFormPlainTextNameB = process.env.FREEFORM_PLAIN_TEXT_NAME_B || 'freeform-plain-text';
19+
const freeFormBase64encodedPlainText = process.env.FREEFORM_BASE64_ENCODED_PLAIN_TEXT_NAME || 'freeform-plain-text';
2120
const featureFlagName = process.env.FEATURE_FLAG_NAME || 'feature-flag';
2221

2322
const defaultProvider = new AppConfigProvider({
@@ -65,28 +64,25 @@ const _call_get = async (
6564
};
6665

6766
export const handler = async (_event: unknown, _context: Context): Promise<void> => {
68-
// Test 1 - get a single parameter as-is (no transformation)
69-
await _call_get(freeFormPlainTextNameA, 'get');
67+
// Test 1 - get a single parameter as-is (no transformation - should return an Uint8Array)
68+
await _call_get(freeFormYamlName, 'get');
7069

71-
// Test 2 - get a free-form JSON and apply binary transformation (should return a stringified JSON)
72-
await _call_get(freeFormJsonName, 'get-freeform-json-binary', { transform: 'binary' });
70+
// Test 2 - get a free-form JSON and apply json transformation (should return an object)
71+
await _call_get(freeFormJsonName, 'get-freeform-json-binary', { transform: 'json' });
7372

74-
// Test 3 - get a free-form YAML and apply binary transformation (should return a string-encoded YAML)
75-
await _call_get(freeFormYamlName, 'get-freeform-yaml-binary', { transform: 'binary' });
73+
// Test 3 - get a free-form base64-encoded plain text and apply binary transformation (should return a decoded string)
74+
await _call_get(freeFormBase64encodedPlainText, 'get-freeform-base64-plaintext-binary', { transform: 'binary' });
7675

77-
// Test 4 - get a free-form plain text and apply binary transformation (should return a string)
78-
await _call_get(freeFormPlainTextNameB, 'get-freeform-plain-text-binary', { transform: 'binary' });
79-
80-
// Test 5 - get a feature flag and apply binary transformation (should return a stringified JSON)
81-
await _call_get(featureFlagName, 'get-feature-flag-binary', { transform: 'binary' });
76+
// Test 5 - get a feature flag and apply json transformation (should return an object)
77+
await _call_get(featureFlagName, 'get-feature-flag-binary', { transform: 'json' });
8278

8379
// Test 6
8480
// get parameter twice with middleware, which counts the number of requests, we check later if we only called AppConfig API once
8581
try {
8682
providerWithMiddleware.clearCache();
8783
middleware.counter = 0;
88-
await providerWithMiddleware.get(freeFormPlainTextNameA);
89-
await providerWithMiddleware.get(freeFormPlainTextNameA);
84+
await providerWithMiddleware.get(freeFormBase64encodedPlainText);
85+
await providerWithMiddleware.get(freeFormBase64encodedPlainText);
9086
logger.log({
9187
test: 'get-cached',
9288
value: middleware.counter // should be 1
@@ -103,8 +99,8 @@ export const handler = async (_event: unknown, _context: Context): Promise<void>
10399
try {
104100
providerWithMiddleware.clearCache();
105101
middleware.counter = 0;
106-
await providerWithMiddleware.get(freeFormPlainTextNameA);
107-
await providerWithMiddleware.get(freeFormPlainTextNameA, { forceFetch: true });
102+
await providerWithMiddleware.get(freeFormBase64encodedPlainText);
103+
await providerWithMiddleware.get(freeFormBase64encodedPlainText, { forceFetch: true });
108104
logger.log({
109105
test: 'get-forced',
110106
value: middleware.counter // should be 2

Diff for: packages/parameters/tests/e2e/appConfigProvider.class.test.ts

+34-69
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66
import path from 'path';
77
import { App, Stack, Aspects } from 'aws-cdk-lib';
8+
import { toBase64 } from '@aws-sdk/util-base64-node';
89
import { v4 } from 'uuid';
910
import {
1011
generateUniqueName,
@@ -44,8 +45,7 @@ const environmentName = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime,
4445
const deploymentStrategyName = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'immediate');
4546
const freeFormJsonName = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'freeFormJson');
4647
const freeFormYamlName = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'freeFormYaml');
47-
const freeFormPlainTextNameA = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'freeFormPlainTextA');
48-
const freeFormPlainTextNameB = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'freeFormPlainTextB');
48+
const freeFormBase64PlainTextName = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'freeFormBase64PlainText');
4949
const featureFlagName = generateUniqueName(RESOURCE_NAME_PREFIX, uuid, runtime, 'featureFlag');
5050

5151
const freeFormJsonValue = {
@@ -85,33 +85,30 @@ let stack: Stack;
8585
* The parameters created are:
8686
* - Free-form JSON
8787
* - Free-form YAML
88-
* - 2x Free-form plain text
88+
* - Free-form plain text base64-encoded string
8989
* - Feature flag
9090
*
9191
* These parameters allow to retrieve the values and test some transformations.
9292
*
9393
* The tests are:
9494
*
9595
* Test 1
96-
* get a single parameter as-is (no transformation)
96+
* get a single parameter as-is (no transformation - should return an Uint8Array)
9797
*
9898
* Test 2
99-
* get a free-form JSON and apply binary transformation (should return a stringified JSON)
99+
* get a free-form JSON and apply json transformation (should return an object)
100100
*
101101
* Test 3
102-
* get a free-form YAML and apply binary transformation (should return a string-encoded YAML)
102+
* get a free-form base64-encoded plain text and apply binary transformation (should return a decoded string)
103103
*
104104
* Test 4
105-
* get a free-form plain text and apply binary transformation (should return a string)
105+
* get a feature flag and apply json transformation (should return an object)
106106
*
107107
* Test 5
108-
* get a feature flag and apply binary transformation (should return a stringified JSON)
109-
*
110-
* Test 6
111108
* get parameter twice with middleware, which counts the number of requests,
112109
* we check later if we only called AppConfig API once
113110
*
114-
* Test 7
111+
* Test 6
115112
* get parameter twice, but force fetch 2nd time, we count number of SDK requests and
116113
* check that we made two API calls
117114
*
@@ -140,8 +137,7 @@ describe(`parameters E2E tests (appConfigProvider) for runtime ${runtime}`, () =
140137
ENVIRONMENT_NAME: environmentName,
141138
FREEFORM_JSON_NAME: freeFormJsonName,
142139
FREEFORM_YAML_NAME: freeFormYamlName,
143-
FREEFORM_PLAIN_TEXT_NAME_A: freeFormPlainTextNameA,
144-
FREEFORM_PLAIN_TEXT_NAME_B: freeFormPlainTextNameB,
140+
FREEFORM_BASE64_ENCODED_PLAIN_TEXT_NAME: freeFormBase64PlainTextName,
145141
FEATURE_FLAG_NAME: featureFlagName,
146142
},
147143
runtime,
@@ -187,33 +183,19 @@ describe(`parameters E2E tests (appConfigProvider) for runtime ${runtime}`, () =
187183
});
188184
freeFormYaml.node.addDependency(freeFormJson);
189185

190-
const freeFormPlainTextA = createAppConfigConfigurationProfile({
186+
const freeFormBase64PlainText = createAppConfigConfigurationProfile({
191187
stack,
192188
application,
193189
environment,
194190
deploymentStrategy,
195-
name: freeFormPlainTextNameA,
191+
name: freeFormBase64PlainTextName,
196192
type: 'AWS.Freeform',
197193
content: {
198-
content: freeFormPlainTextValue,
194+
content: toBase64(new TextEncoder().encode(freeFormPlainTextValue)),
199195
contentType: 'text/plain',
200196
}
201197
});
202-
freeFormPlainTextA.node.addDependency(freeFormYaml);
203-
204-
const freeFormPlainTextB = createAppConfigConfigurationProfile({
205-
stack,
206-
application,
207-
environment,
208-
deploymentStrategy,
209-
name: freeFormPlainTextNameB,
210-
type: 'AWS.Freeform',
211-
content: {
212-
content: freeFormPlainTextValue,
213-
contentType: 'text/plain',
214-
}
215-
});
216-
freeFormPlainTextB.node.addDependency(freeFormPlainTextA);
198+
freeFormBase64PlainText.node.addDependency(freeFormYaml);
217199

218200
const featureFlag = createAppConfigConfigurationProfile({
219201
stack,
@@ -227,14 +209,13 @@ describe(`parameters E2E tests (appConfigProvider) for runtime ${runtime}`, () =
227209
contentType: 'application/json',
228210
}
229211
});
230-
featureFlag.node.addDependency(freeFormPlainTextB);
212+
featureFlag.node.addDependency(freeFormBase64PlainText);
231213

232214
// Grant access to the Lambda function to the AppConfig resources.
233215
Aspects.of(stack).add(new ResourceAccessGranter([
234216
freeFormJson,
235217
freeFormYaml,
236-
freeFormPlainTextA,
237-
freeFormPlainTextB,
218+
freeFormBase64PlainText,
238219
featureFlag,
239220
]));
240221

@@ -248,8 +229,8 @@ describe(`parameters E2E tests (appConfigProvider) for runtime ${runtime}`, () =
248229

249230
describe('AppConfigProvider usage', () => {
250231

251-
// Test 1 - get a single parameter as-is (no transformation)
252-
it('should retrieve single parameter', () => {
232+
// Test 1 - get a single parameter as-is (no transformation - should return an Uint8Array)
233+
it('should retrieve single parameter as-is', () => {
253234

254235
const logs = invocationLogs[0].getFunctionLogs();
255236
const testLog = InvocationLogs.parseFunctionLog(logs[0]);
@@ -258,75 +239,59 @@ describe(`parameters E2E tests (appConfigProvider) for runtime ${runtime}`, () =
258239
test: 'get',
259240
value: JSON.parse(
260241
JSON.stringify(
261-
encoder.encode(freeFormPlainTextValue)
242+
encoder.encode(freeFormYamlValue)
262243
)
263244
),
264245
});
265246

266247
});
267248

268-
// Test 2 - get a free-form JSON and apply binary transformation
269-
// (should return a stringified JSON)
270-
it('should retrieve single free-form JSON parameter with binary transformation', () => {
249+
// Test 2 - get a free-form JSON and apply json transformation (should return an object)
250+
it('should retrieve a free-form JSON parameter with JSON transformation', () => {
271251

272252
const logs = invocationLogs[0].getFunctionLogs();
273253
const testLog = InvocationLogs.parseFunctionLog(logs[1]);
274254

275255
expect(testLog).toStrictEqual({
276256
test: 'get-freeform-json-binary',
277-
value: JSON.stringify(freeFormJsonValue),
257+
value: freeFormJsonValue,
278258
});
279259

280260
});
281261

282-
// Test 3 - get a free-form YAML and apply binary transformation
283-
// (should return a string-encoded YAML)
284-
it('should retrieve single free-form YAML parameter with binary transformation', () => {
262+
// Test 3 - get a free-form base64-encoded plain text and apply binary transformation
263+
// (should return a decoded string)
264+
it('should retrieve a base64-encoded plain text parameter with binary transformation', () => {
285265

286266
const logs = invocationLogs[0].getFunctionLogs();
287267
const testLog = InvocationLogs.parseFunctionLog(logs[2]);
288268

289269
expect(testLog).toStrictEqual({
290-
test: 'get-freeform-yaml-binary',
291-
value: freeFormYamlValue,
292-
});
293-
294-
});
295-
296-
// Test 4 - get a free-form plain text and apply binary transformation
297-
// (should return a string)
298-
it('should retrieve single free-form plain text parameter with binary transformation', () => {
299-
300-
const logs = invocationLogs[0].getFunctionLogs();
301-
const testLog = InvocationLogs.parseFunctionLog(logs[3]);
302-
303-
expect(testLog).toStrictEqual({
304-
test: 'get-freeform-plain-text-binary',
270+
test: 'get-freeform-base64-plaintext-binary',
305271
value: freeFormPlainTextValue,
306272
});
307273

308274
});
309275

310-
// Test 5 - get a feature flag and apply binary transformation
311-
// (should return a stringified JSON)
312-
it('should retrieve single feature flag parameter with binary transformation', () => {
276+
// Test 4 - get a feature flag and apply json transformation (should return an object)
277+
it('should retrieve a feature flag parameter with JSON transformation', () => {
313278

314279
const logs = invocationLogs[0].getFunctionLogs();
315-
const testLog = InvocationLogs.parseFunctionLog(logs[4]);
280+
const testLog = InvocationLogs.parseFunctionLog(logs[3]);
316281

317282
expect(testLog).toStrictEqual({
318283
test: 'get-feature-flag-binary',
319-
value: JSON.stringify(featureFlagValue.values),
284+
value: featureFlagValue.values,
320285
});
321286

322287
});
323-
324-
// Test 6 - get parameter twice with middleware, which counts the number
288+
289+
// Test 5 - get parameter twice with middleware, which counts the number
325290
// of requests, we check later if we only called AppConfig API once
326291
it('should retrieve single parameter cached', () => {
327292

328293
const logs = invocationLogs[0].getFunctionLogs();
329-
const testLog = InvocationLogs.parseFunctionLog(logs[5]);
294+
const testLog = InvocationLogs.parseFunctionLog(logs[4]);
330295

331296
expect(testLog).toStrictEqual({
332297
test: 'get-cached',
@@ -335,12 +300,12 @@ describe(`parameters E2E tests (appConfigProvider) for runtime ${runtime}`, () =
335300

336301
}, TEST_CASE_TIMEOUT);
337302

338-
// Test 7 - get parameter twice, but force fetch 2nd time,
303+
// Test 6 - get parameter twice, but force fetch 2nd time,
339304
// we count number of SDK requests and check that we made two API calls
340305
it('should retrieve single parameter twice without caching', async () => {
341306

342307
const logs = invocationLogs[0].getFunctionLogs();
343-
const testLog = InvocationLogs.parseFunctionLog(logs[6]);
308+
const testLog = InvocationLogs.parseFunctionLog(logs[5]);
344309

345310
expect(testLog).toStrictEqual({
346311
test: 'get-forced',

Diff for: packages/parameters/tests/unit/BaseProvider.test.ts

+3-7
Original file line numberDiff line numberDiff line change
@@ -206,19 +206,15 @@ describe('Class: BaseProvider', () => {
206206

207207
});
208208

209-
test('when called with a binary transform, and the value is a valid binary, it returns the decoded value', async () => {
209+
test('when called with a binary transform, and the value is a valid binary but NOT base64 encoded, it throws', async () => {
210210

211211
// Prepare
212212
const mockData = encoder.encode('my-value');
213213
const provider = new TestProvider();
214214
jest.spyOn(provider, '_get').mockImplementation(() => new Promise((resolve, _reject) => resolve(mockData as unknown as string)));
215215

216-
// Act
217-
const value = await provider.get('my-parameter', { transform: 'binary' });
218-
219-
// Assess
220-
expect(typeof value).toBe('string');
221-
expect(value).toEqual('my-value');
216+
// Act & Assess
217+
await expect(provider.get('my-parameter', { transform: 'binary' })).rejects.toThrowError(TransformParameterError);
222218

223219
});
224220

Diff for: packages/parameters/tests/unit/getAppConfig.test.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ import {
1616
import { mockClient } from 'aws-sdk-client-mock';
1717
import 'aws-sdk-client-mock-jest';
1818
import type { GetAppConfigCombinedInterface } from '../../src/types/AppConfigProvider';
19+
import { toBase64 } from '@aws-sdk/util-base64-node';
1920

2021
describe('Function: getAppConfig', () => {
2122
const client = mockClient(AppConfigDataClient);
2223
const encoder = new TextEncoder();
23-
const decoder = new TextDecoder();
2424

2525
beforeEach(() => {
2626
jest.clearAllMocks();
@@ -103,8 +103,8 @@ describe('Function: getAppConfig', () => {
103103
'AYADeNgfsRxdKiJ37A12OZ9vN2cAXwABABVhd3MtY3J5cHRvLXB1YmxpYy1rZXkAREF1RzlLMTg1Tkx2Wjk4OGV2UXkyQ1';
104104
const mockNextToken =
105105
'ImRmyljpZnxt7FfxeEOE5H8xQF1SfOlWZFnHujbzJmIvNeSAAA8/qA9ivK0ElRMwpvx96damGxt125XtMkmYf6a0OWSqnBw==';
106-
const mockData = encoder.encode('myAppConfiguration');
107-
const decodedData = decoder.decode(mockData);
106+
const expectedValue = 'my-value';
107+
const mockData = encoder.encode(toBase64(encoder.encode(expectedValue)));
108108

109109
client
110110
.on(StartConfigurationSessionCommand)
@@ -121,6 +121,6 @@ describe('Function: getAppConfig', () => {
121121
const result = await getAppConfig(name, options);
122122

123123
// Assess
124-
expect(result).toBe(decodedData);
124+
expect(result).toBe(expectedValue);
125125
});
126126
});

0 commit comments

Comments
 (0)