Skip to content

Commit

Permalink
feat(assertions): matcher support for templateMatches() API (#16789)
Browse files Browse the repository at this point in the history
The `templateMatches()` API behaved differently from the rest of the
`hasXxx()` and `findXxx()` APIs in that it did not accept a Matcher.
This functionality is generally useful to perform partial matching on
the full template.

Further, users can get confused and assume that the `templateMatches()`
API do support Matchers, as this is the only one that is an exception.

Align this API with the rest of the module's behaviour.

A nice side effect of this is that this module no longer needs to vendor
in changes from the 'assert' module and brings this in line with the
other modules in this repo.

nozem can work again! 🙌

BREAKING CHANGE: The `templateMatches()` API previously performed
an exact match. The default behavior has been updated to be
"object-like".

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
Niranjan Jayakar authored and njlynch committed Oct 11, 2021
1 parent 0c1ef51 commit 6ec9e90
Show file tree
Hide file tree
Showing 11 changed files with 92 additions and 209 deletions.
6 changes: 3 additions & 3 deletions packages/@aws-cdk/assertions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,9 @@ The APIs `hasMapping()` and `findMappings()` provide similar functionalities.

## Special Matchers

The expectation provided to the `hasXXX()` and `findXXX()` methods, besides
carrying literal values, as seen in the above examples, also accept special
matchers.
The expectation provided to the `hasXxx()`, `findXxx()` and `templateMatches()`
APIs, besides carrying literal values, as seen in the above examples, also accept
special matchers.

They are available as part of the `Match` class.

Expand Down
3 changes: 2 additions & 1 deletion packages/@aws-cdk/assertions/lib/match.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Matcher, MatchResult } from './matcher';
import { getType } from './private/type';
import { ABSENT } from './vendored/assert';

const ABSENT = '{{ABSENT}}';

