Skip to content

Commit

Permalink
Merge branch 'master' into downgrade-kubectl
Browse files Browse the repository at this point in the history
  • Loading branch information
mergify[bot] authored Jun 28, 2021
2 parents 2826425 + 3a2f642 commit 29cc3a1
Show file tree
Hide file tree
Showing 32 changed files with 1,218 additions and 301 deletions.
124 changes: 114 additions & 10 deletions packages/@aws-cdk/assertions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,34 +78,138 @@ assert.hasResourceProperties('Foo::Bar', {
});
```

The same method allows asserting the complete definition of the 'Resource'
which can be used to verify things other sections like `DependsOn`, `Metadata`,
`DeletionProperty`, etc.
Alternatively, if you would like to assert the entire resource definition, you
can use the `hasResource()` API.

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

By default, the `hasResource()` and `hasResourceProperties()` APIs perform deep
partial object matching. This behavior can be configured using matchers.
See subsequent section on [special matchers](#special-matchers).

## Special Matchers

The expectation provided to the `hasResourceXXX()` methods, besides carrying
literal values, as seen in the above examples, can also have special matchers
encoded.
They are available as part of the `Matchers` class and can be used as follows -
They are available as part of the `Match` class.

### Object Matchers

The `Match.objectLike()` API can be used to assert that the target is a superset
object of the provided pattern.
This API will perform a deep partial match on the target.
Deep partial matching is where objects are matched partially recursively. At each
level, the list of keys in the target is a subset of the provided pattern.

```ts
// Given a template -
// {
// "Resources": {
// "MyBar": {
// "Type": "Foo::Bar",
// "Properties": {
// "Fred": {
// "Wobble": "Flob",
// "Bob": "Cat"
// }
// }
// }
// }
// }

// The following will NOT throw an assertion error
assert.hasResourceProperties('Foo::Bar', {
Foo: 'Bar',
Baz: Match.absentProperty(),
})
Fred: Match.objectLike({
Wobble: 'Flob',
}),
});

// The following will throw an assertion error
assert.hasResourceProperties('Foo::Bar', {
Fred: Match.objectLike({
Brew: 'Coffee',
})
});
```

The `Match.objectEquals()` API can be used to assert a target as a deep exact
match.

In addition, the `Match.absentProperty()` can be used to specify that a specific
property should not exist on the target. This can be used within `Match.objectLike()`
or outside of any matchers.

```ts
// Given a template -
// {
// "Resources": {
// "MyBar": {
// "Type": "Foo::Bar",
// "Properties": {
// "Fred": {
// "Wobble": "Flob",
// }
// }
// }
// }
// }

// The following will NOT throw an assertion error
assert.hasResourceProperties('Foo::Bar', {
Fred: Match.objectLike({
Bob: Match.absentProperty(),
}),
});

// The following will throw an assertion error
assert.hasResourceProperties('Foo::Bar', {
Fred: Match.objectLike({
Wobble: Match.absentProperty(),
})
});
```

### Array Matchers

The `Match.arrayWith()` API can be used to assert that the target is equal to or a subset
of the provided pattern array.
This API will perform subset match on the target.

```ts
// Given a template -
// {
// "Resources": {
// "MyBar": {
// "Type": "Foo::Bar",
// "Properties": {
// "Fred": ["Flob", "Cat"]
// }
// }
// }
// }

// The following will NOT throw an assertion error
assert.hasResourceProperties('Foo::Bar', {
Fred: Match.arrayWith(['Flob']),
});

// The following will throw an assertion error
assert.hasResourceProperties('Foo::Bar', Match.objectLike({
Fred: Match.arrayWith(['Wobble']);
}});
```
The list of available matchers are -
*Note:* The list of items in the pattern array should be in order as they appear in the
target array. Out of order will be recorded as a match failure.
* `absentProperty()`: Specifies that this key must not be present.
Alternatively, the `Match.arrayEquals()` API can be used to assert that the target is
exactly equal to the pattern array.
## Strongly typed languages
Expand Down
20 changes: 15 additions & 5 deletions packages/@aws-cdk/assertions/lib/assertions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { Stack, Stage } from '@aws-cdk/core';
import { hasResource } from './has-resource';
import { Match } from './match';
import { Matcher } from './matcher';
import * as assert from './vendored/assert';

/**
Expand Down Expand Up @@ -53,23 +56,30 @@ export class TemplateAssertions {
/**
* Assert that a resource of the given type and properties exists in the
* 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 hasResourceProperties(type: string, props: any): void {
const assertion = assert.haveResource(type, props, assert.ResourcePart.Properties);
assertion.assertOrThrow(this.inspector);
this.hasResource(type, Match.objectLike({
Properties: Matcher.isMatcher(props) ? props : Match.objectLike(props),
}));
}

/**
* Assert that a resource of the given type and given definition 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 type the resource type; ex: `AWS::S3::Bucket`
* @param props the entire defintion of the resource as should be expected in the template.
*/
public hasResourceDefinition(type: string, props: any): void {
const assertion = assert.haveResource(type, props, assert.ResourcePart.CompleteDefinition);
assertion.assertOrThrow(this.inspector);
public hasResource(type: string, props: any): void {
const matchError = hasResource(this.inspector, type, props);
if (matchError) {
throw new Error(matchError);
}
}

/**
Expand Down
52 changes: 52 additions & 0 deletions packages/@aws-cdk/assertions/lib/has-resource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Match } from './match';
import { Matcher, MatchResult } from './matcher';
import { StackInspector } from './vendored/assert';

export function hasResource(inspector: StackInspector, type: string, props: any): string | void {
const matcher = Matcher.isMatcher(props) ? props : Match.objectLike(props);
let closestResult: MatchResult | undefined = undefined;
let closestResource: { [key: string]: any } | undefined = undefined;
let count: number = 0;

for (const logicalId of Object.keys(inspector.value.Resources ?? {})) {
const resource: { [key: string]: any } = inspector.value.Resources[logicalId];
if (resource.Type === type) {
count++;
const result = matcher.test(resource);
if (!result.hasFailed()) {
return;
}
if (closestResult === undefined || closestResult.failCount > result.failCount) {
closestResult = result;
closestResource = resource;
}
}
}

if (closestResult === undefined) {
return `No resource with type ${type} found`;
}

if (!closestResource) {
throw new Error('unexpected: closestResult is null');
}

return [
`${count} resources with type ${type} found, but none match as expected.`,
formatMessage(closestResult, closestResource),
].join('\n');
}

function formatMessage(closestResult: MatchResult, closestResource: {}): string {
return [
'The closest result is:',
reindent(JSON.stringify(closestResource, undefined, 2)),
'with the following mismatches:',
...closestResult.toHumanStrings().map(s => `\t${s}`),
].join('\n');
}

function reindent(x: string, indent: number = 2): string {
const pad = ' '.repeat(indent);
return pad + x.split('\n').join(`\n${pad}`);
}
3 changes: 2 additions & 1 deletion packages/@aws-cdk/assertions/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './assertions';
export * from './match';
export * from './match';
export * from './matcher';
Loading

0 comments on commit 29cc3a1

Please sign in to comment.