localstack-core 4.10.1.dev42__py3-none-any.whl → 4.12.1.dev18__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 (158) hide show
  1. localstack/aws/api/apigateway/__init__.py +42 -0
  2. localstack/aws/api/cloudformation/__init__.py +161 -0
  3. localstack/aws/api/ec2/__init__.py +1178 -12
  4. localstack/aws/api/iam/__init__.py +228 -0
  5. localstack/aws/api/kms/__init__.py +1 -0
  6. localstack/aws/api/lambda_/__init__.py +1034 -66
  7. localstack/aws/api/logs/__init__.py +500 -0
  8. localstack/aws/api/opensearch/__init__.py +100 -0
  9. localstack/aws/api/redshift/__init__.py +69 -0
  10. localstack/aws/api/resourcegroupstaggingapi/__init__.py +36 -0
  11. localstack/aws/api/route53/__init__.py +45 -0
  12. localstack/aws/api/route53resolver/__init__.py +1 -0
  13. localstack/aws/api/s3/__init__.py +64 -0
  14. localstack/aws/api/s3control/__init__.py +19 -0
  15. localstack/aws/api/secretsmanager/__init__.py +37 -23
  16. localstack/aws/api/stepfunctions/__init__.py +52 -10
  17. localstack/aws/api/sts/__init__.py +52 -0
  18. localstack/aws/connect.py +35 -15
  19. localstack/aws/handlers/logging.py +8 -4
  20. localstack/aws/handlers/service.py +11 -2
  21. localstack/aws/protocol/serializer.py +1 -1
  22. localstack/config.py +8 -0
  23. localstack/constants.py +3 -0
  24. localstack/deprecations.py +0 -6
  25. localstack/dev/kubernetes/__main__.py +39 -14
  26. localstack/runtime/analytics.py +11 -0
  27. localstack/services/acm/provider.py +17 -1
  28. localstack/services/apigateway/legacy/provider.py +28 -15
  29. localstack/services/cloudformation/engine/template_preparer.py +6 -2
  30. localstack/services/cloudformation/engine/v2/change_set_model.py +9 -0
  31. localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +15 -1
  32. localstack/services/cloudformation/engine/v2/change_set_resource_support_checker.py +114 -0
  33. localstack/services/cloudformation/provider.py +26 -1
  34. localstack/services/cloudformation/provider_utils.py +20 -0
  35. localstack/services/cloudformation/resource_provider.py +5 -4
  36. localstack/services/cloudformation/scaffolding/__main__.py +94 -22
  37. localstack/services/cloudformation/v2/provider.py +41 -0
  38. localstack/services/cloudwatch/provider.py +10 -3
  39. localstack/services/cloudwatch/provider_v2.py +6 -3
  40. localstack/services/configservice/provider.py +5 -1
  41. localstack/services/dynamodb/provider.py +1 -0
  42. localstack/services/dynamodb/v2/provider.py +1 -0
  43. localstack/services/dynamodbstreams/provider.py +6 -0
  44. localstack/services/dynamodbstreams/v2/provider.py +6 -0
  45. localstack/services/ec2/provider.py +6 -0
  46. localstack/services/es/provider.py +6 -0
  47. localstack/services/events/provider.py +4 -0
  48. localstack/services/events/v1/provider.py +9 -0
  49. localstack/services/firehose/provider.py +5 -0
  50. localstack/services/iam/provider.py +4 -0
  51. localstack/services/kinesis/packages.py +1 -1
  52. localstack/services/kms/models.py +16 -22
  53. localstack/services/kms/provider.py +4 -0
  54. localstack/services/lambda_/analytics.py +11 -2
  55. localstack/services/lambda_/api_utils.py +37 -20
  56. localstack/services/lambda_/event_source_mapping/pollers/stream_poller.py +1 -1
  57. localstack/services/lambda_/invocation/assignment.py +4 -1
  58. localstack/services/lambda_/invocation/event_manager.py +15 -11
  59. localstack/services/lambda_/invocation/execution_environment.py +21 -2
  60. localstack/services/lambda_/invocation/lambda_models.py +31 -2
  61. localstack/services/lambda_/invocation/lambda_service.py +62 -3
  62. localstack/services/lambda_/invocation/models.py +9 -1
  63. localstack/services/lambda_/invocation/version_manager.py +18 -3
  64. localstack/services/lambda_/provider.py +307 -106
  65. localstack/services/lambda_/resource_providers/aws_lambda_function.py +33 -1
  66. localstack/services/lambda_/runtimes.py +3 -1
  67. localstack/services/logs/provider.py +9 -0
  68. localstack/services/opensearch/packages.py +34 -20
  69. localstack/services/opensearch/provider.py +53 -3
  70. localstack/services/resource_groups/provider.py +5 -1
  71. localstack/services/resourcegroupstaggingapi/provider.py +6 -1
  72. localstack/services/route53/provider.py +7 -0
  73. localstack/services/route53resolver/provider.py +5 -0
  74. localstack/services/s3/constants.py +5 -0
  75. localstack/services/s3/exceptions.py +9 -0
  76. localstack/services/s3/models.py +9 -1
  77. localstack/services/s3/provider.py +51 -43
  78. localstack/services/s3/utils.py +81 -15
  79. localstack/services/s3control/provider.py +107 -2
  80. localstack/services/s3control/validation.py +50 -0
  81. localstack/services/scheduler/provider.py +4 -2
  82. localstack/services/secretsmanager/provider.py +4 -0
  83. localstack/services/ses/provider.py +4 -0
  84. localstack/services/sns/constants.py +16 -1
  85. localstack/services/sns/provider.py +5 -0
  86. localstack/services/sns/publisher.py +15 -6
  87. localstack/services/sns/v2/models.py +9 -0
  88. localstack/services/sns/v2/provider.py +750 -19
  89. localstack/services/sns/v2/utils.py +12 -0
  90. localstack/services/sqs/constants.py +6 -0
  91. localstack/services/sqs/provider.py +9 -1
  92. localstack/services/sqs/resource_providers/aws_sqs_queue.py +61 -46
  93. localstack/services/ssm/provider.py +6 -0
  94. localstack/services/stepfunctions/asl/component/common/path/result_path.py +1 -1
  95. localstack/services/stepfunctions/asl/component/state/state_execution/execute_state.py +0 -1
  96. localstack/services/stepfunctions/asl/component/state/state_execution/state_map/state_map.py +0 -1
  97. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/lambda_eval_utils.py +8 -8
  98. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/{mock_eval_utils.py → local_mock_eval_utils.py} +13 -9
  99. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service.py +6 -6
  100. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_callback.py +1 -1
  101. localstack/services/stepfunctions/asl/component/state/state_fail/state_fail.py +4 -0
  102. localstack/services/stepfunctions/asl/component/test_state/state/base_mock.py +118 -0
  103. localstack/services/stepfunctions/asl/component/test_state/state/common.py +82 -0
  104. localstack/services/stepfunctions/asl/component/test_state/state/execution.py +139 -0
  105. localstack/services/stepfunctions/asl/component/test_state/state/map.py +77 -0
  106. localstack/services/stepfunctions/asl/component/test_state/state/task.py +44 -0
  107. localstack/services/stepfunctions/asl/eval/environment.py +30 -22
  108. localstack/services/stepfunctions/asl/eval/states.py +1 -1
  109. localstack/services/stepfunctions/asl/eval/test_state/environment.py +49 -9
  110. localstack/services/stepfunctions/asl/eval/test_state/program_state.py +22 -0
  111. localstack/services/stepfunctions/asl/jsonata/jsonata.py +5 -1
  112. localstack/services/stepfunctions/asl/parse/preprocessor.py +67 -24
  113. localstack/services/stepfunctions/asl/parse/test_state/asl_parser.py +5 -4
  114. localstack/services/stepfunctions/asl/parse/test_state/preprocessor.py +222 -31
  115. localstack/services/stepfunctions/asl/static_analyser/test_state/test_state_analyser.py +256 -22
  116. localstack/services/stepfunctions/backend/execution.py +10 -11
  117. localstack/services/stepfunctions/backend/execution_worker.py +5 -5
  118. localstack/services/stepfunctions/backend/test_state/execution.py +36 -0
  119. localstack/services/stepfunctions/backend/test_state/execution_worker.py +33 -1
  120. localstack/services/stepfunctions/backend/test_state/test_state_mock.py +127 -0
  121. localstack/services/stepfunctions/local_mocking/__init__.py +9 -0
  122. localstack/services/stepfunctions/{mocking → local_mocking}/mock_config.py +24 -17
  123. localstack/services/stepfunctions/provider.py +83 -25
  124. localstack/services/stepfunctions/test_state/mock_config.py +47 -0
  125. localstack/services/sts/provider.py +7 -0
  126. localstack/services/support/provider.py +5 -1
  127. localstack/services/swf/provider.py +5 -1
  128. localstack/services/transcribe/provider.py +7 -0
  129. localstack/testing/aws/lambda_utils.py +1 -1
  130. localstack/testing/aws/util.py +2 -1
  131. localstack/testing/config.py +1 -0
  132. localstack/testing/pytest/fixtures.py +28 -0
  133. localstack/testing/snapshots/transformer_utility.py +5 -0
  134. localstack/utils/analytics/publisher.py +37 -155
  135. localstack/utils/analytics/service_request_aggregator.py +6 -4
  136. localstack/utils/aws/arns.py +7 -0
  137. localstack/utils/aws/client_types.py +2 -4
  138. localstack/utils/batching.py +258 -0
  139. localstack/utils/bootstrap.py +2 -2
  140. localstack/utils/catalog/catalog.py +3 -2
  141. localstack/utils/collections.py +23 -11
  142. localstack/utils/container_utils/container_client.py +22 -13
  143. localstack/utils/container_utils/docker_cmd_client.py +6 -6
  144. localstack/version.py +2 -2
  145. {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.12.1.dev18.dist-info}/METADATA +7 -7
  146. {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.12.1.dev18.dist-info}/RECORD +155 -146
  147. localstack_core-4.12.1.dev18.dist-info/plux.json +1 -0
  148. localstack/services/stepfunctions/mocking/__init__.py +0 -0
  149. localstack/utils/batch_policy.py +0 -124
  150. localstack_core-4.10.1.dev42.dist-info/plux.json +0 -1
  151. /localstack/services/stepfunctions/{mocking → local_mocking}/mock_config_file.py +0 -0
  152. {localstack_core-4.10.1.dev42.data → localstack_core-4.12.1.dev18.data}/scripts/localstack +0 -0
  153. {localstack_core-4.10.1.dev42.data → localstack_core-4.12.1.dev18.data}/scripts/localstack-supervisor +0 -0
  154. {localstack_core-4.10.1.dev42.data → localstack_core-4.12.1.dev18.data}/scripts/localstack.bat +0 -0
  155. {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.12.1.dev18.dist-info}/WHEEL +0 -0
  156. {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.12.1.dev18.dist-info}/entry_points.txt +0 -0
  157. {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.12.1.dev18.dist-info}/licenses/LICENSE.txt +0 -0
  158. {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.12.1.dev18.dist-info}/top_level.txt +0 -0
