From 8362efe8f1951289236034161d7560f20975b0ec Mon Sep 17 00:00:00 2001 From: Cory Hall <43035978+corymhall@users.noreply.github.com> Date: Mon, 25 Apr 2022 14:33:42 -0400 Subject: [PATCH] feat(integ-tests): make assertions on deployed infrastructure (#20071) This PR introduces a new group of constructs that allow you to make assertions against deployed infrastructure. They are not exported yet so we can work through the todo list in follow up PRs. TODO: - [ ] Add more assertion types (i.e. objectContaining) - [ ] Update integ-runner to collect the assertion results - [ ] Assertion custom resources should not(?) be part of the snapshot diff - [ ] Assertions need to be run on every deploy (i.e. update workflow) but that should not be part of the snapshot diff ---- ### All Submissions: * [ ] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md) ### Adding new Unconventional Dependencies: * [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md/#adding-new-unconventional-dependencies) ### New Features * [ ] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/master/INTEGRATION_TESTS.md)? * [ ] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)? *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../integ-tests/lib/assertions/assertions.ts | 53 + .../lib/assertions/deploy-assert.ts | 95 ++ .../integ-tests/lib/assertions/index.ts | 4 + .../lib/assertions/private/hash.ts | 10 + .../lib/assertions/providers/index.ts | 2 + .../providers/lambda-handler/assertion.ts | 34 + .../providers/lambda-handler/base.ts | 100 ++ .../providers/lambda-handler/index.ts | 21 + .../providers/lambda-handler/results.ts | 12 + .../providers/lambda-handler/sdk.ts | 59 + .../providers/lambda-handler/types.ts | 138 ++ .../lib/assertions/providers/provider.ts | 68 + .../providers/sdk-api-metadata.json | 1156 +++++++++++++++++ .../integ-tests/lib/assertions/sdk.ts | 106 ++ packages/@aws-cdk/integ-tests/lib/index.ts | 2 +- packages/@aws-cdk/integ-tests/package.json | 26 +- .../test/assertions/assertions.test.ts | 48 + .../test/assertions/deploy-assert.test.ts | 99 ++ .../test/assertions/private/hash.test.ts | 17 + .../lambda-handler/assertion.test.ts | 80 ++ .../providers/lambda-handler/base.test.ts | 199 +++ .../providers/lambda-handler/results.test.ts | 59 + .../providers/lambda-handler/sdk.test.ts | 107 ++ .../assertions/providers/provider.test.ts | 122 ++ .../integ-tests/test/assertions/sdk.test.ts | 108 ++ 25 files changed, 2718 insertions(+), 7 deletions(-) create mode 100644 packages/@aws-cdk/integ-tests/lib/assertions/assertions.ts create mode 100644 packages/@aws-cdk/integ-tests/lib/assertions/deploy-assert.ts create mode 100644 packages/@aws-cdk/integ-tests/lib/assertions/index.ts create mode 100644 packages/@aws-cdk/integ-tests/lib/assertions/private/hash.ts create mode 100644 packages/@aws-cdk/integ-tests/lib/assertions/providers/index.ts create mode 100644 packages/@aws-cdk/integ-tests/lib/assertions/providers/lambda-handler/assertion.ts create mode 100644 packages/@aws-cdk/integ-tests/lib/assertions/providers/lambda-handler/base.ts create mode 100644 packages/@aws-cdk/integ-tests/lib/assertions/providers/lambda-handler/index.ts create mode 100644 packages/@aws-cdk/integ-tests/lib/assertions/providers/lambda-handler/results.ts create mode 100644 packages/@aws-cdk/integ-tests/lib/assertions/providers/lambda-handler/sdk.ts create mode 100644 packages/@aws-cdk/integ-tests/lib/assertions/providers/lambda-handler/types.ts create mode 100644 packages/@aws-cdk/integ-tests/lib/assertions/providers/provider.ts create mode 100644 packages/@aws-cdk/integ-tests/lib/assertions/providers/sdk-api-metadata.json create mode 100644 packages/@aws-cdk/integ-tests/lib/assertions/sdk.ts create mode 100644 packages/@aws-cdk/integ-tests/test/assertions/assertions.test.ts create mode 100644 packages/@aws-cdk/integ-tests/test/assertions/deploy-assert.test.ts create mode 100644 packages/@aws-cdk/integ-tests/test/assertions/private/hash.test.ts create mode 100644 packages/@aws-cdk/integ-tests/test/assertions/providers/lambda-handler/assertion.test.ts create mode 100644 packages/@aws-cdk/integ-tests/test/assertions/providers/lambda-handler/base.test.ts create mode 100644 packages/@aws-cdk/integ-tests/test/assertions/providers/lambda-handler/results.test.ts create mode 100644 packages/@aws-cdk/integ-tests/test/assertions/providers/lambda-handler/sdk.test.ts create mode 100644 packages/@aws-cdk/integ-tests/test/assertions/providers/provider.test.ts create mode 100644 packages/@aws-cdk/integ-tests/test/assertions/sdk.test.ts diff --git a/packages/@aws-cdk/integ-tests/lib/assertions/assertions.ts b/packages/@aws-cdk/integ-tests/lib/assertions/assertions.ts new file mode 100644 index 0000000000000..1b3ba0f14f7bf --- /dev/null +++ b/packages/@aws-cdk/integ-tests/lib/assertions/assertions.ts @@ -0,0 +1,53 @@ +import { CustomResource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { IAssertion } from './deploy-assert'; +import { AssertionRequest, AssertionsProvider, ASSERT_RESOURCE_TYPE, AssertionType } 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 { Construct as CoreConstruct } from '@aws-cdk/core'; + +/** + * Options for an EqualsAssertion + */ +export interface EqualsAssertionProps { + /** + * The CustomResource that continains the "actual" results + */ + readonly inputResource: CustomResource; + + /** + * The CustomResource attribute that continains the "actual" results + */ + readonly inputResourceAtt: string; + + /** + * The expected result to assert + */ + readonly expected: any; +} + +/** + * Construct that creates a CustomResource to assert that two + * values are equal + */ +export class EqualsAssertion extends CoreConstruct implements IAssertion { + public readonly result: string; + + constructor(scope: Construct, id: string, props: EqualsAssertionProps) { + super(scope, id); + + const assertionProvider = new AssertionsProvider(this, 'AssertionProvider'); + const properties: AssertionRequest = { + actual: props.inputResource.getAttString(props.inputResourceAtt), + expected: props.expected, + assertionType: AssertionType.EQUALS, + }; + const resource = new CustomResource(this, 'Default', { + serviceToken: assertionProvider.serviceToken, + properties, + resourceType: ASSERT_RESOURCE_TYPE, + }); + this.result = resource.getAttString('data'); + } +} diff --git a/packages/@aws-cdk/integ-tests/lib/assertions/deploy-assert.ts b/packages/@aws-cdk/integ-tests/lib/assertions/deploy-assert.ts new file mode 100644 index 0000000000000..8ef74b5ce56a5 --- /dev/null +++ b/packages/@aws-cdk/integ-tests/lib/assertions/deploy-assert.ts @@ -0,0 +1,95 @@ +import { CfnOutput, CustomResource, Lazy } from '@aws-cdk/core'; +import { Construct, IConstruct, Node } from 'constructs'; +import { md5hash } from './private/hash'; +import { RESULTS_RESOURCE_TYPE, AssertionsProvider } from './providers'; +import { SdkQuery, SdkQueryOptions } 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'; + +/** + * Represents a deploy time assertion + */ +export interface IAssertion { + /** + * The result of the assertion + */ + readonly result: string; +} + +/** + * 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(construct).scopes.reverse(); + const deployAssert = scopes.find(s => DeployAssert.isDeployAssert(s)); + if (!deployAssert) { + throw new Error('No DeployAssert construct found in scopes'); + } + return deployAssert as DeployAssert; + } + + /** @internal */ + public readonly _assertions: IAssertion[]; + + constructor(scope: Construct) { + super(scope, 'DeployAssert'); + + Object.defineProperty(this, DEPLOY_ASSERT_SYMBOL, { value: true }); + this._assertions = []; + + const provider = new AssertionsProvider(this, 'ResultsProvider'); + + const resource = new CustomResource(this, 'ResultsCollection', { + serviceToken: provider.serviceToken, + properties: { + assertionResults: Lazy.list({ + produce: () => this._assertions.map(a => a.result), + }), + }, + resourceType: RESULTS_RESOURCE_TYPE, + }); + + // TODO: need to show/store this information + new CfnOutput(this, 'Results', { + value: `\n${resource.getAttString('message')}`, + }).overrideLogicalId('Results'); + } + + /** + * Query AWS using JavaScript SDK V2 API calls + */ + public queryAws(options: SdkQueryOptions): SdkQuery { + const id = md5hash(options); + return new SdkQuery(this, `SdkQuery${id}`, options); + } + + /** + * Register an assertion that should be run as part of the + * deployment + */ + public registerAssertion(assertion: IAssertion) { + this._assertions.push(assertion); + } +} diff --git a/packages/@aws-cdk/integ-tests/lib/assertions/index.ts b/packages/@aws-cdk/integ-tests/lib/assertions/index.ts new file mode 100644 index 0000000000000..f1f833d9f78a4 --- /dev/null +++ b/packages/@aws-cdk/integ-tests/lib/assertions/index.ts @@ -0,0 +1,4 @@ +export * from './assertions'; +export * from './sdk'; +export * from './deploy-assert'; +export * from './providers'; diff --git a/packages/@aws-cdk/integ-tests/lib/assertions/private/hash.ts b/packages/@aws-cdk/integ-tests/lib/assertions/private/hash.ts new file mode 100644 index 0000000000000..38649bbea4473 --- /dev/null +++ b/packages/@aws-cdk/integ-tests/lib/assertions/private/hash.ts @@ -0,0 +1,10 @@ +import * as crypto from 'crypto'; + +export function md5hash(obj: any): string { + if (!obj || (typeof(obj) === 'object' && Object.keys(obj).length === 0)) { + throw new Error('Cannot compute md5 hash for falsy object'); + } + const hash = crypto.createHash('md5'); + hash.update(JSON.stringify(obj)); + return hash.digest('hex'); +} diff --git a/packages/@aws-cdk/integ-tests/lib/assertions/providers/index.ts b/packages/@aws-cdk/integ-tests/lib/assertions/providers/index.ts new file mode 100644 index 0000000000000..1c21f87ecd74a --- /dev/null +++ b/packages/@aws-cdk/integ-tests/lib/assertions/providers/index.ts @@ -0,0 +1,2 @@ +export * from './lambda-handler/types'; +export * from './provider'; diff --git a/packages/@aws-cdk/integ-tests/lib/assertions/providers/lambda-handler/assertion.ts b/packages/@aws-cdk/integ-tests/lib/assertions/providers/lambda-handler/assertion.ts new file mode 100644 index 0000000000000..8efd972d5f98e --- /dev/null +++ b/packages/@aws-cdk/integ-tests/lib/assertions/providers/lambda-handler/assertion.ts @@ -0,0 +1,34 @@ +/* eslint-disable no-console */ +import * as assert from 'assert'; +import { CustomResourceHandler } from './base'; +import { AssertionRequest, AssertionResult } from './types'; + +export class AssertionHandler extends CustomResourceHandler { + protected async processEvent(request: AssertionRequest): Promise { + let result: AssertionResult; + switch (request.assertionType) { + case 'equals': + console.log(`Testing equality between ${JSON.stringify(request.actual)} and ${JSON.stringify(request.expected)}`); + try { + assert.deepStrictEqual(request.actual, request.expected); + result = { data: { status: 'pass' } }; + } catch (e) { + if (e instanceof assert.AssertionError) { + result = { + data: { + status: 'fail', + message: e.message, + }, + }; + } else { + throw e; + } + } + break; + default: + throw new Error(`Unsupported query type ${request.assertionType}`); + } + + return result; + } +} diff --git a/packages/@aws-cdk/integ-tests/lib/assertions/providers/lambda-handler/base.ts b/packages/@aws-cdk/integ-tests/lib/assertions/providers/lambda-handler/base.ts new file mode 100644 index 0000000000000..829574f80d8eb --- /dev/null +++ b/packages/@aws-cdk/integ-tests/lib/assertions/providers/lambda-handler/base.ts @@ -0,0 +1,100 @@ +/* eslint-disable no-console */ +import * as https from 'https'; +import * as url from 'url'; + +interface HandlerResponse { + readonly status: 'SUCCESS' | 'FAILED'; + readonly reason: 'OK' | string; + readonly data?: any; +} + +// eslint-disable-next-line @typescript-eslint/ban-types +export abstract class CustomResourceHandler { + public readonly physicalResourceId: string; + private readonly timeout: NodeJS.Timeout; + private timedOut = false; + + constructor(protected readonly event: AWSLambda.CloudFormationCustomResourceEvent, protected readonly context: AWSLambda.Context) { + this.timeout = setTimeout(async () => { + await this.respond({ + status: 'FAILED', + reason: 'Lambda Function Timeout', + data: this.context.logStreamName, + }); + this.timedOut = true; + }, context.getRemainingTimeInMillis() - 1200); + this.event = event; + this.physicalResourceId = extractPhysicalResourceId(event); + } + + public async handle(): Promise { + try { + console.log(`Event: ${JSON.stringify(this.event)}`); + const response = await this.processEvent(this.event.ResourceProperties as unknown as Request); + console.log(`Event output : ${JSON.stringify(response)}`); + await this.respond({ + status: 'SUCCESS', + reason: 'OK', + data: response, + }); + } catch (e) { + console.log(e); + await this.respond({ + status: 'FAILED', + reason: e.message ?? 'Internal Error', + }); + } finally { + clearTimeout(this.timeout); + } + } + + protected abstract processEvent(request: Request): Promise; + + private respond(response: HandlerResponse) { + if (this.timedOut) { + return; + } + const cfResponse: AWSLambda.CloudFormationCustomResourceResponse = { + Status: response.status, + Reason: response.reason, + PhysicalResourceId: this.physicalResourceId, + StackId: this.event.StackId, + RequestId: this.event.RequestId, + LogicalResourceId: this.event.LogicalResourceId, + NoEcho: false, + Data: response.data, + }; + const responseBody = JSON.stringify(cfResponse); + + console.log('Responding to CloudFormation', responseBody); + + const parsedUrl = url.parse(this.event.ResponseURL); + const requestOptions = { + hostname: parsedUrl.hostname, + path: parsedUrl.path, + method: 'PUT', + headers: { 'content-type': '', 'content-length': responseBody.length }, + }; + + return new Promise((resolve, reject) => { + try { + const request = https.request(requestOptions, resolve); + request.on('error', reject); + request.write(responseBody); + request.end(); + } catch (e) { + reject(e); + } + }); + } +} + +function extractPhysicalResourceId(event: AWSLambda.CloudFormationCustomResourceEvent): string { + switch (event.RequestType) { + case 'Create': + return event.LogicalResourceId; + case 'Update': + case 'Delete': + return event.PhysicalResourceId; + } +} 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 new file mode 100644 index 0000000000000..07a1911efe4dd --- /dev/null +++ b/packages/@aws-cdk/integ-tests/lib/assertions/providers/lambda-handler/index.ts @@ -0,0 +1,21 @@ +import { AssertionHandler } from './assertion'; +import { ResultsCollectionHandler } from './results'; +import { SdkHandler } from './sdk'; +import * as types from './types'; + +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent, context: AWSLambda.Context) { + const provider = createResourceHandler(event, context); + await provider.handle(); +} + +function createResourceHandler(event: AWSLambda.CloudFormationCustomResourceEvent, context: AWSLambda.Context) { + if (event.ResourceType.startsWith(types.SDK_RESOURCE_TYPE_PREFIX)) { + return new SdkHandler(event, context); + } + 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 new file mode 100644 index 0000000000000..784ff68a05ab6 --- /dev/null +++ b/packages/@aws-cdk/integ-tests/lib/assertions/providers/lambda-handler/results.ts @@ -0,0 +1,12 @@ +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/sdk.ts b/packages/@aws-cdk/integ-tests/lib/assertions/providers/lambda-handler/sdk.ts new file mode 100644 index 0000000000000..fed1174d3fb27 --- /dev/null +++ b/packages/@aws-cdk/integ-tests/lib/assertions/providers/lambda-handler/sdk.ts @@ -0,0 +1,59 @@ +/* eslint-disable no-console */ +import { CustomResourceHandler } from './base'; +import { SdkRequest, SdkResult } from './types'; + +/** + * Flattens a nested object + * + * @param object the object to be flattened + * @returns a flat object with path as keys + */ +export function flatten(object: object): { [key: string]: any } { + return Object.assign( + {}, + ...function _flatten(child: any, path: string[] = []): any { + return [].concat(...Object.keys(child) + .map(key => { + const childKey = Buffer.isBuffer(child[key]) ? child[key].toString('utf8') : child[key]; + return typeof childKey === 'object' && childKey !== null + ? _flatten(childKey, path.concat([key])) + : ({ [path.concat([key]).join('.')]: childKey }); + })); + }(object), + ); +} + + +export class SdkHandler extends CustomResourceHandler { + protected async processEvent(request: SdkRequest): Promise { + // eslint-disable-next-line + const AWS: any = require('aws-sdk'); + console.log(`AWS SDK VERSION: ${AWS.VERSION}`); + + const service = new AWS[request.service](); + const response = await service[request.api](request.parameters && decode(request.parameters)).promise(); + console.log(`SDK response received ${JSON.stringify(response)}`); + delete response.ResponseMetadata; + const respond = { + apiCallResponse: response, + }; + const flatData: { [key: string]: string } = { + ...flatten(respond), + }; + + return request.flattenResponse === 'true' ? flatData : respond; + } +} + +function decode(object: Record) { + return JSON.parse(JSON.stringify(object), (_k, v) => { + switch (v) { + case 'TRUE:BOOLEAN': + return true; + case 'FALSE:BOOLEAN': + return false; + default: + return v; + } + }); +} 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 new file mode 100644 index 0000000000000..f0ff05507ae61 --- /dev/null +++ b/packages/@aws-cdk/integ-tests/lib/assertions/providers/lambda-handler/types.ts @@ -0,0 +1,138 @@ +// This file contains the input and output types for the providers. +// 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'; + +/** + * A AWS JavaScript SDK V2 request + */ +export interface SdkRequest { + /** + * The AWS service i.e. S3 + */ + readonly service: string; + + /** + * The AWS api call to make i.e. getBucketLifecycle + */ + readonly api: string; + + /** + * Any parameters to pass to the api call + * + * @default - no parameters + */ + readonly parameters?: any; + + /** + * Whether or not to flatten the response from the api call + * + * Valid values are 'true' or 'false' as strings + * + * Typically when using an SdkRequest you will be passing it as the + * `actual` value to an assertion provider so this would be set + * to 'false' (you want the actual response). + * + * If you are using the SdkRequest to perform more of a query to return + * a single value to use, then this should be set to 'true'. For example, + * you could make a StepFunctions.startExecution api call and retreive the + * `executionArn` from the response. + * + * @default 'false' + */ + readonly flattenResponse?: string; +} + +/** + * The result from a SdkQuery + */ +export interface SdkResult { + /** + * The full api response + */ + readonly apiCallResponse: any; +} + +/** + * The type of assertion to perform + */ +export enum AssertionType { + /** + * Assert that two values are equal + */ + EQUALS = 'equals', +} + +/** + * A request to make an assertion that the + * actual value matches the expected + */ +export interface AssertionRequest { + /** + * The type of assertion to perform + */ + readonly assertionType: AssertionType; + + /** + * The expected value to assert + */ + readonly expected: any; + + /** + * The actual value received + */ + readonly actual: any; +} +/** + * The result of an Assertion + * wrapping the actual result data in another struct. + * Needed to access the whole message via getAtt() on the custom resource. + */ +export interface AssertionResult { +/** + * The result of an assertion + */ + readonly data: AssertionResultData; +} + +/** + * The result of an assertion + */ +export interface AssertionResultData { + /** + * The status of the assertion, i.e. + * pass or fail + */ + readonly status: 'pass' | 'fail' + + /** + * Any message returned with the assertion result + * typically this will be the diff if there is any + * + * @default - none + */ + 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/providers/provider.ts b/packages/@aws-cdk/integ-tests/lib/assertions/providers/provider.ts new file mode 100644 index 0000000000000..155996452713c --- /dev/null +++ b/packages/@aws-cdk/integ-tests/lib/assertions/providers/provider.ts @@ -0,0 +1,68 @@ +import * as path from 'path'; +import * as iam from '@aws-cdk/aws-iam'; +import * as lambda from '@aws-cdk/aws-lambda'; +import { Duration } from '@aws-cdk/core'; +import { Construct } from 'constructs'; + +// 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'; +let SDK_METADATA: any = undefined; + +/** + * Represents an assertions provider. The creates a singletone + * Lambda Function that will create a single function per stack + * that serves as the custom resource provider for the various + * assertion providers + */ +export class AssertionsProvider extends CoreConstruct { + public readonly serviceToken: string; + private readonly grantPrincipal: iam.IPrincipal; + + constructor(scope: Construct, id: string) { + super(scope, id); + + const handler = new lambda.SingletonFunction(this, 'AssertionsProvider', { + code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-handler')), + runtime: lambda.Runtime.NODEJS_14_X, + handler: 'index.handler', + uuid: '1488541a-7b23-4664-81b6-9b4408076b81', + timeout: Duration.minutes(2), + }); + + this.grantPrincipal = handler.grantPrincipal; + this.serviceToken = handler.functionArn; + } + + public encode(obj: any): any { + if (!obj) { + return obj; + } + return JSON.parse(JSON.stringify(obj), (_k, v) => { + switch (v) { + case true: + return 'TRUE:BOOLEAN'; + case false: + return 'FALSE:BOOLEAN'; + default: + return v; + } + }); + } + + public addPolicyStatementFromSdkCall(service: string, api: string, resources?: string[]): iam.PolicyStatement { + if (SDK_METADATA === undefined) { + // eslint-disable-next-line + SDK_METADATA = require('./sdk-api-metadata.json'); + } + const srv = service.toLowerCase(); + const iamService = (SDK_METADATA[srv] && SDK_METADATA[srv].prefix) || srv; + const iamAction = api.charAt(0).toUpperCase() + api.slice(1); + const statement = new iam.PolicyStatement({ + actions: [`${iamService}:${iamAction}`], + resources: resources || ['*'], + }); + this.grantPrincipal.addToPolicy(statement); + return statement; + } +} diff --git a/packages/@aws-cdk/integ-tests/lib/assertions/providers/sdk-api-metadata.json b/packages/@aws-cdk/integ-tests/lib/assertions/providers/sdk-api-metadata.json new file mode 100644 index 0000000000000..dbd7fbff66522 --- /dev/null +++ b/packages/@aws-cdk/integ-tests/lib/assertions/providers/sdk-api-metadata.json @@ -0,0 +1,1156 @@ +{ + "acm": { + "name": "ACM", + "cors": true + }, + "apigateway": { + "name": "APIGateway", + "cors": true + }, + "applicationautoscaling": { + "prefix": "application-autoscaling", + "name": "ApplicationAutoScaling", + "cors": true + }, + "appstream": { + "name": "AppStream" + }, + "autoscaling": { + "name": "AutoScaling", + "cors": true + }, + "batch": { + "name": "Batch" + }, + "budgets": { + "name": "Budgets" + }, + "clouddirectory": { + "name": "CloudDirectory", + "versions": [ + "2016-05-10*" + ] + }, + "cloudformation": { + "name": "CloudFormation", + "cors": true + }, + "cloudfront": { + "name": "CloudFront", + "versions": [ + "2013-05-12*", + "2013-11-11*", + "2014-05-31*", + "2014-10-21*", + "2014-11-06*", + "2015-04-17*", + "2015-07-27*", + "2015-09-17*", + "2016-01-13*", + "2016-01-28*", + "2016-08-01*", + "2016-08-20*", + "2016-09-07*", + "2016-09-29*", + "2016-11-25*", + "2017-03-25*", + "2017-10-30*", + "2018-06-18*", + "2018-11-05*", + "2019-03-26*" + ], + "cors": true + }, + "cloudhsm": { + "name": "CloudHSM", + "cors": true + }, + "cloudsearch": { + "name": "CloudSearch" + }, + "cloudsearchdomain": { + "name": "CloudSearchDomain" + }, + "cloudtrail": { + "name": "CloudTrail", + "cors": true + }, + "cloudwatch": { + "prefix": "monitoring", + "name": "CloudWatch", + "cors": true + }, + "cloudwatchevents": { + "prefix": "events", + "name": "CloudWatchEvents", + "versions": [ + "2014-02-03*" + ], + "cors": true + }, + "cloudwatchlogs": { + "prefix": "logs", + "name": "CloudWatchLogs", + "cors": true + }, + "codebuild": { + "name": "CodeBuild", + "cors": true + }, + "codecommit": { + "name": "CodeCommit", + "cors": true + }, + "codedeploy": { + "name": "CodeDeploy", + "cors": true + }, + "codepipeline": { + "name": "CodePipeline", + "cors": true + }, + "cognitoidentity": { + "prefix": "cognito-identity", + "name": "CognitoIdentity", + "cors": true + }, + "cognitoidentityserviceprovider": { + "prefix": "cognito-idp", + "name": "CognitoIdentityServiceProvider", + "cors": true + }, + "cognitosync": { + "prefix": "cognito-sync", + "name": "CognitoSync", + "cors": true + }, + "configservice": { + "prefix": "config", + "name": "ConfigService", + "cors": true + }, + "cur": { + "name": "CUR", + "cors": true + }, + "datapipeline": { + "name": "DataPipeline" + }, + "devicefarm": { + "name": "DeviceFarm", + "cors": true + }, + "directconnect": { + "name": "DirectConnect", + "cors": true + }, + "directoryservice": { + "prefix": "ds", + "name": "DirectoryService" + }, + "discovery": { + "name": "Discovery" + }, + "dms": { + "name": "DMS" + }, + "dynamodb": { + "name": "DynamoDB", + "cors": true + }, + "dynamodbstreams": { + "prefix": "streams.dynamodb", + "name": "DynamoDBStreams", + "cors": true + }, + "ec2": { + "name": "EC2", + "versions": [ + "2013-06-15*", + "2013-10-15*", + "2014-02-01*", + "2014-05-01*", + "2014-06-15*", + "2014-09-01*", + "2014-10-01*", + "2015-03-01*", + "2015-04-15*", + "2015-10-01*", + "2016-04-01*", + "2016-09-15*" + ], + "cors": true + }, + "ecr": { + "name": "ECR", + "cors": true + }, + "ecs": { + "name": "ECS", + "cors": true + }, + "efs": { + "prefix": "elasticfilesystem", + "name": "EFS", + "cors": true + }, + "elasticache": { + "name": "ElastiCache", + "versions": [ + "2012-11-15*", + "2014-03-24*", + "2014-07-15*", + "2014-09-30*" + ], + "cors": true + }, + "elasticbeanstalk": { + "name": "ElasticBeanstalk", + "cors": true + }, + "elb": { + "prefix": "elasticloadbalancing", + "name": "ELB", + "cors": true + }, + "elbv2": { + "prefix": "elasticloadbalancingv2", + "name": "ELBv2", + "cors": true + }, + "emr": { + "prefix": "elasticmapreduce", + "name": "EMR", + "cors": true + }, + "es": { + "name": "ES" + }, + "elastictranscoder": { + "name": "ElasticTranscoder", + "cors": true + }, + "firehose": { + "name": "Firehose", + "cors": true + }, + "gamelift": { + "name": "GameLift", + "cors": true + }, + "glacier": { + "name": "Glacier" + }, + "health": { + "name": "Health" + }, + "iam": { + "name": "IAM", + "cors": true + }, + "importexport": { + "name": "ImportExport" + }, + "inspector": { + "name": "Inspector", + "versions": [ + "2015-08-18*" + ], + "cors": true + }, + "iot": { + "name": "Iot", + "cors": true + }, + "iotdata": { + "prefix": "iot-data", + "name": "IotData", + "cors": true + }, + "kinesis": { + "name": "Kinesis", + "cors": true + }, + "kinesisanalytics": { + "name": "KinesisAnalytics" + }, + "kms": { + "name": "KMS", + "cors": true + }, + "lambda": { + "name": "Lambda", + "cors": true + }, + "lexruntime": { + "prefix": "runtime.lex", + "name": "LexRuntime", + "cors": true + }, + "lightsail": { + "name": "Lightsail" + }, + "machinelearning": { + "name": "MachineLearning", + "cors": true + }, + "marketplacecommerceanalytics": { + "name": "MarketplaceCommerceAnalytics", + "cors": true + }, + "marketplacemetering": { + "prefix": "meteringmarketplace", + "name": "MarketplaceMetering" + }, + "mturk": { + "prefix": "mturk-requester", + "name": "MTurk", + "cors": true + }, + "mobileanalytics": { + "name": "MobileAnalytics", + "cors": true + }, + "opsworks": { + "name": "OpsWorks", + "cors": true + }, + "opsworkscm": { + "name": "OpsWorksCM" + }, + "organizations": { + "name": "Organizations" + }, + "pinpoint": { + "name": "Pinpoint" + }, + "polly": { + "name": "Polly", + "cors": true + }, + "rds": { + "name": "RDS", + "versions": [ + "2014-09-01*" + ], + "cors": true + }, + "redshift": { + "name": "Redshift", + "cors": true + }, + "rekognition": { + "name": "Rekognition", + "cors": true + }, + "resourcegroupstaggingapi": { + "name": "ResourceGroupsTaggingAPI" + }, + "route53": { + "name": "Route53", + "cors": true + }, + "route53domains": { + "name": "Route53Domains", + "cors": true + }, + "s3": { + "name": "S3", + "dualstackAvailable": true, + "cors": true + }, + "s3control": { + "name": "S3Control", + "dualstackAvailable": true, + "xmlNoDefaultLists": true + }, + "servicecatalog": { + "name": "ServiceCatalog", + "cors": true + }, + "ses": { + "prefix": "email", + "name": "SES", + "cors": true + }, + "shield": { + "name": "Shield" + }, + "simpledb": { + "prefix": "sdb", + "name": "SimpleDB" + }, + "sms": { + "name": "SMS" + }, + "snowball": { + "name": "Snowball" + }, + "sns": { + "name": "SNS", + "cors": true + }, + "sqs": { + "name": "SQS", + "cors": true + }, + "ssm": { + "name": "SSM", + "cors": true + }, + "storagegateway": { + "name": "StorageGateway", + "cors": true + }, + "stepfunctions": { + "prefix": "states", + "name": "StepFunctions" + }, + "sts": { + "name": "STS", + "cors": true + }, + "support": { + "name": "Support" + }, + "swf": { + "name": "SWF" + }, + "xray": { + "name": "XRay", + "cors": true + }, + "waf": { + "name": "WAF", + "cors": true + }, + "wafregional": { + "prefix": "waf-regional", + "name": "WAFRegional" + }, + "workdocs": { + "name": "WorkDocs", + "cors": true + }, + "workspaces": { + "name": "WorkSpaces" + }, + "codestar": { + "name": "CodeStar" + }, + "lexmodelbuildingservice": { + "prefix": "lex-models", + "name": "LexModelBuildingService", + "cors": true + }, + "marketplaceentitlementservice": { + "prefix": "entitlement.marketplace", + "name": "MarketplaceEntitlementService" + }, + "athena": { + "name": "Athena", + "cors": true + }, + "greengrass": { + "name": "Greengrass" + }, + "dax": { + "name": "DAX" + }, + "migrationhub": { + "prefix": "AWSMigrationHub", + "name": "MigrationHub" + }, + "cloudhsmv2": { + "name": "CloudHSMV2", + "cors": true + }, + "glue": { + "name": "Glue" + }, + "mobile": { + "name": "Mobile" + }, + "pricing": { + "name": "Pricing", + "cors": true + }, + "costexplorer": { + "prefix": "ce", + "name": "CostExplorer", + "cors": true + }, + "mediaconvert": { + "name": "MediaConvert" + }, + "medialive": { + "name": "MediaLive" + }, + "mediapackage": { + "name": "MediaPackage" + }, + "mediastore": { + "name": "MediaStore" + }, + "mediastoredata": { + "prefix": "mediastore-data", + "name": "MediaStoreData", + "cors": true + }, + "appsync": { + "name": "AppSync" + }, + "guardduty": { + "name": "GuardDuty" + }, + "mq": { + "name": "MQ" + }, + "comprehend": { + "name": "Comprehend", + "cors": true + }, + "iotjobsdataplane": { + "prefix": "iot-jobs-data", + "name": "IoTJobsDataPlane" + }, + "kinesisvideoarchivedmedia": { + "prefix": "kinesis-video-archived-media", + "name": "KinesisVideoArchivedMedia", + "cors": true + }, + "kinesisvideomedia": { + "prefix": "kinesis-video-media", + "name": "KinesisVideoMedia", + "cors": true + }, + "kinesisvideo": { + "name": "KinesisVideo", + "cors": true + }, + "sagemakerruntime": { + "prefix": "runtime.sagemaker", + "name": "SageMakerRuntime" + }, + "sagemaker": { + "name": "SageMaker" + }, + "translate": { + "name": "Translate", + "cors": true + }, + "resourcegroups": { + "prefix": "resource-groups", + "name": "ResourceGroups", + "cors": true + }, + "alexaforbusiness": { + "name": "AlexaForBusiness" + }, + "cloud9": { + "name": "Cloud9" + }, + "serverlessapplicationrepository": { + "prefix": "serverlessrepo", + "name": "ServerlessApplicationRepository" + }, + "servicediscovery": { + "name": "ServiceDiscovery" + }, + "workmail": { + "name": "WorkMail" + }, + "autoscalingplans": { + "prefix": "autoscaling-plans", + "name": "AutoScalingPlans" + }, + "transcribeservice": { + "prefix": "transcribe", + "name": "TranscribeService" + }, + "connect": { + "name": "Connect", + "cors": true + }, + "acmpca": { + "prefix": "acm-pca", + "name": "ACMPCA" + }, + "fms": { + "name": "FMS" + }, + "secretsmanager": { + "name": "SecretsManager", + "cors": true + }, + "iotanalytics": { + "name": "IoTAnalytics", + "cors": true + }, + "iot1clickdevicesservice": { + "prefix": "iot1click-devices", + "name": "IoT1ClickDevicesService" + }, + "iot1clickprojects": { + "prefix": "iot1click-projects", + "name": "IoT1ClickProjects" + }, + "pi": { + "name": "PI" + }, + "neptune": { + "name": "Neptune" + }, + "mediatailor": { + "name": "MediaTailor" + }, + "eks": { + "name": "EKS" + }, + "macie": { + "name": "Macie" + }, + "dlm": { + "name": "DLM" + }, + "signer": { + "name": "Signer" + }, + "chime": { + "name": "Chime" + }, + "pinpointemail": { + "prefix": "pinpoint-email", + "name": "PinpointEmail" + }, + "ram": { + "name": "RAM" + }, + "route53resolver": { + "name": "Route53Resolver" + }, + "pinpointsmsvoice": { + "prefix": "sms-voice", + "name": "PinpointSMSVoice" + }, + "quicksight": { + "name": "QuickSight" + }, + "rdsdataservice": { + "prefix": "rds-data", + "name": "RDSDataService" + }, + "amplify": { + "name": "Amplify" + }, + "datasync": { + "name": "DataSync" + }, + "robomaker": { + "name": "RoboMaker" + }, + "transfer": { + "name": "Transfer" + }, + "globalaccelerator": { + "name": "GlobalAccelerator" + }, + "comprehendmedical": { + "name": "ComprehendMedical", + "cors": true + }, + "kinesisanalyticsv2": { + "name": "KinesisAnalyticsV2" + }, + "mediaconnect": { + "name": "MediaConnect" + }, + "fsx": { + "name": "FSx" + }, + "securityhub": { + "name": "SecurityHub" + }, + "appmesh": { + "name": "AppMesh", + "versions": [ + "2018-10-01*" + ] + }, + "licensemanager": { + "prefix": "license-manager", + "name": "LicenseManager" + }, + "kafka": { + "name": "Kafka" + }, + "apigatewaymanagementapi": { + "name": "ApiGatewayManagementApi" + }, + "apigatewayv2": { + "name": "ApiGatewayV2" + }, + "docdb": { + "name": "DocDB" + }, + "backup": { + "name": "Backup" + }, + "worklink": { + "name": "WorkLink" + }, + "textract": { + "name": "Textract" + }, + "managedblockchain": { + "name": "ManagedBlockchain" + }, + "mediapackagevod": { + "prefix": "mediapackage-vod", + "name": "MediaPackageVod" + }, + "groundstation": { + "name": "GroundStation" + }, + "iotthingsgraph": { + "name": "IoTThingsGraph" + }, + "iotevents": { + "name": "IoTEvents" + }, + "ioteventsdata": { + "prefix": "iotevents-data", + "name": "IoTEventsData" + }, + "personalize": { + "name": "Personalize", + "cors": true + }, + "personalizeevents": { + "prefix": "personalize-events", + "name": "PersonalizeEvents", + "cors": true + }, + "personalizeruntime": { + "prefix": "personalize-runtime", + "name": "PersonalizeRuntime", + "cors": true + }, + "applicationinsights": { + "prefix": "application-insights", + "name": "ApplicationInsights" + }, + "servicequotas": { + "prefix": "service-quotas", + "name": "ServiceQuotas" + }, + "ec2instanceconnect": { + "prefix": "ec2-instance-connect", + "name": "EC2InstanceConnect" + }, + "eventbridge": { + "name": "EventBridge" + }, + "lakeformation": { + "name": "LakeFormation" + }, + "forecastservice": { + "prefix": "forecast", + "name": "ForecastService", + "cors": true + }, + "forecastqueryservice": { + "prefix": "forecastquery", + "name": "ForecastQueryService", + "cors": true + }, + "qldb": { + "name": "QLDB" + }, + "qldbsession": { + "prefix": "qldb-session", + "name": "QLDBSession" + }, + "workmailmessageflow": { + "name": "WorkMailMessageFlow" + }, + "codestarnotifications": { + "prefix": "codestar-notifications", + "name": "CodeStarNotifications" + }, + "savingsplans": { + "name": "SavingsPlans" + }, + "sso": { + "name": "SSO" + }, + "ssooidc": { + "prefix": "sso-oidc", + "name": "SSOOIDC" + }, + "marketplacecatalog": { + "prefix": "marketplace-catalog", + "name": "MarketplaceCatalog" + }, + "dataexchange": { + "name": "DataExchange" + }, + "sesv2": { + "name": "SESV2" + }, + "migrationhubconfig": { + "prefix": "migrationhub-config", + "name": "MigrationHubConfig" + }, + "connectparticipant": { + "name": "ConnectParticipant" + }, + "appconfig": { + "name": "AppConfig" + }, + "iotsecuretunneling": { + "name": "IoTSecureTunneling" + }, + "wafv2": { + "name": "WAFV2" + }, + "elasticinference": { + "prefix": "elastic-inference", + "name": "ElasticInference" + }, + "imagebuilder": { + "name": "Imagebuilder" + }, + "schemas": { + "name": "Schemas" + }, + "accessanalyzer": { + "name": "AccessAnalyzer" + }, + "codegurureviewer": { + "prefix": "codeguru-reviewer", + "name": "CodeGuruReviewer" + }, + "codeguruprofiler": { + "name": "CodeGuruProfiler" + }, + "computeoptimizer": { + "prefix": "compute-optimizer", + "name": "ComputeOptimizer" + }, + "frauddetector": { + "name": "FraudDetector" + }, + "kendra": { + "name": "Kendra" + }, + "networkmanager": { + "name": "NetworkManager" + }, + "outposts": { + "name": "Outposts" + }, + "augmentedairuntime": { + "prefix": "sagemaker-a2i-runtime", + "name": "AugmentedAIRuntime" + }, + "ebs": { + "name": "EBS" + }, + "kinesisvideosignalingchannels": { + "prefix": "kinesis-video-signaling", + "name": "KinesisVideoSignalingChannels", + "cors": true + }, + "detective": { + "name": "Detective" + }, + "codestarconnections": { + "prefix": "codestar-connections", + "name": "CodeStarconnections" + }, + "synthetics": { + "name": "Synthetics" + }, + "iotsitewise": { + "name": "IoTSiteWise" + }, + "macie2": { + "name": "Macie2" + }, + "codeartifact": { + "name": "CodeArtifact" + }, + "honeycode": { + "name": "Honeycode" + }, + "ivs": { + "name": "IVS" + }, + "braket": { + "name": "Braket" + }, + "identitystore": { + "name": "IdentityStore" + }, + "appflow": { + "name": "Appflow" + }, + "redshiftdata": { + "prefix": "redshift-data", + "name": "RedshiftData" + }, + "ssoadmin": { + "prefix": "sso-admin", + "name": "SSOAdmin" + }, + "timestreamquery": { + "prefix": "timestream-query", + "name": "TimestreamQuery" + }, + "timestreamwrite": { + "prefix": "timestream-write", + "name": "TimestreamWrite" + }, + "s3outposts": { + "name": "S3Outposts" + }, + "databrew": { + "name": "DataBrew" + }, + "servicecatalogappregistry": { + "prefix": "servicecatalog-appregistry", + "name": "ServiceCatalogAppRegistry" + }, + "networkfirewall": { + "prefix": "network-firewall", + "name": "NetworkFirewall" + }, + "mwaa": { + "name": "MWAA" + }, + "amplifybackend": { + "name": "AmplifyBackend" + }, + "appintegrations": { + "name": "AppIntegrations" + }, + "connectcontactlens": { + "prefix": "connect-contact-lens", + "name": "ConnectContactLens" + }, + "devopsguru": { + "prefix": "devops-guru", + "name": "DevOpsGuru" + }, + "ecrpublic": { + "prefix": "ecr-public", + "name": "ECRPUBLIC" + }, + "lookoutvision": { + "name": "LookoutVision" + }, + "sagemakerfeaturestoreruntime": { + "prefix": "sagemaker-featurestore-runtime", + "name": "SageMakerFeatureStoreRuntime" + }, + "customerprofiles": { + "prefix": "customer-profiles", + "name": "CustomerProfiles" + }, + "auditmanager": { + "name": "AuditManager" + }, + "emrcontainers": { + "prefix": "emr-containers", + "name": "EMRcontainers" + }, + "healthlake": { + "name": "HealthLake" + }, + "sagemakeredge": { + "prefix": "sagemaker-edge", + "name": "SagemakerEdge" + }, + "amp": { + "name": "Amp" + }, + "greengrassv2": { + "name": "GreengrassV2" + }, + "iotdeviceadvisor": { + "name": "IotDeviceAdvisor" + }, + "iotfleethub": { + "name": "IoTFleetHub" + }, + "iotwireless": { + "name": "IoTWireless" + }, + "location": { + "name": "Location", + "cors": true + }, + "wellarchitected": { + "name": "WellArchitected" + }, + "lexmodelsv2": { + "prefix": "models.lex.v2", + "name": "LexModelsV2" + }, + "lexruntimev2": { + "prefix": "runtime.lex.v2", + "name": "LexRuntimeV2", + "cors": true + }, + "fis": { + "name": "Fis" + }, + "lookoutmetrics": { + "name": "LookoutMetrics" + }, + "mgn": { + "name": "Mgn" + }, + "lookoutequipment": { + "name": "LookoutEquipment" + }, + "nimble": { + "name": "Nimble" + }, + "finspace": { + "name": "Finspace" + }, + "finspacedata": { + "prefix": "finspace-data", + "name": "Finspacedata" + }, + "ssmcontacts": { + "prefix": "ssm-contacts", + "name": "SSMContacts" + }, + "ssmincidents": { + "prefix": "ssm-incidents", + "name": "SSMIncidents" + }, + "applicationcostprofiler": { + "name": "ApplicationCostProfiler" + }, + "apprunner": { + "name": "AppRunner" + }, + "proton": { + "name": "Proton" + }, + "route53recoverycluster": { + "prefix": "route53-recovery-cluster", + "name": "Route53RecoveryCluster" + }, + "route53recoverycontrolconfig": { + "prefix": "route53-recovery-control-config", + "name": "Route53RecoveryControlConfig" + }, + "route53recoveryreadiness": { + "prefix": "route53-recovery-readiness", + "name": "Route53RecoveryReadiness" + }, + "chimesdkidentity": { + "prefix": "chime-sdk-identity", + "name": "ChimeSDKIdentity" + }, + "chimesdkmessaging": { + "prefix": "chime-sdk-messaging", + "name": "ChimeSDKMessaging" + }, + "snowdevicemanagement": { + "prefix": "snow-device-management", + "name": "SnowDeviceManagement" + }, + "memorydb": { + "name": "MemoryDB" + }, + "opensearch": { + "name": "OpenSearch" + }, + "kafkaconnect": { + "name": "KafkaConnect" + }, + "voiceid": { + "prefix": "voice-id", + "name": "VoiceID" + }, + "wisdom": { + "name": "Wisdom" + }, + "account": { + "name": "Account" + }, + "cloudcontrol": { + "name": "CloudControl" + }, + "grafana": { + "name": "Grafana" + }, + "panorama": { + "name": "Panorama" + }, + "chimesdkmeetings": { + "prefix": "chime-sdk-meetings", + "name": "ChimeSDKMeetings" + }, + "resiliencehub": { + "name": "Resiliencehub" + }, + "migrationhubstrategy": { + "name": "MigrationHubStrategy" + }, + "appconfigdata": { + "name": "AppConfigData" + }, + "drs": { + "name": "Drs" + }, + "migrationhubrefactorspaces": { + "prefix": "migration-hub-refactor-spaces", + "name": "MigrationHubRefactorSpaces" + }, + "evidently": { + "name": "Evidently" + }, + "inspector2": { + "name": "Inspector2" + }, + "rbin": { + "name": "Rbin" + }, + "rum": { + "name": "RUM" + }, + "backupgateway": { + "prefix": "backup-gateway", + "name": "BackupGateway" + }, + "iottwinmaker": { + "name": "IoTTwinMaker" + }, + "workspacesweb": { + "prefix": "workspaces-web", + "name": "WorkSpacesWeb" + }, + "amplifyuibuilder": { + "name": "AmplifyUIBuilder" + }, + "keyspaces": { + "name": "Keyspaces" + }, + "billingconductor": { + "name": "Billingconductor" + }, + "gamesparks": { + "name": "GameSparks" + }, + "pinpointsmsvoicev2": { + "prefix": "pinpoint-sms-voice-v2", + "name": "PinpointSMSVoiceV2" + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/integ-tests/lib/assertions/sdk.ts b/packages/@aws-cdk/integ-tests/lib/assertions/sdk.ts new file mode 100644 index 0000000000000..ead56af7732d9 --- /dev/null +++ b/packages/@aws-cdk/integ-tests/lib/assertions/sdk.ts @@ -0,0 +1,106 @@ +import { CustomResource, Reference, Lazy } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { EqualsAssertion } from './assertions'; +import { IAssertion } from './deploy-assert'; +import { md5hash } from './private/hash'; +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 { Construct as CoreConstruct } from '@aws-cdk/core'; + +/** + * Options to perform an AWS JavaScript V2 API call + */ +export interface SdkQueryOptions { + /** + * The AWS service, i.e. S3 + */ + readonly service: string; + + /** + * The api call to make, i.e. getBucketLifecycle + */ + readonly api: string; + + /** + * Any parameters to pass to the api call + */ + readonly parameters?: any; +} + +/** + * Options for creating an SDKQuery provider + */ +export interface SdkQueryProps extends SdkQueryOptions {} + +export class SdkQuery extends CoreConstruct { + private readonly sdkCallResource: CustomResource; + private flattenResponse: string = 'false'; + + constructor(scope: Construct, id: string, props: SdkQueryProps) { + super(scope, id); + + const provider = new AssertionsProvider(this, 'SdkProvider'); + provider.addPolicyStatementFromSdkCall(props.service, props.api); + + this.sdkCallResource = new CustomResource(this, 'Default', { + serviceToken: provider.serviceToken, + properties: { + service: props.service, + api: props.api, + parameters: provider.encode(props.parameters), + flattenResponse: Lazy.string({ produce: () => this.flattenResponse }), + }, + resourceType: `${SDK_RESOURCE_TYPE_PREFIX}${props.service}${props.api}`, + }); + + // Needed so that all the policies set up by the provider should be available before the custom resource is provisioned. + this.sdkCallResource.node.addDependency(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}`); + } + + /** + * Creates an assertion custom resource that will assert that the response + * from the SDKQuery equals the 'expected' value + */ + public assertEqual(expected: any, actualAttr?: string): IAssertion { + const hash = md5hash(expected); + let inputResourceAtt = 'apiCallResponse'; + if (actualAttr) { + this.flattenResponse = 'true'; + inputResourceAtt = `apiCallResponse.${actualAttr}`; + } + return new EqualsAssertion(this, `AssertEquals${hash}`, { + expected, + inputResource: this.sdkCallResource, + inputResourceAtt, + }); + } +} diff --git a/packages/@aws-cdk/integ-tests/lib/index.ts b/packages/@aws-cdk/integ-tests/lib/index.ts index 0553319f009fb..638d20a4d1d1a 100644 --- a/packages/@aws-cdk/integ-tests/lib/index.ts +++ b/packages/@aws-cdk/integ-tests/lib/index.ts @@ -1 +1 @@ -export * from './test-case'; \ No newline at end of file +export * from './test-case'; diff --git a/packages/@aws-cdk/integ-tests/package.json b/packages/@aws-cdk/integ-tests/package.json index 813925be51265..83d5b4f5af4f8 100644 --- a/packages/@aws-cdk/integ-tests/package.json +++ b/packages/@aws-cdk/integ-tests/package.json @@ -61,18 +61,37 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/cdk-build-tools": "0.0.0", + "@aws-cdk/assertions": "0.0.0", "@aws-cdk/pkglint": "0.0.0", "@types/fs-extra": "^8.1.2", "@types/jest": "^27.4.1", "@types/node": "^10.17.60", - "jest": "^27.5.1" + "jest": "^27.5.1", + "nock": "^13.2.4", + "aws-sdk-mock": "5.6.0", + "sinon": "^9.2.4", + "aws-sdk": "^2.1093.0" }, "dependencies": { "@aws-cdk/cloud-assembly-schema": "0.0.0", "@aws-cdk/core": "0.0.0", "@aws-cdk/cx-api": "0.0.0", + "@aws-cdk/aws-lambda": "0.0.0", + "@aws-cdk/triggers": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/custom-resources": "0.0.0", "constructs": "^3.3.69" }, + "peerDependencies": { + "@aws-cdk/cloud-assembly-schema": "0.0.0", + "@aws-cdk/assertions": "0.0.0", + "@aws-cdk/core": "0.0.0", + "@aws-cdk/triggers": "0.0.0", + "@aws-cdk/custom-resources": "0.0.0", + "constructs": "^3.3.69", + "@aws-cdk/aws-lambda": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0" + }, "repository": { "url": "https://github.com/aws/aws-cdk.git", "type": "git", @@ -98,11 +117,6 @@ "publishConfig": { "tag": "latest" }, - "peerDependencies": { - "@aws-cdk/cloud-assembly-schema": "0.0.0", - "@aws-cdk/core": "0.0.0", - "constructs": "^3.3.69" - }, "awscdkio": { "announce": false } diff --git a/packages/@aws-cdk/integ-tests/test/assertions/assertions.test.ts b/packages/@aws-cdk/integ-tests/test/assertions/assertions.test.ts new file mode 100644 index 0000000000000..c8558c6460b0a --- /dev/null +++ b/packages/@aws-cdk/integ-tests/test/assertions/assertions.test.ts @@ -0,0 +1,48 @@ +import { Template } from '@aws-cdk/assertions'; +import { App, CustomResource, Stack } from '@aws-cdk/core'; +import { IAssertion, DeployAssert, EqualsAssertion } from '../../lib/assertions'; + +describe('Assertion', () => { + test('registration', () => { + const app = new App(); + const stack = new Stack(app); + const deployAssert = new DeployAssert(stack); + + class MyAssertion implements IAssertion { + public result = 'result'; + } + const assertion = new MyAssertion(); + deployAssert.registerAssertion(assertion); + + expect(deployAssert._assertions).toContain(assertion); + }); +}); + +describe('EqualsAssertion', () => { + test('default', () => { + const app = new App(); + const stack = new Stack(app); + const deployAssert = new DeployAssert(stack); + const customRes = new CustomResource(stack, 'MyCustomResource', { + serviceToken: 'serviceToken', + }); + deployAssert.registerAssertion(new EqualsAssertion(stack, 'MyAssertion', { + expected: { foo: 'bar' }, + inputResource: customRes, + inputResourceAtt: 'foo', + })); + + Template.fromStack(stack).hasResourceProperties('Custom::DeployAssert@AssertEquals', { + actual: { + 'Fn::GetAtt': [ + 'MyCustomResource', + 'foo', + ], + }, + expected: { + foo: 'bar', + }, + assertionType: 'equals', + }); + }); +}); 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 new file mode 100644 index 0000000000000..bb73e87b2da7e --- /dev/null +++ b/packages/@aws-cdk/integ-tests/test/assertions/deploy-assert.test.ts @@ -0,0 +1,99 @@ +import { Template } from '@aws-cdk/assertions'; +// import * as iam from '@aws-cdk/aws-iam'; +import { App, Stack } from '@aws-cdk/core'; +import { IAssertion, DeployAssert } from '../../lib/assertions'; + +describe('DeployAssert', () => { + describe('ResultsCollection', () => { + test('default', () => { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'MyStack'); + + // WHEN + new DeployAssert(stack); + + + // THEN + const template = Template.fromStack(stack); + template.resourceCountIs('Custom::DeployAssert@ResultsCollection', 1); + + template.hasOutput('Results', {}); + }); + + test('assertion results are part of the output', () => { + // GIVEN + class MyAssertion implements IAssertion { + public readonly result: string; + constructor(result: string) { + this.result = result; + } + } + + const app = new App(); + const stack = new Stack(app, 'MyStack'); + + // WHEN + const deployAssert = new DeployAssert(stack); + deployAssert.registerAssertion( + new MyAssertion('MyAssertion1Result'), + ); + deployAssert.registerAssertion( + new MyAssertion('MyAssertion2Result'), + ); + + + // THEN + const template = Template.fromStack(stack); + template.hasResourceProperties('Custom::DeployAssert@ResultsCollection', { + assertionResults: ['MyAssertion1Result', 'MyAssertion2Result'], + }); + }); + }); + + describe('queryAws', () => { + test('default', () => { + // GIVEN + const app = new App(); + const stack = new Stack(app); + + // WHEN + const deplossert = new DeployAssert(stack); + deplossert.queryAws({ + service: 'MyService', + api: 'MyApi', + }); + + + // THEN + Template.fromStack(stack).hasResourceProperties('Custom::DeployAssert@SdkCallMyServiceMyApi', { + api: 'MyApi', + service: 'MyService', + }); + }); + + test('multiple queries can be configured', () => { + // GIVEN + const app = new App(); + const stack = new Stack(app); + + // WHEN + const deplossert = new DeployAssert(stack); + deplossert.queryAws({ + service: 'MyService', + api: 'MyApi1', + }); + deplossert.queryAws({ + service: 'MyService', + api: 'MyApi2', + }); + + + // THEN + const template = Template.fromStack(stack); + 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/private/hash.test.ts b/packages/@aws-cdk/integ-tests/test/assertions/private/hash.test.ts new file mode 100644 index 0000000000000..a6c40777189df --- /dev/null +++ b/packages/@aws-cdk/integ-tests/test/assertions/private/hash.test.ts @@ -0,0 +1,17 @@ +// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. +import { md5hash } from '../../../lib/assertions/private/hash'; + +describe('md5hash', () => { + test('default', () => { + const hash = md5hash({ key: 'value' }); + expect(hash).toEqual('a7353f7cddce808de0032747a0b7be50'); + }); + + test('fails if falsy', () => { + expect(() => md5hash(null)).toThrow(/falsy/); + expect(() => md5hash(undefined)).toThrow(/falsy/); + expect(() => md5hash({})).toThrow(/falsy/); + expect(() => md5hash('')).toThrow(/falsy/); + expect(() => md5hash([])).toThrow(/falsy/); + }); +}); diff --git a/packages/@aws-cdk/integ-tests/test/assertions/providers/lambda-handler/assertion.test.ts b/packages/@aws-cdk/integ-tests/test/assertions/providers/lambda-handler/assertion.test.ts new file mode 100644 index 0000000000000..911876c84bdfb --- /dev/null +++ b/packages/@aws-cdk/integ-tests/test/assertions/providers/lambda-handler/assertion.test.ts @@ -0,0 +1,80 @@ +import { AssertionRequest, AssertionResult, AssertionType } from '../../../../lib/assertions'; +import { AssertionHandler } from '../../../../lib/assertions/providers/lambda-handler/assertion'; + +function assertionHandler() { + const context: any = { + getRemainingTimeInMillis: () => 50000, + }; + return new AssertionHandler({} as any, context); // as any to ignore all type checks +} + +beforeAll(() => { + jest.useFakeTimers(); + jest.spyOn(console, 'log').mockImplementation(() => { return true; }); +}); +afterAll(() => { + jest.useRealTimers(); + jest.restoreAllMocks(); +}); + +describe('AssertionHandler', () => { + describe('equals', () => { + test('pass', async () => { + // GIVEN + const handler = assertionHandler() as any; + const request: AssertionRequest = { + assertionType: AssertionType.EQUALS, + actual: { + stringParam: 'foo', + numberParam: 3, + booleanParam: true, + }, + expected: { + stringParam: 'foo', + numberParam: 3, + booleanParam: true, + }, + }; + + // WHEN + const response: AssertionResult = await handler.processEvent(request); + + // THEN + expect(response.data.status).toEqual('pass'); + }); + + test('fail', async () => { + // GIVEN + const handler = assertionHandler() as any; + const request: AssertionRequest = { + assertionType: AssertionType.EQUALS, + actual: { + stringParam: 'foo', + }, + expected: { + stringParam: 'bar', + }, + }; + + // WHEN + const response: AssertionResult = await handler.processEvent(request); + + // THEN + expect(response.data.status).toEqual('fail'); + }); + }); + + test('unsupported query', async () => { + // GIVEN + const handler = assertionHandler() as any; + const assertionType: any = 'somethingElse'; + const request: AssertionRequest = { + assertionType, + actual: 'foo', + expected: 'bar', + }; + + // THEN + await expect(handler.processEvent(request)).rejects.toThrow(/Unsupported query type/); + }); +}); diff --git a/packages/@aws-cdk/integ-tests/test/assertions/providers/lambda-handler/base.test.ts b/packages/@aws-cdk/integ-tests/test/assertions/providers/lambda-handler/base.test.ts new file mode 100644 index 0000000000000..d30f20a5678f3 --- /dev/null +++ b/packages/@aws-cdk/integ-tests/test/assertions/providers/lambda-handler/base.test.ts @@ -0,0 +1,199 @@ +import * as nock from 'nock'; +import { CustomResourceHandler } from '../../../../lib/assertions/providers/lambda-handler/base'; + +interface MyHandlerRequest { + readonly input: string; +} + +interface MyHandlerResponse { + readonly output: string; +} + +interface CloudFormationResponse extends Omit { + readonly Data: MyHandlerResponse; +} + +describe('CustomResourceHandler', () => { + beforeEach(() => { + jest.spyOn(console, 'log').mockImplementation(() => { return true; }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + nock.cleanAll(); + }); + + test('default', async () => { + // GIVEN + class MyHandler extends CustomResourceHandler { + protected async processEvent(request: MyHandlerRequest): Promise { + return { output: `MyResponseTo${request.input}` }; + } + } + + const nocked = nockUp((body) => { + return body.Status === 'SUCCESS' + && body.Reason === 'OK' + && body.Data.output === 'MyResponseToYourRequest' + && body.StackId === 'MyStackId' + && body.RequestId === 'MyRequestId' + && body.NoEcho === false; + }); + + + // WHEN + const handler = new MyHandler(createEvent({ input: 'YourRequest' }), standardContext); + + await handler.handle(); + + // THEN + expect(nocked.isDone()).toEqual(true); + }); + + test('processEvent fails', async () => { + // GIVEN + class MyHandler extends CustomResourceHandler { + protected async processEvent(_: MyHandlerRequest): Promise { + throw new Error('FooFAIL'); + } + } + + const nocked = nockUp((body) => { + return body.Status === 'FAILED' + && body.Reason === 'FooFAIL'; + }); + + + // WHEN + const handler = new MyHandler(createEvent({ input: 'YourRequest' }), standardContext); + + await handler.handle(); + + // THEN + expect(nocked.isDone()).toEqual(true); + }); + + test('timeout kicks in', async () => { + // GIVEN + class MyHandler extends CustomResourceHandler { + protected async processEvent(_: MyHandlerRequest): Promise { + await new Promise((resolve) => { + setTimeout(resolve, 2000); + }); + return new Promise((resolve, _reject) => resolve(undefined)); + } + } + + const nocked = nockUp((body) => { + return body.Status === 'FAILED' + && body.Reason !== undefined + && /Timeout/.test(body.Reason); + }); + + const handler = new MyHandler(createEvent(), { + ...standardContext, + getRemainingTimeInMillis: () => 1300, + }); + + + // WHEN + await handler.handle(); + + + // THEN + expect(nocked.isDone()).toEqual(true); + }); + + describe('physicalResourceId', () => { + test('create event', async () => { + // GIVEN + class MyHandler extends CustomResourceHandler { + protected async processEvent(request: MyHandlerRequest): Promise { + return { output: `MyResponseTo${request.input}` }; + } + } + + const nocked = nockUp((body) => { + return body.PhysicalResourceId === 'MyLogicalResourceId'; + }); + + + // WHEN + const handler = new MyHandler(createEvent({ input: 'YourRequest' }), standardContext); + + await handler.handle(); + + // THEN + expect(nocked.isDone()).toEqual(true); + }); + + test('update event', async () => { + // GIVEN + class MyHandler extends CustomResourceHandler { + protected async processEvent(request: MyHandlerRequest): Promise { + return { output: `MyResponseTo${request.input}` }; + } + } + + const nocked = nockUp((body) => { + return body.PhysicalResourceId === 'MyPhysicalResourceId'; + }); + + + // WHEN + const handler = new MyHandler(updateEvent({ input: 'YourRequest' }), standardContext); + + await handler.handle(); + + // THEN + expect(nocked.isDone()).toEqual(true); + }); + }); +}); + +function nockUp(predicate: (body: CloudFormationResponse) => boolean) { + return nock('https://someurl.com') + .put('/', predicate) + .reply(200); +} + +const standardContext: any = { // keeping this as any so as to not have to fill all the mandatory attributes of AWSLambda.Context + getRemainingTimeInMillis: () => 5000, +}; + +function createEvent(data?: MyHandlerRequest): AWSLambda.CloudFormationCustomResourceCreateEvent { + return { + LogicalResourceId: 'MyLogicalResourceId', + RequestId: 'MyRequestId', + RequestType: 'Create', + ResourceType: 'MyResourceType', + ResourceProperties: { + ...data, + ServiceToken: 'MyServiceToken', + }, + ResponseURL: 'https://someurl.com', + ServiceToken: 'MyServiceToken', + StackId: 'MyStackId', + }; +} + +function updateEvent(data?: MyHandlerRequest): AWSLambda.CloudFormationCustomResourceUpdateEvent { + return { + LogicalResourceId: 'MyLogicalResourceId', + OldResourceProperties: { + ...data, + ServiceToken: 'MyServiceToken', + }, + PhysicalResourceId: 'MyPhysicalResourceId', + RequestId: 'MyRequestId', + RequestType: 'Update', + ResourceType: 'MyResourceType', + ResourceProperties: { + ...data, + ServiceToken: 'MyServiceToken', + }, + ResponseURL: 'https://someurl.com', + ServiceToken: 'MyServiceToken', + StackId: 'MyStackId', + }; +} diff --git a/packages/@aws-cdk/integ-tests/test/assertions/providers/lambda-handler/results.test.ts b/packages/@aws-cdk/integ-tests/test/assertions/providers/lambda-handler/results.test.ts new file mode 100644 index 0000000000000..33b0cef42677d --- /dev/null +++ b/packages/@aws-cdk/integ-tests/test/assertions/providers/lambda-handler/results.test.ts @@ -0,0 +1,59 @@ +// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. +import { ResultsCollectionRequest, ResultsCollectionResult } from '../../../../lib/assertions'; +import { ResultsCollectionHandler } from '../../../../lib/assertions/providers/lambda-handler/results'; + +function handler() { + const context: any = { + getRemainingTimeInMillis: () => 50000, + }; + return new ResultsCollectionHandler({} as any, context); // as any to ignore all type checks +} +beforeAll(() => { + jest.useFakeTimers(); + jest.spyOn(process.stderr, 'write').mockImplementation(() => { return true; }); + jest.spyOn(process.stdout, 'write').mockImplementation(() => { return true; }); +}); +afterAll(() => { + jest.useRealTimers(); + jest.restoreAllMocks(); +}); + +describe('ResultsCollectionHandler', () => { + test('default', async () => { + // GIVEN + const resultsCollection = handler() as any; + const request: ResultsCollectionRequest = { + assertionResults: [ + { status: 'pass' }, + { status: 'fail', message: 'something failed' }, + ], + }; + + // WHEN + const result: ResultsCollectionResult = await resultsCollection.processEvent(request); + const split = result.message.split('\n'); + + // THEN + expect(split.length).toEqual(2); + expect(split[0]).toEqual('Test0: pass'); + expect(split[1]).toEqual('Test1: fail - something failed'); + }); + + test('message not displayed for pass', async () => { + // GIVEN + const resultsCollection = handler() as any; + const request: ResultsCollectionRequest = { + assertionResults: [ + { status: 'pass', message: 'OK' }, + ], + }; + + // WHEN + const result: ResultsCollectionResult = await resultsCollection.processEvent(request); + const split = result.message.split('\n'); + + // THEN + expect(split.length).toEqual(1); + expect(split[0]).toEqual('Test0: pass'); + }); +}); diff --git a/packages/@aws-cdk/integ-tests/test/assertions/providers/lambda-handler/sdk.test.ts b/packages/@aws-cdk/integ-tests/test/assertions/providers/lambda-handler/sdk.test.ts new file mode 100644 index 0000000000000..bce5f29548cb8 --- /dev/null +++ b/packages/@aws-cdk/integ-tests/test/assertions/providers/lambda-handler/sdk.test.ts @@ -0,0 +1,107 @@ +// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. +import * as SDK from 'aws-sdk'; +import * as AWS from 'aws-sdk-mock'; +import * as sinon from 'sinon'; +import { SdkRequest, SdkResult } from '../../../../lib/assertions'; +import { SdkHandler } from '../../../../lib/assertions/providers/lambda-handler/sdk'; + +function sdkHandler() { + const context: any = { + getRemainingTimeInMillis: () => 50000, + }; + return new SdkHandler({} as any, context); // as any to ignore all type checks +} +beforeAll(() => { + jest.useFakeTimers(); + jest.spyOn(console, 'log').mockImplementation(() => { return true; }); +}); +afterAll(() => { + jest.useRealTimers(); + jest.restoreAllMocks(); +}); + +describe('SdkHandler', () => { + beforeEach(() => { + AWS.setSDKInstance(SDK); + }); + + afterEach(() => { + AWS.restore(); + }); + + test('default', async () => { + // GIVEN + const expectedResponse = { + Contents: [ + { + Key: 'first-key', + ETag: 'first-key-etag', + }, + { + Key: 'second-key', + ETag: 'second-key-etag', + }, + ], + } as SDK.S3.ListObjectsOutput; + AWS.mock('S3', 'listObjects', sinon.fake.resolves(expectedResponse)); + const handler = sdkHandler() as any; + const request: SdkRequest = { + service: 'S3', + api: 'listObjects', + parameters: { + Bucket: 'myBucket', + }, + }; + + // WHEN + const response: SdkResult = await handler.processEvent(request); + + + // THEN + expect(response.apiCallResponse).toEqual(expectedResponse); + }); + + describe('decode', () => { + test('boolean true', async () => { + // GIVEN + const fake = sinon.fake.resolves({}); + AWS.mock('EC2', 'describeInstances', fake); + const handler = sdkHandler() as any; + const request: SdkRequest = { + service: 'EC2', + api: 'describeInstances', + parameters: { + DryRun: 'TRUE:BOOLEAN', + }, + }; + + // WHEN + await handler.processEvent(request); + + + // THEN + sinon.assert.calledWith(fake, { DryRun: true }); + }); + + test('boolean false', async () => { + // GIVEN + const fake = sinon.fake.resolves({}); + AWS.mock('EC2', 'describeInstances', fake); + const handler = sdkHandler() as any; + const request: SdkRequest = { + service: 'EC2', + api: 'describeInstances', + parameters: { + DryRun: 'FALSE:BOOLEAN', + }, + }; + + // WHEN + await handler.processEvent(request); + + + // THEN + sinon.assert.calledWith(fake, { DryRun: false }); + }); + }); +}); diff --git a/packages/@aws-cdk/integ-tests/test/assertions/providers/provider.test.ts b/packages/@aws-cdk/integ-tests/test/assertions/providers/provider.test.ts new file mode 100644 index 0000000000000..376be437ddb8a --- /dev/null +++ b/packages/@aws-cdk/integ-tests/test/assertions/providers/provider.test.ts @@ -0,0 +1,122 @@ +import { Template } from '@aws-cdk/assertions'; +import { Stack } from '@aws-cdk/core'; +import { AssertionsProvider } from '../../../lib/assertions'; + +describe('AssertionProvider', () => { + test('default', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const provider = new AssertionsProvider(stack, 'AssertionProvider'); + + // THEN + expect(stack.resolve(provider.serviceToken)).toEqual({ 'Fn::GetAtt': ['SingletonLambda1488541a7b23466481b69b4408076b81488C0898', 'Arn'] }); + Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { + Handler: 'index.handler', + Timeout: 120, + }); + }); + + describe('addPolicyStatementForSdkCall', () => { + test('default', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const provider = new AssertionsProvider(stack, 'AssertionsProvider'); + provider.addPolicyStatementFromSdkCall('MyService', 'myApi'); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'myservice:MyApi', + Effect: 'Allow', + Resource: '*', + }, + ], + }, + Roles: [{ + Ref: 'SingletonLambda1488541a7b23466481b69b4408076b81ServiceRole4E21F0DA', + }], + }); + }); + + test('prefix different from service name', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const provider = new AssertionsProvider(stack, 'AssertionsProvider'); + provider.addPolicyStatementFromSdkCall('applicationautoscaling', 'myApi'); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'application-autoscaling:MyApi', + Effect: 'Allow', + Resource: '*', + }, + ], + }, + }); + }); + }); + + describe('encode', () => { + test('booleans', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const provider = new AssertionsProvider(stack, 'AssertionsProvider'); + const encoded = provider.encode({ + Key1: true, + Key2: false, + }); + + // THEN + expect(encoded).toEqual({ + Key1: 'TRUE:BOOLEAN', + Key2: 'FALSE:BOOLEAN', + }); + }); + + test('all other values return as usual', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const provider = new AssertionsProvider(stack, 'AssertionsProvider'); + const encoded = provider.encode({ + Key1: 'foo', + Key2: 30, + Key3: ['hello', 'world'], + }); + + // THEN + expect(encoded).toEqual({ + Key1: 'foo', + Key2: 30, + Key3: ['hello', 'world'], + }); + }); + + test('nullish', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const provider = new AssertionsProvider(stack, 'AssertionsProvider'); + + // THEN + expect(provider.encode(undefined)).toBeUndefined(); + expect(provider.encode(null)).toBeNull(); + expect(provider.encode({})).toEqual({}); + }); + }); +}); diff --git a/packages/@aws-cdk/integ-tests/test/assertions/sdk.test.ts b/packages/@aws-cdk/integ-tests/test/assertions/sdk.test.ts new file mode 100644 index 0000000000000..2b54beb326e2d --- /dev/null +++ b/packages/@aws-cdk/integ-tests/test/assertions/sdk.test.ts @@ -0,0 +1,108 @@ +import { Template, Match } from '@aws-cdk/assertions'; +import { App, Stack } from '@aws-cdk/core'; +import { DeployAssert, SdkQuery } from '../../lib/assertions'; + +describe('SdkQuery', () => { + test('default', () => { + // GIVEN + const app = new App(); + const stack = new Stack(app); + const deplossert = new DeployAssert(stack); + + // WHEN + new SdkQuery(deplossert, 'SdkQuery', { + service: 'MyService', + api: 'MyApi', + }); + + + // THEN + const template = Template.fromStack(stack); + template.resourceCountIs('AWS::Lambda::Function', 1); + template.hasResourceProperties('Custom::DeployAssert@SdkCallMyServiceMyApi', { + service: 'MyService', + api: 'MyApi', + parameters: Match.absent(), + }); + }); + + test('parameters', () => { + // GIVEN + const app = new App(); + const stack = new Stack(app); + const deplossert = new DeployAssert(stack); + + // WHEN + new SdkQuery(deplossert, 'SdkQuery', { + service: 'MyService', + api: 'MyApi', + parameters: { + param1: 'val1', + param2: 2, + }, + }); + + + // THEN + const template = Template.fromStack(stack); + template.resourceCountIs('AWS::Lambda::Function', 1); + template.hasResourceProperties('Custom::DeployAssert@SdkCallMyServiceMyApi', { + service: 'MyService', + api: 'MyApi', + parameters: { + param1: 'val1', + param2: 2, + }, + }); + }); + + describe('assertEqual', () => { + test('default', () => { + // GIVEN + const app = new App(); + const stack = new Stack(app); + const deplossert = new DeployAssert(stack); + + // WHEN + const query = new SdkQuery(deplossert, 'SdkQuery', { + service: 'MyService', + api: 'MyApi', + }); + query.assertEqual({ foo: 'bar' }); + + + // THEN + const template = Template.fromStack(stack); + template.hasResourceProperties('Custom::DeployAssert@AssertEquals', { + expected: { foo: 'bar' }, + actual: { + 'Fn::GetAtt': [ + 'DeployAssertSdkQuery94650089', + 'apiCallResponse', + ], + }, + assertionType: 'equals', + }); + }); + + test('multiple asserts to the same query', () => { + // GIVEN + const app = new App(); + const stack = new Stack(app); + const deplossert = new DeployAssert(stack); + + // WHEN + const query = new SdkQuery(deplossert, 'SdkQuery', { + service: 'MyService', + api: 'MyApi', + }); + query.assertEqual({ foo: 'bar' }); + query.assertEqual({ baz: 'zoo' }); + + + // THEN + const template = Template.fromStack(stack); + template.resourceCountIs('Custom::DeployAssert@AssertEquals', 2); + }); + }); +});