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): cdk assert: implement 'haveOutput' assertion (#1906) #5366

Merged
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c1bd451
feat(assert): cdk assert: implement 'haveOutput' assertion (#1906)
ilya-v-trofimov Dec 11, 2019
201582f
Merge branch 'master' into ilya-v-trofimov/feature-haveOutput-assertion
ilya-v-trofimov Dec 16, 2019
6efaca2
Update README
ilya-v-trofimov Dec 21, 2019
7a83f5c
remove blank line as per review recommendations
ilya-v-trofimov Dec 21, 2019
e2b00b1
Update TSDocs as per review comments
ilya-v-trofimov Dec 21, 2019
0a5ea1b
feat(assert): haveOutput - Update README as per review comments
ilya-v-trofimov Dec 21, 2019
f21aedc
feat(assert): haveOutput - remove blank line as per review recommenda…
ilya-v-trofimov Dec 21, 2019
b214ab2
feat(assert): haveOutput - Update TSDocs as per review comments
ilya-v-trofimov Dec 21, 2019
ccc6830
feat(assert): haveOutput - fixing/refactoring as per review comments
ilya-v-trofimov Dec 21, 2019
8663c31
Merge branch 'ilya-v-trofimov/feature-haveOutput-assertion' of github…
ilya-v-trofimov Dec 21, 2019
0a2bc8e
Merge branch 'master' into ilya-v-trofimov/feature-haveOutput-assertion
ilya-v-trofimov Dec 21, 2019
d277197
feat(assert): haveOutput - fixing import style in test file
ilya-v-trofimov Dec 21, 2019
711a5d6
Merge branch 'master' into ilya-v-trofimov/feature-haveOutput-assertion
ilya-v-trofimov Dec 24, 2019
af838c4
Update README.md
RomainMuller Dec 27, 2019
d44f849
feat(assert): haveOutput - fixing TSDoc for HaveOutputProperties inte…
ilya-v-trofimov Dec 31, 2019
fde7e9d
Merge branch 'master' into ilya-v-trofimov/feature-haveOutput-assertion
ilya-v-trofimov Dec 31, 2019
0c7fb09
Merge branch 'master' into ilya-v-trofimov/feature-haveOutput-assertion
RomainMuller Jan 3, 2020
d912431
Merge branch 'master' into ilya-v-trofimov/feature-haveOutput-assertion
RomainMuller Jan 10, 2020
7965c41
make the assetion jest-friendly
RomainMuller Jan 10, 2020
c0fa062
adjust jest coverage threshold
RomainMuller Jan 13, 2020
82f470d
Merge branch 'master' into ilya-v-trofimov/feature-haveOutput-assertion
RomainMuller Jan 13, 2020
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
22 changes: 22 additions & 0 deletions packages/@aws-cdk/assert/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,25 @@ expect(stack).to(haveResource('AWS::CertificateManager::Certificate', {
// Note: some properties omitted here
}));
```
### Check existence of an output
`haveOutput` assertion can be used to check that a stack contains specific output.
Parameters to check against can be:
- `outputName`
- `outputValue`
- `exportName`

If `outputValue` is provided, at least one of `outputName`, `exportName` should be provided as well

Example
```ts
expect(synthStack).to(haveOutput({
outputName: 'TestOutputName',
exportName: 'TestOutputExportName',
outputValue: {
'Fn::GetAtt': [
'TestResource',
'Arn'
]
}
}));
```
98 changes: 98 additions & 0 deletions packages/@aws-cdk/assert/lib/assertions/have-output.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import {Assertion} from "../assertion";
import {StackInspector} from "../inspector";

class HaveOutputAssertion extends Assertion<StackInspector> {
constructor(private readonly outputName?: string, private readonly exportName?: any, private outputValue?: any) {
super();
if (!this.outputName && !this.exportName) {
throw new Error('At least one of [outputName, exportName] should be provided');
}
}

public get description(): string {
const descriptionPartsArray = [
'output',
`${this.outputName ? ' with name ' + this.outputName : ''}`,
`${this.exportName ? ' with export name ' + JSON.stringify(this.exportName) : ''}`,
`${this.outputValue ? ' with value ' + JSON.stringify(this.outputValue) : ''}`
];
return descriptionPartsArray.join();
}

public assertUsing(inspector: StackInspector): boolean {
if (!('Outputs' in inspector.value)) {
return false;
}
return (this.checkOutputName(inspector)) &&
(this.checkExportName(inspector)) &&
(this.checkOutputValue(inspector));
}

private checkOutputName(inspector: StackInspector): boolean {
if (!this.outputName) {
return true;
}
return this.outputName in inspector.value.Outputs;
}

private checkExportName(inspector: StackInspector): boolean {
if (!this.exportName) {
return true;
}
const outputs = Object.entries(inspector.value.Outputs)
.filter(([name, ]) => !this.outputName || this.outputName === name)
.map(([, value]) => value);
const outputWithExport = this.findOutput(outputs);
return !!outputWithExport;
}

private checkOutputValue(inspector: StackInspector): boolean {
if (!this.outputValue) {
return true;
}
const output = this.outputName ?
inspector.value.Outputs[this.outputName] :
this.findOutput(Object.values(inspector.value.Outputs));
return output ?
JSON.stringify(output.Value) === JSON.stringify(this.outputValue) :
false;
}

private findOutput(outputs: any): any {
const thisExportNameString = JSON.stringify(this.exportName);
return outputs.find((out: any) => {
return JSON.stringify(out.Export?.Name) === thisExportNameString;
});
}
}

/**
* Interface for haveOutput function properties
* NOTE that at least one of [outputName, exportName] should be provided
*/
export interface HaveOutputProperties {
/**
* Logical ID of the output
* @default - the logical ID of the output will not be checked
*/
outputName?: string;
/**
* Export name of the output, when it's exported for cross-stack referencing
* @default - the export name is not required and will not be checked
*/
exportName?: any;
/**
* Value of the output;
* @default - the value will not be checked
*/
outputValue?: any;
}

/**
* An assertion to check whether Output with particular properties is present in a stack
* @param props properties of the Output that is being asserted against.
* Check ``HaveOutputProperties`` interface to get full list of available parameters
*/
export function haveOutput(props: HaveOutputProperties): Assertion<StackInspector> {
return new HaveOutputAssertion(props.outputName, props.exportName, props.outputValue);
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/assert/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from './inspector';
export * from './synth-utils';

export * from './assertions/exist';
export * from './assertions/have-output';
export * from './assertions/have-resource';
export * from './assertions/have-type';
export * from './assertions/match-template';
Expand Down
202 changes: 202 additions & 0 deletions packages/@aws-cdk/assert/test/test.have-output.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import * as cxapi from '@aws-cdk/cx-api';
import { unlink, writeFileSync } from 'fs';
import { Test } from 'nodeunit';
import { join } from 'path';
import { expect, haveOutput } from '../lib';

let templateFilePath: string;
let synthStack: cxapi.CloudFormationStackArtifact;
let noOutputStack: cxapi.CloudFormationStackArtifact;
module.exports = {
'setUp'(cb: () => void) {
synthStack = mkStack({
Resources: {
SomeResource: {
Type: 'Some::Resource',
Properties: {
PropA: 'somevalue'
}
},
AnotherResource: {
Type: 'Some::AnotherResource',
Properties: {
PropA: 'anothervalue'
}
}
},
Outputs: {
TestOutput: {
Value: {
'Fn::GetAtt': [
'SomeResource',
'Arn'
]
},
Export: {
Name: 'TestOutputExportName'
}
},
ComplexExportNameOutput: {
Value: {
'Fn::GetAtt': [
'ComplexOutputResource',
'Arn'
]
},
Export: {
Name: {
"Fn::Sub": "${AWS::StackName}-ComplexExportNameOutput"
}
}
}
}
});
noOutputStack = mkStack({
Resources: {
SomeResource: {
Type: 'Some::Resource',
Properties: {
PropA: 'somevalue'
}
}
}
});
cb();
},
'haveOutput should assert true when output with correct name is provided'(test: Test) {
expect(synthStack).to(haveOutput({
outputName: 'TestOutput'
}));
test.done();
},
'haveOutput should assert false when output with incorrect name is provided'(test: Test) {
expect(synthStack).notTo(haveOutput({
outputName: 'WrongOutput'
}));
test.done();
},
'haveOutput should assert true when output with correct name and export name is provided'(test: Test) {
expect(synthStack).to(haveOutput({
outputName: 'TestOutput',
exportName: 'TestOutputExportName',
}));
test.done();
},
'haveOutput should assert false when output with correct name and incorrect export name is provided'(test: Test) {
expect(synthStack).notTo(haveOutput({
outputName: 'TestOutput',
exportName: 'WrongTestOutputExportName',
}));
test.done();
},
'haveOutput should assert true when output with correct name, export name and value is provided'(test: Test) {
expect(synthStack).to(haveOutput({
outputName: 'TestOutput',
exportName: 'TestOutputExportName',
outputValue: {
'Fn::GetAtt': [
'SomeResource',
'Arn'
]
}
}));
test.done();
},
'haveOutput should assert false when output with correct name and export name and incorrect value is provided'(test: Test) {
expect(synthStack).notTo(haveOutput({
outputName: 'TestOutput',
exportName: 'TestOutputExportName',
outputValue: 'SomeWrongValue'
}));
test.done();
},
'haveOutput should assert true when output with correct export name and value is provided'(test: Test) {
expect(synthStack).to(haveOutput({
exportName: 'TestOutputExportName',
outputValue: {
'Fn::GetAtt': [
'SomeResource',
'Arn'
]
}
}));
test.done();
},
'haveOutput should assert false when output with correct export name and incorrect value is provided'(test: Test) {
expect(synthStack).notTo(haveOutput({
exportName: 'TestOutputExportName',
outputValue: 'WrongValue'
}));
test.done();
},
'haveOutput should assert true when output with correct output name and value is provided'(test: Test) {
expect(synthStack).to(haveOutput({
outputName: 'TestOutput',
outputValue: {
'Fn::GetAtt': [
'SomeResource',
'Arn'
]
}
}));
test.done();
},
'haveOutput should assert false when output with correct output name and incorrect value is provided'(test: Test) {
expect(synthStack).notTo(haveOutput({
outputName: 'TestOutput',
outputValue: 'WrongValue'
}));
test.done();
},
'haveOutput should assert false when asserting against noOutputStack'(test: Test) {
expect(noOutputStack).notTo(haveOutput({
outputName: 'TestOutputName',
exportName: 'TestExportName',
outputValue: 'TestOutputValue'
}));
test.done();
},
'haveOutput should throw Error when none of outputName and exportName is provided'(test: Test) {
test.throws(() => {
expect(synthStack).to(haveOutput({
outputValue: 'SomeValue'
}));
});
test.done();
},
'haveOutput should be able to handle complex exportName values'(test: Test) {
expect(synthStack).to(haveOutput({
exportName: {'Fn::Sub': '${AWS::StackName}-ComplexExportNameOutput'},
outputValue: {
'Fn::GetAtt': [
'ComplexOutputResource',
'Arn'
]
}
}));
test.done();
},
'tearDown'(cb: () => void) {
if (templateFilePath) {
unlink(templateFilePath, cb);
} else {
cb();
}
}
};

function mkStack(template: any): cxapi.CloudFormationStackArtifact {
const templateFileName = 'test-have-output-template.json';
const stackName = 'test-have-output';
const assembly = new cxapi.CloudAssemblyBuilder();
assembly.addArtifact(stackName, {
type: cxapi.ArtifactType.AWS_CLOUDFORMATION_STACK,
environment: cxapi.EnvironmentUtils.format('123456789012', 'bermuda-triangle-1'),
properties: {
templateFile: templateFileName
}
});
templateFilePath = join(assembly.outdir, templateFileName);
writeFileSync(templateFilePath, JSON.stringify(template));
return assembly.buildAssembly().getStackByName(stackName);
}