Skip to content

Commit

Permalink
Merge branch 'eksMergeMain' into eksIntegTest
Browse files Browse the repository at this point in the history
  • Loading branch information
comcalvi committed Nov 2, 2022
2 parents 1ba03ae + f9498b4 commit af88f83
Show file tree
Hide file tree
Showing 5 changed files with 276 additions and 14 deletions.
25 changes: 23 additions & 2 deletions packages/@aws-cdk/assertions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,18 @@ The following code asserts that the `Properties` section of a resource of type

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

You can also assert that the `Properties` section of all resources of type
`Foo::Bar` contains the specified properties -

```ts
template.allResourcesProperties('Foo::Bar', {
Lorem: 'Ipsum',
Baz: 5,
Qux: [ 'Waldo', 'Fred' ],
});
Expand All @@ -113,7 +124,17 @@ can use the `hasResource()` API.

```ts
template.hasResource('Foo::Bar', {
Properties: { Foo: 'Bar' },
Properties: { Lorem: 'Ipsum' },
DependsOn: [ 'Waldo', 'Fred' ],
});
```

You can also assert the definitions of all resources of a type using the
`allResources()` API.

```ts
template.allResources('Foo::Bar', {
Properties: { Lorem: 'Ipsum' },
DependsOn: [ 'Waldo', 'Fred' ],
});
```
Expand Down
38 changes: 37 additions & 1 deletion packages/@aws-cdk/assertions/lib/private/resources.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Match, Matcher } from '..';
import { AbsentMatch } from './matchers/absent';
import { formatFailure, matchSection } from './section';
import { formatAllMismatches, formatFailure, matchSection } from './section';
import { Resource, Template } from './template';

export function findResources(template: Template, type: string, props: any = {}): { [key: string]: { [key: string]: any } } {
Expand All @@ -14,6 +14,42 @@ export function findResources(template: Template, type: string, props: any = {})
return result.matches;
}

export function allResources(template: Template, type: string, props: any): string | void {
const section = template.Resources ?? {};
const result = matchSection(filterType(section, type), props);
if (result.match) {
const matchCount = Object.keys(result.matches).length;
if (result.analyzedCount > matchCount) {
return [
`Template has ${result.analyzedCount} resource(s) with type ${type}, but only ${matchCount} match as expected.`,
formatAllMismatches(result.analyzed, result.matches),
].join('\n');
}
} else {
return [
`Template has ${result.analyzedCount} resource(s) with type ${type}, but none match as expected.`,
formatAllMismatches(result.analyzed),
].join('\n');
}
}

export function allResourcesProperties(template: Template, type: string, props: any): string | void {
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);
}

return allResources(amended, type, Match.objectLike({
Properties: props,
}));

}


export function hasResource(template: Template, type: string, props: any): string | void {
const section = template.Resources ?? {};
const result = matchSection(filterType(section, type), props);
Expand Down
25 changes: 16 additions & 9 deletions packages/@aws-cdk/assertions/lib/private/section.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,61 @@
import { Match } from '../match';
import { Matcher, MatchResult } from '../matcher';

export type MatchSuccess = { match: true, matches: {[key: string]: any} };
export type MatchFailure = { match: false, closestResult?: MatchResult, analyzedCount: number };
export type MatchSuccess = { match: true, matches: { [key: string]: any }, analyzed: { [key: string]: any }, analyzedCount: number };
export type MatchFailure = { match: false, closestResult?: MatchResult, analyzed: { [key: string]: any }, analyzedCount: number };

export function matchSection(section: any, props: any): MatchSuccess | MatchFailure {
const matcher = Matcher.isMatcher(props) ? props : Match.objectLike(props);
let closestResult: MatchResult | undefined = undefined;
let matching: {[key: string]: any} = {};
let count = 0;
let matching: { [key: string]: any } = {};
let analyzed: { [key: string]: any } = {};

eachEntryInSection(
section,

(logicalId, entry) => {
analyzed[logicalId] = entry;
const result = matcher.test(entry);
result.finished();
if (!result.hasFailed()) {
matching[logicalId] = entry;
} else {
count++;
if (closestResult === undefined || closestResult.failCount > result.failCount) {
closestResult = result;
}
}
},
);
if (Object.keys(matching).length > 0) {
return { match: true, matches: matching };
return { match: true, matches: matching, analyzedCount: Object.keys(analyzed).length, analyzed: analyzed };
} else {
return { match: false, closestResult, analyzedCount: count };
return { match: false, closestResult, analyzedCount: Object.keys(analyzed).length, analyzed: analyzed };
}
}

function eachEntryInSection(
section: any,
cb: (logicalId: string, entry: {[key: string]: any}) => void): void {
cb: (logicalId: string, entry: { [key: string]: any }) => void): void {

for (const logicalId of Object.keys(section ?? {})) {
const resource: { [key: string]: any } = section[logicalId];
cb(logicalId, resource);
}
}

export function formatAllMatches(matches: {[key: string]: any}): string {
export function formatAllMatches(matches: { [key: string]: any }): string {
return [
leftPad(JSON.stringify(matches, undefined, 2)),
].join('\n');
}

export function formatAllMismatches(analyzed: { [key: string]: any }, matches: { [key: string]: any } = {}): string {
return [
'The following resources do not match the given definition:',
...Object.keys(analyzed).filter(id => !(id in matches)).map(id => `\t${id}`),
].join('\n');
}

export function formatFailure(closestResult: MatchResult): string {
return [
'The closest result is:',
Expand Down
34 changes: 32 additions & 2 deletions 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, countResourcesProperties, findResources, hasResource, hasResourceProperties } from './private/resources';
import { allResources, allResourcesProperties, countResources, countResourcesProperties, findResources, hasResource, hasResourceProperties } from './private/resources';
import { Template as TemplateType } from './private/template';

/**
Expand Down Expand Up @@ -114,7 +114,7 @@ export class 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 type the resource type; ex: `AWS::S3::Bucket`
* @param props the entire defintion of the resource as should be expected in the template.
* @param props the entire definition of the resource as should be expected in the template.
*/
public hasResource(type: string, props: any): void {
const matchError = hasResource(this.template, type, props);
Expand All @@ -134,6 +134,36 @@ export class Template {
return findResources(this.template, type, props);
}

/**
* Assert that all resources of the given type contain the given definition 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 type the resource type; ex: `AWS::S3::Bucket`
* @param props the entire definition of the resources as they should be expected in the template.
*/
public allResources(type: string, props: any): void {
const matchError = allResources(this.template, type, props);
if (matchError) {
throw new Error(matchError);
}
}

/**
* Assert that all resources of the given type contain the given properties
* CloudFormation template.
* By default, performs partial matching on the `Properties` key of the resource, via the
* `Match.objectLike()`. To configure different behavour, use other matchers in the `Match` class.
* @param type the resource type; ex: `AWS::S3::Bucket`
* @param props the 'Properties' section of the resource as should be expected in the template.
*/
public allResourcesProperties(type: string, props: any): void {
const matchError = allResourcesProperties(this.template, type, props);
if (matchError) {
throw new Error(matchError);
}
}

/**
* Assert that a Parameter with the given properties exists in the CloudFormation template.
* By default, performs partial matching on the parameter, via the `Match.objectLike()`.
Expand Down
168 changes: 168 additions & 0 deletions packages/@aws-cdk/assertions/test/template.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,174 @@ describe('Template', () => {
});
});

describe('allResources', () => {
test('all resource of type match', () => {
const stack = new Stack();
const partialProps = { baz: 'qux', fred: 'waldo' };
new CfnResource(stack, 'Foo', {
type: 'Foo::Bar',
properties: { ...partialProps, lorem: 'ipsum' },
});
new CfnResource(stack, 'Foo2', {
type: 'Foo::Bar',
properties: partialProps,
});

const inspect = Template.fromStack(stack);
expect(inspect.allResources('Foo::Bar', { Properties: partialProps }));
});

test('no resources match', (done) => {
const stack = new Stack();
new CfnResource(stack, 'Foo', {
type: 'Foo::Bar',
properties: { lorem: 'ipsum' },
});
new CfnResource(stack, 'Foo2', {
type: 'Foo::Bar',
properties: { baz: 'qux' },
});

const inspect = Template.fromStack(stack);
expectToThrow(
() => inspect.allResources('Foo::Bar', { Properties: { fred: 'waldo' } }),
[
'Template has 2 resource(s) with type Foo::Bar, but none match as expected.',
'The following resources do not match the given definition:',
/Foo/,
/Foo2/,
],
done,
);
done();
});

test('some resources match', (done) => {
const stack = new Stack();
new CfnResource(stack, 'Foo', {
type: 'Foo::Bar',
properties: { lorem: 'ipsum' },
});
new CfnResource(stack, 'Foo2', {
type: 'Foo::Bar',
properties: { baz: 'qux' },
});

const inspect = Template.fromStack(stack);
expectToThrow(
() => inspect.allResources('Foo::Bar', { Properties: { lorem: 'ipsum' } }),
[
'Template has 2 resource(s) with type Foo::Bar, but only 1 match as expected.',
'The following resources do not match the given definition:',
/Foo2/,
],
done,
);
done();
});

test('using a "not" matcher ', () => {
const stack = new Stack();
new CfnResource(stack, 'Foo', {
type: 'Foo::Bar',
properties: { lorem: 'ipsum' },
});
new CfnResource(stack, 'Foo2', {
type: 'Foo::Bar',
properties: { baz: 'baz' },
});

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

describe('allResourcesProperties', () => {
test('all resource of type match', () => {
const stack = new Stack();
const partialProps = { baz: 'qux', fred: 'waldo' };
new CfnResource(stack, 'Foo', {
type: 'Foo::Bar',
properties: { ...partialProps, lorem: 'ipsum' },
});
new CfnResource(stack, 'Foo2', {
type: 'Foo::Bar',
properties: partialProps,
});

const inspect = Template.fromStack(stack);
expect(inspect.allResourcesProperties('Foo::Bar', partialProps));
});

test('no resources match', (done) => {
const stack = new Stack();
new CfnResource(stack, 'Foo', {
type: 'Foo::Bar',
properties: { lorem: 'ipsum' },
});
new CfnResource(stack, 'Foo2', {
type: 'Foo::Bar',
properties: { baz: 'qux' },
});
new CfnResource(stack, 'NotFoo', {
type: 'NotFoo::NotBar',
properties: { fred: 'waldo' },
});

const inspect = Template.fromStack(stack);
expectToThrow(
() => inspect.allResourcesProperties('Foo::Bar', { fred: 'waldo' }),
[
'Template has 2 resource(s) with type Foo::Bar, but none match as expected.',
'The following resources do not match the given definition:',
/Foo/,
/Foo2/,
],
done,
);
done();
});

test('some resources match', (done) => {
const stack = new Stack();
new CfnResource(stack, 'Foo', {
type: 'Foo::Bar',
properties: { lorem: 'ipsum' },
});
new CfnResource(stack, 'Foo2', {
type: 'Foo::Bar',
properties: { baz: 'qux' },
});

const inspect = Template.fromStack(stack);
expectToThrow(
() => inspect.allResourcesProperties('Foo::Bar', { lorem: 'ipsum' }),
[
'Template has 2 resource(s) with type Foo::Bar, but only 1 match as expected.',
'The following resources do not match the given definition:',
/Foo2/,
],
done,
);
done();
});

test('using a "not" matcher ', () => {
const stack = new Stack();
new CfnResource(stack, 'Foo', {
type: 'Foo::Bar',
properties: { lorem: 'ipsum' },
});
new CfnResource(stack, 'Foo2', {
type: 'Foo::Bar',
properties: { baz: 'baz' },
});

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

describe('hasOutput', () => {
test('matching', () => {
const stack = new Stack();
Expand Down

0 comments on commit af88f83

Please sign in to comment.