diff --git a/packages/@aws-cdk/aws-lambda-destinations/test/integ.destinations.ts b/packages/@aws-cdk/aws-lambda-destinations/test/integ.destinations.ts index 61009f180e53d..5dd67661acbb8 100644 --- a/packages/@aws-cdk/aws-lambda-destinations/test/integ.destinations.ts +++ b/packages/@aws-cdk/aws-lambda-destinations/test/integ.destinations.ts @@ -73,13 +73,13 @@ const integ = new IntegTest(app, 'Destinations', { testCases: [stack], }); -integ.assert.invokeFunction({ +integ.assertions.invokeFunction({ functionName: stack.fn.functionName, invocationType: InvocationType.EVENT, payload: JSON.stringify({ status: 'OK' }), }); -const message = integ.assert.awsApiCall('SQS', 'receiveMessage', { +const message = integ.assertions.awsApiCall('SQS', 'receiveMessage', { QueueUrl: stack.queue.queueUrl, WaitTimeSeconds: 20, }); diff --git a/packages/@aws-cdk/aws-lambda/test/integ.bundling.ts b/packages/@aws-cdk/aws-lambda/test/integ.bundling.ts index 30e28c8ef5fe7..8549134ca9e5b 100644 --- a/packages/@aws-cdk/aws-lambda/test/integ.bundling.ts +++ b/packages/@aws-cdk/aws-lambda/test/integ.bundling.ts @@ -47,10 +47,10 @@ const integ = new IntegTest(app, 'Bundling', { stackUpdateWorkflow: false, }); -const invoke = integ.assert.invokeFunction({ +const invoke = integ.assertions.invokeFunction({ functionName: stack.functionName, }); -invoke.assert(ExpectedResult.objectLike({ +invoke.expect(ExpectedResult.objectLike({ Payload: '200', })); app.synth(); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/integ.put-events.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/integ.put-events.ts index 1c6ee34a3341b..d72ecfb494415 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/integ.put-events.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/integ.put-events.ts @@ -46,17 +46,17 @@ const testCase = new IntegTest(app, 'PutEvents', { }); // Start an execution -const start = testCase.assert.awsApiCall('StepFunctions', 'startExecution', { +const start = testCase.assertions.awsApiCall('StepFunctions', 'startExecution', { stateMachineArn: sm.stateMachineArn, }); // describe the results of the execution -const describe = testCase.assert.awsApiCall('StepFunctions', 'describeExecution', { +const describe = testCase.assertions.awsApiCall('StepFunctions', 'describeExecution', { executionArn: start.getAttString('executionArn'), }); // assert the results -describe.assert(ExpectedResult.objectLike({ +describe.expect(ExpectedResult.objectLike({ status: 'SUCCEEDED', })); diff --git a/packages/@aws-cdk/integ-tests/README.md b/packages/@aws-cdk/integ-tests/README.md index 0e8fc9b1ca501..414f21e5a61fd 100644 --- a/packages/@aws-cdk/integ-tests/README.md +++ b/packages/@aws-cdk/integ-tests/README.md @@ -177,41 +177,50 @@ new IntegTest(app, 'Integ', { testCases: [stackUnderTest, testCaseWithAssets] }) This library also provides a utility to make assertions against the infrastructure that the integration test deploys. -The easiest way to do this is to create a `TestCase` and then access the `DeployAssert` that is automatically created. +There are two main scenarios in which assertions are created. + +- Part of an integration test using `integ-runner` + +In this case you would create an integration test using the `IntegTest` construct and then make assertions using the `assert` property. +You should **not** utilize the assertion constructs directly, but should instead use the `methods` on `IntegTest.assert`. ```ts declare const app: App; declare const stack: Stack; const integ = new IntegTest(app, 'Integ', { testCases: [stack] }); -integ.assert.awsApiCall('S3', 'getObject'); +integ.assertions.awsApiCall('S3', 'getObject'); ``` -### DeployAssert - -Assertions are created by using the `DeployAssert` construct. This construct creates it's own `Stack` separate from -any stacks that you create as part of your integration tests. This `Stack` is treated differently from other stacks -by the `integ-runner` tool. For example, this stack will not be diffed by the `integ-runner`. +- Part of a normal CDK deployment -Any assertions that you create should be created in the scope of `DeployAssert`. For example, +In this case you may be using assertions as part of a normal CDK deployment in order to make an assertion on the infrastructure +before the deployment is considered successful. In this case you can utilize the assertions constructs directly. ```ts -declare const app: App; +declare const myAppStack: Stack; -const assert = new DeployAssert(app); -new AwsApiCall(assert, 'GetObject', { +new AwsApiCall(myAppStack, 'GetObject', { service: 'S3', api: 'getObject', }); ``` +### DeployAssert + +Assertions are created by using the `DeployAssert` construct. This construct creates it's own `Stack` separate from +any stacks that you create as part of your integration tests. This `Stack` is treated differently from other stacks +by the `integ-runner` tool. For example, this stack will not be diffed by the `integ-runner`. + `DeployAssert` also provides utilities to register your own assertions. ```ts declare const myCustomResource: CustomResource; +declare const stack: Stack; declare const app: App; -const assert = new DeployAssert(app); -assert.assert( + +const integ = new IntegTest(app, 'Integ', { testCases: [stack] }); +integ.assertions.expect( 'CustomAssertion', ExpectedResult.objectLike({ foo: 'bar' }), ActualResult.fromCustomResource(myCustomResource, 'data'), @@ -228,12 +237,12 @@ AWS API call to receive some data. This library does this by utilizing CloudForm which means that CloudFormation will call out to a Lambda Function which will use the AWS JavaScript SDK to make the API call. -This can be done by using the class directory: +This can be done by using the class directory (in the case of a normal deployment): ```ts -declare const assert: DeployAssert; +declare const stack: Stack; -new AwsApiCall(assert, 'MyAssertion', { +new AwsApiCall(stack, 'MyAssertion', { service: 'SQS', api: 'receiveMessage', parameters: { @@ -242,12 +251,15 @@ new AwsApiCall(assert, 'MyAssertion', { }); ``` -Or by using the `awsApiCall` method on `DeployAssert`: +Or by using the `awsApiCall` method on `DeployAssert` (when writing integration tests): ```ts declare const app: App; -const assert = new DeployAssert(app); -assert.awsApiCall('SQS', 'receiveMessage', { +declare const stack: Stack; +const integ = new IntegTest(app, 'Integ', { + testCases: [stack], +}); +integ.assertions.awsApiCall('SQS', 'receiveMessage', { QueueUrl: 'url', }); ``` @@ -270,32 +282,29 @@ const integ = new IntegTest(app, 'Integ', { testCases: [stack], }); -integ.assert.invokeFunction({ +integ.assertions.invokeFunction({ functionName: fn.functionName, invocationType: InvocationType.EVENT, payload: JSON.stringify({ status: 'OK' }), }); -const message = integ.assert.awsApiCall('SQS', 'receiveMessage', { +const message = integ.assertions.awsApiCall('SQS', 'receiveMessage', { QueueUrl: queue.queueUrl, WaitTimeSeconds: 20, }); -new EqualsAssertion(integ.assert, 'ReceiveMessage', { - actual: ActualResult.fromAwsApiCall(message, 'Messages.0.Body'), - expected: ExpectedResult.objectLike({ - requestContext: { - condition: 'Success', - }, - requestPayload: { - status: 'OK', - }, - responseContext: { - statusCode: 200, - }, - responsePayload: 'success', - }), -}); +message.assertAtPath('Messages.0.Body', ExpectedResult.objectLike({ + requestContext: { + condition: 'Success', + }, + requestPayload: { + status: 'OK', + }, + responseContext: { + statusCode: 200, + }, + responsePayload: 'success', +})); ``` #### Match @@ -305,9 +314,8 @@ can be used to construct the `ExpectedResult`. ```ts declare const message: AwsApiCall; -declare const assert: DeployAssert; -message.assert(ExpectedResult.objectLike({ +message.expect(ExpectedResult.objectLike({ Messages: Match.arrayWith([ { Body: { @@ -336,10 +344,10 @@ const integ = new IntegTest(app, 'IntegTest', { testCases: [stack], }); -const invoke = integ.assert.invokeFunction({ +const invoke = integ.assertions.invokeFunction({ functionName: lambdaFunction.functionName, }); -invoke.assert(ExpectedResult.objectLike({ +invoke.expect(ExpectedResult.objectLike({ Payload: '200', })); ``` @@ -359,17 +367,17 @@ const testCase = new IntegTest(app, 'IntegTest', { }); // Start an execution -const start = testCase.assert.awsApiCall('StepFunctions', 'startExecution', { +const start = testCase.assertions.awsApiCall('StepFunctions', 'startExecution', { stateMachineArn: sm.stateMachineArn, }); // describe the results of the execution -const describe = testCase.assert.awsApiCall('StepFunctions', 'describeExecution', { +const describe = testCase.assertions.awsApiCall('StepFunctions', 'describeExecution', { executionArn: start.getAttString('executionArn'), }); // assert the results -describe.assert(ExpectedResult.objectLike({ +describe.expect(ExpectedResult.objectLike({ status: 'SUCCEEDED', })); ``` diff --git a/packages/@aws-cdk/integ-tests/lib/assertions/common.ts b/packages/@aws-cdk/integ-tests/lib/assertions/common.ts index 6e4fadf5a0388..6daa9e510133c 100644 --- a/packages/@aws-cdk/integ-tests/lib/assertions/common.ts +++ b/packages/@aws-cdk/integ-tests/lib/assertions/common.ts @@ -1,5 +1,6 @@ import { CustomResource } from '@aws-cdk/core'; -import { AwsApiCall } from './sdk'; +import { IAwsApiCall } from './sdk'; + /** * Represents the "actual" results to compare */ @@ -16,7 +17,7 @@ export abstract class ActualResult { /** * Get the actual results from a AwsApiCall */ - public static fromAwsApiCall(query: AwsApiCall, attribute: string): ActualResult { + public static fromAwsApiCall(query: IAwsApiCall, attribute: string): ActualResult { return { result: query.getAttString(attribute), }; diff --git a/packages/@aws-cdk/integ-tests/lib/assertions/deploy-assert.ts b/packages/@aws-cdk/integ-tests/lib/assertions/deploy-assert.ts deleted file mode 100644 index 24bbfd6789fbf..0000000000000 --- a/packages/@aws-cdk/integ-tests/lib/assertions/deploy-assert.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { Stack } from '@aws-cdk/core'; -import { Construct, IConstruct, Node } from 'constructs'; -import { EqualsAssertion } from './assertions'; -import { ExpectedResult, ActualResult } from './common'; -import { md5hash } from './private/hash'; -import { AwsApiCall, LambdaInvokeFunction, LambdaInvokeFunctionProps } from './sdk'; - -const DEPLOY_ASSERT_SYMBOL = Symbol.for('@aws-cdk/integ-tests.DeployAssert'); - - -// keep this import separate from other imports to reduce chance for merge conflicts with v2-main -// eslint-disable-next-line no-duplicate-imports, import/order -import { Construct as CoreConstruct } from '@aws-cdk/core'; - -/** - * Options for DeployAssert - */ -export interface DeployAssertProps { } - -/** - * Construct that allows for registering a list of assertions - * that should be performed on a construct - */ -export class DeployAssert extends CoreConstruct { - - /** - * Returns whether the construct is a DeployAssert construct - */ - public static isDeployAssert(x: any): x is DeployAssert { - return x !== null && typeof(x) === 'object' && DEPLOY_ASSERT_SYMBOL in x; - } - - /** - * Finds a DeployAssert construct in the given scope - */ - public static of(construct: IConstruct): DeployAssert { - const scopes = Node.of(Node.of(construct).root).findAll(); - const deployAssert = scopes.find(s => DeployAssert.isDeployAssert(s)); - if (!deployAssert) { - throw new Error('No DeployAssert construct found in scopes'); - } - return deployAssert as DeployAssert; - } - - constructor(scope: Construct) { - /** - * Normally we would not want to do a scope swapparoo like this - * but in this case this it allows us to provide a better experience - * for the user. This allows DeployAssert to be created _not_ in the - * scope of a Stack. DeployAssert is treated like a Stack, but doesn't - * exose any of the stack functionality (the methods that the user sees - * are just DeployAssert methods and not any Stack methods). So you can do - * something like this, which you would not normally be allowed to do - * - * const deployAssert = new DeployAssert(app); - * new AwsApiCall(deployAssert, 'AwsApiCall', {...}); - */ - scope = new Stack(scope, 'DeployAssert'); - super(scope, 'Default'); - - Object.defineProperty(this, DEPLOY_ASSERT_SYMBOL, { value: true }); - } - - /** - * Query AWS using JavaScript SDK V2 API calls. This can be used to either - * trigger an action or to return a result that can then be asserted against - * an expected value - * - * @example - * declare const app: App; - * const assert = new DeployAssert(app); - * assert.awsApiCall('SQS', 'sendMessage', { - * QueueUrl: 'url', - * MessageBody: 'hello', - * }); - * const message = assert.awsApiCall('SQS', 'receiveMessage', { - * QueueUrl: 'url', - * }); - * message.assert(ExpectedResult.objectLike({ - * Messages: [{ Body: 'hello' }], - * })); - */ - public awsApiCall(service: string, api: string, parameters?: any): AwsApiCall { - return new AwsApiCall(this, `AwsApiCall${service}${api}`, { - api, - service, - parameters, - }); - } - - /** - * Invoke a lambda function and return the response which can be asserted - * - * @example - * declare const app: App; - * const assert = new DeployAssert(app); - * const invoke = assert.invokeFunction({ - * functionName: 'my-function', - * }); - * invoke.assert(ExpectedResult.objectLike({ - * Payload: '200', - * })); - */ - public invokeFunction(props: LambdaInvokeFunctionProps): LambdaInvokeFunction { - const hash = md5hash(Stack.of(this).resolve(props)); - return new LambdaInvokeFunction(this, `LambdaInvoke${hash}`, props); - } - - /** - * Assert that the ExpectedResult is equal - * to the ActualResult - * - * @example - * declare const deployAssert: DeployAssert; - * declare const apiCall: AwsApiCall; - * deployAssert.assert( - * 'invoke', - * ExpectedResult.objectLike({ Payload: 'OK' }), - * ActualResult.fromAwsApiCall(apiCall, 'Body'), - * ); - */ - public assert(id: string, expected: ExpectedResult, actual: ActualResult): void { - new EqualsAssertion(this, `EqualsAssertion${id}`, { - expected, - actual, - }); - } -} diff --git a/packages/@aws-cdk/integ-tests/lib/assertions/index.ts b/packages/@aws-cdk/integ-tests/lib/assertions/index.ts index 3a9defd954be9..6622ddabcb560 100644 --- a/packages/@aws-cdk/integ-tests/lib/assertions/index.ts +++ b/packages/@aws-cdk/integ-tests/lib/assertions/index.ts @@ -1,6 +1,6 @@ -export * from './assertions'; +export * from './types'; export * from './sdk'; -export * from './deploy-assert'; +export * from './assertions'; export * from './providers'; export * from './common'; export * from './match'; diff --git a/packages/@aws-cdk/integ-tests/lib/assertions/private/deploy-assert.ts b/packages/@aws-cdk/integ-tests/lib/assertions/private/deploy-assert.ts new file mode 100644 index 0000000000000..1ff091978e7c5 --- /dev/null +++ b/packages/@aws-cdk/integ-tests/lib/assertions/private/deploy-assert.ts @@ -0,0 +1,76 @@ +import { Stack } from '@aws-cdk/core'; +import { Construct, IConstruct, Node } from 'constructs'; +import { EqualsAssertion } from '../assertions'; +import { ExpectedResult, ActualResult } from '../common'; +import { md5hash } from '../private/hash'; +import { AwsApiCall, LambdaInvokeFunction, IAwsApiCall, LambdaInvokeFunctionProps } from '../sdk'; +import { IDeployAssert } from '../types'; + + +const DEPLOY_ASSERT_SYMBOL = Symbol.for('@aws-cdk/integ-tests.DeployAssert'); + + +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { Construct as CoreConstruct } from '@aws-cdk/core'; + +/** + * Options for DeployAssert + */ +export interface DeployAssertProps { } + +/** + * Construct that allows for registering a list of assertions + * that should be performed on a construct + */ +export class DeployAssert extends CoreConstruct implements IDeployAssert { + + /** + * Returns whether the construct is a DeployAssert construct + */ + public static isDeployAssert(x: any): x is DeployAssert { + return x !== null && typeof(x) === 'object' && DEPLOY_ASSERT_SYMBOL in x; + } + + /** + * Finds a DeployAssert construct in the given scope + */ + public static of(construct: IConstruct): DeployAssert { + const scopes = Node.of(Node.of(construct).root).findAll(); + const deployAssert = scopes.find(s => DeployAssert.isDeployAssert(s)); + if (!deployAssert) { + throw new Error('No DeployAssert construct found in scopes'); + } + return deployAssert as DeployAssert; + } + + public scope: Stack; + + constructor(scope: Construct) { + super(scope, 'Default'); + + this.scope = new Stack(scope, 'DeployAssert'); + + Object.defineProperty(this, DEPLOY_ASSERT_SYMBOL, { value: true }); + } + + public awsApiCall(service: string, api: string, parameters?: any): IAwsApiCall { + return new AwsApiCall(this.scope, `AwsApiCall${service}${api}`, { + api, + service, + parameters, + }); + } + + public invokeFunction(props: LambdaInvokeFunctionProps): IAwsApiCall { + const hash = md5hash(this.scope.resolve(props)); + return new LambdaInvokeFunction(this.scope, `LambdaInvoke${hash}`, props); + } + + public expect(id: string, expected: ExpectedResult, actual: ActualResult): void { + new EqualsAssertion(this.scope, `EqualsAssertion${id}`, { + expected, + actual, + }); + } +} diff --git a/packages/@aws-cdk/integ-tests/lib/assertions/providers/lambda-handler/index.ts b/packages/@aws-cdk/integ-tests/lib/assertions/providers/lambda-handler/index.ts index 78a47c83be1ef..72ca3544cb66d 100644 --- a/packages/@aws-cdk/integ-tests/lib/assertions/providers/lambda-handler/index.ts +++ b/packages/@aws-cdk/integ-tests/lib/assertions/providers/lambda-handler/index.ts @@ -1,5 +1,4 @@ import { AssertionHandler } from './assertion'; -import { ResultsCollectionHandler } from './results'; import { AwsApiCallHandler } from './sdk'; import * as types from './types'; @@ -14,7 +13,6 @@ function createResourceHandler(event: AWSLambda.CloudFormationCustomResourceEven } switch (event.ResourceType) { case types.ASSERT_RESOURCE_TYPE: return new AssertionHandler(event, context); - case types.RESULTS_RESOURCE_TYPE: return new ResultsCollectionHandler(event, context); default: throw new Error(`Unsupported resource type "${event.ResourceType}`); } diff --git a/packages/@aws-cdk/integ-tests/lib/assertions/providers/lambda-handler/results.ts b/packages/@aws-cdk/integ-tests/lib/assertions/providers/lambda-handler/results.ts deleted file mode 100644 index 784ff68a05ab6..0000000000000 --- a/packages/@aws-cdk/integ-tests/lib/assertions/providers/lambda-handler/results.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { CustomResourceHandler } from './base'; -import { ResultsCollectionRequest, ResultsCollectionResult } from './types'; - -export class ResultsCollectionHandler extends CustomResourceHandler { - protected async processEvent(request: ResultsCollectionRequest): Promise { - const reduced: string = request.assertionResults.reduce((agg, result, idx) => { - const msg = result.status === 'pass' ? 'pass' : `fail - ${result.message}`; - return `${agg}\nTest${idx}: ${msg}`; - }, '').trim(); - return { message: reduced }; - } -} diff --git a/packages/@aws-cdk/integ-tests/lib/assertions/providers/lambda-handler/types.ts b/packages/@aws-cdk/integ-tests/lib/assertions/providers/lambda-handler/types.ts index ae9f545476dac..68bd63202afe8 100644 --- a/packages/@aws-cdk/integ-tests/lib/assertions/providers/lambda-handler/types.ts +++ b/packages/@aws-cdk/integ-tests/lib/assertions/providers/lambda-handler/types.ts @@ -2,7 +2,6 @@ // Kept in a separate file for sharing between the handler and the provider constructs. export const ASSERT_RESOURCE_TYPE = 'Custom::DeployAssert@AssertEquals'; -export const RESULTS_RESOURCE_TYPE = 'Custom::DeployAssert@ResultsCollection'; export const SDK_RESOURCE_TYPE_PREFIX = 'Custom::DeployAssert@SdkCall'; /** @@ -155,24 +154,3 @@ export interface AssertionResultData { */ readonly message?: string; } - -/** - * Represents a collection of assertion request results - */ -export interface ResultsCollectionRequest { - /** - * The results of all the assertions that have been - * registered - */ - readonly assertionResults: AssertionResultData[]; -} - -/** - * The result of a results request - */ -export interface ResultsCollectionResult { - /** - * A message containing the results of the assertion - */ - readonly message: string; -} diff --git a/packages/@aws-cdk/integ-tests/lib/assertions/sdk.ts b/packages/@aws-cdk/integ-tests/lib/assertions/sdk.ts index b176c13456f37..443554b5c38f7 100644 --- a/packages/@aws-cdk/integ-tests/lib/assertions/sdk.ts +++ b/packages/@aws-cdk/integ-tests/lib/assertions/sdk.ts @@ -4,10 +4,84 @@ import { EqualsAssertion } from './assertions'; import { ExpectedResult, ActualResult } from './common'; import { AssertionsProvider, SDK_RESOURCE_TYPE_PREFIX } from './providers'; +// keep this import separate from other imports to reduce chance for merge conflicts with v2-main +// eslint-disable-next-line no-duplicate-imports, import/order +import { IConstruct } from '@aws-cdk/core'; + // keep this import separate from other imports to reduce chance for merge conflicts with v2-main // eslint-disable-next-line no-duplicate-imports, import/order import { Construct as CoreConstruct } from '@aws-cdk/core'; +/** + * Interface for creating a custom resource that will perform + * an API call using the AWS SDK + */ +export interface IAwsApiCall extends IConstruct { + /** + * Returns the value of an attribute of the custom resource of an arbitrary + * type. Attributes are returned from the custom resource provider through the + * `Data` map where the key is the attribute name. + * + * @param attributeName the name of the attribute + * @returns a token for `Fn::GetAtt`. Use `Token.asXxx` to encode the returned `Reference` as a specific type or + * use the convenience `getAttString` for string attributes. + */ + getAtt(attributeName: string): Reference; + + /** + * Returns the value of an attribute of the custom resource of type string. + * Attributes are returned from the custom resource provider through the + * `Data` map where the key is the attribute name. + * + * @param attributeName the name of the attribute + * @returns a token for `Fn::GetAtt` encoded as a string. + */ + getAttString(attributeName: string): string; + + /** + * Assert that the ExpectedResult is equal + * to the result of the AwsApiCall + * + * @example + * declare const integ: IntegTest; + * const invoke = integ.assertions.invokeFunction({ + * functionName: 'my-func', + * }); + * invoke.expect(ExpectedResult.objectLike({ Payload: 'OK' })); + */ + expect(expected: ExpectedResult): void; + + /** + * Assert that the ExpectedResult is equal + * to the result of the AwsApiCall at the given path. + * + * For example the SQS.receiveMessage api response would look + * like: + * + * If you wanted to assert the value of `Body` you could do + * + * @example + * const actual = { + * Messages: [{ + * MessageId: '', + * ReceiptHandle: '', + * MD5OfBody: '', + * Body: 'hello', + * Attributes: {}, + * MD5OfMessageAttributes: {}, + * MessageAttributes: {} + * }] + * }; + * + * + * declare const integ: IntegTest; + * const message = integ.assertions.awsApiCall('SQS', 'receiveMessage'); + * + * message.assertAtPath('Messages.0.Body', ExpectedResult.stringLikeRegexp('hello')); + */ + assertAtPath(path: string, expected: ExpectedResult): void; +} + /** * Options to perform an AWS JavaScript V2 API call */ @@ -39,7 +113,7 @@ export interface AwsApiCallProps extends AwsApiCallOptions {} * Construct that creates a custom resource that will perform * a query using the AWS SDK */ -export class AwsApiCall extends CoreConstruct { +export class AwsApiCall extends CoreConstruct implements IAwsApiCall { private readonly sdkCallResource: CustomResource; private flattenResponse: string = 'false'; private readonly name: string; @@ -69,82 +143,23 @@ export class AwsApiCall extends CoreConstruct { this.sdkCallResource.node.addDependency(this.provider); } - /** - * Returns the value of an attribute of the custom resource of an arbitrary - * type. Attributes are returned from the custom resource provider through the - * `Data` map where the key is the attribute name. - * - * @param attributeName the name of the attribute - * @returns a token for `Fn::GetAtt`. Use `Token.asXxx` to encode the returned `Reference` as a specific type or - * use the convenience `getAttString` for string attributes. - */ public getAtt(attributeName: string): Reference { this.flattenResponse = 'true'; return this.sdkCallResource.getAtt(`apiCallResponse.${attributeName}`); } - /** - * Returns the value of an attribute of the custom resource of type string. - * Attributes are returned from the custom resource provider through the - * `Data` map where the key is the attribute name. - * - * @param attributeName the name of the attribute - * @returns a token for `Fn::GetAtt` encoded as a string. - */ public getAttString(attributeName: string): string { this.flattenResponse = 'true'; return this.sdkCallResource.getAttString(`apiCallResponse.${attributeName}`); } - /** - * Assert that the ExpectedResult is equal - * to the result of the AwsApiCall - * - * @example - * declare const assert: DeployAssert; - * const invoke = new LambdaInvokeFunction(assert, 'Invoke', { - * functionName: 'my-func', - * }); - * invoke.assert(ExpectedResult.objectLike({ Payload: 'OK' })); - */ - public assert(expected: ExpectedResult): void { + public expect(expected: ExpectedResult): void { new EqualsAssertion(this, `AssertEquals${this.name}`, { expected, actual: ActualResult.fromCustomResource(this.sdkCallResource, 'apiCallResponse'), }); } - /** - * Assert that the ExpectedResult is equal - * to the result of the AwsApiCall at the given path. - * - * For example the SQS.receiveMessage api response would look - * like: - * - * If you wanted to assert the value of `Body` you could do - * - * @example - * const actual = { - * Messages: [{ - * MessageId: '', - * ReceiptHandle: '', - * MD5OfBody: '', - * Body: 'hello', - * Attributes: {}, - * MD5OfMessageAttributes: {}, - * MessageAttributes: {} - * }] - * }; - * - * - * declare const assert: DeployAssert; - * const message = new AwsApiCall(assert, 'ReceiveMessage', { - * service: 'SQS', - * api: 'receiveMessage' - * }); - * - * message.assertAtPath('Messages.0.Body', ExpectedResult.stringLikeRegexp('hello')); - */ public assertAtPath(path: string, expected: ExpectedResult): void { new EqualsAssertion(this, `AssertEquals${this.name}`, { expected, diff --git a/packages/@aws-cdk/integ-tests/lib/assertions/types.ts b/packages/@aws-cdk/integ-tests/lib/assertions/types.ts new file mode 100644 index 0000000000000..7c5dd185aa058 --- /dev/null +++ b/packages/@aws-cdk/integ-tests/lib/assertions/types.ts @@ -0,0 +1,60 @@ +import { ExpectedResult, ActualResult } from './common'; +import { IAwsApiCall, LambdaInvokeFunctionProps } from './sdk'; + +/** + * Interface that allows for registering a list of assertions + * that should be performed on a construct. This is only necessary + * when writing integration tests. + */ +export interface IDeployAssert { + /** + * Query AWS using JavaScript SDK V2 API calls. This can be used to either + * trigger an action or to return a result that can then be asserted against + * an expected value + * + * @example + * declare const app: App; + * declare const integ: IntegTest; + * integ.assertions.awsApiCall('SQS', 'sendMessage', { + * QueueUrl: 'url', + * MessageBody: 'hello', + * }); + * const message = integ.assertions.awsApiCall('SQS', 'receiveMessage', { + * QueueUrl: 'url', + * }); + * message.expect(ExpectedResult.objectLike({ + * Messages: [{ Body: 'hello' }], + * })); + */ + awsApiCall(service: string, api: string, parameters?: any): IAwsApiCall; + + /** + * Invoke a lambda function and return the response which can be asserted + * + * @example + * declare const app: App; + * declare const integ: IntegTest; + * const invoke = integ.assertions.invokeFunction({ + * functionName: 'my-function', + * }); + * invoke.expect(ExpectedResult.objectLike({ + * Payload: '200', + * })); + */ + invokeFunction(props: LambdaInvokeFunctionProps): IAwsApiCall; + + /** + * Assert that the ExpectedResult is equal + * to the ActualResult + * + * @example + * declare const integ: IntegTest; + * declare const apiCall: AwsApiCall; + * integ.assertions.expect( + * 'invoke', + * ExpectedResult.objectLike({ Payload: 'OK' }), + * ActualResult.fromAwsApiCall(apiCall, 'Body'), + * ); + */ + expect(id: string, expected: ExpectedResult, actual: ActualResult): void; +} diff --git a/packages/@aws-cdk/integ-tests/lib/test-case.ts b/packages/@aws-cdk/integ-tests/lib/test-case.ts index de701bb63d24a..a2b7436481a89 100644 --- a/packages/@aws-cdk/integ-tests/lib/test-case.ts +++ b/packages/@aws-cdk/integ-tests/lib/test-case.ts @@ -1,7 +1,8 @@ import { IntegManifest, Manifest, TestCase, TestOptions } from '@aws-cdk/cloud-assembly-schema'; import { attachCustomSynthesis, Stack, ISynthesisSession, StackProps } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { DeployAssert } from './assertions'; +import { IDeployAssert } from './assertions'; +import { DeployAssert } from './assertions/private/deploy-assert'; import { IntegManifestSynthesizer } from './manifest-synthesizer'; const TEST_CASE_STACK_SYMBOL = Symbol.for('@aws-cdk/integ-tests.IntegTestCaseStack'); @@ -31,12 +32,15 @@ export class IntegTestCase extends CoreConstruct { /** * Make assertions on resources in this test case */ - public readonly assert: DeployAssert; + public readonly assertions: IDeployAssert; + + private readonly _assert: DeployAssert; constructor(scope: Construct, id: string, private readonly props: IntegTestCaseProps) { super(scope, id); - this.assert = new DeployAssert(this); + this._assert = new DeployAssert(this); + this.assertions = this._assert; } /** @@ -53,7 +57,7 @@ export class IntegTestCase extends CoreConstruct { private toTestCase(props: IntegTestCaseProps): TestCase { return { ...props, - assertionStack: Stack.of(this.assert).artifactId, + assertionStack: this._assert.scope.artifactId, stacks: props.stacks.map(s => s.artifactId), }; } @@ -83,7 +87,7 @@ export class IntegTestCaseStack extends Stack { /** * Make assertions on resources in this test case */ - public readonly assert: DeployAssert; + public readonly assertions: IDeployAssert; /** * The underlying IntegTestCase that is created @@ -97,7 +101,7 @@ export class IntegTestCaseStack extends Stack { Object.defineProperty(this, TEST_CASE_STACK_SYMBOL, { value: true }); // TODO: should we only have a single DeployAssert per test? - this.assert = new DeployAssert(this); + this.assertions = new DeployAssert(this); this._testCase = new IntegTestCase(this, `${id}TestCase`, { ...props, stacks: [this], @@ -124,7 +128,7 @@ export class IntegTest extends CoreConstruct { /** * Make assertions on resources in this test case */ - public readonly assert: DeployAssert; + public readonly assertions: IDeployAssert; private readonly testCases: IntegTestCase[]; constructor(scope: Construct, id: string, props: IntegTestProps) { super(scope, id); @@ -138,7 +142,7 @@ export class IntegTest extends CoreConstruct { cdkCommandOptions: props.cdkCommandOptions, stackUpdateWorkflow: props.stackUpdateWorkflow, }); - this.assert = defaultTestCase.assert; + this.assertions = defaultTestCase.assertions; this.testCases = [ defaultTestCase, diff --git a/packages/@aws-cdk/integ-tests/rosetta/default.ts-fixture b/packages/@aws-cdk/integ-tests/rosetta/default.ts-fixture index b9b4f3740b427..e85bf5884afdc 100644 --- a/packages/@aws-cdk/integ-tests/rosetta/default.ts-fixture +++ b/packages/@aws-cdk/integ-tests/rosetta/default.ts-fixture @@ -3,7 +3,6 @@ import { IntegTestCase, IntegTest, IntegTestCaseStack, - DeployAssert, AwsApiCall, EqualsAssertion, ActualResult, diff --git a/packages/@aws-cdk/integ-tests/test/assertions/deploy-assert.test.ts b/packages/@aws-cdk/integ-tests/test/assertions/deploy-assert.test.ts index 847086ed66f7a..5a287200e9fca 100644 --- a/packages/@aws-cdk/integ-tests/test/assertions/deploy-assert.test.ts +++ b/packages/@aws-cdk/integ-tests/test/assertions/deploy-assert.test.ts @@ -1,12 +1,13 @@ import { Template } from '@aws-cdk/assertions'; import { App, Stack } from '@aws-cdk/core'; -import { DeployAssert, LogType, InvocationType, ExpectedResult, ActualResult } from '../../lib/assertions'; +import { LogType, InvocationType, ExpectedResult, ActualResult } from '../../lib/assertions'; +import { DeployAssert } from '../../lib/assertions/private/deploy-assert'; describe('DeployAssert', () => { test('of', () => { const app = new App(); - const stack = new Stack(app); + const stack = new Stack(app, 'TestStack'); new DeployAssert(app); expect(() => { DeployAssert.of(stack); @@ -15,7 +16,7 @@ describe('DeployAssert', () => { test('throws if no DeployAssert', () => { const app = new App(); - const stack = new Stack(app); + const stack = new Stack(app, 'TestStack'); expect(() => { DeployAssert.of(stack); }).toThrow(/No DeployAssert construct found in scopes/); @@ -43,7 +44,7 @@ describe('DeployAssert', () => { }); // THEN - const template = Template.fromStack(Stack.of(deployAssert)); + const template = Template.fromStack(deployAssert.scope); template.hasResourceProperties('Custom::DeployAssert@SdkCallLambdainvoke', { service: 'Lambda', api: 'invoke', @@ -65,14 +66,14 @@ describe('DeployAssert', () => { const query = deplossert.awsApiCall('MyService', 'MyApi'); // WHEN - deplossert.assert( + deplossert.expect( 'MyAssertion', ExpectedResult.stringLikeRegexp('foo'), ActualResult.fromAwsApiCall(query, 'att'), ); // THEN - const template = Template.fromStack(Stack.of(deplossert)); + const template = Template.fromStack(deplossert.scope); template.hasResourceProperties('Custom::DeployAssert@AssertEquals', { expected: JSON.stringify({ $StringLike: 'foo' }), actual: { @@ -91,14 +92,14 @@ describe('DeployAssert', () => { const query = deplossert.awsApiCall('MyService', 'MyApi'); // WHEN - deplossert.assert( + deplossert.expect( 'MyAssertion', ExpectedResult.objectLike({ foo: 'bar' }), ActualResult.fromAwsApiCall(query, 'att'), ); // THEN - const template = Template.fromStack(Stack.of(deplossert)); + const template = Template.fromStack(deplossert.scope); template.hasResourceProperties('Custom::DeployAssert@AssertEquals', { expected: JSON.stringify({ $ObjectLike: { foo: 'bar' } }), actual: { @@ -122,7 +123,7 @@ describe('DeployAssert', () => { // THEN - Template.fromStack(Stack.of(deplossert)).hasResourceProperties('Custom::DeployAssert@SdkCallMyServiceMyApi', { + Template.fromStack(deplossert.scope).hasResourceProperties('Custom::DeployAssert@SdkCallMyServiceMyApi', { api: 'MyApi', service: 'MyService', }); @@ -139,7 +140,7 @@ describe('DeployAssert', () => { // THEN - const template = Template.fromStack(Stack.of(deplossert)); + const template = Template.fromStack(deplossert.scope); template.resourceCountIs('AWS::Lambda::Function', 1); template.resourceCountIs('Custom::DeployAssert@SdkCallMyServiceMyApi1', 1); template.resourceCountIs('Custom::DeployAssert@SdkCallMyServiceMyApi2', 1); diff --git a/packages/@aws-cdk/integ-tests/test/assertions/sdk.test.ts b/packages/@aws-cdk/integ-tests/test/assertions/sdk.test.ts index 31f1bd5068a4b..d8d3d70ec1694 100644 --- a/packages/@aws-cdk/integ-tests/test/assertions/sdk.test.ts +++ b/packages/@aws-cdk/integ-tests/test/assertions/sdk.test.ts @@ -1,6 +1,7 @@ import { Template, Match } from '@aws-cdk/assertions'; -import { App, Stack, CfnOutput } from '@aws-cdk/core'; -import { DeployAssert, AwsApiCall, LambdaInvokeFunction, LogType, InvocationType, ExpectedResult } from '../../lib/assertions'; +import { App, CfnOutput } from '@aws-cdk/core'; +import { LogType, InvocationType, ExpectedResult } from '../../lib/assertions'; +import { DeployAssert } from '../../lib/assertions/private/deploy-assert'; describe('AwsApiCall', () => { test('default', () => { @@ -9,13 +10,10 @@ describe('AwsApiCall', () => { const deplossert = new DeployAssert(app); // WHEN - new AwsApiCall(deplossert, 'AwsApiCall', { - service: 'MyService', - api: 'MyApi', - }); + deplossert.awsApiCall('MyService', 'MyApi'); // THEN - const template = Template.fromStack(Stack.of(deplossert)); + const template = Template.fromStack(deplossert.scope); template.resourceCountIs('AWS::Lambda::Function', 1); template.hasResourceProperties('Custom::DeployAssert@SdkCallMyServiceMyApi', { service: 'MyService', @@ -30,17 +28,13 @@ describe('AwsApiCall', () => { const deplossert = new DeployAssert(app); // WHEN - new AwsApiCall(deplossert, 'AwsApiCall', { - service: 'MyService', - api: 'MyApi', - parameters: { - param1: 'val1', - param2: 2, - }, + deplossert.awsApiCall('MyService', 'MyApi', { + param1: 'val1', + param2: 2, }); // THEN - const template = Template.fromStack(Stack.of(deplossert)); + const template = Template.fromStack(deplossert.scope); template.resourceCountIs('AWS::Lambda::Function', 1); template.hasResourceProperties('Custom::DeployAssert@SdkCallMyServiceMyApi', { service: 'MyService', @@ -59,21 +53,18 @@ describe('AwsApiCall', () => { const deplossert = new DeployAssert(app); // WHEN - const query = new AwsApiCall(deplossert, 'AwsApiCall', { - service: 'MyService', - api: 'MyApi', - }); + const query = deplossert.awsApiCall('MyService', 'MyApi'); - new CfnOutput(deplossert, 'GetAttString', { + new CfnOutput(deplossert.scope, 'GetAttString', { value: query.getAttString('att'), }).overrideLogicalId('GetAtt'); // THEN - const template = Template.fromStack(Stack.of(deplossert)); + const template = Template.fromStack(deplossert.scope); template.hasOutput('GetAtt', { Value: { 'Fn::GetAtt': [ - 'AwsApiCall', + 'AwsApiCallMyServiceMyApi', 'apiCallResponse.att', ], }, @@ -85,27 +76,25 @@ describe('AwsApiCall', () => { flattenResponse: 'true', }); }); + test('getAtt', () => { // GIVEN const app = new App(); const deplossert = new DeployAssert(app); // WHEN - const query = new AwsApiCall(deplossert, 'AwsApiCall', { - service: 'MyService', - api: 'MyApi', - }); + const query = deplossert.awsApiCall('MyService', 'MyApi'); - new CfnOutput(deplossert, 'GetAttString', { + new CfnOutput(deplossert.scope, 'GetAttString', { value: query.getAtt('att').toString(), }).overrideLogicalId('GetAtt'); // THEN - const template = Template.fromStack(Stack.of(deplossert)); + const template = Template.fromStack(deplossert.scope); template.hasOutput('GetAtt', { Value: { 'Fn::GetAtt': [ - 'AwsApiCall', + 'AwsApiCallMyServiceMyApi', 'apiCallResponse.att', ], }, @@ -117,7 +106,6 @@ describe('AwsApiCall', () => { flattenResponse: 'true', }); }); - }); describe('assertEqual', () => { @@ -127,19 +115,16 @@ describe('AwsApiCall', () => { const deplossert = new DeployAssert(app); // WHEN - const query = new AwsApiCall(deplossert, 'AwsApiCall', { - service: 'MyService', - api: 'MyApi', - }); - query.assert(ExpectedResult.exact({ foo: 'bar' })); + const query = deplossert.awsApiCall('MyService', 'MyApi'); + query.expect(ExpectedResult.exact({ foo: 'bar' })); // THEN - const template = Template.fromStack(Stack.of(deplossert)); + const template = Template.fromStack(deplossert.scope); template.hasResourceProperties('Custom::DeployAssert@AssertEquals', { expected: JSON.stringify({ $Exact: { foo: 'bar' } }), actual: { 'Fn::GetAtt': [ - 'AwsApiCall', + 'AwsApiCallMyServiceMyApi', 'apiCallResponse', ], }, @@ -152,19 +137,16 @@ describe('AwsApiCall', () => { const deplossert = new DeployAssert(app); // WHEN - const query = new AwsApiCall(deplossert, 'AwsApiCall', { - service: 'MyService', - api: 'MyApi', - }); - query.assert(ExpectedResult.objectLike({ foo: 'bar' })); + const query = deplossert.awsApiCall('MyService', 'MyApi'); + query.expect(ExpectedResult.objectLike({ foo: 'bar' })); // THEN - const template = Template.fromStack(Stack.of(deplossert)); + const template = Template.fromStack(deplossert.scope); template.hasResourceProperties('Custom::DeployAssert@AssertEquals', { expected: JSON.stringify({ $ObjectLike: { foo: 'bar' } }), actual: { 'Fn::GetAtt': [ - 'AwsApiCall', + 'AwsApiCallMyServiceMyApi', 'apiCallResponse', ], }, @@ -177,19 +159,16 @@ describe('AwsApiCall', () => { const deplossert = new DeployAssert(app); // WHEN - const query = new AwsApiCall(deplossert, 'AwsApiCall', { - service: 'MyService', - api: 'MyApi', - }); - query.assert(ExpectedResult.exact('bar')); + const query = deplossert.awsApiCall('MyService', 'MyApi'); + query.expect(ExpectedResult.exact('bar')); // THEN - const template = Template.fromStack(Stack.of(deplossert)); + const template = Template.fromStack(deplossert.scope); template.hasResourceProperties('Custom::DeployAssert@AssertEquals', { expected: JSON.stringify({ $Exact: 'bar' }), actual: { 'Fn::GetAtt': [ - 'AwsApiCall', + 'AwsApiCallMyServiceMyApi', 'apiCallResponse', ], }, @@ -203,14 +182,14 @@ describe('AwsApiCall', () => { const app = new App(); const deplossert = new DeployAssert(app); - new LambdaInvokeFunction(deplossert, 'Invoke', { + deplossert.invokeFunction({ functionName: 'my-func', logType: LogType.TAIL, payload: JSON.stringify({ key: 'val' }), invocationType: InvocationType.EVENT, }); - const template = Template.fromStack(Stack.of(deplossert)); + const template = Template.fromStack(deplossert.scope); template.hasResourceProperties('Custom::DeployAssert@SdkCallLambdainvoke', { service: 'Lambda', api: 'invoke',