diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index 7300f1da4b9b3..ae628f6c52d3b 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -1,7 +1,7 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import { IVpcEndpoint } from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; -import { ArnFormat, CfnOutput, IResource as IResourceBase, Resource, Stack } from '@aws-cdk/core'; +import { ArnFormat, CfnOutput, IResource as IResourceBase, Resource, Stack, Token } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { ApiDefinition } from './api-definition'; import { ApiKey, ApiKeyOptions, IApiKey } from './api-key'; @@ -368,7 +368,7 @@ export abstract class RestApiBase extends Resource implements IRestApi { } public arnForExecuteApi(method: string = '*', path: string = '/*', stage: string = '*') { - if (!path.startsWith('/')) { + if (!Token.isUnresolved(path) && !path.startsWith('/')) { throw new Error(`"path" must begin with a "/": '${path}'`); } diff --git a/packages/@aws-cdk/aws-apigateway/test/restapi.test.ts b/packages/@aws-cdk/aws-apigateway/test/restapi.test.ts index f873c6c643f5d..0b019719a4873 100644 --- a/packages/@aws-cdk/aws-apigateway/test/restapi.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/restapi.test.ts @@ -1,7 +1,7 @@ import { Template } from '@aws-cdk/assertions'; import { GatewayVpcEndpoint } from '@aws-cdk/aws-ec2'; import { testDeprecated } from '@aws-cdk/cdk-build-tools'; -import { App, CfnElement, CfnResource, Stack } from '@aws-cdk/core'; +import { App, CfnElement, CfnResource, Lazy, Stack } from '@aws-cdk/core'; import * as apigw from '../lib'; describe('restapi', () => { @@ -424,6 +424,16 @@ describe('restapi', () => { expect(() => api.arnForExecuteApi('method', 'hey-path', 'stage')).toThrow(/"path" must begin with a "\/": 'hey-path'/); }); + test('"executeApiArn" path can be a token', () => { + // GIVEN + const stack = new Stack(); + const api = new apigw.RestApi(stack, 'api'); + api.root.addMethod('GET'); + + // THEN + expect(() => api.arnForExecuteApi('method', Lazy.string(({ produce: () => 'path' })), 'stage')).not.toThrow(); + }); + test('"executeApiArn" will convert ANY to "*"', () => { // GIVEN const stack = new Stack(); diff --git a/packages/@aws-cdk/cfnspec/spec-source/cfn-docs/cfn-docs.json b/packages/@aws-cdk/cfnspec/spec-source/cfn-docs/cfn-docs.json index a03c3d9892d9d..fc0ea483029bc 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/cfn-docs/cfn-docs.json +++ b/packages/@aws-cdk/cfnspec/spec-source/cfn-docs/cfn-docs.json @@ -25249,7 +25249,7 @@ "KafkaClusterEncryptionInTransit": "Details of encryption in transit to the Apache Kafka cluster.", "KafkaConnectVersion": "The version of Kafka Connect. It has to be compatible with both the Apache Kafka cluster's version and the plugins.", "LogDelivery": "The settings for delivering connector logs to Amazon CloudWatch Logs.", - "Plugins": "Specifies which plugins were used for this connector.", + "Plugins": "Specifies which plugin to use for the connector. You must specify a single-element list. Amazon MSK Connect does not currently support specifying multiple plugins.", "ServiceExecutionRoleArn": "The Amazon Resource Name (ARN) of the IAM role used by the connector to access Amazon Web Services resources.", "WorkerConfiguration": "The worker configurations that are in use with the connector." } diff --git a/packages/@aws-cdk/integ-tests/README.md b/packages/@aws-cdk/integ-tests/README.md index 0e8fc9b1ca501..f428f7d683a7d 100644 --- a/packages/@aws-cdk/integ-tests/README.md +++ b/packages/@aws-cdk/integ-tests/README.md @@ -177,7 +177,12 @@ 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; @@ -187,31 +192,35 @@ const integ = new IntegTest(app, 'Integ', { testCases: [stack] }); integ.assert.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.assert.assert( '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.assert.awsApiCall('SQS', 'receiveMessage', { QueueUrl: 'url', }); ``` @@ -281,21 +293,18 @@ const message = integ.assert.awsApiCall('SQS', 'receiveMessage', { 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,7 +314,6 @@ can be used to construct the `ExpectedResult`. ```ts declare const message: AwsApiCall; -declare const assert: DeployAssert; message.assert(ExpectedResult.objectLike({ Messages: Match.arrayWith([ 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..361e340bc4a9c --- /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 assert(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..da54b1dbe24db 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.assert.invokeFunction({ + * functionName: 'my-func', + * }); + * invoke.assert(ExpectedResult.objectLike({ Payload: 'OK' })); + */ + assert(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.assert.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,44 +143,16 @@ 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 { new EqualsAssertion(this, `AssertEquals${this.name}`, { expected, @@ -114,37 +160,6 @@ export class AwsApiCall extends CoreConstruct { }); } - /** - * 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..65025fb21d2e4 --- /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.assert.awsApiCall('SQS', 'sendMessage', { + * QueueUrl: 'url', + * MessageBody: 'hello', + * }); + * const message = integ.assert.awsApiCall('SQS', 'receiveMessage', { + * QueueUrl: 'url', + * }); + * message.assert(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.assert.invokeFunction({ + * functionName: 'my-function', + * }); + * invoke.assert(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.assert.assert( + * 'invoke', + * ExpectedResult.objectLike({ Payload: 'OK' }), + * ActualResult.fromAwsApiCall(apiCall, 'Body'), + * ); + */ + assert(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..8c9d66118ac76 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 assert: 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.assert = 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 assert: IDeployAssert; /** * The underlying IntegTestCase that is created @@ -124,7 +128,7 @@ export class IntegTest extends CoreConstruct { /** * Make assertions on resources in this test case */ - public readonly assert: DeployAssert; + public readonly assert: IDeployAssert; private readonly testCases: IntegTestCase[]; constructor(scope: Construct, id: string, props: IntegTestProps) { super(scope, id); 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..c779618d2c1aa 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', @@ -72,7 +73,7 @@ describe('DeployAssert', () => { ); // 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: { @@ -98,7 +99,7 @@ describe('DeployAssert', () => { ); // 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..d2f486a6687d9 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', - }); + const query = deplossert.awsApiCall('MyService', 'MyApi'); query.assert(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', - }); + const query = deplossert.awsApiCall('MyService', 'MyApi'); query.assert(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', - }); + const query = deplossert.awsApiCall('MyService', 'MyApi'); query.assert(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',