diff --git a/packages/@aws-cdk/pipelines/test/codepipeline/codepipeline-sources.test.ts b/packages/@aws-cdk/pipelines/test/codepipeline/codepipeline-sources.test.ts index 84aef10bb171c..46fb468c37623 100644 --- a/packages/@aws-cdk/pipelines/test/codepipeline/codepipeline-sources.test.ts +++ b/packages/@aws-cdk/pipelines/test/codepipeline/codepipeline-sources.test.ts @@ -1,5 +1,4 @@ -import { anything, arrayWith, Capture, objectLike } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Capture, Match, Template } from '@aws-cdk/assertions'; import * as ccommit from '@aws-cdk/aws-codecommit'; import { CodeCommitTrigger, GitHubTrigger } from '@aws-cdk/aws-codepipeline-actions'; import { AnyPrincipal, Role } from '@aws-cdk/aws-iam'; @@ -28,18 +27,18 @@ test('CodeCommit source handles tokenized names correctly', () => { input: cdkp.CodePipelineSource.codeCommit(repo, 'main'), }); - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'Source', Actions: [ - objectLike({ - Configuration: objectLike({ - RepositoryName: { 'Fn::GetAtt': [anything(), 'Name'] }, + Match.objectLike({ + Configuration: Match.objectLike({ + RepositoryName: { 'Fn::GetAtt': [Match.anyValue(), 'Name'] }, }), - Name: { 'Fn::GetAtt': [anything(), 'Name'] }, + Name: { 'Fn::GetAtt': [Match.anyValue(), 'Name'] }, }), ], - }), + }]), }); }); @@ -58,20 +57,20 @@ test('CodeCommit source honors all valid properties', () => { }), }); - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'Source', Actions: [ - objectLike({ - Configuration: objectLike({ + Match.objectLike({ + Configuration: Match.objectLike({ BranchName: 'main', PollForSourceChanges: true, OutputArtifactFormat: 'CODEBUILD_CLONE_REF', }), - RoleArn: { 'Fn::GetAtt': [anything(), 'Arn'] }, + RoleArn: { 'Fn::GetAtt': [Match.anyValue(), 'Arn'] }, }), ], - }), + }]), }); }); @@ -81,19 +80,19 @@ test('S3 source handles tokenized names correctly', () => { input: cdkp.CodePipelineSource.s3(buckit, 'thefile.zip'), }); - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'Source', Actions: [ - objectLike({ - Configuration: objectLike({ - S3Bucket: { Ref: anything() }, + Match.objectLike({ + Configuration: Match.objectLike({ + S3Bucket: { Ref: Match.anyValue() }, S3ObjectKey: 'thefile.zip', }), - Name: { Ref: anything() }, + Name: { Ref: Match.anyValue() }, }), ], - }), + }]), }); }); @@ -105,12 +104,12 @@ test('GitHub source honors all valid properties', () => { }), }); - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'Source', Actions: [ - objectLike({ - Configuration: objectLike({ + Match.objectLike({ + Configuration: Match.objectLike({ Owner: 'owner', Repo: 'repo', Branch: 'main', @@ -120,7 +119,7 @@ test('GitHub source honors all valid properties', () => { Name: 'owner_repo', }), ], - }), + }]), }); }); @@ -145,17 +144,17 @@ test('Dashes in repo names are removed from artifact names', () => { input: cdkp.CodePipelineSource.gitHub('owner/my-repo', 'main'), }); - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'Source', Actions: [ - objectLike({ + Match.objectLike({ OutputArtifacts: [ { Name: 'owner_my_repo_Source' }, ], }), ], - }), + }]), }); }); @@ -164,19 +163,19 @@ test('artifact names are never longer than 128 characters', () => { input: cdkp.CodePipelineSource.gitHub('owner/' + 'my-repo'.repeat(100), 'main'), }); - const artifactId = Capture.aString(); - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + const artifactId = new Capture(); + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'Source', Actions: [ - objectLike({ + Match.objectLike({ OutputArtifacts: [ - { Name: artifactId.capture() }, + { Name: artifactId }, ], }), ], - }), + }]), }); - expect(artifactId.capturedValue.length).toBeLessThanOrEqual(128); + expect(artifactId.asString().length).toBeLessThanOrEqual(128); }); \ No newline at end of file diff --git a/packages/@aws-cdk/pipelines/test/compliance/basic-behavior.test.ts b/packages/@aws-cdk/pipelines/test/compliance/basic-behavior.test.ts index 1248831737bdf..85d7d5911dd0a 100644 --- a/packages/@aws-cdk/pipelines/test/compliance/basic-behavior.test.ts +++ b/packages/@aws-cdk/pipelines/test/compliance/basic-behavior.test.ts @@ -1,11 +1,11 @@ /* eslint-disable import/no-extraneous-dependencies */ import * as fs from 'fs'; import * as path from 'path'; -import { arrayWith, Capture, objectLike, stringLike } from '@aws-cdk/assert-internal'; +import { Capture, Match, Template } from '@aws-cdk/assertions'; import '@aws-cdk/assert-internal/jest'; import { Stack, Stage, StageProps, Tags } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { behavior, LegacyTestGitHubNpmPipeline, OneStackApp, BucketStack, PIPELINE_ENV, TestApp, ModernTestGitHubNpmPipeline } from '../testhelpers'; +import { behavior, LegacyTestGitHubNpmPipeline, OneStackApp, BucketStack, PIPELINE_ENV, TestApp, ModernTestGitHubNpmPipeline, stringLike } from '../testhelpers'; let app: TestApp; let pipelineStack: Stack; @@ -37,20 +37,20 @@ behavior('stack templates in nested assemblies are correctly addressed', (suite) }); 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('*Prepare'), - InputArtifacts: [objectLike({})], - Configuration: objectLike({ + InputArtifacts: [Match.objectLike({})], + Configuration: Match.objectLike({ StackName: 'App-Stack', TemplatePath: stringLike('*::assembly-App/*.template.json'), }), }), - ), - }), + ]), + }]), }); } }); @@ -94,27 +94,27 @@ behavior('overridden stack names are respected', (suite) => { }); function THEN_codePipelineExpectation() { - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith( + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([ { Name: 'App1', - Actions: arrayWith(objectLike({ + Actions: Match.arrayWith([Match.objectLike({ Name: stringLike('*Prepare'), - Configuration: objectLike({ + Configuration: Match.objectLike({ StackName: 'MyFancyStack', }), - })), + })]), }, { Name: 'App2', - Actions: arrayWith(objectLike({ + Actions: Match.arrayWith([Match.objectLike({ Name: stringLike('*Prepare'), - Configuration: objectLike({ + Configuration: Match.objectLike({ StackName: 'MyFancyStack', }), - })), + })]), }, - ), + ]), }); } }); @@ -154,17 +154,17 @@ behavior('changing CLI version leads to a different pipeline structure (restarti function THEN_codePipelineExpectation(stack2: Stack, stack3: Stack) { // THEN - const structure2 = Capture.anyType(); - const structure3 = Capture.anyType(); + const structure2 = new Capture(); + const structure3 = new Capture(); - expect(stack2).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: structure2.capture(), + Template.fromStack(stack2).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: structure2, }); - expect(stack3).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: structure3.capture(), + Template.fromStack(stack3).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: structure3, }); - expect(JSON.stringify(structure2.capturedValue)).not.toEqual(JSON.stringify(structure3.capturedValue)); + expect(JSON.stringify(structure2.asArray())).not.toEqual(JSON.stringify(structure3.asArray())); } }); @@ -190,24 +190,25 @@ behavior('tags get reflected in pipeline', (suite) => { function THEN_codePipelineExpectation() { // THEN - const templateConfig = Capture.aString(); - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + const templateConfig = new Capture(); + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'App', - Actions: arrayWith( - objectLike({ + Actions: Match.arrayWith([ + Match.objectLike({ Name: stringLike('*Prepare'), - InputArtifacts: [objectLike({})], - Configuration: objectLike({ + InputArtifacts: [Match.objectLike({})], + Configuration: Match.objectLike({ StackName: 'App-Stack', - TemplateConfiguration: templateConfig.capture(stringLike('*::assembly-App/*.template.*json')), + TemplateConfiguration: templateConfig, }), }), - ), - }), + ]), + }]), }); - const [, relConfigFile] = templateConfig.capturedValue.split('::'); + expect(templateConfig.asString()).toMatch(/::assembly-App\/.*\.template\..*json/); + const [, relConfigFile] = templateConfig.asString().split('::'); const absConfigFile = path.join(app.outdir, relConfigFile); const configFile = JSON.parse(fs.readFileSync(absConfigFile, { encoding: 'utf-8' })); expect(configFile).toEqual(expect.objectContaining({ diff --git a/packages/@aws-cdk/pipelines/test/compliance/environments.test.ts b/packages/@aws-cdk/pipelines/test/compliance/environments.test.ts index d30e5a423fcb3..6ab303e7df43d 100644 --- a/packages/@aws-cdk/pipelines/test/compliance/environments.test.ts +++ b/packages/@aws-cdk/pipelines/test/compliance/environments.test.ts @@ -1,8 +1,7 @@ /* eslint-disable import/no-extraneous-dependencies */ -import { arrayWith, objectLike, stringLike } from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; +import { Match, Template } from '@aws-cdk/assertions'; import { Stack } from '@aws-cdk/core'; -import { behavior, LegacyTestGitHubNpmPipeline, OneStackApp, PIPELINE_ENV, TestApp, ModernTestGitHubNpmPipeline } from '../testhelpers'; +import { behavior, LegacyTestGitHubNpmPipeline, OneStackApp, PIPELINE_ENV, TestApp, ModernTestGitHubNpmPipeline, stringLike } from '../testhelpers'; let app: TestApp; let pipelineStack: Stack; @@ -51,38 +50,38 @@ behavior('action has right settings for same-env deployment', (suite) => { function THEN_codePipelineExpection(roleArn: (x: string) => any) { // THEN: pipeline structure is correct - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'Same', Actions: [ - objectLike({ + Match.objectLike({ Name: stringLike('*Prepare'), RoleArn: roleArn('deploy-role'), - Configuration: objectLike({ + Configuration: Match.objectLike({ StackName: 'Same-Stack', RoleArn: roleArn('cfn-exec-role'), }), }), - objectLike({ + Match.objectLike({ Name: stringLike('*Deploy'), RoleArn: roleArn('deploy-role'), - Configuration: objectLike({ + Configuration: Match.objectLike({ StackName: 'Same-Stack', }), }), ], - }), + }]), }); // THEN: artifact bucket can be read by deploy role - expect(pipelineStack).toHaveResourceLike('AWS::S3::BucketPolicy', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::S3::BucketPolicy', { PolicyDocument: { - Statement: arrayWith(objectLike({ + Statement: Match.arrayWith([Match.objectLike({ Action: ['s3:GetObject*', 's3:GetBucket*', 's3:List*'], Principal: { AWS: roleArn('deploy-role'), }, - })), + })]), }, }); } @@ -109,11 +108,11 @@ behavior('action has right settings for cross-account deployment', (suite) => { function THEN_codePipelineExpectation() { // THEN: Pipelien structure is correct - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'CrossAccount', Actions: [ - objectLike({ + Match.objectLike({ Name: stringLike('*Prepare'), RoleArn: { 'Fn::Join': ['', [ @@ -123,7 +122,7 @@ behavior('action has right settings for cross-account deployment', (suite) => { { Ref: 'AWS::Region' }, ]], }, - Configuration: objectLike({ + Configuration: Match.objectLike({ StackName: 'CrossAccount-Stack', RoleArn: { 'Fn::Join': ['', [ @@ -135,7 +134,7 @@ behavior('action has right settings for cross-account deployment', (suite) => { }, }), }), - objectLike({ + Match.objectLike({ Name: stringLike('*Deploy'), RoleArn: { 'Fn::Join': ['', [ @@ -145,18 +144,18 @@ behavior('action has right settings for cross-account deployment', (suite) => { { Ref: 'AWS::Region' }, ]], }, - Configuration: objectLike({ + Configuration: Match.objectLike({ StackName: 'CrossAccount-Stack', }), }), ], - }), + }]), }); // THEN: Artifact bucket can be read by deploy role - expect(pipelineStack).toHaveResourceLike('AWS::S3::BucketPolicy', { + Template.fromStack(pipelineStack).hasResourceProperties('AWS::S3::BucketPolicy', { PolicyDocument: { - Statement: arrayWith(objectLike({ + Statement: Match.arrayWith([Match.objectLike({ Action: ['s3:GetObject*', 's3:GetBucket*', 's3:List*'], Principal: { AWS: { @@ -168,7 +167,7 @@ behavior('action has right settings for cross-account deployment', (suite) => { ]], }, }, - })), + })]), }, }); } @@ -194,11 +193,11 @@ behavior('action has right settings for cross-region deployment', (suite) => { function THEN_codePipelineExpectation() { // THEN - expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + Template.fromStack(pipelineStack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'CrossRegion', Actions: [ - objectLike({ + Match.objectLike({ Name: stringLike('*Prepare'), RoleArn: { 'Fn::Join': ['', [ @@ -212,7 +211,7 @@ behavior('action has right settings for cross-region deployment', (suite) => { ]], }, Region: 'elsewhere', - Configuration: objectLike({ + Configuration: Match.objectLike({ StackName: 'CrossRegion-Stack', RoleArn: { 'Fn::Join': ['', [ @@ -227,7 +226,7 @@ behavior('action has right settings for cross-region deployment', (suite) => { }, }), }), - objectLike({ + Match.objectLike({ Name: stringLike('*Deploy'), RoleArn: { 'Fn::Join': ['', [ @@ -241,12 +240,12 @@ behavior('action has right settings for cross-region deployment', (suite) => { ]], }, Region: 'elsewhere', - Configuration: objectLike({ + Configuration: Match.objectLike({ StackName: 'CrossRegion-Stack', }), }), ], - }), + }]), }); } }); @@ -282,11 +281,13 @@ behavior('action has right settings for cross-account/cross-region deployment', function THEN_codePipelineExpectations() { // THEN: pipeline structure must be correct - expect(app.stackArtifact(pipelineStack)).toHaveResourceLike('AWS::CodePipeline::Pipeline', { - Stages: arrayWith({ + const stack = app.stackArtifact(pipelineStack); + expect(stack).toBeDefined(); + Template.fromStack(stack!).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: Match.arrayWith([{ Name: 'CrossBoth', Actions: [ - objectLike({ + Match.objectLike({ Name: stringLike('*Prepare'), RoleArn: { 'Fn::Join': ['', [ @@ -296,7 +297,7 @@ behavior('action has right settings for cross-account/cross-region deployment', ]], }, Region: 'elsewhere', - Configuration: objectLike({ + Configuration: Match.objectLike({ StackName: 'CrossBoth-Stack', RoleArn: { 'Fn::Join': ['', [ @@ -307,7 +308,7 @@ behavior('action has right settings for cross-account/cross-region deployment', }, }), }), - objectLike({ + Match.objectLike({ Name: stringLike('*Deploy'), RoleArn: { 'Fn::Join': ['', [ @@ -317,20 +318,21 @@ behavior('action has right settings for cross-account/cross-region deployment', ]], }, Region: 'elsewhere', - Configuration: objectLike({ + Configuration: Match.objectLike({ StackName: 'CrossBoth-Stack', }), }), ], - }), + }]), }); // THEN: artifact bucket can be read by deploy role - const supportStack = 'PipelineStack-support-elsewhere'; - expect(app.stackArtifact(supportStack)).toHaveResourceLike('AWS::S3::BucketPolicy', { + const supportStack = app.stackArtifact('PipelineStack-support-elsewhere'); + expect(supportStack).toBeDefined(); + Template.fromStack(supportStack!).hasResourceProperties('AWS::S3::BucketPolicy', { PolicyDocument: { - Statement: arrayWith(objectLike({ - Action: arrayWith('s3:GetObject*', 's3:GetBucket*', 's3:List*'), + Statement: Match.arrayWith([Match.objectLike({ + Action: Match.arrayWith(['s3:GetObject*', 's3:GetBucket*', 's3:List*']), Principal: { AWS: { 'Fn::Join': ['', [ @@ -340,15 +342,15 @@ behavior('action has right settings for cross-account/cross-region deployment', ]], }, }, - })), + })]), }, }); // And the key to go along with it - expect(app.stackArtifact(supportStack)).toHaveResourceLike('AWS::KMS::Key', { + Template.fromStack(supportStack!).hasResourceProperties('AWS::KMS::Key', { KeyPolicy: { - Statement: arrayWith(objectLike({ - Action: arrayWith('kms:Decrypt', 'kms:DescribeKey'), + Statement: Match.arrayWith([Match.objectLike({ + Action: Match.arrayWith(['kms:Decrypt', 'kms:DescribeKey']), Principal: { AWS: { 'Fn::Join': ['', [ @@ -358,7 +360,7 @@ behavior('action has right settings for cross-account/cross-region deployment', ]], }, }, - })), + })]), }, }); } diff --git a/packages/@aws-cdk/pipelines/test/testhelpers/index.ts b/packages/@aws-cdk/pipelines/test/testhelpers/index.ts index 21ca108240f27..87a02ce0b6a66 100644 --- a/packages/@aws-cdk/pipelines/test/testhelpers/index.ts +++ b/packages/@aws-cdk/pipelines/test/testhelpers/index.ts @@ -2,4 +2,5 @@ export * from './compliance'; export * from './legacy-pipeline'; export * from './modern-pipeline'; export * from './test-app'; -export * from './testmatchers'; \ No newline at end of file +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 new file mode 100644 index 0000000000000..4ace0148c5eaa --- /dev/null +++ b/packages/@aws-cdk/pipelines/test/testhelpers/matchers.ts @@ -0,0 +1,32 @@ +import { Matcher, MatchResult } from '@aws-cdk/assertions'; + +export function stringLike(pattern: string) { + return new StringLike(pattern); +} + +// 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 { + public name = 'StringLike'; + + constructor(private readonly pattern: string) { + super(); + } + + public test(actual: any): MatchResult { + if (typeof(actual) !== 'string') { + throw new Error(`Expected string but found ${typeof(actual)}`); + } + const re = new RegExp(`^${this.pattern.split('*').map(escapeRegex).join('.*')}$`); + + const result = new MatchResult(actual); + if (!re.test(actual)) { + 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 +} \ No newline at end of file