Skip to content

Commit 80731c1

Browse files
committed
Add some more tests around quoting, fix bugs
1 parent a5275a3 commit 80731c1

File tree

3 files changed

+63
-28
lines changed

3 files changed

+63
-28
lines changed

packages/@aws-cdk/core/lib/private/cloudformation-lang.ts

+10-16
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ function tokenAwareStringify(root: any, space: number, ctx: IResolveContext) {
209209
switch (resolvedTypeHint(intrinsic)) {
210210
case ResolutionTypeHint.STRING:
211211
pushLiteral('"');
212-
pushIntrinsic(quoteInsideIntrinsic(intrinsic));
212+
pushIntrinsic(deepQuoteStringLiterals(intrinsic));
213213
pushLiteral('"');
214214
break;
215215

@@ -368,22 +368,16 @@ function* definedEntries<A extends object>(xs: A): IterableIterator<[string, any
368368
* Logical IDs and attribute names, which cannot contain quotes anyway. Hence,
369369
* we can get away not caring about the distinction and just quoting everything.
370370
*/
371-
function quoteInsideIntrinsic(x: any): any {
372-
if (typeof x === 'object' && x != null && Object.keys(x).length === 1) {
373-
const key = Object.keys(x)[0];
374-
const params = x[key];
375-
switch (key) {
376-
case 'Fn::If':
377-
return { 'Fn::If': [params[0], quoteInsideIntrinsic(params[1]), quoteInsideIntrinsic(params[2])] };
378-
case 'Fn::Join':
379-
return { 'Fn::Join': [quoteInsideIntrinsic(params[0]), params[1].map(quoteInsideIntrinsic)] };
380-
case 'Fn::Sub':
381-
if (Array.isArray(params)) {
382-
return { 'Fn::Sub': [quoteInsideIntrinsic(params[0]), params[1]] };
383-
} else {
384-
return { 'Fn::Sub': quoteInsideIntrinsic(params[0]) };
385-
}
371+
function deepQuoteStringLiterals(x: any): any {
372+
if (Array.isArray(x)) {
373+
return x.map(deepQuoteStringLiterals);
374+
}
375+
if (typeof x === 'object' && x != null) {
376+
const ret: any = {};
377+
for (const [key, value] of Object.entries(x)) {
378+
ret[deepQuoteStringLiterals(key)] = deepQuoteStringLiterals(value);
386379
}
380+
return ret;
387381
}
388382
if (typeof x === 'string') {
389383
return quoteString(x);

packages/@aws-cdk/core/test/cloudformation-json.test.ts

+50-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { App, CfnOutput, Fn, IPostProcessor, IResolvable, IResolveContext, Lazy, Stack, Token } from '../lib';
1+
import { App, Aws, CfnOutput, Fn, IPostProcessor, IResolvable, IResolveContext, Lazy, Stack, Token } from '../lib';
22
import { Intrinsic } from '../lib/private/intrinsic';
33
import { evaluateCFN } from './evaluate-cfn';
44

@@ -196,6 +196,35 @@ describe('tokens returning CloudFormation intrinsics', () => {
196196
expect(evaluateCFN(resolved, context)).toEqual(expected);
197197
});
198198

199+
test('embedded string literals are escaped in Fn.sub (implicit references)', () => {
200+
// GIVEN
201+
const token = Fn.sub('I am in account "${AWS::AccountId}"');
202+
203+
// WHEN
204+
const resolved = stack.resolve(stack.toJsonString({ token }));
205+
206+
// THEN
207+
const context = { 'AWS::AccountId': '1234' };
208+
const expected = '{"token":"I am in account \\"1234\\""}';
209+
expect(evaluateCFN(resolved, context)).toEqual(expected);
210+
});
211+
212+
test('embedded string literals are escaped in Fn.sub (explicit references)', () => {
213+
// GIVEN
214+
const token = Fn.sub('I am in account "${Acct}", also wanted to say: ${Also}', {
215+
Acct: Aws.ACCOUNT_ID,
216+
Also: '"hello world"',
217+
});
218+
219+
// WHEN
220+
const resolved = stack.resolve(stack.toJsonString({ token }));
221+
222+
// THEN
223+
const context = { 'AWS::AccountId': '1234' };
224+
const expected = '{"token":"I am in account \\"1234\\", also wanted to say: \\"hello world\\""}';
225+
expect(evaluateCFN(resolved, context)).toEqual(expected);
226+
});
227+
199228
test('Tokens in Tokens are handled correctly', () => {
200229
// GIVEN
201230
const bucketName = new Intrinsic({ Ref: 'MyBucket' });
@@ -344,6 +373,26 @@ describe('tokens returning CloudFormation intrinsics', () => {
344373
});
345374
});
346375

376+
test('JSON strings nested inside JSON strings have correct quoting', () => {
377+
// GIVEN
378+
const payload = stack.toJsonString({
379+
message: Fn.sub('I am in account "${AWS::AccountId}"'),
380+
});
381+
382+
// WHEN
383+
const resolved = stack.resolve(stack.toJsonString({ payload }));
384+
385+
// THEN
386+
const context = { 'AWS::AccountId': '1234' };
387+
const expected = '{"payload":"{\\"message\\":\\"I am in account \\\\\\"1234\\\\\\"\\"}"}';
388+
const evaluated = evaluateCFN(resolved, context);
389+
expect(evaluated).toEqual(expected);
390+
391+
// Is this even correct? Let's ask JavaScript because I have trouble reading this many backslashes.
392+
expect(JSON.parse(JSON.parse(evaluated).payload).message).toEqual('I am in account "1234"');
393+
});
394+
395+
347396
/**
348397
* Return two Tokens, one of which evaluates to a Token directly, one which evaluates to it lazily
349398
*/

packages/@aws-cdk/core/test/evaluate-cfn.ts

+3-11
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,8 @@ export function evaluateCFN(object: any, context: {[key: string]: string} = {}):
4242
return context[key];
4343
},
4444

45-
'Fn::Sub'(argument: string | [string, Record<string, string>]) {
46-
let template;
47-
let placeholders: Record<string, string>;
48-
if (Array.isArray(argument)) {
49-
template = argument[0];
50-
placeholders = evaluate(argument[1]);
51-
} else {
52-
template = argument;
53-
placeholders = context;
54-
}
45+
'Fn::Sub'(template: string, explicitPlaceholders?: Record<string, string>) {
46+
const placeholders = explicitPlaceholders ? evaluate(explicitPlaceholders) : context;
5547

5648
if (typeof template !== 'string') {
5749
throw new Error('The first argument to {Fn::Sub} must be a string literal (cannot be the result of an expression)');
@@ -79,7 +71,7 @@ export function evaluateCFN(object: any, context: {[key: string]: string} = {}):
7971

8072
const ret: {[key: string]: any} = {};
8173
for (const key of Object.keys(obj)) {
82-
ret[key] = evaluateCFN(obj[key]);
74+
ret[key] = evaluate(obj[key]);
8375
}
8476
return ret;
8577
}

0 commit comments

Comments
 (0)