Skip to content

Commit

Permalink
feat(assertions): add function for verifying the number of matching r…
Browse files Browse the repository at this point in the history
…esource 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*
  • Loading branch information
ddneilson committed Sep 2, 2022
1 parent 5a3db2d commit 80cb527
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 3 deletions.
11 changes: 11 additions & 0 deletions packages/@aws-cdk/assertions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,17 @@ in a template.
template.resourceCountIs('Foo::Bar', 2);
```

You can also count the number of resources of a specific type whose `Properties`
section contains the specified properties:

```ts
template.resourcePropertiesCountIs('Foo::Bar', {
Foo: 'Bar',
Baz: 5,
Qux: [ 'Waldo', 'Fred' ],
}, 1);
```

## Resource Matching & Retrieval

Beyond resource counting, the module also allows asserting that a resource with
Expand Down
26 changes: 24 additions & 2 deletions packages/@aws-cdk/assertions/lib/private/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,12 @@ export function hasResource(template: Template, type: string, props: any): strin
}

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

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

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

export function countResourcesProperties(template: Template, type: string, props: any): number {
let amended = template;

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

const section = amended.Resources ?? {};
const result = matchSection(filterType(section, type), Match.objectLike({
Properties: props,
}));

if (result.match) {
return Object.keys(result.matches).length;
}
return 0;
}

function addEmptyProperties(template: Template): Template {
let section = template.Resources ?? {};

Expand Down
16 changes: 15 additions & 1 deletion packages/@aws-cdk/assertions/lib/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { checkTemplateForCyclicDependencies } from './private/cyclic';
import { findMappings, hasMapping } from './private/mappings';
import { findOutputs, hasOutput } from './private/outputs';
import { findParameters, hasParameter } from './private/parameters';
import { countResources, findResources, hasResource, hasResourceProperties } from './private/resources';
import { countResources, countResourcesProperties, findResources, hasResource, hasResourceProperties } from './private/resources';
import { Template as TemplateType } from './private/template';

/**
Expand Down Expand Up @@ -71,6 +71,20 @@ export class Template {
}
}

/**
* Assert that the given number of resources of the given type and properties exists in the
* CloudFormation template.
* @param type the resource type; ex: `AWS::S3::Bucket`
* @param props the 'Properties' section of the resource as should be expected in the template.
* @param count number of expected instances
*/
public resourcePropertiesCountIs(type: string, props: any, count: number): void {
const counted = countResourcesProperties(this.template, type, props);
if (counted !== count) {
throw new Error(`Expected ${count} resources of type ${type} but found ${counted}`);
}
}

/**
* Assert that a resource of the given type and properties exists in the
* CloudFormation template.
Expand Down
87 changes: 87 additions & 0 deletions packages/@aws-cdk/assertions/test/template.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,93 @@ describe('Template', () => {
});
});

describe('resourcePropertiesCountIs', () => {
test('resource exists', () => {
const stack = new Stack();
new CfnResource(stack, 'Resource', {
type: 'Foo::Bar',
properties: { baz: 'qux' },
});

const inspect = Template.fromStack(stack);
inspect.resourcePropertiesCountIs('Foo::Bar', { baz: 'qux' }, 1);

expect(() => {
inspect.resourcePropertiesCountIs('Foo::Bar', { baz: 'qux' }, 0);
}).toThrow('Expected 0 resources of type Foo::Bar but found 1');
expect(() => {
inspect.resourcePropertiesCountIs('Foo::Bar', { baz: 'qux' }, 2);
}).toThrow('Expected 2 resources of type Foo::Bar but found 1');
expect(() => {
inspect.resourcePropertiesCountIs('Foo::Bar', { baz: 'nope' }, 1);
}).toThrow('Expected 1 resources of type Foo::Bar but found 0');
expect(() => {
inspect.resourcePropertiesCountIs('Foo::Baz', { baz: 'qux' }, 1);
}).toThrow('Expected 1 resources of type Foo::Baz but found 0');
});
test('no resource', () => {
const stack = new Stack();

const inspect = Template.fromStack(stack);
inspect.resourcePropertiesCountIs('Foo::Bar', { baz: 'qux' }, 0);

expect(() => {
inspect.resourcePropertiesCountIs('Foo::Bar', { baz: 'qux' }, 1);
}).toThrow('Expected 1 resources of type Foo::Bar but found 0');
});
test('absent - with properties', () => {
const stack = new Stack();
new CfnResource(stack, 'Foo', {
type: 'Foo::Bar',
properties: { baz: 'qux' },
});

const inspect = Template.fromStack(stack);
inspect.resourcePropertiesCountIs('Foo::Bar', {
bar: Match.absent(),
}, 1);
inspect.resourcePropertiesCountIs('Foo::Bar', {
baz: Match.absent(),
}, 0);
});
test('absent - no properties', () => {
const stack = new Stack();
new CfnResource(stack, 'Foo', {
type: 'Foo::Bar',
});

const inspect = Template.fromStack(stack);
inspect.resourcePropertiesCountIs('Foo::Bar', {
bar: Match.absent(),
baz: 'qux',
}, 0);
inspect.resourcePropertiesCountIs('Foo::Bar', Match.absent(), 1);
});
test('not - with properties', () => {
const stack = new Stack();
new CfnResource(stack, 'Foo', {
type: 'Foo::Bar',
properties: { baz: 'qux' },
});

const inspect = Template.fromStack(stack);
inspect.resourcePropertiesCountIs('Foo::Bar', Match.not({
baz: 'boo',
}), 1);
});
test('not - no properties', () => {
const stack = new Stack();
new CfnResource(stack, 'Foo', {
type: 'Foo::Bar',
});

const inspect = Template.fromStack(stack);
inspect.resourcePropertiesCountIs('Foo::Bar', Match.not({
baz: 'qux',
}), 1);
});
});

describe('templateMatches', () => {
test('matches', () => {
const app = new App();
Expand Down

0 comments on commit 80cb527

Please sign in to comment.