Repository: getmoto/moto

Issue:
Stepfunction describe executions returns a double encoded string
## Issue

When running describe_execution as follows, the result is a string not the expected dictionary

``` python
@mock_aws
def test_a(self):
    client = boto3.client("stepfunctions")
    sf = client.create_state_machine(
        name="name",
        definition="definition",
        roleArn=f"arn:aws:iam::{DEFAULT_ACCOUNT_ID}:role/unknown_sf_role",
    )
    step_function_arn = sf["stateMachineArn"]
    client.start_execution(stateMachineArn=step_function_arn, input=json.dumps({"test": "content"}))
    executions = client.list_executions(
        stateMachineArn=step_function_arn,
        statusFilter="RUNNING",
        maxResults=100,
    )
    for execution in executions["executions"]:
        execution_input = json.loads(client.describe_execution(executionArn=execution["executionArn"])["input"])
        assert execution_input == {"test": "content"}
```

`execution_input` would be equal to `'{"test": "content"}'` not the expected `{"test": "content"}`

### Versions affected

Only `5.0.4` when using python mocks



Discussion:
@lscheilling I took a look at the documentation and `input` should be a `str`, not a `dict` -  so the moto implementation is correct.

https://docs.aws.amazon.com/step-functions/latest/apireference/API_DescribeExecution.html
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/stepfunctions/client/describe_execution.html
> @lscheilling I took a look at the documentation and `input` should be a `str`, not a `dict` - so the moto implementation is correct.
> 
> https://docs.aws.amazon.com/step-functions/latest/apireference/API_DescribeExecution.html https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/stepfunctions/client/describe_execution.html

This was not my issue with this, the output was always a string, but the output was after being run through `json.loads` was still a string, so we would be getting `'"{"test": "content"}"'` for input instead of  `'{"test": "content"}'`

If you switch between moto versions `5.0.3` and `5.0.4` on this test case you should be able to see the differing behaviours
Hi @lscheilling, thanks for raising this. We introduced a new option to execute state machines in 5.0.4, and it looks like that indeed broke something in the existing implementation.

I'll open a PR with a fix now.

Resolving patch:
diff --git a/moto/stepfunctions/models.py b/moto/stepfunctions/models.py
--- a/moto/stepfunctions/models.py
+++ b/moto/stepfunctions/models.py
@@ -60,7 +60,7 @@ def start_execution(
             state_machine_name=self.name,
             execution_name=execution_name,
             state_machine_arn=self.arn,
-            execution_input=execution_input,
+            execution_input=json.loads(execution_input),
         )
         self.executions.append(execution)
         return execution


