localstack-core 4.10.1.dev7__py3-none-any.whl → 4.11.2.dev14__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of localstack-core might be problematic. Click here for more details.

Files changed (152) hide show
  1. localstack/aws/api/acm/__init__.py +122 -122
  2. localstack/aws/api/apigateway/__init__.py +604 -561
  3. localstack/aws/api/cloudcontrol/__init__.py +63 -63
  4. localstack/aws/api/cloudformation/__init__.py +1201 -969
  5. localstack/aws/api/cloudwatch/__init__.py +375 -375
  6. localstack/aws/api/config/__init__.py +784 -786
  7. localstack/aws/api/dynamodb/__init__.py +753 -759
  8. localstack/aws/api/dynamodbstreams/__init__.py +74 -74
  9. localstack/aws/api/ec2/__init__.py +10062 -8826
  10. localstack/aws/api/es/__init__.py +453 -453
  11. localstack/aws/api/events/__init__.py +552 -552
  12. localstack/aws/api/firehose/__init__.py +541 -543
  13. localstack/aws/api/iam/__init__.py +866 -572
  14. localstack/aws/api/kinesis/__init__.py +235 -147
  15. localstack/aws/api/kms/__init__.py +341 -336
  16. localstack/aws/api/lambda_/__init__.py +974 -621
  17. localstack/aws/api/logs/__init__.py +988 -675
  18. localstack/aws/api/opensearch/__init__.py +903 -785
  19. localstack/aws/api/pipes/__init__.py +336 -336
  20. localstack/aws/api/redshift/__init__.py +1257 -1166
  21. localstack/aws/api/resource_groups/__init__.py +175 -175
  22. localstack/aws/api/resourcegroupstaggingapi/__init__.py +103 -67
  23. localstack/aws/api/route53/__init__.py +296 -254
  24. localstack/aws/api/route53resolver/__init__.py +397 -396
  25. localstack/aws/api/s3/__init__.py +1412 -1349
  26. localstack/aws/api/s3control/__init__.py +594 -594
  27. localstack/aws/api/scheduler/__init__.py +118 -118
  28. localstack/aws/api/secretsmanager/__init__.py +221 -216
  29. localstack/aws/api/ses/__init__.py +227 -227
  30. localstack/aws/api/sns/__init__.py +115 -115
  31. localstack/aws/api/sqs/__init__.py +100 -100
  32. localstack/aws/api/ssm/__init__.py +1977 -1971
  33. localstack/aws/api/stepfunctions/__init__.py +375 -333
  34. localstack/aws/api/sts/__init__.py +142 -66
  35. localstack/aws/api/support/__init__.py +112 -112
  36. localstack/aws/api/swf/__init__.py +378 -386
  37. localstack/aws/api/transcribe/__init__.py +425 -425
  38. localstack/aws/handlers/logging.py +8 -4
  39. localstack/aws/handlers/service.py +22 -3
  40. localstack/aws/protocol/parser.py +1 -1
  41. localstack/aws/protocol/serializer.py +1 -1
  42. localstack/aws/scaffold.py +15 -17
  43. localstack/cli/localstack.py +6 -1
  44. localstack/deprecations.py +0 -6
  45. localstack/dev/kubernetes/__main__.py +38 -3
  46. localstack/services/acm/provider.py +4 -0
  47. localstack/services/apigateway/helpers.py +5 -9
  48. localstack/services/apigateway/legacy/provider.py +60 -24
  49. localstack/services/apigateway/patches.py +0 -9
  50. localstack/services/cloudformation/engine/template_preparer.py +6 -2
  51. localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +12 -0
  52. localstack/services/cloudformation/provider.py +2 -2
  53. localstack/services/cloudformation/v2/provider.py +6 -6
  54. localstack/services/cloudwatch/provider.py +10 -3
  55. localstack/services/cloudwatch/provider_v2.py +6 -3
  56. localstack/services/configservice/provider.py +5 -1
  57. localstack/services/dynamodb/provider.py +1 -0
  58. localstack/services/dynamodb/v2/provider.py +1 -0
  59. localstack/services/dynamodbstreams/provider.py +6 -0
  60. localstack/services/dynamodbstreams/v2/provider.py +6 -0
  61. localstack/services/ec2/provider.py +6 -0
  62. localstack/services/es/provider.py +6 -0
  63. localstack/services/events/provider.py +4 -0
  64. localstack/services/events/v1/provider.py +9 -0
  65. localstack/services/firehose/provider.py +5 -0
  66. localstack/services/iam/provider.py +4 -0
  67. localstack/services/kinesis/packages.py +1 -1
  68. localstack/services/kms/models.py +44 -24
  69. localstack/services/kms/provider.py +97 -16
  70. localstack/services/lambda_/api_utils.py +40 -21
  71. localstack/services/lambda_/event_source_mapping/pollers/stream_poller.py +1 -1
  72. localstack/services/lambda_/invocation/assignment.py +4 -1
  73. localstack/services/lambda_/invocation/execution_environment.py +21 -2
  74. localstack/services/lambda_/invocation/lambda_models.py +27 -2
  75. localstack/services/lambda_/invocation/lambda_service.py +51 -3
  76. localstack/services/lambda_/invocation/models.py +9 -1
  77. localstack/services/lambda_/invocation/version_manager.py +18 -3
  78. localstack/services/lambda_/packages.py +1 -1
  79. localstack/services/lambda_/provider.py +240 -96
  80. localstack/services/lambda_/resource_providers/aws_lambda_function.py +33 -1
  81. localstack/services/lambda_/runtimes.py +10 -3
  82. localstack/services/logs/provider.py +45 -19
  83. localstack/services/opensearch/provider.py +53 -3
  84. localstack/services/resource_groups/provider.py +5 -1
  85. localstack/services/resourcegroupstaggingapi/provider.py +6 -1
  86. localstack/services/s3/provider.py +29 -16
  87. localstack/services/s3/utils.py +35 -14
  88. localstack/services/s3control/provider.py +101 -2
  89. localstack/services/s3control/validation.py +50 -0
  90. localstack/services/sns/constants.py +3 -1
  91. localstack/services/sns/publisher.py +15 -6
  92. localstack/services/sns/v2/models.py +30 -1
  93. localstack/services/sns/v2/provider.py +794 -31
  94. localstack/services/sns/v2/utils.py +20 -0
  95. localstack/services/sqs/models.py +37 -10
  96. localstack/services/stepfunctions/asl/component/common/path/result_path.py +1 -1
  97. localstack/services/stepfunctions/asl/component/state/state_execution/execute_state.py +0 -1
  98. localstack/services/stepfunctions/asl/component/state/state_execution/state_map/state_map.py +0 -1
  99. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/lambda_eval_utils.py +8 -8
  100. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/{mock_eval_utils.py → local_mock_eval_utils.py} +13 -9
  101. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service.py +6 -6
  102. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_callback.py +1 -1
  103. localstack/services/stepfunctions/asl/component/state/state_fail/state_fail.py +4 -0
  104. localstack/services/stepfunctions/asl/component/test_state/state/base_mock.py +118 -0
  105. localstack/services/stepfunctions/asl/component/test_state/state/common.py +82 -0
  106. localstack/services/stepfunctions/asl/component/test_state/state/execution.py +139 -0
  107. localstack/services/stepfunctions/asl/component/test_state/state/map.py +77 -0
  108. localstack/services/stepfunctions/asl/component/test_state/state/task.py +44 -0
  109. localstack/services/stepfunctions/asl/eval/environment.py +30 -22
  110. localstack/services/stepfunctions/asl/eval/states.py +1 -1
  111. localstack/services/stepfunctions/asl/eval/test_state/environment.py +49 -9
  112. localstack/services/stepfunctions/asl/eval/test_state/program_state.py +22 -0
  113. localstack/services/stepfunctions/asl/jsonata/jsonata.py +5 -1
  114. localstack/services/stepfunctions/asl/parse/preprocessor.py +67 -24
  115. localstack/services/stepfunctions/asl/parse/test_state/asl_parser.py +5 -4
  116. localstack/services/stepfunctions/asl/parse/test_state/preprocessor.py +222 -31
  117. localstack/services/stepfunctions/asl/static_analyser/test_state/test_state_analyser.py +170 -22
  118. localstack/services/stepfunctions/backend/execution.py +6 -6
  119. localstack/services/stepfunctions/backend/execution_worker.py +5 -5
  120. localstack/services/stepfunctions/backend/test_state/execution.py +36 -0
  121. localstack/services/stepfunctions/backend/test_state/execution_worker.py +33 -1
  122. localstack/services/stepfunctions/backend/test_state/test_state_mock.py +127 -0
  123. localstack/services/stepfunctions/local_mocking/__init__.py +9 -0
  124. localstack/services/stepfunctions/{mocking → local_mocking}/mock_config.py +24 -17
  125. localstack/services/stepfunctions/provider.py +78 -27
  126. localstack/services/stepfunctions/test_state/mock_config.py +47 -0
  127. localstack/testing/pytest/fixtures.py +28 -0
  128. localstack/testing/snapshots/transformer_utility.py +7 -0
  129. localstack/testing/testselection/matching.py +0 -1
  130. localstack/utils/analytics/publisher.py +37 -155
  131. localstack/utils/analytics/service_request_aggregator.py +6 -4
  132. localstack/utils/aws/arns.py +7 -0
  133. localstack/utils/aws/client_types.py +0 -8
  134. localstack/utils/batching.py +258 -0
  135. localstack/utils/catalog/catalog_loader.py +111 -3
  136. localstack/utils/collections.py +23 -11
  137. localstack/utils/crypto.py +109 -0
  138. localstack/version.py +2 -2
  139. {localstack_core-4.10.1.dev7.dist-info → localstack_core-4.11.2.dev14.dist-info}/METADATA +7 -6
  140. {localstack_core-4.10.1.dev7.dist-info → localstack_core-4.11.2.dev14.dist-info}/RECORD +149 -141
  141. localstack_core-4.11.2.dev14.dist-info/plux.json +1 -0
  142. localstack/services/stepfunctions/mocking/__init__.py +0 -0
  143. localstack/utils/batch_policy.py +0 -124
  144. localstack_core-4.10.1.dev7.dist-info/plux.json +0 -1
  145. /localstack/services/stepfunctions/{mocking → local_mocking}/mock_config_file.py +0 -0
  146. {localstack_core-4.10.1.dev7.data → localstack_core-4.11.2.dev14.data}/scripts/localstack +0 -0
  147. {localstack_core-4.10.1.dev7.data → localstack_core-4.11.2.dev14.data}/scripts/localstack-supervisor +0 -0
  148. {localstack_core-4.10.1.dev7.data → localstack_core-4.11.2.dev14.data}/scripts/localstack.bat +0 -0
  149. {localstack_core-4.10.1.dev7.dist-info → localstack_core-4.11.2.dev14.dist-info}/WHEEL +0 -0
  150. {localstack_core-4.10.1.dev7.dist-info → localstack_core-4.11.2.dev14.dist-info}/entry_points.txt +0 -0
  151. {localstack_core-4.10.1.dev7.dist-info → localstack_core-4.11.2.dev14.dist-info}/licenses/LICENSE.txt +0 -0
  152. {localstack_core-4.10.1.dev7.dist-info → localstack_core-4.11.2.dev14.dist-info}/top_level.txt +0 -0
