From fb8cd659808abf8d8a00284d725370247d3277aa Mon Sep 17 00:00:00 2001 From: Kevin DeJong Date: Sat, 13 Jul 2024 09:21:16 -0700 Subject: [PATCH] Move rule to E2532 to E3601 --- .../resources/stepfunctions/StateMachine.py | 202 --------- .../stepfunctions/StateMachineDefinition.py | 11 +- .../stepfunctions/state_machine.yaml | 47 --- .../stepfunctions/state_machine.yaml | 164 -------- .../stepfunctions/test_state_machine.py | 32 -- .../test_state_machine_definition.py | 388 ++++++++++++++++++ 6 files changed, 394 insertions(+), 450 deletions(-) delete mode 100644 src/cfnlint/rules/resources/stepfunctions/StateMachine.py delete mode 100644 test/fixtures/templates/bad/resources/stepfunctions/state_machine.yaml delete mode 100644 test/fixtures/templates/good/resources/stepfunctions/state_machine.yaml delete mode 100644 test/unit/rules/resources/stepfunctions/test_state_machine.py create mode 100644 test/unit/rules/resources/stepfunctions/test_state_machine_definition.py diff --git a/src/cfnlint/rules/resources/stepfunctions/StateMachine.py b/src/cfnlint/rules/resources/stepfunctions/StateMachine.py deleted file mode 100644 index c8608e2d14..0000000000 --- a/src/cfnlint/rules/resources/stepfunctions/StateMachine.py +++ /dev/null @@ -1,202 +0,0 @@ -""" -Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: MIT-0 -""" - -import json - -from cfnlint.rules import CloudFormationLintRule, RuleMatch - - -class StateMachine(CloudFormationLintRule): - """Check State Ma chine Definition""" - - id = "E2532" - shortdesc = "Check State Machine Definition for proper syntax" - description = ( - "Check the State Machine String Definition to make sure its JSON. " - "Validate basic syntax of the file to determine validity." - ) - source_url = "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-stepfunctions-statemachine.html" - tags = ["resources", "stepfunctions"] - - def __init__(self): - """Init""" - super().__init__() - self.resource_property_types.append("AWS::StepFunctions::StateMachine") - - def _check_state_json(self, def_json, state_name, path): - """Check State JSON Definition""" - matches = [] - - # https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-common-fields.html - common_state_keys = [ - "Next", - "End", - "Type", - "Comment", - "InputPath", - "OutputPath", - ] - common_state_required_keys = [ - "Type", - ] - state_key_types = { - "Pass": ["Result", "ResultPath", "Parameters"], - "Task": [ - "Credentials", - "Resource", - "Parameters", - "ResultPath", - "ResultSelector", - "Retry", - "Catch", - "TimeoutSeconds", - "TimeoutSecondsPath", - "Parameters", - "HeartbeatSeconds", - "HeartbeatSecondsPath", - ], - "Map": [ - "MaxConcurrency", - "Iterator", - "ItemsPath", - "ItemProcessor", - "ItemReader", - "ItemSelector", - "ResultPath", - "ResultSelector", - "Retry", - "Catch", - "Parameters", - "ToleratedFailurePercentage", - "ItemBatcher", - ], - "Choice": ["Choices", "Default"], - "Wait": ["Seconds", "Timestamp", "SecondsPath", "TimestampPath"], - "Succeed": [], - "Fail": ["Cause", "CausePath", "Error", "ErrorPath"], - "Parallel": [ - "Branches", - "ResultPath", - "ResultSelector", - "Parameters", - "Retry", - "Catch", - ], - } - state_required_types = { - "Pass": [], - "Task": ["Resource"], - "Choice": ["Choices"], - "Wait": [], - "Succeed": [], - "Fail": [], - "Parallel": ["Branches"], - } - - for req_key in common_state_required_keys: - if req_key not in def_json: - message = ( - f"State Machine Definition required key ({req_key}) for State" - f" ({state_name}) is missing" - ) - matches.append(RuleMatch(path, message)) - return matches - - state_type = def_json.get("Type") - - if state_type in state_key_types: - for state_key, _ in def_json.items(): - if state_key not in common_state_keys + state_key_types.get( - state_type, [] - ): - message = ( - f"State Machine Definition key ({state_key}) for State" - f" ({state_name}) of Type ({state_type}) is not valid" - ) - matches.append(RuleMatch(path, message)) - for req_key in common_state_required_keys + state_required_types.get( - state_type, [] - ): - if req_key not in def_json: - message = ( - f"State Machine Definition required key ({req_key}) for State" - f" ({state_name}) of Type ({state_type}) is missing" - ) - matches.append(RuleMatch(path, message)) - return matches - else: - message = f"State Machine Definition Type ({state_type}) is not valid" - matches.append(RuleMatch(path, message)) - - return matches - - def _check_definition_json(self, def_json, path): - """Check JSON Definition""" - matches = [] - - top_level_keys = ["Comment", "StartAt", "TimeoutSeconds", "Version", "States"] - top_level_required_keys = ["StartAt", "States"] - for top_key, _ in def_json.items(): - if top_key not in top_level_keys: - message = f"State Machine Definition key ({top_key}) is not valid" - matches.append(RuleMatch(path, message)) - - for req_key in top_level_required_keys: - if req_key not in def_json: - message = ( - f"State Machine Definition required key ({req_key}) is missing" - ) - matches.append(RuleMatch(path, message)) - - for state_name, state_value in def_json.get("States", {}).items(): - matches.extend(self._check_state_json(state_value, state_name, path)) - return matches - - def check_value(self, value, path, fail_on_loads=True): - """Check Definition Value""" - matches = [] - try: - def_json = json.loads(value) - # pylint: disable=W0703 - except Exception as err: - if fail_on_loads: - message = ( - "State Machine Definition needs to be formatted as JSON. Error" - f" {err}" - ) - matches.append(RuleMatch(path, message)) - return matches - - self.logger.debug("State Machine definition could not be parsed. Skipping") - return matches - - matches.extend(self._check_definition_json(def_json, path)) - return matches - - def check_sub(self, value, path): - """Check Sub Object""" - matches = [] - if isinstance(value, list): - matches.extend(self.check_value(value[0], path, False)) - elif isinstance(value, str): - matches.extend(self.check_value(value, path, False)) - - return matches - - def match_resource_properties(self, properties, _, path, cfn): - """Check CloudFormation Properties""" - matches = [] - - matches.extend( - cfn.check_value( - obj=properties, - key="DefinitionString", - path=path[:], - check_value=self.check_value, - check_sub=self.check_sub, - ) - ) - - return matches diff --git a/src/cfnlint/rules/resources/stepfunctions/StateMachineDefinition.py b/src/cfnlint/rules/resources/stepfunctions/StateMachineDefinition.py index 831ac747d4..7d7c3de89a 100644 --- a/src/cfnlint/rules/resources/stepfunctions/StateMachineDefinition.py +++ b/src/cfnlint/rules/resources/stepfunctions/StateMachineDefinition.py @@ -16,14 +16,15 @@ from cfnlint.schema.resolver import RefResolver -class Configuration(CfnLintJsonSchema): +class StateMachineDefinition(CfnLintJsonSchema): id = "E3601" - shortdesc = "Basic CloudFormation Resource Check" + shortdesc = "Validate the structure of a StateMachine definition" description = ( - "Making sure the basic CloudFormation resources are properly configured" + "Validate the Definition or DefinitionString inside a " + "AWS::StepFunctions::StateMachine resource" ) - source_url = "https://github.com/aws-cloudformation/cfn-lint" - tags = ["resources"] + source_url = "https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-state-machine-structure.html" + tags = ["resources", "statemachine"] def __init__(self): super().__init__( diff --git a/test/fixtures/templates/bad/resources/stepfunctions/state_machine.yaml b/test/fixtures/templates/bad/resources/stepfunctions/state_machine.yaml deleted file mode 100644 index 889ed6a9f8..0000000000 --- a/test/fixtures/templates/bad/resources/stepfunctions/state_machine.yaml +++ /dev/null @@ -1,47 +0,0 @@ -AWSTemplateFormatVersion: '2010-09-09' -Description: An example template for a Step Functions state machine. -Resources: - MyStateMachine: - Type: AWS::StepFunctions::StateMachine - Properties: - StateMachineName: HelloWorld-StateMachine - # Alert on Bad JSON - DefinitionString: |- - { - "StartAt": "HelloWorld", - "States": { - "HelloWorld": { - "Type": "Task", - "Resource": "arn:aws:lambda:us-east-1:111122223333:function:HelloFunction", - "End": true - - } - } - RoleArn: arn:aws:iam::111122223333:role/service-role/StatesExecutionRole-us-east-1 - MyStateMachine2: - Type: AWS::StepFunctions::StateMachine - Properties: - StateMachineName: HelloWorld-StateMachine - # Missing StartsAt - # ByeWorld is missing Resource - # HelloWorld is missing Type - # Type doesn't exist - DefinitionString: - Fn::Sub: |- - { - "States": { - "HelloWorld": { - "Resource": "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:HelloFunction", - "Next": "GoodDay" - }, - "GoodDay": { - "Type": "DNE", - "Next": "ByeWorld" - }, - "ByeWorld": { - "Type": "Task", - "End": true - } - } - } - RoleArn: arn:aws:iam::111122223333:role/service-role/StatesExecutionRole-us-east-1 diff --git a/test/fixtures/templates/good/resources/stepfunctions/state_machine.yaml b/test/fixtures/templates/good/resources/stepfunctions/state_machine.yaml deleted file mode 100644 index 7c53a4ddef..0000000000 --- a/test/fixtures/templates/good/resources/stepfunctions/state_machine.yaml +++ /dev/null @@ -1,164 +0,0 @@ -AWSTemplateFormatVersion: '2010-09-09' -Description: An example template for a Step Functions state machine. -Parameters: - timeout: - Type: Number -Resources: - MyStateMachine: - Type: AWS::StepFunctions::StateMachine - Properties: - StateMachineName: HelloWorld-StateMachine - DefinitionString: |- - { - "StartAt": "HelloWorld", - "States": { - "HelloWorld": { - "Type": "Task", - "Resource": "arn:aws:lambda:us-east-1:111122223333:function:HelloFunction", - "Next": "CreatePublishedRequest" - }, - "CreatePublishedRequest": { - "Type": "Task", - "Resource": "{$createPublishedRequest}", - "ResultPath":"$.publishedRequest", - "OutputPath":"$.publishedRequest", - "Next": "PutRequest" - }, - "PutRequest": { - "Type": "Task", - "Resource": "{$updateKey}", - "ResultPath":"$.response", - "End": true - } - } - } - RoleArn: arn:aws:iam::111122223333:role/service-role/StatesExecutionRole-us-east-1 - # doesn't fail on sub that can't be parsed - MyStateMachine2: - Type: AWS::StepFunctions::StateMachine - Properties: - StateMachineName: HelloWorld-StateMachine - DefinitionString: - Fn::Sub: - - | - { - "StartAt": "HelloWorld", - "States": { - "HelloWorld": { - "Type": "Task", - "Resource": "arn:aws:lambda:us-east-1:111122223333:function:HelloFunction", - "Next": "CreatePublishedRequest" - }, - "CreatePublishedRequest": { - "Type": "Task", - "Resource": "{$createPublishedRequest}", - "ResultPath":"$.publishedRequest", - "OutputPath":"$.publishedRequest", - "Next": "Read Next Message from DynamoDB" - }, - "Read Next Message from DynamoDB": { - "Type": "Task", - "Resource": "arn:aws:states:::dynamodb:getItem", - "Parameters": { - "TableName": "sqsconnector-DDBTable-1CAFOJWP8QD6I", - "Key": { - "MessageId": {"S.$": "$.List[0]"} - } - }, - "ResultPath": "$.DynamoDB", - "Next": "CreatePublishedRequest" - }, - "Sleep": { - "Type": "Wait", - "Seconds": ${TestParam}, - "Next": "Stop" - }, - "PutRequest": { - "Type": "Task", - "Resource": "{$updateKey}", - "ResultPath":"$.response", - "End": true - } - } - } - - TestParam: !Ref timeout - RoleArn: arn:aws:iam::111122223333:role/service-role/StatesExecutionRole-us-east-1 - MyStateMachineNewTypes: - Type: AWS::StepFunctions::StateMachine - Properties: - StateMachineName: HelloWorld-StateMachine - DefinitionString: |- - { - "StartAt": "ValidatePayment", - "States": { - "ValidatePayment": { - "Type": "Task", - "Resource": "arn:aws:lambda:us-west-2:123456789012:function:validatePayment", - "Next": "CheckPayment" - }, - "CheckPayment": { - "Type": "Choice", - "Choices": [ - { - "Not": { - "Variable": "$.payment", - "StringEquals": "Ok" - }, - "Next": "PaymentFailed" - } - ], - "Default": "ProcessAllItems" - }, - "PaymentFailed": { - "Type": "Task", - "Resource": "arn:aws:lambda:us-west-2:123456789012:function:paymentFailed", - "End": true - }, - "ProcessAllItems": { - "Type": "Map", - "InputPath": "$.detail", - "ItemsPath": "$.items", - "MaxConcurrency": 3, - "Iterator": { - "StartAt": "CheckAvailability", - "States": { - "CheckAvailability": { - "Type": "Task", - "Resource": "arn:aws:lambda:us-west-2:123456789012:function:checkAvailability", - "Retry": [ - { - "ErrorEquals": [ - "TimeOut" - ], - "IntervalSeconds": 1, - "BackoffRate": 2, - "MaxAttempts": 3 - } - ], - "Next": "PrepareForDelivery" - }, - "PrepareForDelivery": { - "Type": "Task", - "Resource": "arn:aws:lambda:us-west-2:123456789012:function:prepareForDelivery", - "Next": "StartDelivery" - }, - "StartDelivery": { - "Type": "Task", - "Resource": "arn:aws:lambda:us-west-2:123456789012:function:startDelivery", - "End": true - } - } - }, - "ResultPath": "$.detail.processedItems", - "Next": "SendOrderSummary" - }, - "SendOrderSummary": { - "Type": "Task", - "InputPath": "$.detail.processedItems", - "Resource": "arn:aws:lambda:us-west-2:123456789012:function:sendOrderSummary", - "ResultPath": "$.detail.summary", - "End": true - } - } - } - RoleArn: arn:aws:iam::111122223333:role/service-role/StatesExecutionRole-us-east-1 diff --git a/test/unit/rules/resources/stepfunctions/test_state_machine.py b/test/unit/rules/resources/stepfunctions/test_state_machine.py deleted file mode 100644 index 0c9db8dd24..0000000000 --- a/test/unit/rules/resources/stepfunctions/test_state_machine.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -SPDX-License-Identifier: MIT-0 -""" - -from test.unit.rules import BaseRuleTestCase - -from cfnlint.rules.resources.stepfunctions.StateMachine import ( - StateMachine, # pylint: disable=E0401 -) - - -class TestStateMachine(BaseRuleTestCase): - """Test StateMachine for Step Functions""" - - def setUp(self): - """Setup""" - super(TestStateMachine, self).setUp() - self.collection.register(StateMachine()) - self.success_templates = [ - "test/fixtures/templates/good/resources/stepfunctions/state_machine.yaml" - ] - - def test_file_positive(self): - """Test Positive""" - self.helper_file_positive() - - def test_file_negative_alias(self): - """Test failure""" - self.helper_file_negative( - "test/fixtures/templates/bad/resources/stepfunctions/state_machine.yaml", 5 - ) diff --git a/test/unit/rules/resources/stepfunctions/test_state_machine_definition.py b/test/unit/rules/resources/stepfunctions/test_state_machine_definition.py new file mode 100644 index 0000000000..c90525cd32 --- /dev/null +++ b/test/unit/rules/resources/stepfunctions/test_state_machine_definition.py @@ -0,0 +1,388 @@ +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" + +from collections import deque + +import pytest + +from cfnlint.jsonschema.validators import ValidationError +from cfnlint.rules.resources.stepfunctions.StateMachineDefinition import ( + StateMachineDefinition, +) + + +@pytest.fixture(scope="module") +def rule(): + rule = StateMachineDefinition() + yield rule + + +# These state machines for testing have long lines +# with lots of indentation +# ruff: noqa: E501 + + +@pytest.mark.parametrize( + "name,instance,expected", + [ + ( + "Valid string json", + { + "Comment": ( + "An example of the Amazon States " + "Language for notification on an " + "AWS Batch job completion" + ), + "StartAt": "Submit Batch Job", + "TimeoutSeconds": 3600, + "States": { + "Submit Batch Job": { + "Type": "Task", + "Resource": "arn:aws:states:::batch:submitJob.sync", + "Parameters": { + "JobName": "BatchJobNotification", + "JobQueue": "arn:aws:batch:us-east-1:123456789012:job-queue/BatchJobQueue-7049d367474b4dd", + "JobDefinition": "arn:aws:batch:us-east-1:123456789012:job-definition/BatchJobDefinition-74d55ec34c4643c:1", + }, + "Next": "Notify Success", + "Catch": [ + {"ErrorEquals": ["States.ALL"], "Next": "Notify Failure"} + ], + }, + "Notify Success": { + "Type": "Task", + "Resource": "arn:aws:states:::sns:publish", + "Parameters": { + "Message": "Batch job submitted through Step Functions succeeded", + "TopicArn": "arn:aws:sns:us-east-1:123456789012:batchjobnotificatiointemplate-SNSTopic-1J757CVBQ2KHM", + }, + "End": True, + }, + "Notify Failure": { + "Type": "Task", + "Resource": "arn:aws:states:::sns:publish", + "Parameters": { + "Message": "Batch job submitted through Step Functions failed", + "TopicArn": "arn:aws:sns:us-east-1:123456789012:batchjobnotificatiointemplate-SNSTopic-1J757CVBQ2KHM", + }, + "End": True, + }, + }, + }, + [], + ), + ( + "Fan out example", + { + "Comment": "An example of the Amazon States Language for fanning out AWS Batch job", + "StartAt": "Generate batch job input", + "TimeoutSeconds": 3600, + "States": { + "Generate batch job input": { + "Type": "Task", + "Resource": "arn:aws:states:::lambda:invoke", + "OutputPath": "$.Payload", + "Parameters": { + "FunctionName": "" + }, + "Next": "Fan out batch jobs", + }, + "Fan out batch jobs": { + "Comment": "Start multiple executions of batch job depending on pre-processed data", + "Type": "Map", + "End": True, + "ItemsPath": "$", + "Parameters": {"BatchNumber.$": "$$.Map.Item.Value"}, + "Iterator": { + "StartAt": "Submit Batch Job", + "States": { + "Submit Batch Job": { + "Type": "Task", + "Resource": "arn:aws:states:::batch:submitJob.sync", + "Parameters": { + "JobName": "BatchJobFanOut", + "JobQueue": "", + "JobDefinition": "", + }, + "End": True, + } + }, + }, + }, + }, + }, + [], + ), + ( + "Error handling", + """ + { + "Comment": "An example of the Amazon States Language for notification on an AWS Batch job completion", + "StartAt": "Submit Batch Job", + "TimeoutSeconds": 3600, + "States": { + "Submit Batch Job": { + "Type": "Task", + "Resource": "arn:aws:states:::batch:submitJob.sync", + "Parameters": { + "JobName": "BatchJobNotification", + "JobQueue": "arn:aws:batch:us-west-2:123456789012:job-queue/BatchJobQueue-123456789abcdef", + "JobDefinition": "arn:aws:batch:us-west-2:123456789012:job-definition/BatchJobDefinition-123456789abcdef:1", + }, + "Next": "Notify Success", + "Retry": [ + { + "ErrorEquals": ["States.ALL"], + "IntervalSeconds": 30, + "MaxAttempts": 2, + "BackoffRate": 1.5, + } + ], + "Catch": [ + {"ErrorEquals": ["States.ALL"], "Next": "Notify Failure"} + ], + }, + "Notify Success": { + "Type": "Task", + "Resource": "arn:aws:states:::sns:publish", + "Parameters": { + "Message": "Batch job submitted through Step Functions succeeded", + "TopicArn": "arn:aws:sns:us-west-2:123456789012:StepFunctionsSample-BatchJobManagement12345678-9abc-def0-1234-567890abcdef-SNSTopic-A2B3C4D5E6F7G", + }, + "End": True, + }, + "Notify Failure": { + "Type": "Task", + "Resource": "arn:aws:states:::sns:publish", + "Parameters": { + "Message": "Batch job submitted through Step Functions failed", + "TopicArn": "arn:aws:sns:us-west-2:123456789012:StepFunctionsSample-BatchJobManagement12345678-9abc-def0-1234-567890abcdef-SNSTopic-A2B3C4D5E6F7G", + }, + "End": True, + }, + }, + } + """, + [], + ), + ( + "Transfer data records", + { + "Comment": "An example of the Amazon States Language for reading messages from a DynamoDB table and sending them to SQS", + "StartAt": "Seed the DynamoDB Table", + "TimeoutSeconds": 3600, + "States": { + "Seed the DynamoDB Table": { + "Type": "Task", + "Resource": "arn:aws:lambda:us-east-1:123456789012:function:sqsconnector-SeedingFunction-T3U43VYDU5OQ", + "ResultPath": "$.List", + "Next": "For Loop Condition", + }, + "For Loop Condition": { + "Type": "Choice", + "Choices": [ + { + "Not": { + "Variable": "$.List[0]", + "StringEquals": "DONE", + }, + "Next": "Read Next Message from DynamoDB", + } + ], + "Default": "Succeed", + }, + "Read Next Message from DynamoDB": { + "Type": "Task", + "Resource": "arn:aws:states:::dynamodb:getItem", + "Parameters": { + "TableName": "sqsconnector-DDBTable-1CAFOJWP8QD6I", + "Key": {"MessageId": {"S.$": "$.List[0]"}}, + }, + "ResultPath": "$.DynamoDB", + "Next": "Send Message to SQS", + }, + "Send Message to SQS": { + "Type": "Task", + "Resource": "arn:aws:states:::sqs:sendMessage", + "Parameters": { + "MessageBody.$": "$.DynamoDB.Item.Message.S", + "QueueUrl": "https://sqs.us-east-1.amazonaws.com/123456789012/sqsconnector-SQSQueue-QVGQBW134PWK", + }, + "ResultPath": "$.SQS", + "Next": "Pop Element from List", + }, + "Pop Element from List": { + "Type": "Pass", + "Parameters": {"List.$": "$.List[1:]"}, + "Next": "For Loop Condition", + }, + "Succeed": {"Type": "Succeed"}, + }, + }, + [], + ), + ( + "A map", + { + "Comment": "An example of the Amazon States Language for reading messages from an SQS queue and iteratively processing each message.", + "StartAt": "Read messages from SQS Queue", + "States": { + "Read messages from SQS Queue": { + "Type": "Task", + "Resource": "arn:aws:states:::lambda:invoke", + "OutputPath": "$.Payload", + "Parameters": { + "FunctionName": "MapSampleProj-ReadFromSQSQueueLambda-1MY3M63RMJVA9" + }, + "Next": "Are there messages to process?", + }, + "Are there messages to process?": { + "Type": "Choice", + "Choices": [ + { + "Variable": "$", + "StringEquals": "No messages", + "Next": "Finish", + } + ], + "Default": "Process messages", + }, + "Process messages": { + "Type": "Map", + "Next": "Finish", + "ItemsPath": "$", + "Parameters": { + "MessageNumber.$": "$$.Map.Item.Index", + "MessageDetails.$": "$$.Map.Item.Value", + }, + "Iterator": { + "StartAt": "Write message to DynamoDB", + "States": { + "Write message to DynamoDB": { + "Type": "Task", + "Resource": "arn:aws:states:::dynamodb:putItem", + "ResultPath": None, + "Parameters": { + "TableName": "MapSampleProj-DDBTable-YJDJ1MKIN6C5", + "ReturnConsumedCapacity": "TOTAL", + "Item": { + "MessageId": { + "S.$": "$.MessageDetails.MessageId" + }, + "Body": {"S.$": "$.MessageDetails.Body"}, + }, + }, + "Next": "Remove message from SQS queue", + }, + "Remove message from SQS queue": { + "Type": "Task", + "Resource": "arn:aws:states:::lambda:invoke", + "InputPath": "$.MessageDetails", + "ResultPath": None, + "Parameters": { + "FunctionName": "MapSampleProj-DeleteFromSQSQueueLambda-198J2839ZO5K2", + "Payload": { + "ReceiptHandle.$": "$.ReceiptHandle" + }, + }, + "Next": "Publish message to SNS topic", + }, + "Publish message to SNS topic": { + "Type": "Task", + "Resource": "arn:aws:states:::sns:publish", + "InputPath": "$.MessageDetails", + "Parameters": { + "Subject": "Message from Step Functions!", + "Message.$": "$.Body", + "TopicArn": "arn:aws:sns:us-east-1:012345678910:MapSampleProj-SNSTopic-1CQO4HQ3IR1KN", + }, + "End": True, + }, + }, + }, + }, + "Finish": {"Type": "Succeed"}, + }, + }, + [], + ), + ( + "Invalid configuration", + { + "StartAt": "SampleChoice", + "States": { + "NoType": {}, + "SampleChoice": { + "Default": "__SucceedEntryPoint__", + "Type": "Choice", + }, + "__SucceedEntryPoint__": {"Type": "Pass"}, + }, + }, + [ + ValidationError( + "'Type' is a required property", + rule=StateMachineDefinition(), + validator="required", + schema_path=deque( + [ + "properties", + "States", + "patternProperties", + "^.{1,128}$", + "required", + ] + ), + path=deque(["States", "NoType"]), + ), + ValidationError( + "'Choices' is a required property", + rule=StateMachineDefinition(), + validator="required", + schema_path=deque( + [ + "properties", + "States", + "patternProperties", + "^.{1,128}$", + "allOf", + 0, + "then", + "required", + ] + ), + path=deque(["States", "SampleChoice"]), + ), + ValidationError( + "Only one of ['Next', 'End'] is a required property", + rule=StateMachineDefinition(), + validator="requiredXor", + schema_path=deque( + [ + "properties", + "States", + "patternProperties", + "^.{1,128}$", + "allOf", + 3, + "then", + "requiredXor", + ] + ), + path=deque(["States", "__SucceedEntryPoint__"]), + ), + ], + ), + ], +) +def test_validate( + name, + instance, + expected, + rule, + validator, +): + errs = list(rule.validate(validator, {}, instance, {})) + assert errs == expected, f"{name!r} test failed with {errs!r}"