localstack-core 4.11.2.dev14__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 (77) hide show
  1. localstack/aws/api/ec2/__init__.py +13 -0
  2. localstack/aws/api/iam/__init__.py +1 -0
  3. localstack/aws/api/lambda_/__init__.py +616 -0
  4. localstack/aws/api/logs/__init__.py +188 -0
  5. localstack/aws/api/opensearch/__init__.py +11 -0
  6. localstack/aws/api/route53/__init__.py +3 -0
  7. localstack/aws/api/s3/__init__.py +2 -0
  8. localstack/aws/api/s3control/__init__.py +19 -0
  9. localstack/aws/api/secretsmanager/__init__.py +9 -0
  10. localstack/aws/connect.py +35 -15
  11. localstack/config.py +8 -0
  12. localstack/constants.py +3 -0
  13. localstack/dev/kubernetes/__main__.py +39 -14
  14. localstack/runtime/analytics.py +11 -0
  15. localstack/services/acm/provider.py +13 -1
  16. localstack/services/cloudformation/engine/v2/change_set_model.py +9 -0
  17. localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +3 -1
  18. localstack/services/cloudformation/engine/v2/change_set_resource_support_checker.py +114 -0
  19. localstack/services/cloudformation/provider.py +26 -1
  20. localstack/services/cloudformation/provider_utils.py +20 -0
  21. localstack/services/cloudformation/resource_provider.py +5 -4
  22. localstack/services/cloudformation/scaffolding/__main__.py +94 -22
  23. localstack/services/cloudformation/v2/provider.py +41 -0
  24. localstack/services/kinesis/packages.py +1 -1
  25. localstack/services/kms/models.py +6 -2
  26. localstack/services/lambda_/analytics.py +11 -2
  27. localstack/services/lambda_/invocation/event_manager.py +15 -11
  28. localstack/services/lambda_/invocation/lambda_models.py +4 -0
  29. localstack/services/lambda_/invocation/lambda_service.py +11 -0
  30. localstack/services/lambda_/provider.py +70 -13
  31. localstack/services/opensearch/packages.py +34 -20
  32. localstack/services/route53/provider.py +7 -0
  33. localstack/services/route53resolver/provider.py +5 -0
  34. localstack/services/s3/constants.py +5 -0
  35. localstack/services/s3/exceptions.py +9 -0
  36. localstack/services/s3/models.py +9 -1
  37. localstack/services/s3/provider.py +25 -30
  38. localstack/services/s3/utils.py +46 -1
  39. localstack/services/s3control/provider.py +6 -0
  40. localstack/services/scheduler/provider.py +4 -2
  41. localstack/services/secretsmanager/provider.py +4 -0
  42. localstack/services/ses/provider.py +4 -0
  43. localstack/services/sns/constants.py +13 -0
  44. localstack/services/sns/provider.py +5 -0
  45. localstack/services/sns/v2/models.py +3 -0
  46. localstack/services/sns/v2/provider.py +100 -0
  47. localstack/services/sqs/constants.py +6 -0
  48. localstack/services/sqs/provider.py +9 -1
  49. localstack/services/sqs/resource_providers/aws_sqs_queue.py +61 -46
  50. localstack/services/ssm/provider.py +6 -0
  51. localstack/services/stepfunctions/asl/static_analyser/test_state/test_state_analyser.py +193 -107
  52. localstack/services/stepfunctions/backend/execution.py +4 -5
  53. localstack/services/stepfunctions/provider.py +21 -14
  54. localstack/services/sts/provider.py +7 -0
  55. localstack/services/support/provider.py +5 -1
  56. localstack/services/swf/provider.py +5 -1
  57. localstack/services/transcribe/provider.py +7 -0
  58. localstack/testing/aws/lambda_utils.py +1 -1
  59. localstack/testing/aws/util.py +2 -1
  60. localstack/testing/config.py +1 -0
  61. localstack/utils/aws/client_types.py +2 -4
  62. localstack/utils/bootstrap.py +2 -2
  63. localstack/utils/catalog/catalog.py +3 -2
  64. localstack/utils/container_utils/container_client.py +22 -13
  65. localstack/utils/container_utils/docker_cmd_client.py +6 -6
  66. localstack/version.py +2 -2
  67. {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev18.dist-info}/METADATA +6 -6
  68. {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev18.dist-info}/RECORD +76 -75
  69. localstack_core-4.12.1.dev18.dist-info/plux.json +1 -0
  70. localstack_core-4.11.2.dev14.dist-info/plux.json +0 -1
  71. {localstack_core-4.11.2.dev14.data → localstack_core-4.12.1.dev18.data}/scripts/localstack +0 -0
  72. {localstack_core-4.11.2.dev14.data → localstack_core-4.12.1.dev18.data}/scripts/localstack-supervisor +0 -0
  73. {localstack_core-4.11.2.dev14.data → localstack_core-4.12.1.dev18.data}/scripts/localstack.bat +0 -0
  74. {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev18.dist-info}/WHEEL +0 -0
  75. {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev18.dist-info}/entry_points.txt +0 -0
  76. {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev18.dist-info}/licenses/LICENSE.txt +0 -0
  77. {localstack_core-4.11.2.dev14.dist-info → localstack_core-4.12.1.dev18.dist-info}/top_level.txt +0 -0
