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

chore(assertions): replace absentProperty() with absent() and support it as a Matcher type #16653

Merged
merged 15 commits into from
Oct 5, 2021
Merged
38 changes: 21 additions & 17 deletions packages/@aws-cdk/assertions/lib/match.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Matcher, MatchResult } from './matcher';
import { getType } from './private/type';
import { ABSENT } from './vendored/assert';

/**
* Partial and special matching during template assertions.
Expand All @@ -9,8 +8,8 @@ export abstract class Match {
/**
* Use this matcher in the place of a field's value, if the field must not be present.
*/
public static absentProperty(): string {
return ABSENT;
public static absentProperty(): Matcher {
kaizencc marked this conversation as resolved.
Show resolved Hide resolved
return new AbsentMatch('absentProperty');
}

/**
Expand Down Expand Up @@ -128,10 +127,6 @@ class LiteralMatch extends Matcher {
return result;
}

if (this.pattern === ABSENT) {
throw new Error('absentProperty() can only be used in an object matcher');
}

if (actual !== this.pattern) {
result.push(this, [], `Expected ${this.pattern} but received ${actual}`);
}
Expand Down Expand Up @@ -184,9 +179,10 @@ class ArrayMatch extends Matcher {
const patternElement = this.pattern[patternIdx];

const matcher = Matcher.isMatcher(patternElement) ? patternElement : new LiteralMatch(this.name, patternElement);
if (this.subsequence && matcher instanceof AnyMatch) {
// array subsequence matcher is not compatible with anyValue() matcher. They don't make sense to be used together.
throw new Error('The Matcher anyValue() cannot be nested within arrayWith()');
const matcherName = matcher.name;
if (this.subsequence && (matcherName == 'absentProperty' || matcherName == 'anyValue')) {
// array subsequence matcher is not compatible with anyValue() or absentProperty() matcher. They don't make sense to be used together.
throw new Error(`The Matcher ${matcherName}() cannot be nested within arrayWith()`);
}

const innerResult = matcher.test(actual[actualIdx]);
Expand Down Expand Up @@ -252,13 +248,7 @@ class ObjectMatch extends Matcher {
}

for (const [patternKey, patternVal] of Object.entries(this.pattern)) {
if (patternVal === ABSENT) {
if (patternKey in actual) {
result.push(this, [`/${patternKey}`], 'Key should be absent');
}
continue;
}
if (!(patternKey in actual)) {
if (!(patternKey in actual) && !(patternVal instanceof AbsentMatch)) {
result.push(this, [`/${patternKey}`], 'Missing key');
continue;
}
Expand Down Expand Up @@ -338,4 +328,18 @@ class AnyMatch extends Matcher {
}
return result;
}
}

class AbsentMatch extends Matcher {
constructor(public readonly name: string) {
super();
}

public test(actual: any): MatchResult {
const result = new MatchResult(actual);
if (actual !== undefined) {
result.push(this, [], `Received ${actual}, but key should be absent`);
}
return result;
}
}
29 changes: 25 additions & 4 deletions packages/@aws-cdk/assertions/test/match.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ describe('Matchers', () => {
});

test('absent', () => {
expect(() => Match.exact(Match.absentProperty()).test('foo')).toThrow(/absentProperty/);
expect(() => Match.exact(Match.absentProperty())).toThrow(/cannot directly contain another matcher/);
});
});

Expand Down Expand Up @@ -125,8 +125,9 @@ describe('Matchers', () => {
expectFailure(matcher, [{ foo: 'baz', fred: 'waldo' }], [/Missing element at pattern index 0/]);
});

test('absent', () => {
expect(() => Match.arrayWith([Match.absentProperty()]).test(['foo'])).toThrow(/absentProperty/);
test('incompatible with absent', () => {
matcher = Match.arrayWith(['foo', Match.absentProperty()]);
expect(() => matcher.test(['foo', 'bar'])).toThrow(/absentProperty\(\) cannot be nested within arrayWith\(\)/);
});

test('incompatible with anyValue', () => {
Expand Down Expand Up @@ -186,7 +187,7 @@ describe('Matchers', () => {
test('absent', () => {
matcher = Match.objectLike({ foo: Match.absentProperty() });
expectPass(matcher, { bar: 'baz' });
expectFailure(matcher, { foo: 'baz' }, [/Key should be absent at \/foo/]);
expectFailure(matcher, { foo: 'baz' }, [/key should be absent at \/foo/]);
});
});

Expand Down Expand Up @@ -363,6 +364,26 @@ describe('Matchers', () => {
expectFailure(matcher, '{ "Foo"', [/invalid JSON string/i]);
});
});

describe('absent property', () => {
let matcher: Matcher;

test('simple', () => {
matcher = Match.absentProperty();
expectFailure(matcher, 'foo', ['Received foo, but key should be absent']);
expectPass(matcher, undefined);
});

test('nested in object', () => {
matcher = Match.objectLike({ foo: Match.absentProperty() });
expectFailure(matcher, { foo: 'bar' }, [/key should be absent at \/foo/]);
expectFailure(matcher, { foo: [1, 2] }, [/key should be absent at \/foo/]);
expectFailure(matcher, { foo: null }, [/key should be absent at \/foo/]);

expectPass(matcher, { foo: undefined });
expectPass(matcher, {});
});
});
});

function expectPass(matcher: Matcher, target: any): void {
Expand Down
43 changes: 42 additions & 1 deletion packages/@aws-cdk/assertions/test/template.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ describe('Template', () => {
});
expect(() => inspect.hasResource('Foo::Bar', {
Properties: Match.objectLike({ baz: Match.absentProperty() }),
})).toThrow(/Key should be absent at \/Properties\/baz/);
})).toThrow(/key should be absent at \/Properties\/baz/);
});

test('incorrect types', () => {
Expand All @@ -269,6 +269,47 @@ describe('Template', () => {
});
});

describe('hasResourceProperties', () => {
test('absent', () => {
const stack = new Stack();
new CfnResource(stack, 'Foo', {
type: 'Foo::Bar',
properties: { baz: 'qux' },
});

const inspect = Template.fromStack(stack);
inspect.hasResourceProperties('Foo::Bar', {
bar: Match.absentProperty(),
});
expect(() => inspect.hasResourceProperties('Foo::Bar', {
baz: Match.absentProperty(),
})).toThrow(/key should be absent at \/Properties\/baz/);
});

test('absent properties', () => {
const stack = new Stack();
new CfnResource(stack, 'Foo', {
type: 'Foo::Bar',
});

const inspect = Template.fromStack(stack);
inspect.hasResourceProperties('Foo::Bar', Match.absentProperty());
});

test('not', () => {
const stack = new Stack();
new CfnResource(stack, 'Foo', {
type: 'Foo::Bar',
properties: { baz: 'qux' },
});

const inspect = Template.fromStack(stack);
inspect.hasResourceProperties('Foo::Bar', Match.not({
baz: 'boo',
}));
});
});

describe('getResources', () => {
test('matching resource type', () => {
const stack = new Stack();
Expand Down