@@ -19,7 +19,9 @@ from localstack.services.stepfunctions.asl.eval.program_state import (
19
19
  ProgramState,
20
20
  )
21
21
  from localstack.services.stepfunctions.asl.eval.test_state.program_state import (
22
+ ProgramCaughtError,
22
23
  ProgramChoiceSelected,
24
+ ProgramRetriable,
23
25
  )
24
26
  from localstack.services.stepfunctions.asl.utils.encoding import to_json_str
25
27
  from localstack.services.stepfunctions.backend.activity import Activity
@@ -31,6 +33,7 @@ from localstack.services.stepfunctions.backend.state_machine import StateMachine
31
33
  from localstack.services.stepfunctions.backend.test_state.execution_worker import (
32
34
  TestStateExecutionWorker,
33
35
  )
36
+ from localstack.services.stepfunctions.backend.test_state.test_state_mock import TestStateMock
34
37
 
35
38
  LOG = logging.getLogger(__name__)
36
39
 
@@ -38,6 +41,8 @@ LOG = logging.getLogger(__name__)
38
41
  class TestStateExecution(Execution):
39
42
  exec_worker: TestStateExecutionWorker | None
40
43
  next_state: str | None
44
+ state_name: str | None
45
+ mock: TestStateMock | None
41
46
 