@@ -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,
@@ -63,7 +63,6 @@ from localstack.aws.api.stepfunctions import (
63
63
  Publish,
64
64
  PublishStateMachineVersionOutput,
65
65
  ResourceNotFound,
66
- RevealSecrets,
67
66
  ReverseOrder,
68
67
  RevisionId,
69
68
  RoutingConfigurationList,
@@ -89,6 +88,7 @@ from localstack.aws.api.stepfunctions import (
89
88
  TaskDoesNotExist,
90
89
  TaskTimedOut,
91
90
  TaskToken,
91
+ TestStateInput,
92
92
  TestStateOutput,
93
93
  ToleratedFailureCount,
94
94
  ToleratedFailurePercentage,
@@ -150,9 +150,10 @@ from localstack.services.stepfunctions.backend.store import SFNStore, sfn_stores
150
150
  from localstack.services.stepfunctions.backend.test_state.execution import (
151
151
  TestStateExecution,
152
152
  )
153
- from localstack.services.stepfunctions.mocking.mock_config import (
154
- MockTestCase,
155
- load_mock_test_case_for,
153
+ from localstack.services.stepfunctions.backend.test_state.test_state_mock import TestStateMock
154
+ from localstack.services.stepfunctions.local_mocking.mock_config import (
155
+ LocalMockTestCase,
156
+ load_local_mock_test_case_for,
156
157
  )
157
158
  from localstack.services.stepfunctions.stepfunctions_utils import (
158
159
  assert_pagination_parameters_valid,
@@ -160,6 +161,7 @@ from localstack.services.stepfunctions.stepfunctions_utils import (
160
161
  normalise_max_results,
161
162
  )
162
163
  from localstack.state import StateVisitor
164
+ from localstack.utils.aws import arns
163
165
  from localstack.utils.aws.arns import (
164
166
  ARN_PARTITION_REGEX,
165
167
  stepfunctions_activity_arn,
@@ -772,14 +774,16 @@ class StepFunctionsProvider(StepfunctionsApi, ServiceLifecycleHook):
772
774
  return state_machine_arn.split("#")[0]
773
775
 
774
776
  @staticmethod
775
- def _get_mock_test_case(state_machine_arn: str, state_machine_name: str) -> MockTestCase | None:
777
+ def _get_local_mock_test_case(
778
+ state_machine_arn: str, state_machine_name: str
779
+ ) -> LocalMockTestCase | None:
776
780
  """Extract and load a mock test case from a state machine ARN if present."""
777
781
  parts = state_machine_arn.split("#")
778
782
  if len(parts) != 2:
779
783
  return None
780
784
 
781
785
  mock_test_case_name = parts[1]
782
- mock_test_case = load_mock_test_case_for(
786
+ mock_test_case = load_local_mock_test_case_for(
783
787
  state_machine_name=state_machine_name, test_case_name=mock_test_case_name
784
788
  )
785
789
  if mock_test_case is None:
@@ -856,7 +860,9 @@ class StepFunctionsProvider(StepfunctionsApi, ServiceLifecycleHook):
856
860
  configuration=state_machine_clone.cloud_watch_logging_configuration,
857
861
  )
858
862
 
859
- mock_test_case = self._get_mock_test_case(state_machine_arn, state_machine_clone.name)
863
+ local_mock_test_case = self._get_local_mock_test_case(
864
+ state_machine_arn, state_machine_clone.name
865
+ )
860
866
 
861
867
  execution = Execution(
862
868
  name=exec_name,
@@ -872,7 +878,7 @@ class StepFunctionsProvider(StepfunctionsApi, ServiceLifecycleHook):
872
878
  input_data=input_data,
873
879
  trace_header=trace_header,
874
880
  activity_store=self.get_store(context).activities,
875
- mock_test_case=mock_test_case,
881
+ local_mock_test_case=local_mock_test_case,
876
882
  )
877
883
 
878
884
  store.executions[exec_arn] = execution
@@ -932,7 +938,9 @@ class StepFunctionsProvider(StepfunctionsApi, ServiceLifecycleHook):
932
938
  configuration=state_machine_clone.cloud_watch_logging_configuration,
933
939
  )
934
940
 
935
- mock_test_case = self._get_mock_test_case(state_machine_arn, state_machine_clone.name)
941
+ local_mock_test_case = self._get_local_mock_test_case(
942
+ state_machine_arn, state_machine_clone.name
943
+ )
936
944
 
937
945
  execution = SyncExecution(
938
946
  name=exec_name,
@@ -947,7 +955,7 @@ class StepFunctionsProvider(StepfunctionsApi, ServiceLifecycleHook):
947
955
  input_data=input_data,
948
956
  trace_header=trace_header,
949
957
  activity_store=self.get_store(context).activities,
950
- mock_test_case=mock_test_case,
958
+ local_mock_test_case=local_mock_test_case,
951
959
  )
952
960
  self.get_store(context).executions[exec_arn] = execution
953
961
 
@@ -1483,35 +1491,83 @@ class StepFunctionsProvider(StepfunctionsApi, ServiceLifecycleHook):
1483
1491
  raise ResourceNotFound()
1484
1492
 
1485
1493
  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,
1494
+ self, context: RequestContext, request: TestStateInput, **kwargs
1495
1495
  ) -> TestStateOutput:
1496
+ state_name = request.get("stateName")
1497
+ definition = request["definition"]
1498
+
1496
1499
  StepFunctionsProvider._validate_definition(
1497
- definition=definition, static_analysers=[TestStateStaticAnalyser()]
1500
+ definition=definition,
1501
+ static_analysers=[TestStateStaticAnalyser(state_name)],
1498
1502
  )
1499
1503
 
1504
+ # if StateName is present, we need to ensure the state being referenced exists in full definition.
1505
+ if state_name and not TestStateStaticAnalyser.is_state_in_definition(
1506
+ definition=definition, state_name=state_name
1507
+ ):
1508
+ raise ValidationException("State not found in definition")
1509
+
1510
+ mock_input = request.get("mock")
1511
+ TestStateStaticAnalyser.validate_mock(test_state_input=request)
1512
+
1513
+ if state_configuration := request.get("stateConfiguration"):
1514
+ # TODO: Add validations for this i.e assert len(input) <= failureCount
1515
+ pass
1516
+
1517
+ if state_context := request.get("context"):
1518
+ # TODO: Add validation ensuring only present if 'mock' is specified
1519
+ # An error occurred (ValidationException) when calling the TestState operation: State type 'Pass' is not supported when a mock is specified
1520
+ pass
1521
+
1522
+ try:
1523
+ state_mock = TestStateMock(
1524
+ mock_input=mock_input,
1525
+ state_configuration=state_configuration,
1526
+ context=state_context,
1527
+ )
1528
+ except ValueError as e:
1529
+ LOG.error(e)
1530
+ raise ValidationException(f"Invalid Context object provided: {e}")
1531
+
1500
1532
  name: Name | None = f"TestState-{short_uid()}"
1501
1533
  arn = stepfunctions_state_machine_arn(
1502
1534
  name=name, account_id=context.account_id, region_name=context.region
1503
1535
  )
1536
+ role_arn = request.get("roleArn")
1537
+ if role_arn is None:
1538
+ TestStateStaticAnalyser.validate_role_arn_required(
1539
+ mock_input=mock_input, definition=definition, state_name=state_name
1540
+ )
1541
+ # HACK: Added dummy role ARN because it is a required field in Execution.
1542
+ # To allow optional roleArn for the test state but preserve the mandatory one for regular executions
1543
+ # we likely need to remove inheritance TestStateExecution(Execution) in favor of composition.
1544
+ # TestState execution starts to have too many simplifications compared to a regular execution
1545
+ # which renders the inheritance mechanism harmful.
1546
+ # TODO make role_arn optional in TestStateExecution
1547
+ role_arn = arns.iam_role_arn(
1548
+ role_name=f"RoleFor-{name}",
1549
+ account_id=context.account_id,
1550
+ region_name=context.region,
1551
+ )
1552
+
1504
1553
  state_machine = TestStateMachine(
1505
1554
  name=name,
1506
1555
  arn=arn,
1507
1556
  role_arn=role_arn,
1508
- definition=definition,
1557
+ definition=request["definition"],
1509
1558
  )
1510
- exec_arn = stepfunctions_standard_execution_arn(state_machine.arn, name)
1511
1559
 
1512
- input_json = json.loads(input)
1560
+ # HACK(gregfurman): The ARN that gets generated has a duplicate 'name' field in the
1561
+ # resource ARN. Just replace this duplication and extract the execution ID.
1562
+ exec_arn = stepfunctions_express_execution_arn(state_machine.arn, name)
1563
+ exec_arn = exec_arn.replace(f":{name}:{name}:", f":{name}:", 1)
1564
+ _, exec_name = exec_arn.rsplit(":", 1)
1565
+
1566
+ if input_json := request.get("input", {}):
1567
+ input_json = json.loads(input_json)
1568
+
1513
1569
  execution = TestStateExecution(
1514
- name=name,
1570
+ name=exec_name,
1515
1571
  role_arn=role_arn,
1516
1572
  exec_arn=exec_arn,
1517
1573
  account_id=context.account_id,
@@ -1519,12 +1575,14 @@ class StepFunctionsProvider(StepfunctionsApi, ServiceLifecycleHook):
1519
1575
  state_machine=state_machine,
1520
1576
  start_date=datetime.datetime.now(tz=datetime.UTC),
1521
1577
  input_data=input_json,
1578
+ state_name=state_name,
1522
1579
  activity_store=self.get_store(context).activities,
1580
+ mock=state_mock,
1523
1581
  )
1524
1582
  execution.start()
1525
1583
 
1526
1584
  test_state_output = execution.to_test_state_output(
1527
- inspection_level=inspection_level or InspectionLevel.INFO
1585
+ inspection_level=request.get("inspectionLevel", InspectionLevel.INFO)
1528
1586
  )
1529
1587
 
1530
1588
  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
+ )
@@ -23,6 +23,7 @@ from localstack.services.iam.iam_patches import apply_iam_patches
23
23
  from localstack.services.moto import call_moto
24
24
  from localstack.services.plugins import ServiceLifecycleHook
25
25
  from localstack.services.sts.models import SessionConfig, sts_stores
26
+ from localstack.state import StateVisitor
26
27
  from localstack.utils.aws.arns import extract_account_id_from_arn
27
28
  from localstack.utils.aws.request_context import extract_access_key_id_from_auth_header
28
29
 
@@ -50,6 +51,12 @@ class StsProvider(StsApi, ServiceLifecycleHook):
50
51
  def __init__(self):
51
52
  apply_iam_patches()
52
53
 
54
+ def accept_state_visitor(self, visitor: StateVisitor):
55
+ from moto.sts.models import sts_backends
56
+
57
+ visitor.visit(sts_backends)
58
+ visitor.visit(sts_stores)
59
+
53
60
  def get_caller_identity(self, context: RequestContext, **kwargs) -> GetCallerIdentityResponse:
54
61
  response = call_moto(context)
55
62
  if "user/moto" in response["Arn"] and "sts" in response["Arn"]:
@@ -1,7 +1,11 @@
1
1
  from abc import ABC
2
2
 
3
3
  from localstack.aws.api.support import SupportApi
4
+ from localstack.state import StateVisitor
4
5
 
5
6
 
6
7
  class SupportProvider(SupportApi, ABC):
7
- pass
8
+ def accept_state_visitor(self, visitor: StateVisitor):
9
+ from moto.support.models import support_backends
10
+
11
+ visitor.visit(support_backends)
@@ -1,7 +1,11 @@
1
1
  from abc import ABC
2
2
 
3
3
  from localstack.aws.api.swf import SwfApi
4
+ from localstack.state import StateVisitor
4
5
 
5
6
 
6
7
  class SWFProvider(SwfApi, ABC):
7
- pass
8
+ def accept_state_visitor(self, visitor: StateVisitor):
9
+ from moto.swf.models import swf_backends
10
+
11
+ visitor.visit(swf_backends)
@@ -38,6 +38,7 @@ from localstack.services.s3.utils import (
38
38
  )
39
39
  from localstack.services.transcribe.models import TranscribeStore, transcribe_stores
40
40
  from localstack.services.transcribe.packages import vosk_package
41
+ from localstack.state import StateVisitor
41
42
  from localstack.utils.files import new_tmp_file
42
43
  from localstack.utils.http import download
43
44
  from localstack.utils.run import run
@@ -101,6 +102,12 @@ _DL_LOCK = threading.Lock()
101
102
 
102
103
 
103
104
  class TranscribeProvider(TranscribeApi):
105
+ def accept_state_visitor(self, visitor: StateVisitor) -> None:
106
+ from moto.transcribe.models import transcribe_backends
107
+
108
+ visitor.visit(transcribe_backends)
109
+ visitor.visit(transcribe_stores)
110
+
104
111
  def get_transcription_job(
105
112
  self, context: RequestContext, transcription_job_name: TranscriptionJobName, **kwargs: Any
106
113
  ) -> GetTranscriptionJobResponse:
@@ -261,7 +261,7 @@ def concurrency_update_done(client, function_name, qualifier):
261
261
 
262
262
  def get_invoke_init_type(
263
263
  client, function_name, qualifier
264
- ) -> Literal["on-demand", "provisioned-concurrency"]:
264
+ ) -> Literal["on-demand", "provisioned-concurrency", "lambda-managed-instances"]:
265
265
  """check the environment in the lambda for AWS_LAMBDA_INITIALIZATION_TYPE indicating ondemand/provisioned"""
266
266
  invoke_result = client.invoke(FunctionName=function_name, Qualifier=qualifier)
267
267
  return json.load(invoke_result["Payload"])
@@ -29,6 +29,7 @@ from localstack.testing.config import (
29
29
  SECONDARY_TEST_AWS_SECRET_ACCESS_KEY,
30
30
  SECONDARY_TEST_AWS_SESSION_TOKEN,
31
31
  TEST_AWS_ACCESS_KEY_ID,
32
+ TEST_AWS_ENDPOINT_URL,
32
33
  TEST_AWS_REGION_NAME,
33
34
  TEST_AWS_SECRET_ACCESS_KEY,
34
35
  )
@@ -240,7 +241,7 @@ def base_aws_client_factory(session: boto3.Session) -> ClientFactory:
240
241
 
241
242
  # Prevent this fixture from using the region configured in system config
242
243
  config = config.merge(botocore.config.Config(region_name=TEST_AWS_REGION_NAME))
243
- return ExternalClientFactory(session=session, config=config)
244
+ return ExternalClientFactory(session=session, config=config, endpoint=TEST_AWS_ENDPOINT_URL)
244
245
 
245
246
 
246
247
  def base_testing_aws_client(client_factory: ClientFactory) -> ServiceLevelClientFactory:
@@ -9,6 +9,7 @@ TEST_AWS_ACCOUNT_ID = os.getenv("TEST_AWS_ACCOUNT_ID") or DEFAULT_AWS_ACCOUNT_ID
9
9
  TEST_AWS_ACCESS_KEY_ID = os.getenv("TEST_AWS_ACCESS_KEY_ID") or "test"
10
10
  TEST_AWS_SECRET_ACCESS_KEY = os.getenv("TEST_AWS_SECRET_ACCESS_KEY") or "test"
11
11
  TEST_AWS_REGION_NAME = os.getenv("TEST_AWS_REGION_NAME") or "us-east-1"
12
+ TEST_AWS_ENDPOINT_URL = os.getenv("TEST_AWS_ENDPOINT_URL")
12
13
 
13
14
  # Secondary test AWS profile - only used for testing against AWS
14
15
  SECONDARY_TEST_AWS_PROFILE = os.getenv("SECONDARY_TEST_AWS_PROFILE")
@@ -1426,6 +1426,34 @@ def create_lambda_function(aws_client, wait_until_lambda_ready, lambda_su_role):
1426
1426
  LOG.debug("Unable to delete log group %s in cleanup", log_group_name)
1427
1427
 
1428
1428
 
1429
+ @pytest.fixture
1430
+ def lambda_is_function_deleted(aws_client):
1431
+ """Example usage:
1432
+ wait_until(lambda_is_function_deleted(function_name))
1433
+ wait_until(lambda_is_function_deleted(function_name, Qualifier="my-alias"))
1434
+
1435
+ function_name can be a function name, function ARN, or partial function ARN.
1436
+ """
1437
+ return _lambda_is_function_deleted(aws_client.lambda_)
1438
+
1439
+
1440
+ def _lambda_is_function_deleted(lambda_client):
1441
+ def _is_function_deleted(
1442
+ function_name: str,
1443
+ **kwargs,
1444
+ ) -> Callable[[], bool]:
1445
+ def _inner() -> bool:
1446
+ try:
1447
+ lambda_client.get_function(FunctionName=function_name, **kwargs)
1448
+ return False
1449
+ except lambda_client.exceptions.ResourceNotFoundException:
1450
+ return True
1451
+
1452
+ return _inner
1453
+
1454
+ return _is_function_deleted
1455
+
1456
+
1429
1457
  @pytest.fixture
1430
1458
  def create_echo_http_server(aws_client, create_lambda_function):
1431
1459
  from localstack.aws.api.lambda_ import Runtime
@@ -791,6 +791,11 @@ class TransformerUtility:
791
791
  "x-amzn-RequestId",
792
792
  replace_reference=False,
793
793
  ),
794
+ JsonpathTransformer(
795
+ "$..x-amzn-requestid",
796
+ "x-amzn-requestid",
797
+ replace_reference=False,
798
+ ),
794
799
  KeyValueBasedTransformer(_transform_stepfunctions_cause_details, "json-input"),
795
800
  ]
796
801