/**
* Partial and special matching during template assertions.
Expand Down
10 changes: 5 additions & 5 deletions packages/@aws-cdk/assertions/lib/private/mappings.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { StackInspector } from '../vendored/assert';
import { filterLogicalId, formatFailure, matchSection } from './section';
import { Template } from './template';

export function findMappings(inspector: StackInspector, logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } {
const section: { [key: string] : {} } = inspector.value.Mappings;
export function findMappings(template: Template, logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } {
const section: { [key: string] : {} } = template.Mappings;
const result = matchSection(filterLogicalId(section, logicalId), props);

if (!result.match) {
Expand All @@ -12,8 +12,8 @@ export function findMappings(inspector: StackInspector, logicalId: string, props
return result.matches;
}

export function hasMapping(inspector: StackInspector, logicalId: string, props: any): string | void {
const section: { [key: string]: {} } = inspector.value.Mappings;
export function hasMapping(template: Template, logicalId: string, props: any): string | void {
const section: { [key: string]: {} } = template.Mappings;
const result = matchSection(filterLogicalId(section, logicalId), props);

if (result.match) {
Expand Down
10 changes: 5 additions & 5 deletions packages/@aws-cdk/assertions/lib/private/outputs.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { StackInspector } from '../vendored/assert';
import { filterLogicalId, formatFailure, matchSection } from './section';
import { Template } from './template';

export function findOutputs(inspector: StackInspector, logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } {
const section: { [key: string] : {} } = inspector.value.Outputs;
export function findOutputs(template: Template, logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } {
const section = template.Outputs;
const result = matchSection(filterLogicalId(section, logicalId), props);

if (!result.match) {
Expand All @@ -12,8 +12,8 @@ export function findOutputs(inspector: StackInspector, logicalId: string, props:
return result.matches;
}

export function hasOutput(inspector: StackInspector, logicalId: string, props: any): string | void {
const section: { [key: string]: {} } = inspector.value.Outputs;
export function hasOutput(template: Template, logicalId: string, props: any): string | void {
const section: { [key: string]: {} } = template.Outputs;
const result = matchSection(filterLogicalId(section, logicalId), props);
if (result.match) {
return;
Expand Down
22 changes: 12 additions & 10 deletions packages/@aws-cdk/assertions/lib/private/resources.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import { StackInspector } from '../vendored/assert';
import { formatFailure, matchSection } from './section';
import { Resource, Template } from './template';

// Partial type for CloudFormation Resource
type Resource = {
Type: string;
}

export function findResources(inspector: StackInspector, type: string, props: any = {}): { [key: string]: { [key: string]: any } } {
const section: { [key: string] : Resource } = inspector.value.Resources;
export function findResources(template: Template, type: string, props: any = {}): { [key: string]: { [key: string]: any } } {
const section = template.Resources;
const result = matchSection(filterType(section, type), props);

if (!result.match) {
Expand All @@ -17,8 +12,8 @@ export function findResources(inspector: StackInspector, type: string, props: an
return result.matches;
}

export function hasResource(inspector: StackInspector, type: string, props: any): string | void {
const section: { [key: string]: Resource } = inspector.value.Resources;
export function hasResource(template: Template, type: string, props: any): string | void {
const section = template.Resources;
const result = matchSection(filterType(section, type), props);

if (result.match) {
Expand All @@ -35,6 +30,13 @@ export function hasResource(inspector: StackInspector, type: string, props: any)
].join('\n');
}

export function countResources(template: Template, type: string): number {
const section = template.Resources;
const types = filterType(section, type);

return Object.entries(types).length;
}

function filterType(section: { [key: string]: Resource }, type: string): { [key: string]: Resource } {
return Object.entries(section ?? {})
.filter(([_, v]) => v.Type === type)
Expand Down
16 changes: 16 additions & 0 deletions packages/@aws-cdk/assertions/lib/private/template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Partial types for CloudFormation Template

export type Template = {
Resources: { [logicalId: string]: Resource },
Outputs: { [logicalId: string]: Output },
Mappings: { [logicalId: string]: Mapping }
}

export type Resource = {
Type: string;
[key: string]: any;
}

export type Output = { [key: string]: any };

export type Mapping = { [key: string]: any };
41 changes: 24 additions & 17 deletions packages/@aws-cdk/assertions/lib/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { Match } from './match';
import { Matcher } from './matcher';
import { findMappings, hasMapping } from './private/mappings';
import { findOutputs, hasOutput } from './private/outputs';
import { findResources, hasResource } from './private/resources';
import * as assert from './vendored/assert';
import { countResources, findResources, hasResource } from './private/resources';
import { Template as TemplateType } from './private/template';

/**
* Suite of assertions that can be run on a CDK stack.
Expand Down Expand Up @@ -39,12 +39,10 @@ export class Template {
return new Template(JSON.parse(template));
}

private readonly template: { [key: string]: any };
private readonly inspector: assert.StackInspector;
private readonly template: TemplateType;

private constructor(template: { [key: string]: any }) {
this.template = template;
this.inspector = new assert.StackInspector(template);
this.template = template as TemplateType;
}

/**
Expand All @@ -61,8 +59,10 @@ export class Template {
* @param count number of expected instances
*/
public resourceCountIs(type: string, count: number): void {
const assertion = assert.countResources(type, count);
assertion.assertOrThrow(this.inspector);
const counted = countResources(this.template, type);
if (counted !== count) {
throw new Error(`Expected ${count} resources of type ${type} but found ${counted}`);
}
}