42
47
  class TestCaseExecutionWorkerCommunication(BaseExecutionWorkerCommunication):
43
48
  _execution: TestStateExecution
@@ -48,6 +53,16 @@ class TestStateExecution(Execution):
48
53
  self.execution.exec_status = ExecutionStatus.SUCCEEDED
49
54
  self.execution.output = self.execution.exec_worker.env.states.get_input()
50
55
  self.execution.next_state = exit_program_state.next_state_name
56
+ elif isinstance(exit_program_state, ProgramCaughtError):
57
+ self.execution.exec_status = ExecutionStatus.SUCCEEDED
58
+ self.execution.error = exit_program_state.error
59
+ self.execution.cause = exit_program_state.cause
60
+ self.execution.output = self.execution.exec_worker.env.states.get_input()
61
+ self.execution.next_state = exit_program_state.next_state_name
62
+ elif isinstance(exit_program_state, ProgramRetriable):
63
+ self.execution.exec_status = ExecutionStatus.SUCCEEDED
64
+ self.execution.error = exit_program_state.error
65
+ self.execution.cause = exit_program_state.cause
51
66
  else:
52
67
  self._reflect_execution_status()
53
68
 
@@ -61,7 +76,9 @@ class TestStateExecution(Execution):
61
76
  state_machine: StateMachineInstance,
62
77
  start_date: Timestamp,
63
78
  activity_store: dict[Arn, Activity],
79
+ state_name: str | None = None,
64
80
  input_data: dict | None = None,
81
+ mock: TestStateMock | None = None,
65
82
  ):
66
83
  super().__init__(
67
84
  name=name,
@@ -79,6 +96,8 @@ class TestStateExecution(Execution):
79
96
  )
80
97
  self._execution_terminated_event = threading.Event()
81
98
  self.next_state = None
99
+ self.state_name = state_name
100
+ self.mock = mock
82
101
 
83
102
  def _get_start_execution_worker_comm(self) -> BaseExecutionWorkerCommunication:
84
103
  return self.TestCaseExecutionWorkerCommunication(self)
@@ -93,6 +112,8 @@ class TestStateExecution(Execution):
93
112
  exec_comm=self._get_start_execution_worker_comm(),
94
113
  cloud_watch_logging_session=self._cloud_watch_logging_session,
95
114
  activity_store=self._activity_store,
115
+ state_name=self.state_name,
116
+ mock=self.mock,
96
117
  )
97
118
 
98
119
  def publish_execution_status_change_event(self):
@@ -117,6 +138,21 @@ class TestStateExecution(Execution):
117
138
  test_state_output = TestStateOutput(
118
139
  status=TestExecutionStatus.SUCCEEDED, nextState=self.next_state, output=output_str
119
140
  )
141
+ elif isinstance(exit_program_state, ProgramCaughtError):
142
+ output_str = to_json_str(self.output)
143
+ test_state_output = TestStateOutput(
144
+ status=TestExecutionStatus.CAUGHT_ERROR,
145
+ nextState=self.next_state,
146
+ output=output_str,
147
+ error=exit_program_state.error,
148
+ cause=exit_program_state.cause,
149
+ )
150
+ elif isinstance(exit_program_state, ProgramRetriable):
151
+ test_state_output = TestStateOutput(
152
+ status=TestExecutionStatus.RETRIABLE,
153
+ error=exit_program_state.error,
154
+ cause=exit_program_state.cause,
155
+ )
120
156
  else:
121
157
  # TODO: handle other statuses
