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/private/conditions.ts b/packages/@aws-cdk/assertions/lib/private/conditions.ts new file mode 100644 index 0000000000000..e7c4665dee219 --- /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 conditions 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..8875a91d0ac9c 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 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. + */ + 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..92bdb405ab9ce 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,150 @@ describe('Template', () => { expect(Object.keys(result).length).toEqual(0); }); }); + + 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(); + 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('Foo', { 'Fn::Equals': ['Bar', 'Qux'] }); + expect(Object.keys(result).length).toEqual(0); + }); + }); }); function expectToThrow(fn: () => void, msgs: (RegExp | string)[], done: jest.DoneCallback): void {