From a843e83c52cc4188c6344abac41e0c5e37959885 Mon Sep 17 00:00:00 2001 From: kaizen3031593 Date: Tue, 18 Jan 2022 19:04:28 -0500 Subject: [PATCH 01/13] prelim implementation for Messages api --- packages/@aws-cdk/assertions/lib/index.ts | 3 +- packages/@aws-cdk/assertions/lib/messages.ts | 67 ++++++++++++++++++ .../assertions/lib/private/message.ts | 19 +++++ .../assertions/lib/private/messages.ts | 28 ++++++++ .../@aws-cdk/assertions/lib/private/util.ts | 16 +++++ packages/@aws-cdk/assertions/lib/template.ts | 16 +---- .../@aws-cdk/assertions/test/messages.test.ts | 70 +++++++++++++++++++ 7 files changed, 205 insertions(+), 14 deletions(-) create mode 100644 packages/@aws-cdk/assertions/lib/messages.ts create mode 100644 packages/@aws-cdk/assertions/lib/private/message.ts create mode 100644 packages/@aws-cdk/assertions/lib/private/messages.ts create mode 100644 packages/@aws-cdk/assertions/lib/private/util.ts create mode 100644 packages/@aws-cdk/assertions/test/messages.test.ts diff --git a/packages/@aws-cdk/assertions/lib/index.ts b/packages/@aws-cdk/assertions/lib/index.ts index 492fad1227af3..2cf1c6c4c2356 100644 --- a/packages/@aws-cdk/assertions/lib/index.ts +++ b/packages/@aws-cdk/assertions/lib/index.ts @@ -1,4 +1,5 @@ export * from './capture'; export * from './template'; export * from './match'; -export * from './matcher'; \ No newline at end of file +export * from './matcher'; +export * from './messages'; \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/lib/messages.ts b/packages/@aws-cdk/assertions/lib/messages.ts new file mode 100644 index 0000000000000..a1a59024a705c --- /dev/null +++ b/packages/@aws-cdk/assertions/lib/messages.ts @@ -0,0 +1,67 @@ +import { Stack } from '@aws-cdk/core'; +import { Message, Messages as MessagesType } from './private/message'; +import { findMessage, hasMessage } from './private/messages'; +import { toStackArtifact } from './private/util'; + +/** + * Messages + */ +export class Messages { + /** + * Base your assertions on the messages returned by a synthesized CDK `Stack`. + * @param stack the CDK Stack to run assertions on + */ + public static fromStack(stack: Stack): Messages { + return new Messages(toMessages(stack)); + } + + private readonly _messages: MessagesType; + + private constructor(messages: Message[]) { + this._messages = convertArrayToMessagesType(messages); + } + + /** + * MessageList + */ + public get messageList(): Message[] { + return convertMessagesTypeToArray(this._messages); + } + + /** + * hasMessage + * @param props + */ + public hasMessage(props: any): void { + const matchError = hasMessage(this._messages, props); + if (matchError) { + throw new Error(matchError); + } + } + + /** + * findMessages + * @param props + * @returns + */ + public findMessage(props: any): Message[] { + return convertMessagesTypeToArray(findMessage(this._messages, props) as MessagesType); + } +} + +function convertArrayToMessagesType(messages: Message[]): MessagesType { + return messages.reduce((obj, item) => { + return { + ...obj, + [item.id]: item, + }; + }, {}) as MessagesType; +} + +function convertMessagesTypeToArray(messages: MessagesType): Message[] { + return Object.values(messages); +} + +function toMessages(stack: Stack): any { + return toStackArtifact(stack).messages; +} diff --git a/packages/@aws-cdk/assertions/lib/private/message.ts b/packages/@aws-cdk/assertions/lib/private/message.ts new file mode 100644 index 0000000000000..1bfa93d86121d --- /dev/null +++ b/packages/@aws-cdk/assertions/lib/private/message.ts @@ -0,0 +1,19 @@ +export type Messages = { + Messages: { [id: string]: Message } +} + +export type Message = { + [key: string]: any; +} + +// export type Message = { +// level: 'info' | 'warning' | 'error'; +// id: string; +// entry: MetadataEntry; +// }; + +// type MetadataEntry = { +// type: string, +// data?: any, +// trace?: string[], +// } \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/lib/private/messages.ts b/packages/@aws-cdk/assertions/lib/private/messages.ts new file mode 100644 index 0000000000000..1e9e4c825c17b --- /dev/null +++ b/packages/@aws-cdk/assertions/lib/private/messages.ts @@ -0,0 +1,28 @@ +import { Messages } from './message'; +import { formatFailure, matchSection } from './section'; + +export function findMessage(messages: Messages, props: any = {}): { [key: string]: { [key: string]: any } } { + const result = matchSection(messages, props); + + if (!result.match) { + return {}; + } + + return result.matches as Messages; +} + +export function hasMessage(messages: Messages, props: any): string | void { + const result = matchSection(messages, props); + if (result.match) { + return; + } + + if (result.closestResult === undefined) { + return 'No parameters found in the template'; + } + + return [ + `Template has ${result.analyzedCount} parameters, but none match as expected.`, + formatFailure(result.closestResult), + ].join('\n'); +} diff --git a/packages/@aws-cdk/assertions/lib/private/util.ts b/packages/@aws-cdk/assertions/lib/private/util.ts new file mode 100644 index 0000000000000..de833467c8d41 --- /dev/null +++ b/packages/@aws-cdk/assertions/lib/private/util.ts @@ -0,0 +1,16 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { Stack, Stage } from '@aws-cdk/core'; + +export function toStackArtifact(stack: Stack): any { + const root = stack.node.root; + if (!Stage.isStage(root)) { + throw new Error('unexpected: all stacks must be part of a Stage or an App'); + } + const assembly = root.synth(); + if (stack.nestedStackParent) { + // if this is a nested stack (it has a parent), then just read the template as a string + return JSON.parse(fs.readFileSync(path.join(assembly.directory, stack.templateFile)).toString('utf-8')); + } + return assembly.getStackArtifact(stack.artifactId); +} \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/lib/template.ts b/packages/@aws-cdk/assertions/lib/template.ts index 631c9f7137dc4..49f35a9896db7 100644 --- a/packages/@aws-cdk/assertions/lib/template.ts +++ b/packages/@aws-cdk/assertions/lib/template.ts @@ -1,6 +1,4 @@ -import * as path from 'path'; -import { Stack, Stage } from '@aws-cdk/core'; -import * as fs from 'fs-extra'; +import { Stack } from '@aws-cdk/core'; import { Match } from './match'; import { Matcher } from './matcher'; import { findMappings, hasMapping } from './private/mappings'; @@ -8,6 +6,7 @@ import { findOutputs, hasOutput } from './private/outputs'; import { findParameters, hasParameter } from './private/parameters'; import { countResources, findResources, hasResource, hasResourceProperties } from './private/resources'; import { Template as TemplateType } from './private/template'; +import { toStackArtifact } from './private/util'; /** * Suite of assertions that can be run on a CDK stack. @@ -201,14 +200,5 @@ export class Template { } function toTemplate(stack: Stack): any { - const root = stack.node.root; - if (!Stage.isStage(root)) { - throw new Error('unexpected: all stacks must be part of a Stage or an App'); - } - const assembly = root.synth(); - if (stack.nestedStackParent) { - // if this is a nested stack (it has a parent), then just read the template as a string - return JSON.parse(fs.readFileSync(path.join(assembly.directory, stack.templateFile)).toString('utf-8')); - } - return assembly.getStackArtifact(stack.artifactId).template; + return toStackArtifact(stack).template; } \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/test/messages.test.ts b/packages/@aws-cdk/assertions/test/messages.test.ts new file mode 100644 index 0000000000000..3d9171a39ad39 --- /dev/null +++ b/packages/@aws-cdk/assertions/test/messages.test.ts @@ -0,0 +1,70 @@ +import { Annotations, App, Aspects, CfnResource, IAspect, Stack } from '@aws-cdk/core'; +import { IConstruct } from 'constructs'; +import { Messages } from '../lib'; + +class MyAspect implements IAspect { + public visit(node: IConstruct): void { + if (node instanceof CfnResource) { + this.warn(node, 'my message'); + } + }; + + protected warn(node: IConstruct, message: string): void { + Annotations.of(node).addWarning(message); + } +} + +describe('Messages', () => { + describe('fromStack', () => { + test('default', () => { + const app = new App({ + context: { + '@aws-cdk/core:newStyleStackSynthesis': false, + }, + }); + const stack = new Stack(app); + new CfnResource(stack, 'Foo', { + type: 'Foo::Bar', + properties: { + Baz: 'Qux', + }, + }); + + Aspects.of(stack).add(new MyAspect()); + + const messages = Messages.fromStack(stack); + expect(messages.messageList).toHaveLength(1); + + const message = messages.messageList[0]; + expect(message.level).toEqual('warning'); + expect(message.entry.data).toEqual('my message'); + }); + }); + + describe('hasMessage', () => { + test('exact match', () => { + const stack = new Stack(); + new CfnResource(stack, 'Foo', { + type: 'Foo::Bar', + properties: { baz: 'qux' }, + }); + + Aspects.of(stack).add(new MyAspect()); + + const messages = Messages.fromStack(stack); + messages.hasMessage({ + level: 'warning', + entry: { + data: 'my message', + }, + }); + + expect(() => messages.hasMessage({ + level: 'error', + entry: { + data: 'my message', + }, + })).toThrowError(); + }); + }); +}); \ No newline at end of file From 1ae84798f3a6bdf336af1e41aa528ee11bab5a40 Mon Sep 17 00:00:00 2001 From: kaizen3031593 Date: Tue, 18 Jan 2022 19:44:54 -0500 Subject: [PATCH 02/13] fix test --- packages/@aws-cdk/assertions/lib/messages.ts | 17 +++++++++++++---- .../@aws-cdk/assertions/lib/private/util.ts | 16 ---------------- packages/@aws-cdk/assertions/lib/template.ts | 18 ++++++++++++++---- 3 files changed, 27 insertions(+), 24 deletions(-) delete mode 100644 packages/@aws-cdk/assertions/lib/private/util.ts diff --git a/packages/@aws-cdk/assertions/lib/messages.ts b/packages/@aws-cdk/assertions/lib/messages.ts index a1a59024a705c..8a14a07b55080 100644 --- a/packages/@aws-cdk/assertions/lib/messages.ts +++ b/packages/@aws-cdk/assertions/lib/messages.ts @@ -1,7 +1,6 @@ -import { Stack } from '@aws-cdk/core'; +import { Stack, Stage } from '@aws-cdk/core'; import { Message, Messages as MessagesType } from './private/message'; import { findMessage, hasMessage } from './private/messages'; -import { toStackArtifact } from './private/util'; /** * Messages @@ -62,6 +61,16 @@ function convertMessagesTypeToArray(messages: MessagesType): Message[] { return Object.values(messages); } -function toMessages(stack: Stack): any { - return toStackArtifact(stack).messages; +export function toMessages(stack: Stack): any { + const root = stack.node.root; + if (!Stage.isStage(root)) { + throw new Error('unexpected: all stacks must be part of a Stage or an App'); + } + + // to support incremental assertions (i.e. "expect(stack).toNotContainSomething(); doSomething(); expect(stack).toContainSomthing()") + const force = true; + + const assembly = root.synth({ force }); + + return assembly.getStackArtifact(stack.artifactId).messages; } diff --git a/packages/@aws-cdk/assertions/lib/private/util.ts b/packages/@aws-cdk/assertions/lib/private/util.ts deleted file mode 100644 index de833467c8d41..0000000000000 --- a/packages/@aws-cdk/assertions/lib/private/util.ts +++ /dev/null @@ -1,16 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import { Stack, Stage } from '@aws-cdk/core'; - -export function toStackArtifact(stack: Stack): any { - const root = stack.node.root; - if (!Stage.isStage(root)) { - throw new Error('unexpected: all stacks must be part of a Stage or an App'); - } - const assembly = root.synth(); - if (stack.nestedStackParent) { - // if this is a nested stack (it has a parent), then just read the template as a string - return JSON.parse(fs.readFileSync(path.join(assembly.directory, stack.templateFile)).toString('utf-8')); - } - return assembly.getStackArtifact(stack.artifactId); -} \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/lib/template.ts b/packages/@aws-cdk/assertions/lib/template.ts index 49f35a9896db7..672d10386bb57 100644 --- a/packages/@aws-cdk/assertions/lib/template.ts +++ b/packages/@aws-cdk/assertions/lib/template.ts @@ -1,4 +1,6 @@ -import { Stack } from '@aws-cdk/core'; +import * as fs from 'fs'; +import * as path from 'path'; +import { Stack, Stage } from '@aws-cdk/core'; import { Match } from './match'; import { Matcher } from './matcher'; import { findMappings, hasMapping } from './private/mappings'; @@ -6,7 +8,6 @@ import { findOutputs, hasOutput } from './private/outputs'; import { findParameters, hasParameter } from './private/parameters'; import { countResources, findResources, hasResource, hasResourceProperties } from './private/resources'; import { Template as TemplateType } from './private/template'; -import { toStackArtifact } from './private/util'; /** * Suite of assertions that can be run on a CDK stack. @@ -199,6 +200,15 @@ export class Template { } } -function toTemplate(stack: Stack): any { - return toStackArtifact(stack).template; +export function toTemplate(stack: Stack): any { + const root = stack.node.root; + if (!Stage.isStage(root)) { + throw new Error('unexpected: all stacks must be part of a Stage or an App'); + } + const assembly = root.synth(); + if (stack.nestedStackParent) { + // if this is a nested stack (it has a parent), then just read the template as a string + return JSON.parse(fs.readFileSync(path.join(assembly.directory, stack.templateFile)).toString('utf-8')); + } + return assembly.getStackArtifact(stack.artifactId).template; } \ No newline at end of file From 6c0786a4891f554b0ab5fe2c9edb511d017f0659 Mon Sep 17 00:00:00 2001 From: kaizen3031593 Date: Wed, 19 Jan 2022 14:45:28 -0500 Subject: [PATCH 03/13] tests for findMessage and hasMessage API --- .../assertions/lib/assert-annotations.ts | 82 +++++++++++++ packages/@aws-cdk/assertions/lib/index.ts | 2 +- packages/@aws-cdk/assertions/lib/messages.ts | 76 ------------ .../assertions/lib/private/message.ts | 24 ++-- .../assertions/lib/private/messages.ts | 27 +++-- packages/@aws-cdk/assertions/lib/template.ts | 3 +- .../test/assert-annotations.test.ts | 109 ++++++++++++++++++ .../@aws-cdk/assertions/test/messages.test.ts | 70 ----------- 8 files changed, 224 insertions(+), 169 deletions(-) create mode 100644 packages/@aws-cdk/assertions/lib/assert-annotations.ts delete mode 100644 packages/@aws-cdk/assertions/lib/messages.ts create mode 100644 packages/@aws-cdk/assertions/test/assert-annotations.test.ts delete mode 100644 packages/@aws-cdk/assertions/test/messages.test.ts diff --git a/packages/@aws-cdk/assertions/lib/assert-annotations.ts b/packages/@aws-cdk/assertions/lib/assert-annotations.ts new file mode 100644 index 0000000000000..90b582560ef15 --- /dev/null +++ b/packages/@aws-cdk/assertions/lib/assert-annotations.ts @@ -0,0 +1,82 @@ +import { Stack, Stage } from '@aws-cdk/core'; +import { Message, Messages } from './private/message'; +import { findMessage, hasMessage } from './private/messages'; + +/** + * Suite of assertions that can be run on a CDK Stack. + * Focused on asserting annotations. + */ +export class AssertAnnotations { + /** + * Base your assertions on the messages returned by a synthesized CDK `Stack`. + * @param stack the CDK Stack to run assertions on + */ + public static fromStack(stack: Stack): AssertAnnotations { + return new AssertAnnotations(toMessages(stack)); + } + + private readonly _messages: Messages; + + private constructor(messages: Message[]) { + this._messages = convertArrayToMessagesType(messages); + } + + /** + * Returns raw data of all messages on the stack. + */ + public get messageList(): Message[] { + return convertMessagesTypeToArray(this._messages); + } + + /** + * Assert that a Message with the given properties exists in the CloudFormation template. + * By default, performs partial matching on the parameter, via the `Match.objectLike()`. + * To configure different behavior, use other matchers in the `Match` class. + * @param logicalId the name of the parameter. Provide `'*'` to match all parameters in the template. + * @param props the message as should be expected. + */ + public hasMessage(logicalId: string, props: any): void { + const matchError = hasMessage(this._messages, logicalId, props); + if (matchError) { + throw new Error(matchError); + } + } + + /** + * Get the set of matching messages of a given id and properties. + * @param logicalId the name of the parameter. Provide `'*'` to match all parameters in the template. + * @param props by default, matches all resources with the given logicalId. + * When a literal is provided, performs a partial match via `Match.objectLike()`. + * Use the `Match` APIs to configure a different behaviour. + */ + public findMessage(logicalId: string, props: any): Message[] { + return convertMessagesTypeToArray(findMessage(this._messages, logicalId, props) as Messages); + } +} + +function convertArrayToMessagesType(messages: Message[]): Messages { + return messages.reduce((obj, item) => { + return { + ...obj, + [item.id]: item, + }; + }, {}) as Messages; +} + +function convertMessagesTypeToArray(messages: Messages): Message[] { + return Object.values(messages) as Message[]; +} + +export function toMessages(stack: Stack): any { + const root = stack.node.root; + if (!Stage.isStage(root)) { + throw new Error('unexpected: all stacks must be part of a Stage or an App'); + } + + // to support incremental assertions (i.e. "expect(stack).toNotContainSomething(); doSomething(); expect(stack).toContainSomthing()") + const force = true; + + const assembly = root.synth({ force }); + + return assembly.getStackArtifact(stack.artifactId).messages; +} diff --git a/packages/@aws-cdk/assertions/lib/index.ts b/packages/@aws-cdk/assertions/lib/index.ts index 2cf1c6c4c2356..221f501b738bb 100644 --- a/packages/@aws-cdk/assertions/lib/index.ts +++ b/packages/@aws-cdk/assertions/lib/index.ts @@ -2,4 +2,4 @@ export * from './capture'; export * from './template'; export * from './match'; export * from './matcher'; -export * from './messages'; \ No newline at end of file +export * from './assert-annotations'; \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/lib/messages.ts b/packages/@aws-cdk/assertions/lib/messages.ts deleted file mode 100644 index 8a14a07b55080..0000000000000 --- a/packages/@aws-cdk/assertions/lib/messages.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Stack, Stage } from '@aws-cdk/core'; -import { Message, Messages as MessagesType } from './private/message'; -import { findMessage, hasMessage } from './private/messages'; - -/** - * Messages - */ -export class Messages { - /** - * Base your assertions on the messages returned by a synthesized CDK `Stack`. - * @param stack the CDK Stack to run assertions on - */ - public static fromStack(stack: Stack): Messages { - return new Messages(toMessages(stack)); - } - - private readonly _messages: MessagesType; - - private constructor(messages: Message[]) { - this._messages = convertArrayToMessagesType(messages); - } - - /** - * MessageList - */ - public get messageList(): Message[] { - return convertMessagesTypeToArray(this._messages); - } - - /** - * hasMessage - * @param props - */ - public hasMessage(props: any): void { - const matchError = hasMessage(this._messages, props); - if (matchError) { - throw new Error(matchError); - } - } - - /** - * findMessages - * @param props - * @returns - */ - public findMessage(props: any): Message[] { - return convertMessagesTypeToArray(findMessage(this._messages, props) as MessagesType); - } -} - -function convertArrayToMessagesType(messages: Message[]): MessagesType { - return messages.reduce((obj, item) => { - return { - ...obj, - [item.id]: item, - }; - }, {}) as MessagesType; -} - -function convertMessagesTypeToArray(messages: MessagesType): Message[] { - return Object.values(messages); -} - -export function toMessages(stack: Stack): any { - const root = stack.node.root; - if (!Stage.isStage(root)) { - throw new Error('unexpected: all stacks must be part of a Stage or an App'); - } - - // to support incremental assertions (i.e. "expect(stack).toNotContainSomething(); doSomething(); expect(stack).toContainSomthing()") - const force = true; - - const assembly = root.synth({ force }); - - return assembly.getStackArtifact(stack.artifactId).messages; -} diff --git a/packages/@aws-cdk/assertions/lib/private/message.ts b/packages/@aws-cdk/assertions/lib/private/message.ts index 1bfa93d86121d..20b6bdbfbb4a0 100644 --- a/packages/@aws-cdk/assertions/lib/private/message.ts +++ b/packages/@aws-cdk/assertions/lib/private/message.ts @@ -1,19 +1,17 @@ export type Messages = { - Messages: { [id: string]: Message } + Messages: { [logicalId: string]: Message } } export type Message = { + level: string; + id: string; + entry: MetadataEntry; [key: string]: any; -} - -// export type Message = { -// level: 'info' | 'warning' | 'error'; -// id: string; -// entry: MetadataEntry; -// }; +}; -// type MetadataEntry = { -// type: string, -// data?: any, -// trace?: string[], -// } \ No newline at end of file +type MetadataEntry = { + type: string, + data?: any, + trace?: string[], + [key: string]: any; +} \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/lib/private/messages.ts b/packages/@aws-cdk/assertions/lib/private/messages.ts index 1e9e4c825c17b..a9356567af311 100644 --- a/packages/@aws-cdk/assertions/lib/private/messages.ts +++ b/packages/@aws-cdk/assertions/lib/private/messages.ts @@ -1,8 +1,10 @@ +import { MatchResult } from '../matcher'; import { Messages } from './message'; -import { formatFailure, matchSection } from './section'; +import { filterLogicalId, formatFailure, matchSection } from './section'; -export function findMessage(messages: Messages, props: any = {}): { [key: string]: { [key: string]: any } } { - const result = matchSection(messages, props); +export function findMessage(messages: Messages, logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } { + const section: { [key: string]: {} } = messages; + const result = matchSection(filterLogicalId(section, logicalId), props); if (!result.match) { return {}; @@ -11,18 +13,27 @@ export function findMessage(messages: Messages, props: any = {}): { [key: string return result.matches as Messages; } -export function hasMessage(messages: Messages, props: any): string | void { - const result = matchSection(messages, props); +export function hasMessage(messages: Messages, logicalId: string, props: any): string | void { + const section: { [key: string]: {} } = messages; + const result = matchSection(filterLogicalId(section, logicalId), props); + if (result.match) { return; } if (result.closestResult === undefined) { - return 'No parameters found in the template'; + return 'No messages found in the stack'; } return [ - `Template has ${result.analyzedCount} parameters, but none match as expected.`, - formatFailure(result.closestResult), + `Stack has ${result.analyzedCount} messages, but none match as expected.`, + formatFailure(formatMessage(result.closestResult)), ].join('\n'); } + +function formatMessage(match: MatchResult, renderTrace: boolean = false): MatchResult { + if (!renderTrace) { + match.target.entry.trace = ['redacted']; + } + return match; +} diff --git a/packages/@aws-cdk/assertions/lib/template.ts b/packages/@aws-cdk/assertions/lib/template.ts index 672d10386bb57..49bbfa7f916a7 100644 --- a/packages/@aws-cdk/assertions/lib/template.ts +++ b/packages/@aws-cdk/assertions/lib/template.ts @@ -128,7 +128,8 @@ export class Template { * @param logicalId the name of the parameter. Provide `'*'` to match all parameters in the template. * @param props by default, matches all Parameters in the template. * When a literal object is provided, performs a partial match via `Match.objectLike()`. - * Use the `Match` APIs to configure a different behaviour. */ + * Use the `Match` APIs to configure a different behaviour. + */ public findParameters(logicalId: string, props: any = {}): { [key: string]: { [key: string]: any } } { return findParameters(this.template, logicalId, props); } diff --git a/packages/@aws-cdk/assertions/test/assert-annotations.test.ts b/packages/@aws-cdk/assertions/test/assert-annotations.test.ts new file mode 100644 index 0000000000000..07bb76dad3ac5 --- /dev/null +++ b/packages/@aws-cdk/assertions/test/assert-annotations.test.ts @@ -0,0 +1,109 @@ +import { Annotations, Aspects, CfnResource, IAspect, Stack } from '@aws-cdk/core'; +import { IConstruct } from 'constructs'; +import { AssertAnnotations } from '../lib'; + +describe('Messages', () => { + let stack: Stack; + beforeAll(() => { + stack = new Stack(); + new CfnResource(stack, 'Foo', { + type: 'Foo::Bar', + properties: { + Fred: 'Thud', + }, + }); + + new CfnResource(stack, 'Bar', { + type: 'Foo::Bar', + properties: { + Baz: 'Qux', + }, + }); + + new CfnResource(stack, 'Buzz', { + type: 'Baz::Qux', + properties: { + Foo: 'Bar', + }, + }); + + Aspects.of(stack).add(new MyAspect()); + }); + + describe('fromStack', () => { + test('default', () => { + const annotations = AssertAnnotations.fromStack(stack); + expect(annotations.messageList).toHaveLength(3); + }); + }); + + describe('hasMessage', () => { + test('exact match', () => { + const annotations = AssertAnnotations.fromStack(stack); + annotations.hasMessage('/Default/Foo', { + level: 'error', + entry: { + data: 'this is an error', + }, + }); + + expect(() => annotations.hasMessage('/Default/Foo', { + level: 'warning', + entry: { + data: 'this is a warning', + }, + })).toThrowError(/Expected warning but received error at \/level/); + + expect(() => annotations.hasMessage('/Default/Foo', { + level: 'error', + entry: { + data: 'this is wrong', + }, + })).toThrowError(/Expected this is wrong but received this is an error at \/entry\/data/); + }); + }); + + describe('findMessage', () => { + test('matching', () => { + const annotations = AssertAnnotations.fromStack(stack); + const result = annotations.findMessage('*', { level: 'error' }); + expect(Object.keys(result).length).toEqual(2); + expect(result[0].id).toEqual('/Default/Foo'); + expect(result[1].id).toEqual('/Default/Bar'); + }); + + test('not matching', () => { + const annotations = AssertAnnotations.fromStack(stack); + const resultA = annotations.findMessage('*', { level: 'info' }); + expect(Object.keys(resultA).length).toEqual(0); + + const resultB = annotations.findMessage('*', { + level: 'warning', + entry: { + data: 'this is not a warning', + }, + }); + expect(Object.keys(resultB).length).toEqual(0); + }); + }); +}); + +class MyAspect implements IAspect { + public visit(node: IConstruct): void { + if (node instanceof CfnResource) { + if (node.cfnResourceType === 'Foo::Bar') { + this.error(node, 'this is an error'); + } else { + this.warn(node, 'this is a warning'); + } + } + }; + + protected warn(node: IConstruct, message: string): void { + Annotations.of(node).addWarning(message); + } + + protected error(node: IConstruct, message: string): void { + Annotations.of(node).addError(message); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/test/messages.test.ts b/packages/@aws-cdk/assertions/test/messages.test.ts deleted file mode 100644 index 3d9171a39ad39..0000000000000 --- a/packages/@aws-cdk/assertions/test/messages.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Annotations, App, Aspects, CfnResource, IAspect, Stack } from '@aws-cdk/core'; -import { IConstruct } from 'constructs'; -import { Messages } from '../lib'; - -class MyAspect implements IAspect { - public visit(node: IConstruct): void { - if (node instanceof CfnResource) { - this.warn(node, 'my message'); - } - }; - - protected warn(node: IConstruct, message: string): void { - Annotations.of(node).addWarning(message); - } -} - -describe('Messages', () => { - describe('fromStack', () => { - test('default', () => { - const app = new App({ - context: { - '@aws-cdk/core:newStyleStackSynthesis': false, - }, - }); - const stack = new Stack(app); - new CfnResource(stack, 'Foo', { - type: 'Foo::Bar', - properties: { - Baz: 'Qux', - }, - }); - - Aspects.of(stack).add(new MyAspect()); - - const messages = Messages.fromStack(stack); - expect(messages.messageList).toHaveLength(1); - - const message = messages.messageList[0]; - expect(message.level).toEqual('warning'); - expect(message.entry.data).toEqual('my message'); - }); - }); - - describe('hasMessage', () => { - test('exact match', () => { - const stack = new Stack(); - new CfnResource(stack, 'Foo', { - type: 'Foo::Bar', - properties: { baz: 'qux' }, - }); - - Aspects.of(stack).add(new MyAspect()); - - const messages = Messages.fromStack(stack); - messages.hasMessage({ - level: 'warning', - entry: { - data: 'my message', - }, - }); - - expect(() => messages.hasMessage({ - level: 'error', - entry: { - data: 'my message', - }, - })).toThrowError(); - }); - }); -}); \ No newline at end of file From 8e2a8f9316a9652cc187ae83ec9c3af8fe6788ad Mon Sep 17 00:00:00 2001 From: kaizen3031593 Date: Wed, 19 Jan 2022 16:42:07 -0500 Subject: [PATCH 04/13] assert-annotation API done --- .../assertions/lib/assert-annotations.ts | 128 +++++++++++++++++- .../assertions/lib/private/message.ts | 7 +- .../assertions/lib/private/messages.ts | 5 +- .../test/assert-annotations.test.ts | 119 +++++++++++++++- 4 files changed, 249 insertions(+), 10 deletions(-) diff --git a/packages/@aws-cdk/assertions/lib/assert-annotations.ts b/packages/@aws-cdk/assertions/lib/assert-annotations.ts index 90b582560ef15..690b37c739c7b 100644 --- a/packages/@aws-cdk/assertions/lib/assert-annotations.ts +++ b/packages/@aws-cdk/assertions/lib/assert-annotations.ts @@ -29,13 +29,13 @@ export class AssertAnnotations { } /** - * Assert that a Message with the given properties exists in the CloudFormation template. + * Assert that a Message with the given properties exists in the synthesized CDK `Stack`. * By default, performs partial matching on the parameter, via the `Match.objectLike()`. * To configure different behavior, use other matchers in the `Match` class. * @param logicalId the name of the parameter. Provide `'*'` to match all parameters in the template. * @param props the message as should be expected. */ - public hasMessage(logicalId: string, props: any): void { + public hasMessage(logicalId: string, props: PartialMessage): void { const matchError = hasMessage(this._messages, logicalId, props); if (matchError) { throw new Error(matchError); @@ -49,9 +49,87 @@ export class AssertAnnotations { * When a literal is provided, performs a partial match via `Match.objectLike()`. * Use the `Match` APIs to configure a different behaviour. */ - public findMessage(logicalId: string, props: any): Message[] { + public findMessage(logicalId: string, props: PartialMessage): Message[] { return convertMessagesTypeToArray(findMessage(this._messages, logicalId, props) as Messages); } + + /** + * Assert that an Error with the given properties exists in the synthesized CDK `Stack`. + * By default, performs partial matching on the parameter, via the `Match.objectLike()`. + * To configure different behavior, use other matchers in the `Match` class. + * @param logicalId the name of the parameter. Provide `'*'` to match all parameters in the template. + * @param props the error as should be expected. + */ + public hasError(logicalId: string, props: PartialMessage): void { + if (props.level && props.level !== 'error') { + throw new Error(`Message level mismatch: expected no level or 'error' but got ${props.level}`); + } + + const matchError = hasMessage(this._messages, logicalId, { + ...props, + level: 'error', + }); + if (matchError) { + throw new Error(matchError); + } + } + + /** + * Get the set of matching Errors of a given id and properties. + * @param logicalId the name of the parameter. Provide `'*'` to match all parameters in the template. + * @param props by default, matches all resources with the given logicalId. + * When a literal is provided, performs a partial match via `Match.objectLike()`. + * Use the `Match` APIs to configure a different behaviour. + */ + public findError(logicalId: string, props: PartialMessage): Message[] { + if (props.level && props.level !== 'error') { + throw new Error(`Message level mismatch: expected no level or 'error' but got ${props.level}`); + } + + return convertMessagesTypeToArray(findMessage(this._messages, logicalId, { + ...props, + level: 'error', + }) as Messages); + } + + /** + * Assert that a Warning with the given properties exists in the synthesized CDK `Stack`. + * By default, performs partial matching on the parameter, via the `Match.objectLike()`. + * To configure different behavior, use other matchers in the `Match` class. + * @param logicalId the name of the parameter. Provide `'*'` to match all parameters in the template. + * @param props the warning as should be expected. + */ + public hasWarning(logicalId: string, props: PartialMessage): void { + if (props.level && props.level !== 'warning') { + throw new Error(`Message level mismatch: expected no level or 'warning' but got ${props.level}`); + } + + const matchError = hasMessage(this._messages, logicalId, { + ...props, + level: 'warning', + }); + if (matchError) { + throw new Error(matchError); + } + } + + /** + * Get the set of matching Warnings of a given id and properties. + * @param logicalId the name of the parameter. Provide `'*'` to match all parameters in the template. + * @param props by default, matches all resources with the given logicalId. + * When a literal is provided, performs a partial match via `Match.objectLike()`. + * Use the `Match` APIs to configure a different behaviour. + */ + public findWarning(logicalId: string, props: PartialMessage): Message[] { + if (props.level && props.level !== 'warning') { + throw new Error(`Message level mismatch: expected no level or 'warning' but got ${props.level}`); + } + + return convertMessagesTypeToArray(findMessage(this._messages, logicalId, { + ...props, + level: 'warning', + }) as Messages); + } } function convertArrayToMessagesType(messages: Message[]): Messages { @@ -67,7 +145,7 @@ function convertMessagesTypeToArray(messages: Messages): Message[] { return Object.values(messages) as Message[]; } -export function toMessages(stack: Stack): any { +function toMessages(stack: Stack): any { const root = stack.node.root; if (!Stage.isStage(root)) { throw new Error('unexpected: all stacks must be part of a Stage or an App'); @@ -80,3 +158,45 @@ export function toMessages(stack: Stack): any { return assembly.getStackArtifact(stack.artifactId).messages; } + +/** + * Message interface where all properties are optional. + * Used to help filter for specific messages. + */ +export interface PartialMessage { + /** + * The type of message. Can be 'info', 'warning', or 'error'. + */ + readonly level?: 'info' | 'warning' | 'error'; + + /** + * The logical id of the message. For example, '/Default/Foo'. + */ + readonly id?: string; + + /** + * The data of the message. + */ + readonly entry?: PartialMetadataEntry; +} + +/** + * MetadataEntry interface where all properties are optional. + * Used to help filter for specific messages. + */ +export interface PartialMetadataEntry { + /** + * The type of message. + */ + readonly type?: string, + + /** + * The data of the message. + */ + readonly data?: any, + + /** + * The stack trace. + */ + readonly trace?: string[], +} diff --git a/packages/@aws-cdk/assertions/lib/private/message.ts b/packages/@aws-cdk/assertions/lib/private/message.ts index 20b6bdbfbb4a0..5fe870d384805 100644 --- a/packages/@aws-cdk/assertions/lib/private/message.ts +++ b/packages/@aws-cdk/assertions/lib/private/message.ts @@ -3,15 +3,14 @@ export type Messages = { } export type Message = { - level: string; + level: 'info' | 'warning' | 'error'; id: string; entry: MetadataEntry; [key: string]: any; }; -type MetadataEntry = { +export type MetadataEntry = { type: string, data?: any, trace?: string[], - [key: string]: any; -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/assertions/lib/private/messages.ts b/packages/@aws-cdk/assertions/lib/private/messages.ts index a9356567af311..ac7847ccabc89 100644 --- a/packages/@aws-cdk/assertions/lib/private/messages.ts +++ b/packages/@aws-cdk/assertions/lib/private/messages.ts @@ -31,9 +31,12 @@ export function hasMessage(messages: Messages, logicalId: string, props: any): s ].join('\n'); } +/** + * We redact the stack trace by default because it is unnecessarily long and unintelligible. + */ function formatMessage(match: MatchResult, renderTrace: boolean = false): MatchResult { if (!renderTrace) { - match.target.entry.trace = ['redacted']; + match.target.entry.trace = 'redacted'; } return match; } diff --git a/packages/@aws-cdk/assertions/test/assert-annotations.test.ts b/packages/@aws-cdk/assertions/test/assert-annotations.test.ts index 07bb76dad3ac5..6f842836b571a 100644 --- a/packages/@aws-cdk/assertions/test/assert-annotations.test.ts +++ b/packages/@aws-cdk/assertions/test/assert-annotations.test.ts @@ -20,7 +20,7 @@ describe('Messages', () => { }, }); - new CfnResource(stack, 'Buzz', { + new CfnResource(stack, 'Fred', { type: 'Baz::Qux', properties: { Foo: 'Bar', @@ -61,6 +61,16 @@ describe('Messages', () => { }, })).toThrowError(/Expected this is wrong but received this is an error at \/entry\/data/); }); + + test('stack trace is redacted', () => { + const annotations = AssertAnnotations.fromStack(stack); + expect(() => annotations.hasMessage('/Default/Foo', { + level: 'error', + entry: { + data: 'this is wrong', + }, + })).toThrowError(/"trace": "redacted"/); + }); }); describe('findMessage', () => { @@ -85,6 +95,113 @@ describe('Messages', () => { }); expect(Object.keys(resultB).length).toEqual(0); }); + + test('matching specific output', () => { + const annotations = AssertAnnotations.fromStack(stack); + const result = annotations.findMessage('/Default/Bar', { + level: 'error', + }); + + expect(Object.keys(result).length).toEqual(1); + expect(result[0].id).toEqual('/Default/Bar'); + }); + + test('not matching specific output', () => { + const annotations = AssertAnnotations.fromStack(stack); + const result = annotations.findMessage('/Default/Bear', { + level: 'error', + }); + + expect(Object.keys(result).length).toEqual(0); + }); + }); + + describe('hasError', () => { + test('match', () => { + AssertAnnotations.fromStack(stack).hasError('/Default/Foo', { + entry: { + data: 'this is an error', + }, + }); + }); + + test('no match', () => { + expect(() => AssertAnnotations.fromStack(stack).hasError('/Default/Fred', { + })).toThrowError(/Stack has 1 messages, but none match as expected./); + }); + + test('incorrect specification', () => { + expect(() => AssertAnnotations.fromStack(stack).hasWarning('*', { + level: 'info', + })).toThrowError(/Message level mismatch:/); + }); + }); + + describe('findError', () => { + test('match', () => { + const result = AssertAnnotations.fromStack(stack).findError('*', { + }); + expect(Object.keys(result).length).toEqual(2); + }); + + test('no match', () => { + const result = AssertAnnotations.fromStack(stack).findError('*', { + entry: { + data: 'no message looks like this', + }, + }); + expect(Object.keys(result).length).toEqual(0); + }); + + test('incorrect specification', () => { + expect(() => AssertAnnotations.fromStack(stack).findError('*', { + level: 'info', + })).toThrowError(/Message level mismatch:/); + }); + }); + + describe('hasWarning', () => { + test('match', () => { + AssertAnnotations.fromStack(stack).hasWarning('/Default/Fred', { + entry: { + data: 'this is a warning', + }, + }); + }); + + test('no match', () => { + expect(() => AssertAnnotations.fromStack(stack).hasWarning('/Default/Foo', { + })).toThrowError(/Stack has 1 messages, but none match as expected./); + }); + + test('incorrect specification', () => { + expect(() => AssertAnnotations.fromStack(stack).hasWarning('*', { + level: 'info', + })).toThrowError(/Message level mismatch:/); + }); + }); + + describe('findWarning', () => { + test('match', () => { + const result = AssertAnnotations.fromStack(stack).findWarning('*', { + }); + expect(Object.keys(result).length).toEqual(1); + }); + + test('no match', () => { + const result = AssertAnnotations.fromStack(stack).findWarning('*', { + entry: { + data: 'no message looks like this', + }, + }); + expect(Object.keys(result).length).toEqual(0); + }); + + test('incorrect specification', () => { + expect(() => AssertAnnotations.fromStack(stack).findWarning('*', { + level: 'info', + })).toThrowError(/Message level mismatch:/); + }); }); }); From bd3eb994bd28b910f49c2d619d88b09441e0892c Mon Sep 17 00:00:00 2001 From: kaizen3031593 Date: Wed, 19 Jan 2022 17:09:53 -0500 Subject: [PATCH 05/13] matcher tests --- .../assertions/lib/assert-annotations.ts | 54 +++---------------- .../assertions/lib/private/messages.ts | 6 ++- .../test/assert-annotations.test.ts | 35 +++++++++++- 3 files changed, 44 insertions(+), 51 deletions(-) diff --git a/packages/@aws-cdk/assertions/lib/assert-annotations.ts b/packages/@aws-cdk/assertions/lib/assert-annotations.ts index 690b37c739c7b..076717868c957 100644 --- a/packages/@aws-cdk/assertions/lib/assert-annotations.ts +++ b/packages/@aws-cdk/assertions/lib/assert-annotations.ts @@ -35,7 +35,7 @@ export class AssertAnnotations { * @param logicalId the name of the parameter. Provide `'*'` to match all parameters in the template. * @param props the message as should be expected. */ - public hasMessage(logicalId: string, props: PartialMessage): void { + public hasMessage(logicalId: string, props: any): void { const matchError = hasMessage(this._messages, logicalId, props); if (matchError) { throw new Error(matchError); @@ -49,7 +49,7 @@ export class AssertAnnotations { * When a literal is provided, performs a partial match via `Match.objectLike()`. * Use the `Match` APIs to configure a different behaviour. */ - public findMessage(logicalId: string, props: PartialMessage): Message[] { + public findMessage(logicalId: string, props: any): Message[] { return convertMessagesTypeToArray(findMessage(this._messages, logicalId, props) as Messages); } @@ -60,7 +60,7 @@ export class AssertAnnotations { * @param logicalId the name of the parameter. Provide `'*'` to match all parameters in the template. * @param props the error as should be expected. */ - public hasError(logicalId: string, props: PartialMessage): void { + public hasError(logicalId: string, props: any): void { if (props.level && props.level !== 'error') { throw new Error(`Message level mismatch: expected no level or 'error' but got ${props.level}`); } @@ -81,7 +81,7 @@ export class AssertAnnotations { * When a literal is provided, performs a partial match via `Match.objectLike()`. * Use the `Match` APIs to configure a different behaviour. */ - public findError(logicalId: string, props: PartialMessage): Message[] { + public findError(logicalId: string, props: any): Message[] { if (props.level && props.level !== 'error') { throw new Error(`Message level mismatch: expected no level or 'error' but got ${props.level}`); } @@ -99,7 +99,7 @@ export class AssertAnnotations { * @param logicalId the name of the parameter. Provide `'*'` to match all parameters in the template. * @param props the warning as should be expected. */ - public hasWarning(logicalId: string, props: PartialMessage): void { + public hasWarning(logicalId: string, props: any): void { if (props.level && props.level !== 'warning') { throw new Error(`Message level mismatch: expected no level or 'warning' but got ${props.level}`); } @@ -120,7 +120,7 @@ export class AssertAnnotations { * When a literal is provided, performs a partial match via `Match.objectLike()`. * Use the `Match` APIs to configure a different behaviour. */ - public findWarning(logicalId: string, props: PartialMessage): Message[] { + public findWarning(logicalId: string, props: any): Message[] { if (props.level && props.level !== 'warning') { throw new Error(`Message level mismatch: expected no level or 'warning' but got ${props.level}`); } @@ -158,45 +158,3 @@ function toMessages(stack: Stack): any { return assembly.getStackArtifact(stack.artifactId).messages; } - -/** - * Message interface where all properties are optional. - * Used to help filter for specific messages. - */ -export interface PartialMessage { - /** - * The type of message. Can be 'info', 'warning', or 'error'. - */ - readonly level?: 'info' | 'warning' | 'error'; - - /** - * The logical id of the message. For example, '/Default/Foo'. - */ - readonly id?: string; - - /** - * The data of the message. - */ - readonly entry?: PartialMetadataEntry; -} - -/** - * MetadataEntry interface where all properties are optional. - * Used to help filter for specific messages. - */ -export interface PartialMetadataEntry { - /** - * The type of message. - */ - readonly type?: string, - - /** - * The data of the message. - */ - readonly data?: any, - - /** - * The stack trace. - */ - readonly trace?: string[], -} diff --git a/packages/@aws-cdk/assertions/lib/private/messages.ts b/packages/@aws-cdk/assertions/lib/private/messages.ts index ac7847ccabc89..a5fbafbc1f944 100644 --- a/packages/@aws-cdk/assertions/lib/private/messages.ts +++ b/packages/@aws-cdk/assertions/lib/private/messages.ts @@ -10,7 +10,7 @@ export function findMessage(messages: Messages, logicalId: string, props: any = return {}; } - return result.matches as Messages; + return result.matches; } export function hasMessage(messages: Messages, logicalId: string, props: any): string | void { @@ -25,14 +25,16 @@ export function hasMessage(messages: Messages, logicalId: string, props: any): s return 'No messages found in the stack'; } + const renderTrace = props.entry?.trace ? true : false; return [ `Stack has ${result.analyzedCount} messages, but none match as expected.`, - formatFailure(formatMessage(result.closestResult)), + formatFailure(formatMessage(result.closestResult, renderTrace)), ].join('\n'); } /** * We redact the stack trace by default because it is unnecessarily long and unintelligible. + * The only time where we include the stack trace is when we are asserting against it. */ function formatMessage(match: MatchResult, renderTrace: boolean = false): MatchResult { if (!renderTrace) { diff --git a/packages/@aws-cdk/assertions/test/assert-annotations.test.ts b/packages/@aws-cdk/assertions/test/assert-annotations.test.ts index 6f842836b571a..e23b9819bec10 100644 --- a/packages/@aws-cdk/assertions/test/assert-annotations.test.ts +++ b/packages/@aws-cdk/assertions/test/assert-annotations.test.ts @@ -1,6 +1,6 @@ import { Annotations, Aspects, CfnResource, IAspect, Stack } from '@aws-cdk/core'; import { IConstruct } from 'constructs'; -import { AssertAnnotations } from '../lib'; +import { AssertAnnotations, Match } from '../lib'; describe('Messages', () => { let stack: Stack; @@ -71,6 +71,21 @@ describe('Messages', () => { }, })).toThrowError(/"trace": "redacted"/); }); + + test('with matchers', () => { + const annotations = AssertAnnotations.fromStack(stack); + annotations.hasMessage('/Default/Foo', Match.not({ + level: 'warning', + })); + annotations.hasMessage('/Default/Fred', { + level: Match.anyValue(), + entry: Match.objectEquals({ + type: 'aws:cdk:warning', + data: 'this is a warning', + trace: Match.anyValue(), + }), + }); + }); }); describe('findMessage', () => { @@ -114,6 +129,24 @@ describe('Messages', () => { expect(Object.keys(result).length).toEqual(0); }); + + test('with matchers', () => { + const annotations = AssertAnnotations.fromStack(stack); + const resultA = annotations.findMessage('/Default/Foo', Match.not({ + level: 'warning', + })); + expect(Object.keys(resultA).length).toEqual(1); + + const resultB = annotations.findMessage('/Default/Fred', { + level: Match.anyValue(), + entry: Match.objectEquals({ + type: 'aws:cdk:warning', + data: 'this is a warning', + trace: Match.anyValue(), + }), + }); + expect(Object.keys(resultB).length).toEqual(1); + }); }); describe('hasError', () => { From 7612a2a42c17f01262f92bd815fda7073d8856d5 Mon Sep 17 00:00:00 2001 From: kaizen3031593 Date: Wed, 19 Jan 2022 17:11:37 -0500 Subject: [PATCH 06/13] minor stuff --- packages/@aws-cdk/assertions/lib/template.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/assertions/lib/template.ts b/packages/@aws-cdk/assertions/lib/template.ts index 49bbfa7f916a7..243383e3db8b3 100644 --- a/packages/@aws-cdk/assertions/lib/template.ts +++ b/packages/@aws-cdk/assertions/lib/template.ts @@ -1,6 +1,6 @@ -import * as fs from 'fs'; import * as path from 'path'; import { Stack, Stage } from '@aws-cdk/core'; +import * as fs from 'fs-extra'; import { Match } from './match'; import { Matcher } from './matcher'; import { findMappings, hasMapping } from './private/mappings'; @@ -201,7 +201,7 @@ export class Template { } } -export function toTemplate(stack: Stack): any { +function toTemplate(stack: Stack): any { const root = stack.node.root; if (!Stage.isStage(root)) { throw new Error('unexpected: all stacks must be part of a Stage or an App'); From c94f1f08d6d71cee922ba80df484be59c17c03c0 Mon Sep 17 00:00:00 2001 From: kaizen3031593 Date: Wed, 19 Jan 2022 17:41:21 -0500 Subject: [PATCH 07/13] rosetta extract compile --- packages/@aws-cdk/assertions/README.md | 73 ++++++++++++++++++- .../assertions/rosetta/default.ts-fixture | 4 +- 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/assertions/README.md b/packages/@aws-cdk/assertions/README.md index 55b8d3520d550..2eaf6cbccc97b 100644 --- a/packages/@aws-cdk/assertions/README.md +++ b/packages/@aws-cdk/assertions/README.md @@ -251,7 +251,7 @@ This matcher can be combined with any of the other matchers. // The following will NOT throw an assertion error template.hasResourceProperties('Foo::Bar', { Fred: { - Wobble: [ Match.anyValue(), "Flip" ], + Wobble: [ Match.anyValue(), Match.anyValue() ], }, }); @@ -371,7 +371,7 @@ template.hasResourceProperties('Foo::Bar', { ## Capturing Values -This matcher APIs documented above allow capturing values in the matching entry +The matcher APIs documented above allow capturing values in the matching entry (Resource, Output, Mapping, etc.). The following code captures a string from a matching resource. @@ -463,3 +463,72 @@ fredCapture.asString(); // returns "Flob" fredCapture.next(); // returns true fredCapture.asString(); // returns "Quib" ``` + +## Asserting Annotations + +In addition to template matching, we provide an API for annotation matching. +[Annotations](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.Annotations.html) +can be added via the [Aspects](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.Aspects.html) +API. which you can learn more about +[here](https://docs.aws.amazon.com/cdk/v2/guide/aspects.html). + +Say you have a `MyAspect` and a `MyStack` that uses `MyAspect`: + +```ts nofixture +import * as cdk from '@aws-cdk/core'; +import { Construct, IConstruct } from 'constructs'; + +class MyAspect implements cdk.IAspect { + public visit(node: IConstruct): void { + if (node instanceof cdk.CfnResource && node.cfnResourceType === 'Foo::Bar') { + this.error(node, 'we do not want a Foo::Bar resource'); + } + } + + protected error(node: IConstruct, message: string): void { + cdk.Annotations.of(node).addError(message); + } +} + +class MyStack extends cdk.Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + const stack = new cdk.Stack(); + new cdk.CfnResource(stack, 'Foo', { + type: 'Foo::Bar', + properties: { + Fred: 'Thud', + }, + }); + cdk.Aspects.of(stack).add(new MyAspect()); + } +} +``` + +We can then assert that the stack contains the expected Error: + +```ts +AssertAnnotations.fromStack(stack).hasMessage('/Default/Foo', { + level: 'error', + entry: { + data: 'we do not want a Foo::Bar resource', + }, +}); +``` + +In addition to `hasMessage()`, we can also use `hasError()` or `hasWarning()` to similar +effect: + +```ts +AssertAnnotations.fromStack(stack).hasError('/Default/Foo', { + entry: { + data: 'we do not want a Foo::Bar resource', + } +}); +``` + +Similarly to `Template`, `AssertAnnotations` also provides APIs to retrieve matching +messages. The `findMessages()` API is complementary to the `hasMessages()` API, except +instead of asserting its presence, it returns the set of matching messages. Each +`hasXxx()` API has a complementary `findXxx()` API. \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/rosetta/default.ts-fixture b/packages/@aws-cdk/assertions/rosetta/default.ts-fixture index fa862da5c609a..47c4c8dbbcf06 100644 --- a/packages/@aws-cdk/assertions/rosetta/default.ts-fixture +++ b/packages/@aws-cdk/assertions/rosetta/default.ts-fixture @@ -1,7 +1,7 @@ // Fixture with packages imported, but nothing else import { Construct } from 'constructs'; -import { Stack } from '@aws-cdk/core'; -import { Capture, Match, Template } from '@aws-cdk/assertions'; +import { Aspects, CfnResource, Stack } from '@aws-cdk/core'; +import { AssertAnnotations, Capture, Match, Template } from '@aws-cdk/assertions'; class Fixture extends Stack { constructor(scope: Construct, id: string) { From 79aac7acc7593a757c1f7f2779ad553f2d9ac1af Mon Sep 17 00:00:00 2001 From: kaizen3031593 Date: Thu, 20 Jan 2022 12:37:04 -0500 Subject: [PATCH 08/13] finalize public API --- .../assertions/lib/assert-annotations.ts | 122 +++++++----------- 1 file changed, 45 insertions(+), 77 deletions(-) diff --git a/packages/@aws-cdk/assertions/lib/assert-annotations.ts b/packages/@aws-cdk/assertions/lib/assert-annotations.ts index 076717868c957..dd29d2b38d85a 100644 --- a/packages/@aws-cdk/assertions/lib/assert-annotations.ts +++ b/packages/@aws-cdk/assertions/lib/assert-annotations.ts @@ -22,116 +22,84 @@ export class AssertAnnotations { } /** - * Returns raw data of all messages on the stack. + * Assert that an error with the given message exists in the synthesized CDK `Stack`. + * + * @param constructPath the construct path to the error. Provide `'*'` to match all errors in the template. + * @param message the error message as should be expected. This should be a string or Matcher object. */ - public get messageList(): Message[] { - return convertMessagesTypeToArray(this._messages); - } - - /** - * Assert that a Message with the given properties exists in the synthesized CDK `Stack`. - * By default, performs partial matching on the parameter, via the `Match.objectLike()`. - * To configure different behavior, use other matchers in the `Match` class. - * @param logicalId the name of the parameter. Provide `'*'` to match all parameters in the template. - * @param props the message as should be expected. - */ - public hasMessage(logicalId: string, props: any): void { - const matchError = hasMessage(this._messages, logicalId, props); + public hasError(constructPath: string, message: any): void { + const matchError = hasMessage(this._messages, constructPath, constructMessage('error', message)); if (matchError) { throw new Error(matchError); } } /** - * Get the set of matching messages of a given id and properties. - * @param logicalId the name of the parameter. Provide `'*'` to match all parameters in the template. - * @param props by default, matches all resources with the given logicalId. - * When a literal is provided, performs a partial match via `Match.objectLike()`. - * Use the `Match` APIs to configure a different behaviour. + * Get the set of matching errors of a given construct path and message. + * + * @param constructPath the construct path to the error. Provide `'*'` to match all errors in the template. + * @param message the error message as should be expected. This should be a string or Matcher object. */ - public findMessage(logicalId: string, props: any): Message[] { - return convertMessagesTypeToArray(findMessage(this._messages, logicalId, props) as Messages); + public findError(constructPath: string, message: any): Message[] { + return convertMessagesTypeToArray(findMessage(this._messages, constructPath, constructMessage('error', message)) as Messages); } /** - * Assert that an Error with the given properties exists in the synthesized CDK `Stack`. - * By default, performs partial matching on the parameter, via the `Match.objectLike()`. - * To configure different behavior, use other matchers in the `Match` class. - * @param logicalId the name of the parameter. Provide `'*'` to match all parameters in the template. - * @param props the error as should be expected. + * Assert that an warning with the given message exists in the synthesized CDK `Stack`. + * + * @param constructPath the construct path to the warning. Provide `'*'` to match all warnings in the template. + * @param message the warning message as should be expected. This should be a string or Matcher object. */ - public hasError(logicalId: string, props: any): void { - if (props.level && props.level !== 'error') { - throw new Error(`Message level mismatch: expected no level or 'error' but got ${props.level}`); - } - - const matchError = hasMessage(this._messages, logicalId, { - ...props, - level: 'error', - }); + public hasWarning(constructPath: string, message: any): void { + const matchError = hasMessage(this._messages, constructPath, constructMessage('warning', message)); if (matchError) { throw new Error(matchError); } } /** - * Get the set of matching Errors of a given id and properties. - * @param logicalId the name of the parameter. Provide `'*'` to match all parameters in the template. - * @param props by default, matches all resources with the given logicalId. - * When a literal is provided, performs a partial match via `Match.objectLike()`. - * Use the `Match` APIs to configure a different behaviour. + * Get the set of matching warning of a given construct path and message. + * + * @param constructPath the construct path to the warning. Provide `'*'` to match all warnings in the template. + * @param message the warning message as should be expected. This should be a string or Matcher object. */ - public findError(logicalId: string, props: any): Message[] { - if (props.level && props.level !== 'error') { - throw new Error(`Message level mismatch: expected no level or 'error' but got ${props.level}`); - } - - return convertMessagesTypeToArray(findMessage(this._messages, logicalId, { - ...props, - level: 'error', - }) as Messages); + public findWarning(constructPath: string, message: any): Message[] { + return convertMessagesTypeToArray(findMessage(this._messages, constructPath, constructMessage('warning', message)) as Messages); } /** - * Assert that a Warning with the given properties exists in the synthesized CDK `Stack`. - * By default, performs partial matching on the parameter, via the `Match.objectLike()`. - * To configure different behavior, use other matchers in the `Match` class. - * @param logicalId the name of the parameter. Provide `'*'` to match all parameters in the template. - * @param props the warning as should be expected. + * Assert that an info with the given message exists in the synthesized CDK `Stack`. + * + * @param constructPath the construct path to the info. Provide `'*'` to match all info in the template. + * @param message the info message as should be expected. This should be a string or Matcher object. */ - public hasWarning(logicalId: string, props: any): void { - if (props.level && props.level !== 'warning') { - throw new Error(`Message level mismatch: expected no level or 'warning' but got ${props.level}`); - } - - const matchError = hasMessage(this._messages, logicalId, { - ...props, - level: 'warning', - }); + public hasInfo(constructPath: string, message: any): void { + const matchError = hasMessage(this._messages, constructPath, constructMessage('info', message)); if (matchError) { throw new Error(matchError); } } /** - * Get the set of matching Warnings of a given id and properties. - * @param logicalId the name of the parameter. Provide `'*'` to match all parameters in the template. - * @param props by default, matches all resources with the given logicalId. - * When a literal is provided, performs a partial match via `Match.objectLike()`. - * Use the `Match` APIs to configure a different behaviour. + * Get the set of matching infos of a given construct path and message. + * + * @param constructPath the construct path to the info. Provide `'*'` to match all infos in the template. + * @param message the info message as should be expected. This should be a string or Matcher object. */ - public findWarning(logicalId: string, props: any): Message[] { - if (props.level && props.level !== 'warning') { - throw new Error(`Message level mismatch: expected no level or 'warning' but got ${props.level}`); - } - - return convertMessagesTypeToArray(findMessage(this._messages, logicalId, { - ...props, - level: 'warning', - }) as Messages); + public findInfo(constructPath: string, message: any): Message[] { + return convertMessagesTypeToArray(findMessage(this._messages, constructPath, constructMessage('info', message)) as Messages); } } +function constructMessage(type: 'info' | 'warning' | 'error', message: any): {[key:string]: any } { + return { + level: type, + entry: { + data: message, + }, + }; +} + function convertArrayToMessagesType(messages: Message[]): Messages { return messages.reduce((obj, item) => { return { From 6a3c075136d6449476eac113af345fba00d95b54 Mon Sep 17 00:00:00 2001 From: kaizen3031593 Date: Thu, 20 Jan 2022 17:35:07 -0500 Subject: [PATCH 09/13] fix tests --- .../assertions/lib/assert-annotations.ts | 17 +- .../assertions/lib/private/message.ts | 17 +- .../test/assert-annotations.test.ts | 217 +++++------------- 3 files changed, 66 insertions(+), 185 deletions(-) diff --git a/packages/@aws-cdk/assertions/lib/assert-annotations.ts b/packages/@aws-cdk/assertions/lib/assert-annotations.ts index dd29d2b38d85a..f05d94233837c 100644 --- a/packages/@aws-cdk/assertions/lib/assert-annotations.ts +++ b/packages/@aws-cdk/assertions/lib/assert-annotations.ts @@ -1,5 +1,6 @@ import { Stack, Stage } from '@aws-cdk/core'; -import { Message, Messages } from './private/message'; +import { SynthesisMessage } from '@aws-cdk/cx-api'; +import { Messages } from './private/message'; import { findMessage, hasMessage } from './private/messages'; /** @@ -17,7 +18,7 @@ export class AssertAnnotations { private readonly _messages: Messages; - private constructor(messages: Message[]) { + private constructor(messages: SynthesisMessage[]) { this._messages = convertArrayToMessagesType(messages); } @@ -40,7 +41,7 @@ export class AssertAnnotations { * @param constructPath the construct path to the error. Provide `'*'` to match all errors in the template. * @param message the error message as should be expected. This should be a string or Matcher object. */ - public findError(constructPath: string, message: any): Message[] { + public findError(constructPath: string, message: any): SynthesisMessage[] { return convertMessagesTypeToArray(findMessage(this._messages, constructPath, constructMessage('error', message)) as Messages); } @@ -63,7 +64,7 @@ export class AssertAnnotations { * @param constructPath the construct path to the warning. Provide `'*'` to match all warnings in the template. * @param message the warning message as should be expected. This should be a string or Matcher object. */ - public findWarning(constructPath: string, message: any): Message[] { + public findWarning(constructPath: string, message: any): SynthesisMessage[] { return convertMessagesTypeToArray(findMessage(this._messages, constructPath, constructMessage('warning', message)) as Messages); } @@ -86,7 +87,7 @@ export class AssertAnnotations { * @param constructPath the construct path to the info. Provide `'*'` to match all infos in the template. * @param message the info message as should be expected. This should be a string or Matcher object. */ - public findInfo(constructPath: string, message: any): Message[] { + public findInfo(constructPath: string, message: any): SynthesisMessage[] { return convertMessagesTypeToArray(findMessage(this._messages, constructPath, constructMessage('info', message)) as Messages); } } @@ -100,7 +101,7 @@ function constructMessage(type: 'info' | 'warning' | 'error', message: any): {[k }; } -function convertArrayToMessagesType(messages: Message[]): Messages { +function convertArrayToMessagesType(messages: SynthesisMessage[]): Messages { return messages.reduce((obj, item) => { return { ...obj, @@ -109,8 +110,8 @@ function convertArrayToMessagesType(messages: Message[]): Messages { }, {}) as Messages; } -function convertMessagesTypeToArray(messages: Messages): Message[] { - return Object.values(messages) as Message[]; +function convertMessagesTypeToArray(messages: Messages): SynthesisMessage[] { + return Object.values(messages) as SynthesisMessage[]; } function toMessages(stack: Stack): any { diff --git a/packages/@aws-cdk/assertions/lib/private/message.ts b/packages/@aws-cdk/assertions/lib/private/message.ts index 5fe870d384805..9657a5d90ad99 100644 --- a/packages/@aws-cdk/assertions/lib/private/message.ts +++ b/packages/@aws-cdk/assertions/lib/private/message.ts @@ -1,16 +1,5 @@ -export type Messages = { - Messages: { [logicalId: string]: Message } -} +import { SynthesisMessage } from '@aws-cdk/cx-api'; -export type Message = { - level: 'info' | 'warning' | 'error'; - id: string; - entry: MetadataEntry; - [key: string]: any; -}; - -export type MetadataEntry = { - type: string, - data?: any, - trace?: string[], +export type Messages = { + [logicalId: string]: SynthesisMessage; } diff --git a/packages/@aws-cdk/assertions/test/assert-annotations.test.ts b/packages/@aws-cdk/assertions/test/assert-annotations.test.ts index e23b9819bec10..a2d33e3e2c947 100644 --- a/packages/@aws-cdk/assertions/test/assert-annotations.test.ts +++ b/packages/@aws-cdk/assertions/test/assert-annotations.test.ts @@ -4,6 +4,7 @@ import { AssertAnnotations, Match } from '../lib'; describe('Messages', () => { let stack: Stack; + let annotations: AssertAnnotations; beforeAll(() => { stack = new Stack(); new CfnResource(stack, 'Foo', { @@ -27,213 +28,97 @@ describe('Messages', () => { }, }); - Aspects.of(stack).add(new MyAspect()); - }); - - describe('fromStack', () => { - test('default', () => { - const annotations = AssertAnnotations.fromStack(stack); - expect(annotations.messageList).toHaveLength(3); + new CfnResource(stack, 'Qux', { + type: 'Fred::Thud', + properties: { + Fred: 'Bar', + }, }); - }); - - describe('hasMessage', () => { - test('exact match', () => { - const annotations = AssertAnnotations.fromStack(stack); - annotations.hasMessage('/Default/Foo', { - level: 'error', - entry: { - data: 'this is an error', - }, - }); - expect(() => annotations.hasMessage('/Default/Foo', { - level: 'warning', - entry: { - data: 'this is a warning', - }, - })).toThrowError(/Expected warning but received error at \/level/); - - expect(() => annotations.hasMessage('/Default/Foo', { - level: 'error', - entry: { - data: 'this is wrong', - }, - })).toThrowError(/Expected this is wrong but received this is an error at \/entry\/data/); - }); + Aspects.of(stack).add(new MyAspect()); + annotations = AssertAnnotations.fromStack(stack); + }); - test('stack trace is redacted', () => { - const annotations = AssertAnnotations.fromStack(stack); - expect(() => annotations.hasMessage('/Default/Foo', { - level: 'error', - entry: { - data: 'this is wrong', - }, - })).toThrowError(/"trace": "redacted"/); + describe('hasError', () => { + test('match', () => { + annotations.hasError('/Default/Foo', 'this is an error'); }); - test('with matchers', () => { - const annotations = AssertAnnotations.fromStack(stack); - annotations.hasMessage('/Default/Foo', Match.not({ - level: 'warning', - })); - annotations.hasMessage('/Default/Fred', { - level: Match.anyValue(), - entry: Match.objectEquals({ - type: 'aws:cdk:warning', - data: 'this is a warning', - trace: Match.anyValue(), - }), - }); + test('no match', () => { + expect(() => annotations.hasError('/Default/Fred', Match.anyValue())) + .toThrowError(/Stack has 1 messages, but none match as expected./); }); }); - describe('findMessage', () => { - test('matching', () => { - const annotations = AssertAnnotations.fromStack(stack); - const result = annotations.findMessage('*', { level: 'error' }); + describe('findError', () => { + test('match', () => { + const result = annotations.findError('*', Match.anyValue()); expect(Object.keys(result).length).toEqual(2); - expect(result[0].id).toEqual('/Default/Foo'); - expect(result[1].id).toEqual('/Default/Bar'); }); - test('not matching', () => { - const annotations = AssertAnnotations.fromStack(stack); - const resultA = annotations.findMessage('*', { level: 'info' }); - expect(Object.keys(resultA).length).toEqual(0); - - const resultB = annotations.findMessage('*', { - level: 'warning', - entry: { - data: 'this is not a warning', - }, - }); - expect(Object.keys(resultB).length).toEqual(0); - }); - - test('matching specific output', () => { - const annotations = AssertAnnotations.fromStack(stack); - const result = annotations.findMessage('/Default/Bar', { - level: 'error', - }); - - expect(Object.keys(result).length).toEqual(1); - expect(result[0].id).toEqual('/Default/Bar'); - }); - - test('not matching specific output', () => { - const annotations = AssertAnnotations.fromStack(stack); - const result = annotations.findMessage('/Default/Bear', { - level: 'error', - }); - + test('no match', () => { + const result = annotations.findError('*', 'no message looks like this'); expect(Object.keys(result).length).toEqual(0); }); - - test('with matchers', () => { - const annotations = AssertAnnotations.fromStack(stack); - const resultA = annotations.findMessage('/Default/Foo', Match.not({ - level: 'warning', - })); - expect(Object.keys(resultA).length).toEqual(1); - - const resultB = annotations.findMessage('/Default/Fred', { - level: Match.anyValue(), - entry: Match.objectEquals({ - type: 'aws:cdk:warning', - data: 'this is a warning', - trace: Match.anyValue(), - }), - }); - expect(Object.keys(resultB).length).toEqual(1); - }); }); - describe('hasError', () => { + describe('hasWarning', () => { test('match', () => { - AssertAnnotations.fromStack(stack).hasError('/Default/Foo', { - entry: { - data: 'this is an error', - }, - }); + annotations.hasWarning('/Default/Fred', 'this is a warning'); }); test('no match', () => { - expect(() => AssertAnnotations.fromStack(stack).hasError('/Default/Fred', { - })).toThrowError(/Stack has 1 messages, but none match as expected./); - }); - - test('incorrect specification', () => { - expect(() => AssertAnnotations.fromStack(stack).hasWarning('*', { - level: 'info', - })).toThrowError(/Message level mismatch:/); + expect(() => annotations.hasWarning('/Default/Foo', Match.anyValue())).toThrowError(/Stack has 1 messages, but none match as expected./); }); }); - describe('findError', () => { + describe('findWarning', () => { test('match', () => { - const result = AssertAnnotations.fromStack(stack).findError('*', { - }); - expect(Object.keys(result).length).toEqual(2); + const result = annotations.findWarning('*', Match.anyValue()); + expect(Object.keys(result).length).toEqual(1); }); test('no match', () => { - const result = AssertAnnotations.fromStack(stack).findError('*', { - entry: { - data: 'no message looks like this', - }, - }); + const result = annotations.findWarning('*', 'no message looks like this'); expect(Object.keys(result).length).toEqual(0); }); - - test('incorrect specification', () => { - expect(() => AssertAnnotations.fromStack(stack).findError('*', { - level: 'info', - })).toThrowError(/Message level mismatch:/); - }); }); - describe('hasWarning', () => { + describe('hasInfo', () => { test('match', () => { - AssertAnnotations.fromStack(stack).hasWarning('/Default/Fred', { - entry: { - data: 'this is a warning', - }, - }); + annotations.hasInfo('/Default/Qux', 'this is an info'); }); test('no match', () => { - expect(() => AssertAnnotations.fromStack(stack).hasWarning('/Default/Foo', { - })).toThrowError(/Stack has 1 messages, but none match as expected./); - }); - - test('incorrect specification', () => { - expect(() => AssertAnnotations.fromStack(stack).hasWarning('*', { - level: 'info', - })).toThrowError(/Message level mismatch:/); + expect(() => annotations.hasInfo('/Default/Qux', 'this info is incorrect')).toThrowError(/Stack has 1 messages, but none match as expected./); }); }); - describe('findWarning', () => { + describe('findInfo', () => { test('match', () => { - const result = AssertAnnotations.fromStack(stack).findWarning('*', { - }); + const result = annotations.findInfo('/Default/Qux', 'this is an info'); expect(Object.keys(result).length).toEqual(1); }); test('no match', () => { - const result = AssertAnnotations.fromStack(stack).findWarning('*', { - entry: { - data: 'no message looks like this', - }, - }); + const result = annotations.findInfo('*', 'no message looks like this'); expect(Object.keys(result).length).toEqual(0); }); + }); + + describe('with matchers', () => { + test('anyValue', () => { + const result = annotations.findError('*', Match.anyValue()); + expect(Object.keys(result).length).toEqual(2); + }); - test('incorrect specification', () => { - expect(() => AssertAnnotations.fromStack(stack).findWarning('*', { - level: 'info', - })).toThrowError(/Message level mismatch:/); + test('not', () => { + expect(() => annotations.hasError('/Default/Foo', Match.not('this is an error'))) + .toThrowError(/Found unexpected match: "this is an error" at \/entry\/data/); + }); + + test('stringLikeRegEx', () => { + annotations.hasError('/Default/Foo', Match.stringLikeRegexp('.*error')); }); }); }); @@ -243,8 +128,10 @@ class MyAspect implements IAspect { if (node instanceof CfnResource) { if (node.cfnResourceType === 'Foo::Bar') { this.error(node, 'this is an error'); - } else { + } else if (node.cfnResourceType === 'Baz::Qux') { this.warn(node, 'this is a warning'); + } else { + this.info(node, 'this is an info'); } } }; @@ -256,4 +143,8 @@ class MyAspect implements IAspect { protected error(node: IConstruct, message: string): void { Annotations.of(node).addError(message); } + + protected info(node: IConstruct, message: string): void { + Annotations.of(node).addInfo(message); + } } \ No newline at end of file From b9ec3a952d0ce2d6fed55ab3c6ee73ce552363a1 Mon Sep 17 00:00:00 2001 From: kaizen3031593 Date: Thu, 20 Jan 2022 17:48:13 -0500 Subject: [PATCH 10/13] touched up readme --- packages/@aws-cdk/assertions/README.md | 37 +++++++++++++------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/packages/@aws-cdk/assertions/README.md b/packages/@aws-cdk/assertions/README.md index 65139db5ad47a..9c2eb6d2fb91b 100644 --- a/packages/@aws-cdk/assertions/README.md +++ b/packages/@aws-cdk/assertions/README.md @@ -538,26 +538,27 @@ class MyStack extends cdk.Stack { We can then assert that the stack contains the expected Error: ```ts -AssertAnnotations.fromStack(stack).hasMessage('/Default/Foo', { - level: 'error', - entry: { - data: 'we do not want a Foo::Bar resource', - }, -}); +AssertAnnotations.fromStack(stack).hasError( + '/Default/Foo', + 'we do not want a Foo::Bar resource', +); ``` -In addition to `hasMessage()`, we can also use `hasError()` or `hasWarning()` to similar -effect: +Here are the available APIs for `AssertAnnotations`: + +- `hasError()` and `findError()` +- `hasWarning()` and `findWarning()` +- `hasInfo()` and `findInfo()` + +The corresponding `findXxx()` API is complementary to the `hasXxx()` API, except instead +of asserting its presence, it returns the set of matching messages. + +In addition, this suite of APIs is compatable with `Matchers` for more fine-grained control. +For example, the following assertion works as well: ```ts -AssertAnnotations.fromStack(stack).hasError('/Default/Foo', { - entry: { - data: 'we do not want a Foo::Bar resource', - } -}); +AssertAnnotations.fromStack(stack).hasError( + '/Default/Foo', + '.*Foo::Bar.*', +); ``` - -Similarly to `Template`, `AssertAnnotations` also provides APIs to retrieve matching -messages. The `findMessages()` API is complementary to the `hasMessages()` API, except -instead of asserting its presence, it returns the set of matching messages. Each -`hasXxx()` API has a complementary `findXxx()` API. \ No newline at end of file From d44ff35f2994e7677d8728b1ef01aca7dec7195d Mon Sep 17 00:00:00 2001 From: kaizen3031593 Date: Thu, 20 Jan 2022 17:50:55 -0500 Subject: [PATCH 11/13] stack trace --- packages/@aws-cdk/assertions/lib/private/messages.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/assertions/lib/private/messages.ts b/packages/@aws-cdk/assertions/lib/private/messages.ts index a5fbafbc1f944..75c6fe3ae50b1 100644 --- a/packages/@aws-cdk/assertions/lib/private/messages.ts +++ b/packages/@aws-cdk/assertions/lib/private/messages.ts @@ -25,17 +25,14 @@ export function hasMessage(messages: Messages, logicalId: string, props: any): s return 'No messages found in the stack'; } - const renderTrace = props.entry?.trace ? true : false; return [ `Stack has ${result.analyzedCount} messages, but none match as expected.`, - formatFailure(formatMessage(result.closestResult, renderTrace)), + formatFailure(formatMessage(result.closestResult)), ].join('\n'); } -/** - * We redact the stack trace by default because it is unnecessarily long and unintelligible. - * The only time where we include the stack trace is when we are asserting against it. - */ +// We redact the stack trace by default because it is unnecessarily long and unintelligible. +// If there is a use case for rendering the trace, we can add it later. function formatMessage(match: MatchResult, renderTrace: boolean = false): MatchResult { if (!renderTrace) { match.target.entry.trace = 'redacted'; From f06dcd1ddfcbd981a96378a9ecfdb47ea1a01dd4 Mon Sep 17 00:00:00 2001 From: kaizen3031593 Date: Mon, 24 Jan 2022 10:33:40 -0500 Subject: [PATCH 12/13] rename AssertAnnotations to Annotations --- .../lib/{assert-annotations.ts => annotations.ts} | 6 +++--- packages/@aws-cdk/assertions/lib/index.ts | 2 +- .../test/{assert-annotations.test.ts => annotations.ts} | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) rename packages/@aws-cdk/assertions/lib/{assert-annotations.ts => annotations.ts} (97%) rename packages/@aws-cdk/assertions/test/{assert-annotations.test.ts => annotations.ts} (96%) diff --git a/packages/@aws-cdk/assertions/lib/assert-annotations.ts b/packages/@aws-cdk/assertions/lib/annotations.ts similarity index 97% rename from packages/@aws-cdk/assertions/lib/assert-annotations.ts rename to packages/@aws-cdk/assertions/lib/annotations.ts index f05d94233837c..c656b15d6bab8 100644 --- a/packages/@aws-cdk/assertions/lib/assert-annotations.ts +++ b/packages/@aws-cdk/assertions/lib/annotations.ts @@ -7,13 +7,13 @@ import { findMessage, hasMessage } from './private/messages'; * Suite of assertions that can be run on a CDK Stack. * Focused on asserting annotations. */ -export class AssertAnnotations { +export class Annotations { /** * Base your assertions on the messages returned by a synthesized CDK `Stack`. * @param stack the CDK Stack to run assertions on */ - public static fromStack(stack: Stack): AssertAnnotations { - return new AssertAnnotations(toMessages(stack)); + public static fromStack(stack: Stack): Annotations { + return new Annotations(toMessages(stack)); } private readonly _messages: Messages; diff --git a/packages/@aws-cdk/assertions/lib/index.ts b/packages/@aws-cdk/assertions/lib/index.ts index 221f501b738bb..eccbfac38637f 100644 --- a/packages/@aws-cdk/assertions/lib/index.ts +++ b/packages/@aws-cdk/assertions/lib/index.ts @@ -2,4 +2,4 @@ export * from './capture'; export * from './template'; export * from './match'; export * from './matcher'; -export * from './assert-annotations'; \ No newline at end of file +export * from './annotations'; \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/test/assert-annotations.test.ts b/packages/@aws-cdk/assertions/test/annotations.ts similarity index 96% rename from packages/@aws-cdk/assertions/test/assert-annotations.test.ts rename to packages/@aws-cdk/assertions/test/annotations.ts index a2d33e3e2c947..8275e52d39dff 100644 --- a/packages/@aws-cdk/assertions/test/assert-annotations.test.ts +++ b/packages/@aws-cdk/assertions/test/annotations.ts @@ -1,10 +1,10 @@ import { Annotations, Aspects, CfnResource, IAspect, Stack } from '@aws-cdk/core'; import { IConstruct } from 'constructs'; -import { AssertAnnotations, Match } from '../lib'; +import { Annotations as _Annotations, Match } from '../lib'; describe('Messages', () => { let stack: Stack; - let annotations: AssertAnnotations; + let annotations: _Annotations; beforeAll(() => { stack = new Stack(); new CfnResource(stack, 'Foo', { @@ -36,7 +36,7 @@ describe('Messages', () => { }); Aspects.of(stack).add(new MyAspect()); - annotations = AssertAnnotations.fromStack(stack); + annotations = _Annotations.fromStack(stack); }); describe('hasError', () => { From 46438be188ae11b13ef71058aad5b1b269ed7ef3 Mon Sep 17 00:00:00 2001 From: kaizen3031593 Date: Mon, 24 Jan 2022 10:38:57 -0500 Subject: [PATCH 13/13] fix up readme --- packages/@aws-cdk/assertions/README.md | 13 +++++++------ .../@aws-cdk/assertions/rosetta/default.ts-fixture | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/assertions/README.md b/packages/@aws-cdk/assertions/README.md index 871f476891379..5370b1d094c8e 100644 --- a/packages/@aws-cdk/assertions/README.md +++ b/packages/@aws-cdk/assertions/README.md @@ -498,8 +498,7 @@ fredCapture.asString(); // returns "Quib" In addition to template matching, we provide an API for annotation matching. [Annotations](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.Annotations.html) can be added via the [Aspects](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.Aspects.html) -API. which you can learn more about -[here](https://docs.aws.amazon.com/cdk/v2/guide/aspects.html). +API. You can learn more about Aspects [here](https://docs.aws.amazon.com/cdk/v2/guide/aspects.html). Say you have a `MyAspect` and a `MyStack` that uses `MyAspect`: @@ -538,13 +537,15 @@ class MyStack extends cdk.Stack { We can then assert that the stack contains the expected Error: ```ts -AssertAnnotations.fromStack(stack).hasError( +// import { Annotations } from '@aws-cdk/assertions'; + +Annotations.fromStack(stack).hasError( '/Default/Foo', 'we do not want a Foo::Bar resource', ); ``` -Here are the available APIs for `AssertAnnotations`: +Here are the available APIs for `Annotations`: - `hasError()` and `findError()` - `hasWarning()` and `findWarning()` @@ -557,8 +558,8 @@ In addition, this suite of APIs is compatable with `Matchers` for more fine-grai For example, the following assertion works as well: ```ts -AssertAnnotations.fromStack(stack).hasError( +Annotations.fromStack(stack).hasError( '/Default/Foo', - '.*Foo::Bar.*', + Match.stringLikeRegexp('.*Foo::Bar.*'), ); ``` diff --git a/packages/@aws-cdk/assertions/rosetta/default.ts-fixture b/packages/@aws-cdk/assertions/rosetta/default.ts-fixture index 47c4c8dbbcf06..32d751f8c38fe 100644 --- a/packages/@aws-cdk/assertions/rosetta/default.ts-fixture +++ b/packages/@aws-cdk/assertions/rosetta/default.ts-fixture @@ -1,7 +1,7 @@ // Fixture with packages imported, but nothing else import { Construct } from 'constructs'; import { Aspects, CfnResource, Stack } from '@aws-cdk/core'; -import { AssertAnnotations, Capture, Match, Template } from '@aws-cdk/assertions'; +import { Annotations, Capture, Match, Template } from '@aws-cdk/assertions'; class Fixture extends Stack { constructor(scope: Construct, id: string) {