Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(assert): haveResource lists failing properties #1016

Merged
merged 3 commits into from
Oct 26, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 61 additions & 20 deletions packages/@aws-cdk/assert/lib/assertions/have-resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<StackInspector> {
private inspected: any[] = [];
private inspected: InspectionFailure[] = [];
private readonly part: ResourcePart;
private readonly predicate: PropertyPredicate;

Expand All @@ -33,13 +33,17 @@ class HaveResourceAssertion extends Assertion<StackInspector> {
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);
}
}

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

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'));
}
}

Expand All @@ -58,46 +70,75 @@ class HaveResourceAssertion extends Assertion<StackInspector> {
}
}

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;
}

/**
Expand Down
51 changes: 51 additions & 0 deletions packages/@aws-cdk/assert/test/test.have-resource.ts
Original file line number Diff line number Diff line change
@@ -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'
}
};
}