Skip to content

Commit 7f6f3fd

Browse files
authored
feat(assert): haveResource lists failing properties (#1016)
Make the haveResource assertion tell you which fields failed matching. This helps in case of differences in hard-to-read fields, such as '10.0.0.0/16' vs '10.10.0.0/16'.
1 parent 5a6ad3c commit 7f6f3fd

File tree

2 files changed

+112
-20
lines changed

2 files changed

+112
-20
lines changed

packages/@aws-cdk/assert/lib/assertions/have-resource.ts

Lines changed: 61 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ export function haveResource(resourceType: string, properties?: any, comparison?
1313
return new HaveResourceAssertion(resourceType, properties, comparison);
1414
}
1515

16-
type PropertyPredicate = (props: any) => boolean;
16+
type PropertyPredicate = (props: any, inspection: InspectionFailure) => boolean;
1717

1818
class HaveResourceAssertion extends Assertion<StackInspector> {
19-
private inspected: any[] = [];
19+
private inspected: InspectionFailure[] = [];
2020
private readonly part: ResourcePart;
2121
private readonly predicate: PropertyPredicate;
2222

@@ -33,13 +33,17 @@ class HaveResourceAssertion extends Assertion<StackInspector> {
3333
for (const logicalId of Object.keys(inspector.value.Resources)) {
3434
const resource = inspector.value.Resources[logicalId];
3535
if (resource.Type === this.resourceType) {
36-
this.inspected.push(resource);
37-
3836
const propsToCheck = this.part === ResourcePart.Properties ? resource.Properties : resource;
3937

40-
if (this.predicate(propsToCheck)) {
38+
// Pass inspection object as 2nd argument, initialize failure with default string,
39+
// to maintain backwards compatibility with old predicate API.
40+
const inspection = { resource, failureReason: 'Object did not match predicate' };
41+
42+
if (this.predicate(propsToCheck, inspection)) {
4143
return true;
4244
}
45+
46+
this.inspected.push(inspection);
4347
}
4448
}
4549

@@ -48,7 +52,15 @@ class HaveResourceAssertion extends Assertion<StackInspector> {
4852

4953
public assertOrThrow(inspector: StackInspector) {
5054
if (!this.assertUsing(inspector)) {
51-
throw new Error(`None of ${JSON.stringify(this.inspected, null, 2)} match ${this.description}`);
55+
const lines: string[] = [];
56+
lines.push(`None of ${this.inspected.length} resources matches ${this.description}.`);
57+
58+
for (const inspected of this.inspected) {
59+
lines.push(`- ${inspected.failureReason} in:`);
60+
lines.push(indent(4, JSON.stringify(inspected.resource, null, 2)));
61+
}
62+
63+
throw new Error(lines.join('\n'));
5264
}
5365
}
5466

@@ -58,46 +70,75 @@ class HaveResourceAssertion extends Assertion<StackInspector> {
5870
}
5971
}
6072

73+
function indent(n: number, s: string) {
74+
const prefix = ' '.repeat(n);
75+
return prefix + s.replace(/\n/g, '\n' + prefix);
76+
}
77+
6178
/**
6279
* Make a predicate that checks property superset
6380
*/
6481
function makeSuperObjectPredicate(obj: any) {
65-
return (resourceProps: any) => {
66-
return isSuperObject(resourceProps, obj);
82+
return (resourceProps: any, inspection: InspectionFailure) => {
83+
const errors: string[] = [];
84+
const ret = isSuperObject(resourceProps, obj, errors);
85+
inspection.failureReason = errors.join(',');
86+
return ret;
6787
};
6888
}
6989

90+
interface InspectionFailure {
91+
resource: any;
92+
failureReason: string;
93+
}
94+
7095
/**
7196
* Return whether `superObj` is a super-object of `obj`.
7297
*
7398
* A super-object has the same or more property values, recursing into nested objects.
7499
*/
75-
export function isSuperObject(superObj: any, obj: any): boolean {
100+
export function isSuperObject(superObj: any, obj: any, errors: string[] = []): boolean {
76101
if (obj == null) { return true; }
77-
if (Array.isArray(superObj) !== Array.isArray(obj)) { return false; }
102+
if (Array.isArray(superObj) !== Array.isArray(obj)) {
103+
errors.push('Array type mismatch');
104+
return false;
105+
}
78106
if (Array.isArray(superObj)) {
79-
if (obj.length !== superObj.length) { return false; }
107+
if (obj.length !== superObj.length) {
108+
errors.push('Array length mismatch');
109+
return false;
110+
}
80111

81112
// Do isSuperObject comparison for individual objects
82113
for (let i = 0; i < obj.length; i++) {
83-
if (!isSuperObject(superObj[i], obj[i])) {
84-
return false;
114+
if (!isSuperObject(superObj[i], obj[i], [])) {
115+
errors.push(`Array element ${i} mismatch`);
85116
}
86117
}
87-
return true;
118+
return errors.length === 0;
119+
}
120+
if ((typeof superObj === 'object') !== (typeof obj === 'object')) {
121+
errors.push('Object type mismatch');
122+
return false;
88123
}
89-
if ((typeof superObj === 'object') !== (typeof obj === 'object')) { return false; }
90124
if (typeof obj === 'object') {
91125
for (const key of Object.keys(obj)) {
92-
if (!(key in superObj)) { return false; }
126+
if (!(key in superObj)) {
127+
errors.push(`Field ${key} missing`);
128+
continue;
129+
}
93130

94-
if (!isSuperObject(superObj[key], obj[key])) {
95-
return false;
131+
if (!isSuperObject(superObj[key], obj[key], [])) {
132+
errors.push(`Field ${key} mismatch`);
96133
}
97134
}
98-
return true;
135+
return errors.length === 0;
136+
}
137+
138+
if (superObj !== obj) {
139+
errors.push('Different values');
99140
}
100-
return superObj === obj;
141+
return errors.length === 0;
101142
}
102143

103144
/**
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Test } from 'nodeunit';
2+
import { expect, haveResource } from '../lib/index';
3+
4+
export = {
5+
'support resource with no properties'(test: Test) {
6+
const synthStack = mkStack({
7+
Resources: {
8+
SomeResource: {
9+
Type: 'Some::Resource'
10+
}
11+
}
12+
});
13+
expect(synthStack).to(haveResource('Some::Resource'));
14+
15+
test.done();
16+
},
17+
18+
'haveResource tells you about mismatched fields'(test: Test) {
19+
const synthStack = mkStack({
20+
Resources: {
21+
SomeResource: {
22+
Type: 'Some::Resource',
23+
Properties: {
24+
PropA: 'somevalue'
25+
}
26+
}
27+
}
28+
});
29+
30+
test.throws(() => {
31+
expect(synthStack).to(haveResource('Some::Resource', {
32+
PropA: 'othervalue'
33+
}));
34+
}, /PropA/);
35+
36+
test.done();
37+
}
38+
};
39+
40+
function mkStack(template: any) {
41+
return {
42+
name: 'test',
43+
template,
44+
metadata: {},
45+
environment: {
46+
name: 'test',
47+
account: 'test',
48+
region: 'test'
49+
}
50+
};
51+
}

0 commit comments

Comments
 (0)