From 49f617bf2468b3827db9c89cea54ec3353ddc5e9 Mon Sep 17 00:00:00 2001 From: robertd Date: Thu, 20 Jan 2022 15:12:28 -0700 Subject: [PATCH 1/4] feat(assertions): support for conditions --- .../assertions/lib/private/conditions.ts | 30 +++++++ .../assertions/lib/private/template.ts | 7 +- packages/@aws-cdk/assertions/lib/template.ts | 26 ++++++ .../@aws-cdk/assertions/test/template.test.ts | 80 ++++++++++++++++++- 4 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 packages/@aws-cdk/assertions/lib/private/conditions.ts diff --git a/packages/@aws-cdk/assertions/lib/private/conditions.ts b/packages/@aws-cdk/assertions/lib/private/conditions.ts new file mode 100644 index 0000000000000..d941d2d849215 --- /dev/null +++ b/packages/@aws-cdk/assertions/lib/private/conditions.ts @@ -0,0 +1,30 @@ +import { filterLogicalId, formatFailure, matchSection } from './section'; +import { Template } from './template'; + +export function findConditions(template: Template, logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } { + const section: { [key: string] : {} } = template.Conditions; + const result = matchSection(filterLogicalId(section, logicalId), props); + + if (!result.match) { + return {}; + } + + return result.matches; +} + +export function hasCondition(template: Template, logicalId: string, props: any): string | void { + const section: { [key: string] : {} } = template.Conditions; + const result = matchSection(filterLogicalId(section, logicalId), props); + if (result.match) { + return; + } + + if (result.closestResult === undefined) { + return 'No parameters found in the template'; + } + + return [ + `Template has ${result.analyzedCount} conditions, but none match as expected.`, + formatFailure(result.closestResult), + ].join('\n'); +} diff --git a/packages/@aws-cdk/assertions/lib/private/template.ts b/packages/@aws-cdk/assertions/lib/private/template.ts index 72dbeb8b64661..fc5d0cb6b1e01 100644 --- a/packages/@aws-cdk/assertions/lib/private/template.ts +++ b/packages/@aws-cdk/assertions/lib/private/template.ts @@ -4,7 +4,8 @@ export type Template = { Resources: { [logicalId: string]: Resource }, Outputs: { [logicalId: string]: Output }, Mappings: { [logicalId: string]: Mapping }, - Parameters: { [logicalId: string]: Parameter } + Parameters: { [logicalId: string]: Parameter }, + Conditions: { [logicalId: string]: Condition }, } export type Resource = { @@ -19,4 +20,6 @@ export type Mapping = { [key: string]: any }; export type Parameter = { Type: string; [key: string]: any; -} \ No newline at end of file +} + +export type Condition = { [key: string]: any }; \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/lib/template.ts b/packages/@aws-cdk/assertions/lib/template.ts index 2de687096bf7b..368181b5ee97c 100644 --- a/packages/@aws-cdk/assertions/lib/template.ts +++ b/packages/@aws-cdk/assertions/lib/template.ts @@ -3,6 +3,7 @@ import { Stack, Stage } from '@aws-cdk/core'; import * as fs from 'fs-extra'; import { Match } from './match'; import { Matcher } from './matcher'; +import { findConditions, hasCondition } from './private/conditions'; import { findMappings, hasMapping } from './private/mappings'; import { findOutputs, hasOutput } from './private/outputs'; import { findParameters, hasParameter } from './private/parameters'; @@ -183,6 +184,31 @@ export class Template { return findMappings(this.template, logicalId, props); } + /** + * Assert that a Condition with the given properties exists in the CloudFormation template. + * By default, performs partial matching on the resource, via the `Match.objectLike()`. + * To configure different behavour, use other matchers in the `Match` class. + * @param logicalId the name of the mapping. Provide `'*'` to match all conditions in the template. + * @param props the output as should be expected in the template. + */ + public hasCondition(logicalId: string, props: any): void { + const matchError = hasCondition(this.template, logicalId, props); + if (matchError) { + throw new Error(matchError); + } + } + + /** + * Get the set of matching Conditions that match the given properties in the CloudFormation template. + * @param logicalId the name of the mapping. Provide `'*'` to match all conditions in the template. + * @param props by default, matches all Conditions in the template. + * When a literal object is provided, performs a partial match via `Match.objectLike()`. + * Use the `Match` APIs to configure a different behaviour. + */ + public findConditions(logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } { + return findConditions(this.template, logicalId, props); + } + /** * Assert that the CloudFormation template matches the given value * @param expected the expected CloudFormation template as key-value pairs. diff --git a/packages/@aws-cdk/assertions/test/template.test.ts b/packages/@aws-cdk/assertions/test/template.test.ts index dd8377892f405..834c27319bffe 100644 --- a/packages/@aws-cdk/assertions/test/template.test.ts +++ b/packages/@aws-cdk/assertions/test/template.test.ts @@ -1,4 +1,4 @@ -import { App, CfnMapping, CfnOutput, CfnParameter, CfnResource, NestedStack, Stack } from '@aws-cdk/core'; +import { App, CfnCondition, CfnMapping, CfnOutput, CfnParameter, CfnResource, Fn, NestedStack, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { Capture, Match, Template } from '../lib'; @@ -940,6 +940,84 @@ describe('Template', () => { expect(Object.keys(result).length).toEqual(0); }); }); + + describe('findConditions', () => { + test('matching', () => { + const stack = new Stack(); + new CfnCondition(stack, 'Foo', { + expression: Fn.conditionEquals('Bar', 'Baz'), + }); + + new CfnCondition(stack, 'Qux', { + expression: Fn.conditionNot(Fn.conditionEquals('Quux', 'Quuz')), + }); + + const inspect = Template.fromStack(stack); + const firstCondition = inspect.findConditions('Foo'); + expect(firstCondition).toEqual({ + Foo: { + 'Fn::Equals': [ + 'Bar', + 'Baz', + ], + }, + }); + + const secondCondition = inspect.findConditions('Qux'); + expect(secondCondition).toEqual({ + Qux: { + 'Fn::Not': [ + { + 'Fn::Equals': [ + 'Quux', + 'Quuz', + ], + }, + ], + }, + }); + }); + + test('not matching', () => { + const stack = new Stack(); + new CfnCondition(stack, 'Foo', { + expression: Fn.conditionEquals('Bar', 'Baz'), + }); + + const inspect = Template.fromStack(stack); + const result = inspect.findMappings('Bar'); + expect(Object.keys(result).length).toEqual(0); + }); + + test('matching with specific outputName', () => { + const stack = new Stack(); + new CfnCondition(stack, 'Foo', { + expression: Fn.conditionEquals('Bar', 'Baz'), + }); + + const inspect = Template.fromStack(stack); + const result = inspect.findConditions('Foo', { 'Fn::Equals': ['Bar', 'Baz'] }); + expect(result).toEqual({ + Foo: { + 'Fn::Equals': [ + 'Bar', + 'Baz', + ], + }, + }); + }); + + test('not matching specific output name', () => { + const stack = new Stack(); + new CfnCondition(stack, 'Foo', { + expression: Fn.conditionEquals('Bar', 'Baz'), + }); + + const inspect = Template.fromStack(stack); + const result = inspect.findConditions('Fred', { Baz: { Bar: 'Qux' } }); + expect(Object.keys(result).length).toEqual(0); + }); + }); }); function expectToThrow(fn: () => void, msgs: (RegExp | string)[], done: jest.DoneCallback): void { From 1daa57e0f5182000628149d32205eb21eee99609 Mon Sep 17 00:00:00 2001 From: robertd Date: Thu, 20 Jan 2022 16:05:12 -0700 Subject: [PATCH 2/4] update readme and minor correction --- packages/@aws-cdk/assertions/README.md | 2 +- packages/@aws-cdk/assertions/lib/template.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/assertions/README.md b/packages/@aws-cdk/assertions/README.md index cfa09352b2469..3d1275d2b4a3e 100644 --- a/packages/@aws-cdk/assertions/README.md +++ b/packages/@aws-cdk/assertions/README.md @@ -139,7 +139,7 @@ expect(result.Foo).toEqual({ Value: 'Fred', Description: 'FooFred' }); expect(result.Bar).toEqual({ Value: 'Fred', Description: 'BarFred' }); ``` -The APIs `hasMapping()` and `findMappings()` provide similar functionalities. +The APIs `hasMapping()`, `findMappings()`, `hasCondition()`, and `hasCondtions()` provide similar functionalities. ## Special Matchers diff --git a/packages/@aws-cdk/assertions/lib/template.ts b/packages/@aws-cdk/assertions/lib/template.ts index 368181b5ee97c..8875a91d0ac9c 100644 --- a/packages/@aws-cdk/assertions/lib/template.ts +++ b/packages/@aws-cdk/assertions/lib/template.ts @@ -200,7 +200,7 @@ export class Template { /** * Get the set of matching Conditions that match the given properties in the CloudFormation template. - * @param logicalId the name of the mapping. Provide `'*'` to match all conditions in the template. + * @param logicalId the name of the condition. Provide `'*'` to match all conditions in the template. * @param props by default, matches all Conditions in the template. * When a literal object is provided, performs a partial match via `Match.objectLike()`. * Use the `Match` APIs to configure a different behaviour. From 9931f105e89ac8b1f1d7779d2f2be27279492a0e Mon Sep 17 00:00:00 2001 From: robertd Date: Thu, 20 Jan 2022 16:05:44 -0700 Subject: [PATCH 3/4] minor correction --- packages/@aws-cdk/assertions/test/template.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/assertions/test/template.test.ts b/packages/@aws-cdk/assertions/test/template.test.ts index 834c27319bffe..1600cdc20bc25 100644 --- a/packages/@aws-cdk/assertions/test/template.test.ts +++ b/packages/@aws-cdk/assertions/test/template.test.ts @@ -1014,7 +1014,7 @@ describe('Template', () => { }); const inspect = Template.fromStack(stack); - const result = inspect.findConditions('Fred', { Baz: { Bar: 'Qux' } }); + const result = inspect.findConditions('Foo', { 'Fn::Equals': { 'Bar': 'Qux' } }); expect(Object.keys(result).length).toEqual(0); }); }); From 4dbf25738e1e5cec552a1525bdfbf5d103217eb9 Mon Sep 17 00:00:00 2001 From: robertd Date: Thu, 20 Jan 2022 16:33:32 -0700 Subject: [PATCH 4/4] add more tests --- .../assertions/lib/private/conditions.ts | 2 +- .../@aws-cdk/assertions/test/template.test.ts | 68 ++++++++++++++++++- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/assertions/lib/private/conditions.ts b/packages/@aws-cdk/assertions/lib/private/conditions.ts index d941d2d849215..e7c4665dee219 100644 --- a/packages/@aws-cdk/assertions/lib/private/conditions.ts +++ b/packages/@aws-cdk/assertions/lib/private/conditions.ts @@ -20,7 +20,7 @@ export function hasCondition(template: Template, logicalId: string, props: any): } if (result.closestResult === undefined) { - return 'No parameters found in the template'; + return 'No conditions found in the template'; } return [ diff --git a/packages/@aws-cdk/assertions/test/template.test.ts b/packages/@aws-cdk/assertions/test/template.test.ts index 1600cdc20bc25..92bdb405ab9ce 100644 --- a/packages/@aws-cdk/assertions/test/template.test.ts +++ b/packages/@aws-cdk/assertions/test/template.test.ts @@ -941,6 +941,72 @@ describe('Template', () => { }); }); + describe('hasCondition', () => { + test('matching', () => { + const stack = new Stack(); + new CfnCondition(stack, 'Foo', { + expression: Fn.conditionEquals('Bar', 'Baz'), + }); + + const inspect = Template.fromStack(stack); + expect(() => inspect.hasCondition('*', { 'Fn::Equals': ['Bar', 'Baz'] })).not.toThrow(); + }); + + test('not matching', (done) => { + const stack = new Stack(); + new CfnCondition(stack, 'Foo', { + expression: Fn.conditionEquals('Bar', 'Baz'), + }); + + new CfnCondition(stack, 'Qux', { + expression: Fn.conditionNot(Fn.conditionEquals('Quux', 'Quuz')), + }); + + const inspect = Template.fromStack(stack); + expectToThrow( + () => inspect.hasCondition('*', { + 'Fn::Equals': ['Baz', 'Bar'], + }), + [ + /2 conditions/, + /Missing key/, + ], + done, + ); + done(); + }); + + test('matching specific outputName', () => { + const stack = new Stack(); + new CfnCondition(stack, 'Foo', { + expression: Fn.conditionEquals('Bar', 'Baz'), + }); + + const inspect = Template.fromStack(stack); + expect(() => inspect.hasCondition('Foo', { 'Fn::Equals': ['Bar', 'Baz'] })).not.toThrow(); + }); + + test('not matching specific outputName', (done) => { + const stack = new Stack(); + new CfnCondition(stack, 'Foo', { + expression: Fn.conditionEquals('Baz', 'Bar'), + }); + + const inspect = Template.fromStack(stack); + expectToThrow( + () => inspect.hasCondition('Foo', { + 'Fn::Equals': ['Bar', 'Baz'], + }), + [ + /1 conditions/, + /Expected Baz but received Bar/, + ], + done, + ); + done(); + }); + }); + describe('findConditions', () => { test('matching', () => { const stack = new Stack(); @@ -1014,7 +1080,7 @@ describe('Template', () => { }); const inspect = Template.fromStack(stack); - const result = inspect.findConditions('Foo', { 'Fn::Equals': { 'Bar': 'Qux' } }); + const result = inspect.findConditions('Foo', { 'Fn::Equals': ['Bar', 'Qux'] }); expect(Object.keys(result).length).toEqual(0); }); });