diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md index 269135eed0652..ecf995be46d3d 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md @@ -26,6 +26,8 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aw - [ResultPath](#resultpath) - [Parameters](#task-parameters-from-the-state-json) - [Evaluate Expression](#evaluate-expression) +- [API Gateway](#api-gateway) + - [Invoke](#invoke) - [Athena](#athena) - [StartQueryExecution](#startQueryExecution) - [GetQueryExecution](#getQueryExecution) @@ -211,6 +213,27 @@ runtime to use to evaluate the expression. Currently, the only runtime supported is `lambda.Runtime.NODEJS_10_X`. +## API Gateway + +Step Functions supports [API Gateway](https://docs.aws.amazon.com/step-functions/latest/dg/connect-api-gateway.html) through the service integration pattern. + +### Invoke + +The [Invoke](https://docs.aws.amazon.com/step-functions/latest/dg/connect-api-gateway.html) API calls the API endpoint. + +```ts +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as tasks from `@aws-cdk/aws-stepfunctions-tasks`; + +const restApi = new apigateway.RestApi(stack, 'MyRestApi'); + +const invokeJob = new tasks.ApiGatewayInvoke(stack, 'Invoke APIGW', { + api: restApi, + stageName: 'prod', + method: ApiGatewayMethodType.GET, +}); +``` + ## Athena Step Functions supports [Athena](https://docs.aws.amazon.com/step-functions/latest/dg/connect-athena.html) through the service integration pattern. diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/invoke.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/invoke.ts new file mode 100644 index 0000000000000..389f457b4fc20 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/apigateway/invoke.ts @@ -0,0 +1,209 @@ +import * as apigateway from '@aws-cdk/aws-apigateway'; +import * as iam from '@aws-cdk/aws-iam'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { integrationResourceArn, validatePatternSupported } from '../private/task-utils'; + +/** + * Properties for invoking an API Endpoint with ApiGatewayInvoke + */ +export interface ApiGatewayInvokeProps extends sfn.TaskStateBaseProps { + + /** API to call */ + readonly api: apigateway.IRestApi; + + /** Http method for the API */ + readonly method: HttpMethod; + + /** + * HTTP request information that does not relate to contents of the request + * @default - No headers + * @example + * Headers: { + * type: 1, + * value:{ + * 'TaskToken.$': 'States.Array($$.Task.Token)', + * } + * }, + */ + readonly headers?: { [key: string]: any }; + + /** + * Name of the stage where the API is deployed to in API Gateway + * @default - Required for REST and $default for HTTP which acts as a catch-all for requests that don’t match any other routes + */ + readonly stageName?: string; + + /** + * Path parameters appended after API endpoint + * @default - No path + */ + readonly path?: string; + + /** + * Query strings attatched to end of request + * @default - No query parameters + * @example + * "QueryParameters": { + * "billId": ["123", "456"] + * }, + */ + readonly queryParameters?: { [key: string]: any }; + + /** + * HTTP Request body + * @default - No requestBody + * @example + * "RequestBody": { + * "billId": ["my-new-bill"] + * }, + */ + readonly requestBody?: sfn.TaskInput; + + /** + * Authentication methods + * @default AuthType.NO_AUTH + */ + readonly authType?: AuthType; + +} + +/** + * Invoke an API endpoint as a Task + * + * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-api-gateway.html + */ +export class ApiGatewayInvoke extends sfn.TaskStateBase { + + private static readonly SUPPORTED_INTEGRATION_PATTERNS: sfn.IntegrationPattern[] = [ + sfn.IntegrationPattern.REQUEST_RESPONSE, + sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + ]; + protected readonly taskMetrics?: sfn.TaskMetricsConfig; + protected readonly taskPolicies?: iam.PolicyStatement[]; + protected readonly apiEndpoint: string; + + private readonly integrationPattern: sfn.IntegrationPattern; + + constructor(scope: Construct, id: string, private readonly props: ApiGatewayInvokeProps) { + super(scope, id, props); + this.integrationPattern = props.integrationPattern ?? sfn.IntegrationPattern.REQUEST_RESPONSE; + + validatePatternSupported(this.integrationPattern, ApiGatewayInvoke.SUPPORTED_INTEGRATION_PATTERNS); + + this.taskPolicies = this.createPolicyStatements(); + this.apiEndpoint = this.createApiEndpoint(); + } + + /** + * Provides the API Gateway Invoke service integration task configuration + * @internal + */ + protected _renderTask(): any { + return { + Resource: integrationResourceArn('apigateway', 'invoke', this.integrationPattern), + Parameters: sfn.FieldUtils.renderObject({ + ApiEndpoint: this.apiEndpoint, + Method: this.props.method, + Headers: this.props.headers, + Stage: this.props.stageName, + Path: this.props.path, + QueryParameters: this.props.queryParameters, + RequestBody: this.props.requestBody, + AuthType: this.props.authType ? this.props.authType : 'NO_AUTH', + }), + }; + } + + /** + * Gets the resource to attatched to the ExecuteAPI:Invoke + * @returns resource. + * @default "*" returns the execute API ARN for all methods/resources in this API. + * @param method The method (default `*`) + * @param path The resource path. Must start with '/' (default `*`) + * @param stage The stage (default `*`) + * @example {ApiId}/{stage}/{method}/{path} + */ + get arnForExecuteApi() { + return this.props.api.arnForExecuteApi(this.props.method, this.props.path, this.props.stageName); + } + + /** + * Generates the api endpoint + * @returns The api id + * @example {ApiId}.execute-api.{region}.amazonaws.com + */ + private createApiEndpoint(): string { + const apiStack = cdk.Stack.of(this.props.api); + return `${this.props.api.restApiId}.execute-api.${apiStack.region}.${apiStack.urlSuffix}`; + } + + /** + * This generates the PolicyStatements required by the Task to call invoke. + */ + private createPolicyStatements(): iam.PolicyStatement[] { + if (this.props.authType === AuthType.IAM_ROLE) { + return [ + new iam.PolicyStatement({ + resources: [this.arnForExecuteApi], + actions: ['ExecuteAPI:Invoke'], + }), + ]; + } else if (this.props.authType === AuthType.RESOURCE_POLICY) { + if (!sfn.FieldUtils.containsTaskToken(this.props.headers)) { + throw new Error('Task Token is required in `headers` Use JsonPath.taskToken to set the token.'); + } + return [ + new iam.PolicyStatement({ + resources: [this.arnForExecuteApi], + actions: ['ExecuteAPI:Invoke'], + conditions: { + StringEquals: { + 'aws:SourceArn': '*', + }, + }, + }), + ]; + } + return []; + } +} + +/** Http Methods that API Gateway supports */ +export enum HttpMethod { + /** Retreive data from a server at the specified resource */ + GET = 'GET', + + /** Send data to the API endpoint to create or udpate a resource */ + POST = 'POST', + + /** Send data to the API endpoint to update or create a resource */ + PUT = 'PUT', + + /** Delete the resource at the specified endpoint */ + DELETE = 'DELETE', + + /** Apply partial modifications to the resource */ + PATCH = 'PATCH', + + /** Retreive data from a server at the specified resource without the response body */ + HEAD = 'HEAD', + + /** Return data describing what other methods and operations the server supports */ + OPTIONS = 'OPTIONS' +} + +/** + * The authentication method used to call the endpoint + */ +export enum AuthType { + /** Call the API direclty with no authorization method */ + NO_AUTH = 'NO_AUTH', + + /** * Use the IAM role associated with the current state machine for authorization */ + IAM_ROLE = 'IAM_ROLE', + + /** Use the resource policy of the API for authorization */ + RESOURCE_POLICY = 'RESOURCE_POLICY', +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts index 8e3567f2a8f88..196d4dfecaa2d 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts @@ -43,3 +43,4 @@ export * from './athena/start-query-execution'; export * from './athena/stop-query-execution'; export * from './athena/get-query-execution'; export * from './athena/get-query-results'; +export * from './apigateway/invoke'; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json index 9d7dca5eee6fd..99dd28480d26d 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json @@ -74,6 +74,7 @@ }, "dependencies": { "@aws-cdk/assets": "0.0.0", + "@aws-cdk/aws-apigateway": "0.0.0", "@aws-cdk/aws-batch": "0.0.0", "@aws-cdk/aws-cloudwatch": "0.0.0", "@aws-cdk/aws-codebuild": "0.0.0", @@ -96,6 +97,7 @@ "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { "@aws-cdk/assets": "0.0.0", + "@aws-cdk/aws-apigateway": "0.0.0", "@aws-cdk/aws-batch": "0.0.0", "@aws-cdk/aws-cloudwatch": "0.0.0", "@aws-cdk/aws-codebuild": "0.0.0", diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.invoke.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.invoke.expected.json new file mode 100644 index 0000000000000..ca507e207c129 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.invoke.expected.json @@ -0,0 +1,394 @@ +{ + "Resources": { + "MyRestApi2D1F47A9": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Name": "MyRestApi" + } + }, + "MyRestApiCloudWatchRoleD4042E8E": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs" + ] + ] + } + ] + } + }, + "MyRestApiAccount2FB6DB7A": { + "Type": "AWS::ApiGateway::Account", + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "MyRestApiCloudWatchRoleD4042E8E", + "Arn" + ] + } + }, + "DependsOn": [ + "MyRestApi2D1F47A9" + ] + }, + "MyRestApiDeploymentB555B582d61dc696e12272a0706c826196fa8d62": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "MyRestApi2D1F47A9" + }, + "Description": "Automatically created by the RestApi construct" + }, + "DependsOn": [ + "MyRestApiANY05143F93" + ] + }, + "MyRestApiDeploymentStageprodC33B8E5F": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "RestApiId": { + "Ref": "MyRestApi2D1F47A9" + }, + "DeploymentId": { + "Ref": "MyRestApiDeploymentB555B582d61dc696e12272a0706c826196fa8d62" + }, + "StageName": "prod" + } + }, + "MyRestApiANYApiPermissionawsstepfunctionstasksapigatewayinvokeintegMyRestApi2586FD6CANYF85B0B1B": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "Hello4A628BD4", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "MyRestApi2D1F47A9" + }, + "/", + { + "Ref": "MyRestApiDeploymentStageprodC33B8E5F" + }, + "/*/" + ] + ] + } + } + }, + "MyRestApiANYApiPermissionTestawsstepfunctionstasksapigatewayinvokeintegMyRestApi2586FD6CANY2BA29F0F": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "Hello4A628BD4", + "Arn" + ] + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "MyRestApi2D1F47A9" + }, + "/test-invoke-stage/*/" + ] + ] + } + } + }, + "MyRestApiANY05143F93": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "HttpMethod": "ANY", + "ResourceId": { + "Fn::GetAtt": [ + "MyRestApi2D1F47A9", + "RootResourceId" + ] + }, + "RestApiId": { + "Ref": "MyRestApi2D1F47A9" + }, + "AuthorizationType": "NONE", + "Integration": { + "IntegrationHttpMethod": "POST", + "Type": "AWS_PROXY", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":apigateway:", + { + "Ref": "AWS::Region" + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "Hello4A628BD4", + "Arn" + ] + }, + "/invocations" + ] + ] + } + } + } + }, + "HelloServiceRole1E55EA16": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "Hello4A628BD4": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = function helloCode(_event, _context, callback) {\n return callback(undefined, {\n statusCode: 200,\n body: 'hello, world!',\n });\n}" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "HelloServiceRole1E55EA16", + "Arn" + ] + }, + "Runtime": "nodejs10.x" + }, + "DependsOn": [ + "HelloServiceRole1E55EA16" + ] + }, + "StateMachineRoleB840431D": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "states.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StateMachineRoleDefaultPolicyDF1E6607": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "ExecuteAPI:Invoke", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":execute-api:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":", + { + "Ref": "MyRestApi2D1F47A9" + }, + "/prod/GET/*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "StateMachineRoleDefaultPolicyDF1E6607", + "Roles": [ + { + "Ref": "StateMachineRoleB840431D" + } + ] + } + }, + "StateMachine2E01A3A5": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + }, + "DefinitionString": { + "Fn::Join": [ + "", + [ + "{\"StartAt\":\"Invoke APIGW\",\"States\":{\"Invoke APIGW\":{\"End\":true,\"Type\":\"Task\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::apigateway:invoke\",\"Parameters\":{\"ApiEndpoint\":\"", + { + "Ref": "MyRestApi2D1F47A9" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "\",\"Method\":\"GET\",\"Stage\":\"prod\",\"AuthType\":\"IAM_ROLE\"}}},\"TimeoutSeconds\":30}" + ] + ] + } + }, + "DependsOn": [ + "StateMachineRoleDefaultPolicyDF1E6607", + "StateMachineRoleB840431D" + ] + } + }, + "Outputs": { + "MyRestApiEndpoint4C55E4CB": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "MyRestApi2D1F47A9" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "MyRestApiDeploymentStageprodC33B8E5F" + }, + "/" + ] + ] + } + }, + "stateMachineArn": { + "Value": { + "Ref": "StateMachine2E01A3A5" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.invoke.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.invoke.ts new file mode 100644 index 0000000000000..b8c5d9ea0ee50 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/integ.invoke.ts @@ -0,0 +1,48 @@ +import * as apigateway from '@aws-cdk/aws-apigateway'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { ApiGatewayInvoke, AuthType, HttpMethod } from '../../lib'; + +/* + * Stack verification steps: + * * aws stepfunctions start-execution --state-machine-arn : should return execution arn + * * aws stepfunctions describe-execution --execution-arn : should return status as SUCCEEDED and a query-execution-id + * * aws apigateway test-invoke-method --rest-api-id --resource-id --http-method : should return the same response body and status as in the task succeeded/failed execution + */ +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-stepfunctions-tasks-apigateway-invoke-integ'); +const restApi = new apigateway.RestApi(stack, 'MyRestApi'); + +function helloCode(_event: any, _context: any, callback: any) { + return callback(undefined, { + statusCode: 200, + body: 'hello, world!', + }); +} + +const hello = new apigateway.LambdaIntegration(new lambda.Function(stack, 'Hello', { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.inline(`exports.handler = ${helloCode}`), +})); + +restApi.root.addMethod('ANY', hello); + +const invokeJob = new ApiGatewayInvoke(stack, 'Invoke APIGW', { + api: restApi, + stageName: 'prod', + method: HttpMethod.GET, + authType: AuthType.IAM_ROLE, +}); + +const chain = sfn.Chain.start(invokeJob); + +const sm = new sfn.StateMachine(stack, 'StateMachine', { + definition: chain, + timeout: cdk.Duration.seconds(30), +}); + +new cdk.CfnOutput(stack, 'stateMachineArn', { + value: sm.stateMachineArn, +}); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/invoke.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/invoke.test.ts new file mode 100644 index 0000000000000..9e28cddb790d8 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/apigateway/invoke.test.ts @@ -0,0 +1,140 @@ +import * as apigateway from '@aws-cdk/aws-apigateway'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import { ApiGatewayInvoke, AuthType, HttpMethod } from '../../lib/apigateway/invoke'; + +describe('Invoke API', () => { + + test('default settings', () => { + // GIVEN + const stack = new cdk.Stack(); + const restApi = new apigateway.RestApi(stack, 'apiid.execute-api.region.amazonaws.com'); + + // WHEN + const task = new ApiGatewayInvoke(stack, 'Invoke', { + api: restApi, + method: HttpMethod.GET, + stageName: '$default', + path: 'path', + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::apigateway:invoke', + ], + ], + }, + End: true, + Parameters: { + ApiEndpoint: { + 'Fn::Join': [ + '', + [ + { + Ref: 'apiidexecuteapiregionamazonawscomF3F21B4A', + }, + '.execute-api.', + { + Ref: 'AWS::Region', + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + ], + ], + }, + Method: HttpMethod.GET, + Stage: '$default', + AuthType: 'NO_AUTH', + Path: 'path', + }, + }); + }); + + test('Wait for Task Token', () => { + // GIVEN + const stack = new cdk.Stack(); + const restApi = new apigateway.RestApi(stack, 'apiid.execute-api.region.amazonaws.com'); + const taskToken = { + 'TaskToken.$': 'States.Array($$.Task.Token)', + }; + + // WHEN + const task = new ApiGatewayInvoke(stack, 'Invoke', { + integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + api: restApi, + method: HttpMethod.GET, + headers: taskToken, + stageName: '$default', + path: 'path', + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::apigateway:invoke.waitForTaskToken', + ], + ], + }, + End: true, + Parameters: { + ApiEndpoint: { + 'Fn::Join': [ + '', + [ + { + Ref: 'apiidexecuteapiregionamazonawscomF3F21B4A', + }, + '.execute-api.', + { + Ref: 'AWS::Region', + }, + '.', + { + Ref: 'AWS::URLSuffix', + }, + ], + ], + }, + Headers: { + 'TaskToken.$': 'States.Array($$.Task.Token)', + }, + Method: HttpMethod.GET, + AuthType: 'NO_AUTH', + Path: 'path', + Stage: '$default', + }, + }); + }); + + test('Invoke - Wait For Task Token - Missing Task Token', () => { + const stack = new cdk.Stack(); + const restApi = new apigateway.RestApi(stack, 'apiid.execute-api.region.amazonaws.com'); + expect(() => { + new ApiGatewayInvoke(stack, 'Invoke', { + integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + api: restApi, + method: HttpMethod.GET, + authType: AuthType.RESOURCE_POLICY, + }); + }).toThrow('Task Token is required in `headers` Use JsonPath.taskToken to set the token.'); + }); +}); \ No newline at end of file