diff --git a/packages/@aws-cdk/assertions/README.md b/packages/@aws-cdk/assertions/README.md index 0c2d9d1ecdadf..ce480bc152dc4 100644 --- a/packages/@aws-cdk/assertions/README.md +++ b/packages/@aws-cdk/assertions/README.md @@ -144,9 +144,9 @@ The APIs `hasMapping()` and `findMappings()` provide similar functionalities. ## Special Matchers -The expectation provided to the `hasXXX()` and `findXXX()` methods, besides -carrying literal values, as seen in the above examples, also accept special -matchers. +The expectation provided to the `hasXxx()`, `findXxx()` and `templateMatches()` +APIs, besides carrying literal values, as seen in the above examples, also accept +special matchers. They are available as part of the `Match` class. diff --git a/packages/@aws-cdk/assertions/lib/match.ts b/packages/@aws-cdk/assertions/lib/match.ts index 4fea0ed0f713e..9b18400abcbf6 100644 --- a/packages/@aws-cdk/assertions/lib/match.ts +++ b/packages/@aws-cdk/assertions/lib/match.ts @@ -1,6 +1,7 @@ import { Matcher, MatchResult } from './matcher'; import { getType } from './private/type'; -import { ABSENT } from './vendored/assert'; + +const ABSENT = '{{ABSENT}}'; /** * Partial and special matching during template assertions. diff --git a/packages/@aws-cdk/assertions/lib/private/mappings.ts b/packages/@aws-cdk/assertions/lib/private/mappings.ts index e19afe541d204..e080843dd87f8 100644 --- a/packages/@aws-cdk/assertions/lib/private/mappings.ts +++ b/packages/@aws-cdk/assertions/lib/private/mappings.ts @@ -1,8 +1,8 @@ -import { StackInspector } from '../vendored/assert'; import { filterLogicalId, formatFailure, matchSection } from './section'; +import { Template } from './template'; -export function findMappings(inspector: StackInspector, logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } { - const section: { [key: string] : {} } = inspector.value.Mappings; +export function findMappings(template: Template, logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } { + const section: { [key: string] : {} } = template.Mappings; const result = matchSection(filterLogicalId(section, logicalId), props); if (!result.match) { @@ -12,8 +12,8 @@ export function findMappings(inspector: StackInspector, logicalId: string, props return result.matches; } -export function hasMapping(inspector: StackInspector, logicalId: string, props: any): string | void { - const section: { [key: string]: {} } = inspector.value.Mappings; +export function hasMapping(template: Template, logicalId: string, props: any): string | void { + const section: { [key: string]: {} } = template.Mappings; const result = matchSection(filterLogicalId(section, logicalId), props); if (result.match) { diff --git a/packages/@aws-cdk/assertions/lib/private/outputs.ts b/packages/@aws-cdk/assertions/lib/private/outputs.ts index 320016d22a8eb..f00f05bc9bb0f 100644 --- a/packages/@aws-cdk/assertions/lib/private/outputs.ts +++ b/packages/@aws-cdk/assertions/lib/private/outputs.ts @@ -1,8 +1,8 @@ -import { StackInspector } from '../vendored/assert'; import { filterLogicalId, formatFailure, matchSection } from './section'; +import { Template } from './template'; -export function findOutputs(inspector: StackInspector, logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } { - const section: { [key: string] : {} } = inspector.value.Outputs; +export function findOutputs(template: Template, logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } { + const section = template.Outputs; const result = matchSection(filterLogicalId(section, logicalId), props); if (!result.match) { @@ -12,8 +12,8 @@ export function findOutputs(inspector: StackInspector, logicalId: string, props: return result.matches; } -export function hasOutput(inspector: StackInspector, logicalId: string, props: any): string | void { - const section: { [key: string]: {} } = inspector.value.Outputs; +export function hasOutput(template: Template, logicalId: string, props: any): string | void { + const section: { [key: string]: {} } = template.Outputs; const result = matchSection(filterLogicalId(section, logicalId), props); if (result.match) { return; diff --git a/packages/@aws-cdk/assertions/lib/private/resources.ts b/packages/@aws-cdk/assertions/lib/private/resources.ts index 81b77346f61f5..aeb2037d81ad4 100644 --- a/packages/@aws-cdk/assertions/lib/private/resources.ts +++ b/packages/@aws-cdk/assertions/lib/private/resources.ts @@ -1,13 +1,8 @@ -import { StackInspector } from '../vendored/assert'; import { formatFailure, matchSection } from './section'; +import { Resource, Template } from './template'; -// Partial type for CloudFormation Resource -type Resource = { - Type: string; -} - -export function findResources(inspector: StackInspector, type: string, props: any = {}): { [key: string]: { [key: string]: any } } { - const section: { [key: string] : Resource } = inspector.value.Resources; +export function findResources(template: Template, type: string, props: any = {}): { [key: string]: { [key: string]: any } } { + const section = template.Resources; const result = matchSection(filterType(section, type), props); if (!result.match) { @@ -17,8 +12,8 @@ export function findResources(inspector: StackInspector, type: string, props: an return result.matches; } -export function hasResource(inspector: StackInspector, type: string, props: any): string | void { - const section: { [key: string]: Resource } = inspector.value.Resources; +export function hasResource(template: Template, type: string, props: any): string | void { + const section = template.Resources; const result = matchSection(filterType(section, type), props); if (result.match) { @@ -35,6 +30,13 @@ export function hasResource(inspector: StackInspector, type: string, props: any) ].join('\n'); } +export function countResources(template: Template, type: string): number { + const section = template.Resources; + const types = filterType(section, type); + + return Object.entries(types).length; +} + function filterType(section: { [key: string]: Resource }, type: string): { [key: string]: Resource } { return Object.entries(section ?? {}) .filter(([_, v]) => v.Type === type) diff --git a/packages/@aws-cdk/assertions/lib/private/template.ts b/packages/@aws-cdk/assertions/lib/private/template.ts new file mode 100644 index 0000000000000..3b44368138435 --- /dev/null +++ b/packages/@aws-cdk/assertions/lib/private/template.ts @@ -0,0 +1,16 @@ +// Partial types for CloudFormation Template + +export type Template = { + Resources: { [logicalId: string]: Resource }, + Outputs: { [logicalId: string]: Output }, + Mappings: { [logicalId: string]: Mapping } +} + +export type Resource = { + Type: string; + [key: string]: any; +} + +export type Output = { [key: string]: any }; + +export type Mapping = { [key: string]: any }; \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/lib/template.ts b/packages/@aws-cdk/assertions/lib/template.ts index e6f721d928576..d3871c6cda36b 100644 --- a/packages/@aws-cdk/assertions/lib/template.ts +++ b/packages/@aws-cdk/assertions/lib/template.ts @@ -3,8 +3,8 @@ import { Match } from './match'; import { Matcher } from './matcher'; import { findMappings, hasMapping } from './private/mappings'; import { findOutputs, hasOutput } from './private/outputs'; -import { findResources, hasResource } from './private/resources'; -import * as assert from './vendored/assert'; +import { countResources, findResources, hasResource } from './private/resources'; +import { Template as TemplateType } from './private/template'; /** * Suite of assertions that can be run on a CDK stack. @@ -39,12 +39,10 @@ export class Template { return new Template(JSON.parse(template)); } - private readonly template: { [key: string]: any }; - private readonly inspector: assert.StackInspector; + private readonly template: TemplateType; private constructor(template: { [key: string]: any }) { - this.template = template; - this.inspector = new assert.StackInspector(template); + this.template = template as TemplateType; } /** @@ -61,8 +59,10 @@ export class Template { * @param count number of expected instances */ public resourceCountIs(type: string, count: number): void { - const assertion = assert.countResources(type, count); - assertion.assertOrThrow(this.inspector); + const counted = countResources(this.template, type); + if (counted !== count) { + throw new Error(`Expected ${count} resources of type ${type} but found ${counted}`); + } } /** @@ -88,7 +88,7 @@ export class Template { * @param props the entire defintion of the resource as should be expected in the template. */ public hasResource(type: string, props: any): void { - const matchError = hasResource(this.inspector, type, props); + const matchError = hasResource(this.template, type, props); if (matchError) { throw new Error(matchError); } @@ -102,7 +102,7 @@ export class Template { * Use the `Match` APIs to configure a different behaviour. */ public findResources(type: string, props: any = {}): { [key: string]: { [key: string]: any } } { - return findResources(this.inspector, type, props); + return findResources(this.template, type, props); } /** @@ -113,7 +113,7 @@ export class Template { * @param props the output as should be expected in the template. */ public hasOutput(logicalId: string, props: any): void { - const matchError = hasOutput(this.inspector, logicalId, props); + const matchError = hasOutput(this.template, logicalId, props); if (matchError) { throw new Error(matchError); } @@ -127,7 +127,7 @@ export class Template { * Use the `Match` APIs to configure a different behaviour. */ public findOutputs(logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } { - return findOutputs(this.inspector, logicalId, props); + return findOutputs(this.template, logicalId, props); } /** @@ -138,7 +138,7 @@ export class Template { * @param props the output as should be expected in the template. */ public hasMapping(logicalId: string, props: any): void { - const matchError = hasMapping(this.inspector, logicalId, props); + const matchError = hasMapping(this.template, logicalId, props); if (matchError) { throw new Error(matchError); } @@ -152,16 +152,23 @@ export class Template { * Use the `Match` APIs to configure a different behaviour. */ public findMappings(logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } { - return findMappings(this.inspector, logicalId, props); + return findMappings(this.template, logicalId, props); } /** * Assert that the CloudFormation template matches the given value * @param expected the expected CloudFormation template as key-value pairs. */ - public templateMatches(expected: {[key: string]: any}): void { - const assertion = assert.matchTemplate(expected); - assertion.assertOrThrow(this.inspector); + public templateMatches(expected: any): void { + const matcher = Matcher.isMatcher(expected) ? expected : Match.objectLike(expected); + const result = matcher.test(this.template); + + if (result.hasFailed()) { + throw new Error([ + 'Template did not match as expected. The following mismatches were found:', + ...result.toHumanStrings().map(s => `\t${s}`), + ].join('\n')); + } } } diff --git a/packages/@aws-cdk/assertions/package.json b/packages/@aws-cdk/assertions/package.json index e3c0612bf486d..58420966a5bfe 100644 --- a/packages/@aws-cdk/assertions/package.json +++ b/packages/@aws-cdk/assertions/package.json @@ -48,11 +48,6 @@ }, "projectReferences": true }, - "cdk-build": { - "pre": [ - "./vendor-in.sh" - ] - }, "author": { "name": "Amazon Web Services", "url": "https://aws.amazon.com", @@ -61,7 +56,6 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/cdk-build-tools": "0.0.0", - "@aws-cdk/cfnspec": "0.0.0", "@aws-cdk/pkglint": "0.0.0", "@types/jest": "^26.0.24", "constructs": "^3.3.69", @@ -106,26 +100,6 @@ "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" }, - "nozem": { - "ostools": [ - "dirname", - "cd", - "bash", - "rm", - "xargs", - "sed", - "mkdir", - "rsync", - "cat", - "find" - ], - "additionalDirs": [ - "../cfnspec/lib", - "ARTIFACTS:../cfnspec/spec", - "../cloudformation-diff", - "../assert-internal" - ] - }, "stability": "experimental", "maturity": "experimental", "publishConfig": { diff --git a/packages/@aws-cdk/assertions/test/template.test.ts b/packages/@aws-cdk/assertions/test/template.test.ts index 7c3221763446c..97e7e4ca241a6 100644 --- a/packages/@aws-cdk/assertions/test/template.test.ts +++ b/packages/@aws-cdk/assertions/test/template.test.ts @@ -82,10 +82,10 @@ describe('Template', () => { const inspect = Template.fromStack(stack); inspect.resourceCountIs('Foo::Bar', 1); - expect(() => inspect.resourceCountIs('Foo::Bar', 0)).toThrow(/has 1 resource of type Foo::Bar/); - expect(() => inspect.resourceCountIs('Foo::Bar', 2)).toThrow(/has 1 resource of type Foo::Bar/); + expect(() => inspect.resourceCountIs('Foo::Bar', 0)).toThrow('Expected 0 resources of type Foo::Bar but found 1'); + expect(() => inspect.resourceCountIs('Foo::Bar', 2)).toThrow('Expected 2 resources of type Foo::Bar but found 1'); - expect(() => inspect.resourceCountIs('Foo::Baz', 1)).toThrow(/has 0 resource of type Foo::Baz/); + expect(() => inspect.resourceCountIs('Foo::Baz', 1)).toThrow('Expected 1 resources of type Foo::Baz but found 0'); }); test('no resource', () => { @@ -94,7 +94,7 @@ describe('Template', () => { const inspect = Template.fromStack(stack); inspect.resourceCountIs('Foo::Bar', 0); - expect(() => inspect.resourceCountIs('Foo::Bar', 1)).toThrow(/has 0 resource of type Foo::Bar/); + expect(() => inspect.resourceCountIs('Foo::Bar', 1)).toThrow('Expected 1 resources of type Foo::Bar but found 0'); }); }); @@ -132,7 +132,7 @@ describe('Template', () => { Properties: { baz: 'waldo' }, }, }, - })).toThrowError(); + })).toThrowError(/Expected waldo but received qux at \/Resources\/Foo\/Properties\/baz/); }); }); diff --git a/packages/@aws-cdk/assertions/vendor-in.sh b/packages/@aws-cdk/assertions/vendor-in.sh deleted file mode 100755 index 803ff1a7a28ff..0000000000000 --- a/packages/@aws-cdk/assertions/vendor-in.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/bash - -set -e - -porta_sed() { - local lookup=$1 - local replacement=$2 - local file=$3 - - # inplace replacement of sed is not portable across BSD and GNU. Use backup extension and then delete. - sed -i'.del' "s^${lookup}^${replacement}^g" $file - rm $file.del -} -export -f porta_sed - -echo "⏳ Vendoring in modules..." - -scriptdir=$(cd $(dirname $0) && pwd) -cd $scriptdir - -if [[ "$PHASE" == "transform" ]]; then - # Make the script short-circuit when running in the packages/individual-packages build context. - # That's required because the build done by individual-pkg-gen does import re-writing when copying the TS files - # (because it needs to know which modules are also unstable to do the rewriting correctly), - # but the vendor.sh script runs only after the 'build' script for this package has been invoked, - # which means any TS files copied by it successfully would not have their imports re-written. - exit 0 -fi - -set -euo pipefail -dest="lib/vendored" -mkdir -p $dest - -# cfnspec -mkdir -p $dest/cfnspec -rsync -a --exclude '*.d.ts' --exclude '*.js' ../cfnspec/lib/ $dest/cfnspec/lib -rsync -a ../cfnspec/spec/ $dest/cfnspec/spec - -# cloudformation-diff -rsync -a --exclude '*.d.ts' --exclude '*.js' ../cloudformation-diff/lib/ $dest/cloudformation-diff -find $dest/cloudformation-diff -name '*.ts' | xargs -n1 bash -c 'porta_sed "$@"' _ '@aws-cdk/cfnspec' '../../cfnspec/lib' - -# assert-internal -rsync -a --exclude '*.d.ts' --exclude '*.js' ../assert-internal/lib/ $dest/assert -find $dest/assert -name '*.ts' | xargs -n1 bash -c 'porta_sed "$@"' _ '@aws-cdk/cloudformation-diff' '../../cloudformation-diff' - -# readme -cat > $dest/README.md < { test('channel default properties', () => { new ivs.Channel(stack, 'Channel'); - Template.fromStack(stack).templateMatches({ - Resources: { - Channel4048F119: { - Type: 'AWS::IVS::Channel', - }, - }, + Template.fromStack(stack).hasResource('AWS::IVS::Channel', { + Properties: Match.absentProperty(), }); }); @@ -36,15 +32,8 @@ test('channel name', () => { name: 'CarrotsAreTasty', }); - Template.fromStack(stack).templateMatches({ - Resources: { - Channel4048F119: { - Type: 'AWS::IVS::Channel', - Properties: { - Name: 'CarrotsAreTasty', - }, - }, - }, + Template.fromStack(stack).hasResourceProperties('AWS::IVS::Channel', { + Name: 'CarrotsAreTasty', }); }); @@ -53,15 +42,8 @@ test('channel is authorized', () => { authorized: true, }); - Template.fromStack(stack).templateMatches({ - Resources: { - Channel4048F119: { - Type: 'AWS::IVS::Channel', - Properties: { - Authorized: 'true', - }, - }, - }, + Template.fromStack(stack).hasResourceProperties('AWS::IVS::Channel', { + Authorized: true, }); }); @@ -70,15 +52,8 @@ test('channel type', () => { type: ivs.ChannelType.BASIC, }); - Template.fromStack(stack).templateMatches({ - Resources: { - Channel4048F119: { - Type: 'AWS::IVS::Channel', - Properties: { - Type: 'BASIC', - }, - }, - }, + Template.fromStack(stack).hasResourceProperties('AWS::IVS::Channel', { + Type: 'BASIC', }); }); @@ -87,15 +62,8 @@ test('channel latency mode', () => { latencyMode: ivs.LatencyMode.NORMAL, }); - Template.fromStack(stack).templateMatches({ - Resources: { - Channel4048F119: { - Type: 'AWS::IVS::Channel', - Properties: { - LatencyMode: 'NORMAL', - }, - }, - }, + Template.fromStack(stack).hasResourceProperties('AWS::IVS::Channel', { + LatencyMode: 'NORMAL', }); }); @@ -116,15 +84,8 @@ test('playback key pair mandatory properties', () => { publicKeyMaterial: publicKey, }); - Template.fromStack(stack).templateMatches({ - Resources: { - PlaybackKeyPairBE17315B: { - Type: 'AWS::IVS::PlaybackKeyPair', - Properties: { - PublicKeyMaterial: publicKey, - }, - }, - }, + Template.fromStack(stack).hasResourceProperties('AWS::IVS::PlaybackKeyPair', { + PublicKeyMaterial: publicKey, }); }); @@ -134,16 +95,9 @@ test('playback key pair name', () => { name: 'CarrotsAreNutritious', }); - Template.fromStack(stack).templateMatches({ - Resources: { - PlaybackKeyPairBE17315B: { - Type: 'AWS::IVS::PlaybackKeyPair', - Properties: { - PublicKeyMaterial: publicKey, - Name: 'CarrotsAreNutritious', - }, - }, - }, + Template.fromStack(stack).hasResourceProperties('AWS::IVS::PlaybackKeyPair', { + PublicKeyMaterial: publicKey, + Name: 'CarrotsAreNutritious', }); }); @@ -159,15 +113,8 @@ test('stream key mandatory properties', () => { channel: ivs.Channel.fromChannelArn(stack, 'ChannelRef', 'arn:aws:ivs:us-west-2:123456789012:channel/abcdABCDefgh'), }); - Template.fromStack(stack).templateMatches({ - Resources: { - StreamKey9F296F4F: { - Type: 'AWS::IVS::StreamKey', - Properties: { - ChannelArn: 'arn:aws:ivs:us-west-2:123456789012:channel/abcdABCDefgh', - }, - }, - }, + Template.fromStack(stack).hasResourceProperties('AWS::IVS::StreamKey', { + ChannelArn: 'arn:aws:ivs:us-west-2:123456789012:channel/abcdABCDefgh', }); }); @@ -194,15 +141,8 @@ test('stream key from channel reference', () => { const channel = ivs.Channel.fromChannelArn(stack, 'Channel', 'arn:aws:ivs:us-west-2:123456789012:channel/abcdABCDefgh'); channel.addStreamKey('StreamKey'); - Template.fromStack(stack).templateMatches({ - Resources: { - ChannelStreamKey60BDC2BE: { - Type: 'AWS::IVS::StreamKey', - Properties: { - ChannelArn: 'arn:aws:ivs:us-west-2:123456789012:channel/abcdABCDefgh', - }, - }, - }, + Template.fromStack(stack).hasResourceProperties('AWS::IVS::StreamKey', { + ChannelArn: 'arn:aws:ivs:us-west-2:123456789012:channel/abcdABCDefgh', }); });