From 3ff3fb7c5ec9636022b3046036376c09a3166fb0 Mon Sep 17 00:00:00 2001 From: Cory Hall <43035978+corymhall@users.noreply.github.com> Date: Mon, 16 May 2022 12:48:26 -0400 Subject: [PATCH] feat(integ-tests): enhancements to integ-tests (#20180) This PR contains various enhancements including - `integ-tests` - removed dependency on other CDK libraries (other than core) - API ergonomics improvements - renamed `queryAws` to `awsApiCall` - added some additional methods - Now using `Match` from @aws-cdk/assertions for the assertions provider - `DeployAssert` now creates its own stack - This stack is written to a new IntegManifest property so that it can be treated differently (i.e. don't diff this stack) - Additional assertion types (OBJECT_LIKE) - Refactored assertion results - removed separate results handler in favor of just writing results to a stack output - utility for invoking lambda functions (separate from `awsApiCall`) - `IntegTest` now creates a test case by default. - Added `IntegTestCaseStack` class - `integ-runner` - Updated to handle the results of assertions - When running with update workflow, the assertion stack is only deployed during the "update" deployment - The stack outputs containing the assertion results are are written to a file that the runner can read. I've also converted/added assertions to a couple of existing integration tests - `aws-lambda/test/integ.bundling.ts` - `aws-lambda-destinations/test/integ.destinations.ts` - `aws-stepfunctions-tasks/test/eventbridge/integ.put-events.ts` ---- ### 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* --- .../assertions/lib/helpers-internal/index.ts | 2 + packages/@aws-cdk/assertions/package.json | 5 + .../aws-lambda-destinations/package.json | 1 + ...aultTestDeployAssertCC49E667.template.json | 244 +++++++ .../index.js | 644 ++++++++++++++++++ .../aws-cdk-lambda-destinations.template.json | 22 +- .../test/destinations.integ.snapshot/cdk.out | 2 +- .../destinations.integ.snapshot/integ.json | 11 +- .../destinations.integ.snapshot/manifest.json | 102 ++- .../destinations.integ.snapshot/tree.json | 302 +++++++- .../test/integ.destinations.ts | 46 +- packages/@aws-cdk/aws-lambda/package.json | 1 + ...aultTestDeployAssertAACA0CAF.template.json | 210 ++++++ .../cdk-integ-lambda-bundling.template.json | 30 +- .../test/bundling.integ.snapshot/cdk.out | 2 +- .../test/bundling.integ.snapshot/integ.json | 12 +- .../bundling.integ.snapshot/manifest.json | 112 ++- .../test/bundling.integ.snapshot/tree.json | 264 ++++++- .../aws-lambda/test/integ.bundling.ts | 26 +- .../aws-stepfunctions-tasks/package.json | 1 + .../test/eventbridge/integ.put-events.ts | 21 +- ...aultTestDeployAssert1A6BA3F3.template.json | 198 ++++++ .../index.js | 644 ++++++++++++++++++ ...eventbridge-put-events-integ.template.json | 17 +- .../put-events.integ.snapshot/cdk.out | 2 +- .../put-events.integ.snapshot/integ.json | 11 +- .../put-events.integ.snapshot/manifest.json | 92 ++- .../put-events.integ.snapshot/tree.json | 290 +++++++- .../lib/integ-tests/test-case.ts | 7 + .../schema/cloud-assembly.version.json | 2 +- .../schema/integ.schema.json | 4 + .../lib/runner/integ-test-runner.ts | 73 +- .../integ-runner/lib/workers/common.ts | 40 ++ .../lib/workers/extract/extract_worker.ts | 30 +- .../test/runner/integ-test-runner.test.ts | 4 + .../manifest.json | 2 +- .../manifest.json | 2 +- .../manifest.json | 2 +- packages/@aws-cdk/integ-tests/README.md | 282 +++++++- .../integ-tests/lib/assertions/assertions.ts | 47 +- .../integ-tests/lib/assertions/common.ts | 141 ++++ .../lib/assertions/deploy-assert.ts | 117 ++-- .../integ-tests/lib/assertions/index.ts | 2 + .../integ-tests/lib/assertions/match.ts | 30 + .../providers/lambda-handler/assertion.ts | 147 +++- .../providers/lambda-handler/index.ts | 4 +- .../providers/lambda-handler/sdk.ts | 20 +- .../providers/lambda-handler/types.ts | 41 +- .../providers/lambda-handler/utils.ts | 13 + .../lib/assertions/providers/provider.ts | 205 +++++- .../integ-tests/lib/assertions/sdk.ts | 213 +++++- packages/@aws-cdk/integ-tests/lib/index.ts | 1 + .../@aws-cdk/integ-tests/lib/test-case.ts | 120 +++- packages/@aws-cdk/integ-tests/package.json | 27 +- .../integ-tests/rosetta/default.ts-fixture | 18 +- .../test/assertions/assertions.test.ts | 48 -- .../test/assertions/deploy-assert.test.ts | 145 ++-- .../lambda-handler/assertion.test.ts | 218 +++++- .../providers/lambda-handler/results.test.ts | 59 -- .../providers/lambda-handler/sdk.test.ts | 14 +- .../assertions/providers/provider.test.ts | 118 +++- .../integ-tests/test/assertions/sdk.test.ts | 256 ++++++- .../test/manifest-synthesizer.test.ts | 83 ++- packages/aws-cdk-lib/package.json | 3 + 64 files changed, 5275 insertions(+), 577 deletions(-) create mode 100644 packages/@aws-cdk/assertions/lib/helpers-internal/index.ts create mode 100644 packages/@aws-cdk/aws-lambda-destinations/test/destinations.integ.snapshot/DestinationsDefaultTestDeployAssertCC49E667.template.json create mode 100644 packages/@aws-cdk/aws-lambda-destinations/test/destinations.integ.snapshot/asset.1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b.bundle/index.js create mode 100644 packages/@aws-cdk/aws-lambda/test/bundling.integ.snapshot/BundlingDefaultTestDeployAssertAACA0CAF.template.json create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/put-events.integ.snapshot/PutEventsDefaultTestDeployAssert1A6BA3F3.template.json create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/put-events.integ.snapshot/asset.1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b.bundle/index.js create mode 100644 packages/@aws-cdk/integ-tests/lib/assertions/common.ts create mode 100644 packages/@aws-cdk/integ-tests/lib/assertions/match.ts create mode 100644 packages/@aws-cdk/integ-tests/lib/assertions/providers/lambda-handler/utils.ts delete mode 100644 packages/@aws-cdk/integ-tests/test/assertions/assertions.test.ts delete mode 100644 packages/@aws-cdk/integ-tests/test/assertions/providers/lambda-handler/results.test.ts diff --git a/packages/@aws-cdk/assertions/lib/helpers-internal/index.ts b/packages/@aws-cdk/assertions/lib/helpers-internal/index.ts new file mode 100644 index 0000000000000..7fbde68f0a98c --- /dev/null +++ b/packages/@aws-cdk/assertions/lib/helpers-internal/index.ts @@ -0,0 +1,2 @@ +export * from '../match'; +export * from '../matcher'; diff --git a/packages/@aws-cdk/assertions/package.json b/packages/@aws-cdk/assertions/package.json index 58e29d74546da..4de6a5b604427 100644 --- a/packages/@aws-cdk/assertions/package.json +++ b/packages/@aws-cdk/assertions/package.json @@ -18,6 +18,11 @@ "build+extract": "yarn build && yarn rosetta:extract", "build+test+extract": "yarn build+test && yarn rosetta:extract" }, + "ubergen": { + "exports": { + "./lib/helpers-internal": "./lib/helpers-internal/index.js" + } + }, "jsii": { "outdir": "dist", "diagnostics": { diff --git a/packages/@aws-cdk/aws-lambda-destinations/package.json b/packages/@aws-cdk/aws-lambda-destinations/package.json index 632f9c27f4474..d3871ee4288c2 100644 --- a/packages/@aws-cdk/aws-lambda-destinations/package.json +++ b/packages/@aws-cdk/aws-lambda-destinations/package.json @@ -72,6 +72,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assertions": "0.0.0", + "@aws-cdk/integ-tests": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/integ-runner": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-lambda-destinations/test/destinations.integ.snapshot/DestinationsDefaultTestDeployAssertCC49E667.template.json b/packages/@aws-cdk/aws-lambda-destinations/test/destinations.integ.snapshot/DestinationsDefaultTestDeployAssertCC49E667.template.json new file mode 100644 index 0000000000000..57bd8ef6a2df0 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-destinations/test/destinations.integ.snapshot/DestinationsDefaultTestDeployAssertCC49E667.template.json @@ -0,0 +1,244 @@ +{ + "Resources": { + "LambdaInvoked12df417a1b74909abb3ea643735a310": { + "Type": "Custom::DeployAssert@SdkCallLambdainvoke", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "SingletonFunction1488541a7b23466481b69b4408076b81HandlerCD40AE9F", + "Arn" + ] + }, + "service": "Lambda", + "api": "invoke", + "parameters": { + "FunctionName": { + "Fn::ImportValue": "aws-cdk-lambda-destinations:ExportsOutputRefSnsSqsC4810B27404A5AFF" + }, + "InvocationType": "Event", + "Payload": "{\"status\":\"OK\"}" + }, + "flattenResponse": "false", + "salt": "1651691787842" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "LambdaInvoked12df417a1b74909abb3ea643735a310InvokeF590C289": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::ImportValue": "aws-cdk-lambda-destinations:ExportsOutputRefSnsSqsC4810B27404A5AFF" + }, + "Principal": { + "Fn::GetAtt": [ + "SingletonFunction1488541a7b23466481b69b4408076b81Role37ABCE73", + "Arn" + ] + } + } + }, + "AwsApiCallSQSreceiveMessage": { + "Type": "Custom::DeployAssert@SdkCallSQSreceiveMessage", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "SingletonFunction1488541a7b23466481b69b4408076b81HandlerCD40AE9F", + "Arn" + ] + }, + "service": "SQS", + "api": "receiveMessage", + "parameters": { + "QueueUrl": { + "Fn::ImportValue": "aws-cdk-lambda-destinations:ExportsOutputRefQueue4A7E3555425E8BD3" + }, + "WaitTimeSeconds": 20 + }, + "flattenResponse": "true", + "salt": "1651691787842" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "AwsApiCallSQSreceiveMessageAssertEqualsSQSreceiveMessage56120636": { + "Type": "Custom::DeployAssert@AssertEquals", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "SingletonFunction1488541a7b23466481b69b4408076b81HandlerCD40AE9F", + "Arn" + ] + }, + "actual": { + "Fn::GetAtt": [ + "AwsApiCallSQSreceiveMessage", + "apiCallResponse.Messages.0.Body" + ] + }, + "expected": "{\"$ObjectLike\":{\"requestContext\":{\"condition\":\"Success\"},\"requestPayload\":{\"status\":\"OK\"},\"responseContext\":{\"statusCode\":200},\"responsePayload\":\"success\"}}", + "salt": "1651691787843" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "SingletonFunction1488541a7b23466481b69b4408076b81Role37ABCE73": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ] + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + } + ], + "Policies": [ + { + "PolicyName": "Inline", + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "lambda:Invoke" + ], + "Effect": "Allow", + "Resource": [ + "*" + ] + }, + { + "Action": [ + "lambda:InvokeFunction" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":lambda:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":function:", + { + "Fn::ImportValue": "aws-cdk-lambda-destinations:ExportsOutputRefSnsSqsC4810B27404A5AFF" + } + ] + ] + } + ] + }, + { + "Action": [ + "sqs:ReceiveMessage" + ], + "Effect": "Allow", + "Resource": [ + "*" + ] + } + ] + } + } + ] + } + }, + "SingletonFunction1488541a7b23466481b69b4408076b81HandlerCD40AE9F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Runtime": "nodejs14.x", + "Code": { + "S3Bucket": { + "Ref": "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bS3BucketF7210344" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bS3VersionKey1E71961C" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bS3VersionKey1E71961C" + } + ] + } + ] + } + ] + ] + } + }, + "Timeout": 120, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "SingletonFunction1488541a7b23466481b69b4408076b81Role37ABCE73", + "Arn" + ] + } + } + } + }, + "Outputs": { + "AssertionResultsAssertEqualsSQSreceiveMessage": { + "Value": { + "Fn::GetAtt": [ + "AwsApiCallSQSreceiveMessageAssertEqualsSQSreceiveMessage56120636", + "data" + ] + } + } + }, + "Parameters": { + "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bS3BucketF7210344": { + "Type": "String", + "Description": "S3 bucket for asset \"1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b\"" + }, + "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bS3VersionKey1E71961C": { + "Type": "String", + "Description": "S3 key for asset version \"1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b\"" + }, + "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bArtifactHash4F8362F2": { + "Type": "String", + "Description": "Artifact hash for asset \"1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-destinations/test/destinations.integ.snapshot/asset.1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b.bundle/index.js b/packages/@aws-cdk/aws-lambda-destinations/test/destinations.integ.snapshot/asset.1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b.bundle/index.js new file mode 100644 index 0000000000000..32e3e2c1e5a95 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda-destinations/test/destinations.integ.snapshot/asset.1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b.bundle/index.js @@ -0,0 +1,644 @@ +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getOwnPropSymbols = Object.getOwnPropertySymbols; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __propIsEnum = Object.prototype.propertyIsEnumerable; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __spreadValues = (a, b) => { + for (var prop in b || (b = {})) + if (__hasOwnProp.call(b, prop)) + __defNormalProp(a, prop, b[prop]); + if (__getOwnPropSymbols) + for (var prop of __getOwnPropSymbols(b)) { + if (__propIsEnum.call(b, prop)) + __defNormalProp(a, prop, b[prop]); + } + return a; +}; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod)); +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// lib/assertions/providers/lambda-handler/index.ts +var lambda_handler_exports = {}; +__export(lambda_handler_exports, { + handler: () => handler +}); +module.exports = __toCommonJS(lambda_handler_exports); + +// ../assertions/lib/matcher.ts +var Matcher = class { + static isMatcher(x) { + return x && x instanceof Matcher; + } +}; +var MatchResult = class { + constructor(target) { + this.failures = []; + this.captures = /* @__PURE__ */ new Map(); + this.finalized = false; + this.target = target; + } + push(matcher, path, message) { + return this.recordFailure({ matcher, path, message }); + } + recordFailure(failure) { + this.failures.push(failure); + return this; + } + hasFailed() { + return this.failures.length !== 0; + } + get failCount() { + return this.failures.length; + } + compose(id, inner) { + const innerF = inner.failures; + this.failures.push(...innerF.map((f) => { + return { path: [id, ...f.path], message: f.message, matcher: f.matcher }; + })); + inner.captures.forEach((vals, capture) => { + vals.forEach((value) => this.recordCapture({ capture, value })); + }); + return this; + } + finished() { + if (this.finalized) { + return this; + } + if (this.failCount === 0) { + this.captures.forEach((vals, cap) => cap._captured.push(...vals)); + } + this.finalized = true; + return this; + } + toHumanStrings() { + return this.failures.map((r) => { + const loc = r.path.length === 0 ? "" : ` at ${r.path.join("")}`; + return "" + r.message + loc + ` (using ${r.matcher.name} matcher)`; + }); + } + recordCapture(options) { + let values = this.captures.get(options.capture); + if (values === void 0) { + values = []; + } + values.push(options.value); + this.captures.set(options.capture, values); + } +}; + +// ../assertions/lib/private/matchers/absent.ts +var AbsentMatch = class extends Matcher { + constructor(name) { + super(); + this.name = name; + } + test(actual) { + const result = new MatchResult(actual); + if (actual !== void 0) { + result.recordFailure({ + matcher: this, + path: [], + message: `Received ${actual}, but key should be absent` + }); + } + return result; + } +}; + +// ../assertions/lib/private/type.ts +function getType(obj) { + return Array.isArray(obj) ? "array" : typeof obj; +} + +// ../assertions/lib/match.ts +var Match = class { + static absent() { + return new AbsentMatch("absent"); + } + static arrayWith(pattern) { + return new ArrayMatch("arrayWith", pattern); + } + static arrayEquals(pattern) { + return new ArrayMatch("arrayEquals", pattern, { subsequence: false }); + } + static exact(pattern) { + return new LiteralMatch("exact", pattern, { partialObjects: false }); + } + static objectLike(pattern) { + return new ObjectMatch("objectLike", pattern); + } + static objectEquals(pattern) { + return new ObjectMatch("objectEquals", pattern, { partial: false }); + } + static not(pattern) { + return new NotMatch("not", pattern); + } + static serializedJson(pattern) { + return new SerializedJson("serializedJson", pattern); + } + static anyValue() { + return new AnyMatch("anyValue"); + } + static stringLikeRegexp(pattern) { + return new StringLikeRegexpMatch("stringLikeRegexp", pattern); + } +}; +var LiteralMatch = class extends Matcher { + constructor(name, pattern, options = {}) { + super(); + this.name = name; + this.pattern = pattern; + this.partialObjects = options.partialObjects ?? false; + if (Matcher.isMatcher(this.pattern)) { + throw new Error("LiteralMatch cannot directly contain another matcher. Remove the top-level matcher or nest it more deeply."); + } + } + test(actual) { + if (Array.isArray(this.pattern)) { + return new ArrayMatch(this.name, this.pattern, { subsequence: false, partialObjects: this.partialObjects }).test(actual); + } + if (typeof this.pattern === "object") { + return new ObjectMatch(this.name, this.pattern, { partial: this.partialObjects }).test(actual); + } + const result = new MatchResult(actual); + if (typeof this.pattern !== typeof actual) { + result.recordFailure({ + matcher: this, + path: [], + message: `Expected type ${typeof this.pattern} but received ${getType(actual)}` + }); + return result; + } + if (actual !== this.pattern) { + result.recordFailure({ + matcher: this, + path: [], + message: `Expected ${this.pattern} but received ${actual}` + }); + } + return result; + } +}; +var ArrayMatch = class extends Matcher { + constructor(name, pattern, options = {}) { + super(); + this.name = name; + this.pattern = pattern; + this.subsequence = options.subsequence ?? true; + this.partialObjects = options.partialObjects ?? false; + } + test(actual) { + if (!Array.isArray(actual)) { + return new MatchResult(actual).recordFailure({ + matcher: this, + path: [], + message: `Expected type array but received ${getType(actual)}` + }); + } + if (!this.subsequence && this.pattern.length !== actual.length) { + return new MatchResult(actual).recordFailure({ + matcher: this, + path: [], + message: `Expected array of length ${this.pattern.length} but received ${actual.length}` + }); + } + let patternIdx = 0; + let actualIdx = 0; + const result = new MatchResult(actual); + while (patternIdx < this.pattern.length && actualIdx < actual.length) { + const patternElement = this.pattern[patternIdx]; + const matcher = Matcher.isMatcher(patternElement) ? patternElement : new LiteralMatch(this.name, patternElement, { partialObjects: this.partialObjects }); + const matcherName = matcher.name; + if (this.subsequence && (matcherName == "absent" || matcherName == "anyValue")) { + throw new Error(`The Matcher ${matcherName}() cannot be nested within arrayWith()`); + } + const innerResult = matcher.test(actual[actualIdx]); + if (!this.subsequence || !innerResult.hasFailed()) { + result.compose(`[${actualIdx}]`, innerResult); + patternIdx++; + actualIdx++; + } else { + actualIdx++; + } + } + for (; patternIdx < this.pattern.length; patternIdx++) { + const pattern = this.pattern[patternIdx]; + const element = Matcher.isMatcher(pattern) || typeof pattern === "object" ? " " : ` [${pattern}] `; + result.recordFailure({ + matcher: this, + path: [], + message: `Missing element${element}at pattern index ${patternIdx}` + }); + } + return result; + } +}; +var ObjectMatch = class extends Matcher { + constructor(name, pattern, options = {}) { + super(); + this.name = name; + this.pattern = pattern; + this.partial = options.partial ?? true; + } + test(actual) { + if (typeof actual !== "object" || Array.isArray(actual)) { + return new MatchResult(actual).recordFailure({ + matcher: this, + path: [], + message: `Expected type object but received ${getType(actual)}` + }); + } + const result = new MatchResult(actual); + if (!this.partial) { + for (const a of Object.keys(actual)) { + if (!(a in this.pattern)) { + result.recordFailure({ + matcher: this, + path: [`/${a}`], + message: "Unexpected key" + }); + } + } + } + for (const [patternKey, patternVal] of Object.entries(this.pattern)) { + if (!(patternKey in actual) && !(patternVal instanceof AbsentMatch)) { + result.recordFailure({ + matcher: this, + path: [`/${patternKey}`], + message: `Missing key '${patternKey}' among {${Object.keys(actual).join(",")}}` + }); + continue; + } + const matcher = Matcher.isMatcher(patternVal) ? patternVal : new LiteralMatch(this.name, patternVal, { partialObjects: this.partial }); + const inner = matcher.test(actual[patternKey]); + result.compose(`/${patternKey}`, inner); + } + return result; + } +}; +var SerializedJson = class extends Matcher { + constructor(name, pattern) { + super(); + this.name = name; + this.pattern = pattern; + } + test(actual) { + const result = new MatchResult(actual); + if (getType(actual) !== "string") { + result.recordFailure({ + matcher: this, + path: [], + message: `Expected JSON as a string but found ${getType(actual)}` + }); + return result; + } + let parsed; + try { + parsed = JSON.parse(actual); + } catch (err) { + if (err instanceof SyntaxError) { + result.recordFailure({ + matcher: this, + path: [], + message: `Invalid JSON string: ${actual}` + }); + return result; + } else { + throw err; + } + } + const matcher = Matcher.isMatcher(this.pattern) ? this.pattern : new LiteralMatch(this.name, this.pattern); + const innerResult = matcher.test(parsed); + result.compose(`(${this.name})`, innerResult); + return result; + } +}; +var NotMatch = class extends Matcher { + constructor(name, pattern) { + super(); + this.name = name; + this.pattern = pattern; + } + test(actual) { + const matcher = Matcher.isMatcher(this.pattern) ? this.pattern : new LiteralMatch(this.name, this.pattern); + const innerResult = matcher.test(actual); + const result = new MatchResult(actual); + if (innerResult.failCount === 0) { + result.recordFailure({ + matcher: this, + path: [], + message: `Found unexpected match: ${JSON.stringify(actual, void 0, 2)}` + }); + } + return result; + } +}; +var AnyMatch = class extends Matcher { + constructor(name) { + super(); + this.name = name; + } + test(actual) { + const result = new MatchResult(actual); + if (actual == null) { + result.recordFailure({ + matcher: this, + path: [], + message: "Expected a value but found none" + }); + } + return result; + } +}; +var StringLikeRegexpMatch = class extends Matcher { + constructor(name, pattern) { + super(); + this.name = name; + this.pattern = pattern; + } + test(actual) { + const result = new MatchResult(actual); + const regex = new RegExp(this.pattern, "gm"); + if (typeof actual !== "string") { + result.recordFailure({ + matcher: this, + path: [], + message: `Expected a string, but got '${typeof actual}'` + }); + } + if (!regex.test(actual)) { + result.recordFailure({ + matcher: this, + path: [], + message: `String '${actual}' did not match pattern '${this.pattern}'` + }); + } + return result; + } +}; + +// lib/assertions/providers/lambda-handler/base.ts +var https = __toESM(require("https")); +var url = __toESM(require("url")); +var CustomResourceHandler = class { + constructor(event, context) { + this.event = event; + this.context = context; + this.timedOut = false; + 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); + } + async handle() { + try { + console.log(`Event: ${JSON.stringify(this.event)}`); + const response = await this.processEvent(this.event.ResourceProperties); + 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); + } + } + respond(response) { + if (this.timedOut) { + return; + } + const cfResponse = { + 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 request2 = https.request(requestOptions, resolve); + request2.on("error", reject); + request2.write(responseBody); + request2.end(); + } catch (e) { + reject(e); + } + }); + } +}; +function extractPhysicalResourceId(event) { + switch (event.RequestType) { + case "Create": + return event.LogicalResourceId; + case "Update": + case "Delete": + return event.PhysicalResourceId; + } +} + +// lib/assertions/providers/lambda-handler/assertion.ts +var AssertionHandler = class extends CustomResourceHandler { + async processEvent(request2) { + let actual = decodeCall(request2.actual); + const expected = decodeCall(request2.expected); + let result; + const matcher = new MatchCreator(expected).getMatcher(); + console.log(`Testing equality between ${JSON.stringify(request2.actual)} and ${JSON.stringify(request2.expected)}`); + const matchResult = matcher.test(actual); + matchResult.finished(); + if (matchResult.hasFailed()) { + result = { + data: JSON.stringify({ + status: "fail", + message: [ + ...matchResult.toHumanStrings(), + JSON.stringify(matchResult.target, void 0, 2) + ].join("\n") + }) + }; + } else { + result = { + data: JSON.stringify({ + status: "pass" + }) + }; + } + return result; + } +}; +var MatchCreator = class { + constructor(obj) { + switch (Object.keys(obj)[0]) { + case "$ObjectLike": + this.type = "objectLike"; + this.parsedObj = obj.$ObjectLike; + break; + case "$ArrayWith": + this.type = "arrayWith"; + this.parsedObj = obj.$ArrayWith; + break; + case "$Exact": + this.type = "exact"; + this.parsedObj = obj.$Exact; + break; + case "$StringLike": + this.type = "stringLikeRegexp"; + this.parsedObj = obj.$StringLike; + break; + default: + this.type = "exact"; + this.parsedObj = obj; + } + } + getMatcher() { + try { + const final = JSON.parse(JSON.stringify(this.parsedObj), function(_k, v) { + const nested = Object.keys(v)[0]; + switch (nested) { + case "$ArrayWith": + return Match.arrayWith(v[nested]); + case "$ObjectLike": + return Match.objectLike(v[nested]); + case "$StringLike": + return Match.stringLikeRegexp(v[nested]); + default: + return v; + } + }); + return Match[this.type](final); + } catch { + return Match[this.type](this.parsedObj); + } + } +}; +function decodeCall(call) { + if (!call) { + return void 0; + } + try { + const parsed = JSON.parse(call); + return parsed; + } catch (e) { + return call; + } +} + +// lib/assertions/providers/lambda-handler/results.ts +var ResultsCollectionHandler = class extends CustomResourceHandler { + async processEvent(request2) { + const reduced = request2.assertionResults.reduce((agg, result, idx) => { + const msg = result.status === "pass" ? "pass" : `fail - ${result.message}`; + return `${agg} +Test${idx}: ${msg}`; + }, "").trim(); + return { message: reduced }; + } +}; + +// lib/assertions/providers/lambda-handler/utils.ts +function decode(object) { + return JSON.parse(JSON.stringify(object), (_k, v) => { + switch (v) { + case "TRUE:BOOLEAN": + return true; + case "FALSE:BOOLEAN": + return false; + default: + return v; + } + }); +} + +// lib/assertions/providers/lambda-handler/sdk.ts +function flatten(object) { + return Object.assign({}, ...function _flatten(child, path = []) { + 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)); +} +var AwsApiCallHandler = class extends CustomResourceHandler { + async processEvent(request2) { + const AWS = require("aws-sdk"); + console.log(`AWS SDK VERSION: ${AWS.VERSION}`); + const service = new AWS[request2.service](); + const response = await service[request2.api](request2.parameters && decode(request2.parameters)).promise(); + console.log(`SDK response received ${JSON.stringify(response)}`); + delete response.ResponseMetadata; + const respond = { + apiCallResponse: response + }; + const flatData = __spreadValues({}, flatten(respond)); + return request2.flattenResponse === "true" ? flatData : respond; + } +}; + +// lib/assertions/providers/lambda-handler/types.ts +var ASSERT_RESOURCE_TYPE = "Custom::DeployAssert@AssertEquals"; +var RESULTS_RESOURCE_TYPE = "Custom::DeployAssert@ResultsCollection"; +var SDK_RESOURCE_TYPE_PREFIX = "Custom::DeployAssert@SdkCall"; + +// lib/assertions/providers/lambda-handler/index.ts +async function handler(event, context) { + const provider = createResourceHandler(event, context); + await provider.handle(); +} +function createResourceHandler(event, context) { + if (event.ResourceType.startsWith(SDK_RESOURCE_TYPE_PREFIX)) { + return new AwsApiCallHandler(event, context); + } + switch (event.ResourceType) { + case ASSERT_RESOURCE_TYPE: + return new AssertionHandler(event, context); + case RESULTS_RESOURCE_TYPE: + return new ResultsCollectionHandler(event, context); + default: + throw new Error(`Unsupported resource type "${event.ResourceType}`); + } +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + handler +}); diff --git a/packages/@aws-cdk/aws-lambda-destinations/test/destinations.integ.snapshot/aws-cdk-lambda-destinations.template.json b/packages/@aws-cdk/aws-lambda-destinations/test/destinations.integ.snapshot/aws-cdk-lambda-destinations.template.json index 87d704ed4327a..84e4ea60c1c0e 100644 --- a/packages/@aws-cdk/aws-lambda-destinations/test/destinations.integ.snapshot/aws-cdk-lambda-destinations.template.json +++ b/packages/@aws-cdk/aws-lambda-destinations/test/destinations.integ.snapshot/aws-cdk-lambda-destinations.template.json @@ -80,7 +80,7 @@ "Type": "AWS::Lambda::Function", "Properties": { "Code": { - "ZipFile": "exports.handler = async (event) => {\n if (event === 'OK') return 'success';\n throw new Error('failure');\n };" + "ZipFile": "exports.handler = async (event) => {\n if (event.status === 'OK') return 'success';\n throw new Error('failure');\n };" }, "Role": { "Fn::GetAtt": [ @@ -281,7 +281,7 @@ "Type": "AWS::Lambda::Function", "Properties": { "Code": { - "ZipFile": "exports.handler = async (event) => {\n if (event === 'OK') return 'success';\n throw new Error('failure');\n };" + "ZipFile": "exports.handler = async (event) => {\n if (event.status === 'OK') return 'success';\n throw new Error('failure');\n };" }, "Role": { "Fn::GetAtt": [ @@ -391,5 +391,23 @@ "MaximumRetryAttempts": 0 } } + }, + "Outputs": { + "ExportsOutputRefSnsSqsC4810B27404A5AFF": { + "Value": { + "Ref": "SnsSqsC4810B27" + }, + "Export": { + "Name": "aws-cdk-lambda-destinations:ExportsOutputRefSnsSqsC4810B27404A5AFF" + } + }, + "ExportsOutputRefQueue4A7E3555425E8BD3": { + "Value": { + "Ref": "Queue4A7E3555" + }, + "Export": { + "Name": "aws-cdk-lambda-destinations:ExportsOutputRefQueue4A7E3555425E8BD3" + } + } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-destinations/test/destinations.integ.snapshot/cdk.out b/packages/@aws-cdk/aws-lambda-destinations/test/destinations.integ.snapshot/cdk.out index 90bef2e09ad39..ccdfc1ff96a9d 100644 --- a/packages/@aws-cdk/aws-lambda-destinations/test/destinations.integ.snapshot/cdk.out +++ b/packages/@aws-cdk/aws-lambda-destinations/test/destinations.integ.snapshot/cdk.out @@ -1 +1 @@ -{"version":"17.0.0"} \ No newline at end of file +{"version":"19.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-destinations/test/destinations.integ.snapshot/integ.json b/packages/@aws-cdk/aws-lambda-destinations/test/destinations.integ.snapshot/integ.json index 20b73bceabb1f..96acfc50bc0cf 100644 --- a/packages/@aws-cdk/aws-lambda-destinations/test/destinations.integ.snapshot/integ.json +++ b/packages/@aws-cdk/aws-lambda-destinations/test/destinations.integ.snapshot/integ.json @@ -1,14 +1,11 @@ { - "version": "18.0.0", + "version": "19.0.0", "testCases": { - "aws-lambda-destinations/test/integ.destinations": { + "Destinations/DefaultTest": { "stacks": [ "aws-cdk-lambda-destinations" ], - "diffAssets": false, - "stackUpdateWorkflow": true + "assertionStack": "DestinationsDefaultTestDeployAssertCC49E667" } - }, - "synthContext": {}, - "enableLookups": false + } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-destinations/test/destinations.integ.snapshot/manifest.json b/packages/@aws-cdk/aws-lambda-destinations/test/destinations.integ.snapshot/manifest.json index 2684d0546624f..87df11e84970f 100644 --- a/packages/@aws-cdk/aws-lambda-destinations/test/destinations.integ.snapshot/manifest.json +++ b/packages/@aws-cdk/aws-lambda-destinations/test/destinations.integ.snapshot/manifest.json @@ -1,5 +1,5 @@ { - "version": "17.0.0", + "version": "19.0.0", "artifacts": { "Tree": { "type": "cdk:tree", @@ -104,9 +104,109 @@ "type": "aws:cdk:logicalId", "data": "MySpecialAliasEventInvokeConfig05FF4E2F" } + ], + "/aws-cdk-lambda-destinations/Exports/Output{\"Ref\":\"SnsSqsC4810B27\"}": [ + { + "type": "aws:cdk:logicalId", + "data": "ExportsOutputRefSnsSqsC4810B27404A5AFF" + } + ], + "/aws-cdk-lambda-destinations/Exports/Output{\"Ref\":\"Queue4A7E3555\"}": [ + { + "type": "aws:cdk:logicalId", + "data": "ExportsOutputRefQueue4A7E3555425E8BD3" + } ] }, "displayName": "aws-cdk-lambda-destinations" + }, + "DestinationsDefaultTestDeployAssertCC49E667": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "DestinationsDefaultTestDeployAssertCC49E667.template.json", + "validateOnSynth": false + }, + "dependencies": [ + "aws-cdk-lambda-destinations" + ], + "metadata": { + "/Destinations/DefaultTest/DeployAssert": [ + { + "type": "aws:cdk:asset", + "data": { + "path": "asset.1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b.bundle", + "id": "1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b", + "packaging": "zip", + "sourceHash": "1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b", + "s3BucketParameter": "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bS3BucketF7210344", + "s3KeyParameter": "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bS3VersionKey1E71961C", + "artifactHashParameter": "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bArtifactHash4F8362F2" + } + } + ], + "/Destinations/DefaultTest/DeployAssert/Default/LambdaInvoked12df417a1b74909abb3ea643735a310/Default/Default": [ + { + "type": "aws:cdk:logicalId", + "data": "LambdaInvoked12df417a1b74909abb3ea643735a310" + } + ], + "/Destinations/DefaultTest/DeployAssert/Default/LambdaInvoked12df417a1b74909abb3ea643735a310/Invoke": [ + { + "type": "aws:cdk:logicalId", + "data": "LambdaInvoked12df417a1b74909abb3ea643735a310InvokeF590C289" + } + ], + "/Destinations/DefaultTest/DeployAssert/Default/AwsApiCallSQSreceiveMessage/Default/Default": [ + { + "type": "aws:cdk:logicalId", + "data": "AwsApiCallSQSreceiveMessage" + } + ], + "/Destinations/DefaultTest/DeployAssert/Default/AwsApiCallSQSreceiveMessage/AssertEqualsSQSreceiveMessage/Default/Default": [ + { + "type": "aws:cdk:logicalId", + "data": "AwsApiCallSQSreceiveMessageAssertEqualsSQSreceiveMessage56120636" + } + ], + "/Destinations/DefaultTest/DeployAssert/Default/AwsApiCallSQSreceiveMessage/AssertEqualsSQSreceiveMessage/AssertionResults": [ + { + "type": "aws:cdk:logicalId", + "data": "AssertionResultsAssertEqualsSQSreceiveMessage" + } + ], + "/Destinations/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Role": [ + { + "type": "aws:cdk:logicalId", + "data": "SingletonFunction1488541a7b23466481b69b4408076b81Role37ABCE73" + } + ], + "/Destinations/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Handler": [ + { + "type": "aws:cdk:logicalId", + "data": "SingletonFunction1488541a7b23466481b69b4408076b81HandlerCD40AE9F" + } + ], + "/Destinations/DefaultTest/DeployAssert/AssetParameters/1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b/S3Bucket": [ + { + "type": "aws:cdk:logicalId", + "data": "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bS3BucketF7210344" + } + ], + "/Destinations/DefaultTest/DeployAssert/AssetParameters/1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b/S3VersionKey": [ + { + "type": "aws:cdk:logicalId", + "data": "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bS3VersionKey1E71961C" + } + ], + "/Destinations/DefaultTest/DeployAssert/AssetParameters/1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b/ArtifactHash": [ + { + "type": "aws:cdk:logicalId", + "data": "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bArtifactHash4F8362F2" + } + ] + }, + "displayName": "Destinations/DefaultTest/DeployAssert" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda-destinations/test/destinations.integ.snapshot/tree.json b/packages/@aws-cdk/aws-lambda-destinations/test/destinations.integ.snapshot/tree.json index 60299ae021368..21deb427e0113 100644 --- a/packages/@aws-cdk/aws-lambda-destinations/test/destinations.integ.snapshot/tree.json +++ b/packages/@aws-cdk/aws-lambda-destinations/test/destinations.integ.snapshot/tree.json @@ -175,7 +175,7 @@ "aws:cdk:cloudformation:type": "AWS::Lambda::Function", "aws:cdk:cloudformation:props": { "code": { - "zipFile": "exports.handler = async (event) => {\n if (event === 'OK') return 'success';\n throw new Error('failure');\n };" + "zipFile": "exports.handler = async (event) => {\n if (event.status === 'OK') return 'success';\n throw new Error('failure');\n };" }, "role": { "Fn::GetAtt": [ @@ -495,7 +495,7 @@ "aws:cdk:cloudformation:type": "AWS::Lambda::Function", "aws:cdk:cloudformation:props": { "code": { - "zipFile": "exports.handler = async (event) => {\n if (event === 'OK') return 'success';\n throw new Error('failure');\n };" + "zipFile": "exports.handler = async (event) => {\n if (event.status === 'OK') return 'success';\n throw new Error('failure');\n };" }, "role": { "Fn::GetAtt": [ @@ -681,12 +681,310 @@ "fqn": "@aws-cdk/aws-lambda.Alias", "version": "0.0.0" } + }, + "Exports": { + "id": "Exports", + "path": "aws-cdk-lambda-destinations/Exports", + "children": { + "Output{\"Ref\":\"SnsSqsC4810B27\"}": { + "id": "Output{\"Ref\":\"SnsSqsC4810B27\"}", + "path": "aws-cdk-lambda-destinations/Exports/Output{\"Ref\":\"SnsSqsC4810B27\"}", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnOutput", + "version": "0.0.0" + } + }, + "Output{\"Ref\":\"Queue4A7E3555\"}": { + "id": "Output{\"Ref\":\"Queue4A7E3555\"}", + "path": "aws-cdk-lambda-destinations/Exports/Output{\"Ref\":\"Queue4A7E3555\"}", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnOutput", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Construct", + "version": "0.0.0" + } } }, "constructInfo": { "fqn": "@aws-cdk/core.Stack", "version": "0.0.0" } + }, + "Destinations": { + "id": "Destinations", + "path": "Destinations", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "Destinations/DefaultTest", + "children": { + "DeployAssert": { + "id": "DeployAssert", + "path": "Destinations/DefaultTest/DeployAssert", + "children": { + "Default": { + "id": "Default", + "path": "Destinations/DefaultTest/DeployAssert/Default", + "children": { + "LambdaInvoked12df417a1b74909abb3ea643735a310": { + "id": "LambdaInvoked12df417a1b74909abb3ea643735a310", + "path": "Destinations/DefaultTest/DeployAssert/Default/LambdaInvoked12df417a1b74909abb3ea643735a310", + "children": { + "SdkProvider": { + "id": "SdkProvider", + "path": "Destinations/DefaultTest/DeployAssert/Default/LambdaInvoked12df417a1b74909abb3ea643735a310/SdkProvider", + "children": { + "AssertionsProvider": { + "id": "AssertionsProvider", + "path": "Destinations/DefaultTest/DeployAssert/Default/LambdaInvoked12df417a1b74909abb3ea643735a310/SdkProvider/AssertionsProvider", + "constructInfo": { + "fqn": "@aws-cdk/core.Construct", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.AssertionsProvider", + "version": "0.0.0" + } + }, + "Default": { + "id": "Default", + "path": "Destinations/DefaultTest/DeployAssert/Default/LambdaInvoked12df417a1b74909abb3ea643735a310/Default", + "children": { + "Default": { + "id": "Default", + "path": "Destinations/DefaultTest/DeployAssert/Default/LambdaInvoked12df417a1b74909abb3ea643735a310/Default/Default", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.CustomResource", + "version": "0.0.0" + } + }, + "Invoke": { + "id": "Invoke", + "path": "Destinations/DefaultTest/DeployAssert/Default/LambdaInvoked12df417a1b74909abb3ea643735a310/Invoke", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.LambdaInvokeFunction", + "version": "0.0.0" + } + }, + "AwsApiCallSQSreceiveMessage": { + "id": "AwsApiCallSQSreceiveMessage", + "path": "Destinations/DefaultTest/DeployAssert/Default/AwsApiCallSQSreceiveMessage", + "children": { + "SdkProvider": { + "id": "SdkProvider", + "path": "Destinations/DefaultTest/DeployAssert/Default/AwsApiCallSQSreceiveMessage/SdkProvider", + "children": { + "AssertionsProvider": { + "id": "AssertionsProvider", + "path": "Destinations/DefaultTest/DeployAssert/Default/AwsApiCallSQSreceiveMessage/SdkProvider/AssertionsProvider", + "constructInfo": { + "fqn": "@aws-cdk/core.Construct", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.AssertionsProvider", + "version": "0.0.0" + } + }, + "Default": { + "id": "Default", + "path": "Destinations/DefaultTest/DeployAssert/Default/AwsApiCallSQSreceiveMessage/Default", + "children": { + "Default": { + "id": "Default", + "path": "Destinations/DefaultTest/DeployAssert/Default/AwsApiCallSQSreceiveMessage/Default/Default", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.CustomResource", + "version": "0.0.0" + } + }, + "AssertEqualsSQSreceiveMessage": { + "id": "AssertEqualsSQSreceiveMessage", + "path": "Destinations/DefaultTest/DeployAssert/Default/AwsApiCallSQSreceiveMessage/AssertEqualsSQSreceiveMessage", + "children": { + "AssertionProvider": { + "id": "AssertionProvider", + "path": "Destinations/DefaultTest/DeployAssert/Default/AwsApiCallSQSreceiveMessage/AssertEqualsSQSreceiveMessage/AssertionProvider", + "children": { + "AssertionsProvider": { + "id": "AssertionsProvider", + "path": "Destinations/DefaultTest/DeployAssert/Default/AwsApiCallSQSreceiveMessage/AssertEqualsSQSreceiveMessage/AssertionProvider/AssertionsProvider", + "constructInfo": { + "fqn": "@aws-cdk/core.Construct", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.AssertionsProvider", + "version": "0.0.0" + } + }, + "Default": { + "id": "Default", + "path": "Destinations/DefaultTest/DeployAssert/Default/AwsApiCallSQSreceiveMessage/AssertEqualsSQSreceiveMessage/Default", + "children": { + "Default": { + "id": "Default", + "path": "Destinations/DefaultTest/DeployAssert/Default/AwsApiCallSQSreceiveMessage/AssertEqualsSQSreceiveMessage/Default/Default", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.CustomResource", + "version": "0.0.0" + } + }, + "AssertionResults": { + "id": "AssertionResults", + "path": "Destinations/DefaultTest/DeployAssert/Default/AwsApiCallSQSreceiveMessage/AssertEqualsSQSreceiveMessage/AssertionResults", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnOutput", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.EqualsAssertion", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.AwsApiCall", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.DeployAssert", + "version": "0.0.0" + } + }, + "SingletonFunction1488541a7b23466481b69b4408076b81": { + "id": "SingletonFunction1488541a7b23466481b69b4408076b81", + "path": "Destinations/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81", + "children": { + "Staging": { + "id": "Staging", + "path": "Destinations/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Staging", + "constructInfo": { + "fqn": "@aws-cdk/core.AssetStaging", + "version": "0.0.0" + } + }, + "Role": { + "id": "Role", + "path": "Destinations/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Role", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + }, + "Handler": { + "id": "Handler", + "path": "Destinations/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Handler", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Construct", + "version": "0.0.0" + } + }, + "AssetParameters": { + "id": "AssetParameters", + "path": "Destinations/DefaultTest/DeployAssert/AssetParameters", + "children": { + "1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b": { + "id": "1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b", + "path": "Destinations/DefaultTest/DeployAssert/AssetParameters/1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b", + "children": { + "S3Bucket": { + "id": "S3Bucket", + "path": "Destinations/DefaultTest/DeployAssert/AssetParameters/1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b/S3Bucket", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "S3VersionKey": { + "id": "S3VersionKey", + "path": "Destinations/DefaultTest/DeployAssert/AssetParameters/1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b/S3VersionKey", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "ArtifactHash": { + "id": "ArtifactHash", + "path": "Destinations/DefaultTest/DeployAssert/AssetParameters/1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b/ArtifactHash", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Construct", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Construct", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Construct", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Construct", + "version": "0.0.0" + } } }, "constructInfo": { diff --git a/packages/@aws-cdk/aws-lambda-destinations/test/integ.destinations.ts b/packages/@aws-cdk/aws-lambda-destinations/test/integ.destinations.ts index e505b441c52c1..61009f180e53d 100644 --- a/packages/@aws-cdk/aws-lambda-destinations/test/integ.destinations.ts +++ b/packages/@aws-cdk/aws-lambda-destinations/test/integ.destinations.ts @@ -2,6 +2,7 @@ import * as lambda from '@aws-cdk/aws-lambda'; import * as sns from '@aws-cdk/aws-sns'; import * as sqs from '@aws-cdk/aws-sqs'; import { App, Duration, Stack, StackProps } from '@aws-cdk/core'; +import { IntegTest, InvocationType, ExpectedResult } from '@aws-cdk/integ-tests'; import { Construct } from 'constructs'; import * as destinations from '../lib'; @@ -12,21 +13,23 @@ import * as destinations from '../lib'; */ class TestStack extends Stack { + public readonly fn: lambda.Function; + public readonly queue: sqs.Queue; constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); const topic = new sns.Topic(this, 'Topic'); - const queue = new sqs.Queue(this, 'Queue'); + this.queue = new sqs.Queue(this, 'Queue'); - const fn = new lambda.Function(this, 'SnsSqs', { + this.fn = new lambda.Function(this, 'SnsSqs', { runtime: lambda.Runtime.NODEJS_14_X, handler: 'index.handler', code: lambda.Code.fromInline(`exports.handler = async (event) => { - if (event === 'OK') return 'success'; + if (event.status === 'OK') return 'success'; throw new Error('failure'); };`), onFailure: new destinations.SnsDestination(topic), - onSuccess: new destinations.SqsDestination(queue), + onSuccess: new destinations.SqsDestination(this.queue), maxEventAge: Duration.hours(3), retryAttempts: 1, }); @@ -43,19 +46,19 @@ class TestStack extends Stack { runtime: lambda.Runtime.NODEJS_14_X, handler: 'index.handler', code: lambda.Code.fromInline(`exports.handler = async (event) => { - if (event === 'OK') return 'success'; + if (event.status === 'OK') return 'success'; throw new Error('failure'); };`), onFailure: new destinations.EventBridgeDestination(), onSuccess: new destinations.LambdaDestination(onSuccessLambda), }); - const version = fn.addVersion('MySpecialVersion'); + const version = this.fn.addVersion('MySpecialVersion'); new lambda.Alias(this, 'MySpecialAlias', { aliasName: 'MySpecialAlias', version, - onSuccess: new destinations.SqsDestination(queue), + onSuccess: new destinations.SqsDestination(this.queue), onFailure: new destinations.SnsDestination(topic), maxEventAge: Duration.hours(2), retryAttempts: 0, @@ -65,6 +68,33 @@ class TestStack extends Stack { const app = new App(); -new TestStack(app, 'aws-cdk-lambda-destinations'); +const stack = new TestStack(app, 'aws-cdk-lambda-destinations'); +const integ = new IntegTest(app, 'Destinations', { + testCases: [stack], +}); + +integ.assert.invokeFunction({ + functionName: stack.fn.functionName, + invocationType: InvocationType.EVENT, + payload: JSON.stringify({ status: 'OK' }), +}); + +const message = integ.assert.awsApiCall('SQS', 'receiveMessage', { + QueueUrl: stack.queue.queueUrl, + WaitTimeSeconds: 20, +}); + +message.assertAtPath('Messages.0.Body', ExpectedResult.objectLike({ + requestContext: { + condition: 'Success', + }, + requestPayload: { + status: 'OK', + }, + responseContext: { + statusCode: 200, + }, + responsePayload: 'success', +})); app.synth(); diff --git a/packages/@aws-cdk/aws-lambda/package.json b/packages/@aws-cdk/aws-lambda/package.json index fa1de400f868f..8783b4048e5f6 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -84,6 +84,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assertions": "0.0.0", + "@aws-cdk/integ-tests": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/integ-runner": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-lambda/test/bundling.integ.snapshot/BundlingDefaultTestDeployAssertAACA0CAF.template.json b/packages/@aws-cdk/aws-lambda/test/bundling.integ.snapshot/BundlingDefaultTestDeployAssertAACA0CAF.template.json new file mode 100644 index 0000000000000..f1587148e1a58 --- /dev/null +++ b/packages/@aws-cdk/aws-lambda/test/bundling.integ.snapshot/BundlingDefaultTestDeployAssertAACA0CAF.template.json @@ -0,0 +1,210 @@ +{ + "Resources": { + "LambdaInvoke55933c6da447c7ea94ebd3a50e8557a8": { + "Type": "Custom::DeployAssert@SdkCallLambdainvoke", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "SingletonFunction1488541a7b23466481b69b4408076b81HandlerCD40AE9F", + "Arn" + ] + }, + "service": "Lambda", + "api": "invoke", + "parameters": { + "FunctionName": { + "Fn::ImportValue": "cdk-integ-lambda-bundling:ExportsOutputRefFunction76856677C48862D5" + } + }, + "flattenResponse": "false", + "salt": "1651691789905" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "LambdaInvoke55933c6da447c7ea94ebd3a50e8557a8InvokeA3F6E40A": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::ImportValue": "cdk-integ-lambda-bundling:ExportsOutputRefFunction76856677C48862D5" + }, + "Principal": { + "Fn::GetAtt": [ + "SingletonFunction1488541a7b23466481b69b4408076b81Role37ABCE73", + "Arn" + ] + } + } + }, + "LambdaInvoke55933c6da447c7ea94ebd3a50e8557a8AssertEqualsLambdainvoke89C63F4A": { + "Type": "Custom::DeployAssert@AssertEquals", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "SingletonFunction1488541a7b23466481b69b4408076b81HandlerCD40AE9F", + "Arn" + ] + }, + "actual": { + "Fn::GetAtt": [ + "LambdaInvoke55933c6da447c7ea94ebd3a50e8557a8", + "apiCallResponse" + ] + }, + "expected": "{\"$ObjectLike\":{\"Payload\":\"200\"}}", + "salt": "1651691789906" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "SingletonFunction1488541a7b23466481b69b4408076b81Role37ABCE73": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ] + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + } + ], + "Policies": [ + { + "PolicyName": "Inline", + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "lambda:Invoke" + ], + "Effect": "Allow", + "Resource": [ + "*" + ] + }, + { + "Action": [ + "lambda:InvokeFunction" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":lambda:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":function:", + { + "Fn::ImportValue": "cdk-integ-lambda-bundling:ExportsOutputRefFunction76856677C48862D5" + } + ] + ] + } + ] + } + ] + } + } + ] + } + }, + "SingletonFunction1488541a7b23466481b69b4408076b81HandlerCD40AE9F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Runtime": "nodejs14.x", + "Code": { + "S3Bucket": { + "Ref": "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bS3BucketF7210344" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bS3VersionKey1E71961C" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bS3VersionKey1E71961C" + } + ] + } + ] + } + ] + ] + } + }, + "Timeout": 120, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "SingletonFunction1488541a7b23466481b69b4408076b81Role37ABCE73", + "Arn" + ] + } + } + } + }, + "Outputs": { + "AssertionResultsAssertEqualsLambdainvoke": { + "Value": { + "Fn::GetAtt": [ + "LambdaInvoke55933c6da447c7ea94ebd3a50e8557a8AssertEqualsLambdainvoke89C63F4A", + "data" + ] + } + } + }, + "Parameters": { + "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bS3BucketF7210344": { + "Type": "String", + "Description": "S3 bucket for asset \"1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b\"" + }, + "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bS3VersionKey1E71961C": { + "Type": "String", + "Description": "S3 key for asset version \"1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b\"" + }, + "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bArtifactHash4F8362F2": { + "Type": "String", + "Description": "Artifact hash for asset \"1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/test/bundling.integ.snapshot/cdk-integ-lambda-bundling.template.json b/packages/@aws-cdk/aws-lambda/test/bundling.integ.snapshot/cdk-integ-lambda-bundling.template.json index 33783d01d4fa5..836c76f7cc95e 100644 --- a/packages/@aws-cdk/aws-lambda/test/bundling.integ.snapshot/cdk-integ-lambda-bundling.template.json +++ b/packages/@aws-cdk/aws-lambda/test/bundling.integ.snapshot/cdk-integ-lambda-bundling.template.json @@ -36,7 +36,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParametersfec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509S3BucketBF50F97C" + "Ref": "AssetParametersb0011b8704c0cceee88b3cdf79d915b7babbe192f420c472879803f44c2c374eS3Bucket305E1975" }, "S3Key": { "Fn::Join": [ @@ -49,7 +49,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersfec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509S3VersionKeyF21AC8C1" + "Ref": "AssetParametersb0011b8704c0cceee88b3cdf79d915b7babbe192f420c472879803f44c2c374eS3VersionKeyCC928AE5" } ] } @@ -62,7 +62,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersfec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509S3VersionKeyF21AC8C1" + "Ref": "AssetParametersb0011b8704c0cceee88b3cdf79d915b7babbe192f420c472879803f44c2c374eS3VersionKeyCC928AE5" } ] } @@ -79,7 +79,7 @@ ] }, "Handler": "index.handler", - "Runtime": "python3.6" + "Runtime": "python3.9" }, "DependsOn": [ "FunctionServiceRole675BB04A" @@ -87,26 +87,26 @@ } }, "Parameters": { - "AssetParametersfec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509S3BucketBF50F97C": { + "AssetParametersb0011b8704c0cceee88b3cdf79d915b7babbe192f420c472879803f44c2c374eS3Bucket305E1975": { "Type": "String", - "Description": "S3 bucket for asset \"fec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509\"" + "Description": "S3 bucket for asset \"b0011b8704c0cceee88b3cdf79d915b7babbe192f420c472879803f44c2c374e\"" }, - "AssetParametersfec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509S3VersionKeyF21AC8C1": { + "AssetParametersb0011b8704c0cceee88b3cdf79d915b7babbe192f420c472879803f44c2c374eS3VersionKeyCC928AE5": { "Type": "String", - "Description": "S3 key for asset version \"fec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509\"" + "Description": "S3 key for asset version \"b0011b8704c0cceee88b3cdf79d915b7babbe192f420c472879803f44c2c374e\"" }, - "AssetParametersfec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509ArtifactHash5D8C129B": { + "AssetParametersb0011b8704c0cceee88b3cdf79d915b7babbe192f420c472879803f44c2c374eArtifactHashBE058EE4": { "Type": "String", - "Description": "Artifact hash for asset \"fec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509\"" + "Description": "Artifact hash for asset \"b0011b8704c0cceee88b3cdf79d915b7babbe192f420c472879803f44c2c374e\"" } }, "Outputs": { - "FunctionArn": { + "ExportsOutputRefFunction76856677C48862D5": { "Value": { - "Fn::GetAtt": [ - "Function76856677", - "Arn" - ] + "Ref": "Function76856677" + }, + "Export": { + "Name": "cdk-integ-lambda-bundling:ExportsOutputRefFunction76856677C48862D5" } } } diff --git a/packages/@aws-cdk/aws-lambda/test/bundling.integ.snapshot/cdk.out b/packages/@aws-cdk/aws-lambda/test/bundling.integ.snapshot/cdk.out index 90bef2e09ad39..ccdfc1ff96a9d 100644 --- a/packages/@aws-cdk/aws-lambda/test/bundling.integ.snapshot/cdk.out +++ b/packages/@aws-cdk/aws-lambda/test/bundling.integ.snapshot/cdk.out @@ -1 +1 @@ -{"version":"17.0.0"} \ No newline at end of file +{"version":"19.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/test/bundling.integ.snapshot/integ.json b/packages/@aws-cdk/aws-lambda/test/bundling.integ.snapshot/integ.json index 5f0450b8a4c09..713bce6bb246e 100644 --- a/packages/@aws-cdk/aws-lambda/test/bundling.integ.snapshot/integ.json +++ b/packages/@aws-cdk/aws-lambda/test/bundling.integ.snapshot/integ.json @@ -1,14 +1,12 @@ { - "version": "18.0.0", + "version": "19.0.0", "testCases": { - "aws-lambda/test/integ.bundling": { + "Bundling/DefaultTest": { "stacks": [ "cdk-integ-lambda-bundling" ], - "diffAssets": false, - "stackUpdateWorkflow": false + "stackUpdateWorkflow": false, + "assertionStack": "BundlingDefaultTestDeployAssertAACA0CAF" } - }, - "synthContext": {}, - "enableLookups": false + } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/test/bundling.integ.snapshot/manifest.json b/packages/@aws-cdk/aws-lambda/test/bundling.integ.snapshot/manifest.json index 360adb50592e9..d49cacb755c4f 100644 --- a/packages/@aws-cdk/aws-lambda/test/bundling.integ.snapshot/manifest.json +++ b/packages/@aws-cdk/aws-lambda/test/bundling.integ.snapshot/manifest.json @@ -1,5 +1,5 @@ { - "version": "17.0.0", + "version": "19.0.0", "artifacts": { "Tree": { "type": "cdk:tree", @@ -19,13 +19,13 @@ { "type": "aws:cdk:asset", "data": { - "path": "asset.fec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509", - "id": "fec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509", + "path": "asset.b0011b8704c0cceee88b3cdf79d915b7babbe192f420c472879803f44c2c374e", + "id": "b0011b8704c0cceee88b3cdf79d915b7babbe192f420c472879803f44c2c374e", "packaging": "zip", - "sourceHash": "fec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509", - "s3BucketParameter": "AssetParametersfec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509S3BucketBF50F97C", - "s3KeyParameter": "AssetParametersfec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509S3VersionKeyF21AC8C1", - "artifactHashParameter": "AssetParametersfec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509ArtifactHash5D8C129B" + "sourceHash": "b0011b8704c0cceee88b3cdf79d915b7babbe192f420c472879803f44c2c374e", + "s3BucketParameter": "AssetParametersb0011b8704c0cceee88b3cdf79d915b7babbe192f420c472879803f44c2c374eS3Bucket305E1975", + "s3KeyParameter": "AssetParametersb0011b8704c0cceee88b3cdf79d915b7babbe192f420c472879803f44c2c374eS3VersionKeyCC928AE5", + "artifactHashParameter": "AssetParametersb0011b8704c0cceee88b3cdf79d915b7babbe192f420c472879803f44c2c374eArtifactHashBE058EE4" } } ], @@ -41,32 +41,114 @@ "data": "Function76856677" } ], - "/cdk-integ-lambda-bundling/AssetParameters/fec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509/S3Bucket": [ + "/cdk-integ-lambda-bundling/AssetParameters/b0011b8704c0cceee88b3cdf79d915b7babbe192f420c472879803f44c2c374e/S3Bucket": [ { "type": "aws:cdk:logicalId", - "data": "AssetParametersfec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509S3BucketBF50F97C" + "data": "AssetParametersb0011b8704c0cceee88b3cdf79d915b7babbe192f420c472879803f44c2c374eS3Bucket305E1975" } ], - "/cdk-integ-lambda-bundling/AssetParameters/fec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509/S3VersionKey": [ + "/cdk-integ-lambda-bundling/AssetParameters/b0011b8704c0cceee88b3cdf79d915b7babbe192f420c472879803f44c2c374e/S3VersionKey": [ { "type": "aws:cdk:logicalId", - "data": "AssetParametersfec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509S3VersionKeyF21AC8C1" + "data": "AssetParametersb0011b8704c0cceee88b3cdf79d915b7babbe192f420c472879803f44c2c374eS3VersionKeyCC928AE5" } ], - "/cdk-integ-lambda-bundling/AssetParameters/fec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509/ArtifactHash": [ + "/cdk-integ-lambda-bundling/AssetParameters/b0011b8704c0cceee88b3cdf79d915b7babbe192f420c472879803f44c2c374e/ArtifactHash": [ { "type": "aws:cdk:logicalId", - "data": "AssetParametersfec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509ArtifactHash5D8C129B" + "data": "AssetParametersb0011b8704c0cceee88b3cdf79d915b7babbe192f420c472879803f44c2c374eArtifactHashBE058EE4" } ], - "/cdk-integ-lambda-bundling/FunctionArn": [ + "/cdk-integ-lambda-bundling/Exports/Output{\"Ref\":\"Function76856677\"}": [ { "type": "aws:cdk:logicalId", - "data": "FunctionArn" + "data": "ExportsOutputRefFunction76856677C48862D5" } ] }, "displayName": "cdk-integ-lambda-bundling" + }, + "BundlingDefaultTestDeployAssertAACA0CAF": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "BundlingDefaultTestDeployAssertAACA0CAF.template.json", + "validateOnSynth": false + }, + "dependencies": [ + "cdk-integ-lambda-bundling" + ], + "metadata": { + "/Bundling/DefaultTest/DeployAssert": [ + { + "type": "aws:cdk:asset", + "data": { + "path": "asset.1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b.bundle", + "id": "1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b", + "packaging": "zip", + "sourceHash": "1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b", + "s3BucketParameter": "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bS3BucketF7210344", + "s3KeyParameter": "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bS3VersionKey1E71961C", + "artifactHashParameter": "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bArtifactHash4F8362F2" + } + } + ], + "/Bundling/DefaultTest/DeployAssert/Default/LambdaInvoke55933c6da447c7ea94ebd3a50e8557a8/Default/Default": [ + { + "type": "aws:cdk:logicalId", + "data": "LambdaInvoke55933c6da447c7ea94ebd3a50e8557a8" + } + ], + "/Bundling/DefaultTest/DeployAssert/Default/LambdaInvoke55933c6da447c7ea94ebd3a50e8557a8/Invoke": [ + { + "type": "aws:cdk:logicalId", + "data": "LambdaInvoke55933c6da447c7ea94ebd3a50e8557a8InvokeA3F6E40A" + } + ], + "/Bundling/DefaultTest/DeployAssert/Default/LambdaInvoke55933c6da447c7ea94ebd3a50e8557a8/AssertEqualsLambdainvoke/Default/Default": [ + { + "type": "aws:cdk:logicalId", + "data": "LambdaInvoke55933c6da447c7ea94ebd3a50e8557a8AssertEqualsLambdainvoke89C63F4A" + } + ], + "/Bundling/DefaultTest/DeployAssert/Default/LambdaInvoke55933c6da447c7ea94ebd3a50e8557a8/AssertEqualsLambdainvoke/AssertionResults": [ + { + "type": "aws:cdk:logicalId", + "data": "AssertionResultsAssertEqualsLambdainvoke" + } + ], + "/Bundling/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Role": [ + { + "type": "aws:cdk:logicalId", + "data": "SingletonFunction1488541a7b23466481b69b4408076b81Role37ABCE73" + } + ], + "/Bundling/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Handler": [ + { + "type": "aws:cdk:logicalId", + "data": "SingletonFunction1488541a7b23466481b69b4408076b81HandlerCD40AE9F" + } + ], + "/Bundling/DefaultTest/DeployAssert/AssetParameters/1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b/S3Bucket": [ + { + "type": "aws:cdk:logicalId", + "data": "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bS3BucketF7210344" + } + ], + "/Bundling/DefaultTest/DeployAssert/AssetParameters/1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b/S3VersionKey": [ + { + "type": "aws:cdk:logicalId", + "data": "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bS3VersionKey1E71961C" + } + ], + "/Bundling/DefaultTest/DeployAssert/AssetParameters/1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b/ArtifactHash": [ + { + "type": "aws:cdk:logicalId", + "data": "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bArtifactHash4F8362F2" + } + ] + }, + "displayName": "Bundling/DefaultTest/DeployAssert" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-lambda/test/bundling.integ.snapshot/tree.json b/packages/@aws-cdk/aws-lambda/test/bundling.integ.snapshot/tree.json index 032800c2d957a..55062d01c72fa 100644 --- a/packages/@aws-cdk/aws-lambda/test/bundling.integ.snapshot/tree.json +++ b/packages/@aws-cdk/aws-lambda/test/bundling.integ.snapshot/tree.json @@ -103,7 +103,7 @@ "aws:cdk:cloudformation:props": { "code": { "s3Bucket": { - "Ref": "AssetParametersfec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509S3BucketBF50F97C" + "Ref": "AssetParametersb0011b8704c0cceee88b3cdf79d915b7babbe192f420c472879803f44c2c374eS3Bucket305E1975" }, "s3Key": { "Fn::Join": [ @@ -116,7 +116,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersfec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509S3VersionKeyF21AC8C1" + "Ref": "AssetParametersb0011b8704c0cceee88b3cdf79d915b7babbe192f420c472879803f44c2c374eS3VersionKeyCC928AE5" } ] } @@ -129,7 +129,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParametersfec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509S3VersionKeyF21AC8C1" + "Ref": "AssetParametersb0011b8704c0cceee88b3cdf79d915b7babbe192f420c472879803f44c2c374eS3VersionKeyCC928AE5" } ] } @@ -146,7 +146,7 @@ ] }, "handler": "index.handler", - "runtime": "python3.6" + "runtime": "python3.9" } }, "constructInfo": { @@ -164,13 +164,13 @@ "id": "AssetParameters", "path": "cdk-integ-lambda-bundling/AssetParameters", "children": { - "fec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509": { - "id": "fec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509", - "path": "cdk-integ-lambda-bundling/AssetParameters/fec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509", + "b0011b8704c0cceee88b3cdf79d915b7babbe192f420c472879803f44c2c374e": { + "id": "b0011b8704c0cceee88b3cdf79d915b7babbe192f420c472879803f44c2c374e", + "path": "cdk-integ-lambda-bundling/AssetParameters/b0011b8704c0cceee88b3cdf79d915b7babbe192f420c472879803f44c2c374e", "children": { "S3Bucket": { "id": "S3Bucket", - "path": "cdk-integ-lambda-bundling/AssetParameters/fec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509/S3Bucket", + "path": "cdk-integ-lambda-bundling/AssetParameters/b0011b8704c0cceee88b3cdf79d915b7babbe192f420c472879803f44c2c374e/S3Bucket", "constructInfo": { "fqn": "@aws-cdk/core.CfnParameter", "version": "0.0.0" @@ -178,7 +178,7 @@ }, "S3VersionKey": { "id": "S3VersionKey", - "path": "cdk-integ-lambda-bundling/AssetParameters/fec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509/S3VersionKey", + "path": "cdk-integ-lambda-bundling/AssetParameters/b0011b8704c0cceee88b3cdf79d915b7babbe192f420c472879803f44c2c374e/S3VersionKey", "constructInfo": { "fqn": "@aws-cdk/core.CfnParameter", "version": "0.0.0" @@ -186,7 +186,7 @@ }, "ArtifactHash": { "id": "ArtifactHash", - "path": "cdk-integ-lambda-bundling/AssetParameters/fec1c56a3f23d9d27f58815e0c34c810cc02f431ac63a078f9b5d2aa44cc3509/ArtifactHash", + "path": "cdk-integ-lambda-bundling/AssetParameters/b0011b8704c0cceee88b3cdf79d915b7babbe192f420c472879803f44c2c374e/ArtifactHash", "constructInfo": { "fqn": "@aws-cdk/core.CfnParameter", "version": "0.0.0" @@ -204,11 +204,21 @@ "version": "0.0.0" } }, - "FunctionArn": { - "id": "FunctionArn", - "path": "cdk-integ-lambda-bundling/FunctionArn", + "Exports": { + "id": "Exports", + "path": "cdk-integ-lambda-bundling/Exports", + "children": { + "Output{\"Ref\":\"Function76856677\"}": { + "id": "Output{\"Ref\":\"Function76856677\"}", + "path": "cdk-integ-lambda-bundling/Exports/Output{\"Ref\":\"Function76856677\"}", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnOutput", + "version": "0.0.0" + } + } + }, "constructInfo": { - "fqn": "@aws-cdk/core.CfnOutput", + "fqn": "@aws-cdk/core.Construct", "version": "0.0.0" } } @@ -217,6 +227,232 @@ "fqn": "@aws-cdk/core.Stack", "version": "0.0.0" } + }, + "Bundling": { + "id": "Bundling", + "path": "Bundling", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "Bundling/DefaultTest", + "children": { + "DeployAssert": { + "id": "DeployAssert", + "path": "Bundling/DefaultTest/DeployAssert", + "children": { + "Default": { + "id": "Default", + "path": "Bundling/DefaultTest/DeployAssert/Default", + "children": { + "LambdaInvoke55933c6da447c7ea94ebd3a50e8557a8": { + "id": "LambdaInvoke55933c6da447c7ea94ebd3a50e8557a8", + "path": "Bundling/DefaultTest/DeployAssert/Default/LambdaInvoke55933c6da447c7ea94ebd3a50e8557a8", + "children": { + "SdkProvider": { + "id": "SdkProvider", + "path": "Bundling/DefaultTest/DeployAssert/Default/LambdaInvoke55933c6da447c7ea94ebd3a50e8557a8/SdkProvider", + "children": { + "AssertionsProvider": { + "id": "AssertionsProvider", + "path": "Bundling/DefaultTest/DeployAssert/Default/LambdaInvoke55933c6da447c7ea94ebd3a50e8557a8/SdkProvider/AssertionsProvider", + "constructInfo": { + "fqn": "@aws-cdk/core.Construct", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.AssertionsProvider", + "version": "0.0.0" + } + }, + "Default": { + "id": "Default", + "path": "Bundling/DefaultTest/DeployAssert/Default/LambdaInvoke55933c6da447c7ea94ebd3a50e8557a8/Default", + "children": { + "Default": { + "id": "Default", + "path": "Bundling/DefaultTest/DeployAssert/Default/LambdaInvoke55933c6da447c7ea94ebd3a50e8557a8/Default/Default", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.CustomResource", + "version": "0.0.0" + } + }, + "Invoke": { + "id": "Invoke", + "path": "Bundling/DefaultTest/DeployAssert/Default/LambdaInvoke55933c6da447c7ea94ebd3a50e8557a8/Invoke", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + }, + "AssertEqualsLambdainvoke": { + "id": "AssertEqualsLambdainvoke", + "path": "Bundling/DefaultTest/DeployAssert/Default/LambdaInvoke55933c6da447c7ea94ebd3a50e8557a8/AssertEqualsLambdainvoke", + "children": { + "AssertionProvider": { + "id": "AssertionProvider", + "path": "Bundling/DefaultTest/DeployAssert/Default/LambdaInvoke55933c6da447c7ea94ebd3a50e8557a8/AssertEqualsLambdainvoke/AssertionProvider", + "children": { + "AssertionsProvider": { + "id": "AssertionsProvider", + "path": "Bundling/DefaultTest/DeployAssert/Default/LambdaInvoke55933c6da447c7ea94ebd3a50e8557a8/AssertEqualsLambdainvoke/AssertionProvider/AssertionsProvider", + "constructInfo": { + "fqn": "@aws-cdk/core.Construct", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.AssertionsProvider", + "version": "0.0.0" + } + }, + "Default": { + "id": "Default", + "path": "Bundling/DefaultTest/DeployAssert/Default/LambdaInvoke55933c6da447c7ea94ebd3a50e8557a8/AssertEqualsLambdainvoke/Default", + "children": { + "Default": { + "id": "Default", + "path": "Bundling/DefaultTest/DeployAssert/Default/LambdaInvoke55933c6da447c7ea94ebd3a50e8557a8/AssertEqualsLambdainvoke/Default/Default", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.CustomResource", + "version": "0.0.0" + } + }, + "AssertionResults": { + "id": "AssertionResults", + "path": "Bundling/DefaultTest/DeployAssert/Default/LambdaInvoke55933c6da447c7ea94ebd3a50e8557a8/AssertEqualsLambdainvoke/AssertionResults", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnOutput", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.EqualsAssertion", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.LambdaInvokeFunction", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.DeployAssert", + "version": "0.0.0" + } + }, + "SingletonFunction1488541a7b23466481b69b4408076b81": { + "id": "SingletonFunction1488541a7b23466481b69b4408076b81", + "path": "Bundling/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81", + "children": { + "Staging": { + "id": "Staging", + "path": "Bundling/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Staging", + "constructInfo": { + "fqn": "@aws-cdk/core.AssetStaging", + "version": "0.0.0" + } + }, + "Role": { + "id": "Role", + "path": "Bundling/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Role", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + }, + "Handler": { + "id": "Handler", + "path": "Bundling/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Handler", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Construct", + "version": "0.0.0" + } + }, + "AssetParameters": { + "id": "AssetParameters", + "path": "Bundling/DefaultTest/DeployAssert/AssetParameters", + "children": { + "1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b": { + "id": "1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b", + "path": "Bundling/DefaultTest/DeployAssert/AssetParameters/1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b", + "children": { + "S3Bucket": { + "id": "S3Bucket", + "path": "Bundling/DefaultTest/DeployAssert/AssetParameters/1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b/S3Bucket", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "S3VersionKey": { + "id": "S3VersionKey", + "path": "Bundling/DefaultTest/DeployAssert/AssetParameters/1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b/S3VersionKey", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "ArtifactHash": { + "id": "ArtifactHash", + "path": "Bundling/DefaultTest/DeployAssert/AssetParameters/1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b/ArtifactHash", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Construct", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Construct", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Construct", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Construct", + "version": "0.0.0" + } } }, "constructInfo": { diff --git a/packages/@aws-cdk/aws-lambda/test/integ.bundling.ts b/packages/@aws-cdk/aws-lambda/test/integ.bundling.ts index e25ee277c7ed4..30e28c8ef5fe7 100644 --- a/packages/@aws-cdk/aws-lambda/test/integ.bundling.ts +++ b/packages/@aws-cdk/aws-lambda/test/integ.bundling.ts @@ -1,6 +1,7 @@ /// !cdk-integ pragma:disable-update-workflow import * as path from 'path'; -import { App, CfnOutput, Stack, StackProps } from '@aws-cdk/core'; +import { App, Stack, StackProps } from '@aws-cdk/core'; +import { IntegTest, ExpectedResult } from '@aws-cdk/integ-tests'; import { Construct } from 'constructs'; import * as lambda from '../lib'; @@ -12,6 +13,7 @@ import * as lambda from '../lib'; * The last command should show '200' */ class TestStack extends Stack { + public readonly functionName: string; constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); @@ -19,7 +21,7 @@ class TestStack extends Stack { const fn = new lambda.Function(this, 'Function', { code: lambda.Code.fromAsset(assetPath, { bundling: { - image: lambda.Runtime.PYTHON_3_6.bundlingImage, + image: lambda.Runtime.PYTHON_3_9.bundlingImage, command: [ 'bash', '-c', [ 'cp -au . /asset-output', @@ -29,16 +31,26 @@ class TestStack extends Stack { ], }, }), - runtime: lambda.Runtime.PYTHON_3_6, + runtime: lambda.Runtime.PYTHON_3_9, handler: 'index.handler', }); - new CfnOutput(this, 'FunctionArn', { - value: fn.functionArn, - }); + this.functionName = fn.functionName; } } const app = new App(); -new TestStack(app, 'cdk-integ-lambda-bundling'); +const stack = new TestStack(app, 'cdk-integ-lambda-bundling'); + +const integ = new IntegTest(app, 'Bundling', { + testCases: [stack], + stackUpdateWorkflow: false, +}); + +const invoke = integ.assert.invokeFunction({ + functionName: stack.functionName, +}); +invoke.assert(ExpectedResult.objectLike({ + Payload: '200', +})); app.synth(); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json index bdb985a058522..bfec871b0b138 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json @@ -78,6 +78,7 @@ }, "license": "Apache-2.0", "devDependencies": { + "@aws-cdk/integ-tests": "0.0.0", "@aws-cdk/assertions": "0.0.0", "@aws-cdk/aws-apigatewayv2": "0.0.0", "@aws-cdk/aws-apigatewayv2-integrations": "0.0.0", diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/integ.put-events.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/integ.put-events.ts index 84198e2df0b45..1c6ee34a3341b 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/integ.put-events.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/integ.put-events.ts @@ -1,6 +1,7 @@ import * as events from '@aws-cdk/aws-events'; import * as sfn from '@aws-cdk/aws-stepfunctions'; import * as cdk from '@aws-cdk/core'; +import { IntegTest, ExpectedResult } from '@aws-cdk/integ-tests'; import { EventBridgePutEvents } from '../../lib'; /* @@ -39,8 +40,24 @@ const sm = new sfn.StateMachine(stack, 'StateMachine', { timeout: cdk.Duration.seconds(30), }); -new cdk.CfnOutput(stack, 'stateMachineArn', { - value: sm.stateMachineArn, + +const testCase = new IntegTest(app, 'PutEvents', { + testCases: [stack], +}); + +// Start an execution +const start = testCase.assert.awsApiCall('StepFunctions', 'startExecution', { + stateMachineArn: sm.stateMachineArn, }); +// describe the results of the execution +const describe = testCase.assert.awsApiCall('StepFunctions', 'describeExecution', { + executionArn: start.getAttString('executionArn'), +}); + +// assert the results +describe.assert(ExpectedResult.objectLike({ + status: 'SUCCEEDED', +})); + app.synth(); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/put-events.integ.snapshot/PutEventsDefaultTestDeployAssert1A6BA3F3.template.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/put-events.integ.snapshot/PutEventsDefaultTestDeployAssert1A6BA3F3.template.json new file mode 100644 index 0000000000000..d8ba1b9c2554d --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/put-events.integ.snapshot/PutEventsDefaultTestDeployAssert1A6BA3F3.template.json @@ -0,0 +1,198 @@ +{ + "Resources": { + "AwsApiCallStepFunctionsstartExecution": { + "Type": "Custom::DeployAssert@SdkCallStepFunctionsstartExecution", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "SingletonFunction1488541a7b23466481b69b4408076b81HandlerCD40AE9F", + "Arn" + ] + }, + "service": "StepFunctions", + "api": "startExecution", + "parameters": { + "stateMachineArn": { + "Fn::ImportValue": "aws-stepfunctions-tasks-eventbridge-put-events-integ:ExportsOutputRefStateMachine2E01A3A5BA46F753" + } + }, + "flattenResponse": "true", + "salt": "1651691787968" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "AwsApiCallStepFunctionsdescribeExecution": { + "Type": "Custom::DeployAssert@SdkCallStepFunctionsdescribeExecution", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "SingletonFunction1488541a7b23466481b69b4408076b81HandlerCD40AE9F", + "Arn" + ] + }, + "service": "StepFunctions", + "api": "describeExecution", + "parameters": { + "executionArn": { + "Fn::GetAtt": [ + "AwsApiCallStepFunctionsstartExecution", + "apiCallResponse.executionArn" + ] + } + }, + "flattenResponse": "false", + "salt": "1651691787969" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "AwsApiCallStepFunctionsdescribeExecutionAssertEqualsStepFunctionsdescribeExecution58E75A69": { + "Type": "Custom::DeployAssert@AssertEquals", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "SingletonFunction1488541a7b23466481b69b4408076b81HandlerCD40AE9F", + "Arn" + ] + }, + "actual": { + "Fn::GetAtt": [ + "AwsApiCallStepFunctionsdescribeExecution", + "apiCallResponse" + ] + }, + "expected": "{\"$ObjectLike\":{\"status\":\"SUCCEEDED\"}}", + "salt": "1651691787970" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "SingletonFunction1488541a7b23466481b69b4408076b81Role37ABCE73": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ] + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + } + ], + "Policies": [ + { + "PolicyName": "Inline", + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "states:StartExecution" + ], + "Effect": "Allow", + "Resource": [ + "*" + ] + }, + { + "Action": [ + "states:DescribeExecution" + ], + "Effect": "Allow", + "Resource": [ + "*" + ] + } + ] + } + } + ] + } + }, + "SingletonFunction1488541a7b23466481b69b4408076b81HandlerCD40AE9F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Runtime": "nodejs14.x", + "Code": { + "S3Bucket": { + "Ref": "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bS3BucketF7210344" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bS3VersionKey1E71961C" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bS3VersionKey1E71961C" + } + ] + } + ] + } + ] + ] + } + }, + "Timeout": 120, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "SingletonFunction1488541a7b23466481b69b4408076b81Role37ABCE73", + "Arn" + ] + } + } + } + }, + "Outputs": { + "AssertionResultsAssertEqualsStepFunctionsdescribeExecution": { + "Value": { + "Fn::GetAtt": [ + "AwsApiCallStepFunctionsdescribeExecutionAssertEqualsStepFunctionsdescribeExecution58E75A69", + "data" + ] + } + } + }, + "Parameters": { + "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bS3BucketF7210344": { + "Type": "String", + "Description": "S3 bucket for asset \"1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b\"" + }, + "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bS3VersionKey1E71961C": { + "Type": "String", + "Description": "S3 key for asset version \"1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b\"" + }, + "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bArtifactHash4F8362F2": { + "Type": "String", + "Description": "Artifact hash for asset \"1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/put-events.integ.snapshot/asset.1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b.bundle/index.js b/packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/put-events.integ.snapshot/asset.1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b.bundle/index.js new file mode 100644 index 0000000000000..32e3e2c1e5a95 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/put-events.integ.snapshot/asset.1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b.bundle/index.js @@ -0,0 +1,644 @@ +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getOwnPropSymbols = Object.getOwnPropertySymbols; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __propIsEnum = Object.prototype.propertyIsEnumerable; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __spreadValues = (a, b) => { + for (var prop in b || (b = {})) + if (__hasOwnProp.call(b, prop)) + __defNormalProp(a, prop, b[prop]); + if (__getOwnPropSymbols) + for (var prop of __getOwnPropSymbols(b)) { + if (__propIsEnum.call(b, prop)) + __defNormalProp(a, prop, b[prop]); + } + return a; +}; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod)); +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// lib/assertions/providers/lambda-handler/index.ts +var lambda_handler_exports = {}; +__export(lambda_handler_exports, { + handler: () => handler +}); +module.exports = __toCommonJS(lambda_handler_exports); + +// ../assertions/lib/matcher.ts +var Matcher = class { + static isMatcher(x) { + return x && x instanceof Matcher; + } +}; +var MatchResult = class { + constructor(target) { + this.failures = []; + this.captures = /* @__PURE__ */ new Map(); + this.finalized = false; + this.target = target; + } + push(matcher, path, message) { + return this.recordFailure({ matcher, path, message }); + } + recordFailure(failure) { + this.failures.push(failure); + return this; + } + hasFailed() { + return this.failures.length !== 0; + } + get failCount() { + return this.failures.length; + } + compose(id, inner) { + const innerF = inner.failures; + this.failures.push(...innerF.map((f) => { + return { path: [id, ...f.path], message: f.message, matcher: f.matcher }; + })); + inner.captures.forEach((vals, capture) => { + vals.forEach((value) => this.recordCapture({ capture, value })); + }); + return this; + } + finished() { + if (this.finalized) { + return this; + } + if (this.failCount === 0) { + this.captures.forEach((vals, cap) => cap._captured.push(...vals)); + } + this.finalized = true; + return this; + } + toHumanStrings() { + return this.failures.map((r) => { + const loc = r.path.length === 0 ? "" : ` at ${r.path.join("")}`; + return "" + r.message + loc + ` (using ${r.matcher.name} matcher)`; + }); + } + recordCapture(options) { + let values = this.captures.get(options.capture); + if (values === void 0) { + values = []; + } + values.push(options.value); + this.captures.set(options.capture, values); + } +}; + +// ../assertions/lib/private/matchers/absent.ts +var AbsentMatch = class extends Matcher { + constructor(name) { + super(); + this.name = name; + } + test(actual) { + const result = new MatchResult(actual); + if (actual !== void 0) { + result.recordFailure({ + matcher: this, + path: [], + message: `Received ${actual}, but key should be absent` + }); + } + return result; + } +}; + +// ../assertions/lib/private/type.ts +function getType(obj) { + return Array.isArray(obj) ? "array" : typeof obj; +} + +// ../assertions/lib/match.ts +var Match = class { + static absent() { + return new AbsentMatch("absent"); + } + static arrayWith(pattern) { + return new ArrayMatch("arrayWith", pattern); + } + static arrayEquals(pattern) { + return new ArrayMatch("arrayEquals", pattern, { subsequence: false }); + } + static exact(pattern) { + return new LiteralMatch("exact", pattern, { partialObjects: false }); + } + static objectLike(pattern) { + return new ObjectMatch("objectLike", pattern); + } + static objectEquals(pattern) { + return new ObjectMatch("objectEquals", pattern, { partial: false }); + } + static not(pattern) { + return new NotMatch("not", pattern); + } + static serializedJson(pattern) { + return new SerializedJson("serializedJson", pattern); + } + static anyValue() { + return new AnyMatch("anyValue"); + } + static stringLikeRegexp(pattern) { + return new StringLikeRegexpMatch("stringLikeRegexp", pattern); + } +}; +var LiteralMatch = class extends Matcher { + constructor(name, pattern, options = {}) { + super(); + this.name = name; + this.pattern = pattern; + this.partialObjects = options.partialObjects ?? false; + if (Matcher.isMatcher(this.pattern)) { + throw new Error("LiteralMatch cannot directly contain another matcher. Remove the top-level matcher or nest it more deeply."); + } + } + test(actual) { + if (Array.isArray(this.pattern)) { + return new ArrayMatch(this.name, this.pattern, { subsequence: false, partialObjects: this.partialObjects }).test(actual); + } + if (typeof this.pattern === "object") { + return new ObjectMatch(this.name, this.pattern, { partial: this.partialObjects }).test(actual); + } + const result = new MatchResult(actual); + if (typeof this.pattern !== typeof actual) { + result.recordFailure({ + matcher: this, + path: [], + message: `Expected type ${typeof this.pattern} but received ${getType(actual)}` + }); + return result; + } + if (actual !== this.pattern) { + result.recordFailure({ + matcher: this, + path: [], + message: `Expected ${this.pattern} but received ${actual}` + }); + } + return result; + } +}; +var ArrayMatch = class extends Matcher { + constructor(name, pattern, options = {}) { + super(); + this.name = name; + this.pattern = pattern; + this.subsequence = options.subsequence ?? true; + this.partialObjects = options.partialObjects ?? false; + } + test(actual) { + if (!Array.isArray(actual)) { + return new MatchResult(actual).recordFailure({ + matcher: this, + path: [], + message: `Expected type array but received ${getType(actual)}` + }); + } + if (!this.subsequence && this.pattern.length !== actual.length) { + return new MatchResult(actual).recordFailure({ + matcher: this, + path: [], + message: `Expected array of length ${this.pattern.length} but received ${actual.length}` + }); + } + let patternIdx = 0; + let actualIdx = 0; + const result = new MatchResult(actual); + while (patternIdx < this.pattern.length && actualIdx < actual.length) { + const patternElement = this.pattern[patternIdx]; + const matcher = Matcher.isMatcher(patternElement) ? patternElement : new LiteralMatch(this.name, patternElement, { partialObjects: this.partialObjects }); + const matcherName = matcher.name; + if (this.subsequence && (matcherName == "absent" || matcherName == "anyValue")) { + throw new Error(`The Matcher ${matcherName}() cannot be nested within arrayWith()`); + } + const innerResult = matcher.test(actual[actualIdx]); + if (!this.subsequence || !innerResult.hasFailed()) { + result.compose(`[${actualIdx}]`, innerResult); + patternIdx++; + actualIdx++; + } else { + actualIdx++; + } + } + for (; patternIdx < this.pattern.length; patternIdx++) { + const pattern = this.pattern[patternIdx]; + const element = Matcher.isMatcher(pattern) || typeof pattern === "object" ? " " : ` [${pattern}] `; + result.recordFailure({ + matcher: this, + path: [], + message: `Missing element${element}at pattern index ${patternIdx}` + }); + } + return result; + } +}; +var ObjectMatch = class extends Matcher { + constructor(name, pattern, options = {}) { + super(); + this.name = name; + this.pattern = pattern; + this.partial = options.partial ?? true; + } + test(actual) { + if (typeof actual !== "object" || Array.isArray(actual)) { + return new MatchResult(actual).recordFailure({ + matcher: this, + path: [], + message: `Expected type object but received ${getType(actual)}` + }); + } + const result = new MatchResult(actual); + if (!this.partial) { + for (const a of Object.keys(actual)) { + if (!(a in this.pattern)) { + result.recordFailure({ + matcher: this, + path: [`/${a}`], + message: "Unexpected key" + }); + } + } + } + for (const [patternKey, patternVal] of Object.entries(this.pattern)) { + if (!(patternKey in actual) && !(patternVal instanceof AbsentMatch)) { + result.recordFailure({ + matcher: this, + path: [`/${patternKey}`], + message: `Missing key '${patternKey}' among {${Object.keys(actual).join(",")}}` + }); + continue; + } + const matcher = Matcher.isMatcher(patternVal) ? patternVal : new LiteralMatch(this.name, patternVal, { partialObjects: this.partial }); + const inner = matcher.test(actual[patternKey]); + result.compose(`/${patternKey}`, inner); + } + return result; + } +}; +var SerializedJson = class extends Matcher { + constructor(name, pattern) { + super(); + this.name = name; + this.pattern = pattern; + } + test(actual) { + const result = new MatchResult(actual); + if (getType(actual) !== "string") { + result.recordFailure({ + matcher: this, + path: [], + message: `Expected JSON as a string but found ${getType(actual)}` + }); + return result; + } + let parsed; + try { + parsed = JSON.parse(actual); + } catch (err) { + if (err instanceof SyntaxError) { + result.recordFailure({ + matcher: this, + path: [], + message: `Invalid JSON string: ${actual}` + }); + return result; + } else { + throw err; + } + } + const matcher = Matcher.isMatcher(this.pattern) ? this.pattern : new LiteralMatch(this.name, this.pattern); + const innerResult = matcher.test(parsed); + result.compose(`(${this.name})`, innerResult); + return result; + } +}; +var NotMatch = class extends Matcher { + constructor(name, pattern) { + super(); + this.name = name; + this.pattern = pattern; + } + test(actual) { + const matcher = Matcher.isMatcher(this.pattern) ? this.pattern : new LiteralMatch(this.name, this.pattern); + const innerResult = matcher.test(actual); + const result = new MatchResult(actual); + if (innerResult.failCount === 0) { + result.recordFailure({ + matcher: this, + path: [], + message: `Found unexpected match: ${JSON.stringify(actual, void 0, 2)}` + }); + } + return result; + } +}; +var AnyMatch = class extends Matcher { + constructor(name) { + super(); + this.name = name; + } + test(actual) { + const result = new MatchResult(actual); + if (actual == null) { + result.recordFailure({ + matcher: this, + path: [], + message: "Expected a value but found none" + }); + } + return result; + } +}; +var StringLikeRegexpMatch = class extends Matcher { + constructor(name, pattern) { + super(); + this.name = name; + this.pattern = pattern; + } + test(actual) { + const result = new MatchResult(actual); + const regex = new RegExp(this.pattern, "gm"); + if (typeof actual !== "string") { + result.recordFailure({ + matcher: this, + path: [], + message: `Expected a string, but got '${typeof actual}'` + }); + } + if (!regex.test(actual)) { + result.recordFailure({ + matcher: this, + path: [], + message: `String '${actual}' did not match pattern '${this.pattern}'` + }); + } + return result; + } +}; + +// lib/assertions/providers/lambda-handler/base.ts +var https = __toESM(require("https")); +var url = __toESM(require("url")); +var CustomResourceHandler = class { + constructor(event, context) { + this.event = event; + this.context = context; + this.timedOut = false; + 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); + } + async handle() { + try { + console.log(`Event: ${JSON.stringify(this.event)}`); + const response = await this.processEvent(this.event.ResourceProperties); + 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); + } + } + respond(response) { + if (this.timedOut) { + return; + } + const cfResponse = { + 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 request2 = https.request(requestOptions, resolve); + request2.on("error", reject); + request2.write(responseBody); + request2.end(); + } catch (e) { + reject(e); + } + }); + } +}; +function extractPhysicalResourceId(event) { + switch (event.RequestType) { + case "Create": + return event.LogicalResourceId; + case "Update": + case "Delete": + return event.PhysicalResourceId; + } +} + +// lib/assertions/providers/lambda-handler/assertion.ts +var AssertionHandler = class extends CustomResourceHandler { + async processEvent(request2) { + let actual = decodeCall(request2.actual); + const expected = decodeCall(request2.expected); + let result; + const matcher = new MatchCreator(expected).getMatcher(); + console.log(`Testing equality between ${JSON.stringify(request2.actual)} and ${JSON.stringify(request2.expected)}`); + const matchResult = matcher.test(actual); + matchResult.finished(); + if (matchResult.hasFailed()) { + result = { + data: JSON.stringify({ + status: "fail", + message: [ + ...matchResult.toHumanStrings(), + JSON.stringify(matchResult.target, void 0, 2) + ].join("\n") + }) + }; + } else { + result = { + data: JSON.stringify({ + status: "pass" + }) + }; + } + return result; + } +}; +var MatchCreator = class { + constructor(obj) { + switch (Object.keys(obj)[0]) { + case "$ObjectLike": + this.type = "objectLike"; + this.parsedObj = obj.$ObjectLike; + break; + case "$ArrayWith": + this.type = "arrayWith"; + this.parsedObj = obj.$ArrayWith; + break; + case "$Exact": + this.type = "exact"; + this.parsedObj = obj.$Exact; + break; + case "$StringLike": + this.type = "stringLikeRegexp"; + this.parsedObj = obj.$StringLike; + break; + default: + this.type = "exact"; + this.parsedObj = obj; + } + } + getMatcher() { + try { + const final = JSON.parse(JSON.stringify(this.parsedObj), function(_k, v) { + const nested = Object.keys(v)[0]; + switch (nested) { + case "$ArrayWith": + return Match.arrayWith(v[nested]); + case "$ObjectLike": + return Match.objectLike(v[nested]); + case "$StringLike": + return Match.stringLikeRegexp(v[nested]); + default: + return v; + } + }); + return Match[this.type](final); + } catch { + return Match[this.type](this.parsedObj); + } + } +}; +function decodeCall(call) { + if (!call) { + return void 0; + } + try { + const parsed = JSON.parse(call); + return parsed; + } catch (e) { + return call; + } +} + +// lib/assertions/providers/lambda-handler/results.ts +var ResultsCollectionHandler = class extends CustomResourceHandler { + async processEvent(request2) { + const reduced = request2.assertionResults.reduce((agg, result, idx) => { + const msg = result.status === "pass" ? "pass" : `fail - ${result.message}`; + return `${agg} +Test${idx}: ${msg}`; + }, "").trim(); + return { message: reduced }; + } +}; + +// lib/assertions/providers/lambda-handler/utils.ts +function decode(object) { + return JSON.parse(JSON.stringify(object), (_k, v) => { + switch (v) { + case "TRUE:BOOLEAN": + return true; + case "FALSE:BOOLEAN": + return false; + default: + return v; + } + }); +} + +// lib/assertions/providers/lambda-handler/sdk.ts +function flatten(object) { + return Object.assign({}, ...function _flatten(child, path = []) { + 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)); +} +var AwsApiCallHandler = class extends CustomResourceHandler { + async processEvent(request2) { + const AWS = require("aws-sdk"); + console.log(`AWS SDK VERSION: ${AWS.VERSION}`); + const service = new AWS[request2.service](); + const response = await service[request2.api](request2.parameters && decode(request2.parameters)).promise(); + console.log(`SDK response received ${JSON.stringify(response)}`); + delete response.ResponseMetadata; + const respond = { + apiCallResponse: response + }; + const flatData = __spreadValues({}, flatten(respond)); + return request2.flattenResponse === "true" ? flatData : respond; + } +}; + +// lib/assertions/providers/lambda-handler/types.ts +var ASSERT_RESOURCE_TYPE = "Custom::DeployAssert@AssertEquals"; +var RESULTS_RESOURCE_TYPE = "Custom::DeployAssert@ResultsCollection"; +var SDK_RESOURCE_TYPE_PREFIX = "Custom::DeployAssert@SdkCall"; + +// lib/assertions/providers/lambda-handler/index.ts +async function handler(event, context) { + const provider = createResourceHandler(event, context); + await provider.handle(); +} +function createResourceHandler(event, context) { + if (event.ResourceType.startsWith(SDK_RESOURCE_TYPE_PREFIX)) { + return new AwsApiCallHandler(event, context); + } + switch (event.ResourceType) { + case ASSERT_RESOURCE_TYPE: + return new AssertionHandler(event, context); + case RESULTS_RESOURCE_TYPE: + return new ResultsCollectionHandler(event, context); + default: + throw new Error(`Unsupported resource type "${event.ResourceType}`); + } +} +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + handler +}); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/put-events.integ.snapshot/aws-stepfunctions-tasks-eventbridge-put-events-integ.template.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/put-events.integ.snapshot/aws-stepfunctions-tasks-eventbridge-put-events-integ.template.json index 77e5fcbe59b03..7a6899ba12948 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/put-events.integ.snapshot/aws-stepfunctions-tasks-eventbridge-put-events-integ.template.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/put-events.integ.snapshot/aws-stepfunctions-tasks-eventbridge-put-events-integ.template.json @@ -114,13 +114,6 @@ ] } }, - "Outputs": { - "stateMachineArn": { - "Value": { - "Ref": "StateMachine2E01A3A5" - } - } - }, "Mappings": { "ServiceprincipalMap": { "af-south-1": { @@ -214,5 +207,15 @@ "states": "states.us-west-2.amazonaws.com" } } + }, + "Outputs": { + "ExportsOutputRefStateMachine2E01A3A5BA46F753": { + "Value": { + "Ref": "StateMachine2E01A3A5" + }, + "Export": { + "Name": "aws-stepfunctions-tasks-eventbridge-put-events-integ:ExportsOutputRefStateMachine2E01A3A5BA46F753" + } + } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/put-events.integ.snapshot/cdk.out b/packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/put-events.integ.snapshot/cdk.out index 90bef2e09ad39..ccdfc1ff96a9d 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/put-events.integ.snapshot/cdk.out +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/put-events.integ.snapshot/cdk.out @@ -1 +1 @@ -{"version":"17.0.0"} \ No newline at end of file +{"version":"19.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/put-events.integ.snapshot/integ.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/put-events.integ.snapshot/integ.json index 11fd50aaf44cd..078110c0feb39 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/put-events.integ.snapshot/integ.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/put-events.integ.snapshot/integ.json @@ -1,14 +1,11 @@ { - "version": "18.0.0", + "version": "19.0.0", "testCases": { - "aws-stepfunctions-tasks/test/eventbridge/integ.put-events": { + "PutEvents/DefaultTest": { "stacks": [ "aws-stepfunctions-tasks-eventbridge-put-events-integ" ], - "diffAssets": false, - "stackUpdateWorkflow": true + "assertionStack": "PutEventsDefaultTestDeployAssert1A6BA3F3" } - }, - "synthContext": {}, - "enableLookups": false + } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/put-events.integ.snapshot/manifest.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/put-events.integ.snapshot/manifest.json index 9000908b6b329..ed816bb0e425f 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/put-events.integ.snapshot/manifest.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/put-events.integ.snapshot/manifest.json @@ -1,5 +1,5 @@ { - "version": "17.0.0", + "version": "19.0.0", "artifacts": { "Tree": { "type": "cdk:tree", @@ -39,20 +39,102 @@ "data": "StateMachine2E01A3A5" } ], - "/aws-stepfunctions-tasks-eventbridge-put-events-integ/stateMachineArn": [ + "/aws-stepfunctions-tasks-eventbridge-put-events-integ/Service-principalMap": [ { "type": "aws:cdk:logicalId", - "data": "stateMachineArn" + "data": "ServiceprincipalMap" } ], - "/aws-stepfunctions-tasks-eventbridge-put-events-integ/Service-principalMap": [ + "/aws-stepfunctions-tasks-eventbridge-put-events-integ/Exports/Output{\"Ref\":\"StateMachine2E01A3A5\"}": [ { "type": "aws:cdk:logicalId", - "data": "ServiceprincipalMap" + "data": "ExportsOutputRefStateMachine2E01A3A5BA46F753" } ] }, "displayName": "aws-stepfunctions-tasks-eventbridge-put-events-integ" + }, + "PutEventsDefaultTestDeployAssert1A6BA3F3": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "PutEventsDefaultTestDeployAssert1A6BA3F3.template.json", + "validateOnSynth": false + }, + "dependencies": [ + "aws-stepfunctions-tasks-eventbridge-put-events-integ" + ], + "metadata": { + "/PutEvents/DefaultTest/DeployAssert": [ + { + "type": "aws:cdk:asset", + "data": { + "path": "asset.1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b.bundle", + "id": "1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b", + "packaging": "zip", + "sourceHash": "1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b", + "s3BucketParameter": "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bS3BucketF7210344", + "s3KeyParameter": "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bS3VersionKey1E71961C", + "artifactHashParameter": "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bArtifactHash4F8362F2" + } + } + ], + "/PutEvents/DefaultTest/DeployAssert/Default/AwsApiCallStepFunctionsstartExecution/Default/Default": [ + { + "type": "aws:cdk:logicalId", + "data": "AwsApiCallStepFunctionsstartExecution" + } + ], + "/PutEvents/DefaultTest/DeployAssert/Default/AwsApiCallStepFunctionsdescribeExecution/Default/Default": [ + { + "type": "aws:cdk:logicalId", + "data": "AwsApiCallStepFunctionsdescribeExecution" + } + ], + "/PutEvents/DefaultTest/DeployAssert/Default/AwsApiCallStepFunctionsdescribeExecution/AssertEqualsStepFunctionsdescribeExecution/Default/Default": [ + { + "type": "aws:cdk:logicalId", + "data": "AwsApiCallStepFunctionsdescribeExecutionAssertEqualsStepFunctionsdescribeExecution58E75A69" + } + ], + "/PutEvents/DefaultTest/DeployAssert/Default/AwsApiCallStepFunctionsdescribeExecution/AssertEqualsStepFunctionsdescribeExecution/AssertionResults": [ + { + "type": "aws:cdk:logicalId", + "data": "AssertionResultsAssertEqualsStepFunctionsdescribeExecution" + } + ], + "/PutEvents/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Role": [ + { + "type": "aws:cdk:logicalId", + "data": "SingletonFunction1488541a7b23466481b69b4408076b81Role37ABCE73" + } + ], + "/PutEvents/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Handler": [ + { + "type": "aws:cdk:logicalId", + "data": "SingletonFunction1488541a7b23466481b69b4408076b81HandlerCD40AE9F" + } + ], + "/PutEvents/DefaultTest/DeployAssert/AssetParameters/1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b/S3Bucket": [ + { + "type": "aws:cdk:logicalId", + "data": "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bS3BucketF7210344" + } + ], + "/PutEvents/DefaultTest/DeployAssert/AssetParameters/1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b/S3VersionKey": [ + { + "type": "aws:cdk:logicalId", + "data": "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bS3VersionKey1E71961C" + } + ], + "/PutEvents/DefaultTest/DeployAssert/AssetParameters/1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b/ArtifactHash": [ + { + "type": "aws:cdk:logicalId", + "data": "AssetParameters1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38bArtifactHash4F8362F2" + } + ] + }, + "displayName": "PutEvents/DefaultTest/DeployAssert" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/put-events.integ.snapshot/tree.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/put-events.integ.snapshot/tree.json index 11ad076a1ad87..da0158eadc152 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/put-events.integ.snapshot/tree.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/eventbridge/put-events.integ.snapshot/tree.json @@ -205,14 +205,6 @@ "version": "0.0.0" } }, - "stateMachineArn": { - "id": "stateMachineArn", - "path": "aws-stepfunctions-tasks-eventbridge-put-events-integ/stateMachineArn", - "constructInfo": { - "fqn": "@aws-cdk/core.CfnOutput", - "version": "0.0.0" - } - }, "Service-principalMap": { "id": "Service-principalMap", "path": "aws-stepfunctions-tasks-eventbridge-put-events-integ/Service-principalMap", @@ -220,12 +212,294 @@ "fqn": "@aws-cdk/core.CfnMapping", "version": "0.0.0" } + }, + "Exports": { + "id": "Exports", + "path": "aws-stepfunctions-tasks-eventbridge-put-events-integ/Exports", + "children": { + "Output{\"Ref\":\"StateMachine2E01A3A5\"}": { + "id": "Output{\"Ref\":\"StateMachine2E01A3A5\"}", + "path": "aws-stepfunctions-tasks-eventbridge-put-events-integ/Exports/Output{\"Ref\":\"StateMachine2E01A3A5\"}", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnOutput", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Construct", + "version": "0.0.0" + } } }, "constructInfo": { "fqn": "@aws-cdk/core.Stack", "version": "0.0.0" } + }, + "PutEvents": { + "id": "PutEvents", + "path": "PutEvents", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "PutEvents/DefaultTest", + "children": { + "DeployAssert": { + "id": "DeployAssert", + "path": "PutEvents/DefaultTest/DeployAssert", + "children": { + "Default": { + "id": "Default", + "path": "PutEvents/DefaultTest/DeployAssert/Default", + "children": { + "AwsApiCallStepFunctionsstartExecution": { + "id": "AwsApiCallStepFunctionsstartExecution", + "path": "PutEvents/DefaultTest/DeployAssert/Default/AwsApiCallStepFunctionsstartExecution", + "children": { + "SdkProvider": { + "id": "SdkProvider", + "path": "PutEvents/DefaultTest/DeployAssert/Default/AwsApiCallStepFunctionsstartExecution/SdkProvider", + "children": { + "AssertionsProvider": { + "id": "AssertionsProvider", + "path": "PutEvents/DefaultTest/DeployAssert/Default/AwsApiCallStepFunctionsstartExecution/SdkProvider/AssertionsProvider", + "constructInfo": { + "fqn": "@aws-cdk/core.Construct", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.AssertionsProvider", + "version": "0.0.0" + } + }, + "Default": { + "id": "Default", + "path": "PutEvents/DefaultTest/DeployAssert/Default/AwsApiCallStepFunctionsstartExecution/Default", + "children": { + "Default": { + "id": "Default", + "path": "PutEvents/DefaultTest/DeployAssert/Default/AwsApiCallStepFunctionsstartExecution/Default/Default", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.CustomResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.AwsApiCall", + "version": "0.0.0" + } + }, + "AwsApiCallStepFunctionsdescribeExecution": { + "id": "AwsApiCallStepFunctionsdescribeExecution", + "path": "PutEvents/DefaultTest/DeployAssert/Default/AwsApiCallStepFunctionsdescribeExecution", + "children": { + "SdkProvider": { + "id": "SdkProvider", + "path": "PutEvents/DefaultTest/DeployAssert/Default/AwsApiCallStepFunctionsdescribeExecution/SdkProvider", + "children": { + "AssertionsProvider": { + "id": "AssertionsProvider", + "path": "PutEvents/DefaultTest/DeployAssert/Default/AwsApiCallStepFunctionsdescribeExecution/SdkProvider/AssertionsProvider", + "constructInfo": { + "fqn": "@aws-cdk/core.Construct", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.AssertionsProvider", + "version": "0.0.0" + } + }, + "Default": { + "id": "Default", + "path": "PutEvents/DefaultTest/DeployAssert/Default/AwsApiCallStepFunctionsdescribeExecution/Default", + "children": { + "Default": { + "id": "Default", + "path": "PutEvents/DefaultTest/DeployAssert/Default/AwsApiCallStepFunctionsdescribeExecution/Default/Default", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.CustomResource", + "version": "0.0.0" + } + }, + "AssertEqualsStepFunctionsdescribeExecution": { + "id": "AssertEqualsStepFunctionsdescribeExecution", + "path": "PutEvents/DefaultTest/DeployAssert/Default/AwsApiCallStepFunctionsdescribeExecution/AssertEqualsStepFunctionsdescribeExecution", + "children": { + "AssertionProvider": { + "id": "AssertionProvider", + "path": "PutEvents/DefaultTest/DeployAssert/Default/AwsApiCallStepFunctionsdescribeExecution/AssertEqualsStepFunctionsdescribeExecution/AssertionProvider", + "children": { + "AssertionsProvider": { + "id": "AssertionsProvider", + "path": "PutEvents/DefaultTest/DeployAssert/Default/AwsApiCallStepFunctionsdescribeExecution/AssertEqualsStepFunctionsdescribeExecution/AssertionProvider/AssertionsProvider", + "constructInfo": { + "fqn": "@aws-cdk/core.Construct", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.AssertionsProvider", + "version": "0.0.0" + } + }, + "Default": { + "id": "Default", + "path": "PutEvents/DefaultTest/DeployAssert/Default/AwsApiCallStepFunctionsdescribeExecution/AssertEqualsStepFunctionsdescribeExecution/Default", + "children": { + "Default": { + "id": "Default", + "path": "PutEvents/DefaultTest/DeployAssert/Default/AwsApiCallStepFunctionsdescribeExecution/AssertEqualsStepFunctionsdescribeExecution/Default/Default", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.CustomResource", + "version": "0.0.0" + } + }, + "AssertionResults": { + "id": "AssertionResults", + "path": "PutEvents/DefaultTest/DeployAssert/Default/AwsApiCallStepFunctionsdescribeExecution/AssertEqualsStepFunctionsdescribeExecution/AssertionResults", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnOutput", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.EqualsAssertion", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.AwsApiCall", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.DeployAssert", + "version": "0.0.0" + } + }, + "SingletonFunction1488541a7b23466481b69b4408076b81": { + "id": "SingletonFunction1488541a7b23466481b69b4408076b81", + "path": "PutEvents/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81", + "children": { + "Staging": { + "id": "Staging", + "path": "PutEvents/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Staging", + "constructInfo": { + "fqn": "@aws-cdk/core.AssetStaging", + "version": "0.0.0" + } + }, + "Role": { + "id": "Role", + "path": "PutEvents/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Role", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + }, + "Handler": { + "id": "Handler", + "path": "PutEvents/DefaultTest/DeployAssert/SingletonFunction1488541a7b23466481b69b4408076b81/Handler", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Construct", + "version": "0.0.0" + } + }, + "AssetParameters": { + "id": "AssetParameters", + "path": "PutEvents/DefaultTest/DeployAssert/AssetParameters", + "children": { + "1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b": { + "id": "1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b", + "path": "PutEvents/DefaultTest/DeployAssert/AssetParameters/1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b", + "children": { + "S3Bucket": { + "id": "S3Bucket", + "path": "PutEvents/DefaultTest/DeployAssert/AssetParameters/1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b/S3Bucket", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "S3VersionKey": { + "id": "S3VersionKey", + "path": "PutEvents/DefaultTest/DeployAssert/AssetParameters/1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b/S3VersionKey", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "ArtifactHash": { + "id": "ArtifactHash", + "path": "PutEvents/DefaultTest/DeployAssert/AssetParameters/1bc7cf3a01a7153f942391263b3bac937812996cc28f9abaf83ffebbbe03e38b/ArtifactHash", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Construct", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Construct", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Construct", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Construct", + "version": "0.0.0" + } } }, "constructInfo": { diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/integ-tests/test-case.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/integ-tests/test-case.ts index d04ffad502c67..c3de8c8711853 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/integ-tests/test-case.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/integ-tests/test-case.ts @@ -73,6 +73,13 @@ export interface TestCase extends TestOptions { * `exclusively` is passed */ readonly stacks: string[]; + + /** + * The name of the stack that contains assertions + * + * @default - no assertion stack + */ + readonly assertionStack?: string; } /** diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json index 2efc89439fab8..ccdfc1ff96a9d 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json @@ -1 +1 @@ -{"version":"18.0.0"} \ No newline at end of file +{"version":"19.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/integ.schema.json b/packages/@aws-cdk/cloud-assembly-schema/schema/integ.schema.json index 94306087e1147..f18827244648c 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/integ.schema.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/integ.schema.json @@ -45,6 +45,10 @@ "type": "string" } }, + "assertionStack": { + "description": "The name of the stack that contains assertions (Default - no assertion stack)", + "type": "string" + }, "stackUpdateWorkflow": { "description": "Run update workflow on this test case\nThis should only be set to false to test scenarios\nthat are not possible to test as part of the update workflow (Default true)", "type": "boolean" diff --git a/packages/@aws-cdk/integ-runner/lib/runner/integ-test-runner.ts b/packages/@aws-cdk/integ-runner/lib/runner/integ-test-runner.ts index aa51bd3523e33..705f9f045e6e9 100644 --- a/packages/@aws-cdk/integ-runner/lib/runner/integ-test-runner.ts +++ b/packages/@aws-cdk/integ-runner/lib/runner/integ-test-runner.ts @@ -1,9 +1,10 @@ import * as path from 'path'; import { RequireApproval } from '@aws-cdk/cloud-assembly-schema'; import { DeployOptions, DestroyOptions } from 'cdk-cli-wrapper'; +import * as fs from 'fs-extra'; import * as logger from '../logger'; import { chain, exec } from '../utils'; -import { DestructiveChange } from '../workers/common'; +import { DestructiveChange, AssertionResults, AssertionResult } from '../workers/common'; import { IntegRunnerOptions, IntegRunner, DEFAULT_SYNTH_OPTIONS } from './runner-base'; /** @@ -125,14 +126,15 @@ export class IntegTestRunner extends IntegRunner { * The update workflow exists to check for cases where a change would cause * a failure to an existing stack, but not for a newly created stack. */ - public runIntegTestCase(options: RunOptions): void { + public runIntegTestCase(options: RunOptions): AssertionResults | undefined { + let assertionResults: AssertionResults | undefined; const actualTestCase = this.actualTestSuite.testSuite[options.testCaseName]; const clean = options.clean ?? true; const updateWorkflowEnabled = (options.updateWorkflow ?? true) && (actualTestCase.stackUpdateWorkflow ?? true); try { if (!options.dryRun && (actualTestCase.cdkCommandOptions?.deploy?.enabled ?? true)) { - this.deploy( + assertionResults = this.deploy( { ...this.defaultArgs, profile: this.profile, @@ -152,7 +154,11 @@ export class IntegTestRunner extends IntegRunner { output: this.cdkOutDir, }); } - this.createSnapshot(); + // only create the snapshot if there are no assertion assertion results + // (i.e. no failures) + if (!assertionResults) { + this.createSnapshot(); + } } catch (e) { throw e; } finally { @@ -172,6 +178,7 @@ export class IntegTestRunner extends IntegRunner { } this.cleanup(); } + return assertionResults; } /** @@ -210,7 +217,7 @@ export class IntegTestRunner extends IntegRunner { deployArgs: DeployOptions, updateWorkflowEnabled: boolean, testCaseName: string, - ): void { + ): AssertionResults | undefined { const actualTestCase = this.actualTestSuite.testSuite[testCaseName]; try { if (actualTestCase.hooks?.preDeploy) { @@ -238,26 +245,79 @@ export class IntegTestRunner extends IntegRunner { lookups: this.expectedTestSuite?.enableLookups, }); } + // now deploy the "actual" test. If there are any assertions + // deploy the assertion stack as well this.cdk.deploy({ ...deployArgs, lookups: this.actualTestSuite.enableLookups, - stacks: actualTestCase.stacks, + stacks: [ + ...actualTestCase.stacks, + ...actualTestCase.assertionStack ? [actualTestCase.assertionStack] : [], + ], + rollback: false, output: this.cdkOutDir, ...actualTestCase?.cdkCommandOptions?.deploy?.args, + ...actualTestCase.assertionStack ? { outputsFile: path.join(this.cdkOutDir, 'assertion-results.json') } : undefined, context: this.getContext(actualTestCase?.cdkCommandOptions?.deploy?.args?.context), app: this.hasTmpActualSnapshot() ? this.cdkOutDir : this.cdkApp, }); + if (actualTestCase.hooks?.postDeploy) { exec([chain(actualTestCase.hooks?.postDeploy)], { cwd: path.dirname(this.snapshotDir), }); } + + if (actualTestCase.assertionStack) { + return this.processAssertionResults( + path.join(this.directory, this.cdkOutDir, 'assertion-results.json'), + actualTestCase.assertionStack, + ); + } } catch (e) { this.parseError(e, actualTestCase.cdkCommandOptions?.deploy?.expectError ?? false, actualTestCase.cdkCommandOptions?.deploy?.expectedMessage, ); } + return; + } + + /** + * Process the outputsFile which contains the assertions results as stack + * outputs + */ + private processAssertionResults(file: string, assertionStackId: string): AssertionResults | undefined { + const results: AssertionResults = {}; + if (fs.existsSync(file)) { + try { + const outputs: { [key: string]: { [key: string]: string } } = fs.readJSONSync(file); + + if (assertionStackId in outputs) { + for (const [assertionId, result] of Object.entries(outputs[assertionStackId])) { + if (assertionId.startsWith('AssertionResults')) { + const assertionResult: AssertionResult = JSON.parse(result.replace(/\n/g, '\\n')); + if (assertionResult.status === 'fail') { + results[assertionId] = assertionResult; + } + } + } + } + } catch (e) { + // if there are outputs, but they cannot be processed, then throw an error + // so that the test fails + results[assertionStackId] = { + status: 'fail', + message: `error processing assertion results: ${e}`, + }; + } finally { + // remove the outputs file so it is not part of the snapshot + // it will contain env specific information from values + // resolved at deploy time + fs.unlinkSync(file); + } + } + return Object.keys(results).length > 0 ? results : undefined; } /** @@ -276,4 +336,3 @@ export class IntegTestRunner extends IntegRunner { } } } - diff --git a/packages/@aws-cdk/integ-runner/lib/workers/common.ts b/packages/@aws-cdk/integ-runner/lib/workers/common.ts index 8b9e297e772ec..bcd6a03e36d2b 100644 --- a/packages/@aws-cdk/integ-runner/lib/workers/common.ts +++ b/packages/@aws-cdk/integ-runner/lib/workers/common.ts @@ -1,8 +1,30 @@ +import { format } from 'util'; import { ResourceImpact } from '@aws-cdk/cloudformation-diff'; import * as chalk from 'chalk'; import * as logger from '../logger'; import { IntegTestConfig } from '../runner/integration-tests'; +/** + * The aggregate results from running assertions on a test case + */ +export type AssertionResults = { [id: string]: AssertionResult }; + +/** + * The result of an individual assertion + */ +export interface AssertionResult { + /** + * The assertion message. If the assertion failed, this will + * include the reason. + */ + readonly message: string; + + /** + * Whether the assertion succeeded or failed + */ + readonly status: 'success' | 'fail'; +} + /** * Config for an integration test */ @@ -155,6 +177,11 @@ export enum DiagnosticReason { * The integration test succeeded */ TEST_SUCCESS = 'TEST_SUCCESS', + + /** + * The assertion failed + */ + ASSERTION_FAILED = 'ASSERTION_FAILED', } /** @@ -191,6 +218,16 @@ export function printSummary(total: number, failed: number): void { } } +/** + * Format the assertion results so that the results can be + * printed + */ +export function formatAssertionResults(results: AssertionResults): string { + return Object.entries(results) + .map(([id, result]) => format('%s\n%s', id, result.message)) + .join('\n'); +} + /** * Print out the results from tests */ @@ -210,5 +247,8 @@ export function printResults(diagnostic: Diagnostic): void { break; case DiagnosticReason.TEST_FAILED: logger.error(' %s - Failed! %s\n%s', diagnostic.testName, chalk.gray(`${diagnostic.duration}s`), diagnostic.message); + break; + case DiagnosticReason.ASSERTION_FAILED: + logger.error(' %s - Assertions Failed! %s\n%s', diagnostic.testName, chalk.gray(`${diagnostic.duration}s`), diagnostic.message); } } diff --git a/packages/@aws-cdk/integ-runner/lib/workers/extract/extract_worker.ts b/packages/@aws-cdk/integ-runner/lib/workers/extract/extract_worker.ts index 748e0d1b88448..c38d97a55856c 100644 --- a/packages/@aws-cdk/integ-runner/lib/workers/extract/extract_worker.ts +++ b/packages/@aws-cdk/integ-runner/lib/workers/extract/extract_worker.ts @@ -1,7 +1,7 @@ import * as workerpool from 'workerpool'; import { IntegSnapshotRunner, IntegTestRunner } from '../../runner'; import { IntegTestConfig } from '../../runner/integration-tests'; -import { DiagnosticReason, IntegTestWorkerConfig } from '../common'; +import { DiagnosticReason, IntegTestWorkerConfig, formatAssertionResults } from '../common'; import { IntegTestBatchRequest } from '../integ-test-worker'; /** @@ -32,23 +32,33 @@ export function integTestWorker(request: IntegTestBatchRequest): IntegTestWorker } for (const testCaseName of Object.keys(tests)) { try { - runner.runIntegTestCase({ + const results = runner.runIntegTestCase({ testCaseName, clean: request.clean, dryRun: request.dryRun, updateWorkflow: request.updateWorkflow, }); - workerpool.workerEmit({ - reason: DiagnosticReason.TEST_SUCCESS, - testName: testCaseName, - message: 'Success', - duration: (Date.now() - start) / 1000, - }); + if (results) { + failures.push(test); + workerpool.workerEmit({ + reason: DiagnosticReason.ASSERTION_FAILED, + testName: `${runner.testName}-${testCaseName} (${request.profile}/${request.region})`, + message: formatAssertionResults(results), + duration: (Date.now() - start) / 1000, + }); + } else { + workerpool.workerEmit({ + reason: DiagnosticReason.TEST_SUCCESS, + testName: `${runner.testName}-${testCaseName}`, + message: 'Success', + duration: (Date.now() - start) / 1000, + }); + } } catch (e) { failures.push(test); workerpool.workerEmit({ reason: DiagnosticReason.TEST_FAILED, - testName: testCaseName, + testName: `${runner.testName}-${testCaseName} (${request.profile}/${request.region})`, message: `Integration test failed: ${e}`, duration: (Date.now() - start) / 1000, }); @@ -58,7 +68,7 @@ export function integTestWorker(request: IntegTestBatchRequest): IntegTestWorker failures.push(test); workerpool.workerEmit({ reason: DiagnosticReason.TEST_FAILED, - testName: test.fileName, + testName: `${test.fileName} (${request.profile}/${request.region})`, message: `Integration test failed: ${e}`, duration: (Date.now() - start) / 1000, }); diff --git a/packages/@aws-cdk/integ-runner/test/runner/integ-test-runner.test.ts b/packages/@aws-cdk/integ-runner/test/runner/integ-test-runner.test.ts index c75a8b23e247f..78f321e81d9aa 100644 --- a/packages/@aws-cdk/integ-runner/test/runner/integ-test-runner.test.ts +++ b/packages/@aws-cdk/integ-runner/test/runner/integ-test-runner.test.ts @@ -87,6 +87,7 @@ describe('IntegTest runIntegTests', () => { context: expect.any(Object), versionReporting: false, lookups: false, + rollback: false, stacks: ['test-stack', 'new-test-stack'], }); expect(destroyMock).toHaveBeenCalledWith({ @@ -127,6 +128,7 @@ describe('IntegTest runIntegTests', () => { context: expect.not.objectContaining({ [AVAILABILITY_ZONE_FALLBACK_CONTEXT_KEY]: ['test-region-1a', 'test-region-1b', 'test-region-1c'], }), + rollback: false, lookups: false, stacks: ['stack1'], output: 'cdk-integ.out.integ-test1', @@ -170,6 +172,7 @@ describe('IntegTest runIntegTests', () => { }), versionReporting: false, lookups: true, + rollback: false, stacks: ['test-stack'], output: 'cdk-integ.out.test-with-snapshot-assets-diff', profile: undefined, @@ -324,6 +327,7 @@ describe('IntegTest runIntegTests', () => { versionReporting: false, context: expect.any(Object), profile: 'test-profile', + rollback: false, lookups: false, stacks: ['stack1'], output: 'cdk-integ.out.integ-test1', diff --git a/packages/@aws-cdk/integ-runner/test/test-data/test-with-snapshot-assets-diff.integ.snapshot/manifest.json b/packages/@aws-cdk/integ-runner/test/test-data/test-with-snapshot-assets-diff.integ.snapshot/manifest.json index 0ee57e25c6164..d1c7a0ea225c1 100644 --- a/packages/@aws-cdk/integ-runner/test/test-data/test-with-snapshot-assets-diff.integ.snapshot/manifest.json +++ b/packages/@aws-cdk/integ-runner/test/test-data/test-with-snapshot-assets-diff.integ.snapshot/manifest.json @@ -1,5 +1,5 @@ { - "version": "18.0.0", + "version": "19.0.0", "artifacts": { "Tree": { "type": "cdk:tree", diff --git a/packages/@aws-cdk/integ-runner/test/test-data/test-with-snapshot-assets.integ.snapshot/manifest.json b/packages/@aws-cdk/integ-runner/test/test-data/test-with-snapshot-assets.integ.snapshot/manifest.json index b0fce9b7b6d09..8ef72f9af40ea 100644 --- a/packages/@aws-cdk/integ-runner/test/test-data/test-with-snapshot-assets.integ.snapshot/manifest.json +++ b/packages/@aws-cdk/integ-runner/test/test-data/test-with-snapshot-assets.integ.snapshot/manifest.json @@ -1,5 +1,5 @@ { - "version": "18.0.0", + "version": "19.0.0", "artifacts": { "Tree": { "type": "cdk:tree", diff --git a/packages/@aws-cdk/integ-runner/test/test-data/test-with-snapshot.integ.snapshot/manifest.json b/packages/@aws-cdk/integ-runner/test/test-data/test-with-snapshot.integ.snapshot/manifest.json index 4be1dff08ac1e..bc0e09d1ce230 100644 --- a/packages/@aws-cdk/integ-runner/test/test-data/test-with-snapshot.integ.snapshot/manifest.json +++ b/packages/@aws-cdk/integ-runner/test/test-data/test-with-snapshot.integ.snapshot/manifest.json @@ -1,5 +1,5 @@ { - "version": "18.0.0", + "version": "19.0.0", "artifacts": { "Tree": { "type": "cdk:tree", diff --git a/packages/@aws-cdk/integ-tests/README.md b/packages/@aws-cdk/integ-tests/README.md index aaa74d5447641..0e8fc9b1ca501 100644 --- a/packages/@aws-cdk/integ-tests/README.md +++ b/packages/@aws-cdk/integ-tests/README.md @@ -16,8 +16,52 @@ +## Overview + +This library is meant to be used in combination with the [integ-runner]() CLI +to enable users to write and execute integration tests for AWS CDK Constructs. + +An integration test should be defined as a CDK application, and +there should be a 1:1 relationship between an integration test and a CDK application. + +So for example, in order to create an integration test called `my-function` +we would need to create a file to contain our integration test application. + +*test/integ.my-function.ts* + +```ts +const app = new App(); +const stack = new Stack(); +new lambda.Function(stack, 'MyFunction', { + runtime: lambda.Runtime.NODEJS_12_X, + handler: 'index.handler', + code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-handler')), +}); +``` + +This is a self contained CDK application which we could deploy by running + +```bash +cdk deploy --app 'node test/integ.my-function.js' +``` + +In order to turn this into an integration test, all that is needed is to +use the `IntegTest` construct. + +```ts +declare const app: App; +declare const stack: Stack; +new IntegTest(app, 'Integ', { testCases: [stack] }); +``` + +You will notice that the `stack` is registered to the `IntegTest` as a test case. +Each integration test can contain multiple test cases, which are just instances +of a stack. See the [Usage](#usage) section for more details. + ## Usage +### IntegTest + Suppose you have a simple stack, that only encapsulates a Lambda function with a certain handler: @@ -66,10 +110,8 @@ class StackUnderTest extends Stack { // Beginning of the test suite const app = new App(); -const stack = new Stack(app, 'stack'); - -const differentArchsCase = new IntegTestCase(stack, 'DifferentArchitectures', { - stacks: [ +new IntegTest(app, 'DifferentArchitectures', { + testCases: [ new StackUnderTest(app, 'Stack1', { architecture: lambda.Architecture.ARM_64, }), @@ -78,13 +120,6 @@ const differentArchsCase = new IntegTestCase(stack, 'DifferentArchitectures', { }), ], }); - -// There must be exactly one instance of TestCase per file -new IntegTest(app, 'integ-test', { - - // Register as many test cases as you want here - testCases: [differentArchsCase], -}); ``` This is all the instruction you need for the integration test runner to know @@ -98,8 +133,8 @@ const stackUnderTest = new Stack(app, 'StackUnderTest', /* ... */); const stack = new Stack(app, 'stack'); -const testCase = new IntegTestCase(stack, 'CustomizedDeploymentWorkflow', { - stacks: [stackUnderTest], +const testCase = new IntegTest(app, 'CustomizedDeploymentWorkflow', { + testCases: [stackUnderTest], diffAssets: true, stackUpdateWorkflow: true, cdkCommandOptions: { @@ -116,9 +151,226 @@ const testCase = new IntegTestCase(stack, 'CustomizedDeploymentWorkflow', { }, }, }); +``` + +### IntegTestCaseStack -new IntegTest(app, 'integ-test', { - testCases: [testCase], +In the majority of cases an integration test will contain a single `IntegTestCase`. +By default when you create an `IntegTest` an `IntegTestCase` is created for you +and all of your test cases are registered to this `IntegTestCase`. The `IntegTestCase` +and `IntegTestCaseStack` constructs are only needed when it is necessary to +defined different options for individual test cases. + +For example, you might want to have one test case where `diffAssets` is enabled. + +```ts +declare const app: App; +declare const stackUnderTest: Stack; +const testCaseWithAssets = new IntegTestCaseStack(app, 'TestCaseAssets', { + diffAssets: true, +}); + +new IntegTest(app, 'Integ', { testCases: [stackUnderTest, testCaseWithAssets] }); +``` + +## Assertions + +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. + +```ts +declare const app: App; +declare const stack: Stack; + +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`. + +Any assertions that you create should be created in the scope of `DeployAssert`. For example, + +```ts +declare const app: App; + +const assert = new DeployAssert(app); +new AwsApiCall(assert, 'GetObject', { + service: 'S3', + api: 'getObject', }); ``` +`DeployAssert` also provides utilities to register your own assertions. + +```ts +declare const myCustomResource: CustomResource; +declare const app: App; +const assert = new DeployAssert(app); +assert.assert( + 'CustomAssertion', + ExpectedResult.objectLike({ foo: 'bar' }), + ActualResult.fromCustomResource(myCustomResource, 'data'), +); +``` + +In the above example an assertion is created that will trigger a user defined `CustomResource` +and assert that the `data` attribute is equal to `{ foo: 'bar' }`. + +### AwsApiCall + +A common method to retrieve the "actual" results to compare with what is expected is to make an +AWS API call to receive some data. This library does this by utilizing CloudFormation custom resources +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: + +```ts +declare const assert: DeployAssert; + +new AwsApiCall(assert, 'MyAssertion', { + service: 'SQS', + api: 'receiveMessage', + parameters: { + QueueUrl: 'url', + }, +}); +``` + +Or by using the `awsApiCall` method on `DeployAssert`: + +```ts +declare const app: App; +const assert = new DeployAssert(app); +assert.awsApiCall('SQS', 'receiveMessage', { + QueueUrl: 'url', +}); +``` + +### EqualsAssertion + +This library currently provides the ability to assert that two values are equal +to one another by utilizing the `EqualsAssertion` class. This utilizes a Lambda +backed `CustomResource` which in tern uses the [Match](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.assertions.Match.html) utility from the +[@aws-cdk/assertions](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.assertions-readme.html) library. + + +```ts +declare const app: App; +declare const stack: Stack; +declare const queue: sqs.Queue; +declare const fn: lambda.IFunction; + +const integ = new IntegTest(app, 'Integ', { + testCases: [stack], +}); + +integ.assert.invokeFunction({ + functionName: fn.functionName, + invocationType: InvocationType.EVENT, + payload: JSON.stringify({ status: 'OK' }), +}); + +const message = integ.assert.awsApiCall('SQS', 'receiveMessage', { + QueueUrl: queue.queueUrl, + WaitTimeSeconds: 20, +}); + +new EqualsAssertion(integ.assert, 'ReceiveMessage', { + actual: ActualResult.fromAwsApiCall(message, 'Messages.0.Body'), + expected: ExpectedResult.objectLike({ + requestContext: { + condition: 'Success', + }, + requestPayload: { + status: 'OK', + }, + responseContext: { + statusCode: 200, + }, + responsePayload: 'success', + }), +}); +``` + +#### Match + +`integ-tests` also provides a `Match` utility similar to the `@aws-cdk/assertions` module. `Match` +can be used to construct the `ExpectedResult`. + +```ts +declare const message: AwsApiCall; +declare const assert: DeployAssert; + +message.assert(ExpectedResult.objectLike({ + Messages: Match.arrayWith([ + { + Body: { + Values: Match.arrayWith([{ Asdf: 3 }]), + Message: Match.stringLikeRegexp('message'), + }, + }, + ]), +})); +``` + +### Examples + +#### Invoke a Lambda Function + +In this example there is a Lambda Function that is invoked and +we assert that the payload that is returned is equal to '200'. + +```ts +declare const lambdaFunction: lambda.IFunction; +declare const app: App; + +const stack = new Stack(app, 'cdk-integ-lambda-bundling'); + +const integ = new IntegTest(app, 'IntegTest', { + testCases: [stack], +}); + +const invoke = integ.assert.invokeFunction({ + functionName: lambdaFunction.functionName, +}); +invoke.assert(ExpectedResult.objectLike({ + Payload: '200', +})); +``` + +#### Make an AWS API Call + +In this example there is a StepFunctions state machine that is executed +and then we assert that the result of the execution is successful. + +```ts +declare const app: App; +declare const stack: Stack; +declare const sm: IStateMachine; + +const testCase = new IntegTest(app, 'IntegTest', { + testCases: [stack], +}); + +// Start an execution +const start = testCase.assert.awsApiCall('StepFunctions', 'startExecution', { + stateMachineArn: sm.stateMachineArn, +}); + +// describe the results of the execution +const describe = testCase.assert.awsApiCall('StepFunctions', 'describeExecution', { + executionArn: start.getAttString('executionArn'), +}); + +// assert the results +describe.assert(ExpectedResult.objectLike({ + status: 'SUCCEEDED', +})); +``` + diff --git a/packages/@aws-cdk/integ-tests/lib/assertions/assertions.ts b/packages/@aws-cdk/integ-tests/lib/assertions/assertions.ts index 1b3ba0f14f7bf..c7f20e005e526 100644 --- a/packages/@aws-cdk/integ-tests/lib/assertions/assertions.ts +++ b/packages/@aws-cdk/integ-tests/lib/assertions/assertions.ts @@ -1,37 +1,47 @@ -import { CustomResource } from '@aws-cdk/core'; +import { CustomResource, CfnOutput } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { IAssertion } from './deploy-assert'; -import { AssertionRequest, AssertionsProvider, ASSERT_RESOURCE_TYPE, AssertionType } from './providers'; -// +import { ExpectedResult, ActualResult } from './common'; +import { AssertionRequest, AssertionsProvider, ASSERT_RESOURCE_TYPE } 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 + * The actual results to compare */ - readonly inputResource: CustomResource; + readonly actual: ActualResult; /** - * The CustomResource attribute that continains the "actual" results + * The expected result to assert */ - readonly inputResourceAtt: string; + readonly expected: ExpectedResult; /** - * The expected result to assert + * Set this to true if a failed assertion should + * result in a CloudFormation deployment failure + * + * This is only necessary if assertions are being + * executed outside of `integ-runner`. + * + * @default false */ - readonly expected: any; + readonly failDeployment?: boolean; } /** * Construct that creates a CustomResource to assert that two * values are equal */ -export class EqualsAssertion extends CoreConstruct implements IAssertion { +export class EqualsAssertion extends CoreConstruct { + /** + * The result of the assertion + */ public readonly result: string; constructor(scope: Construct, id: string, props: EqualsAssertionProps) { @@ -39,15 +49,22 @@ export class EqualsAssertion extends CoreConstruct implements IAssertion { const assertionProvider = new AssertionsProvider(this, 'AssertionProvider'); const properties: AssertionRequest = { - actual: props.inputResource.getAttString(props.inputResourceAtt), - expected: props.expected, - assertionType: AssertionType.EQUALS, + actual: props.actual.result, + expected: props.expected.result, + failDeployment: props.failDeployment, }; const resource = new CustomResource(this, 'Default', { serviceToken: assertionProvider.serviceToken, - properties, + properties: { + ...properties, + salt: Date.now().toString(), // always update, + }, resourceType: ASSERT_RESOURCE_TYPE, }); this.result = resource.getAttString('data'); + + new CfnOutput(this, 'AssertionResults', { + value: this.result, + }).overrideLogicalId(`AssertionResults${id}`); } } diff --git a/packages/@aws-cdk/integ-tests/lib/assertions/common.ts b/packages/@aws-cdk/integ-tests/lib/assertions/common.ts new file mode 100644 index 0000000000000..6e4fadf5a0388 --- /dev/null +++ b/packages/@aws-cdk/integ-tests/lib/assertions/common.ts @@ -0,0 +1,141 @@ +import { CustomResource } from '@aws-cdk/core'; +import { AwsApiCall } from './sdk'; +/** + * Represents the "actual" results to compare + */ +export abstract class ActualResult { + /** + * Get the actual results from a CustomResource + */ + public static fromCustomResource(customResource: CustomResource, attribute: string): ActualResult { + return { + result: customResource.getAttString(attribute), + }; + } + + /** + * Get the actual results from a AwsApiCall + */ + public static fromAwsApiCall(query: AwsApiCall, attribute: string): ActualResult { + return { + result: query.getAttString(attribute), + }; + } + + /** + * The actual results as a string + */ + public abstract result: string; +} + +/** + * Represents the "expected" results to compare + */ +export abstract class ExpectedResult { + /** + * The actual results must match exactly. Missing data + * will result in a failure + * + * @example + * // actual results + * const actual = { + * stringParam: 'hello', + * numberParam: 3, + * booleanParam: true, + * }; + * // pass + * ExpectedResult.exact({ + * stringParam: 'hello', + * numberParam: 3, + * booleanParam: true, + * }) + * + * // fail + * ExpectedResult.exact({ + * stringParam: 'hello', + * }); + */ + public static exact(expected: any): ExpectedResult { + return { + result: JSON.stringify({ + $Exact: expected, + }), + }; + } + + /** + * The expected results must be a subset of the + * actual results. + * + * @example + * // actual results + * const actual = { + * stringParam: 'hello', + * numberParam: 3, + * booleanParam: true, + * }; + * // pass + * ExpectedResult.objectLike({ + * stringParam: 'hello', + * }); + */ + public static objectLike(expected: { [key: string]: any }): ExpectedResult { + return { + result: JSON.stringify({ + $ObjectLike: expected, + }), + }; + } + + /** + * The actual results must be a list and must contain + * an item with the expected results. + * + * @example + * // actual results + * const actual = [ + * { + * stringParam: 'hello', + * }, + * { + * stringParam: 'world', + * }, + * ]; + * // pass + * ExpectedResult.arrayWith([ + * { + * stringParam: 'hello', + * }, + * ]); + */ + public static arrayWith(expected: any[]): ExpectedResult { + return { + result: JSON.stringify({ + $ArrayWith: expected, + }), + }; + } + /** + * Actual results is a string that matches + * the Expected result regex + * + * @example + * // actual results + * const actual = 'some string value'; + * + * // pass + * ExpectedResult.stringLikeRegexp('value'); + */ + public static stringLikeRegexp(expected: string): ExpectedResult { + return { + result: JSON.stringify({ + $StringLike: expected, + }), + }; + } + + /** + * The expected results encoded as a string + */ + public abstract result: string; +} diff --git a/packages/@aws-cdk/integ-tests/lib/assertions/deploy-assert.ts b/packages/@aws-cdk/integ-tests/lib/assertions/deploy-assert.ts index 8ef74b5ce56a5..24bbfd6789fbf 100644 --- a/packages/@aws-cdk/integ-tests/lib/assertions/deploy-assert.ts +++ b/packages/@aws-cdk/integ-tests/lib/assertions/deploy-assert.ts @@ -1,25 +1,17 @@ -import { CfnOutput, CustomResource, Lazy } from '@aws-cdk/core'; +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 { RESULTS_RESOURCE_TYPE, AssertionsProvider } from './providers'; -import { SdkQuery, SdkQueryOptions } from './sdk'; +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'; -/** - * Represents a deploy time assertion - */ -export interface IAssertion { - /** - * The result of the assertion - */ - readonly result: string; -} - /** * Options for DeployAssert */ @@ -42,7 +34,7 @@ export class DeployAssert extends CoreConstruct { * Finds a DeployAssert construct in the given scope */ public static of(construct: IConstruct): DeployAssert { - const scopes = Node.of(construct).scopes.reverse(); + 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'); @@ -50,46 +42,87 @@ export class DeployAssert extends CoreConstruct { return deployAssert as DeployAssert; } - /** @internal */ - public readonly _assertions: IAssertion[]; - constructor(scope: Construct) { - super(scope, 'DeployAssert'); + /** + * 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 }); - 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, + /** + * 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, }); - - // 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 + * 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 queryAws(options: SdkQueryOptions): SdkQuery { - const id = md5hash(options); - return new SdkQuery(this, `SdkQuery${id}`, options); + public invokeFunction(props: LambdaInvokeFunctionProps): LambdaInvokeFunction { + const hash = md5hash(Stack.of(this).resolve(props)); + return new LambdaInvokeFunction(this, `LambdaInvoke${hash}`, props); } /** - * Register an assertion that should be run as part of the - * deployment + * 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 registerAssertion(assertion: IAssertion) { - this._assertions.push(assertion); + 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 f1f833d9f78a4..3a9defd954be9 100644 --- a/packages/@aws-cdk/integ-tests/lib/assertions/index.ts +++ b/packages/@aws-cdk/integ-tests/lib/assertions/index.ts @@ -2,3 +2,5 @@ export * from './assertions'; export * from './sdk'; export * from './deploy-assert'; export * from './providers'; +export * from './common'; +export * from './match'; diff --git a/packages/@aws-cdk/integ-tests/lib/assertions/match.ts b/packages/@aws-cdk/integ-tests/lib/assertions/match.ts new file mode 100644 index 0000000000000..a736895021b63 --- /dev/null +++ b/packages/@aws-cdk/integ-tests/lib/assertions/match.ts @@ -0,0 +1,30 @@ + +/** + * Partial and special matching during assertions. + */ +export abstract class Match { + /** + * Matches the specified pattern with the array found in the same relative path of the target. + * The set of elements (or matchers) must be in the same order as would be found. + * @param pattern the pattern to match + */ + public static arrayWith(pattern: any[]): { [key: string]: any[] } { + return { $ArrayWith: pattern }; + } + + /** + * Matches the specified pattern to an object found in the same relative path of the target. + * The keys and their values (or matchers) must be present in the target but the target can be a superset. + * @param pattern the pattern to match + */ + public static objectLike(pattern: { [key: string]: any }): { [key: string]: { [key: string]: any } } { + return { $ObjectLike: pattern }; + } + + /** + * Matches targets according to a regular expression + */ + public static stringLikeRegexp(pattern: string): { [key: string]: string } { + return { $StringLike: pattern }; + } +} 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 index 8efd972d5f98e..fdb7bb04b7bdd 100644 --- 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 @@ -1,34 +1,135 @@ /* eslint-disable no-console */ -import * as assert from 'assert'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { Match, Matcher } from '@aws-cdk/assertions/lib/helpers-internal'; import { CustomResourceHandler } from './base'; -import { AssertionRequest, AssertionResult } from './types'; +import { AssertionResult, AssertionRequest } from './types'; export class AssertionHandler extends CustomResourceHandler { protected async processEvent(request: AssertionRequest): Promise { + let actual = decodeCall(request.actual); + const expected = decodeCall(request.expected); 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}`); + const matcher = new MatchCreator(expected).getMatcher(); + console.log(`Testing equality between ${JSON.stringify(request.actual)} and ${JSON.stringify(request.expected)}`); + + const matchResult = matcher.test(actual); + matchResult.finished(); + if (matchResult.hasFailed()) { + result = { + data: JSON.stringify({ + status: 'fail', + message: [ + ...matchResult.toHumanStrings(), + JSON.stringify(matchResult.target, undefined, 2), + ].join('\n'), + }), + }; + if (request.failDeployment) { + throw new Error(result.data); + } + } else { + result = { + data: JSON.stringify({ + status: 'pass', + }), + }; } return result; } } + + +class MatchCreator { + private readonly parsedObj: { [key: string]: any }; + constructor(obj: { [key: string]: any }) { + this.parsedObj = { + matcher: obj, + }; + } + + /** + * Return a Matcher that can be tested against the actual results. + * This will convert the encoded matchers into their corresponding + * assertions matcher. + * + * For example: + * + * ExpectedResult.objectLike({ + * Messages: [{ + * Body: Match.objectLike({ + * Elements: Match.arrayWith([{ Asdf: 3 }]), + * }), + * }], + * }); + * + * Will be encoded as: + * { + * $ObjectLike: { + * Messages: [{ + * Body: { + * $ObjectLike: { + * Elements: { + * $ArrayWith: [{ Asdf: 3 }], + * }, + * }, + * }, + * }], + * }, + * } + * + * Which can then be parsed by this function. For each key (recursively) + * the parser will check if the value has one of the encoded matchers as a key + * and if so, it will set the value as the Matcher. So, + * + * { + * Body: { + * $ObjectLike: { + * Elements: { + * $ArrayWith: [{ Asdf: 3 }], + * }, + * }, + * }, + * } + * + * Will be converted to + * { + * Body: Match.objectLike({ + * Elements: Match.arrayWith([{ Asdf: 3 }]), + * }), + * } + */ + public getMatcher(): Matcher { + try { + const final = JSON.parse(JSON.stringify(this.parsedObj), function(_k, v) { + const nested = Object.keys(v)[0]; + switch (nested) { + case '$ArrayWith': + return Match.arrayWith(v[nested]); + case '$ObjectLike': + return Match.objectLike(v[nested]); + case '$StringLike': + return Match.stringLikeRegexp(v[nested]); + default: + return v; + } + }); + if (Matcher.isMatcher(final.matcher)) { + return final.matcher; + } + return Match.exact(final.matcher); + } catch { + return Match.exact(this.parsedObj.matcher); + } + } +} + +function decodeCall(call?: string) { + if (!call) { return undefined; } + try { + const parsed = JSON.parse(call); + return parsed; + } catch (e) { + return call; + } +} 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 07a1911efe4dd..78a47c83be1ef 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,6 +1,6 @@ import { AssertionHandler } from './assertion'; import { ResultsCollectionHandler } from './results'; -import { SdkHandler } from './sdk'; +import { AwsApiCallHandler } from './sdk'; import * as types from './types'; export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent, context: AWSLambda.Context) { @@ -10,7 +10,7 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent function createResourceHandler(event: AWSLambda.CloudFormationCustomResourceEvent, context: AWSLambda.Context) { if (event.ResourceType.startsWith(types.SDK_RESOURCE_TYPE_PREFIX)) { - return new SdkHandler(event, context); + return new AwsApiCallHandler(event, context); } switch (event.ResourceType) { case types.ASSERT_RESOURCE_TYPE: return new AssertionHandler(event, context); 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 index fed1174d3fb27..5d53df2f5b38e 100644 --- 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 @@ -1,6 +1,7 @@ /* eslint-disable no-console */ import { CustomResourceHandler } from './base'; -import { SdkRequest, SdkResult } from './types'; +import { AwsApiCallRequest, AwsApiCallResult } from './types'; +import { decode } from './utils'; /** * Flattens a nested object @@ -24,8 +25,8 @@ export function flatten(object: object): { [key: string]: any } { } -export class SdkHandler extends CustomResourceHandler { - protected async processEvent(request: SdkRequest): Promise { +export class AwsApiCallHandler extends CustomResourceHandler { + protected async processEvent(request: AwsApiCallRequest): Promise { // eslint-disable-next-line const AWS: any = require('aws-sdk'); console.log(`AWS SDK VERSION: ${AWS.VERSION}`); @@ -44,16 +45,3 @@ export class SdkHandler extends CustomResourceHandler) { - 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 index f0ff05507ae61..334e45130d01c 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 @@ -8,7 +8,7 @@ export const SDK_RESOURCE_TYPE_PREFIX = 'Custom::DeployAssert@SdkCall'; /** * A AWS JavaScript SDK V2 request */ -export interface SdkRequest { +export interface AwsApiCallRequest { /** * The AWS service i.e. S3 */ @@ -48,7 +48,7 @@ export interface SdkRequest { /** * The result from a SdkQuery */ -export interface SdkResult { +export interface AwsApiCallResult { /** * The full api response */ @@ -63,6 +63,18 @@ export enum AssertionType { * Assert that two values are equal */ EQUALS = 'equals', + + /** + * The keys and their values must be present in the target but the target + * can be a superset. + */ + OBJECT_LIKE = 'objectLike', + + /** + * Matches the specified pattern with the array + * The set of elements must be in the same order as would be found + */ + ARRAY_WITH = 'arrayWith', } /** @@ -70,11 +82,6 @@ export enum AssertionType { * actual value matches the expected */ export interface AssertionRequest { - /** - * The type of assertion to perform - */ - readonly assertionType: AssertionType; - /** * The expected value to assert */ @@ -84,6 +91,17 @@ export interface AssertionRequest { * The actual value received */ readonly actual: any; + + /** + * Set this to true if a failed assertion should + * result in a CloudFormation deployment failure + * + * This is only necessary if assertions are being + * executed outside of `integ-runner`. + * + * @default false + */ + readonly failDeployment?: boolean; } /** * The result of an Assertion @@ -94,7 +112,14 @@ export interface AssertionResult { /** * The result of an assertion */ - readonly data: AssertionResultData; + readonly data: string; + + /** + * Whether or not the assertion failed + * + * @default false + */ + readonly failed?: boolean; } /** diff --git a/packages/@aws-cdk/integ-tests/lib/assertions/providers/lambda-handler/utils.ts b/packages/@aws-cdk/integ-tests/lib/assertions/providers/lambda-handler/utils.ts new file mode 100644 index 0000000000000..12e4ec65ff8e3 --- /dev/null +++ b/packages/@aws-cdk/integ-tests/lib/assertions/providers/lambda-handler/utils.ts @@ -0,0 +1,13 @@ + +export function decode(object: Record): any { + 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/provider.ts b/packages/@aws-cdk/integ-tests/lib/assertions/providers/provider.ts index 155996452713c..0b416158cc717 100644 --- a/packages/@aws-cdk/integ-tests/lib/assertions/providers/provider.ts +++ b/packages/@aws-cdk/integ-tests/lib/assertions/providers/provider.ts @@ -1,7 +1,5 @@ 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 { Duration, CfnResource, AssetStaging, Stack, FileAssetPackaging, Token, Lazy, Reference } from '@aws-cdk/core'; import { Construct } from 'constructs'; // keep this import separate from other imports to reduce chance for merge conflicts with v2-main @@ -9,6 +7,153 @@ import { Construct } from 'constructs'; import { Construct as CoreConstruct } from '@aws-cdk/core'; let SDK_METADATA: any = undefined; + +/** + * integ-tests can only depend on '@aws-cdk/core' so + * this construct creates a lambda function provider using + * only CfnResource + */ +class LambdaFunctionProvider extends CoreConstruct { + /** + * The ARN of the lambda function which can be used + * as a serviceToken to a CustomResource + */ + public readonly serviceToken: string; + + /** + * A Reference to the provider lambda exeuction role ARN + */ + public readonly roleArn: Reference; + + private readonly policies: any[] = []; + + constructor(scope: Construct, id: string/*, props?: LambdaFunctionProviderProps*/) { + super(scope, id); + + const staging = new AssetStaging(this, 'Staging', { + sourcePath: path.join(__dirname, 'lambda-handler.bundle'), + }); + + const stack = Stack.of(this); + const asset = stack.synthesizer.addFileAsset({ + fileName: staging.relativeStagedPath(stack), + sourceHash: staging.assetHash, + packaging: FileAssetPackaging.ZIP_DIRECTORY, + }); + + const role = new CfnResource(this, 'Role', { + type: 'AWS::IAM::Role', + properties: { + AssumeRolePolicyDocument: { + Version: '2012-10-17', + Statement: [{ Action: 'sts:AssumeRole', Effect: 'Allow', Principal: { Service: 'lambda.amazonaws.com' } }], + }, + ManagedPolicyArns: [ + { 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' }, + ], + Policies: [ + { + PolicyName: 'Inline', + PolicyDocument: { + Version: '2012-10-17', + Statement: Lazy.list({ produce: () => this.policies }), + }, + }, + ], + }, + }); + + const handler = new CfnResource(this, 'Handler', { + type: 'AWS::Lambda::Function', + properties: { + Runtime: 'nodejs14.x', + Code: { + S3Bucket: asset.bucketName, + S3Key: asset.objectKey, + }, + Timeout: Duration.minutes(2).toSeconds(), + Handler: 'index.handler', + Role: role.getAtt('Arn'), + }, + }); + + this.serviceToken = Token.asString(handler.getAtt('Arn')); + this.roleArn = role.getAtt('Arn'); + } + + public addPolicies(policies: any[]): void { + this.policies.push(...policies); + } + +} + +interface SingletonFunctionProps { + /** + * A unique identifier to identify this lambda + * + * The identifier should be unique across all custom resource providers. + * We recommend generating a UUID per provider. + */ + readonly uuid: string; + + /** + * A list of IAM policies to add to the lambdaFunction + * execution role + */ + readonly policies: any[]; +} + +/** + * Mimic the singletonfunction construct in '@aws-cdk/aws-lambda' + */ +class SingletonFunction extends CoreConstruct { + public readonly serviceToken: string; + + public readonly lambdaFunction: LambdaFunctionProvider; + private readonly policies: any[] = []; + constructor(scope: Construct, id: string, props: SingletonFunctionProps) { + super(scope, id); + this.lambdaFunction = this.ensureFunction(props); + this.serviceToken = this.lambdaFunction.serviceToken; + } + + /** + * The policies can be added by different constructs + */ + onPrepare(): void { + this.lambdaFunction.addPolicies(this.policies); + } + + private ensureFunction(props: SingletonFunctionProps): LambdaFunctionProvider { + const constructName = 'SingletonFunction' + slugify(props.uuid); + const existing = Stack.of(this).node.tryFindChild(constructName); + if (existing) { + return existing as LambdaFunctionProvider; + } + + return new LambdaFunctionProvider(Stack.of(this), constructName); + } + + /** + * Create a policy statement from a specific api call + */ + public addPolicyStatementFromSdkCall(service: string, api: string, resources?: string[]): void { + 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); + this.policies.push({ + Action: [`${iamService}:${iamAction}`], + Effect: 'Allow', + Resource: resources || ['*'], + }); + } + +} + /** * Represents an assertions provider. The creates a singletone * Lambda Function that will create a single function per stack @@ -16,24 +161,39 @@ let SDK_METADATA: any = undefined; * assertion providers */ export class AssertionsProvider extends CoreConstruct { + /** + * The ARN of the lambda function which can be used + * as a serviceToken to a CustomResource + */ public readonly serviceToken: string; - private readonly grantPrincipal: iam.IPrincipal; + /** + * A reference to the provider Lambda Function + * execution Role ARN + */ + public readonly handlerRoleArn: Reference; + + private readonly policies: any[] = []; + private readonly handler: SingletonFunction; 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', + this.handler = new SingletonFunction(this, 'AssertionsProvider', { uuid: '1488541a-7b23-4664-81b6-9b4408076b81', - timeout: Duration.minutes(2), + policies: Lazy.list({ produce: () => this.policies }), }); - this.grantPrincipal = handler.grantPrincipal; - this.serviceToken = handler.functionArn; + this.handlerRoleArn = this.handler.lambdaFunction.roleArn; + + this.serviceToken = this.handler.serviceToken; } + /** + * Encode an object so it can be passed + * as custom resource parameters. Custom resources will convert + * all input parameters to strings so we encode non-strings here + * so we can then decode them correctly in the provider function + */ public encode(obj: any): any { if (!obj) { return obj; @@ -50,19 +210,14 @@ export class AssertionsProvider extends CoreConstruct { }); } - 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; + /** + * Create a policy statement from a specific api call + */ + public addPolicyStatementFromSdkCall(service: string, api: string, resources?: string[]): void { + this.handler.addPolicyStatementFromSdkCall(service, api, resources); } } + +function slugify(x: string): string { + return x.replace(/[^a-zA-Z0-9]/g, ''); +} diff --git a/packages/@aws-cdk/integ-tests/lib/assertions/sdk.ts b/packages/@aws-cdk/integ-tests/lib/assertions/sdk.ts index ead56af7732d9..b176c13456f37 100644 --- a/packages/@aws-cdk/integ-tests/lib/assertions/sdk.ts +++ b/packages/@aws-cdk/integ-tests/lib/assertions/sdk.ts @@ -1,8 +1,7 @@ -import { CustomResource, Reference, Lazy } from '@aws-cdk/core'; +import { CustomResource, Reference, Lazy, CfnResource, Stack, ArnFormat } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { EqualsAssertion } from './assertions'; -import { IAssertion } from './deploy-assert'; -import { md5hash } from './private/hash'; +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 @@ -12,7 +11,7 @@ import { Construct as CoreConstruct } from '@aws-cdk/core'; /** * Options to perform an AWS JavaScript V2 API call */ -export interface SdkQueryOptions { +export interface AwsApiCallOptions { /** * The AWS service, i.e. S3 */ @@ -25,6 +24,8 @@ export interface SdkQueryOptions { /** * Any parameters to pass to the api call + * + * @default - no parameters */ readonly parameters?: any; } @@ -32,31 +33,40 @@ export interface SdkQueryOptions { /** * Options for creating an SDKQuery provider */ -export interface SdkQueryProps extends SdkQueryOptions {} +export interface AwsApiCallProps extends AwsApiCallOptions {} -export class SdkQuery extends CoreConstruct { +/** + * Construct that creates a custom resource that will perform + * a query using the AWS SDK + */ +export class AwsApiCall extends CoreConstruct { private readonly sdkCallResource: CustomResource; private flattenResponse: string = 'false'; + private readonly name: string; - constructor(scope: Construct, id: string, props: SdkQueryProps) { + protected provider: AssertionsProvider; + + constructor(scope: Construct, id: string, props: AwsApiCallProps) { super(scope, id); - const provider = new AssertionsProvider(this, 'SdkProvider'); - provider.addPolicyStatementFromSdkCall(props.service, props.api); + this.provider = new AssertionsProvider(this, 'SdkProvider'); + this.provider.addPolicyStatementFromSdkCall(props.service, props.api); + this.name = `${props.service}${props.api}`; this.sdkCallResource = new CustomResource(this, 'Default', { - serviceToken: provider.serviceToken, + serviceToken: this.provider.serviceToken, properties: { service: props.service, api: props.api, - parameters: provider.encode(props.parameters), + parameters: this.provider.encode(props.parameters), flattenResponse: Lazy.string({ produce: () => this.flattenResponse }), + salt: Date.now().toString(), }, - resourceType: `${SDK_RESOURCE_TYPE_PREFIX}${props.service}${props.api}`, + resourceType: `${SDK_RESOURCE_TYPE_PREFIX}${this.name}`, }); // 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); + this.sdkCallResource.node.addDependency(this.provider); } /** @@ -87,20 +97,173 @@ export class SdkQuery extends CoreConstruct { } /** - * Creates an assertion custom resource that will assert that the response - * from the SDKQuery equals the 'expected' value + * 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, + actual: ActualResult.fromCustomResource(this.sdkCallResource, 'apiCallResponse'), + }); + } + + /** + * Assert that the ExpectedResult is equal + * to the result of the AwsApiCall at the given path. + * + * For example the SQS.receiveMessage api response would look + * like: + * + * If you wanted to assert the value of `Body` you could do + * + * @example + * const actual = { + * Messages: [{ + * MessageId: '', + * ReceiptHandle: '', + * MD5OfBody: '', + * Body: 'hello', + * Attributes: {}, + * MD5OfMessageAttributes: {}, + * MessageAttributes: {} + * }] + * }; + * + * + * declare const assert: DeployAssert; + * const message = new AwsApiCall(assert, 'ReceiveMessage', { + * service: 'SQS', + * api: 'receiveMessage' + * }); + * + * message.assertAtPath('Messages.0.Body', ExpectedResult.stringLikeRegexp('hello')); */ - public 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}`, { + public assertAtPath(path: string, expected: ExpectedResult): void { + new EqualsAssertion(this, `AssertEquals${this.name}`, { expected, - inputResource: this.sdkCallResource, - inputResourceAtt, + actual: ActualResult.fromAwsApiCall(this, path), }); } } + +/** + * Set to Tail to include the execution log in the response. + * Applies to synchronously invoked functions only. + */ +export enum LogType { + /** + * The log messages are not returned in the response + */ + NONE = 'None', + + /** + * The log messages are returned in the response + */ + TAIL = 'Tail', +} + +/** + * The type of invocation. Default is REQUEST_RESPONE + */ +export enum InvocationType { + /** + * Invoke the function asynchronously. + * Send events that fail multiple times to the function's + * dead-letter queue (if it's configured). + * The API response only includes a status code. + */ + EVENT = 'Event', + + /** + * Invoke the function synchronously. + * Keep the connection open until the function returns a response or times out. + * The API response includes the function response and additional data. + */ + REQUEST_RESPONE = 'RequestResponse', + + /** + * Validate parameter values and verify that the user + * or role has permission to invoke the function. + */ + DRY_RUN = 'DryRun', +} + +/** + * Options to pass to the Lambda invokeFunction API call + */ +export interface LambdaInvokeFunctionProps { + /** + * The name of the function to invoke + */ + readonly functionName: string; + + /** + * The type of invocation to use + * + * @default InvocationType.REQUEST_RESPONE + */ + readonly invocationType?: InvocationType; + + /** + * Whether to return the logs as part of the response + * + * @default LogType.NONE + */ + readonly logType?: LogType; + + /** + * Payload to send as part of the invoke + * + * @default - no payload + */ + readonly payload?: string; +} + +/** + * An AWS Lambda Invoke function API call. + * Use this istead of the generic AwsApiCall in order to + * invoke a lambda function. This will automatically create + * the correct permissions to invoke the function + */ +export class LambdaInvokeFunction extends AwsApiCall { + constructor(scope: Construct, id: string, props: LambdaInvokeFunctionProps) { + super(scope, id, { + api: 'invoke', + service: 'Lambda', + parameters: { + FunctionName: props.functionName, + InvocationType: props.invocationType, + LogType: props.logType, + Payload: props.payload, + }, + }); + + const stack = Stack.of(this); + // need to give the assertion lambda permission to invoke + new CfnResource(this, 'Invoke', { + type: 'AWS::Lambda::Permission', + properties: { + Action: 'lambda:InvokeFunction', + FunctionName: props.functionName, + Principal: this.provider.handlerRoleArn, + }, + }); + + // the api call is 'invoke', but the permission is 'invokeFunction' + // so need to handle it specially + this.provider.addPolicyStatementFromSdkCall('Lambda', 'invokeFunction', [stack.formatArn({ + service: 'lambda', + resource: 'function', + arnFormat: ArnFormat.COLON_RESOURCE_NAME, + resourceName: props.functionName, + })]); + } +} + diff --git a/packages/@aws-cdk/integ-tests/lib/index.ts b/packages/@aws-cdk/integ-tests/lib/index.ts index 638d20a4d1d1a..bacf0560f3cbf 100644 --- a/packages/@aws-cdk/integ-tests/lib/index.ts +++ b/packages/@aws-cdk/integ-tests/lib/index.ts @@ -1 +1,2 @@ export * from './test-case'; +export * from './assertions'; diff --git a/packages/@aws-cdk/integ-tests/lib/test-case.ts b/packages/@aws-cdk/integ-tests/lib/test-case.ts index 2c0c6582a413c..de701bb63d24a 100644 --- a/packages/@aws-cdk/integ-tests/lib/test-case.ts +++ b/packages/@aws-cdk/integ-tests/lib/test-case.ts @@ -1,10 +1,14 @@ import { IntegManifest, Manifest, TestCase, TestOptions } from '@aws-cdk/cloud-assembly-schema'; -import { attachCustomSynthesis, Stack, ISynthesisSession } from '@aws-cdk/core'; +import { attachCustomSynthesis, Stack, ISynthesisSession, StackProps } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { DeployAssert } from './assertions'; import { IntegManifestSynthesizer } from './manifest-synthesizer'; +const TEST_CASE_STACK_SYMBOL = Symbol.for('@aws-cdk/integ-tests.IntegTestCaseStack'); + // 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 } from '@aws-cdk/core'; +import { Construct as CoreConstruct } from '@aws-cdk/core'; /** * Properties of an integration test case @@ -19,10 +23,20 @@ export interface IntegTestCaseProps extends TestOptions { /** * An integration test case. Allows the definition of test properties that * apply to all stacks under this case. + * + * It is recommended that you use the IntegTest construct since that will create + * a default IntegTestCase */ -export class IntegTestCase extends Construct { - constructor(scope: Construct, private readonly id: string, private readonly props: IntegTestCaseProps) { +export class IntegTestCase extends CoreConstruct { + /** + * Make assertions on resources in this test case + */ + public readonly assert: DeployAssert; + + constructor(scope: Construct, id: string, private readonly props: IntegTestCaseProps) { super(scope, id); + + this.assert = new DeployAssert(this); } /** @@ -32,43 +46,115 @@ export class IntegTestCase extends Construct { get manifest(): IntegManifest { return { version: Manifest.version(), - testCases: { [this.id]: toTestCase(this.props) }, + testCases: { [this.node.path]: this.toTestCase(this.props) }, + }; + } + + private toTestCase(props: IntegTestCaseProps): TestCase { + return { + ...props, + assertionStack: Stack.of(this.assert).artifactId, + stacks: props.stacks.map(s => s.artifactId), }; } } +/** + * Properties of an integration test case stack + */ +export interface IntegTestCaseStackProps extends TestOptions, StackProps {} + +/** + * An integration test case stack. Allows the definition of test properties + * that should apply to this stack. + * + * This should be used if there are multiple stacks in the integration test + * and it is necessary to specify different test case option for each. Otherwise + * normal stacks should be added to IntegTest + */ +export class IntegTestCaseStack extends Stack { + /** + * Returns whether the construct is a IntegTestCaseStack + */ + public static isIntegTestCaseStack(x: any): x is IntegTestCaseStack { + return x !== null && typeof(x) === 'object' && TEST_CASE_STACK_SYMBOL in x; + } + + /** + * Make assertions on resources in this test case + */ + public readonly assert: DeployAssert; + + /** + * The underlying IntegTestCase that is created + * @internal + */ + public readonly _testCase: IntegTestCase; + + constructor(scope: Construct, id: string, props?: IntegTestCaseStackProps) { + super(scope, id, props); + + Object.defineProperty(this, TEST_CASE_STACK_SYMBOL, { value: true }); + + // TODO: should we only have a single DeployAssert per test? + this.assert = new DeployAssert(this); + this._testCase = new IntegTestCase(this, `${id}TestCase`, { + ...props, + stacks: [this], + }); + } + +} + /** * Integration test properties */ -export interface IntegTestProps { +export interface IntegTestProps extends TestOptions { /** * List of test cases that make up this test */ - readonly testCases: IntegTestCase[]; + readonly testCases: Stack[]; } /** * A collection of test cases. Each test case file should contain exactly one * instance of this class. */ -export class IntegTest extends Construct { - constructor(scope: Construct, id: string, private readonly props: IntegTestProps) { +export class IntegTest extends CoreConstruct { + /** + * Make assertions on resources in this test case + */ + public readonly assert: DeployAssert; + private readonly testCases: IntegTestCase[]; + constructor(scope: Construct, id: string, props: IntegTestProps) { super(scope, id); + + const defaultTestCase = new IntegTestCase(this, 'DefaultTest', { + stacks: props.testCases.filter(stack => !IntegTestCaseStack.isIntegTestCaseStack(stack)), + hooks: props.hooks, + regions: props.regions, + diffAssets: props.diffAssets, + allowDestroy: props.allowDestroy, + cdkCommandOptions: props.cdkCommandOptions, + stackUpdateWorkflow: props.stackUpdateWorkflow, + }); + this.assert = defaultTestCase.assert; + + this.testCases = [ + defaultTestCase, + ...props.testCases + .filter(stack => IntegTestCaseStack.isIntegTestCaseStack(stack)) + .map(stack => (stack as IntegTestCaseStack)._testCase), + ]; } + protected onPrepare(): void { attachCustomSynthesis(this, { onSynthesize: (session: ISynthesisSession) => { - const synthesizer = new IntegManifestSynthesizer(this.props.testCases); + const synthesizer = new IntegManifestSynthesizer(this.testCases); synthesizer.synthesize(session); }, }); } } - -function toTestCase(props: IntegTestCaseProps): TestCase { - return { - ...props, - stacks: props.stacks.map(s => s.artifactId), - }; -} diff --git a/packages/@aws-cdk/integ-tests/package.json b/packages/@aws-cdk/integ-tests/package.json index 58056e9ccf02e..61d5db28e95b1 100644 --- a/packages/@aws-cdk/integ-tests/package.json +++ b/packages/@aws-cdk/integ-tests/package.json @@ -40,6 +40,7 @@ }, "scripts": { "build": "cdk-build", + "bundle": "esbuild --bundle lib/assertions/providers/lambda-handler/index.ts --target=node14 --platform=node --external:aws-sdk --outfile=lib/assertions/providers/lambda-handler.bundle/index.js", "lint": "cdk-lint", "package": "cdk-package", "awslint": "cdk-awslint", @@ -61,6 +62,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/cdk-build-tools": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "@aws-cdk/assertions": "0.0.0", "@aws-cdk/pkglint": "0.0.0", "@types/fs-extra": "^8.1.2", @@ -75,22 +77,12 @@ "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" + "constructs": "^3.3.69" }, "repository": { "url": "https://github.com/aws/aws-cdk.git", @@ -105,6 +97,19 @@ "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" }, + "cdk-build": { + "pre": [ + "yarn bundle" + ], + "env": { + "AWSLINT_BASE_CONSTRUCT": true + } + }, + "awslint": { + "exclude": [ + "construct-ctor:@aws-cdk/integ-tests.DeployAssert." + ] + }, "nozem": { "ostools": [ "unzip", diff --git a/packages/@aws-cdk/integ-tests/rosetta/default.ts-fixture b/packages/@aws-cdk/integ-tests/rosetta/default.ts-fixture index 648e54426b3e4..b9b4f3740b427 100644 --- a/packages/@aws-cdk/integ-tests/rosetta/default.ts-fixture +++ b/packages/@aws-cdk/integ-tests/rosetta/default.ts-fixture @@ -1,12 +1,28 @@ import * as lambda from '@aws-cdk/aws-lambda'; -import { IntegTestCase, IntegTest } from '@aws-cdk/integ-tests'; +import { + IntegTestCase, + IntegTest, + IntegTestCaseStack, + DeployAssert, + AwsApiCall, + EqualsAssertion, + ActualResult, + ExpectedResult, + InvocationType, + AssertionType, + LambdaInvokeFunction, + Match, +} from '@aws-cdk/integ-tests'; import { App, Construct, Stack, StackProps, + CustomResource, } from '@aws-cdk/core'; import * as path from 'path'; +import * as sqs from '@aws-cdk/aws-sqs'; +import { IStateMachine } from '@aws-cdk/aws-stepfunctions'; import { RequireApproval } from '@aws-cdk/cloud-assembly-schema'; /// here diff --git a/packages/@aws-cdk/integ-tests/test/assertions/assertions.test.ts b/packages/@aws-cdk/integ-tests/test/assertions/assertions.test.ts deleted file mode 100644 index c8558c6460b0a..0000000000000 --- a/packages/@aws-cdk/integ-tests/test/assertions/assertions.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -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 index bb73e87b2da7e..847086ed66f7a 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,96 +1,145 @@ 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'; +import { DeployAssert, LogType, InvocationType, ExpectedResult, ActualResult } from '../../lib/assertions'; describe('DeployAssert', () => { - describe('ResultsCollection', () => { + + test('of', () => { + const app = new App(); + const stack = new Stack(app); + new DeployAssert(app); + expect(() => { + DeployAssert.of(stack); + }).not.toThrow(); + }); + + test('throws if no DeployAssert', () => { + const app = new App(); + const stack = new Stack(app); + expect(() => { + DeployAssert.of(stack); + }).toThrow(/No DeployAssert construct found in scopes/); + }); + + test('isDeployAssert', () => { + const app = new App(); + const deployAssert = new DeployAssert(app); + const isDeployAssert = DeployAssert.isDeployAssert(deployAssert); + expect(isDeployAssert).toEqual(true); + }); + + describe('invokeFunction', () => { test('default', () => { // GIVEN const app = new App(); - const stack = new Stack(app, 'MyStack'); + const deployAssert = new DeployAssert(app); // WHEN - new DeployAssert(stack); - + deployAssert.invokeFunction({ + functionName: 'my-func', + logType: LogType.TAIL, + payload: JSON.stringify({ key: 'val' }), + invocationType: InvocationType.EVENT, + }); // THEN - const template = Template.fromStack(stack); - template.resourceCountIs('Custom::DeployAssert@ResultsCollection', 1); - - template.hasOutput('Results', {}); + const template = Template.fromStack(Stack.of(deployAssert)); + template.hasResourceProperties('Custom::DeployAssert@SdkCallLambdainvoke', { + service: 'Lambda', + api: 'invoke', + parameters: { + FunctionName: 'my-func', + InvocationType: 'Event', + LogType: 'Tail', + Payload: '{"key":"val"}', + }, + }); }); + }); - test('assertion results are part of the output', () => { + describe('assertions', () => { + test('stringLike', () => { // 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'); + const deplossert = new DeployAssert(app); + const query = deplossert.awsApiCall('MyService', 'MyApi'); // WHEN - const deployAssert = new DeployAssert(stack); - deployAssert.registerAssertion( - new MyAssertion('MyAssertion1Result'), - ); - deployAssert.registerAssertion( - new MyAssertion('MyAssertion2Result'), + deplossert.assert( + 'MyAssertion', + ExpectedResult.stringLikeRegexp('foo'), + ActualResult.fromAwsApiCall(query, 'att'), ); + // THEN + const template = Template.fromStack(Stack.of(deplossert)); + template.hasResourceProperties('Custom::DeployAssert@AssertEquals', { + expected: JSON.stringify({ $StringLike: 'foo' }), + actual: { + 'Fn::GetAtt': [ + 'AwsApiCallMyServiceMyApi', + 'apiCallResponse.att', + ], + }, + }); + }); + + test('objectLike', () => { + // GIVEN + const app = new App(); + const deplossert = new DeployAssert(app); + const query = deplossert.awsApiCall('MyService', 'MyApi'); + + // WHEN + deplossert.assert( + 'MyAssertion', + ExpectedResult.objectLike({ foo: 'bar' }), + ActualResult.fromAwsApiCall(query, 'att'), + ); // THEN - const template = Template.fromStack(stack); - template.hasResourceProperties('Custom::DeployAssert@ResultsCollection', { - assertionResults: ['MyAssertion1Result', 'MyAssertion2Result'], + const template = Template.fromStack(Stack.of(deplossert)); + template.hasResourceProperties('Custom::DeployAssert@AssertEquals', { + expected: JSON.stringify({ $ObjectLike: { foo: 'bar' } }), + actual: { + 'Fn::GetAtt': [ + 'AwsApiCallMyServiceMyApi', + 'apiCallResponse.att', + ], + }, }); }); }); - describe('queryAws', () => { + describe('awsApiCall', () => { test('default', () => { // GIVEN const app = new App(); - const stack = new Stack(app); + const deplossert = new DeployAssert(app); // WHEN - const deplossert = new DeployAssert(stack); - deplossert.queryAws({ - service: 'MyService', - api: 'MyApi', - }); + deplossert.awsApiCall('MyService', 'MyApi'); // THEN - Template.fromStack(stack).hasResourceProperties('Custom::DeployAssert@SdkCallMyServiceMyApi', { + Template.fromStack(Stack.of(deplossert)).hasResourceProperties('Custom::DeployAssert@SdkCallMyServiceMyApi', { api: 'MyApi', service: 'MyService', }); }); - test('multiple queries can be configured', () => { + test('multiple calls 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', - }); + const deplossert = new DeployAssert(app); + deplossert.awsApiCall('MyService', 'MyApi1'); + deplossert.awsApiCall('MyService', 'MyApi2'); // THEN - const template = Template.fromStack(stack); + const template = Template.fromStack(Stack.of(deplossert)); 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/providers/lambda-handler/assertion.test.ts b/packages/@aws-cdk/integ-tests/test/assertions/providers/lambda-handler/assertion.test.ts index 911876c84bdfb..05740c33cc09b 100644 --- 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 @@ -1,4 +1,5 @@ -import { AssertionRequest, AssertionResult, AssertionType } from '../../../../lib/assertions'; +import { AssertionRequest, AssertionResult, ExpectedResult } from '../../../../lib/assertions'; +import { Match } from '../../../../lib/assertions/match'; import { AssertionHandler } from '../../../../lib/assertions/providers/lambda-handler/assertion'; function assertionHandler() { @@ -18,63 +19,234 @@ afterAll(() => { }); describe('AssertionHandler', () => { - describe('equals', () => { + test('report failure', async () => { + // GIVEN + const handler = assertionHandler() as any; + const request: AssertionRequest = { + actual: 'this is the actual results', + expected: ExpectedResult.stringLikeRegexp('abcd').result, + failDeployment: true, + }; + + // THEN + let failed: Error = new Error(); + try { + await handler.processEvent(request); + } catch (e) { + failed = e; + } + expect(failed.message).toMatch(/String 'this is the actual results' did not match pattern 'abcd' (using stringLikeRegexp matcher)*/); + }); + describe('stringLike', () => { + test('pass', async () => { + // GIVEN + const handler = assertionHandler() as any; + const request: AssertionRequest = { + actual: 'this is the actual results', + expected: ExpectedResult.stringLikeRegexp('this is').result, + }; + + // WHEN + const response: AssertionResult = await handler.processEvent(request); + + // THEN + expect(response.data).toEqual('{"status":"pass"}'); + }); + + test('fail', async () => { + // GIVEN + const handler = assertionHandler() as any; + const request: AssertionRequest = { + actual: 'this is the actual results', + expected: ExpectedResult.stringLikeRegexp('abcd').result, + }; + + // WHEN + const response: AssertionResult = await handler.processEvent(request); + + // THEN + expect(JSON.parse(response.data)).toEqual({ + status: 'fail', + message: expect.stringMatching(/String 'this is the actual results' did not match pattern 'abcd' (using stringLikeRegexp matcher)*/), + }); + }); + }); + describe('arrayWith', () => { + test('pass', async () => { + // GIVEN + const handler = assertionHandler() as any; + const request: AssertionRequest = { + actual: [ + { + Elements: [{ Asdf: 3 }, { Asdf: 4 }], + }, + { + Elements: [{ Asdf: 2 }, { Asdf: 1 }], + }, + ], + expected: ExpectedResult.arrayWith([ + { + Elements: Match.arrayWith([{ Asdf: 3 }]), + }, + ]).result, + }; + + // WHEN + const response: AssertionResult = await handler.processEvent(request); + + // THEN + expect(response.data).toEqual('{"status":"pass"}'); + }); + + test('fail', async () => { + // GIVEN + const handler = assertionHandler() as any; + const request: AssertionRequest = { + actual: [ + { + Elements: [{ Asdf: 5 }, { Asdf: 4 }], + }, + { + Elements: [{ Asdf: 2 }, { Asdf: 1 }], + }, + ], + expected: ExpectedResult.arrayWith([ + { + Elements: [{ Asdf: 3 }], + }, + ]).result, + }; + + // WHEN + const response: AssertionResult = await handler.processEvent(request); + + // THEN + expect(JSON.parse(response.data)).toEqual({ + status: 'fail', + message: expect.stringMatching(/Missing element at pattern index 0 (using arrayWith matcher)*/), + }); + }); + }); + + describe('objectLike', () => { test('pass', async () => { // GIVEN const handler = assertionHandler() as any; const request: AssertionRequest = { - assertionType: AssertionType.EQUALS, + actual: { + Message: [ + { + OtherKey: 'value', + Payload: 'some status', + Body: { + OtherKey: 4, + Elements: [{ Asdf: 3 }, { Asdf: 4 }], + }, + }, + ], + }, + expected: ExpectedResult.objectLike({ + Message: [{ + Payload: Match.stringLikeRegexp('status'), + Body: Match.objectLike({ + Elements: Match.arrayWith([{ Asdf: 3 }]), + }), + }], + }).result, + }; + + // WHEN + const response: AssertionResult = await handler.processEvent(request); + + // THEN + expect(response.data).toEqual('{"status":"pass"}'); + }); + + test('fail', async () => { + // GIVEN + const handler = assertionHandler() as any; + const request: AssertionRequest = { actual: { stringParam: 'foo', numberParam: 3, booleanParam: true, }, - expected: { + expected: ExpectedResult.objectLike({ + stringParam: 'bar', + }).result, + }; + + // WHEN + const response: AssertionResult = await handler.processEvent(request); + + // THEN + expect(JSON.parse(response.data)).toEqual({ + status: 'fail', + message: 'Expected bar but received foo at /stringParam (using objectLike matcher)\n' + + '{\n \"stringParam\": \"foo\",\n \"numberParam\": 3,\n \"booleanParam\": true\n}', + }); + }); + }); + + describe('not using Match', () => { + test('pass', async () => { + // GIVEN + const handler = assertionHandler() as any; + const request: AssertionRequest = { + actual: { stringParam: 'foo', numberParam: 3, booleanParam: true, }, + expected: JSON.stringify({ + stringParam: 'foo', + numberParam: 3, + booleanParam: true, + }), }; // WHEN const response: AssertionResult = await handler.processEvent(request); // THEN - expect(response.data.status).toEqual('pass'); + expect(response.data).toEqual('{"status":"pass"}'); + }); + + test('string equals pass', async () => { + // GIVEN + const handler = assertionHandler() as any; + const request: AssertionRequest = { + actual: 'foo', + expected: 'foo', + }; + + // WHEN + const response: AssertionResult = await handler.processEvent(request); + + // THEN + expect(response.data).toEqual('{"status":"pass"}'); }); test('fail', async () => { // GIVEN const handler = assertionHandler() as any; const request: AssertionRequest = { - assertionType: AssertionType.EQUALS, actual: { stringParam: 'foo', }, - expected: { + expected: JSON.stringify({ stringParam: 'bar', - }, + }), }; // WHEN const response: AssertionResult = await handler.processEvent(request); // THEN - expect(response.data.status).toEqual('fail'); + expect(JSON.parse(response.data)).toEqual({ + status: 'fail', + message: 'Expected bar but received foo at /stringParam (using exact matcher)\n{\n \"stringParam\": \"foo\"\n}', + }); }); }); - - 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/results.test.ts b/packages/@aws-cdk/integ-tests/test/assertions/providers/lambda-handler/results.test.ts deleted file mode 100644 index 33b0cef42677d..0000000000000 --- a/packages/@aws-cdk/integ-tests/test/assertions/providers/lambda-handler/results.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -// 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 index bce5f29548cb8..c9d9c606d38d9 100644 --- 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 @@ -2,14 +2,14 @@ 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'; +import { AwsApiCallRequest, AwsApiCallResult } from '../../../../lib/assertions'; +import { AwsApiCallHandler } 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 + return new AwsApiCallHandler({} as any, context); // as any to ignore all type checks } beforeAll(() => { jest.useFakeTimers(); @@ -45,7 +45,7 @@ describe('SdkHandler', () => { } as SDK.S3.ListObjectsOutput; AWS.mock('S3', 'listObjects', sinon.fake.resolves(expectedResponse)); const handler = sdkHandler() as any; - const request: SdkRequest = { + const request: AwsApiCallRequest = { service: 'S3', api: 'listObjects', parameters: { @@ -54,7 +54,7 @@ describe('SdkHandler', () => { }; // WHEN - const response: SdkResult = await handler.processEvent(request); + const response: AwsApiCallResult = await handler.processEvent(request); // THEN @@ -67,7 +67,7 @@ describe('SdkHandler', () => { const fake = sinon.fake.resolves({}); AWS.mock('EC2', 'describeInstances', fake); const handler = sdkHandler() as any; - const request: SdkRequest = { + const request: AwsApiCallRequest = { service: 'EC2', api: 'describeInstances', parameters: { @@ -88,7 +88,7 @@ describe('SdkHandler', () => { const fake = sinon.fake.resolves({}); AWS.mock('EC2', 'describeInstances', fake); const handler = sdkHandler() as any; - const request: SdkRequest = { + const request: AwsApiCallRequest = { service: 'EC2', api: 'describeInstances', parameters: { 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 index 376be437ddb8a..f27d61189e72f 100644 --- a/packages/@aws-cdk/integ-tests/test/assertions/providers/provider.test.ts +++ b/packages/@aws-cdk/integ-tests/test/assertions/providers/provider.test.ts @@ -11,7 +11,7 @@ describe('AssertionProvider', () => { const provider = new AssertionsProvider(stack, 'AssertionProvider'); // THEN - expect(stack.resolve(provider.serviceToken)).toEqual({ 'Fn::GetAtt': ['SingletonLambda1488541a7b23466481b69b4408076b81488C0898', 'Arn'] }); + expect(stack.resolve(provider.serviceToken)).toEqual({ 'Fn::GetAtt': ['SingletonFunction1488541a7b23466481b69b4408076b81HandlerCD40AE9F', 'Arn'] }); Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { Handler: 'index.handler', Timeout: 120, @@ -28,19 +28,88 @@ describe('AssertionProvider', () => { provider.addPolicyStatementFromSdkCall('MyService', 'myApi'); // THEN - Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { - PolicyDocument: { - Statement: [ - { - Action: 'myservice:MyApi', - Effect: 'Allow', - Resource: '*', + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { + Policies: [ + { + PolicyName: 'Inline', + PolicyDocument: { + Statement: [ + { + Action: ['myservice:MyApi'], + Resource: ['*'], + Effect: 'Allow', + }, + ], }, - ], - }, - Roles: [{ - Ref: 'SingletonLambda1488541a7b23466481b69b4408076b81ServiceRole4E21F0DA', - }], + }, + ], + }); + }); + + test('multiple calls', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const provider = new AssertionsProvider(stack, 'AssertionsProvider'); + provider.addPolicyStatementFromSdkCall('MyService', 'myApi'); + provider.addPolicyStatementFromSdkCall('MyService2', 'myApi2'); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { + Policies: [ + { + PolicyName: 'Inline', + PolicyDocument: { + Statement: [ + { + Action: ['myservice:MyApi'], + Resource: ['*'], + Effect: 'Allow', + }, + { + Action: ['myservice2:MyApi2'], + Resource: ['*'], + Effect: 'Allow', + }, + ], + }, + }, + ], + }); + }); + + test('multiple providers, 1 resource', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const provider = new AssertionsProvider(stack, 'AssertionsProvider'); + const provider2 = new AssertionsProvider(stack, 'AssertionsProvider2'); + provider.addPolicyStatementFromSdkCall('MyService', 'myApi'); + provider2.addPolicyStatementFromSdkCall('MyService2', 'myApi2'); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { + Policies: [ + { + PolicyName: 'Inline', + PolicyDocument: { + Statement: [ + { + Action: ['myservice:MyApi'], + Resource: ['*'], + Effect: 'Allow', + }, + { + Action: ['myservice2:MyApi2'], + Resource: ['*'], + Effect: 'Allow', + }, + ], + }, + }, + ], }); }); @@ -53,16 +122,21 @@ describe('AssertionProvider', () => { provider.addPolicyStatementFromSdkCall('applicationautoscaling', 'myApi'); // THEN - Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { - PolicyDocument: { - Statement: [ - { - Action: 'application-autoscaling:MyApi', - Effect: 'Allow', - Resource: '*', + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { + Policies: [ + { + PolicyName: 'Inline', + PolicyDocument: { + Statement: [ + { + Action: ['application-autoscaling:MyApi'], + Effect: 'Allow', + Resource: ['*'], + }, + ], }, - ], - }, + }, + ], }); }); }); 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 2b54beb326e2d..31f1bd5068a4b 100644 --- a/packages/@aws-cdk/integ-tests/test/assertions/sdk.test.ts +++ b/packages/@aws-cdk/integ-tests/test/assertions/sdk.test.ts @@ -1,23 +1,21 @@ import { Template, Match } from '@aws-cdk/assertions'; -import { App, Stack } from '@aws-cdk/core'; -import { DeployAssert, SdkQuery } from '../../lib/assertions'; +import { App, Stack, CfnOutput } from '@aws-cdk/core'; +import { DeployAssert, AwsApiCall, LambdaInvokeFunction, LogType, InvocationType, ExpectedResult } from '../../lib/assertions'; -describe('SdkQuery', () => { +describe('AwsApiCall', () => { test('default', () => { // GIVEN const app = new App(); - const stack = new Stack(app); - const deplossert = new DeployAssert(stack); + const deplossert = new DeployAssert(app); // WHEN - new SdkQuery(deplossert, 'SdkQuery', { + new AwsApiCall(deplossert, 'AwsApiCall', { service: 'MyService', api: 'MyApi', }); - // THEN - const template = Template.fromStack(stack); + const template = Template.fromStack(Stack.of(deplossert)); template.resourceCountIs('AWS::Lambda::Function', 1); template.hasResourceProperties('Custom::DeployAssert@SdkCallMyServiceMyApi', { service: 'MyService', @@ -29,11 +27,10 @@ describe('SdkQuery', () => { test('parameters', () => { // GIVEN const app = new App(); - const stack = new Stack(app); - const deplossert = new DeployAssert(stack); + const deplossert = new DeployAssert(app); // WHEN - new SdkQuery(deplossert, 'SdkQuery', { + new AwsApiCall(deplossert, 'AwsApiCall', { service: 'MyService', api: 'MyApi', parameters: { @@ -42,9 +39,8 @@ describe('SdkQuery', () => { }, }); - // THEN - const template = Template.fromStack(stack); + const template = Template.fromStack(Stack.of(deplossert)); template.resourceCountIs('AWS::Lambda::Function', 1); template.hasResourceProperties('Custom::DeployAssert@SdkCallMyServiceMyApi', { service: 'MyService', @@ -56,53 +52,251 @@ describe('SdkQuery', () => { }); }); - describe('assertEqual', () => { - test('default', () => { + describe('get attribute', () => { + test('getAttString', () => { + // GIVEN + const app = new App(); + const deplossert = new DeployAssert(app); + + // WHEN + const query = new AwsApiCall(deplossert, 'AwsApiCall', { + service: 'MyService', + api: 'MyApi', + }); + + new CfnOutput(deplossert, 'GetAttString', { + value: query.getAttString('att'), + }).overrideLogicalId('GetAtt'); + + // THEN + const template = Template.fromStack(Stack.of(deplossert)); + template.hasOutput('GetAtt', { + Value: { + 'Fn::GetAtt': [ + 'AwsApiCall', + 'apiCallResponse.att', + ], + }, + }); + template.resourceCountIs('AWS::Lambda::Function', 1); + template.hasResourceProperties('Custom::DeployAssert@SdkCallMyServiceMyApi', { + service: 'MyService', + api: 'MyApi', + flattenResponse: 'true', + }); + }); + test('getAtt', () => { // GIVEN const app = new App(); - const stack = new Stack(app); - const deplossert = new DeployAssert(stack); + const deplossert = new DeployAssert(app); // WHEN - const query = new SdkQuery(deplossert, 'SdkQuery', { + const query = new AwsApiCall(deplossert, 'AwsApiCall', { service: 'MyService', api: 'MyApi', }); - query.assertEqual({ foo: 'bar' }); + new CfnOutput(deplossert, 'GetAttString', { + value: query.getAtt('att').toString(), + }).overrideLogicalId('GetAtt'); + + // THEN + const template = Template.fromStack(Stack.of(deplossert)); + template.hasOutput('GetAtt', { + Value: { + 'Fn::GetAtt': [ + 'AwsApiCall', + 'apiCallResponse.att', + ], + }, + }); + template.resourceCountIs('AWS::Lambda::Function', 1); + template.hasResourceProperties('Custom::DeployAssert@SdkCallMyServiceMyApi', { + service: 'MyService', + api: 'MyApi', + flattenResponse: 'true', + }); + }); + + }); + + describe('assertEqual', () => { + test('objectEqual', () => { + // GIVEN + const app = new App(); + const deplossert = new DeployAssert(app); + + // WHEN + const query = new AwsApiCall(deplossert, 'AwsApiCall', { + service: 'MyService', + api: 'MyApi', + }); + query.assert(ExpectedResult.exact({ foo: 'bar' })); // THEN - const template = Template.fromStack(stack); + const template = Template.fromStack(Stack.of(deplossert)); template.hasResourceProperties('Custom::DeployAssert@AssertEquals', { - expected: { foo: 'bar' }, + expected: JSON.stringify({ $Exact: { foo: 'bar' } }), actual: { 'Fn::GetAtt': [ - 'DeployAssertSdkQuery94650089', + 'AwsApiCall', 'apiCallResponse', ], }, - assertionType: 'equals', }); }); - test('multiple asserts to the same query', () => { + test('objectLike', () => { // GIVEN const app = new App(); - const stack = new Stack(app); - const deplossert = new DeployAssert(stack); + const deplossert = new DeployAssert(app); // WHEN - const query = new SdkQuery(deplossert, 'SdkQuery', { + const query = new AwsApiCall(deplossert, 'AwsApiCall', { service: 'MyService', api: 'MyApi', }); - query.assertEqual({ foo: 'bar' }); - query.assertEqual({ baz: 'zoo' }); + query.assert(ExpectedResult.objectLike({ foo: 'bar' })); + // THEN + const template = Template.fromStack(Stack.of(deplossert)); + template.hasResourceProperties('Custom::DeployAssert@AssertEquals', { + expected: JSON.stringify({ $ObjectLike: { foo: 'bar' } }), + actual: { + 'Fn::GetAtt': [ + 'AwsApiCall', + 'apiCallResponse', + ], + }, + }); + }); + + test('string', () => { + // GIVEN + const app = new App(); + const deplossert = new DeployAssert(app); + + // WHEN + const query = new AwsApiCall(deplossert, 'AwsApiCall', { + service: 'MyService', + api: 'MyApi', + }); + query.assert(ExpectedResult.exact('bar')); // THEN - const template = Template.fromStack(stack); - template.resourceCountIs('Custom::DeployAssert@AssertEquals', 2); + const template = Template.fromStack(Stack.of(deplossert)); + template.hasResourceProperties('Custom::DeployAssert@AssertEquals', { + expected: JSON.stringify({ $Exact: 'bar' }), + actual: { + 'Fn::GetAtt': [ + 'AwsApiCall', + 'apiCallResponse', + ], + }, + }); + }); + }); + + describe('invoke lambda', () => { + test('default', () => { + // GIVEN + const app = new App(); + const deplossert = new DeployAssert(app); + + new LambdaInvokeFunction(deplossert, 'Invoke', { + functionName: 'my-func', + logType: LogType.TAIL, + payload: JSON.stringify({ key: 'val' }), + invocationType: InvocationType.EVENT, + }); + + const template = Template.fromStack(Stack.of(deplossert)); + template.hasResourceProperties('Custom::DeployAssert@SdkCallLambdainvoke', { + service: 'Lambda', + api: 'invoke', + parameters: { + FunctionName: 'my-func', + InvocationType: 'Event', + LogType: 'Tail', + Payload: '{"key":"val"}', + }, + }); + template.hasResourceProperties('AWS::Lambda::Permission', { + Action: 'lambda:InvokeFunction', + FunctionName: 'my-func', + Principal: { + 'Fn::GetAtt': [ + 'SingletonFunction1488541a7b23466481b69b4408076b81Role37ABCE73', + 'Arn', + ], + }, + }); + template.hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'lambda.amazonaws.com', + }, + }, + ], + }, + ManagedPolicyArns: [ + { + 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole', + }, + ], + Policies: [ + { + PolicyName: 'Inline', + PolicyDocument: { + Version: '2012-10-17', + Statement: [ + { + Action: [ + 'lambda:Invoke', + ], + Effect: 'Allow', + Resource: [ + '*', + ], + }, + { + Action: [ + 'lambda:InvokeFunction', + ], + Effect: 'Allow', + Resource: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':lambda:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':function:my-func', + ], + ], + }, + ], + }, + ], + }, + }, + ], + }); }); }); }); diff --git a/packages/@aws-cdk/integ-tests/test/manifest-synthesizer.test.ts b/packages/@aws-cdk/integ-tests/test/manifest-synthesizer.test.ts index b033faf2be0c6..a8b564ad11910 100644 --- a/packages/@aws-cdk/integ-tests/test/manifest-synthesizer.test.ts +++ b/packages/@aws-cdk/integ-tests/test/manifest-synthesizer.test.ts @@ -4,18 +4,27 @@ import * as path from 'path'; import { Manifest } from '@aws-cdk/cloud-assembly-schema'; import { App, Stack } from '@aws-cdk/core'; import { CloudAssemblyBuilder } from '@aws-cdk/cx-api'; -import { IntegTestCase } from '../lib'; +import { IntegTestCase, IntegTest, IntegTestCaseStack } from '../lib'; import { IntegManifestSynthesizer } from '../lib/manifest-synthesizer'; import { IntegManifestWriter } from '../lib/manifest-writer'; +let write: jest.SpyInstance; +let tmpDir: string; +let assembly: CloudAssemblyBuilder; -describe(IntegManifestSynthesizer, () => { - it('synthesizes a multiple manifests', () => { - const write = jest.spyOn(IntegManifestWriter, 'write'); +beforeEach(() => { + write = jest.spyOn(IntegManifestWriter, 'write'); + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cdk-test')); + assembly = new CloudAssemblyBuilder(tmpDir); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); +describe(IntegManifestSynthesizer, () => { + it('synthesizes multiple test cases', () => { const app = new App(); const stack = new Stack(app, 'stack'); - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cdk-test')); - const assembly = new CloudAssemblyBuilder(tmpDir); const synthesizer = new IntegManifestSynthesizer([ new IntegTestCase(stack, 'case1', { @@ -34,13 +43,71 @@ describe(IntegManifestSynthesizer, () => { expect(write).toHaveBeenCalledWith({ version: Manifest.version(), testCases: { - case1: { + ['stack/case1']: { + assertionStack: expect.stringMatching(/DeployAssert/), stacks: ['stack-under-test-1'], }, - case2: { + ['stack/case2']: { + assertionStack: expect.stringMatching(/DeployAssert/), stacks: ['stack-under-test-2'], }, }, }, tmpDir); }); + + test('default', () => { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'stack'); + + // WHEN + new IntegTest(app, 'Integ', { + testCases: [stack], + }); + const integAssembly = app.synth(); + const integManifest = Manifest.loadIntegManifest(path.join(integAssembly.directory, 'integ.json')); + + // THEN + expect(integManifest).toEqual({ + version: Manifest.version(), + testCases: { + ['Integ/DefaultTest']: { + assertionStack: expect.stringMatching(/DeployAssert/), + stacks: ['stack'], + }, + }, + }); + }); + + test('with IntegTestCaseStack', () => { + // GIVEN + const app = new App(); + const stack = new Stack(app, 'stack'); + const testCase = new IntegTestCaseStack(app, 'Case', { + diffAssets: true, + }); + + // WHEN + new IntegTest(app, 'Integ', { + testCases: [stack, testCase], + }); + const integAssembly = app.synth(); + const integManifest = Manifest.loadIntegManifest(path.join(integAssembly.directory, 'integ.json')); + + // THEN + expect(integManifest).toEqual({ + version: Manifest.version(), + testCases: { + ['Integ/DefaultTest']: { + assertionStack: expect.stringMatching(/DeployAssert/), + stacks: ['stack'], + }, + ['Case/CaseTestCase']: { + assertionStack: expect.stringMatching(/DeployAssert/), + diffAssets: true, + stacks: ['Case'], + }, + }, + }); + }); }); diff --git a/packages/aws-cdk-lib/package.json b/packages/aws-cdk-lib/package.json index f8d545aad06af..7bfb5f121c69e 100644 --- a/packages/aws-cdk-lib/package.json +++ b/packages/aws-cdk-lib/package.json @@ -391,6 +391,7 @@ "./.jsii": "./.jsii", "./.warnings.jsii.js": "./.warnings.jsii.js", "./alexa-ask": "./alexa-ask/index.js", + "./assertions/lib/helpers-internal": "./assertions/lib/helpers-internal/index.js", "./assertions": "./assertions/index.js", "./assets": "./assets/index.js", "./aws-accessanalyzer": "./aws-accessanalyzer/index.js", @@ -505,6 +506,7 @@ "./aws-iotfleethub": "./aws-iotfleethub/index.js", "./aws-iotsitewise": "./aws-iotsitewise/index.js", "./aws-iotthingsgraph": "./aws-iotthingsgraph/index.js", + "./aws-iottwinmaker": "./aws-iottwinmaker/index.js", "./aws-iotwireless": "./aws-iotwireless/index.js", "./aws-ivs": "./aws-ivs/index.js", "./aws-kafkaconnect": "./aws-kafkaconnect/index.js", @@ -536,6 +538,7 @@ "./aws-medialive": "./aws-medialive/index.js", "./aws-mediapackage": "./aws-mediapackage/index.js", "./aws-mediastore": "./aws-mediastore/index.js", + "./aws-mediatailor": "./aws-mediatailor/index.js", "./aws-memorydb": "./aws-memorydb/index.js", "./aws-msk": "./aws-msk/index.js", "./aws-mwaa": "./aws-mwaa/index.js",