diff --git a/localstack-core/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_api_gateway.py b/localstack-core/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_api_gateway.py index 7067f5c740591..f381979844fea 100644 --- a/localstack-core/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_api_gateway.py +++ b/localstack-core/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_api_gateway.py @@ -174,6 +174,11 @@ def _headers_of(parameters: TaskParameters) -> Optional[dict]: for forbidden_prefix in StateTaskServiceApiGateway._FORBIDDEN_HTTP_HEADERS_PREFIX: if key.startswith(forbidden_prefix): raise ValueError(f"The 'Headers' field contains unsupported values: {key}") + + value = headers.get(key) + if isinstance(value, list): + headers[key] = f"[{','.join(value)}]" + if "RequestBody" in parameters: headers[HEADER_CONTENT_TYPE] = APPLICATION_JSON headers["Accept"] = APPLICATION_JSON diff --git a/tests/aws/services/stepfunctions/templates/services/services_templates.py b/tests/aws/services/stepfunctions/templates/services/services_templates.py index 8e5df3fed365c..eabb59b58e1e1 100644 --- a/tests/aws/services/stepfunctions/templates/services/services_templates.py +++ b/tests/aws/services/stepfunctions/templates/services/services_templates.py @@ -44,6 +44,9 @@ class ServicesTemplates(TemplateLoader): API_GATEWAY_INVOKE_WITH_BODY: Final[str] = os.path.join( _THIS_FOLDER, "statemachines/api_gateway_invoke_with_body.json5" ) + API_GATEWAY_INVOKE_WITH_HEADERS: Final[str] = os.path.join( + _THIS_FOLDER, "statemachines/api_gateway_invoke_with_headers.json5" + ) API_GATEWAY_INVOKE_WITH_QUERY_PARAMETERS: Final[str] = os.path.join( _THIS_FOLDER, "statemachines/api_gateway_invoke_with_query_parameters.json5" ) diff --git a/tests/aws/services/stepfunctions/templates/services/statemachines/api_gateway_invoke_with_headers.json5 b/tests/aws/services/stepfunctions/templates/services/statemachines/api_gateway_invoke_with_headers.json5 new file mode 100644 index 0000000000000..2cfc4f724d70b --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/services/statemachines/api_gateway_invoke_with_headers.json5 @@ -0,0 +1,19 @@ +{ + "Comment": "API_GATEWAY_INVOKE_WITH_HEADERS", + "StartAt": "ApiGatewayInvoke", + "States": { + "ApiGatewayInvoke": { + "Type": "Task", + "Resource": "arn:aws:states:::apigateway:invoke", + "Parameters": { + "ApiEndpoint.$": "$.ApiEndpoint", + "Method.$": "$.Method", + "Path.$": "$.Path", + "Stage.$": "$.Stage", + "RequestBody.$": "$.RequestBody", + "Headers.$": "$.Headers", + }, + "End": true + } + } +} \ No newline at end of file diff --git a/tests/aws/services/stepfunctions/v2/services/test_apigetway_task_service.py b/tests/aws/services/stepfunctions/v2/services/test_apigetway_task_service.py index e264a9a733065..295d3dc0c8f0e 100644 --- a/tests/aws/services/stepfunctions/v2/services/test_apigetway_task_service.py +++ b/tests/aws/services/stepfunctions/v2/services/test_apigetway_task_service.py @@ -280,6 +280,67 @@ def test_invoke_with_body_post( exec_input, ) + @pytest.mark.parametrize( + "custom_header", + [ + ## TODO: Implement checks for singleStringHeader case to cause exception + pytest.param( + "singleStringHeader", + marks=pytest.mark.skip(reason="Behavior parity not implemented"), + ), + ["arrayHeader0"], + ["arrayHeader0", "arrayHeader1"], + ], + ) + @markers.aws.validated + def test_invoke_with_headers( + self, + aws_client, + create_lambda_function, + create_role_with_policy, + create_iam_role_for_sfn, + create_state_machine, + create_rest_apigw, + sfn_snapshot, + custom_header, + ): + self._add_api_gateway_transformers(sfn_snapshot) + + http_method = "POST" + part_path = "id_func" + + api_url, api_stage = self._create_lambda_api_response( + apigw_client=aws_client.apigateway, + create_lambda_function=create_lambda_function, + create_role_with_policy=create_role_with_policy, + lambda_function_filename=ST.LAMBDA_ID_FUNCTION, + create_rest_apigw=create_rest_apigw, + http_method=http_method, + part_path=part_path, + ) + + template = ST.load_sfn_template(ST.API_GATEWAY_INVOKE_WITH_HEADERS) + definition = json.dumps(template) + + exec_input = json.dumps( + { + "ApiEndpoint": api_url, + "Method": http_method, + "Path": part_path, + "Stage": api_stage, + "RequestBody": {"message": "HelloWorld!"}, + "Headers": {"custom_header": custom_header}, + } + ) + create_and_record_execution( + aws_client.stepfunctions, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + ) + @markers.snapshot.skip_snapshot_verify( paths=[ # TODO: ApiGateway return incorrect output type (string instead of json) either here or in other scenarios, diff --git a/tests/aws/services/stepfunctions/v2/services/test_apigetway_task_service.snapshot.json b/tests/aws/services/stepfunctions/v2/services/test_apigetway_task_service.snapshot.json index 6f25f941e0811..ee88f11d33538 100644 --- a/tests/aws/services/stepfunctions/v2/services/test_apigetway_task_service.snapshot.json +++ b/tests/aws/services/stepfunctions/v2/services/test_apigetway_task_service.snapshot.json @@ -2108,5 +2108,519 @@ } } } + }, + "tests/aws/services/stepfunctions/v2/services/test_apigetway_task_service.py::TestTaskApiGateway::test_invoke_with_headers[singleStringHeader]": { + "recorded-date": "06-10-2024, 14:50:24", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "ApiEndpoint": "", + "Method": "POST", + "Path": "id_func", + "Stage": "sfn-apigw-api", + "RequestBody": { + "message": "HelloWorld!" + }, + "Headers": { + "custom_header": "singleStringHeader" + } + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "ApiEndpoint": "", + "Method": "POST", + "Path": "id_func", + "Stage": "sfn-apigw-api", + "RequestBody": { + "message": "HelloWorld!" + }, + "Headers": { + "custom_header": "singleStringHeader" + } + }, + "inputDetails": { + "truncated": false + }, + "name": "ApiGatewayInvoke" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "executionFailedEventDetails": { + "cause": "An error occurred while executing the state 'ApiGatewayInvoke' (entered at the event id #2). The Parameters '' could not be used to start the Task: [The value of the field 'Headers' has an invalid format]", + "error": "States.Runtime" + }, + "id": 3, + "previousEventId": 2, + "timestamp": "timestamp", + "type": "ExecutionFailed" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/services/test_apigetway_task_service.py::TestTaskApiGateway::test_invoke_with_headers[custom_header1]": { + "recorded-date": "06-10-2024, 14:50:48", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "ApiEndpoint": "", + "Method": "POST", + "Path": "id_func", + "Stage": "sfn-apigw-api", + "RequestBody": { + "message": "HelloWorld!" + }, + "Headers": { + "custom_header": [ + "arrayHeader0" + ] + } + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "ApiEndpoint": "", + "Method": "POST", + "Path": "id_func", + "Stage": "sfn-apigw-api", + "RequestBody": { + "message": "HelloWorld!" + }, + "Headers": { + "custom_header": [ + "arrayHeader0" + ] + } + }, + "inputDetails": { + "truncated": false + }, + "name": "ApiGatewayInvoke" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "taskScheduledEventDetails": { + "parameters": { + "Path": "id_func", + "Headers": { + "custom_header": [ + "arrayHeader0" + ] + }, + "Stage": "sfn-apigw-api", + "ApiEndpoint": "", + "Method": "POST", + "RequestBody": { + "message": "HelloWorld!" + } + }, + "region": "", + "resource": "invoke", + "resourceType": "apigateway" + }, + "timestamp": "timestamp", + "type": "TaskScheduled" + }, + { + "id": 4, + "previousEventId": 3, + "taskStartedEventDetails": { + "resource": "invoke", + "resourceType": "apigateway" + }, + "timestamp": "timestamp", + "type": "TaskStarted" + }, + { + "id": 5, + "previousEventId": 4, + "taskSucceededEventDetails": { + "output": { + "Headers": { + "Connection": [ + "keep-alive" + ], + "Content-Length": [ + "26" + ], + "Content-Type": [ + "application/json" + ], + "Date": "", + "Via": "", + "x-amz-apigw-id": "", + "X-Amz-Cf-Id": "", + "X-Amz-Cf-Pop": "", + "x-amzn-RequestId": "", + "X-Amzn-Trace-Id": "", + "X-Cache": [ + "Miss from cloudfront" + ] + }, + "ResponseBody": { + "message": "HelloWorld!" + }, + "StatusCode": 200, + "StatusText": "OK" + }, + "outputDetails": { + "truncated": false + }, + "resource": "invoke", + "resourceType": "apigateway" + }, + "timestamp": "timestamp", + "type": "TaskSucceeded" + }, + { + "id": 6, + "previousEventId": 5, + "stateExitedEventDetails": { + "name": "ApiGatewayInvoke", + "output": { + "Headers": { + "Connection": [ + "keep-alive" + ], + "Content-Length": [ + "26" + ], + "Content-Type": [ + "application/json" + ], + "Date": "", + "Via": "", + "x-amz-apigw-id": "", + "X-Amz-Cf-Id": "", + "X-Amz-Cf-Pop": "", + "x-amzn-RequestId": "", + "X-Amzn-Trace-Id": "", + "X-Cache": [ + "Miss from cloudfront" + ] + }, + "ResponseBody": { + "message": "HelloWorld!" + }, + "StatusCode": 200, + "StatusText": "OK" + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "TaskStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "Headers": { + "Connection": [ + "keep-alive" + ], + "Content-Length": [ + "26" + ], + "Content-Type": [ + "application/json" + ], + "Date": "", + "Via": "", + "x-amz-apigw-id": "", + "X-Amz-Cf-Id": "", + "X-Amz-Cf-Pop": "", + "x-amzn-RequestId": "", + "X-Amzn-Trace-Id": "", + "X-Cache": [ + "Miss from cloudfront" + ] + }, + "ResponseBody": { + "message": "HelloWorld!" + }, + "StatusCode": 200, + "StatusText": "OK" + }, + "outputDetails": { + "truncated": false + } + }, + "id": 7, + "previousEventId": 6, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/services/test_apigetway_task_service.py::TestTaskApiGateway::test_invoke_with_headers[custom_header2]": { + "recorded-date": "06-10-2024, 14:51:07", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "ApiEndpoint": "", + "Method": "POST", + "Path": "id_func", + "Stage": "sfn-apigw-api", + "RequestBody": { + "message": "HelloWorld!" + }, + "Headers": { + "custom_header": [ + "arrayHeader0", + "arrayHeader1" + ] + } + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "ApiEndpoint": "", + "Method": "POST", + "Path": "id_func", + "Stage": "sfn-apigw-api", + "RequestBody": { + "message": "HelloWorld!" + }, + "Headers": { + "custom_header": [ + "arrayHeader0", + "arrayHeader1" + ] + } + }, + "inputDetails": { + "truncated": false + }, + "name": "ApiGatewayInvoke" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "taskScheduledEventDetails": { + "parameters": { + "Path": "id_func", + "Headers": { + "custom_header": [ + "arrayHeader0", + "arrayHeader1" + ] + }, + "Stage": "sfn-apigw-api", + "ApiEndpoint": "", + "Method": "POST", + "RequestBody": { + "message": "HelloWorld!" + } + }, + "region": "", + "resource": "invoke", + "resourceType": "apigateway" + }, + "timestamp": "timestamp", + "type": "TaskScheduled" + }, + { + "id": 4, + "previousEventId": 3, + "taskStartedEventDetails": { + "resource": "invoke", + "resourceType": "apigateway" + }, + "timestamp": "timestamp", + "type": "TaskStarted" + }, + { + "id": 5, + "previousEventId": 4, + "taskSucceededEventDetails": { + "output": { + "Headers": { + "Connection": [ + "keep-alive" + ], + "Content-Length": [ + "26" + ], + "Content-Type": [ + "application/json" + ], + "Date": "", + "Via": "", + "x-amz-apigw-id": "", + "X-Amz-Cf-Id": "", + "X-Amz-Cf-Pop": "", + "x-amzn-RequestId": "", + "X-Amzn-Trace-Id": "", + "X-Cache": [ + "Miss from cloudfront" + ] + }, + "ResponseBody": { + "message": "HelloWorld!" + }, + "StatusCode": 200, + "StatusText": "OK" + }, + "outputDetails": { + "truncated": false + }, + "resource": "invoke", + "resourceType": "apigateway" + }, + "timestamp": "timestamp", + "type": "TaskSucceeded" + }, + { + "id": 6, + "previousEventId": 5, + "stateExitedEventDetails": { + "name": "ApiGatewayInvoke", + "output": { + "Headers": { + "Connection": [ + "keep-alive" + ], + "Content-Length": [ + "26" + ], + "Content-Type": [ + "application/json" + ], + "Date": "", + "Via": "", + "x-amz-apigw-id": "", + "X-Amz-Cf-Id": "", + "X-Amz-Cf-Pop": "", + "x-amzn-RequestId": "", + "X-Amzn-Trace-Id": "", + "X-Cache": [ + "Miss from cloudfront" + ] + }, + "ResponseBody": { + "message": "HelloWorld!" + }, + "StatusCode": 200, + "StatusText": "OK" + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "TaskStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "Headers": { + "Connection": [ + "keep-alive" + ], + "Content-Length": [ + "26" + ], + "Content-Type": [ + "application/json" + ], + "Date": "", + "Via": "", + "x-amz-apigw-id": "", + "X-Amz-Cf-Id": "", + "X-Amz-Cf-Pop": "", + "x-amzn-RequestId": "", + "X-Amzn-Trace-Id": "", + "X-Cache": [ + "Miss from cloudfront" + ] + }, + "ResponseBody": { + "message": "HelloWorld!" + }, + "StatusCode": 200, + "StatusText": "OK" + }, + "outputDetails": { + "truncated": false + } + }, + "id": 7, + "previousEventId": 6, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/stepfunctions/v2/services/test_apigetway_task_service.validation.json b/tests/aws/services/stepfunctions/v2/services/test_apigetway_task_service.validation.json index 5b7de803e6835..22773c1a8de00 100644 --- a/tests/aws/services/stepfunctions/v2/services/test_apigetway_task_service.validation.json +++ b/tests/aws/services/stepfunctions/v2/services/test_apigetway_task_service.validation.json @@ -17,6 +17,15 @@ "tests/aws/services/stepfunctions/v2/services/test_apigetway_task_service.py::TestTaskApiGateway::test_invoke_with_body_post[request_body3]": { "last_validated_date": "2023-08-25T10:42:12+00:00" }, + "tests/aws/services/stepfunctions/v2/services/test_apigetway_task_service.py::TestTaskApiGateway::test_invoke_with_headers[custom_header1]": { + "last_validated_date": "2024-10-06T14:50:48+00:00" + }, + "tests/aws/services/stepfunctions/v2/services/test_apigetway_task_service.py::TestTaskApiGateway::test_invoke_with_headers[custom_header2]": { + "last_validated_date": "2024-10-06T14:51:07+00:00" + }, + "tests/aws/services/stepfunctions/v2/services/test_apigetway_task_service.py::TestTaskApiGateway::test_invoke_with_headers[singleStringHeader]": { + "last_validated_date": "2024-10-06T14:50:24+00:00" + }, "tests/aws/services/stepfunctions/v2/services/test_apigetway_task_service.py::TestTaskApiGateway::test_invoke_with_query_parameters": { "last_validated_date": "2023-08-20T13:18:59+00:00" }