From fed30fc815bac1006003524ac6232778f3c3babe Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Mon, 13 Sep 2021 14:23:19 +0100 Subject: [PATCH] feat(assertions): match into serialized json (#16456) Introduce `Match.serializedJson()` that can parse JSON serialized as a string, and continue matching into the parsed JSON. Migrate the rest of the tests in the `pipelines` module. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/assertions/README.md | 41 +++ packages/@aws-cdk/assertions/lib/match.ts | 41 +++ .../@aws-cdk/assertions/test/match.test.ts | 51 +++- packages/@aws-cdk/pipelines/package.json | 1 - .../pipelines/test/compliance/assets.test.ts | 229 ++++++++-------- .../compliance/docker-credentials.test.ts | 65 +++-- .../test/compliance/security-check.test.ts | 127 +++++---- .../test/compliance/self-mutation.test.ts | 57 ++-- .../test/compliance/stack-ordering.test.ts | 91 ++++--- .../pipelines/test/compliance/synths.test.ts | 151 ++++++----- .../test/compliance/validations.test.ts | 247 +++++++++--------- .../pipelines/test/testhelpers/index.ts | 1 - .../pipelines/test/testhelpers/matchers.ts | 63 ++++- .../test/testhelpers/testmatchers.ts | 42 --- 14 files changed, 675 insertions(+), 532 deletions(-) delete mode 100644 packages/@aws-cdk/pipelines/test/testhelpers/testmatchers.ts diff --git a/packages/@aws-cdk/assertions/README.md b/packages/@aws-cdk/assertions/README.md index 1fc0bb28e0cd3..d2763d0ba24ef 100644 --- a/packages/@aws-cdk/assertions/README.md +++ b/packages/@aws-cdk/assertions/README.md @@ -321,6 +321,47 @@ assert.hasResourceProperties('Foo::Bar', Match.objectLike({ }}); ``` +### Serialized JSON + +Often, we find that some CloudFormation Resource types declare properties as a string, +but actually expect JSON serialized as a string. +For example, the [`BuildSpec` property of `AWS::CodeBuild::Project`][Pipeline BuildSpec], +the [`Definition` property of `AWS::StepFunctions::StateMachine`][StateMachine Definition], +to name a couple. + +The `Match.serializedJson()` matcher allows deep matching within a stringified JSON. + +```ts +// Given a template - +// { +// "Resources": { +// "MyBar": { +// "Type": "Foo::Bar", +// "Properties": { +// "Baz": "{ \"Fred\": [\"Waldo\", \"Willow\"] }" +// } +// } +// } +// } + +// The following will NOT throw an assertion error +assert.hasResourceProperties('Foo::Bar', { + Baz: Match.serializedJson({ + Fred: Match.arrayWith(["Waldo"]), + }), +}); + +// The following will throw an assertion error +assert.hasResourceProperties('Foo::Bar', { + Baz: Match.serializedJson({ + Fred: ["Waldo", "Johnny"], + }), +}); +``` + +[Pipeline BuildSpec]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codebuild-project-source.html#cfn-codebuild-project-source-buildspec +[StateMachine Definition]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-stepfunctions-statemachine.html#cfn-stepfunctions-statemachine-definition + ## Capturing Values This matcher APIs documented above allow capturing values in the matching entry diff --git a/packages/@aws-cdk/assertions/lib/match.ts b/packages/@aws-cdk/assertions/lib/match.ts index 5c7e3fad8e90c..4fea0ed0f713e 100644 --- a/packages/@aws-cdk/assertions/lib/match.ts +++ b/packages/@aws-cdk/assertions/lib/match.ts @@ -65,6 +65,14 @@ export abstract class Match { return new NotMatch('not', pattern); } + /** + * Matches any string-encoded JSON and applies the specified pattern after parsing it. + * @param pattern the pattern to match after parsing the encoded JSON. + */ + public static serializedJson(pattern: any): Matcher { + return new SerializedJson('serializedJson', pattern); + } + /** * Matches any non-null value at the target. */ @@ -265,6 +273,39 @@ class ObjectMatch extends Matcher { } } +class SerializedJson extends Matcher { + constructor( + public readonly name: string, + private readonly pattern: any, + ) { + super(); + }; + + public test(actual: any): MatchResult { + const result = new MatchResult(actual); + if (getType(actual) !== 'string') { + result.push(this, [], `Expected JSON as a string but found ${getType(actual)}`); + return result; + } + let parsed; + try { + parsed = JSON.parse(actual); + } catch (err) { + if (err instanceof SyntaxError) { + result.push(this, [], `Invalid JSON string: ${actual}`); + return result; + } else { + throw err; + } + } + + const matcher = Matcher.isMatcher(this.pattern) ? this.pattern : new LiteralMatch(this.name, this.pattern); + const innerResult = matcher.test(parsed); + result.compose(`(${this.name})`, innerResult); + return result; + } +} + class NotMatch extends Matcher { constructor( public readonly name: string, diff --git a/packages/@aws-cdk/assertions/test/match.test.ts b/packages/@aws-cdk/assertions/test/match.test.ts index 268810857f9a8..b0c92a2da2c8f 100644 --- a/packages/@aws-cdk/assertions/test/match.test.ts +++ b/packages/@aws-cdk/assertions/test/match.test.ts @@ -323,18 +323,63 @@ describe('Matchers', () => { expectFailure(matcher, {}, ['Missing key at /foo']); }); }); + + describe('serializedJson()', () => { + let matcher: Matcher; + + test('all types', () => { + matcher = Match.serializedJson({ Foo: 'Bar', Baz: 3, Boo: true, Fred: [1, 2] }); + expectPass(matcher, '{ "Foo": "Bar", "Baz": 3, "Boo": true, "Fred": [1, 2] }'); + }); + + test('simple match', () => { + matcher = Match.serializedJson({ Foo: 'Bar' }); + expectPass(matcher, '{ "Foo": "Bar" }'); + + expectFailure(matcher, '{ "Foo": "Baz" }', ['Expected Bar but received Baz at (serializedJson)/Foo']); + expectFailure(matcher, '{ "Foo": 4 }', ['Expected type string but received number at (serializedJson)/Foo']); + expectFailure(matcher, '{ "Bar": "Baz" }', [ + 'Unexpected key at (serializedJson)/Bar', + 'Missing key at (serializedJson)/Foo', + ]); + }); + + test('nested matcher', () => { + matcher = Match.serializedJson(Match.objectLike({ + Foo: Match.arrayWith(['Bar']), + })); + + expectPass(matcher, '{ "Foo": ["Bar"] }'); + expectPass(matcher, '{ "Foo": ["Bar", "Baz"] }'); + expectPass(matcher, '{ "Foo": ["Bar", "Baz"], "Fred": "Waldo" }'); + + expectFailure(matcher, '{ "Foo": ["Baz"] }', ['Missing element [Bar] at pattern index 0 at (serializedJson)/Foo']); + expectFailure(matcher, '{ "Bar": ["Baz"] }', ['Missing key at (serializedJson)/Foo']); + }); + + test('invalid json string', () => { + matcher = Match.serializedJson({ Foo: 'Bar' }); + + expectFailure(matcher, '{ "Foo"', [/invalid JSON string/i]); + }); + }); }); function expectPass(matcher: Matcher, target: any): void { - expect(matcher.test(target).hasFailed()).toEqual(false); + const result = matcher.test(target); + if (result.hasFailed()) { + fail(result.toHumanStrings()); // eslint-disable-line jest/no-jasmine-globals + } } function expectFailure(matcher: Matcher, target: any, expected: (string | RegExp)[] = []): void { const result = matcher.test(target); expect(result.failCount).toBeGreaterThan(0); const actual = result.toHumanStrings(); - if (expected.length > 0) { - expect(actual.length).toEqual(expected.length); + if (expected.length > 0 && actual.length !== expected.length) { + // only do this if the lengths are different, so as to display a nice failure message. + // otherwise need to use `toMatch()` to support RegExp + expect(actual).toEqual(expected); } for (let i = 0; i < expected.length; i++) { const e = expected[i]; diff --git a/packages/@aws-cdk/pipelines/package.json b/packages/@aws-cdk/pipelines/package.json index 37605df5210d4..7daeb0589feb0 100644 --- a/packages/@aws-cdk/pipelines/package.json +++ b/packages/@aws-cdk/pipelines/package.json @@ -32,7 +32,6 @@ "organization": true }, "devDependencies": { - "@aws-cdk/assert-internal": "0.0.0", "@aws-cdk/assertions": "0.0.0", "@aws-cdk/aws-apigateway": "0.0.0", "@aws-cdk/aws-ecr-assets": "0.0.0", diff --git a/packages/@aws-cdk/pipelines/test/compliance/assets.test.ts b/packages/@aws-cdk/pipelines/test/compliance/assets.test.ts index c1b72cf7ab316..68b10d259683f 100644 --- a/packages/@aws-cdk/pipelines/test/compliance/assets.test.ts +++ b/packages/@aws-cdk/pipelines/test/compliance/assets.test.ts @@ -1,11 +1,10 @@ import * as fs from 'fs'; import * as path from 'path'; -import { arrayWith, Capture, deepObjectLike, encodedJson, notMatching, objectLike, ResourcePart, stringLike, SynthUtils } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Capture, Match, Template } from '@aws-cdk/assertions'; import * as cb from '@aws-cdk/aws-codebuild'; import * as ec2 from '@aws-cdk/aws-ec2'; -import { Stack } from '@aws-cdk/core'; -import { behavior, PIPELINE_ENV, TestApp, LegacyTestGitHubNpmPipeline, ModernTestGitHubNpmPipeline, FileAssetApp, MegaAssetsApp, TwoFileAssetsApp, DockerAssetApp, PlainStackApp } from '../testhelpers'; +import { Stack, Stage } from '@aws-cdk/core'; +import { behavior, PIPELINE_ENV, TestApp, LegacyTestGitHubNpmPipeline, ModernTestGitHubNpmPipeline, FileAssetApp, MegaAssetsApp, TwoFileAssetsApp, DockerAssetApp, PlainStackApp, stringLike } from '../testhelpers'; const FILE_ASSET_SOURCE_HASH = '8289faf53c7da377bb2b90615999171adef5e1d8f6b88810e5fef75e6ca09ba5'; const FILE_ASSET_SOURCE_HASH2 = 'ac76997971c3f6ddf37120660003f1ced72b4fc58c498dfd99c78fa77e721e0e'; @@ -42,10 +41,10 @@ describe('basic pipeline', () => { function THEN_codePipelineExpectation() { // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: notMatching(arrayWith(objectLike({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.not(Match.arrayWith([Match.objectLike({ Name: 'Assets', - }))), + })])), }); } }); @@ -67,13 +66,13 @@ describe('basic pipeline', () => { }); function THEN_codePipelineExpectation() { - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: [ - objectLike({ Name: 'Source' }), - objectLike({ Name: 'Build' }), - objectLike({ Name: 'UpdatePipeline' }), - objectLike({ Name: 'Assets' }), - objectLike({ Name: 'App' }), + Match.objectLike({ Name: 'Source' }), + Match.objectLike({ Name: 'Build' }), + Match.objectLike({ Name: 'UpdatePipeline' }), + Match.objectLike({ Name: 'Assets' }), + Match.objectLike({ Name: 'App' }), ], }); } @@ -96,13 +95,13 @@ describe('basic pipeline', () => { }); function THEN_codePipelineExpectation() { - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: [ - objectLike({ Name: 'Source' }), - objectLike({ Name: 'Build' }), - objectLike({ Name: 'UpdatePipeline' }), - objectLike({ Name: 'Assets' }), - objectLike({ Name: 'App' }), + Match.objectLike({ Name: 'Source' }), + Match.objectLike({ Name: 'Build' }), + Match.objectLike({ Name: 'UpdatePipeline' }), + Match.objectLike({ Name: 'Assets' }), + Match.objectLike({ Name: 'App' }), ], }); } @@ -126,14 +125,14 @@ describe('basic pipeline', () => { }); function THEN_codePipelineExpectation() { - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: [ - objectLike({ Name: 'Source' }), - objectLike({ Name: 'Build' }), - objectLike({ Name: 'UpdatePipeline' }), - objectLike({ Name: stringLike('Assets*') }), - objectLike({ Name: stringLike('Assets*2') }), - objectLike({ Name: 'App' }), + Match.objectLike({ Name: 'Source' }), + Match.objectLike({ Name: 'Build' }), + Match.objectLike({ Name: 'UpdatePipeline' }), + Match.objectLike({ Name: stringLike('Assets*') }), + Match.objectLike({ Name: stringLike('Assets*2') }), + Match.objectLike({ Name: 'App' }), ], }); } @@ -155,15 +154,15 @@ describe('basic pipeline', () => { }); function THEN_codePipelineExpectation() { - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: [ - objectLike({ Name: 'Source' }), - objectLike({ Name: 'Build' }), - objectLike({ Name: 'UpdatePipeline' }), - objectLike({ Name: stringLike('Assets*') }), // 'Assets' vs 'Assets.1' - objectLike({ Name: stringLike('Assets*2') }), - objectLike({ Name: stringLike('Assets*3') }), - objectLike({ Name: 'App' }), + Match.objectLike({ Name: 'Source' }), + Match.objectLike({ Name: 'Build' }), + Match.objectLike({ Name: 'UpdatePipeline' }), + Match.objectLike({ Name: stringLike('Assets*') }), // 'Assets' vs 'Assets.1' + Match.objectLike({ Name: stringLike('Assets*2') }), + Match.objectLike({ Name: stringLike('Assets*3') }), + Match.objectLike({ Name: 'App' }), ], }); } @@ -186,15 +185,15 @@ describe('basic pipeline', () => { }); function THEN_codePipelineExpectation() { - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: 'aws/codebuild/standard:5.0', }, Source: { - BuildSpec: encodedJson(deepObjectLike({ + BuildSpec: Match.serializedJson(Match.objectLike({ phases: { build: { - commands: arrayWith(`cdk-assets --path "assembly-FileAssetApp/FileAssetAppStackEADD68C5.assets.json" --verbose publish "${FILE_ASSET_SOURCE_HASH}:current_account-current_region"`), + commands: Match.arrayWith([`cdk-assets --path "assembly-FileAssetApp/FileAssetAppStackEADD68C5.assets.json" --verbose publish "${FILE_ASSET_SOURCE_HASH}:current_account-current_region"`]), }, }, })), @@ -220,14 +219,14 @@ describe('basic pipeline', () => { }); function THEN_codePipelineExpectation() { - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'Assets', Actions: [ - objectLike({ RunOrder: 1 }), - objectLike({ RunOrder: 1 }), + Match.objectLike({ RunOrder: 1 }), + Match.objectLike({ RunOrder: 1 }), ], - }), + }]), }); } }); @@ -242,16 +241,16 @@ describe('basic pipeline', () => { pipeline.addStage('SomeStage').addStackArtifactDeployment(asm.getStackByName('FileAssetApp-Stack')); // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'Assets', Actions: [ - objectLike({ + Match.objectLike({ Name: 'FileAsset1', RunOrder: 1, }), ], - }), + }]), }); }); @@ -277,17 +276,17 @@ describe('basic pipeline', () => { }); function THEN_codePipelineExpectation() { - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { Source: { - BuildSpec: encodedJson(deepObjectLike({ + BuildSpec: Match.serializedJson(Match.objectLike({ phases: { build: { - commands: arrayWith(stringLike('cdk-assets *')), + commands: Match.arrayWith([stringLike('cdk-assets *')]), }, }, })), }, - Environment: objectLike({ + Environment: Match.objectLike({ PrivilegedMode: false, Image: 'aws/codebuild/standard:5.0', }), @@ -311,17 +310,17 @@ describe('basic pipeline', () => { }); function THEN_codePipelineExpectation() { - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { Source: { - BuildSpec: encodedJson(deepObjectLike({ + BuildSpec: Match.serializedJson(Match.objectLike({ phases: { build: { - commands: arrayWith(stringLike('cdk-assets *')), + commands: Match.arrayWith([stringLike('cdk-assets *')]), }, }, })), }, - Environment: objectLike({ + Environment: Match.objectLike({ Image: 'aws/codebuild/standard:5.0', PrivilegedMode: true, }), @@ -349,12 +348,12 @@ describe('basic pipeline', () => { }); function THEN_codePipelineExpectation() { - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: 'aws/codebuild/standard:5.0', }, Source: { - BuildSpec: encodedJson(deepObjectLike({ + BuildSpec: Match.serializedJson(Match.objectLike({ phases: { install: { commands: ['npm install -g cdk-assets@1.2.3'], @@ -386,7 +385,7 @@ describe('basic pipeline', () => { }); function THEN_codePipelineExpectation() { - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Role', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [{ Action: 'sts:AssumeRole', @@ -402,7 +401,7 @@ describe('basic pipeline', () => { }], }, }); - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', + Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', expectedAssetRolePolicy(FILE_PUBLISHING_ROLE, 'CdkAssetsFileRole6BE17A07')); } }); @@ -439,7 +438,7 @@ describe('basic pipeline', () => { }); function THEN_codePipelineExpectation() { - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', + Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', expectedAssetRolePolicy([FILE_PUBLISHING_ROLE, 'arn:${AWS::Partition}:iam::0123456789012:role/cdk-hnb659fds-file-publishing-role-0123456789012-eu-west-1'], 'CdkAssetsFileRole6BE17A07')); } @@ -468,7 +467,7 @@ describe('basic pipeline', () => { }); function THEN_codePipelineExpectation() { - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', + Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', expectedAssetRolePolicy(FILE_PUBLISHING_ROLE, 'CdkAssetsFileRole6BE17A07')); } }); @@ -492,7 +491,7 @@ describe('basic pipeline', () => { }); function THEN_codePipelineExpectation() { - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Role', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [{ Action: 'sts:AssumeRole', @@ -508,7 +507,7 @@ describe('basic pipeline', () => { }], }, }); - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', + Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', expectedAssetRolePolicy(IMAGE_PUBLISHING_ROLE, 'CdkAssetsDockerRole484B6DD3')); } }); @@ -534,9 +533,9 @@ describe('basic pipeline', () => { }); function THEN_codePipelineExpectation() { - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', + Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', expectedAssetRolePolicy(FILE_PUBLISHING_ROLE, 'CdkAssetsFileRole6BE17A07')); - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', + Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', expectedAssetRolePolicy(IMAGE_PUBLISHING_ROLE, 'CdkAssetsDockerRole484B6DD3')); } }); @@ -576,12 +575,12 @@ behavior('can supply pre-install scripts to asset upload', (suite) => { }); function THEN_codePipelineExpectation() { - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: 'aws/codebuild/standard:5.0', }, Source: { - BuildSpec: encodedJson(deepObjectLike({ + BuildSpec: Match.serializedJson(Match.objectLike({ phases: { install: { commands: ['npm config set registry https://registry.com', 'npm install -g cdk-assets'], @@ -620,8 +619,8 @@ describe('pipeline with VPC', () => { function THEN_codePipelineExpectation() { // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - VpcConfig: objectLike({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { + VpcConfig: Match.objectLike({ SecurityGroupIds: [ { 'Fn::GetAtt': ['CdkAssetsDockerAsset1SecurityGroup078F5C66', 'GroupId'] }, ], @@ -655,16 +654,16 @@ describe('pipeline with VPC', () => { function THEN_codePipelineExpectation() { // Assets Project - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', { Roles: [ { Ref: 'CdkAssetsDockerRole484B6DD3' }, ], PolicyDocument: { - Statement: arrayWith({ - Action: arrayWith('ec2:DescribeSecurityGroups'), + Statement: Match.arrayWith([{ + Action: Match.arrayWith(['ec2:DescribeSecurityGroups']), Effect: 'Allow', Resource: '*', - }), + }]), }, }); } @@ -690,10 +689,10 @@ describe('pipeline with VPC', () => { function THEN_codePipelineExpectation() { // Assets Project - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ - { + Match.objectLike({ Resource: '*', Action: [ 'ec2:CreateNetworkInterface', @@ -704,19 +703,19 @@ describe('pipeline with VPC', () => { 'ec2:DescribeDhcpOptions', 'ec2:DescribeVpcs', ], - }, + }), ], }, Roles: [{ Ref: 'CdkAssetsDockerRole484B6DD3' }], }); - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack).hasResource('AWS::CodeBuild::Project', { Properties: { ServiceRole: { 'Fn::GetAtt': ['CdkAssetsDockerRole484B6DD3', 'Arn'] }, }, DependsOn: [ 'CdkAssetsDockerAsset1PolicyDocument8DA96A22', ], - }, ResourcePart.CompleteDefinition); + }); } }); }); @@ -743,28 +742,29 @@ describe('pipeline with single asset publisher', () => { function THEN_codePipelineExpectation() { // THEN - const buildSpecName = Capture.aString(); - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + const buildSpecName = new Capture(); + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'Assets', Actions: [ // Only one file asset action - objectLike({ RunOrder: 1, Name: 'FileAsset' }), + Match.objectLike({ RunOrder: 1, Name: 'FileAsset' }), ], - }), + }]), }); - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: 'aws/codebuild/standard:5.0', }, Source: { - BuildSpec: buildSpecName.capture(stringLike('buildspec-*.yaml')), + BuildSpec: buildSpecName, }, }); - const assembly = SynthUtils.synthesize(pipelineStack, { skipValidation: true }).assembly; + const assembly = synthesize(pipelineStack); - const actualFileName = buildSpecName.capturedValue; + const actualFileName = buildSpecName.asString(); + expect(actualFileName).toMatch(/^buildspec-.*\.yaml$/); const buildSpec = JSON.parse(fs.readFileSync(path.join(assembly.directory, actualFileName), { encoding: 'utf-8' })); expect(buildSpec.phases.build.commands).toContain(`cdk-assets --path "assembly-FileAssetApp/FileAssetAppStackEADD68C5.assets.json" --verbose publish "${FILE_ASSET_SOURCE_HASH}:current_account-current_region"`); expect(buildSpec.phases.build.commands).toContain(`cdk-assets --path "assembly-FileAssetApp/FileAssetAppStackEADD68C5.assets.json" --verbose publish "${FILE_ASSET_SOURCE_HASH2}:current_account-current_region"`); @@ -804,20 +804,20 @@ describe('pipeline with single asset publisher', () => { function THEN_codePipelineExpectation(pipelineStack2: Stack) { // THEN - const buildSpecName1 = Capture.aString(); - const buildSpecName2 = Capture.aString(); - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + const buildSpecName1 = new Capture(); + const buildSpecName2 = new Capture(); + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { Source: { - BuildSpec: buildSpecName1.capture(stringLike('buildspec-*.yaml')), + BuildSpec: buildSpecName1, }, }); - expect(pipelineStack2).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack2).hasResourceProperties('AWS::CodeBuild::Project', { Source: { - BuildSpec: buildSpecName2.capture(stringLike('buildspec-*.yaml')), + BuildSpec: buildSpecName2, }, }); - expect(buildSpecName1.capturedValue).not.toEqual(buildSpecName2.capturedValue); + expect(buildSpecName1.asString()).not.toEqual(buildSpecName2.asString()); } }); }); @@ -870,27 +870,27 @@ describe('pipeline with custom asset publisher BuildSpec', () => { function THEN_codePipelineExpectation() { - const buildSpecName = Capture.aString(); + const buildSpecName = new Capture(); - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'Assets', Actions: [ // Only one file asset action - objectLike({ RunOrder: 1, Name: 'FileAsset' }), + Match.objectLike({ RunOrder: 1, Name: 'FileAsset' }), ], - }), + }]), }); - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: 'aws/codebuild/standard:5.0', }, Source: { - BuildSpec: buildSpecName.capture(stringLike('buildspec-*.yaml')), + BuildSpec: buildSpecName, }, }); - const assembly = SynthUtils.synthesize(pipelineStack, { skipValidation: true }).assembly; - const buildSpec = JSON.parse(fs.readFileSync(path.join(assembly.directory, buildSpecName.capturedValue)).toString()); + const assembly = synthesize(pipelineStack); + const buildSpec = JSON.parse(fs.readFileSync(path.join(assembly.directory, buildSpecName.asString())).toString()); expect(buildSpec.phases.build.commands).toContain(`cdk-assets --path "assembly-FileAssetApp/FileAssetAppStackEADD68C5.assets.json" --verbose publish "${FILE_ASSET_SOURCE_HASH}:current_account-current_region"`); expect(buildSpec.phases.build.commands).toContain(`cdk-assets --path "assembly-FileAssetApp/FileAssetAppStackEADD68C5.assets.json" --verbose publish "${FILE_ASSET_SOURCE_HASH2}:current_account-current_region"`); expect(buildSpec.phases.pre_install.commands).toContain('preinstall'); @@ -978,9 +978,9 @@ behavior('necessary secrets manager permissions get added to asset roles', suite }); function THEN_codePipelineExpectation() { - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith({ + Statement: Match.arrayWith([{ Action: 'secretsmanager:GetSecretValue', Effect: 'Allow', Resource: { @@ -993,7 +993,7 @@ behavior('necessary secrets manager permissions get added to asset roles', suite ], ], }, - }), + }]), }, Roles: [ { Ref: 'PipelineAssetsFileRole59943A77' }, @@ -1021,10 +1021,10 @@ behavior('adding environment variable to assets job adds SecretsManager permissi }); pipeline.addStage(new FileAssetApp(pipelineStack, 'MyApp')); - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith( - objectLike({ + Statement: Match.arrayWith([ + Match.objectLike({ Action: 'secretsmanager:GetSecretValue', Effect: 'Allow', Resource: { @@ -1035,8 +1035,17 @@ behavior('adding environment variable to assets job adds SecretsManager permissi ]], }, }), - ), + ]), }, }); }); -}); \ No newline at end of file +}); + +function synthesize(stack: Stack) { + const root = stack.node.root; + if (!Stage.isStage(root)) { + throw new Error('unexpected: all stacks must be part of a Stage'); + } + + return root.synth({ skipValidation: true }); +} \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/compliance/docker-credentials.test.ts b/packages/@aws-cdk/pipelines/test/compliance/docker-credentials.test.ts index 5ada88b49b937..e1356304fe811 100644 --- a/packages/@aws-cdk/pipelines/test/compliance/docker-credentials.test.ts +++ b/packages/@aws-cdk/pipelines/test/compliance/docker-credentials.test.ts @@ -1,12 +1,11 @@ -import { arrayWith, deepObjectLike, encodedJson, stringLike } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as cb from '@aws-cdk/aws-codebuild'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; import { Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import * as cdkp from '../../lib'; import { CodeBuildStep } from '../../lib'; -import { behavior, PIPELINE_ENV, TestApp, LegacyTestGitHubNpmPipeline, ModernTestGitHubNpmPipeline, DockerAssetApp } from '../testhelpers'; +import { behavior, PIPELINE_ENV, TestApp, LegacyTestGitHubNpmPipeline, ModernTestGitHubNpmPipeline, DockerAssetApp, stringLike } from '../testhelpers'; const secretSynthArn = 'arn:aws:secretsmanager:eu-west-1:0123456789012:secret:synth-012345'; const secretUpdateArn = 'arn:aws:secretsmanager:eu-west-1:0123456789012:secret:update-012345'; @@ -51,32 +50,32 @@ behavior('synth action receives install commands and access to relevant credenti domainCredentials: { 'synth.example.com': { secretsManagerSecretId: secretSynthArn } }, }); - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: 'aws/codebuild/standard:5.0' }, Source: { - BuildSpec: encodedJson(deepObjectLike({ + BuildSpec: Match.serializedJson(Match.objectLike({ phases: { pre_build: { - commands: arrayWith( + commands: Match.arrayWith([ 'mkdir $HOME/.cdk', `echo '${expectedCredsConfig}' > $HOME/.cdk/cdk-docker-creds.json`, - ), + ]), }, // Prove we're looking at the Synth project build: { - commands: arrayWith(stringLike('*cdk*synth*')), + commands: Match.arrayWith([stringLike('*cdk*synth*')]), }, }, })), }, }); - expect(pipelineStack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith({ + Statement: Match.arrayWith([{ Action: ['secretsmanager:GetSecretValue', 'secretsmanager:DescribeSecret'], Effect: 'Allow', Resource: secretSynthArn, - }), + }]), Version: '2012-10-17', }, Roles: [{ Ref: stringLike('Cdk*BuildProjectRole*') }], @@ -121,20 +120,20 @@ behavior('synth action receives Windows install commands if a Windows image is d domainCredentials: { 'synth.example.com': { secretsManagerSecretId: secretSynthArn } }, }); - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: 'aws/codebuild/windows-base:2.0' }, Source: { - BuildSpec: encodedJson(deepObjectLike({ + BuildSpec: Match.serializedJson(Match.objectLike({ phases: { pre_build: { - commands: arrayWith( + commands: Match.arrayWith([ 'mkdir %USERPROFILE%\\.cdk', `echo '${expectedCredsConfig}' > %USERPROFILE%\\.cdk\\cdk-docker-creds.json`, - ), + ]), }, // Prove we're looking at the Synth project build: { - commands: arrayWith(stringLike('*cdk*synth*')), + commands: Match.arrayWith([stringLike('*cdk*synth*')]), }, }, })), @@ -164,34 +163,34 @@ behavior('self-update receives install commands and access to relevant credentia domainCredentials: { 'selfupdate.example.com': { secretsManagerSecretId: secretUpdateArn } }, }); - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: 'aws/codebuild/standard:5.0' }, Source: { - BuildSpec: encodedJson(deepObjectLike({ + BuildSpec: Match.serializedJson(Match.objectLike({ phases: { [expectedPhase]: { - commands: arrayWith( + commands: Match.arrayWith([ 'mkdir $HOME/.cdk', `echo '${expectedCredsConfig}' > $HOME/.cdk/cdk-docker-creds.json`, - ), + ]), }, // Prove we're looking at the SelfMutate project build: { - commands: arrayWith( + commands: Match.arrayWith([ stringLike('cdk * deploy PipelineStack*'), - ), + ]), }, }, })), }, }); - expect(pipelineStack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith({ + Statement: Match.arrayWith([{ Action: ['secretsmanager:GetSecretValue', 'secretsmanager:DescribeSecret'], Effect: 'Allow', Resource: secretUpdateArn, - }), + }]), Version: '2012-10-17', }, Roles: [{ Ref: stringLike('*SelfMutat*Role*') }], @@ -220,32 +219,32 @@ behavior('asset publishing receives install commands and access to relevant cred domainCredentials: { 'publish.example.com': { secretsManagerSecretId: secretPublishArn } }, }); - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: 'aws/codebuild/standard:5.0' }, Source: { - BuildSpec: encodedJson(deepObjectLike({ + BuildSpec: Match.serializedJson(Match.objectLike({ phases: { [expectedPhase]: { - commands: arrayWith( + commands: Match.arrayWith([ 'mkdir $HOME/.cdk', `echo '${expectedCredsConfig}' > $HOME/.cdk/cdk-docker-creds.json`, - ), + ]), }, // Prove we're looking at the Publishing project build: { - commands: arrayWith(stringLike('cdk-assets*')), + commands: Match.arrayWith([stringLike('cdk-assets*')]), }, }, })), }, }); - expect(pipelineStack).toHaveResource('AWS::IAM::Policy', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith({ + Statement: Match.arrayWith([{ Action: ['secretsmanager:GetSecretValue', 'secretsmanager:DescribeSecret'], Effect: 'Allow', Resource: secretPublishArn, - }), + }]), Version: '2012-10-17', }, Roles: [{ Ref: 'CdkAssetsDockerRole484B6DD3' }], diff --git a/packages/@aws-cdk/pipelines/test/compliance/security-check.test.ts b/packages/@aws-cdk/pipelines/test/compliance/security-check.test.ts index 7367930e6618a..d2ea77f45ff7d 100644 --- a/packages/@aws-cdk/pipelines/test/compliance/security-check.test.ts +++ b/packages/@aws-cdk/pipelines/test/compliance/security-check.test.ts @@ -1,9 +1,8 @@ -import { anything, arrayWith, encodedJson, objectLike, stringLike } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import { Topic } from '@aws-cdk/aws-sns'; import { Stack } from '@aws-cdk/core'; import * as cdkp from '../../lib'; -import { LegacyTestGitHubNpmPipeline, ModernTestGitHubNpmPipeline, OneStackApp, PIPELINE_ENV, TestApp } from '../testhelpers'; +import { LegacyTestGitHubNpmPipeline, ModernTestGitHubNpmPipeline, OneStackApp, PIPELINE_ENV, TestApp, stringLike } from '../testhelpers'; import { behavior } from '../testhelpers/compliance'; let app: TestApp; @@ -41,8 +40,8 @@ behavior('security check option generates lambda/codebuild at pipeline scope', ( }); function THEN_codePipelineExpectation() { - expect(pipelineStack).toCountResources('AWS::Lambda::Function', 1); - expect(pipelineStack).toHaveResourceLike('AWS::Lambda::Function', { + Template.fromStack(pipelineStack).resourceCountIs('AWS::Lambda::Function', 1); + Template.fromStack(pipelineStack).hasResourceProperties('AWS::Lambda::Function', { Role: { 'Fn::GetAtt': [ stringLike('CdkPipeline*SecurityCheckCDKPipelinesAutoApproveServiceRole*'), @@ -51,7 +50,7 @@ behavior('security check option generates lambda/codebuild at pipeline scope', ( }, }); // 1 for github build, 1 for synth stage, and 1 for the application security check - expect(pipelineStack).toCountResources('AWS::CodeBuild::Project', 3); + Template.fromStack(pipelineStack).resourceCountIs('AWS::CodeBuild::Project', 3); } }); @@ -78,24 +77,24 @@ behavior('security check option passes correct environment variables to check pr }); function THEN_codePipelineExpectation() { - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith( + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([ { Name: 'App', - Actions: arrayWith( - objectLike({ + Actions: Match.arrayWith([ + Match.objectLike({ Name: stringLike('*Check'), - Configuration: objectLike({ - EnvironmentVariables: encodedJson([ + Configuration: Match.objectLike({ + EnvironmentVariables: Match.serializedJson([ { name: 'STAGE_PATH', type: 'PLAINTEXT', value: 'PipelineSecurityStack/App' }, { name: 'STAGE_NAME', type: 'PLAINTEXT', value: 'App' }, - { name: 'ACTION_NAME', type: 'PLAINTEXT', value: anything() }, + { name: 'ACTION_NAME', type: 'PLAINTEXT', value: Match.anyValue() }, ]), }), }), - ), + ]), }, - ), + ]), }); } }); @@ -124,7 +123,7 @@ behavior('pipeline created with auto approve tags and lambda/codebuild w/ valid function THEN_codePipelineExpectation() { // CodePipeline must be tagged as SECURITY_CHECK=ALLOW_APPROVE - expect(pipelineStack).toHaveResource('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { Tags: [ { Key: 'SECURITY_CHECK', @@ -133,7 +132,7 @@ behavior('pipeline created with auto approve tags and lambda/codebuild w/ valid ], }); // Lambda Function only has access to pipelines tagged SECURITY_CHECK=ALLOW_APPROVE - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -148,9 +147,9 @@ behavior('pipeline created with auto approve tags and lambda/codebuild w/ valid }, }); // CodeBuild must have access to the stacks and invoking the lambda function - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith( + Statement: Match.arrayWith([ { Action: 'sts:AssumeRole', Condition: { @@ -173,7 +172,7 @@ behavior('pipeline created with auto approve tags and lambda/codebuild w/ valid ], }, }, - ), + ]), }, }); } @@ -193,32 +192,32 @@ behavior('confirmBroadeningPermissions option at addApplicationStage runs securi suite.doesNotApply.modern(); function THEN_codePipelineExpectation() { - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: [ { - Actions: [{ Name: 'GitHub', RunOrder: 1 }], + Actions: [Match.objectLike({ Name: 'GitHub', RunOrder: 1 })], Name: 'Source', }, { - Actions: [{ Name: 'Synth', RunOrder: 1 }], + Actions: [Match.objectLike({ Name: 'Synth', RunOrder: 1 })], Name: 'Build', }, { - Actions: [{ Name: 'SelfMutate', RunOrder: 1 }], + Actions: [Match.objectLike({ Name: 'SelfMutate', RunOrder: 1 })], Name: 'UpdatePipeline', }, { Actions: [ - { Name: 'StageSecurityCheckStackSecurityCheck', RunOrder: 1 }, - { Name: 'StageSecurityCheckStackManualApproval', RunOrder: 2 }, - { Name: 'AnotherStackSecurityCheck', RunOrder: 5 }, - { Name: 'AnotherStackManualApproval', RunOrder: 6 }, - { Name: 'Stack.Prepare', RunOrder: 3 }, - { Name: 'Stack.Deploy', RunOrder: 4 }, - { Name: 'AnotherStack-Stack.Prepare', RunOrder: 7 }, - { Name: 'AnotherStack-Stack.Deploy', RunOrder: 8 }, - { Name: 'SkipCheckStack-Stack.Prepare', RunOrder: 9 }, - { Name: 'SkipCheckStack-Stack.Deploy', RunOrder: 10 }, + Match.objectLike({ Name: 'StageSecurityCheckStackSecurityCheck', RunOrder: 1 }), + Match.objectLike({ Name: 'StageSecurityCheckStackManualApproval', RunOrder: 2 }), + Match.objectLike({ Name: 'AnotherStackSecurityCheck', RunOrder: 5 }), + Match.objectLike({ Name: 'AnotherStackManualApproval', RunOrder: 6 }), + Match.objectLike({ Name: 'Stack.Prepare', RunOrder: 3 }), + Match.objectLike({ Name: 'Stack.Deploy', RunOrder: 4 }), + Match.objectLike({ Name: 'AnotherStack-Stack.Prepare', RunOrder: 7 }), + Match.objectLike({ Name: 'AnotherStack-Stack.Deploy', RunOrder: 8 }), + Match.objectLike({ Name: 'SkipCheckStack-Stack.Prepare', RunOrder: 9 }), + Match.objectLike({ Name: 'SkipCheckStack-Stack.Deploy', RunOrder: 10 }), ], Name: 'StageSecurityCheckStack', }, @@ -240,28 +239,28 @@ behavior('confirmBroadeningPermissions option at addApplication runs security ch suite.doesNotApply.modern(); function THEN_codePipelineExpectation() { - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { Stages: [ { - Actions: [{ Name: 'GitHub', RunOrder: 1 }], + Actions: [Match.objectLike({ Name: 'GitHub', RunOrder: 1 })], Name: 'Source', }, { - Actions: [{ Name: 'Synth', RunOrder: 1 }], + Actions: [Match.objectLike({ Name: 'Synth', RunOrder: 1 })], Name: 'Build', }, { - Actions: [{ Name: 'SelfMutate', RunOrder: 1 }], + Actions: [Match.objectLike({ Name: 'SelfMutate', RunOrder: 1 })], Name: 'UpdatePipeline', }, { Actions: [ - { Name: 'EnableCheckStackSecurityCheck', RunOrder: 3 }, - { Name: 'EnableCheckStackManualApproval', RunOrder: 4 }, - { Name: 'Stack.Prepare', RunOrder: 1 }, - { Name: 'Stack.Deploy', RunOrder: 2 }, - { Name: 'EnableCheckStack-Stack.Prepare', RunOrder: 5 }, - { Name: 'EnableCheckStack-Stack.Deploy', RunOrder: 6 }, + Match.objectLike({ Name: 'EnableCheckStackSecurityCheck', RunOrder: 3 }), + Match.objectLike({ Name: 'EnableCheckStackManualApproval', RunOrder: 4 }), + Match.objectLike({ Name: 'Stack.Prepare', RunOrder: 1 }), + Match.objectLike({ Name: 'Stack.Deploy', RunOrder: 2 }), + Match.objectLike({ Name: 'EnableCheckStack-Stack.Prepare', RunOrder: 5 }), + Match.objectLike({ Name: 'EnableCheckStack-Stack.Deploy', RunOrder: 6 }), ], Name: 'NoSecurityCheckStack', }, @@ -299,13 +298,13 @@ behavior('confirmBroadeningPermissions and notification topic options generates }); function THEN_codePipelineExpectation() { - expect(pipelineStack).toCountResources('AWS::SNS::Topic', 1); - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith( + Template.fromStack(pipelineStack).resourceCountIs('AWS::SNS::Topic', 1); + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([ { Name: 'MyStack', Actions: [ - objectLike({ + Match.objectLike({ Configuration: { ProjectName: { Ref: stringLike('*SecurityCheck*') }, EnvironmentVariables: { @@ -320,7 +319,7 @@ behavior('confirmBroadeningPermissions and notification topic options generates Namespace: stringLike('*'), RunOrder: 1, }), - objectLike({ + Match.objectLike({ Configuration: { CustomData: stringLike('#{*.MESSAGE}'), ExternalEntityLink: stringLike('#{*.LINK}'), @@ -328,11 +327,11 @@ behavior('confirmBroadeningPermissions and notification topic options generates Name: stringLike('*Approv*'), RunOrder: 2, }), - objectLike({ Name: 'Stack.Prepare', RunOrder: 3 }), - objectLike({ Name: 'Stack.Deploy', RunOrder: 4 }), + Match.objectLike({ Name: 'Stack.Prepare', RunOrder: 3 }), + Match.objectLike({ Name: 'Stack.Deploy', RunOrder: 4 }), ], }, - ), + ]), }); } }); @@ -365,10 +364,10 @@ behavior('Stages declared outside the pipeline create their own ApplicationSecur suite.doesNotApply.modern(); function THEN_codePipelineExpectation() { - expect(pipelineStack).toCountResources('AWS::Lambda::Function', 1); + Template.fromStack(pipelineStack).resourceCountIs('AWS::Lambda::Function', 1); // 1 for github build, 1 for synth stage, and 1 for the application security check - expect(pipelineStack).toCountResources('AWS::CodeBuild::Project', 3); - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Template.fromStack(pipelineStack).resourceCountIs('AWS::CodeBuild::Project', 3); + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { Tags: [ { Key: 'SECURITY_CHECK', @@ -376,28 +375,28 @@ behavior('Stages declared outside the pipeline create their own ApplicationSecur }, ], Stages: [ - { Name: 'Source' }, - { Name: 'Build' }, - { Name: 'UpdatePipeline' }, + Match.objectLike({ Name: 'Source' }), + Match.objectLike({ Name: 'Build' }), + Match.objectLike({ Name: 'UpdatePipeline' }), { Actions: [ - { + Match.objectLike({ Configuration: { ProjectName: { Ref: 'UnattachedStageStageApplicationSecurityCheckCDKSecurityCheckADCE795B' }, }, Name: 'UnattachedStageSecurityCheck', RunOrder: 1, - }, - { + }), + Match.objectLike({ Configuration: { CustomData: '#{UnattachedStageSecurityCheck.MESSAGE}', ExternalEntityLink: '#{UnattachedStageSecurityCheck.LINK}', }, Name: 'UnattachedStageManualApproval', RunOrder: 2, - }, - { Name: 'Stack.Prepare', RunOrder: 3 }, - { Name: 'Stack.Deploy', RunOrder: 4 }, + }), + Match.objectLike({ Name: 'Stack.Prepare', RunOrder: 3 }), + Match.objectLike({ Name: 'Stack.Deploy', RunOrder: 4 }), ], Name: 'UnattachedStage', }, diff --git a/packages/@aws-cdk/pipelines/test/compliance/self-mutation.test.ts b/packages/@aws-cdk/pipelines/test/compliance/self-mutation.test.ts index 8196c84a0920b..f672898107c30 100644 --- a/packages/@aws-cdk/pipelines/test/compliance/self-mutation.test.ts +++ b/packages/@aws-cdk/pipelines/test/compliance/self-mutation.test.ts @@ -1,10 +1,9 @@ /* eslint-disable import/no-extraneous-dependencies */ -import { anything, arrayWith, deepObjectLike, encodedJson, notMatching, objectLike } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import * as cb from '@aws-cdk/aws-codebuild'; import * as cp from '@aws-cdk/aws-codepipeline'; import { Stack, Stage } from '@aws-cdk/core'; -import { behavior, LegacyTestGitHubNpmPipeline, PIPELINE_ENV, stackTemplate, TestApp, ModernTestGitHubNpmPipeline } from '../testhelpers'; +import { behavior, LegacyTestGitHubNpmPipeline, PIPELINE_ENV, TestApp, ModernTestGitHubNpmPipeline } from '../testhelpers'; let app: TestApp; let pipelineStack: Stack; @@ -31,32 +30,32 @@ behavior('CodePipeline has self-mutation stage', (suite) => { function THEN_codePipelineExpectation() { // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'UpdatePipeline', Actions: [ - objectLike({ + Match.objectLike({ Name: 'SelfMutate', - Configuration: objectLike({ - ProjectName: { Ref: anything() }, + Configuration: Match.objectLike({ + ProjectName: { Ref: Match.anyValue() }, }), }), ], - }), + }]), }); - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: 'aws/codebuild/standard:5.0', }, Source: { - BuildSpec: encodedJson(deepObjectLike({ + BuildSpec: Match.serializedJson(Match.objectLike({ phases: { install: { commands: ['npm install -g aws-cdk'], }, build: { - commands: arrayWith('cdk -a . deploy PipelineStack --require-approval=never --verbose'), + commands: Match.arrayWith(['cdk -a . deploy PipelineStack --require-approval=never --verbose']), }, }, })), @@ -84,15 +83,15 @@ behavior('selfmutation stage correctly identifies nested assembly of pipeline st }); function THEN_codePipelineExpectation(nestedPipelineStack: Stack) { - expect(stackTemplate(nestedPipelineStack)).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(nestedPipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: 'aws/codebuild/standard:5.0', }, Source: { - BuildSpec: encodedJson(deepObjectLike({ + BuildSpec: Match.serializedJson(Match.objectLike({ phases: { build: { - commands: arrayWith('cdk -a assembly-PipelineStage deploy PipelineStage/PipelineStack --require-approval=never --verbose'), + commands: Match.arrayWith(['cdk -a assembly-PipelineStage deploy PipelineStage/PipelineStack --require-approval=never --verbose']), }, }, })), @@ -123,11 +122,11 @@ behavior('selfmutation feature can be turned off', (suite) => { }); function THEN_codePipelineExpectation() { - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: notMatching(arrayWith({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.not(Match.arrayWith([{ Name: 'UpdatePipeline', - Actions: anything(), - })), + Actions: Match.anyValue(), + }])), }); } }); @@ -154,10 +153,10 @@ behavior('can control fix/CLI version used in pipeline selfupdate', (suite) => { function THEN_codePipelineExpectation() { // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { Name: 'vpipe-selfupdate', Source: { - BuildSpec: encodedJson(deepObjectLike({ + BuildSpec: Match.serializedJson(Match.objectLike({ phases: { install: { commands: ['npm install -g aws-cdk@1.2.3'], @@ -177,7 +176,7 @@ behavior('Pipeline stack itself can use assets (has implications for selfupdate) }); // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { PrivilegedMode: true, }, @@ -191,7 +190,7 @@ behavior('Pipeline stack itself can use assets (has implications for selfupdate) }); // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { PrivilegedMode: true, }, @@ -212,9 +211,9 @@ behavior('self-update project role uses tagged bootstrap-role permissions', (sui }); function THEN_codePipelineExpectations() { - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith( + Statement: Match.arrayWith([ { Action: 'sts:AssumeRole', Effect: 'Allow', @@ -235,7 +234,7 @@ behavior('self-update project role uses tagged bootstrap-role permissions', (sui Effect: 'Allow', Resource: '*', }, - ), + ]), }, }); } @@ -280,19 +279,19 @@ behavior('self-mutation stage can be customized with BuildSpec', (suite) => { }); function THEN_codePipelineExpectation() { - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: 'aws/codebuild/standard:5.0', PrivilegedMode: false, }, Source: { - BuildSpec: encodedJson(deepObjectLike({ + BuildSpec: Match.serializedJson(Match.objectLike({ phases: { install: { commands: ['npm config set registry example.com', 'npm install -g aws-cdk'], }, build: { - commands: arrayWith('cdk -a . deploy PipelineStack --require-approval=never --verbose'), + commands: Match.arrayWith(['cdk -a . deploy PipelineStack --require-approval=never --verbose']), }, }, cache: { diff --git a/packages/@aws-cdk/pipelines/test/compliance/stack-ordering.test.ts b/packages/@aws-cdk/pipelines/test/compliance/stack-ordering.test.ts index cb21139b16364..9b056b8af2ece 100644 --- a/packages/@aws-cdk/pipelines/test/compliance/stack-ordering.test.ts +++ b/packages/@aws-cdk/pipelines/test/compliance/stack-ordering.test.ts @@ -1,7 +1,6 @@ -import { arrayWith, objectLike } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import { App, Stack } from '@aws-cdk/core'; -import { behavior, LegacyTestGitHubNpmPipeline, ModernTestGitHubNpmPipeline, OneStackApp, PIPELINE_ENV, sortedByRunOrder, TestApp, ThreeStackApp, TwoStackApp } from '../testhelpers'; +import { behavior, LegacyTestGitHubNpmPipeline, ModernTestGitHubNpmPipeline, OneStackApp, PIPELINE_ENV, sortByRunOrder, TestApp, ThreeStackApp, TwoStackApp } from '../testhelpers'; let app: App; let pipelineStack: Stack; @@ -28,16 +27,16 @@ behavior('interdependent stacks are in the right order', (suite) => { function THEN_codePipelineExpectation() { // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'MyApp', - Actions: sortedByRunOrder([ - objectLike({ Name: 'Stack1.Prepare' }), - objectLike({ Name: 'Stack1.Deploy' }), - objectLike({ Name: 'Stack2.Prepare' }), - objectLike({ Name: 'Stack2.Deploy' }), + Actions: sortByRunOrder([ + Match.objectLike({ Name: 'Stack1.Prepare' }), + Match.objectLike({ Name: 'Stack1.Deploy' }), + Match.objectLike({ Name: 'Stack2.Prepare' }), + Match.objectLike({ Name: 'Stack2.Deploy' }), ]), - }), + }]), }); } }); @@ -59,20 +58,20 @@ behavior('multiple independent stacks go in parallel', (suite) => { }); function THEN_codePipelineExpectation() { - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'MyApp', - Actions: sortedByRunOrder([ + Actions: sortByRunOrder([ // 1 and 2 in parallel - objectLike({ Name: 'Stack1.Prepare' }), - objectLike({ Name: 'Stack2.Prepare' }), - objectLike({ Name: 'Stack1.Deploy' }), - objectLike({ Name: 'Stack2.Deploy' }), + Match.objectLike({ Name: 'Stack1.Prepare' }), + Match.objectLike({ Name: 'Stack2.Prepare' }), + Match.objectLike({ Name: 'Stack1.Deploy' }), + Match.objectLike({ Name: 'Stack2.Deploy' }), // Then 3 - objectLike({ Name: 'Stack3.Prepare' }), - objectLike({ Name: 'Stack3.Deploy' }), + Match.objectLike({ Name: 'Stack3.Prepare' }), + Match.objectLike({ Name: 'Stack3.Deploy' }), ]), - }), + }]), }); } }); @@ -86,18 +85,18 @@ behavior('user can request manual change set approvals', (suite) => { }); // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'MyApp', - Actions: sortedByRunOrder([ - objectLike({ Name: 'Stack1.Prepare' }), - objectLike({ Name: 'ManualApproval' }), - objectLike({ Name: 'Stack1.Deploy' }), - objectLike({ Name: 'Stack2.Prepare' }), - objectLike({ Name: 'ManualApproval2' }), - objectLike({ Name: 'Stack2.Deploy' }), + Actions: sortByRunOrder([ + Match.objectLike({ Name: 'Stack1.Prepare' }), + Match.objectLike({ Name: 'ManualApproval' }), + Match.objectLike({ Name: 'Stack1.Deploy' }), + Match.objectLike({ Name: 'Stack2.Prepare' }), + Match.objectLike({ Name: 'ManualApproval2' }), + Match.objectLike({ Name: 'Stack2.Deploy' }), ]), - }), + }]), }); }); @@ -114,28 +113,28 @@ behavior('user can request extra runorder space between prepare and deploy', (su }); // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'MyApp', - Actions: sortedByRunOrder([ - objectLike({ + Actions: sortByRunOrder([ + Match.objectLike({ Name: 'Stack1.Prepare', RunOrder: 1, }), - objectLike({ + Match.objectLike({ Name: 'Stack1.Deploy', RunOrder: 3, }), - objectLike({ + Match.objectLike({ Name: 'Stack2.Prepare', RunOrder: 4, }), - objectLike({ + Match.objectLike({ Name: 'Stack2.Deploy', RunOrder: 6, }), ]), - }), + }]), }); }); @@ -153,24 +152,24 @@ behavior('user can request both manual change set approval and extraRunOrderSpac }); // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'MyApp', - Actions: sortedByRunOrder([ - objectLike({ + Actions: sortByRunOrder([ + Match.objectLike({ Name: 'Stack.Prepare', RunOrder: 1, }), - objectLike({ + Match.objectLike({ Name: 'ManualApproval', RunOrder: 2, }), - objectLike({ + Match.objectLike({ Name: 'Stack.Deploy', RunOrder: 4, }), ]), - }), + }]), }); }); diff --git a/packages/@aws-cdk/pipelines/test/compliance/synths.test.ts b/packages/@aws-cdk/pipelines/test/compliance/synths.test.ts index 58bae441ee156..f8e39a536309f 100644 --- a/packages/@aws-cdk/pipelines/test/compliance/synths.test.ts +++ b/packages/@aws-cdk/pipelines/test/compliance/synths.test.ts @@ -1,5 +1,4 @@ -import { arrayWith, deepObjectLike, encodedJson, objectLike, Capture, anything } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Capture, Match, Template } from '@aws-cdk/assertions'; import * as cbuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as ec2 from '@aws-cdk/aws-ec2'; @@ -64,12 +63,12 @@ behavior('synth takes arrays of commands', (suite) => { function THEN_codePipelineExpectation(installPhase: string) { // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: 'aws/codebuild/standard:5.0', }, Source: { - BuildSpec: encodedJson(deepObjectLike({ + BuildSpec: Match.serializedJson(Match.objectLike({ phases: { [installPhase]: { commands: [ @@ -112,12 +111,12 @@ behavior('synth sets artifact base-directory to cdk.out', (suite) => { function THEN_codePipelineExpectation() { // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: 'aws/codebuild/standard:5.0', }, Source: { - BuildSpec: encodedJson(deepObjectLike({ + BuildSpec: Match.serializedJson(Match.objectLike({ artifacts: { 'base-directory': 'cdk.out', }, @@ -154,15 +153,15 @@ behavior('synth supports setting subdirectory', (suite) => { function THEN_codePipelineExpectation(installPhase: string) { // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: 'aws/codebuild/standard:5.0', }, Source: { - BuildSpec: encodedJson(deepObjectLike({ + BuildSpec: Match.serializedJson(Match.objectLike({ phases: { [installPhase]: { - commands: arrayWith('cd subdir'), + commands: Match.arrayWith(['cd subdir']), }, }, artifacts: { @@ -201,7 +200,7 @@ behavior('npm synth sets, or allows setting, UNSAFE_PERM=true', (suite) => { function THEN_codePipelineExpectation() { // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { EnvironmentVariables: [ { @@ -225,12 +224,12 @@ behavior('synth assumes a JavaScript project by default (no build, yes synth)', }); // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: 'aws/codebuild/standard:5.0', }, Source: { - BuildSpec: encodedJson(deepObjectLike({ + BuildSpec: Match.serializedJson(Match.objectLike({ phases: { pre_build: { commands: ['npm ci'], @@ -278,24 +277,24 @@ behavior('Magic CodePipeline variables passed to synth envvars must be rendered function THEN_codePipelineExpectation() { // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'Build', Actions: [ - objectLike({ + Match.objectLike({ Name: 'Synth', - Configuration: objectLike({ - EnvironmentVariables: encodedJson(arrayWith( + Configuration: Match.objectLike({ + EnvironmentVariables: Match.serializedJson(Match.arrayWith([ { name: 'VERSION', type: 'PLAINTEXT', value: '#{codepipeline.PipelineExecutionId}', }, - )), + ])), }), }), ], - }), + }]), }); } }); @@ -354,24 +353,24 @@ behavior('CodeBuild: environment variables specified in multiple places are corr function THEN_codePipelineExpectation(installPhase: string) { // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: objectLike({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { + Environment: Match.objectLike({ PrivilegedMode: true, - EnvironmentVariables: arrayWith( + EnvironmentVariables: Match.arrayWith([ { - Name: 'SOME_ENV_VAR', + Name: 'INNER_VAR', Type: 'PLAINTEXT', - Value: 'SomeValue', + Value: 'InnerValue', }, { - Name: 'INNER_VAR', + Name: 'SOME_ENV_VAR', Type: 'PLAINTEXT', - Value: 'InnerValue', + Value: 'SomeValue', }, - ), + ]), }), Source: { - BuildSpec: encodedJson(deepObjectLike({ + BuildSpec: Match.serializedJson(Match.objectLike({ phases: { [installPhase]: { commands: ['install1', 'install2'], @@ -413,12 +412,12 @@ behavior('install command can be overridden/specified', (suite) => { function THEN_codePipelineExpectation(installPhase: string) { // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: 'aws/codebuild/standard:5.0', }, Source: { - BuildSpec: encodedJson(deepObjectLike({ + BuildSpec: Match.serializedJson(Match.objectLike({ phases: { [installPhase]: { commands: ['/bin/true'], @@ -445,12 +444,12 @@ behavior('synth can have its test commands set', (suite) => { }); // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: 'aws/codebuild/standard:5.0', }, Source: { - BuildSpec: encodedJson(objectLike({ + BuildSpec: Match.serializedJson(Match.objectLike({ phases: { pre_build: { commands: ['/bin/true'], @@ -506,12 +505,12 @@ behavior('Synth can output additional artifacts', (suite) => { function THEN_codePipelineExpectation(asmArtifact: string, testArtifact: string) { // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: 'aws/codebuild/standard:5.0', }, Source: { - BuildSpec: encodedJson(deepObjectLike({ + BuildSpec: Match.serializedJson(Match.objectLike({ artifacts: { 'secondary-artifacts': { [asmArtifact]: { @@ -585,7 +584,7 @@ behavior('Synth can be made to run in a VPC', (suite) => { function THEN_codePipelineExpectation() { // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { VpcConfig: { SecurityGroupIds: [ { 'Fn::GetAtt': ['CdkPipelineBuildSynthCdkBuildProjectSecurityGroupEA44D7C2', 'GroupId'] }, @@ -599,16 +598,16 @@ behavior('Synth can be made to run in a VPC', (suite) => { }, }); - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', { Roles: [ { Ref: 'CdkPipelineBuildSynthCdkBuildProjectRole5E173C62' }, ], PolicyDocument: { - Statement: arrayWith({ - Action: arrayWith('ec2:DescribeSecurityGroups'), + Statement: Match.arrayWith([{ + Action: Match.arrayWith(['ec2:DescribeSecurityGroups']), Effect: 'Allow', Resource: '*', - }), + }]), }, }); } @@ -721,28 +720,28 @@ behavior('Pipeline action contains a hash that changes as the buildspec changes' } function captureProjectConfigHash(_pipelineStack: Stack) { - const theHash = Capture.aString(); - expect(_pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + const theHash = new Capture(); + Template.fromStack(_pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'Build', Actions: [ - objectLike({ + Match.objectLike({ Name: 'Synth', - Configuration: objectLike({ - EnvironmentVariables: encodedJson([ + Configuration: Match.objectLike({ + EnvironmentVariables: Match.serializedJson([ { name: '_PROJECT_CONFIG_HASH', type: 'PLAINTEXT', - value: theHash.capture(), + value: theHash, }, ]), }), }), ], - }), + }]), }); - return theHash.capturedValue; + return theHash.asString(); } }); @@ -784,12 +783,12 @@ behavior('Synth CodeBuild project role can be granted permissions', (suite) => { function THEN_codePipelineExpectation() { // THEN - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith(deepObjectLike({ + Statement: Match.arrayWith([Match.objectLike({ Action: ['s3:GetObject*', 's3:GetBucket*', 's3:List*'], Resource: ['arn:aws:s3:::ThisParticularBucket', 'arn:aws:s3:::ThisParticularBucket/*'], - })), + })]), }, }); } @@ -878,15 +877,15 @@ behavior('CodeBuild: Can specify additional policy statements', (suite) => { }); function THEN_codePipelineExpectation() { - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith(deepObjectLike({ + Statement: Match.arrayWith([Match.objectLike({ Action: [ 'codeartifact:*', 'sts:GetServiceBearerToken', ], Resource: 'arn:my:arn', - })), + })]), }, }); } @@ -913,38 +912,38 @@ behavior('Multiple input sources in side-by-side directories', (suite) => { }), }); - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith( + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([ { Name: 'Source', Actions: [ - objectLike({ Configuration: objectLike({ Repo: 'bar' }) }), - objectLike({ Configuration: objectLike({ Repo: 'build' }) }), - objectLike({ Configuration: objectLike({ Repo: 'test' }) }), + Match.objectLike({ Configuration: Match.objectLike({ Repo: 'bar' }) }), + Match.objectLike({ Configuration: Match.objectLike({ Repo: 'build' }) }), + Match.objectLike({ Configuration: Match.objectLike({ Repo: 'test' }) }), ], }, { Name: 'Build', Actions: [ - objectLike({ Name: 'Prebuild', RunOrder: 1 }), - objectLike({ + Match.objectLike({ Name: 'Prebuild', RunOrder: 1 }), + Match.objectLike({ Name: 'Synth', RunOrder: 2, InputArtifacts: [ // 3 input artifacts - anything(), - anything(), - anything(), + Match.anyValue(), + Match.anyValue(), + Match.anyValue(), ], }), ], }, - ), + ]), }); - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { Source: { - BuildSpec: encodedJson(deepObjectLike({ + BuildSpec: Match.serializedJson(Match.objectLike({ phases: { install: { commands: [ @@ -975,12 +974,12 @@ behavior('Can easily switch on privileged mode for synth', (suite) => { commands: ['LookAtMe'], }); - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: objectLike({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { + Environment: Match.objectLike({ PrivilegedMode: true, }), Source: { - BuildSpec: encodedJson(deepObjectLike({ + BuildSpec: Match.serializedJson(Match.objectLike({ phases: { build: { commands: [ @@ -1079,19 +1078,19 @@ behavior('can provide custom BuildSpec that is merged with generated one', (suit function THEN_codePipelineExpectation() { // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { - Environment: objectLike({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { + Environment: Match.objectLike({ PrivilegedMode: true, - EnvironmentVariables: arrayWith( + EnvironmentVariables: Match.arrayWith([ { Name: 'INNER_VAR', Type: 'PLAINTEXT', Value: 'InnerValue', }, - ), + ]), }), Source: { - BuildSpec: encodedJson(deepObjectLike({ + BuildSpec: Match.serializedJson(Match.objectLike({ env: { variables: { FOO: 'bar', @@ -1099,7 +1098,7 @@ behavior('can provide custom BuildSpec that is merged with generated one', (suit }, phases: { pre_build: { - commands: arrayWith('installCustom'), + commands: Match.arrayWith(['installCustom']), }, build: { commands: ['synth'], diff --git a/packages/@aws-cdk/pipelines/test/compliance/validations.test.ts b/packages/@aws-cdk/pipelines/test/compliance/validations.test.ts index 447e22da59124..7a6a562a8707a 100644 --- a/packages/@aws-cdk/pipelines/test/compliance/validations.test.ts +++ b/packages/@aws-cdk/pipelines/test/compliance/validations.test.ts @@ -1,6 +1,5 @@ /* eslint-disable import/no-extraneous-dependencies */ -import { anything, arrayWith, Capture, deepObjectLike, encodedJson, objectLike } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Capture, Match, Template } from '@aws-cdk/assertions'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as ec2 from '@aws-cdk/aws-ec2'; @@ -9,7 +8,7 @@ import * as s3 from '@aws-cdk/aws-s3'; import { Stack } from '@aws-cdk/core'; import * as cdkp from '../../lib'; import { CodePipelineSource, ShellStep } from '../../lib'; -import { AppWithOutput, behavior, LegacyTestGitHubNpmPipeline, ModernTestGitHubNpmPipeline, OneStackApp, PIPELINE_ENV, sortedByRunOrder, StageWithStackOutput, stringNoLongerThan, TestApp, TwoStackApp } from '../testhelpers'; +import { AppWithOutput, behavior, LegacyTestGitHubNpmPipeline, ModernTestGitHubNpmPipeline, OneStackApp, PIPELINE_ENV, sortByRunOrder, StageWithStackOutput, stringNoLongerThan, TestApp, TwoStackApp } from '../testhelpers'; let app: TestApp; let pipelineStack: Stack; @@ -37,17 +36,17 @@ behavior('can add manual approval after app', (suite) => { }); // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'MyApp', - Actions: sortedByRunOrder([ - objectLike({ Name: 'Stack1.Prepare' }), - objectLike({ Name: 'Stack1.Deploy' }), - objectLike({ Name: 'Stack2.Prepare' }), - objectLike({ Name: 'Stack2.Deploy' }), - objectLike({ Name: 'Approve' }), + Actions: sortByRunOrder([ + Match.objectLike({ Name: 'Stack1.Prepare' }), + Match.objectLike({ Name: 'Stack1.Deploy' }), + Match.objectLike({ Name: 'Stack2.Prepare' }), + Match.objectLike({ Name: 'Stack2.Deploy' }), + Match.objectLike({ Name: 'Approve' }), ]), - }), + }]), }); }); }); @@ -69,19 +68,19 @@ behavior('can add steps to wave', (suite) => { wave.addStage(new OneStackApp(pipelineStack, 'Stage3')); // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'MyWave', - Actions: sortedByRunOrder([ - objectLike({ Name: 'Stage1.Stack.Prepare' }), - objectLike({ Name: 'Stage2.Stack.Prepare' }), - objectLike({ Name: 'Stage3.Stack.Prepare' }), - objectLike({ Name: 'Stage1.Stack.Deploy' }), - objectLike({ Name: 'Stage2.Stack.Deploy' }), - objectLike({ Name: 'Stage3.Stack.Deploy' }), - objectLike({ Name: 'Approve' }), + Actions: sortByRunOrder([ + Match.objectLike({ Name: 'Stage1.Stack.Prepare' }), + Match.objectLike({ Name: 'Stage2.Stack.Prepare' }), + Match.objectLike({ Name: 'Stage3.Stack.Prepare' }), + Match.objectLike({ Name: 'Stage1.Stack.Deploy' }), + Match.objectLike({ Name: 'Stage2.Stack.Deploy' }), + Match.objectLike({ Name: 'Stage3.Stack.Deploy' }), + Match.objectLike({ Name: 'Approve' }), ]), - }), + }]), }); }); }); @@ -104,37 +103,37 @@ behavior('script validation steps can use stack outputs as environment variables })); // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'MyApp', - Actions: arrayWith( - deepObjectLike({ - Name: 'Stack.Deploy', - OutputArtifacts: [{ Name: anything() }], - Configuration: { - OutputFileName: 'outputs.json', - }, - }), - deepObjectLike({ + Actions: Match.arrayWith([ + Match.objectLike({ ActionTypeId: { Provider: 'CodeBuild', }, Configuration: { - ProjectName: anything(), + ProjectName: Match.anyValue(), }, - InputArtifacts: [{ Name: anything() }], + InputArtifacts: [{ Name: Match.anyValue() }], Name: 'TestOutput', }), - ), - }), + Match.objectLike({ + Name: 'Stack.Deploy', + OutputArtifacts: [{ Name: Match.anyValue() }], + Configuration: { + OutputFileName: 'outputs.json', + }, + }), + ]), + }]), }); - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: 'aws/codebuild/standard:5.0', }, Source: { - BuildSpec: encodedJson(deepObjectLike({ + BuildSpec: Match.serializedJson(Match.objectLike({ phases: { build: { commands: [ @@ -164,24 +163,24 @@ behavior('script validation steps can use stack outputs as environment variables }); // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'Alpha', - Actions: arrayWith( - objectLike({ + Actions: Match.arrayWith([ + Match.objectLike({ Name: 'Stack.Deploy', Namespace: 'AlphaStack6B3389FA', }), - objectLike({ + Match.objectLike({ Name: 'Approve', - Configuration: objectLike({ - EnvironmentVariables: encodedJson([ + Configuration: Match.objectLike({ + EnvironmentVariables: Match.serializedJson([ { name: 'THE_OUTPUT', value: '#{AlphaStack6B3389FA.MyOutput}', type: 'PLAINTEXT' }, ]), }), }), - ), - }), + ]), + }]), }); }); }); @@ -200,29 +199,29 @@ behavior('stackOutput generates names limited to 100 characters', (suite) => { })); // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'APreposterouslyLongAndComplicatedNameMadeUpJustToMakeItExceedTheLimitDefinedByCodeBuild', - Actions: arrayWith( - deepObjectLike({ - Name: 'Stack.Deploy', - OutputArtifacts: [{ Name: stringNoLongerThan(100) }], - Configuration: { - OutputFileName: 'outputs.json', - }, - }), - deepObjectLike({ + Actions: Match.arrayWith([ + Match.objectLike({ ActionTypeId: { Provider: 'CodeBuild', }, Configuration: { - ProjectName: anything(), + ProjectName: Match.anyValue(), }, InputArtifacts: [{ Name: stringNoLongerThan(100) }], Name: 'TestOutput', }), - ), - }), + Match.objectLike({ + Name: 'Stack.Deploy', + OutputArtifacts: [{ Name: stringNoLongerThan(100) }], + Configuration: { + OutputFileName: 'outputs.json', + }, + }), + ]), + }]), }); }); @@ -240,16 +239,16 @@ behavior('stackOutput generates names limited to 100 characters', (suite) => { ], }); - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'APreposterouslyLongAndComplicatedNameMadeUpJustToMakeItExceedTheLimitDefinedByCodeBuild', - Actions: arrayWith( - deepObjectLike({ + Actions: Match.arrayWith([ + Match.objectLike({ Name: 'Stack.Deploy', Namespace: stringNoLongerThan(100), }), - ), - }), + ]), + }]), }); }); }); @@ -283,35 +282,35 @@ behavior('validation step can run from scripts in source', (suite) => { }); function THEN_codePipelineExpectation() { - const sourceArtifact = Capture.aString(); + const sourceArtifact = new Capture(); - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'Source', Actions: [ - deepObjectLike({ - OutputArtifacts: [{ Name: sourceArtifact.capture() }], + Match.objectLike({ + OutputArtifacts: [{ Name: sourceArtifact }], }), ], - }), + }]), }); - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'Test', - Actions: arrayWith( - deepObjectLike({ + Actions: Match.arrayWith([ + Match.objectLike({ Name: 'UseSources', - InputArtifacts: [{ Name: sourceArtifact.capturedValue }], + InputArtifacts: [{ Name: sourceArtifact.asString() }], }), - ), - }), + ]), + }]), }); - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: 'aws/codebuild/standard:5.0', }, Source: { - BuildSpec: encodedJson(deepObjectLike({ + BuildSpec: Match.serializedJson(Match.objectLike({ phases: { build: { commands: [ @@ -361,40 +360,40 @@ behavior('can use additional output artifacts from build', (suite) => { }); function THEN_codePipelineExpectation() { - const integArtifact = Capture.aString(); + const integArtifact = new Capture(); - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'Build', Actions: [ - deepObjectLike({ + Match.objectLike({ Name: 'Synth', OutputArtifacts: [ - { Name: anything() }, // It's not the first output - { Name: integArtifact.capture() }, + { Name: Match.anyValue() }, // It's not the first output + { Name: integArtifact }, ], }), ], - }), + }]), }); - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'Test', - Actions: arrayWith( - deepObjectLike({ + Actions: Match.arrayWith([ + Match.objectLike({ Name: 'UseBuildArtifact', - InputArtifacts: [{ Name: integArtifact.capturedValue }], + InputArtifacts: [{ Name: integArtifact.asString() }], }), - ), - }), + ]), + }]), }); - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: 'aws/codebuild/standard:5.0', }, Source: { - BuildSpec: encodedJson(deepObjectLike({ + BuildSpec: Match.serializedJson(Match.objectLike({ phases: { build: { commands: [ @@ -450,12 +449,12 @@ behavior('can add policy statements to shell script action', (suite) => { function THEN_codePipelineExpectation() { // THEN - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith(deepObjectLike({ + Statement: Match.arrayWith([Match.objectLike({ Action: 's3:Banana', Resource: '*', - })), + })]), }, }); } @@ -502,12 +501,12 @@ behavior('can grant permissions to shell script action', (suite) => { function THEN_codePipelineExpectation() { // THEN - expect(pipelineStack).toHaveResourceLike('AWS::IAM::Policy', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { - Statement: arrayWith(deepObjectLike({ + Statement: Match.arrayWith([Match.objectLike({ Action: ['s3:GetObject*', 's3:GetBucket*', 's3:List*'], Resource: ['arn:aws:s3:::ThisParticularBucket', 'arn:aws:s3:::ThisParticularBucket/*'], - })), + })]), }, }); } @@ -562,7 +561,7 @@ behavior('can run shell script actions in a VPC', (suite) => { }); function THEN_codePipelineExpectation() { - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: 'aws/codebuild/standard:5.0', }, @@ -583,7 +582,7 @@ behavior('can run shell script actions in a VPC', (suite) => { }, }, Source: { - BuildSpec: encodedJson(deepObjectLike({ + BuildSpec: Match.serializedJson(Match.objectLike({ phases: { build: { commands: [ @@ -636,17 +635,17 @@ behavior('can run shell script actions with a specific SecurityGroup', (suite) = }); function THEN_codePipelineExpectation() { - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'Test', - Actions: arrayWith( - deepObjectLike({ + Actions: Match.arrayWith([ + Match.objectLike({ Name: 'sgAction', }), - ), - }), + ]), + }]), }); - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { VpcConfig: { SecurityGroupIds: [ { @@ -714,7 +713,7 @@ behavior('can run scripts with specified BuildEnvironment', (suite) => { }); function THEN_codePipelineExpectation() { - expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodeBuild::Project', { Environment: { Image: 'aws/codebuild/standard:2.0', }, @@ -755,14 +754,14 @@ behavior('can run scripts with magic environment variables', (suite) => { function THEN_codePipelineExpectation() { // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'Test', - Actions: arrayWith( - objectLike({ + Actions: Match.arrayWith([ + Match.objectLike({ Name: 'imageAction', - Configuration: objectLike({ - EnvironmentVariables: encodedJson([ + Configuration: Match.objectLike({ + EnvironmentVariables: Match.serializedJson([ { name: 'VERSION', type: 'PLAINTEXT', @@ -771,8 +770,8 @@ behavior('can run scripts with magic environment variables', (suite) => { ]), }), }), - ), - }), + ]), + }]), }); } }); diff --git a/packages/@aws-cdk/pipelines/test/testhelpers/index.ts b/packages/@aws-cdk/pipelines/test/testhelpers/index.ts index 87a02ce0b6a66..fbc50d3b1a003 100644 --- a/packages/@aws-cdk/pipelines/test/testhelpers/index.ts +++ b/packages/@aws-cdk/pipelines/test/testhelpers/index.ts @@ -2,5 +2,4 @@ export * from './compliance'; export * from './legacy-pipeline'; export * from './modern-pipeline'; export * from './test-app'; -export * from './testmatchers'; export * from './matchers'; \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/testhelpers/matchers.ts b/packages/@aws-cdk/pipelines/test/testhelpers/matchers.ts index 4ace0148c5eaa..97a02fc1dc10d 100644 --- a/packages/@aws-cdk/pipelines/test/testhelpers/matchers.ts +++ b/packages/@aws-cdk/pipelines/test/testhelpers/matchers.ts @@ -1,9 +1,20 @@ -import { Matcher, MatchResult } from '@aws-cdk/assertions'; +import { Match, Matcher, MatchResult } from '@aws-cdk/assertions'; export function stringLike(pattern: string) { return new StringLike(pattern); } +export function sortByRunOrder(pattern: any[]): Matcher { + return new Sorter('SortByRunOrder', pattern, (a: any, b: any) => { + if (a.RunOrder !== b.RunOrder) { return a.RunOrder - b.RunOrder; } + return (a.Name as string).localeCompare(b.Name); + }); +} + +export function stringNoLongerThan(max: number): Matcher { + return new StringLengthMatcher(max); +} + // Reimplementation of // https://github.com/aws/aws-cdk/blob/430f50a546e9c575f8cdbd259367e440d985e68f/packages/%40aws-cdk/assert-internal/lib/assertions/have-resource-matchers.ts#L244 class StringLike extends Matcher { @@ -24,9 +35,55 @@ class StringLike extends Matcher { result.push(this, [], `Looking for string with pattern "${this.pattern}" but found "${actual}"`); } return result; + + function escapeRegex(s: string) { + return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string + } + } +} + +class Sorter extends Matcher { + constructor( + public readonly name: string, + private readonly pattern: any[], + private readonly compareFn: (a: any, b: any) => number, + ) { + super(); + } + + public test(actual: any): MatchResult { + const result = new MatchResult(actual); + if (!Array.isArray(actual)) { + result.push(this, [], `Expected an Array, but got '${typeof actual}'`); + return result; + } + + const copy = actual.slice(); + copy.sort(this.compareFn); + + const matcher = Matcher.isMatcher(this.pattern) ? this.pattern : Match.exact(this.pattern); + return matcher.test(copy); } } -function escapeRegex(s: string) { - return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +class StringLengthMatcher extends Matcher { + public name: string = 'StringLength' + + constructor(private readonly length: number) { + super(); + } + + public test(actual: any): MatchResult { + const result = new MatchResult(actual); + + if (typeof actual !== 'string') { + result.push(this, [], `Expected a string, but got '${typeof actual}'`); + } + + if (actual.length > this.length) { + result.push(this, [], `String is ${actual.length} characters long. Expected at most ${this.length} characters`); + } + + return result; + } } \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/testhelpers/testmatchers.ts b/packages/@aws-cdk/pipelines/test/testhelpers/testmatchers.ts deleted file mode 100644 index 8faa855b71abf..0000000000000 --- a/packages/@aws-cdk/pipelines/test/testhelpers/testmatchers.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* eslint-disable import/no-extraneous-dependencies */ -import { annotateMatcher, InspectionFailure, matcherFrom, PropertyMatcher } from '@aws-cdk/assert-internal'; - -/** - * Sort an array (of Actions) by their RunOrder field before applying a matcher. - * - * Makes the matcher independent of the order in which the Actions get synthed - * to the template. Elements with the same RunOrder will be sorted by name. - */ -export function sortedByRunOrder(matcher: any): PropertyMatcher { - return annotateMatcher({ $sortedByRunOrder: matcher }, (value: any, failure: InspectionFailure) => { - if (!Array.isArray(value)) { - failure.failureReason = `Expected an Array, but got '${typeof value}'`; - return false; - } - - value = value.slice(); - - value.sort((a: any, b: any) => { - if (a.RunOrder !== b.RunOrder) { return a.RunOrder - b.RunOrder; } - return (a.Name as string).localeCompare(b.Name); - }); - - return matcherFrom(matcher)(value, failure); - }); -} - -export function stringNoLongerThan(length: number): PropertyMatcher { - return annotateMatcher({ $stringIsNoLongerThan: length }, (value: any, failure: InspectionFailure) => { - if (typeof value !== 'string') { - failure.failureReason = `Expected a string, but got '${typeof value}'`; - return false; - } - - if (value.length > length) { - failure.failureReason = `String is ${value.length} characters long. Expected at most ${length} characters`; - return false; - } - - return true; - }); -} \ No newline at end of file