122
158
  LOG.warning(
@@ -1,8 +1,13 @@
1
+ from localstack.aws.api.stepfunctions import Arn, StateName
1
2
  from localstack.services.stepfunctions.asl.component.eval_component import EvalComponent
2
3
  from localstack.services.stepfunctions.asl.eval.environment import Environment
4
+ from localstack.services.stepfunctions.asl.eval.evaluation_details import EvaluationDetails
3
5
  from localstack.services.stepfunctions.asl.eval.event.event_manager import (
4
6
  EventHistoryContext,
5
7
  )
8
+ from localstack.services.stepfunctions.asl.eval.event.logging import (
9
+ CloudWatchLoggingSession,
10
+ )
6
11
  from localstack.services.stepfunctions.asl.eval.states import (
7
12
  ContextObjectData,
8
13
  ExecutionData,
@@ -12,15 +17,41 @@ from localstack.services.stepfunctions.asl.eval.test_state.environment import Te
12
17
  from localstack.services.stepfunctions.asl.parse.test_state.asl_parser import (
13
18
  TestStateAmazonStateLanguageParser,
14
19
  )
20
+ from localstack.services.stepfunctions.backend.activity import Activity
15
21
  from localstack.services.stepfunctions.backend.execution_worker import SyncExecutionWorker
22
+ from localstack.services.stepfunctions.backend.execution_worker_comm import (
23
+ ExecutionWorkerCommunication,
24
+ )
25
+ from localstack.services.stepfunctions.backend.test_state.test_state_mock import TestStateMock
16
26
 
17
27
 
18
28
  class TestStateExecutionWorker(SyncExecutionWorker):
19
29
  env: TestStateEnvironment | None
30
+ state_name: str | None = None
31
+ mock: TestStateMock | None
32
+
33
+ def __init__(
34
+ self,
35
+ evaluation_details: EvaluationDetails,
36
+ exec_comm: ExecutionWorkerCommunication,
37
+ cloud_watch_logging_session: CloudWatchLoggingSession | None,
38
+ activity_store: dict[Arn, Activity],
39
+ state_name: StateName | None = None,
40
+ mock: TestStateMock | None = None,
41
+ ):
42
+ super().__init__(
43
+ evaluation_details,
44
+ exec_comm,
45
+ cloud_watch_logging_session,
46
+ activity_store,
47
+ local_mock_test_case=None, # local mock is only applicable to SFN Local, but not for TestState
48
+ )
49
+ self.state_name = state_name
50
+ self.mock = mock
20
51
 
21
52
  def _get_evaluation_entrypoint(self) -> EvalComponent:
22
53
  return TestStateAmazonStateLanguageParser.parse(
23
- self._evaluation_details.state_machine_details.definition
54
+ self._evaluation_details.state_machine_details.definition, self.state_name
24
55
  )[0]
25
56
 
26
57
  def _get_evaluation_environment(self) -> Environment:
@@ -43,4 +74,5 @@ class TestStateExecutionWorker(SyncExecutionWorker):
43
74
  event_history_context=EventHistoryContext.of_program_start(),
44
75
  cloud_watch_logging_session=self._cloud_watch_logging_session,
45
76
  activity_store=self._activity_store,
77
+ mock=self.mock,
46
78
  )
@@ -0,0 +1,127 @@
1
+ import copy
2
+ import json
3
+ from typing import Final
4
+
5
+ from pydantic import (
6
+ ValidationError,
7
+ )
8
+
9
+ from localstack.aws.api.stepfunctions import (
10
+ HistoryEventType,
11
+ MockInput,
12
+ TaskFailedEventDetails,
13
+ TestStateConfiguration,
14
+ )
15
+ from localstack.services.stepfunctions.asl.component.common.error_name.error_name import ErrorName
16
+ from localstack.services.stepfunctions.asl.component.common.error_name.failure_event import (
17
+ FailureEvent,
18
+ FailureEventException,
19
+ )
20
+ from localstack.services.stepfunctions.asl.component.state.state_type import StateType
21
+ from localstack.services.stepfunctions.asl.eval.environment import Environment
22
+ from localstack.services.stepfunctions.asl.eval.event.event_detail import EventDetails
23
+ from localstack.services.stepfunctions.asl.eval.states import (
24
+ ContextObjectData,
25
+ )
26
+ from localstack.services.stepfunctions.test_state.mock_config import (
27
+ TestStateContextObjectValidator,
28
+ TestStateMockedResponse,
29
+ TestStateResponseReturn,
30
+ TestStateResponseThrow,
31
+ )
32
+
33
+
34
+ def eval_mocked_response_throw(env: Environment, mocked_response: TestStateResponseThrow) -> None:
35
+ task_failed_event_details = TaskFailedEventDetails(
36
+ error=mocked_response.error, cause=mocked_response.cause
37
+ )
38
+ error_name = ErrorName(mocked_response.error)
39
+ failure_event = FailureEvent(
40
+ env=env,
41
+ error_name=error_name,
42
+ event_type=HistoryEventType.TaskFailed, # TODO(gregfurman): Should this be state specific?
43
+ event_details=EventDetails(taskFailedEventDetails=task_failed_event_details),
44
+ )
45
+ raise FailureEventException(failure_event=failure_event)
46
+
47
+
48
+ class TestStateMock:
49
+ _mock_input: MockInput | None
50
+ _state_configuration: TestStateConfiguration | None
51
+ _result_stack: Final[list[TestStateMockedResponse]]
52
+ _context: Final[ContextObjectData | None]
53
+
54
+ def __init__(
55
+ self,
56
+ mock_input: MockInput | None,
57
+ state_configuration: TestStateConfiguration | None,
58
+ context: str | None,
59
+ ):
60
+ self._mock_input = mock_input
61
+ self._state_configuration = state_configuration
62
+ self._result_stack = []
63
+ self._context = None
64
+
65
+ if not mock_input:
66
+ return
67
+
68
+ self._context = None if context is None else self.parse_context(context)
69
+
70
+ if mock_result_raw := mock_input.get("result"):
71
+ mock = json.loads(mock_result_raw)
72
+ self._result_stack.append(TestStateResponseReturn(mock))
73
+ return
74
+
75
+ if mock_error_output := mock_input.get("errorOutput"):
76
+ mock = copy.deepcopy(mock_error_output)
77
+ self._result_stack.append(TestStateResponseThrow(**mock))
78
+ return
79
+
80
+ def is_mocked(self):
81
+ if self._mock_input or self._state_configuration:
82
+ return True
83
+
84
+ return False
85
+
86
+ def add_result(self, result: TestStateMockedResponse):
87
+ mock = copy.deepcopy(result)
88
+ self._result_stack.append(mock)
89
+
90
+ def get_next_result(self) -> TestStateMockedResponse:
91
+ if not self._result_stack:
92
+ return None
93
+ return self._result_stack.pop()
94
+
95
+ def get_context(self) -> ContextObjectData | None:
96
+ if self._context is not None:
97
+ return copy.deepcopy(self._context)
98
+ return None
99
+
100
+ @staticmethod
101
+ def parse_context(context: str, state_type: StateType = None) -> ContextObjectData:
102
+ """Parse and validate context JSON string."""
103
+ try:
104
+ validation_result = TestStateContextObjectValidator.model_validate_json(context)
105
+ return validation_result.model_dump(exclude_unset=True, exclude_none=True)
106
+ except ValidationError as e:
107
+ error = e.errors()[0]
108
+ path_str = ".".join(str(x) for x in error["loc"])
109
+
110
+ match error:
111
+ case {"type": "extra_forbidden", "loc": ("Map",)}:
112
+ raise ValueError("'Map' field is not supported when mocking a Context object")
113
+
114
+ case {"type": "extra_forbidden", "loc": (*_, forbidden_key)}:
115
+ raise ValueError(f"Field '{forbidden_key}' is not allowed")
116
+
117
+ case {"type": t} if t in ("string_type", "int_type", "dict_type", "model_type"):
118
+ expected_map = {
119
+ "string_type": "string",
120
+ "int_type": "integer",
121
+ "dict_type": "object",
122
+ "model_type": "object",
123
+ }
124
+ expected = expected_map.get(t, "valid type")
125
+ raise ValueError(f"{path_str} must be a {expected}")
126
+ case _:
127
+ raise ValueError(f"{error['msg']}")
@@ -0,0 +1,9 @@
1
+ """
2
+ local_mocking
3
+ -------------
4
+
5
+ The implementation of the Step Functions Local Mocking
6
+
7
+ Note that Step Functions Local is different from TestState API mocks.
8
+ TestState API mocking works differently and is implemented separately.
9
+ """
@@ -1,7 +1,7 @@
1
1
  import abc
2
2
  from typing import Any, Final
3
3
 
4
- from localstack.services.stepfunctions.mocking.mock_config_file import (
4
+ from localstack.services.stepfunctions.local_mocking.mock_config_file import (
5
5
  RawMockConfig,
6
6
  RawResponseModel,
7
7
  RawTestCase,
@@ -9,7 +9,7 @@ from localstack.services.stepfunctions.mocking.mock_config_file import (
9
9
  )
10
10
 
11
11
 
12
- class MockedResponse(abc.ABC):
12
+ class LocalMockedResponse(abc.ABC):
13
13
  range_start: Final[int]
14
14
  range_end: Final[int]
15
15
 
@@ -28,7 +28,7 @@ class MockedResponse(abc.ABC):
28
28
  self.range_end = range_end
29
29
 
30
30
 
31
- class MockedResponseReturn(MockedResponse):
31
+ class LocalMockedResponseReturn(LocalMockedResponse):
32
32
  payload: Final[Any]
33
33
 
34
34
  def __init__(self, range_start: int, range_end: int, payload: Any):
@@ -36,7 +36,7 @@ class MockedResponseReturn(MockedResponse):
36
36
  self.payload = payload
37
37
 
38
38
 
39
- class MockedResponseThrow(MockedResponse):
39
+ class LocalMockedResponseThrow(LocalMockedResponse):
40
40
  error: Final[str]
41
41
  cause: Final[str]
42
42
 
@@ -49,10 +49,13 @@ class MockedResponseThrow(MockedResponse):
49
49
  class StateMockedResponses:
50
50
  state_name: Final[str]
51
51
  mocked_response_name: Final[str]
52
- mocked_responses: Final[list[MockedResponse]]
52
+ mocked_responses: Final[list[LocalMockedResponse]]
53
53
 
54
54
  def __init__(
55
- self, state_name: str, mocked_response_name: str, mocked_responses: list[MockedResponse]
55
+ self,
56
+ state_name: str,
57
+ mocked_response_name: str,
58
+ mocked_responses: list[LocalMockedResponse],
56
59
  ):
57
60
  self.state_name = state_name
58
61
  self.mocked_response_name = mocked_response_name
@@ -74,7 +77,7 @@ class StateMockedResponses:
74
77
  last_range_end = mocked_response.range_end
75
78
 
76
79
 
77
- class MockTestCase:
80
+ class LocalMockTestCase:
78
81
  state_machine_name: Final[str]
79
82
  test_case_name: Final[str]
80
83
  state_mocked_responses: Final[dict[str, StateMockedResponses]]
@@ -127,13 +130,15 @@ def _parse_mocked_response_range(string_definition: str) -> tuple[int, int]:
127
130
 
128
131
  def _mocked_response_from_raw(
129
132
  raw_response_model_range: str, raw_response_model: RawResponseModel
130
- ) -> MockedResponse:
133
+ ) -> LocalMockedResponse:
131
134
  range_start, range_end = _parse_mocked_response_range(raw_response_model_range)
132
135
  if raw_response_model.Return:
133
136
  payload = raw_response_model.Return.model_dump()
134
- return MockedResponseReturn(range_start=range_start, range_end=range_end, payload=payload)
137
+ return LocalMockedResponseReturn(
138
+ range_start=range_start, range_end=range_end, payload=payload
139
+ )
135
140
  throw_definition = raw_response_model.Throw
136
- return MockedResponseThrow(
141
+ return LocalMockedResponseThrow(
137
142
  range_start=range_start,
138
143
  range_end=range_end,
139
144
  error=throw_definition.Error,
@@ -143,7 +148,7 @@ def _mocked_response_from_raw(
143
148
 
144
149
  def _mocked_responses_from_raw(
145
150
  mocked_response_name: str, raw_mock_config: RawMockConfig
146
- ) -> list[MockedResponse]:
151
+ ) -> list[LocalMockedResponse]:
147
152
  raw_response_models: dict[str, RawResponseModel] | None = raw_mock_config.MockedResponses.get(
148
153
  mocked_response_name
149
154
  )
@@ -151,9 +156,9 @@ def _mocked_responses_from_raw(
151
156
  raise RuntimeError(
152
157
  f"No definitions for mocked response '{mocked_response_name}' in the mock configuration file."
153
158
  )
154
- mocked_responses: list[MockedResponse] = []
159
+ mocked_responses: list[LocalMockedResponse] = []
155
160
  for raw_response_model_range, raw_response_model in raw_response_models.items():
156
- mocked_response: MockedResponse = _mocked_response_from_raw(
161
+ mocked_response: LocalMockedResponse = _mocked_response_from_raw(
157
162
  raw_response_model_range=raw_response_model_range, raw_response_model=raw_response_model
158
163
  )
159
164
  mocked_responses.append(mocked_response)
@@ -175,7 +180,7 @@ def _state_mocked_responses_from_raw(
175
180
 
176
181
  def _mock_test_case_from_raw(
177
182
  state_machine_name: str, test_case_name: str, raw_mock_config: RawMockConfig
178
- ) -> MockTestCase:
183
+ ) -> LocalMockTestCase:
179
184
  state_machine = raw_mock_config.StateMachines.get(state_machine_name)
180
185
  if not state_machine:
181
186
  raise RuntimeError(
@@ -195,18 +200,20 @@ def _mock_test_case_from_raw(
195
200
  raw_mock_config=raw_mock_config,
196
201
  )
197
202
  state_mocked_responses_list.append(state_mocked_responses)
198
- return MockTestCase(
203
+ return LocalMockTestCase(
199
204
  state_machine_name=state_machine_name,
200
205
  test_case_name=test_case_name,
201
206
  state_mocked_responses_list=state_mocked_responses_list,
202
207
  )
203
208
 
204
209
 
205
- def load_mock_test_case_for(state_machine_name: str, test_case_name: str) -> MockTestCase | None:
210
+ def load_local_mock_test_case_for(
211
+ state_machine_name: str, test_case_name: str
212
+ ) -> LocalMockTestCase | None:
206
213
  raw_mock_config: RawMockConfig | None = _load_sfn_raw_mock_config()
207
214
  if raw_mock_config is None:
208
215
  return None
209
- mock_test_case: MockTestCase = _mock_test_case_from_raw(
216
+ mock_test_case: LocalMockTestCase = _mock_test_case_from_raw(
210
217
  state_machine_name=state_machine_name,
211
218
  test_case_name=test_case_name,
212
219
  raw_mock_config=raw_mock_config,
@@ -57,13 +57,13 @@ from localstack.aws.api.stepfunctions import (
57
57
  LongArn,
58
58
  MaxConcurrency,
59
59
  MissingRequiredParameter,
60
+ MockInput,
60
61
  Name,
61
62
  PageSize,
62
63
  PageToken,
63
64
  Publish,
64
65
  PublishStateMachineVersionOutput,
65
66
  ResourceNotFound,
66
- RevealSecrets,
67
67
  ReverseOrder,
68
68
  RevisionId,
69
69
  RoutingConfigurationList,
@@ -89,6 +89,7 @@ from localstack.aws.api.stepfunctions import (
89
89
  TaskDoesNotExist,
90
90
  TaskTimedOut,
91
91
  TaskToken,
92
+ TestStateInput,
92
93
  TestStateOutput,
93
94
  ToleratedFailureCount,
94
95
  ToleratedFailurePercentage,
@@ -150,9 +151,10 @@ from localstack.services.stepfunctions.backend.store import SFNStore, sfn_stores
150
151
  from localstack.services.stepfunctions.backend.test_state.execution import (
151
152
  TestStateExecution,
152
153
  )
153
- from localstack.services.stepfunctions.mocking.mock_config import (
154
- MockTestCase,
155
- load_mock_test_case_for,
154
+ from localstack.services.stepfunctions.backend.test_state.test_state_mock import TestStateMock
155
+ from localstack.services.stepfunctions.local_mocking.mock_config import (
156
+ LocalMockTestCase,
157
+ load_local_mock_test_case_for,
156
158
  )
157
159
  from localstack.services.stepfunctions.stepfunctions_utils import (
158
160
  assert_pagination_parameters_valid,
@@ -240,6 +242,12 @@ class StepFunctionsProvider(StepfunctionsApi, ServiceLifecycleHook):
240
242
  f"Invalid Arn: 'Resource type not valid in this context: {lower_resource_type}'"
241
243
  )
242
244
 
245
+ @staticmethod
246
+ def _validate_test_state_mock_input(mock_input: MockInput) -> None:
247
+ if {"result", "errorOutput"} <= mock_input.keys():
248
+ # FIXME create proper error
249
+ raise ValidationException("Cannot define both 'result' and 'errorOutput'")
250
+
243
251
  @staticmethod
244
252
  def _validate_activity_name(name: str) -> None:
245
253
  # The activity name is validated according to the AWS StepFunctions documentation, the name should not contain:
@@ -772,14 +780,16 @@ class StepFunctionsProvider(StepfunctionsApi, ServiceLifecycleHook):
772
780
  return state_machine_arn.split("#")[0]
773
781
 
774
782
  @staticmethod
775
- def _get_mock_test_case(state_machine_arn: str, state_machine_name: str) -> MockTestCase | None:
783
+ def _get_local_mock_test_case(
784
+ state_machine_arn: str, state_machine_name: str
785
+ ) -> LocalMockTestCase | None:
776
786
  """Extract and load a mock test case from a state machine ARN if present."""
777
787
  parts = state_machine_arn.split("#")
778
788
  if len(parts) != 2:
779
789
  return None
780
790
 
781
791
  mock_test_case_name = parts[1]
782
- mock_test_case = load_mock_test_case_for(
792
+ mock_test_case = load_local_mock_test_case_for(
783
793
  state_machine_name=state_machine_name, test_case_name=mock_test_case_name
784
794
  )
785
795
  if mock_test_case is None:
@@ -856,7 +866,9 @@ class StepFunctionsProvider(StepfunctionsApi, ServiceLifecycleHook):
856
866
  configuration=state_machine_clone.cloud_watch_logging_configuration,
857
867
  )
858
868
 
859
- mock_test_case = self._get_mock_test_case(state_machine_arn, state_machine_clone.name)
869
+ local_mock_test_case = self._get_local_mock_test_case(
870
+ state_machine_arn, state_machine_clone.name
871
+ )
860
872
 
861
873
  execution = Execution(
862
874
  name=exec_name,
@@ -872,7 +884,7 @@ class StepFunctionsProvider(StepfunctionsApi, ServiceLifecycleHook):
872
884
  input_data=input_data,
873
885
  trace_header=trace_header,
874
886
  activity_store=self.get_store(context).activities,
875
- mock_test_case=mock_test_case,
887
+ local_mock_test_case=local_mock_test_case,
876
888
  )
877
889
 
878
890
  store.executions[exec_arn] = execution
@@ -932,7 +944,9 @@ class StepFunctionsProvider(StepfunctionsApi, ServiceLifecycleHook):
932
944
  configuration=state_machine_clone.cloud_watch_logging_configuration,
933
945
  )
934
946
 
935
- mock_test_case = self._get_mock_test_case(state_machine_arn, state_machine_clone.name)
947
+ local_mock_test_case = self._get_local_mock_test_case(
948
+ state_machine_arn, state_machine_clone.name
949
+ )
936
950
 
937
951
  execution = SyncExecution(
938
952
  name=exec_name,
@@ -947,7 +961,7 @@ class StepFunctionsProvider(StepfunctionsApi, ServiceLifecycleHook):
947
961
  input_data=input_data,
948
962
  trace_header=trace_header,
949
963
  activity_store=self.get_store(context).activities,
950
- mock_test_case=mock_test_case,
964
+ local_mock_test_case=local_mock_test_case,
951
965
  )
952
966
  self.get_store(context).executions[exec_arn] = execution
953
967
 
@@ -1483,20 +1497,48 @@ class StepFunctionsProvider(StepfunctionsApi, ServiceLifecycleHook):
1483
1497
  raise ResourceNotFound()
1484
1498
 
1485
1499
  def test_state(
1486
- self,
1487
- context: RequestContext,
1488
- definition: Definition,
1489
- role_arn: Arn = None,
1490
- input: SensitiveData = None,
1491
- inspection_level: InspectionLevel = None,
1492
- reveal_secrets: RevealSecrets = None,
1493
- variables: SensitiveData = None,
1494
- **kwargs,
1500
+ self, context: RequestContext, request: TestStateInput, **kwargs
1495
1501
  ) -> TestStateOutput:
1502
+ state_name = request.get("stateName")
1503
+ definition = request["definition"]
1504
+
1496
1505
  StepFunctionsProvider._validate_definition(
1497
- definition=definition, static_analysers=[TestStateStaticAnalyser()]
1506
+ definition=definition,
1507
+ static_analysers=[TestStateStaticAnalyser(state_name)],
1498
1508
  )
1499
1509
 
1510
+ # if StateName is present, we need to ensure the state being referenced exists in full definition.
1511
+ if state_name and not TestStateStaticAnalyser.is_state_in_definition(
1512
+ definition=definition, state_name=state_name
1513
+ ):
1514
+ raise ValidationException("State not found in definition")
1515
+
1516
+ mock_input = request.get("mock")
1517
+ if mock_input is not None:
1518
+ self._validate_test_state_mock_input(mock_input)
1519
+ TestStateStaticAnalyser.validate_mock(
1520
+ mock_input=mock_input, definition=definition, state_name=state_name
1521
+ )
1522
+
1523
+ if state_configuration := request.get("stateConfiguration"):
1524
+ # TODO: Add validations for this i.e assert len(input) <= failureCount
1525
+ pass
1526
+
1527
+ if state_context := request.get("context"):
1528
+ # TODO: Add validation ensuring only present if 'mock' is specified
1529
+ # An error occurred (ValidationException) when calling the TestState operation: State type 'Pass' is not supported when a mock is specified
1530
+ pass
1531
+
1532
+ try:
1533
+ state_mock = TestStateMock(
1534
+ mock_input=mock_input,
1535
+ state_configuration=state_configuration,
1536
+ context=state_context,
1537
+ )
1538
+ except ValueError as e:
1539
+ LOG.error(e)
1540
+ raise ValidationException(f"Invalid Context object provided: {e}")
1541
+
1500
1542
  name: Name | None = f"TestState-{short_uid()}"
1501
1543
  arn = stepfunctions_state_machine_arn(
1502
1544
  name=name, account_id=context.account_id, region_name=context.region
@@ -1504,27 +1546,36 @@ class StepFunctionsProvider(StepfunctionsApi, ServiceLifecycleHook):
1504
1546
  state_machine = TestStateMachine(
1505
1547
  name=name,
1506
1548
  arn=arn,
1507
- role_arn=role_arn,
1508
- definition=definition,
1549
+ role_arn=request["roleArn"],
1550
+ definition=request["definition"],
1509
1551
  )
1510
- exec_arn = stepfunctions_standard_execution_arn(state_machine.arn, name)
1511
1552
 
1512
- input_json = json.loads(input)
1553
+ # HACK(gregfurman): The ARN that gets generated has a duplicate 'name' field in the
1554
+ # resource ARN. Just replace this duplication and extract the execution ID.
1555
+ exec_arn = stepfunctions_express_execution_arn(state_machine.arn, name)
1556
+ exec_arn = exec_arn.replace(f":{name}:{name}:", f":{name}:", 1)
1557
+ _, exec_name = exec_arn.rsplit(":", 1)
1558
+
1559
+ if input_json := request.get("input", {}):
1560
+ input_json = json.loads(input_json)
1561
+
1513
1562
  execution = TestStateExecution(
1514
- name=name,
1515
- role_arn=role_arn,
1563
+ name=exec_name,
1564
+ role_arn=request["roleArn"],
1516
1565
  exec_arn=exec_arn,
1517
1566
  account_id=context.account_id,
1518
1567
  region_name=context.region,
1519
1568
  state_machine=state_machine,
1520
1569
  start_date=datetime.datetime.now(tz=datetime.UTC),
1521
1570
  input_data=input_json,
1571
+ state_name=state_name,
1522
1572
  activity_store=self.get_store(context).activities,
1573
+ mock=state_mock,
1523
1574
  )
1524
1575
  execution.start()
1525
1576
 
1526
1577
  test_state_output = execution.to_test_state_output(
1527
- inspection_level=inspection_level or InspectionLevel.INFO
1578
+ inspection_level=request.get("inspectionLevel", InspectionLevel.INFO)
1528
1579
  )
1529
1580
 
1530
1581
  return test_state_output
@@ -0,0 +1,47 @@
1
+ import abc
2
+ from typing import Any, Final
3
+
4
+ from pydantic import BaseModel, ConfigDict, StrictInt, StrictStr, create_model
5
+
6
+ from localstack.services.stepfunctions.asl.eval.states import (
7
+ ExecutionData,
8
+ StateData,
9
+ StateMachineData,
10
+ TaskData,
11
+ )
12
+
13
+
14
+ class TestStateMockedResponse(abc.ABC):
15
+ pass
16
+
17
+
18
+ class TestStateResponseReturn(TestStateMockedResponse):
19
+ payload: Final[Any]
20
+
21
+ def __init__(self, payload: Any):
22
+ self.payload = payload
23
+
24
+
25
+ class TestStateResponseThrow(TestStateMockedResponse):
26
+ error: Final[str]
27
+ cause: Final[str]
28
+
29
+ def __init__(self, error: str, cause: str):
30
+ self.error = error
31
+ self.cause = cause
32
+
33
+
34
+ def _to_strict_model(name: str, source: type):
35
+ type_map = {str: StrictStr, int: StrictInt}
36
+ fields = {k: (type_map.get(v, v) | None, None) for k, v in source.__annotations__.items()}
37
+ return create_model(name, __config__=ConfigDict(extra="forbid"), **fields)
38
+
39
+
40
+ TestStateContextObjectValidator: Final[type[BaseModel]] = create_model(
41
+ "ContextValidator",
42
+ __config__=ConfigDict(extra="forbid"),
43
+ Execution=(_to_strict_model("Execution", ExecutionData) | None, None),
44
+ State=(_to_strict_model("State", StateData) | None, None),
45
+ StateMachine=(_to_strict_model("StateMachine", StateMachineData) | None, None),
46
+ Task=(_to_strict_model("Task", TaskData) | None, None),
47
+ )