/**
Expand All @@ -88,7 +88,7 @@ export class Template {
* @param props the entire defintion of the resource as should be expected in the template.
*/
public hasResource(type: string, props: any): void {
const matchError = hasResource(this.inspector, type, props);
const matchError = hasResource(this.template, type, props);
if (matchError) {
throw new Error(matchError);
}
Expand All @@ -102,7 +102,7 @@ export class Template {
* Use the `Match` APIs to configure a different behaviour.
*/
public findResources(type: string, props: any = {}): { [key: string]: { [key: string]: any } } {
return findResources(this.inspector, type, props);
return findResources(this.template, type, props);
}

/**
Expand All @@ -113,7 +113,7 @@ export class Template {
* @param props the output as should be expected in the template.
*/
public hasOutput(logicalId: string, props: any): void {
const matchError = hasOutput(this.inspector, logicalId, props);
const matchError = hasOutput(this.template, logicalId, props);
if (matchError) {
throw new Error(matchError);
}
Expand All @@ -127,7 +127,7 @@ export class Template {
* Use the `Match` APIs to configure a different behaviour.
*/
public findOutputs(logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } {
return findOutputs(this.inspector, logicalId, props);
return findOutputs(this.template, logicalId, props);
}

/**
Expand All @@ -138,7 +138,7 @@ export class Template {
* @param props the output as should be expected in the template.
*/
public hasMapping(logicalId: string, props: any): void {
const matchError = hasMapping(this.inspector, logicalId, props);
const matchError = hasMapping(this.template, logicalId, props);
if (matchError) {
throw new Error(matchError);
}
Expand All @@ -152,16 +152,23 @@ export class Template {
* Use the `Match` APIs to configure a different behaviour.
*/
public findMappings(logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } {
return findMappings(this.inspector, logicalId, props);
return findMappings(this.template, logicalId, props);
}

/**
* Assert that the CloudFormation template matches the given value
* @param expected the expected CloudFormation template as key-value pairs.
*/
public templateMatches(expected: {[key: string]: any}): void {
const assertion = assert.matchTemplate(expected);
assertion.assertOrThrow(this.inspector);
public templateMatches(expected: any): void {
const matcher = Matcher.isMatcher(expected) ? expected : Match.objectLike(expected);
const result = matcher.test(this.template);

if (result.hasFailed()) {
throw new Error([
'Template did not match as expected. The following mismatches were found:',
...result.toHumanStrings().map(s => `\t${s}`),
].join('\n'));
}
}
}

Expand Down
26 changes: 0 additions & 26 deletions packages/@aws-cdk/assertions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,6 @@
},
"projectReferences": true
},
"cdk-build": {
"pre": [
"./vendor-in.sh"
]
},
"author": {
"name": "Amazon Web Services",
"url": "https://aws.amazon.com",
Expand All @@ -61,7 +56,6 @@
"license": "Apache-2.0",
"devDependencies": {
"@aws-cdk/cdk-build-tools": "0.0.0",
"@aws-cdk/cfnspec": "0.0.0",
"@aws-cdk/pkglint": "0.0.0",
"@types/jest": "^26.0.24",
"constructs": "^3.3.69",
Expand Down Expand Up @@ -106,26 +100,6 @@
"engines": {
"node": ">= 10.13.0 <13 || >=13.7.0"
},
"nozem": {
"ostools": [
"dirname",
"cd",
"bash",
"rm",
"xargs",
"sed",
"mkdir",
"rsync",
"cat",
"find"
],
"additionalDirs": [
"../cfnspec/lib",
"ARTIFACTS:../cfnspec/spec",
"../cloudformation-diff",
"../assert-internal"
]
},
"stability": "experimental",
"maturity": "experimental",
"publishConfig": {
Expand Down
10 changes: 5 additions & 5 deletions packages/@aws-cdk/assertions/test/template.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,10 @@ describe('Template', () => {
const inspect = Template.fromStack(stack);
inspect.resourceCountIs('Foo::Bar', 1);

expect(() => inspect.resourceCountIs('Foo::Bar', 0)).toThrow(/has 1 resource of type Foo::Bar/);
expect(() => inspect.resourceCountIs('Foo::Bar', 2)).toThrow(/has 1 resource of type Foo::Bar/);
expect(() => inspect.resourceCountIs('Foo::Bar', 0)).toThrow('Expected 0 resources of type Foo::Bar but found 1');
expect(() => inspect.resourceCountIs('Foo::Bar', 2)).toThrow('Expected 2 resources of type Foo::Bar but found 1');

expect(() => inspect.resourceCountIs('Foo::Baz', 1)).toThrow(/has 0 resource of type Foo::Baz/);
expect(() => inspect.resourceCountIs('Foo::Baz', 1)).toThrow('Expected 1 resources of type Foo::Baz but found 0');
});

test('no resource', () => {
Expand All @@ -94,7 +94,7 @@ describe('Template', () => {
const inspect = Template.fromStack(stack);
inspect.resourceCountIs('Foo::Bar', 0);

expect(() => inspect.resourceCountIs('Foo::Bar', 1)).toThrow(/has 0 resource of type Foo::Bar/);
expect(() => inspect.resourceCountIs('Foo::Bar', 1)).toThrow('Expected 1 resources of type Foo::Bar but found 0');
});
});

Expand Down Expand Up @@ -132,7 +132,7 @@ describe('Template', () => {
Properties: { baz: 'waldo' },
},
},
})).toThrowError();
})).toThrowError(/Expected waldo but received qux at \/Resources\/Foo\/Properties\/baz/);
});
});

Expand Down
57 changes: 0 additions & 57 deletions packages/@aws-cdk/assertions/vendor-in.sh

This file was deleted.

Loading

0 comments on commit 6ec9e90

Please sign in to comment.