Test patch:
diff --git a/tests/test_stepfunctions/parser/__init__.py b/tests/test_stepfunctions/parser/__init__.py
--- a/tests/test_stepfunctions/parser/__init__.py
+++ b/tests/test_stepfunctions/parser/__init__.py
@@ -94,15 +94,20 @@ def verify_execution_result(
 
 
 def _start_execution(client, definition, exec_input, sfn_role):
-    name = "sfn_name"
+    name = f"sfn_name_{str(uuid4())[0:6]}"
     #
     response = client.create_state_machine(
         name=name, definition=json.dumps(definition), roleArn=sfn_role
     )
     state_machine_arn = response["stateMachineArn"]
-    execution = client.start_execution(
-        name="exec1", stateMachineArn=state_machine_arn, input=exec_input or "{}"
-    )
+    if exec_input:
+        execution = client.start_execution(
+            name="exec1", stateMachineArn=state_machine_arn, input=exec_input or "{}"
+        )
+    else:
+        execution = client.start_execution(
+            name="exec1", stateMachineArn=state_machine_arn
+        )
     execution_arn = execution["executionArn"]
     return execution_arn, state_machine_arn
 
diff --git a/tests/test_stepfunctions/parser/templates/failure.json b/tests/test_stepfunctions/parser/templates/failure.json
new file mode 100644
--- /dev/null
+++ b/tests/test_stepfunctions/parser/templates/failure.json
@@ -0,0 +1,11 @@
+{
+    "Comment": "An example of the Amazon States Language using a choice state.",
+    "StartAt": "DefaultState",
+    "States": {
+        "DefaultState": {
+            "Type": "Fail",
+            "Error": "DefaultStateError",
+            "Cause": "No Matches!"
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/test_stepfunctions/parser/test_stepfunctions.py b/tests/test_stepfunctions/parser/test_stepfunctions.py
--- a/tests/test_stepfunctions/parser/test_stepfunctions.py
+++ b/tests/test_stepfunctions/parser/test_stepfunctions.py
@@ -1,72 +1,59 @@
 import json
-from time import sleep
 from unittest import SkipTest
 
-import boto3
+import pytest
 
-from moto import mock_aws, settings
-from tests import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
+from moto import settings
 
-failure_definition = {
-    "Comment": "An example of the Amazon States Language using a choice state.",
-    "StartAt": "DefaultState",
-    "States": {
-        "DefaultState": {
-            "Type": "Fail",
-            "Error": "DefaultStateError",
-            "Cause": "No Matches!",
-        }
-    },
-}
+from . import aws_verified, verify_execution_result
 
 
-@mock_aws(config={"stepfunctions": {"execute_state_machine": True}})
+@aws_verified
+@pytest.mark.aws_verified
 def test_state_machine_with_simple_failure_state():
     if settings.TEST_SERVER_MODE:
         raise SkipTest("Don't need to test this in ServerMode")
-    client = boto3.client("stepfunctions", region_name="us-east-1")
-    execution_arn, state_machine_arn = _start_execution(client, failure_definition)
 
-    for _ in range(5):
-        execution = client.describe_execution(executionArn=execution_arn)
-        if execution["status"] == "FAILED":
-            assert "stopDate" in execution
-            assert execution["error"] == "DefaultStateError"
-            assert execution["cause"] == "No Matches!"
-
-            history = client.get_execution_history(executionArn=execution_arn)
-            assert len(history["events"]) == 3
-            assert [e["type"] for e in history["events"]] == [
-                "ExecutionStarted",
-                "FailStateEntered",
-                "ExecutionFailed",
-            ]
-
-            executions = client.list_executions(stateMachineArn=state_machine_arn)[
-                "executions"
-            ]
-            assert len(executions) == 1
-            assert executions[0]["executionArn"] == execution_arn
-            assert executions[0]["stateMachineArn"] == state_machine_arn
-            assert executions[0]["status"] == "FAILED"
+    def _verify_result(client, execution, execution_arn):
+        assert "stopDate" in execution
+        assert execution["error"] == "DefaultStateError"
+        assert execution["cause"] == "No Matches!"
+        assert execution["input"] == "{}"
+
+        history = client.get_execution_history(executionArn=execution_arn)
+        assert len(history["events"]) == 3
+        assert [e["type"] for e in history["events"]] == [
+            "ExecutionStarted",
+            "FailStateEntered",
+            "ExecutionFailed",
+        ]
+
+        state_machine_arn = execution["stateMachineArn"]
+        executions = client.list_executions(stateMachineArn=state_machine_arn)[
+            "executions"
+        ]
+        assert len(executions) == 1
+        assert executions[0]["executionArn"] == execution_arn
+        assert executions[0]["stateMachineArn"] == state_machine_arn
+        assert executions[0]["status"] == "FAILED"
+
+    verify_execution_result(_verify_result, "FAILED", "failure")
+
+
+@aws_verified
+@pytest.mark.aws_verified
+def test_state_machine_with_input():
+    if settings.TEST_SERVER_MODE:
+        raise SkipTest("Don't need to test this in ServerMode")
 
-            break
-        sleep(0.2)
-    else:
-        assert False, "Should have failed already"
+    _input = {"my": "data"}
 
+    def _verify_result(client, execution, execution_arn):
+        assert execution["input"] == json.dumps(_input)
 
-def _start_execution(client, definition):
-    name = "sfn_name"
-    #
-    response = client.create_state_machine(
-        name=name, definition=json.dumps(definition), roleArn=_get_default_role()
+    verify_execution_result(
+        _verify_result,
+        "FAILED",
+        "failure",
+        exec_input=json.dumps(_input),
     )
-    state_machine_arn = response["stateMachineArn"]
-    execution = client.start_execution(name="exec1", stateMachineArn=state_machine_arn)
-    execution_arn = execution["executionArn"]
-    return execution_arn, state_machine_arn
-
-
-def _get_default_role():
-    return "arn:aws:iam::" + ACCOUNT_ID + ":role/unknown_sf_role"
diff --git a/tests/test_stepfunctions/test_stepfunctions.py b/tests/test_stepfunctions/test_stepfunctions.py
--- a/tests/test_stepfunctions/test_stepfunctions.py
+++ b/tests/test_stepfunctions/test_stepfunctions.py
@@ -544,9 +544,9 @@ def test_state_machine_start_execution_with_invalid_input():
         name="name", definition=str(simple_definition), roleArn=_get_default_role()
     )
     with pytest.raises(ClientError):
-        _ = client.start_execution(stateMachineArn=sm["stateMachineArn"], input="")
+        client.start_execution(stateMachineArn=sm["stateMachineArn"], input="")
     with pytest.raises(ClientError):
-        _ = client.start_execution(stateMachineArn=sm["stateMachineArn"], input="{")
+        client.start_execution(stateMachineArn=sm["stateMachineArn"], input="{")
 
 
 @mock_aws
@@ -657,7 +657,7 @@ def test_state_machine_describe_execution_with_no_input():
     #
     assert description["ResponseMetadata"]["HTTPStatusCode"] == 200
     assert description["executionArn"] == execution["executionArn"]
-    assert description["input"] == '"{}"'
+    assert description["input"] == "{}"
     assert re.match("[-0-9a-z]+", description["name"])
     assert description["startDate"] == execution["startDate"]
     assert description["stateMachineArn"] == sm["stateMachineArn"]
@@ -680,7 +680,7 @@ def test_state_machine_describe_execution_with_custom_input():
     #
     assert description["ResponseMetadata"]["HTTPStatusCode"] == 200
     assert description["executionArn"] == execution["executionArn"]
-    assert json.loads(description["input"]) == execution_input
+    assert description["input"] == execution_input
     assert re.match("[-a-z0-9]+", description["name"])
     assert description["startDate"] == execution["startDate"]
     assert description["stateMachineArn"] == sm["stateMachineArn"]