Skip to content

Commit 80cb527

Browse files
authored
feat(assertions): add function for verifying the number of matching resource properties (#21707)
This PR adds the `Template.resourcePropertiesCountIs()` method for counting the number of resources of a specified type whose `Properties` section matches given properties. Implements: #21706 ---- ### All Submissions: * [X] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) ### Adding new Unconventional Dependencies: * [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md/#adding-new-unconventional-dependencies) ### New Features * [ ] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)? * [ ] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)? *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 5a3db2d commit 80cb527

File tree

4 files changed

+137
-3
lines changed

4 files changed

+137
-3
lines changed

packages/@aws-cdk/assertions/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,17 @@ in a template.
7676
template.resourceCountIs('Foo::Bar', 2);
7777
```
7878

79+
You can also count the number of resources of a specific type whose `Properties`
80+
section contains the specified properties:
81+
82+
```ts
83+
template.resourcePropertiesCountIs('Foo::Bar', {
84+
Foo: 'Bar',
85+
Baz: 5,
86+
Qux: [ 'Waldo', 'Fred' ],
87+
}, 1);
88+
```
89+
7990
## Resource Matching & Retrieval
8091

8192
Beyond resource counting, the module also allows asserting that a resource with

packages/@aws-cdk/assertions/lib/private/resources.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,12 @@ export function hasResource(template: Template, type: string, props: any): strin
3232
}
3333

3434
export function hasResourceProperties(template: Template, type: string, props: any): string | void {
35-
// amended needs to be a deep copy to avoid modifying the template.
36-
let amended = JSON.parse(JSON.stringify(template));
35+
let amended = template;
3736

3837
// special case to exclude AbsentMatch because adding an empty Properties object will affect its evaluation.
3938
if (!Matcher.isMatcher(props) || !(props instanceof AbsentMatch)) {
39+
// amended needs to be a deep copy to avoid modifying the template.
40+
amended = JSON.parse(JSON.stringify(template));
4041
amended = addEmptyProperties(amended);
4142
}
4243

@@ -52,6 +53,27 @@ export function countResources(template: Template, type: string): number {
5253
return Object.entries(types).length;
5354
}
5455

56+
export function countResourcesProperties(template: Template, type: string, props: any): number {
57+
let amended = template;
58+
59+
// special case to exclude AbsentMatch because adding an empty Properties object will affect its evaluation.
60+
if (!Matcher.isMatcher(props) || !(props instanceof AbsentMatch)) {
61+
// amended needs to be a deep copy to avoid modifying the template.
62+
amended = JSON.parse(JSON.stringify(template));
63+
amended = addEmptyProperties(amended);
64+
}
65+
66+
const section = amended.Resources ?? {};
67+
const result = matchSection(filterType(section, type), Match.objectLike({
68+
Properties: props,
69+
}));
70+
71+
if (result.match) {
72+
return Object.keys(result.matches).length;
73+
}
74+
return 0;
75+
}
76+
5577
function addEmptyProperties(template: Template): Template {
5678
let section = template.Resources ?? {};
5779

packages/@aws-cdk/assertions/lib/template.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { checkTemplateForCyclicDependencies } from './private/cyclic';
88
import { findMappings, hasMapping } from './private/mappings';
99
import { findOutputs, hasOutput } from './private/outputs';
1010
import { findParameters, hasParameter } from './private/parameters';
11-
import { countResources, findResources, hasResource, hasResourceProperties } from './private/resources';
11+
import { countResources, countResourcesProperties, findResources, hasResource, hasResourceProperties } from './private/resources';
1212
import { Template as TemplateType } from './private/template';
1313

1414
/**
@@ -71,6 +71,20 @@ export class Template {
7171
}
7272
}
7373

74+
/**
75+
* Assert that the given number of resources of the given type and properties exists in the
76+
* CloudFormation template.
77+
* @param type the resource type; ex: `AWS::S3::Bucket`
78+
* @param props the 'Properties' section of the resource as should be expected in the template.
79+
* @param count number of expected instances
80+
*/
81+
public resourcePropertiesCountIs(type: string, props: any, count: number): void {
82+
const counted = countResourcesProperties(this.template, type, props);
83+
if (counted !== count) {
84+
throw new Error(`Expected ${count} resources of type ${type} but found ${counted}`);
85+
}
86+
}
87+
7488
/**
7589
* Assert that a resource of the given type and properties exists in the
7690
* CloudFormation template.

packages/@aws-cdk/assertions/test/template.test.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,93 @@ describe('Template', () => {
124124
});
125125
});
126126

127+
describe('resourcePropertiesCountIs', () => {
128+
test('resource exists', () => {
129+
const stack = new Stack();
130+
new CfnResource(stack, 'Resource', {
131+
type: 'Foo::Bar',
132+
properties: { baz: 'qux' },
133+
});
134+
135+
const inspect = Template.fromStack(stack);
136+
inspect.resourcePropertiesCountIs('Foo::Bar', { baz: 'qux' }, 1);
137+
138+
expect(() => {
139+
inspect.resourcePropertiesCountIs('Foo::Bar', { baz: 'qux' }, 0);
140+
}).toThrow('Expected 0 resources of type Foo::Bar but found 1');
141+
expect(() => {
142+
inspect.resourcePropertiesCountIs('Foo::Bar', { baz: 'qux' }, 2);
143+
}).toThrow('Expected 2 resources of type Foo::Bar but found 1');
144+
expect(() => {
145+
inspect.resourcePropertiesCountIs('Foo::Bar', { baz: 'nope' }, 1);
146+
}).toThrow('Expected 1 resources of type Foo::Bar but found 0');
147+
expect(() => {
148+
inspect.resourcePropertiesCountIs('Foo::Baz', { baz: 'qux' }, 1);
149+
}).toThrow('Expected 1 resources of type Foo::Baz but found 0');
150+
});
151+
test('no resource', () => {
152+
const stack = new Stack();
153+
154+
const inspect = Template.fromStack(stack);
155+
inspect.resourcePropertiesCountIs('Foo::Bar', { baz: 'qux' }, 0);
156+
157+
expect(() => {
158+
inspect.resourcePropertiesCountIs('Foo::Bar', { baz: 'qux' }, 1);
159+
}).toThrow('Expected 1 resources of type Foo::Bar but found 0');
160+
});
161+
test('absent - with properties', () => {
162+
const stack = new Stack();
163+
new CfnResource(stack, 'Foo', {
164+
type: 'Foo::Bar',
165+
properties: { baz: 'qux' },
166+
});
167+
168+
const inspect = Template.fromStack(stack);
169+
inspect.resourcePropertiesCountIs('Foo::Bar', {
170+
bar: Match.absent(),
171+
}, 1);
172+
inspect.resourcePropertiesCountIs('Foo::Bar', {
173+
baz: Match.absent(),
174+
}, 0);
175+
});
176+
test('absent - no properties', () => {
177+
const stack = new Stack();
178+
new CfnResource(stack, 'Foo', {
179+
type: 'Foo::Bar',
180+
});
181+
182+
const inspect = Template.fromStack(stack);
183+
inspect.resourcePropertiesCountIs('Foo::Bar', {
184+
bar: Match.absent(),
185+
baz: 'qux',
186+
}, 0);
187+
inspect.resourcePropertiesCountIs('Foo::Bar', Match.absent(), 1);
188+
});
189+
test('not - with properties', () => {
190+
const stack = new Stack();
191+
new CfnResource(stack, 'Foo', {
192+
type: 'Foo::Bar',
193+
properties: { baz: 'qux' },
194+
});
195+
196+
const inspect = Template.fromStack(stack);
197+
inspect.resourcePropertiesCountIs('Foo::Bar', Match.not({
198+
baz: 'boo',
199+
}), 1);
200+
});
201+
test('not - no properties', () => {
202+
const stack = new Stack();
203+
new CfnResource(stack, 'Foo', {
204+
type: 'Foo::Bar',
205+
});
206+
207+
const inspect = Template.fromStack(stack);
208+
inspect.resourcePropertiesCountIs('Foo::Bar', Match.not({
209+
baz: 'qux',
210+
}), 1);
211+
});
212+
});
213+
127214
describe('templateMatches', () => {
128215
test('matches', () => {
129216
const app = new App();

0 commit comments

Comments
 (0)