From 7f6f3fd8c4f3de6142c17012b177977161222a80 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 26 Oct 2018 12:26:56 +0200 Subject: [PATCH] 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'. --- .../assert/lib/assertions/have-resource.ts | 81 ++++++++++++++----- .../assert/test/test.have-resource.ts | 51 ++++++++++++ 2 files changed, 112 insertions(+), 20 deletions(-) create mode 100644 packages/@aws-cdk/assert/test/test.have-resource.ts diff --git a/packages/@aws-cdk/assert/lib/assertions/have-resource.ts b/packages/@aws-cdk/assert/lib/assertions/have-resource.ts index 3978879873585..12cd276cf2470 100644 --- a/packages/@aws-cdk/assert/lib/assertions/have-resource.ts +++ b/packages/@aws-cdk/assert/lib/assertions/have-resource.ts @@ -13,10 +13,10 @@ export function haveResource(resourceType: string, properties?: any, comparison? return new HaveResourceAssertion(resourceType, properties, comparison); } -type PropertyPredicate = (props: any) => boolean; +type PropertyPredicate = (props: any, inspection: InspectionFailure) => boolean; class HaveResourceAssertion extends Assertion { - private inspected: any[] = []; + private inspected: InspectionFailure[] = []; private readonly part: ResourcePart; private readonly predicate: PropertyPredicate; @@ -33,13 +33,17 @@ class HaveResourceAssertion extends Assertion { for (const logicalId of Object.keys(inspector.value.Resources)) { const resource = inspector.value.Resources[logicalId]; if (resource.Type === this.resourceType) { - this.inspected.push(resource); - const propsToCheck = this.part === ResourcePart.Properties ? resource.Properties : resource; - if (this.predicate(propsToCheck)) { + // Pass inspection object as 2nd argument, initialize failure with default string, + // to maintain backwards compatibility with old predicate API. + const inspection = { resource, failureReason: 'Object did not match predicate' }; + + if (this.predicate(propsToCheck, inspection)) { return true; } + + this.inspected.push(inspection); } } @@ -48,7 +52,15 @@ class HaveResourceAssertion extends Assertion { public assertOrThrow(inspector: StackInspector) { if (!this.assertUsing(inspector)) { - throw new Error(`None of ${JSON.stringify(this.inspected, null, 2)} match ${this.description}`); + const lines: string[] = []; + lines.push(`None of ${this.inspected.length} resources matches ${this.description}.`); + + for (const inspected of this.inspected) { + lines.push(`- ${inspected.failureReason} in:`); + lines.push(indent(4, JSON.stringify(inspected.resource, null, 2))); + } + + throw new Error(lines.join('\n')); } } @@ -58,46 +70,75 @@ class HaveResourceAssertion extends Assertion { } } +function indent(n: number, s: string) { + const prefix = ' '.repeat(n); + return prefix + s.replace(/\n/g, '\n' + prefix); +} + /** * Make a predicate that checks property superset */ function makeSuperObjectPredicate(obj: any) { - return (resourceProps: any) => { - return isSuperObject(resourceProps, obj); + return (resourceProps: any, inspection: InspectionFailure) => { + const errors: string[] = []; + const ret = isSuperObject(resourceProps, obj, errors); + inspection.failureReason = errors.join(','); + return ret; }; } +interface InspectionFailure { + resource: any; + failureReason: string; +} + /** * Return whether `superObj` is a super-object of `obj`. * * A super-object has the same or more property values, recursing into nested objects. */ -export function isSuperObject(superObj: any, obj: any): boolean { +export function isSuperObject(superObj: any, obj: any, errors: string[] = []): boolean { if (obj == null) { return true; } - if (Array.isArray(superObj) !== Array.isArray(obj)) { return false; } + if (Array.isArray(superObj) !== Array.isArray(obj)) { + errors.push('Array type mismatch'); + return false; + } if (Array.isArray(superObj)) { - if (obj.length !== superObj.length) { return false; } + if (obj.length !== superObj.length) { + errors.push('Array length mismatch'); + return false; + } // Do isSuperObject comparison for individual objects for (let i = 0; i < obj.length; i++) { - if (!isSuperObject(superObj[i], obj[i])) { - return false; + if (!isSuperObject(superObj[i], obj[i], [])) { + errors.push(`Array element ${i} mismatch`); } } - return true; + return errors.length === 0; + } + if ((typeof superObj === 'object') !== (typeof obj === 'object')) { + errors.push('Object type mismatch'); + return false; } - if ((typeof superObj === 'object') !== (typeof obj === 'object')) { return false; } if (typeof obj === 'object') { for (const key of Object.keys(obj)) { - if (!(key in superObj)) { return false; } + if (!(key in superObj)) { + errors.push(`Field ${key} missing`); + continue; + } - if (!isSuperObject(superObj[key], obj[key])) { - return false; + if (!isSuperObject(superObj[key], obj[key], [])) { + errors.push(`Field ${key} mismatch`); } } - return true; + return errors.length === 0; + } + + if (superObj !== obj) { + errors.push('Different values'); } - return superObj === obj; + return errors.length === 0; } /** diff --git a/packages/@aws-cdk/assert/test/test.have-resource.ts b/packages/@aws-cdk/assert/test/test.have-resource.ts new file mode 100644 index 0000000000000..fcac6a72debc7 --- /dev/null +++ b/packages/@aws-cdk/assert/test/test.have-resource.ts @@ -0,0 +1,51 @@ +import { Test } from 'nodeunit'; +import { expect, haveResource } from '../lib/index'; + +export = { + 'support resource with no properties'(test: Test) { + const synthStack = mkStack({ + Resources: { + SomeResource: { + Type: 'Some::Resource' + } + } + }); + expect(synthStack).to(haveResource('Some::Resource')); + + test.done(); + }, + + 'haveResource tells you about mismatched fields'(test: Test) { + const synthStack = mkStack({ + Resources: { + SomeResource: { + Type: 'Some::Resource', + Properties: { + PropA: 'somevalue' + } + } + } + }); + + test.throws(() => { + expect(synthStack).to(haveResource('Some::Resource', { + PropA: 'othervalue' + })); + }, /PropA/); + + test.done(); + } +}; + +function mkStack(template: any) { + return { + name: 'test', + template, + metadata: {}, + environment: { + name: 'test', + account: 'test', + region: 'test' + } + }; +} \ No newline at end of file