@@ -64,6 +64,35 @@ class SQSQueueProvider(ResourceProvider[SQSQueueProperties]):
64
64
  TYPE = "AWS::SQS::Queue" # Autogenerated. Don't change
65
65
  SCHEMA = util.get_schema_path(Path(__file__)) # Autogenerated. Don't change
66
66
 
67
+ # Values used when a property is removed from a template and needs to be set to its default.
68
+ # If AWS changes their defaults in the future, our parity tests should break.
69
+ DEFAULT_ATTRIBUTE_VALUES = {
70
+ "ReceiveMessageWaitTimeSeconds": "0",
71
+ "DelaySeconds": "0",
72
+ "KmsMasterKeyId": "",
73
+ "RedrivePolicy": "",
74
+ "MessageRetentionPeriod": "345600",
75
+ "MaximumMessageSize": "262144", # Note: CloudFormation sets this to 256KB on update, but 1MB on create
76
+ "VisibilityTimeout": "30",
77
+ "KmsDataKeyReusePeriodSeconds": "300",
78
+ }
79
+
80
+ # Private method for creating a unique queue name, if none is specified.
81
+ def _autogenerated_queue_name(self, request: ResourceRequest[SQSQueueProperties]) -> str:
82
+ queue_name = util.generate_default_name(request.stack_name, request.logical_resource_id)
83
+ isFifoQueue = request.desired_state.get("FifoQueue")
84
+
85
+ # Note that it's an SQS FIFO queue only if the FifoQueue property is set to boolean True, or the string "true"
86
+ # (case insensitive). If it's None (property was omitted) or False, or any type of string (e.g. a typo
87
+ # such as "Fasle"), then it's not a FIFO queue. This extra check is needed because the CloudFormation engine
88
+ # doesn't fully validate the FifoQueue property before passing it to the resource provider.
89
+ if (
90
+ isFifoQueue == True # noqa: E712
91
+ or (isinstance(isFifoQueue, str) and isFifoQueue.lower() == "true")
92
+ ):
93
+ queue_name = f"{queue_name[:-5]}.fifo"
94
+ return queue_name
95
+
67
96
  def create(
68
97
  self,
69
98
  request: ResourceRequest[SQSQueueProperties],
@@ -74,8 +103,6 @@ class SQSQueueProvider(ResourceProvider[SQSQueueProperties]):
74
103
  Primary identifier fields:
75
104
  - /properties/QueueUrl
76
105
 
77
-
78
-
79
106
  Create-only properties:
80
107
  - /properties/FifoQueue
81
108
  - /properties/QueueName
@@ -92,26 +119,13 @@ class SQSQueueProvider(ResourceProvider[SQSQueueProperties]):
92
119
  - sqs:TagQueue
93
120
 
94
121
  """
95
- # TODO: validations
122
+ # TODO: validations - what validations are needed?
96
123
  model = request.desired_state
97
124
  sqs = request.aws_client_factory.sqs
98
125
 
99
- if model.get("FifoQueue", False):
100
- model["FifoQueue"] = model["FifoQueue"]
101
-
102
- queue_name = model.get("QueueName")
103
- if not queue_name:
104
- # TODO: verify patterns here
105
- if model.get("FifoQueue"):
106
- queue_name = util.generate_default_name(
107
- request.stack_name, request.logical_resource_id
108
- )[:-5]
109
- queue_name = f"{queue_name}.fifo"
110
- else:
111
- queue_name = util.generate_default_name(
112
- request.stack_name, request.logical_resource_id
113
- )
114
- model["QueueName"] = queue_name
126
+ # if no QueueName is specified, automatically generate one
127
+ if not model.get("QueueName"):
128
+ model["QueueName"] = self._autogenerated_queue_name(request)
115
129
 
116
130
  attributes = self._compile_sqs_queue_attributes(model)
117
131
  result = request.aws_client_factory.sqs.create_queue(
@@ -184,38 +198,30 @@ class SQSQueueProvider(ResourceProvider[SQSQueueProperties]):
184
198
  """
185
199
  sqs = request.aws_client_factory.sqs
186
200
  model = request.desired_state
201
+ prev_model = request.previous_state
187
202
 
188
203
  assert request.previous_state is not None
189
204
 
190
- should_replace = (
191
- request.desired_state.get("QueueName", request.previous_state["QueueName"])
192
- != request.previous_state["QueueName"]
193
- ) or (
194
- request.desired_state.get("FifoQueue", request.previous_state.get("FifoQueue"))
195
- != request.previous_state.get("FifoQueue")
205
+ queue_url = prev_model["QueueUrl"]
206
+ self._populate_missing_attributes_with_defaults(model)
207
+ sqs.set_queue_attributes(
208
+ QueueUrl=queue_url, Attributes=self._compile_sqs_queue_attributes(model)
196
209
  )
197
210
 
198
- if not should_replace:
199
- return ProgressEvent(OperationStatus.SUCCESS, resource_model=request.previous_state)
200
-
201
- # TODO: copied from the create handler, extract?
202
- if model.get("FifoQueue"):
203
- queue_name = util.generate_default_name(
204
- request.stack_name, request.logical_resource_id
205
- )[:-5]
206
- queue_name = f"{queue_name}.fifo"
207
- else:
208
- queue_name = util.generate_default_name(request.stack_name, request.logical_resource_id)
209
-
210
- # replacement (TODO: find out if we should handle this in the provider or outside of it)
211
- # delete old queue
212
- sqs.delete_queue(QueueUrl=request.previous_state["QueueUrl"])
213
- # create new queue (TODO: re-use create logic to make this more robust, e.g. for
214
- # auto-generated queue names)
215
- model["QueueUrl"] = sqs.create_queue(QueueName=queue_name)["QueueUrl"]
216
- model["Arn"] = sqs.get_queue_attributes(
217
- QueueUrl=model["QueueUrl"], AttributeNames=["QueueArn"]
218
- )["Attributes"]["QueueArn"]
211
+ (tags_to_remove, tags_to_add_or_update) = util.resource_tags_to_remove_or_update(
212
+ prev_model.get("Tags", []), model.get("Tags", [])
213
+ )
214
+ sqs.untag_queue(QueueUrl=queue_url, TagKeys=tags_to_remove)
215
+ sqs.tag_queue(QueueUrl=queue_url, Tags=tags_to_add_or_update)
216
+
217
+ model["QueueUrl"] = queue_url
218
+ model["Arn"] = request.previous_state["Arn"]
219
+
220
+ # For QueueName and FifoQueue, always use the value from the previous model. These fields
221
+ # are create-only, so they cannot be changed via an update (even though they might be omitted)
222
+ model["QueueName"] = prev_model.get("QueueName")
223
+ model["FifoQueue"] = prev_model.get("FifoQueue", False)
224
+
219
225
  return ProgressEvent(OperationStatus.SUCCESS, resource_model=model)
220
226
 
221
227
  def _compile_sqs_queue_attributes(self, properties: SQSQueueProperties) -> dict[str, str]:
@@ -250,6 +256,15 @@ class SQSQueueProvider(ResourceProvider[SQSQueueProperties]):
250
256
 
251
257
  return result
252
258
 
259
+ def _populate_missing_attributes_with_defaults(self, properties: SQSQueueProperties) -> None:
260
+ """
261
+ For any attribute that is missing from the desired state, populate it with the default value.
262
+ This is the only way to remove an attribute from an existing SQS queue's configuration.
263
+ :param properties: the properties passed from cloudformation
264
+ """
265
+ for k, v in self.DEFAULT_ATTRIBUTE_VALUES.items():
266
+ properties.setdefault(k, v)
267
+
253
268
  def list(
254
269
  self,
255
270
  request: ResourceRequest[SQSQueueProperties],
@@ -78,6 +78,7 @@ from localstack.aws.api.ssm import (
78
78
  )
79
79
  from localstack.aws.connect import connect_to
80
80
  from localstack.services.moto import call_moto, call_moto_with_request
81
+ from localstack.state import StateVisitor
81
82
  from localstack.utils.aws.arns import extract_resource_from_arn, is_arn
82
83
  from localstack.utils.bootstrap import is_api_enabled
83
84
  from localstack.utils.collections import remove_attributes
@@ -105,6 +106,11 @@ class InvalidParameterNameException(ValidationException):
105
106
 
106
107
  # TODO: check if _normalize_name(..) calls are still required here
107
108
  class SsmProvider(SsmApi, ABC):
109
+ def accept_state_visitor(self, visitor: StateVisitor):
110
+ from moto.ssm.models import ssm_backends
111
+
112
+ visitor.visit(ssm_backends)
113
+
108
114
  def get_parameters(
109
115
  self,
110
116
  context: RequestContext,
@@ -12,18 +12,35 @@ from botocore.model import (
12
12
 
13
13
  from localstack.aws.api.stepfunctions import (
14
14
  Definition,
15
+ InvalidDefinition,
15
16
  MockInput,
16
17
  MockResponseValidationMode,
17
18
  StateName,
19
+ TestStateInput,
18
20
  ValidationException,
19
21
  )
20
22
  from localstack.services.stepfunctions.asl.antlr.runtime.ASLParser import ASLParser
23
+ from localstack.services.stepfunctions.asl.component.state.state import CommonStateField
24
+ from localstack.services.stepfunctions.asl.component.state.state_execution.state_map.state_map import (
25
+ StateMap,
26
+ )
27
+ from localstack.services.stepfunctions.asl.component.state.state_execution.state_parallel.state_parallel import (
28
+ StateParallel,
29
+ )
21
30
  from localstack.services.stepfunctions.asl.component.state.state_execution.state_task.service.state_task_service import (
22
31
  StateTaskService,
23
32
  )
24
33
  from localstack.services.stepfunctions.asl.component.state.state_execution.state_task.service.state_task_service_api_gateway import (
25
34
  StateTaskServiceApiGateway,
26
35
  )
36
+ from localstack.services.stepfunctions.asl.component.state.state_execution.state_task.state_task import (
37
+ StateTask,
38
+ )
39
+ from localstack.services.stepfunctions.asl.component.state.state_fail.state_fail import StateFail
40
+ from localstack.services.stepfunctions.asl.component.state.state_pass.state_pass import StatePass
41
+ from localstack.services.stepfunctions.asl.component.state.state_succeed.state_succeed import (
42
+ StateSucceed,
43
+ )
27
44
  from localstack.services.stepfunctions.asl.component.state.state_type import StateType
28
45
  from localstack.services.stepfunctions.asl.component.test_state.program.test_state_program import (
29
46
  TestStateProgram,
@@ -59,10 +76,92 @@ class TestStateStaticAnalyser(StaticAnalyser):
59
76
  return test_program.test_state is not None
60
77
 
61
78
  @staticmethod
62
- def validate_mock(mock_input: MockInput, definition: Definition, state_name: StateName) -> None:
79
+ def validate_role_arn_required(
80
+ mock_input: MockInput, definition: Definition, state_name: StateName
81
+ ) -> None:
63
82
  test_program, _ = TestStateAmazonStateLanguageParser.parse(definition, state_name)
64
83
  test_state = test_program.test_state
84
+ if isinstance(test_state, StateTask) and mock_input is None:
85
+ raise ValidationException("RoleArn must be specified when testing a Task state")
86
+
87
+ @staticmethod
88
+ def validate_mock(test_state_input: TestStateInput) -> None:
89
+ test_program, _ = TestStateAmazonStateLanguageParser.parse(
90
+ test_state_input.get("definition"), test_state_input.get("stateName")
91
+ )
92
+ test_state = test_program.test_state
93
+ mock_input = test_state_input.get("mock")
94
+
95
+ TestStateStaticAnalyser.validate_test_state_allows_mocking(
96
+ mock_input=mock_input, test_state=test_state
97
+ )
98
+
99
+ if mock_input is None:
100
+ return
101
+
102
+ if test_state_input.get("revealSecrets"):
103
+ raise ValidationException(
104
+ "TestState does not support RevealSecrets when a mock is specified."
105
+ )
106
+
107
+ if {"result", "errorOutput"} <= mock_input.keys():
108
+ raise ValidationException(
109
+ "A test mock should have only one of the following fields: [result, errorOutput]."
110
+ )
111
+
112
+ mock_result_raw = mock_input.get("result")
113
+ if mock_result_raw is None:
114
+ return
115
+ try:
116
+ mock_result = json.loads(mock_result_raw)
117
+ except json.JSONDecodeError:
118
+ raise ValidationException("Mocked result must be valid JSON")
119
+
120
+ if isinstance(test_state, StateMap):
121
+ TestStateStaticAnalyser.validate_mock_result_matches_map_definition(
122
+ mock_result=mock_result, test_state=test_state
123
+ )
124
+
125
+ if isinstance(test_state, StateTaskService):
126
+ field_validation_mode = mock_input.get(
127
+ "fieldValidationMode", MockResponseValidationMode.STRICT
128
+ )
129
+ TestStateStaticAnalyser.validate_mock_result_matches_api_shape(
130
+ mock_result=mock_result,
131
+ field_validation_mode=field_validation_mode,
132
+ test_state=test_state,
133
+ )
65
134
 
135
+ @staticmethod
136
+ def validate_test_state_allows_mocking(
137
+ mock_input: MockInput, test_state: CommonStateField
138
+ ) -> None:
139
+ if mock_input is None and isinstance(test_state, (StateMap, StateParallel)):
140
+ # This is a literal message when a Map or Parallel state is not accompanied by a mock in a test state request.
141
+ # The message is the same for both cases and is not parametrised anyhow.
142
+ raise InvalidDefinition(
143
+ "TestState API does not support Map or Parallel states. Supported state types include: [Task, Wait, Pass, Succeed, Fail, Choice]"
144
+ )
145
+
146
+ if mock_input is not None and isinstance(test_state, (StatePass, StateFail, StateSucceed)):
147
+ raise ValidationException(
148
+ f"State type '{test_state.state_type.name}' is not supported when a mock is specified"
149
+ )
150
+
151
+ @staticmethod
152
+ def validate_mock_result_matches_map_definition(mock_result: Any, test_state: StateMap):
153
+ if test_state.result_writer is not None and not isinstance(mock_result, dict):
154
+ raise ValidationException("Mocked result must be a JSON object.")
155
+
156
+ if test_state.result_writer is None and not isinstance(mock_result, list):
157
+ raise ValidationException("Mocked result must be an array.")
158
+
159
+ @staticmethod
160
+ def validate_mock_result_matches_api_shape(
161
+ mock_result: Any,
162
+ field_validation_mode: MockResponseValidationMode,
163
+ test_state: StateTaskService,
164
+ ):
66
165
  # apigateway:invoke: has no equivalent in the AWS SDK service integration.
67
166
  # Hence, the validation against botocore doesn't apply.
68
167
  # See the note in https://docs.aws.amazon.com/step-functions/latest/dg/connect-api-gateway.html
@@ -70,119 +169,106 @@ class TestStateStaticAnalyser(StaticAnalyser):
70
169
  if isinstance(test_state, StateTaskServiceApiGateway):
71
170
  return
72
171
 
73
- if isinstance(test_state, StateTaskService):
74
- field_validation_mode = mock_input.get(
75
- "fieldValidationMode", MockResponseValidationMode.STRICT
172
+ if field_validation_mode == MockResponseValidationMode.NONE:
173
+ return
174
+
175
+ boto_service_name = test_state._get_boto_service_name()
176
+ service_action_name = test_state._get_boto_service_action()
177
+ output_shape = test_state._get_boto_operation_model(
178
+ boto_service_name=boto_service_name, service_action_name=service_action_name
179
+ ).output_shape
180
+
181
+ # If the operation has no output, there's nothing to validate
182
+ if output_shape is None:
183
+ return
184
+
185
+ def _raise_type_error(expected_type: str, field_name: str) -> None:
186
+ raise ValidationException(
187
+ f"Mock result schema validation error: Field '{field_name}' must be {expected_type}"
76
188
  )
77
- mock_result_raw = mock_input.get("result")
78
- if mock_result_raw is None:
189
+
190
+ def _validate_value(value: Any, shape: Shape, field_name: str | None = None) -> None:
191
+ # Document type accepts any JSON value
192
+ if shape.type_name == "document":
79
193
  return
80
- try:
81
- mock_result = json.loads(mock_result_raw)
82
- except json.JSONDecodeError:
83
- raise ValidationException("Mocked result must be valid JSON")
84
- if mock_result is None or field_validation_mode == MockResponseValidationMode.NONE:
194
+
195
+ if isinstance(shape, StructureShape):
196
+ if not isinstance(value, dict):
197
+ # this is a defensive check, the mock result is loaded from JSON before, so should always be a dict
198
+ raise ValidationException(
199
+ f"Mock result must be a valid JSON object, but got '{type(value)}' instead"
200
+ )
201
+ # Build a mapping from SFN-normalised member keys -> botocore member shapes
202
+ members = shape.members
203
+ sfn_key_to_member_shape: dict[str, Shape] = {
204
+ StateTaskService._to_sfn_cased(member_key): member_shape
205
+ for member_key, member_shape in members.items()
206
+ }
207
+ if field_validation_mode == MockResponseValidationMode.STRICT:
208
+ # Ensure required members are present, using SFN-normalised keys
209
+ for required_key in shape.required_members:
210
+ sfn_required_key = StateTaskService._to_sfn_cased(required_key)
211
+ if sfn_required_key not in value:
212
+ raise ValidationException(
213
+ f"Mock result schema validation error: Required field '{sfn_required_key}' is missing"
214
+ )
215
+ # Validate present fields (match SFN-normalised keys to member shapes)
216
+ for mock_field_name, mock_field_value in value.items():
217
+ member_shape = sfn_key_to_member_shape.get(mock_field_name)
218
+ if member_shape is None:
219
+ # Fields that are present in mock but are not in the API spec should not raise validation errors - forward compatibility
220
+ continue
221
+ _validate_value(mock_field_value, member_shape, mock_field_name)
85
222
  return
86
223
 
87
- boto_service_name = test_state._get_boto_service_name()
88
- service_action_name = test_state._get_boto_service_action()
89
- output_shape = test_state._get_boto_operation_model(
90
- boto_service_name=boto_service_name, service_action_name=service_action_name
91
- ).output_shape
224
+ if isinstance(shape, ListShape):
225
+ if not isinstance(value, list):
226
+ _raise_type_error("an array", field_name)
227
+ member_shape = shape.member
228
+ for list_item in value:
229
+ _validate_value(list_item, member_shape, field_name)
230
+ return
92
231
 
93
- # If the operation has no output, there's nothing to validate
94
- if output_shape is None:
232
+ if isinstance(shape, MapShape):
233
+ if not isinstance(value, dict):
234
+ _raise_type_error("an object", field_name)
235
+ value_shape = shape.value
236
+ for _, map_item_value in value.items():
237
+ _validate_value(map_item_value, value_shape, field_name)
95
238
  return
96
239
 
97
- def _raise_type_error(expected_type: str, field_name: str) -> None:
98
- raise ValidationException(
99
- f"Mock result schema validation error: Field '{field_name}' must be {expected_type}"
100
- )
101
-
102
- def _validate_value(value: Any, shape: Shape, field_name: str | None = None) -> None:
103
- # Document type accepts any JSON value
104
- if shape.type_name == "document":
105
- return
106
-
107
- if isinstance(shape, StructureShape):
108
- if not isinstance(value, dict):
109
- # this is a defensive check, the mock result is loaded from JSON before, so should always be a dict
110
- raise ValidationException(
111
- f"Mock result must be a valid JSON object, but got '{type(value)}' instead"
112
- )
113
- # Build a mapping from SFN-normalised member keys -> botocore member shapes
114
- members = shape.members
115
- sfn_key_to_member_shape: dict[str, Shape] = {
116
- StateTaskService._to_sfn_cased(member_key): member_shape
117
- for member_key, member_shape in members.items()
118
- }
119
- if field_validation_mode == MockResponseValidationMode.STRICT:
120
- # Ensure required members are present, using SFN-normalised keys
121
- for required_key in shape.required_members:
122
- sfn_required_key = StateTaskService._to_sfn_cased(required_key)
123
- if sfn_required_key not in value:
124
- raise ValidationException(
125
- f"Mock result schema validation error: Required field '{sfn_required_key}' is missing"
126
- )
127
- # Validate present fields (match SFN-normalised keys to member shapes)
128
- for mock_field_name, mock_field_value in value.items():
129
- member_shape = sfn_key_to_member_shape.get(mock_field_name)
130
- if member_shape is None:
131
- # Fields that are present in mock but are not in the API spec should not raise validation errors - forward compatibility
132
- continue
133
- _validate_value(mock_field_value, member_shape, mock_field_name)
134
- return
135
-
136
- if isinstance(shape, ListShape):
137
- if not isinstance(value, list):
138
- _raise_type_error("an array", field_name)
139
- member_shape = shape.member
140
- for list_item in value:
141
- _validate_value(list_item, member_shape, field_name)
142
- return
143
-
144
- if isinstance(shape, MapShape):
145
- if not isinstance(value, dict):
146
- _raise_type_error("an object", field_name)
147
- value_shape = shape.value
148
- for _, map_item_value in value.items():
149
- _validate_value(map_item_value, value_shape, field_name)
150
- return
151
-
152
- # Primitive shapes and others
153
- type_name = shape.type_name
154
- match type_name:
155
- case "string" | "timestamp":
156
- if not isinstance(value, str):
157
- _raise_type_error("a string", field_name)
158
- # Validate enum if present
159
- if isinstance(shape, StringShape):
160
- enum = getattr(shape, "enum", None)
161
- if enum and value not in enum:
162
- raise ValidationException(
163
- f"Mock result schema validation error: Field '{field_name}' is not an expected value"
164
- )
165
-
166
- case "integer" | "long":
167
- if not isinstance(value, int) or isinstance(value, bool):
168
- _raise_type_error("a number", field_name)
169
-
170
- case "float" | "double":
171
- if not (isinstance(value, (int, float)) or isinstance(value, bool)):
172
- _raise_type_error("a number", field_name)
173
-
174
- case "boolean":
175
- if not isinstance(value, bool):
176
- _raise_type_error("a boolean", field_name)
177
-
178
- case "blob":
179
- if not (isinstance(value, (str, bytes))):
180
- _raise_type_error("a string", field_name)
181
-
182
- # Perform validation against the output shape
183
- _validate_value(mock_result, output_shape)
184
- # Non-service tasks or other cases: nothing to validate
185
- return
240
+ # Primitive shapes and others
241
+ type_name = shape.type_name
242
+ match type_name:
243
+ case "string" | "timestamp":
244
+ if not isinstance(value, str):
245
+ _raise_type_error("a string", field_name)
246
+ # Validate enum if present
247
+ if isinstance(shape, StringShape):
248
+ enum = getattr(shape, "enum", None)
249
+ if enum and value not in enum:
250
+ raise ValidationException(
251
+ f"Mock result schema validation error: Field '{field_name}' is not an expected value"
252
+ )
253
+
254
+ case "integer" | "long":
255
+ if not isinstance(value, int) or isinstance(value, bool):
256
+ _raise_type_error("a number", field_name)
257
+
258
+ case "float" | "double":
259
+ if not (isinstance(value, (int, float)) or isinstance(value, bool)):
260
+ _raise_type_error("a number", field_name)
261
+
262
+ case "boolean":
263
+ if not isinstance(value, bool):
264
+ _raise_type_error("a boolean", field_name)
265
+
266
+ case "blob":
267
+ if not (isinstance(value, (str, bytes))):
268
+ _raise_type_error("a string", field_name)
269
+
270
+ # Perform validation against the output shape
271
+ _validate_value(mock_result, output_shape)
186
272
 
187
273
  def analyse(self, definition: str) -> None:
188
274
  _, parser_rule_context = TestStateAmazonStateLanguageParser.parse(
@@ -1,9 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import datetime
4
- import json
5
4
  import logging
6
- from typing import Final
5
+ from typing import Any, Final
7
6
 
8
7
  from localstack.aws.api.events import PutEventsRequestEntry
9
8
  from localstack.aws.api.stepfunctions import (
@@ -111,7 +110,7 @@ class Execution:
111
110
  local_mock_test_case: Final[LocalMockTestCase | None]
112
111
 
113
112
  start_date: Final[Timestamp]
114
- input_data: Final[json | None]
113
+ input_data: Final[dict[str, Any] | None]
115
114
  input_details: Final[CloudWatchEventsExecutionDataDetails | None]
116
115
  trace_header: Final[TraceHeader | None]
117
116
  _cloud_watch_logging_session: Final[CloudWatchLoggingSession | None]
@@ -119,7 +118,7 @@ class Execution:
119
118
  exec_status: ExecutionStatus | None
120
119
  stop_date: Timestamp | None
121
120
 
122
- output: json | None
121
+ output: dict[str, Any] | None
123
122
  output_details: CloudWatchEventsExecutionDataDetails | None
124
123
 
125
124
  error: SensitiveError | None
@@ -141,7 +140,7 @@ class Execution:
141
140
  start_date: Timestamp,
142
141
  cloud_watch_logging_session: CloudWatchLoggingSession | None,
143
142
  activity_store: dict[Arn, Activity],
144
- input_data: json | None = None,
143
+ input_data: dict[str, Any] | None = None,
145
144
  trace_header: TraceHeader | None = None,
146
145
  state_machine_alias_arn: Arn | None = None,
147
146
  local_mock_test_case: LocalMockTestCase | None = None,
@@ -57,7 +57,6 @@ from localstack.aws.api.stepfunctions import (
57
57
  LongArn,
58
58
  MaxConcurrency,
59
59
  MissingRequiredParameter,
60
- MockInput,
61
60
  Name,
62
61
  PageSize,
63
62
  PageToken,
@@ -162,6 +161,7 @@ from localstack.services.stepfunctions.stepfunctions_utils import (
162
161
  normalise_max_results,
163
162
  )
164
163
  from localstack.state import StateVisitor
164
+ from localstack.utils.aws import arns
165
165
  from localstack.utils.aws.arns import (
166
166
  ARN_PARTITION_REGEX,
167
167
  stepfunctions_activity_arn,
@@ -242,12 +242,6 @@ class StepFunctionsProvider(StepfunctionsApi, ServiceLifecycleHook):
242
242
  f"Invalid Arn: 'Resource type not valid in this context: {lower_resource_type}'"
243
243
  )
244
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
-
251
245
  @staticmethod
252
246
  def _validate_activity_name(name: str) -> None:
253
247
  # The activity name is validated according to the AWS StepFunctions documentation, the name should not contain:
@@ -1514,11 +1508,7 @@ class StepFunctionsProvider(StepfunctionsApi, ServiceLifecycleHook):
1514
1508
  raise ValidationException("State not found in definition")
1515
1509
 
1516
1510
  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
- )
1511
+ TestStateStaticAnalyser.validate_mock(test_state_input=request)
1522
1512
 
1523
1513
  if state_configuration := request.get("stateConfiguration"):
1524
1514
  # TODO: Add validations for this i.e assert len(input) <= failureCount
@@ -1543,10 +1533,27 @@ class StepFunctionsProvider(StepfunctionsApi, ServiceLifecycleHook):
1543
1533
  arn = stepfunctions_state_machine_arn(
1544
1534
  name=name, account_id=context.account_id, region_name=context.region
1545
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
+
1546
1553
  state_machine = TestStateMachine(
1547
1554
  name=name,
1548
1555
  arn=arn,
1549
- role_arn=request["roleArn"],
1556
+ role_arn=role_arn,
1550
1557
  definition=request["definition"],
1551
1558
  )
1552
1559
 
@@ -1561,7 +1568,7 @@ class StepFunctionsProvider(StepfunctionsApi, ServiceLifecycleHook):
1561
1568
 
1562
1569
  execution = TestStateExecution(
1563
1570
  name=exec_name,
1564
- role_arn=request["roleArn"],
1571
+ role_arn=role_arn,
1565
1572
  exec_arn=exec_arn,
1566
1573
  account_id=context.account_id,
1567
1574
  region_name=context.region,
@@ -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"]: