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

fix(assertions): output and mapping assertions do not accept logical id #16329

Merged
merged 13 commits into from
Sep 8, 2021
30 changes: 23 additions & 7 deletions packages/@aws-cdk/assertions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,16 +107,32 @@ By default, the `hasResource()` and `hasResourceProperties()` APIs perform deep
partial object matching. This behavior can be configured using matchers.
See subsequent section on [special matchers](#special-matchers).

## Other Sections
## Output and Mapping sections

Similar to the `hasResource()` and `findResources()`, we have equivalent methods
to check and find other sections of the CloudFormation resources.
The module allows you to assert that the CloudFormation template contains an Output
that matches specific properties. The following code asserts that a template contains
an Output with a `logicalId` of `Foo` and the specified properties -

* Outputs - `hasOutput()` and `findOutputs()`
* Mapping - `hasMapping()` and `findMappings()`
```ts
assert.hasOutput('Foo', {
Value: 'Bar',
Export: { Name: 'ExportBaz' },
});
```

If you want to match against all Outputs in the template, use `*` as the `logicalId`.

```ts
assert.hasOutput('*', {
Value: 'Bar',
Export: { Name: 'ExportBaz' },
});
```

`findOutputs()` will return a list of outputs that match the `logicalId` and `props`,
and you can use the `'*'` special case as well.

All of the defaults and behaviour documented for `hasResource()` and
`findResources()` apply to these methods.
The APIs `hasMapping()` and `findMappings()` provide similar functionalities.

## Special Matchers

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,9 +1,9 @@
import { StackInspector } from '../vendored/assert';
import { formatFailure, matchSection } from './section';
import { filterLogicalId, formatFailure, matchSection } from './section';

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

if (!result.match) {
return [];
Expand All @@ -12,9 +12,9 @@ export function findMappings(inspector: StackInspector, props: any = {}): { [key
return result.matches;
}

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

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

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

if (!result.match) {
return [];
Expand All @@ -12,20 +12,19 @@ export function findOutputs(inspector: StackInspector, props: any = {}): { [key:
return result.matches;
}

export function hasOutput(inspector: StackInspector, props: any): string | void {
export function hasOutput(inspector: StackInspector, logicalId: string, props: any): string | void {
const section: { [key: string]: {} } = inspector.value.Outputs;
const result = matchSection(section, props);

const result = matchSection(filterLogicalId(section, logicalId), props);
if (result.match) {
return;
}

if (result.closestResult === undefined) {
return 'No outputs found in the template';
return `No outputs named ${logicalId} found in the template.`;
}

return [
`Template has ${result.analyzedCount} outputs, but none match as expected.`,
`Template has ${result.analyzedCount} outputs named ${logicalId}, but none match as expected.`,
formatFailure(result.closestResult),
].join('\n');
}
}
9 changes: 9 additions & 0 deletions packages/@aws-cdk/assertions/lib/private/section.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,13 @@ export function formatFailure(closestResult: MatchResult): string {
function leftPad(x: string, indent: number = 2): string {
const pad = ' '.repeat(indent);
return pad + x.split('\n').join(`\n${pad}`);
}

export function filterLogicalId(section: { [key: string]: {} }, logicalId: string): { [key: string]: {} } {
// default signal for all logicalIds is '*'
if (logicalId === '*') return section;

return Object.entries(section ?? {})
.filter(([k, _]) => k === logicalId)
.reduce((agg, [k, v]) => { return { ...agg, [k]: v }; }, {});
}
20 changes: 12 additions & 8 deletions packages/@aws-cdk/assertions/lib/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,46 +109,50 @@ export class Template {
* Assert that an Output with the given properties exists in the CloudFormation template.
* By default, performs partial matching on the resource, via the `Match.objectLike()`.
* To configure different behavour, use other matchers in the `Match` class.
* @param logicalId the name of the output. Provide `'*'` to match all outputs in the template.
* @param props the output as should be expected in the template.
*/
public hasOutput(props: any): void {
const matchError = hasOutput(this.inspector, props);
public hasOutput(logicalId: string, props: any): void {
const matchError = hasOutput(this.inspector, logicalId, props);
if (matchError) {
throw new Error(matchError);
}
}

/**
* Get the set of matching Outputs that match the given properties in the CloudFormation template.
* @param logicalId the name of the output. Provide `'*'` to match all outputs in the template.
* @param props by default, matches all Outputs in the template.
* When a literal object is provided, performs a partial match via `Match.objectLike()`.
* Use the `Match` APIs to configure a different behaviour.
*/
public findOutputs(props: any = {}): { [key: string]: any }[] {
return findOutputs(this.inspector, props);
public findOutputs(logicalId: string, props: any = {}): { [key: string]: any }[] {
return findOutputs(this.inspector, logicalId, props);
}

/**
* Assert that a Mapping with the given properties exists in the CloudFormation template.
* By default, performs partial matching on the resource, via the `Match.objectLike()`.
* To configure different behavour, use other matchers in the `Match` class.
* @param logicalId the name of the mapping. Provide `'*'` to match all mappings in the template.
* @param props the output as should be expected in the template.
*/
public hasMapping(props: any): void {
const matchError = hasMapping(this.inspector, props);
public hasMapping(logicalId: string, props: any): void {
const matchError = hasMapping(this.inspector, logicalId, props);
if (matchError) {
throw new Error(matchError);
}
}

/**
* Get the set of matching Mappings that match the given properties in the CloudFormation template.
* @param logicalId the name of the mapping. Provide `'*'` to match all mappings in the template.
* @param props by default, matches all Mappings in the template.
* When a literal object is provided, performs a partial match via `Match.objectLike()`.
* Use the `Match` APIs to configure a different behaviour.
*/
public findMappings(props: any = {}): { [key: string]: any }[] {
return findMappings(this.inspector, props);
public findMappings(logicalId: string, props: any = {}): { [key: string]: any }[] {
return findMappings(this.inspector, logicalId, props);
}

/**
Expand Down
Loading