diff --git a/localstack-core/localstack/services/cloudformation/provider_utils.py b/localstack-core/localstack/services/cloudformation/provider_utils.py index c87238a9ae86d..b48ce0b36f20a 100644 --- a/localstack-core/localstack/services/cloudformation/provider_utils.py +++ b/localstack-core/localstack/services/cloudformation/provider_utils.py @@ -275,6 +275,26 @@ def recursive_convert(obj): return recursive_convert(input_dict) +def resource_tags_to_remove_or_update( + prev_tags: list[dict], new_tags: list[dict] +) -> tuple[list[str], dict[str, str]]: + """ + When updating resources that have tags, we need to determine which tags to remove and which to add/update, + as these are typically done in separate API calls. The format of prev_tags and new_tags is expected to + be [{ "Key": tagName, "Value": tagValue }, ...]. The return value will be a tuple of (tags_to_remove, tags_to_update), + where: + - tags_to_remove is a list of tag keys that are present in prev_tags but not in new_tags. + - tags_to_update is a dict of tags to add or update, with the format: { tagName: tagValue, ... }. + """ + prev_tag_keys = [tag["Key"] for tag in prev_tags] + new_tag_keys = [tag["Key"] for tag in new_tags] + tags_to_remove = list(set(prev_tag_keys) - set(new_tag_keys)) + + # convert from list of dicts, to a single dict because that's what tag_queue APIs expect. + tags_to_update = {tag["Key"]: tag["Value"] for tag in new_tags} + return (tags_to_remove, tags_to_update) + + # LocalStack specific utilities def get_schema_path(file_path: Path) -> dict: file_name_base = file_path.name.removesuffix(".py").removesuffix(".py.enc") diff --git a/localstack-core/localstack/services/sqs/constants.py b/localstack-core/localstack/services/sqs/constants.py index 83662a9262e79..8d3583d5f7520 100644 --- a/localstack-core/localstack/services/sqs/constants.py +++ b/localstack-core/localstack/services/sqs/constants.py @@ -31,6 +31,12 @@ QueueAttributeName.QueueArn, ] +# +# If these attributes are set to their default values, they are effectively +# deleted from the queue attributes and not returned in future calls to get_queue_attributes() +# +DELETE_IF_DEFAULT = {"KmsMasterKeyId": "", "KmsDataKeyReusePeriodSeconds": "300"} + INVALID_STANDARD_QUEUE_ATTRIBUTES = [ QueueAttributeName.FifoQueue, QueueAttributeName.ContentBasedDeduplication, diff --git a/localstack-core/localstack/services/sqs/provider.py b/localstack-core/localstack/services/sqs/provider.py index 9df6cb724d265..ee19a23428daf 100644 --- a/localstack-core/localstack/services/sqs/provider.py +++ b/localstack-core/localstack/services/sqs/provider.py @@ -1273,7 +1273,11 @@ def set_queue_attributes( for k, v in attributes.items(): if k in sqs_constants.INTERNAL_QUEUE_ATTRIBUTES: raise InvalidAttributeName(f"Unknown Attribute {k}.") - queue.attributes[k] = v + if k in sqs_constants.DELETE_IF_DEFAULT and v == sqs_constants.DELETE_IF_DEFAULT[k]: + if k in queue.attributes: + del queue.attributes[k] + else: + queue.attributes[k] = v # Special cases if queue.attributes.get(QueueAttributeName.Policy) == "": diff --git a/localstack-core/localstack/services/sqs/resource_providers/aws_sqs_queue.py b/localstack-core/localstack/services/sqs/resource_providers/aws_sqs_queue.py index 861449ed53bc5..884077599a0b6 100644 --- a/localstack-core/localstack/services/sqs/resource_providers/aws_sqs_queue.py +++ b/localstack-core/localstack/services/sqs/resource_providers/aws_sqs_queue.py @@ -64,6 +64,35 @@ class SQSQueueProvider(ResourceProvider[SQSQueueProperties]): TYPE = "AWS::SQS::Queue" # Autogenerated. Don't change SCHEMA = util.get_schema_path(Path(__file__)) # Autogenerated. Don't change + # Values used when a property is removed from a template and needs to be set to its default. + # If AWS changes their defaults in the future, our parity tests should break. + DEFAULT_ATTRIBUTE_VALUES = { + "ReceiveMessageWaitTimeSeconds": "0", + "DelaySeconds": "0", + "KmsMasterKeyId": "", + "RedrivePolicy": "", + "MessageRetentionPeriod": "345600", + "MaximumMessageSize": "262144", # Note: CloudFormation sets this to 256KB on update, but 1MB on create + "VisibilityTimeout": "30", + "KmsDataKeyReusePeriodSeconds": "300", + } + + # Private method for creating a unique queue name, if none is specified. + def _autogenerated_queue_name(self, request: ResourceRequest[SQSQueueProperties]) -> str: + queue_name = util.generate_default_name(request.stack_name, request.logical_resource_id) + isFifoQueue = request.desired_state.get("FifoQueue") + + # Note that it's an SQS FIFO queue only if the FifoQueue property is set to boolean True, or the string "true" + # (case insensitive). If it's None (property was omitted) or False, or any type of string (e.g. a typo + # such as "Fasle"), then it's not a FIFO queue. This extra check is needed because the CloudFormation engine + # doesn't fully validate the FifoQueue property before passing it to the resource provider. + if ( + isFifoQueue == True # noqa: E712 + or (isinstance(isFifoQueue, str) and isFifoQueue.lower() == "true") + ): + queue_name = f"{queue_name[:-5]}.fifo" + return queue_name + def create( self, request: ResourceRequest[SQSQueueProperties], @@ -74,8 +103,6 @@ def create( Primary identifier fields: - /properties/QueueUrl - - Create-only properties: - /properties/FifoQueue - /properties/QueueName @@ -92,26 +119,13 @@ def create( - sqs:TagQueue """ - # TODO: validations + # TODO: validations - what validations are needed? model = request.desired_state sqs = request.aws_client_factory.sqs - if model.get("FifoQueue", False): - model["FifoQueue"] = model["FifoQueue"] - - queue_name = model.get("QueueName") - if not queue_name: - # TODO: verify patterns here - if model.get("FifoQueue"): - queue_name = util.generate_default_name( - request.stack_name, request.logical_resource_id - )[:-5] - queue_name = f"{queue_name}.fifo" - else: - queue_name = util.generate_default_name( - request.stack_name, request.logical_resource_id - ) - model["QueueName"] = queue_name + # if no QueueName is specified, automatically generate one + if not model.get("QueueName"): + model["QueueName"] = self._autogenerated_queue_name(request) attributes = self._compile_sqs_queue_attributes(model) result = request.aws_client_factory.sqs.create_queue( @@ -184,38 +198,30 @@ def update( """ sqs = request.aws_client_factory.sqs model = request.desired_state + prev_model = request.previous_state assert request.previous_state is not None - should_replace = ( - request.desired_state.get("QueueName", request.previous_state["QueueName"]) - != request.previous_state["QueueName"] - ) or ( - request.desired_state.get("FifoQueue", request.previous_state.get("FifoQueue")) - != request.previous_state.get("FifoQueue") + queue_url = prev_model["QueueUrl"] + self._populate_missing_attributes_with_defaults(model) + sqs.set_queue_attributes( + QueueUrl=queue_url, Attributes=self._compile_sqs_queue_attributes(model) ) - if not should_replace: - return ProgressEvent(OperationStatus.SUCCESS, resource_model=request.previous_state) - - # TODO: copied from the create handler, extract? - if model.get("FifoQueue"): - queue_name = util.generate_default_name( - request.stack_name, request.logical_resource_id - )[:-5] - queue_name = f"{queue_name}.fifo" - else: - queue_name = util.generate_default_name(request.stack_name, request.logical_resource_id) - - # replacement (TODO: find out if we should handle this in the provider or outside of it) - # delete old queue - sqs.delete_queue(QueueUrl=request.previous_state["QueueUrl"]) - # create new queue (TODO: re-use create logic to make this more robust, e.g. for - # auto-generated queue names) - model["QueueUrl"] = sqs.create_queue(QueueName=queue_name)["QueueUrl"] - model["Arn"] = sqs.get_queue_attributes( - QueueUrl=model["QueueUrl"], AttributeNames=["QueueArn"] - )["Attributes"]["QueueArn"] + (tags_to_remove, tags_to_add_or_update) = util.resource_tags_to_remove_or_update( + prev_model.get("Tags", []), model.get("Tags", []) + ) + sqs.untag_queue(QueueUrl=queue_url, TagKeys=tags_to_remove) + sqs.tag_queue(QueueUrl=queue_url, Tags=tags_to_add_or_update) + + model["QueueUrl"] = queue_url + model["Arn"] = request.previous_state["Arn"] + + # For QueueName and FifoQueue, always use the value from the previous model. These fields + # are create-only, so they cannot be changed via an update (even though they might be omitted) + model["QueueName"] = prev_model.get("QueueName") + model["FifoQueue"] = prev_model.get("FifoQueue", False) + return ProgressEvent(OperationStatus.SUCCESS, resource_model=model) def _compile_sqs_queue_attributes(self, properties: SQSQueueProperties) -> dict[str, str]: @@ -250,6 +256,15 @@ def _compile_sqs_queue_attributes(self, properties: SQSQueueProperties) -> dict[ return result + def _populate_missing_attributes_with_defaults(self, properties: SQSQueueProperties) -> None: + """ + For any attribute that is missing from the desired state, populate it with the default value. + This is the only way to remove an attribute from an existing SQS queue's configuration. + :param properties: the properties passed from cloudformation + """ + for k, v in self.DEFAULT_ATTRIBUTE_VALUES.items(): + properties.setdefault(k, v) + def list( self, request: ResourceRequest[SQSQueueProperties], diff --git a/tests/aws/services/cloudformation/api/test_stacks.py b/tests/aws/services/cloudformation/api/test_stacks.py index becc58e9d3241..bb39d16db8414 100644 --- a/tests/aws/services/cloudformation/api/test_stacks.py +++ b/tests/aws/services/cloudformation/api/test_stacks.py @@ -340,6 +340,7 @@ def test_update_stack_with_same_template_withoutchange_transformation( ) @markers.aws.validated + @skip_if_legacy_engine() def test_update_stack_actual_update(self, deploy_cfn_template, aws_client): template = load_file( os.path.join(os.path.dirname(__file__), "../../../templates/sqs_queue_update.yml") diff --git a/tests/aws/services/cloudformation/resources/test_sqs.py b/tests/aws/services/cloudformation/resources/test_sqs.py deleted file mode 100644 index d054c3f82a55d..0000000000000 --- a/tests/aws/services/cloudformation/resources/test_sqs.py +++ /dev/null @@ -1,143 +0,0 @@ -import os - -import pytest -from botocore.exceptions import ClientError - -from localstack.testing.pytest import markers -from localstack.utils.strings import short_uid -from localstack.utils.sync import wait_until - - -@markers.aws.validated -def test_sqs_queue_policy(deploy_cfn_template, aws_client, snapshot): - result = deploy_cfn_template( - template_path=os.path.join( - os.path.dirname(__file__), "../../../templates/sqs_with_queuepolicy.yaml" - ) - ) - queue_url = result.outputs["QueueUrlOutput"] - resp = aws_client.sqs.get_queue_attributes(QueueUrl=queue_url, AttributeNames=["Policy"]) - snapshot.match("policy", resp) - snapshot.add_transformer(snapshot.transform.key_value("Resource")) - - -@markers.aws.validated -def test_sqs_fifo_queue_generates_valid_name(deploy_cfn_template): - result = deploy_cfn_template( - template_path=os.path.join( - os.path.dirname(__file__), "../../../templates/sqs_fifo_autogenerate_name.yaml" - ), - parameters={"IsFifo": "true"}, - max_wait=240, - ) - assert ".fifo" in result.outputs["FooQueueName"] - - -@markers.aws.validated -def test_sqs_non_fifo_queue_generates_valid_name(deploy_cfn_template): - result = deploy_cfn_template( - template_path=os.path.join( - os.path.dirname(__file__), "../../../templates/sqs_fifo_autogenerate_name.yaml" - ), - parameters={"IsFifo": "false"}, - max_wait=240, - ) - assert ".fifo" not in result.outputs["FooQueueName"] - - -@markers.aws.validated -def test_cfn_handle_sqs_resource(deploy_cfn_template, aws_client, snapshot): - queue_name = f"queue-{short_uid()}" - - stack = deploy_cfn_template( - template_path=os.path.join( - os.path.dirname(__file__), "../../../templates/sqs_fifo_queue.yml" - ), - parameters={"QueueName": queue_name}, - ) - - rs = aws_client.sqs.get_queue_attributes( - QueueUrl=stack.outputs["QueueURL"], AttributeNames=["All"] - ) - snapshot.match("queue", rs) - snapshot.add_transformer(snapshot.transform.regex(queue_name, "")) - - # clean up - stack.destroy() - - with pytest.raises(ClientError) as ctx: - aws_client.sqs.get_queue_url(QueueName=f"{queue_name}.fifo") - snapshot.match("error", ctx.value.response) - - -@markers.aws.validated -def test_update_queue_no_change(deploy_cfn_template, aws_client, snapshot): - bucket_name = f"bucket-{short_uid()}" - - stack = deploy_cfn_template( - template_path=os.path.join( - os.path.dirname(__file__), "../../../templates/sqs_queue_update_no_change.yml" - ), - parameters={ - "AddBucket": "false", - "BucketName": bucket_name, - }, - ) - queue_url = stack.outputs["QueueUrl"] - queue_arn = stack.outputs["QueueArn"] - snapshot.add_transformer(snapshot.transform.regex(queue_url, "")) - snapshot.add_transformer(snapshot.transform.regex(queue_arn, "")) - - snapshot.match("outputs-1", stack.outputs) - - # deploy a second time with no change to the SQS queue - updated_stack = deploy_cfn_template( - template_path=os.path.join( - os.path.dirname(__file__), "../../../templates/sqs_queue_update_no_change.yml" - ), - is_update=True, - stack_name=stack.stack_name, - parameters={ - "AddBucket": "true", - "BucketName": bucket_name, - }, - ) - snapshot.match("outputs-2", updated_stack.outputs) - - -@markers.aws.validated -def test_update_sqs_queuepolicy(deploy_cfn_template, aws_client, snapshot): - stack = deploy_cfn_template( - template_path=os.path.join( - os.path.dirname(__file__), "../../../templates/sqs_with_queuepolicy.yaml" - ) - ) - - policy = aws_client.sqs.get_queue_attributes( - QueueUrl=stack.outputs["QueueUrlOutput"], AttributeNames=["Policy"] - ) - snapshot.match("policy1", policy["Attributes"]["Policy"]) - - updated_stack = deploy_cfn_template( - template_path=os.path.join( - os.path.dirname(__file__), "../../../templates/sqs_with_queuepolicy_updated.yaml" - ), - is_update=True, - stack_name=stack.stack_name, - ) - - def check_policy_updated(): - policy_updated = aws_client.sqs.get_queue_attributes( - QueueUrl=updated_stack.outputs["QueueUrlOutput"], AttributeNames=["Policy"] - ) - assert policy_updated["Attributes"]["Policy"] != policy["Attributes"]["Policy"] - return policy_updated - - wait_until(check_policy_updated) - - policy = aws_client.sqs.get_queue_attributes( - QueueUrl=updated_stack.outputs["QueueUrlOutput"], AttributeNames=["Policy"] - ) - - snapshot.match("policy2", policy["Attributes"]["Policy"]) - snapshot.add_transformer(snapshot.transform.cloudformation_api()) diff --git a/tests/aws/services/cloudformation/resources/test_sqs.snapshot.json b/tests/aws/services/cloudformation/resources/test_sqs.snapshot.json deleted file mode 100644 index 9f26c7054cc7c..0000000000000 --- a/tests/aws/services/cloudformation/resources/test_sqs.snapshot.json +++ /dev/null @@ -1,119 +0,0 @@ -{ - "tests/aws/services/cloudformation/resources/test_sqs.py::test_update_queue_no_change": { - "recorded-date": "08-12-2023, 21:11:26", - "recorded-content": { - "outputs-1": { - "QueueArn": "", - "QueueUrl": "" - }, - "outputs-2": { - "QueueArn": "", - "QueueUrl": "" - } - } - }, - "tests/aws/services/cloudformation/resources/test_sqs.py::test_update_sqs_queuepolicy": { - "recorded-date": "27-03-2024, 20:30:24", - "recorded-content": { - "policy1": { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Principal": "*", - "Action": [ - "sqs:SendMessage", - "sqs:GetQueueAttributes", - "sqs:GetQueueUrl" - ], - "Resource": "arn::sqs::111111111111:" - } - ] - }, - "policy2": { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Deny", - "Principal": "*", - "Action": [ - "sqs:SendMessage", - "sqs:GetQueueAttributes", - "sqs:GetQueueUrl" - ], - "Resource": "arn::sqs::111111111111:" - } - ] - } - } - }, - "tests/aws/services/cloudformation/resources/test_sqs.py::test_sqs_queue_policy": { - "recorded-date": "03-07-2024, 19:49:04", - "recorded-content": { - "policy": { - "Attributes": { - "Policy": { - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Principal": "*", - "Action": [ - "sqs:SendMessage", - "sqs:GetQueueAttributes", - "sqs:GetQueueUrl" - ], - "Resource": "" - } - ] - } - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/cloudformation/resources/test_sqs.py::test_cfn_handle_sqs_resource": { - "recorded-date": "27-08-2025, 09:37:44", - "recorded-content": { - "queue": { - "Attributes": { - "ApproximateNumberOfMessages": "0", - "ApproximateNumberOfMessagesDelayed": "0", - "ApproximateNumberOfMessagesNotVisible": "0", - "ContentBasedDeduplication": "false", - "CreatedTimestamp": "timestamp", - "DeduplicationScope": "queue", - "DelaySeconds": "0", - "FifoQueue": "true", - "FifoThroughputLimit": "perQueue", - "LastModifiedTimestamp": "timestamp", - "MaximumMessageSize": "1048576", - "MessageRetentionPeriod": "345600", - "QueueArn": "arn::sqs::111111111111:.fifo", - "ReceiveMessageWaitTimeSeconds": "0", - "SqsManagedSseEnabled": "true", - "VisibilityTimeout": "30" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "error": { - "Error": { - "Code": "AWS.SimpleQueueService.NonExistentQueue", - "Message": "The specified queue does not exist.", - "QueryErrorCode": "QueueDoesNotExist", - "Type": "Sender" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 400 - } - } - } - } -} diff --git a/tests/aws/services/cloudformation/resources/test_sqs.validation.json b/tests/aws/services/cloudformation/resources/test_sqs.validation.json deleted file mode 100644 index 1b9bdb2d7d36b..0000000000000 --- a/tests/aws/services/cloudformation/resources/test_sqs.validation.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "tests/aws/services/cloudformation/resources/test_sqs.py::test_cfn_handle_sqs_resource": { - "last_validated_date": "2025-08-27T09:37:44+00:00", - "durations_in_seconds": { - "setup": 0.82, - "call": 74.96, - "teardown": 0.11, - "total": 75.89 - } - }, - "tests/aws/services/cloudformation/resources/test_sqs.py::test_sqs_fifo_queue_generates_valid_name": { - "last_validated_date": "2024-05-15T02:01:00+00:00" - }, - "tests/aws/services/cloudformation/resources/test_sqs.py::test_sqs_non_fifo_queue_generates_valid_name": { - "last_validated_date": "2024-05-15T01:59:34+00:00" - }, - "tests/aws/services/cloudformation/resources/test_sqs.py::test_sqs_queue_policy": { - "last_validated_date": "2024-07-03T19:49:04+00:00" - }, - "tests/aws/services/cloudformation/resources/test_sqs.py::test_update_queue_no_change": { - "last_validated_date": "2023-12-08T20:11:26+00:00" - }, - "tests/aws/services/cloudformation/resources/test_sqs.py::test_update_sqs_queuepolicy": { - "last_validated_date": "2024-03-27T20:30:23+00:00" - } -} diff --git a/tests/aws/services/cloudformation/test_change_sets.py b/tests/aws/services/cloudformation/test_change_sets.py index b49ebd6b4c68b..9a2ed1d89f22a 100644 --- a/tests/aws/services/cloudformation/test_change_sets.py +++ b/tests/aws/services/cloudformation/test_change_sets.py @@ -782,15 +782,7 @@ def test_single_resource_static_update( snapshot.match("parameter-2", parameter) @markers.aws.validated - @markers.snapshot.skip_snapshot_verify( - paths=[ - "$..PhysicalResourceId", - # TODO: parity in the resource provider - "$..Attributes.MaximumMessageSize", - "$..Attributes.MessageRetentionPeriod", - "$..Attributes.ReceiveMessageWaitTimeSeconds", - ] - ) + @markers.snapshot.skip_snapshot_verify(paths=["$..PhysicalResourceId"]) def test_queue_update( self, aws_client, deploy_cfn_template, capture_per_resource_events, snapshot ): diff --git a/tests/aws/services/sqs/resource_providers/templates/sqs_fifo_queue_all_properties.yml b/tests/aws/services/sqs/resource_providers/templates/sqs_fifo_queue_all_properties.yml new file mode 100644 index 0000000000000..7367bfc9d9225 --- /dev/null +++ b/tests/aws/services/sqs/resource_providers/templates/sqs_fifo_queue_all_properties.yml @@ -0,0 +1,49 @@ +# +# Define an SQS FIFO Queue with all possible properties set to valid values. +# +Resources: + FifoQueue: + Type: 'AWS::SQS::Queue' + Properties: + QueueName: 'MyFifoQueueWithAllProperties.fifo' + FifoQueue: true + ContentBasedDeduplication: true + DeduplicationScope: 'messageGroup' + FifoThroughputLimit: 'perMessageGroupId' + DelaySeconds: 13 + MaximumMessageSize: 3232 + MessageRetentionPeriod: 13425 + ReceiveMessageWaitTimeSeconds: 15 + VisibilityTimeout: 42 + RedrivePolicy: + deadLetterTargetArn: !GetAtt DeadLetterQueue.Arn + maxReceiveCount: 7 + SqsManagedSseEnabled: false + KmsMasterKeyId: 'alias/aws/sqs' + KmsDataKeyReusePeriodSeconds: 200 + Tags: + - Key: 'Environment' + Value: 'Production' + - Key: 'Department' + Value: 'Finance' + + DeadLetterQueue: + Type: 'AWS::SQS::Queue' + Properties: + FifoQueue: true + RedriveAllowPolicy: + redrivePermission: 'allowAll' + +Outputs: + QueueUrl: + Description: 'URL of the SQS Fifo Queue' + Value: !Ref FifoQueue + QueueArn: + Description: 'ARN of the SQS Fifo Queue' + Value: !GetAtt FifoQueue.Arn + DeadLetterQueueUrl: + Description: 'URL of the Dead Letter Queue' + Value: !Ref DeadLetterQueue + DeadLetterQueueArn: + Description: 'ARN of the Dead Letter Queue' + Value: !GetAtt DeadLetterQueue.Arn diff --git a/tests/aws/services/sqs/resource_providers/templates/sqs_fifo_queue_required_properties.yml b/tests/aws/services/sqs/resource_providers/templates/sqs_fifo_queue_required_properties.yml new file mode 100644 index 0000000000000..c60e20a744287 --- /dev/null +++ b/tests/aws/services/sqs/resource_providers/templates/sqs_fifo_queue_required_properties.yml @@ -0,0 +1,17 @@ +# +# Define an SQS FIFO Queue with only the required properties set. All other properties will +# take their default values. +# +Resources: + FifoQueue: + Type: 'AWS::SQS::Queue' + Properties: + FifoQueue: true + +Outputs: + QueueUrl: + Description: 'URL of the SQS Fifo Queue' + Value: !Ref FifoQueue + QueueArn: + Description: 'ARN of the SQS Fifo Queue' + Value: !GetAtt FifoQueue.Arn diff --git a/tests/aws/services/sqs/resource_providers/templates/sqs_standard_queue_all_properties.yml b/tests/aws/services/sqs/resource_providers/templates/sqs_standard_queue_all_properties.yml new file mode 100644 index 0000000000000..2e23499ba1746 --- /dev/null +++ b/tests/aws/services/sqs/resource_providers/templates/sqs_standard_queue_all_properties.yml @@ -0,0 +1,44 @@ +# +# A standard SQS queue with all properties set, including a dead-letter queue. All properties are set to valid values. +# +Resources: + StandardQueue: + Type: 'AWS::SQS::Queue' + Properties: + QueueName: 'MyStandardQueueWithAllProperties' + DelaySeconds: 17 + MaximumMessageSize: 1234 + MessageRetentionPeriod: 13579 + ReceiveMessageWaitTimeSeconds: 18 + VisibilityTimeout: 45 + RedrivePolicy: + deadLetterTargetArn: !GetAtt DeadLetterQueue.Arn + maxReceiveCount: 7 + SqsManagedSseEnabled: false + KmsMasterKeyId: 'alias/aws/sqs' + KmsDataKeyReusePeriodSeconds: 297 + Tags: + - Key: 'Environment' + Value: 'Production' + - Key: 'Department' + Value: 'Finance' + + DeadLetterQueue: + Type: 'AWS::SQS::Queue' + Properties: + RedriveAllowPolicy: + redrivePermission: 'allowAll' + +Outputs: + QueueUrl: + Description: 'URL of the SQS Standard Queue' + Value: !Ref StandardQueue + QueueArn: + Description: 'ARN of the SQS Standard Queue' + Value: !GetAtt StandardQueue.Arn + DeadLetterQueueUrl: + Description: 'URL of the Dead Letter Queue' + Value: !Ref DeadLetterQueue + DeadLetterQueueArn: + Description: 'ARN of the Dead Letter Queue' + Value: !GetAtt DeadLetterQueue.Arn diff --git a/tests/aws/services/sqs/resource_providers/templates/sqs_standard_queue_all_properties_variant_1.yml b/tests/aws/services/sqs/resource_providers/templates/sqs_standard_queue_all_properties_variant_1.yml new file mode 100644 index 0000000000000..e42ffa651e223 --- /dev/null +++ b/tests/aws/services/sqs/resource_providers/templates/sqs_standard_queue_all_properties_variant_1.yml @@ -0,0 +1,43 @@ +# +# Similar to sqs_standard_queue_all_properties.yml in that it defines a standard SQS queue with all properties set, +# however, the property values are different. +# +Resources: + StandardQueue: + Type: 'AWS::SQS::Queue' + Properties: + QueueName: 'MyStandardQueueWithAllProperties' + DelaySeconds: 16 + MaximumMessageSize: 4321 + MessageRetentionPeriod: 17539 + ReceiveMessageWaitTimeSeconds: 17 + VisibilityTimeout: 40 + RedrivePolicy: + deadLetterTargetArn: !GetAtt DeadLetterQueue.Arn + maxReceiveCount: 3 + SqsManagedSseEnabled: true + Tags: + - Key: 'Environment' + Value: 'Staging' + - Key: 'Department' + Value: 'Sales' + + DeadLetterQueue: + Type: 'AWS::SQS::Queue' + Properties: + RedriveAllowPolicy: + redrivePermission: 'allowAll' + +Outputs: + QueueUrl: + Description: 'URL of the SQS Standard Queue' + Value: !Ref StandardQueue + QueueArn: + Description: 'ARN of the SQS Standard Queue' + Value: !GetAtt StandardQueue.Arn + DeadLetterQueueUrl: + Description: 'URL of the Dead Letter Queue' + Value: !Ref DeadLetterQueue + DeadLetterQueueArn: + Description: 'ARN of the Dead Letter Queue' + Value: !GetAtt DeadLetterQueue.Arn diff --git a/tests/aws/services/sqs/resource_providers/templates/sqs_standard_queue_all_properties_variant_2.yml b/tests/aws/services/sqs/resource_providers/templates/sqs_standard_queue_all_properties_variant_2.yml new file mode 100644 index 0000000000000..6c1da7b64661d --- /dev/null +++ b/tests/aws/services/sqs/resource_providers/templates/sqs_standard_queue_all_properties_variant_2.yml @@ -0,0 +1,43 @@ +# +# Similar to sqs_standard_queue_all_properties.yml in that it defines a standard SQS queue with all properties set, +# however, the property values are different, including the Queue name. +# +Resources: + StandardQueue: + Type: 'AWS::SQS::Queue' + Properties: + QueueName: 'MyStandardQueueWithADifferentName' + DelaySeconds: 15 + MaximumMessageSize: 4444 + MessageRetentionPeriod: 17777 + ReceiveMessageWaitTimeSeconds: 14 + VisibilityTimeout: 35 + RedrivePolicy: + deadLetterTargetArn: !GetAtt DeadLetterQueue.Arn + maxReceiveCount: 4 + SqsManagedSseEnabled: true + Tags: + - Key: 'Environment' + Value: 'Pre-Test' + - Key: 'Department' + Value: 'Marketing' + + DeadLetterQueue: + Type: 'AWS::SQS::Queue' + Properties: + RedriveAllowPolicy: + redrivePermission: 'allowAll' + +Outputs: + QueueUrl: + Description: 'URL of the SQS Standard Queue' + Value: !Ref StandardQueue + QueueArn: + Description: 'ARN of the SQS Standard Queue' + Value: !GetAtt StandardQueue.Arn + DeadLetterQueueUrl: + Description: 'URL of the Dead Letter Queue' + Value: !Ref DeadLetterQueue + DeadLetterQueueArn: + Description: 'ARN of the Dead Letter Queue' + Value: !GetAtt DeadLetterQueue.Arn diff --git a/tests/aws/services/sqs/resource_providers/templates/sqs_standard_queue_all_properties_with_error.yml b/tests/aws/services/sqs/resource_providers/templates/sqs_standard_queue_all_properties_with_error.yml new file mode 100644 index 0000000000000..6b60a7bd16c3d --- /dev/null +++ b/tests/aws/services/sqs/resource_providers/templates/sqs_standard_queue_all_properties_with_error.yml @@ -0,0 +1,45 @@ +# +# A standard SQS queue with all properties set, including a dead-letter queue. The ReceiveMessageWaitTimeSeconds +# property is set to an invalid value (21) to simulate an error scenario. +# +Resources: + StandardQueue: + Type: 'AWS::SQS::Queue' + Properties: + QueueName: 'MyStandardQueueWithAllProperties' + DelaySeconds: 31 + MaximumMessageSize: 1048576 + MessageRetentionPeriod: 13579 + ReceiveMessageWaitTimeSeconds: 21 + VisibilityTimeout: 45 + RedrivePolicy: + deadLetterTargetArn: !GetAtt DeadLetterQueue.Arn + maxReceiveCount: 7 + SqsManagedSseEnabled: false + KmsMasterKeyId: 'alias/aws/sqs' + KmsDataKeyReusePeriodSeconds: 297 + Tags: + - Key: 'Environment' + Value: 'Production' + - Key: 'Department' + Value: 'Finance' + + DeadLetterQueue: + Type: 'AWS::SQS::Queue' + Properties: + RedriveAllowPolicy: + redrivePermission: 'allowAll' + +Outputs: + QueueUrl: + Description: 'URL of the SQS Standard Queue' + Value: !Ref StandardQueue + QueueArn: + Description: 'ARN of the SQS Standard Queue' + Value: !GetAtt StandardQueue.Arn + DeadLetterQueueUrl: + Description: 'URL of the Dead Letter Queue' + Value: !Ref DeadLetterQueue + DeadLetterQueueArn: + Description: 'ARN of the Dead Letter Queue' + Value: !GetAtt DeadLetterQueue.Arn diff --git a/tests/aws/services/sqs/resource_providers/templates/sqs_standard_queue_no_resource.yml b/tests/aws/services/sqs/resource_providers/templates/sqs_standard_queue_no_resource.yml new file mode 100644 index 0000000000000..c9a18dc5f149a --- /dev/null +++ b/tests/aws/services/sqs/resource_providers/templates/sqs_standard_queue_no_resource.yml @@ -0,0 +1,18 @@ +# +# A template defining only a dead-letter SQS queue without any additional properties. This template does not +# contain the main queue, so applying this template should remove any existing main queue. +# +Resources: + DeadLetterQueue: + Type: 'AWS::SQS::Queue' + Properties: + RedriveAllowPolicy: + redrivePermission: 'allowAll' + +Outputs: + DeadLetterQueueUrl: + Description: 'URL of the Dead Letter Queue' + Value: !Ref DeadLetterQueue + DeadLetterQueueArn: + Description: 'ARN of the Dead Letter Queue' + Value: !GetAtt DeadLetterQueue.Arn diff --git a/tests/aws/services/sqs/resource_providers/templates/sqs_standard_queue_required_properties.yml b/tests/aws/services/sqs/resource_providers/templates/sqs_standard_queue_required_properties.yml new file mode 100644 index 0000000000000..f5f8ee5c1798e --- /dev/null +++ b/tests/aws/services/sqs/resource_providers/templates/sqs_standard_queue_required_properties.yml @@ -0,0 +1,14 @@ +# +# A standard SQS queue with no properties set, including the name which will be auto-generated. +# +Resources: + StandardQueue: + Type: 'AWS::SQS::Queue' + +Outputs: + QueueUrl: + Description: 'URL of the SQS Standard Queue' + Value: !Ref StandardQueue + QueueArn: + Description: 'ARN of the SQS Standard Queue' + Value: !GetAtt StandardQueue.Arn diff --git a/tests/aws/services/sqs/resource_providers/templates/sqs_standard_queue_some_properties.yml b/tests/aws/services/sqs/resource_providers/templates/sqs_standard_queue_some_properties.yml new file mode 100644 index 0000000000000..f8be1c306d625 --- /dev/null +++ b/tests/aws/services/sqs/resource_providers/templates/sqs_standard_queue_some_properties.yml @@ -0,0 +1,38 @@ +# +# A standard SQS queue with some properties set, but not all. The properties set have valid values, and the unset +# properties will take their default values. +# +Resources: + StandardQueue: + Type: 'AWS::SQS::Queue' + Properties: + QueueName: 'MyStandardQueueWithAllProperties' + DelaySeconds: 17 + VisibilityTimeout: 43 + RedrivePolicy: + deadLetterTargetArn: !GetAtt DeadLetterQueue.Arn + maxReceiveCount: 7 + SqsManagedSseEnabled: false + Tags: + - Key: 'Environment' + Value: 'Pre-Production' + + DeadLetterQueue: + Type: 'AWS::SQS::Queue' + Properties: + RedriveAllowPolicy: + redrivePermission: 'allowAll' + +Outputs: + QueueUrl: + Description: 'URL of the SQS Standard Queue' + Value: !Ref StandardQueue + QueueArn: + Description: 'ARN of the SQS Standard Queue' + Value: !GetAtt StandardQueue.Arn + DeadLetterQueueUrl: + Description: 'URL of the Dead Letter Queue' + Value: !Ref DeadLetterQueue + DeadLetterQueueArn: + Description: 'ARN of the Dead Letter Queue' + Value: !GetAtt DeadLetterQueue.Arn diff --git a/tests/aws/services/sqs/resource_providers/templates/sqs_standard_queue_some_properties_without_name.yml b/tests/aws/services/sqs/resource_providers/templates/sqs_standard_queue_some_properties_without_name.yml new file mode 100644 index 0000000000000..bba8bd1bed745 --- /dev/null +++ b/tests/aws/services/sqs/resource_providers/templates/sqs_standard_queue_some_properties_without_name.yml @@ -0,0 +1,21 @@ +# +# A standard SQS queue with some properties set, but not all. The properties set have valid values, and the unset +# properties will take their default values. This is similar to sqs_standard_queue_some_properties.yml, but without +# specifying a name for the queue, so it'll be auto-generated. +# +Resources: + StandardQueue: + Type: 'AWS::SQS::Queue' + Properties: + VisibilityTimeout: 30 + MaximumMessageSize: 4321 + MessageRetentionPeriod: 17539 + ReceiveMessageWaitTimeSeconds: 17 + +Outputs: + QueueUrl: + Description: 'URL of the SQS Standard Queue' + Value: !Ref StandardQueue + QueueArn: + Description: 'ARN of the SQS Standard Queue' + Value: !GetAtt StandardQueue.Arn diff --git a/tests/aws/templates/sqs_with_queuepolicy.yaml b/tests/aws/services/sqs/resource_providers/templates/sqs_with_queuepolicy.yml similarity index 87% rename from tests/aws/templates/sqs_with_queuepolicy.yaml rename to tests/aws/services/sqs/resource_providers/templates/sqs_with_queuepolicy.yml index 0fabf18902847..6621eeb477a91 100644 --- a/tests/aws/templates/sqs_with_queuepolicy.yaml +++ b/tests/aws/services/sqs/resource_providers/templates/sqs_with_queuepolicy.yml @@ -1,3 +1,6 @@ +# +# A standard SQS queue with an associated queue policy allowing a few actions. +# Resources: Queue4A7E3555: Type: AWS::SQS::Queue diff --git a/tests/aws/templates/sqs_with_queuepolicy_updated.yaml b/tests/aws/services/sqs/resource_providers/templates/sqs_with_queuepolicy_updated.yml similarity index 73% rename from tests/aws/templates/sqs_with_queuepolicy_updated.yaml rename to tests/aws/services/sqs/resource_providers/templates/sqs_with_queuepolicy_updated.yml index 17818bddc3fd2..2ab4f38fb72fa 100644 --- a/tests/aws/templates/sqs_with_queuepolicy_updated.yaml +++ b/tests/aws/services/sqs/resource_providers/templates/sqs_with_queuepolicy_updated.yml @@ -1,3 +1,7 @@ +# +# A standard SQS queue with an associated queue policy allowing a slightly different set of actions compared +# to sqs_with_queuepolicy.yml. +# Resources: Queue4A7E3555: Type: AWS::SQS::Queue @@ -10,7 +14,8 @@ Resources: - sqs:SendMessage - sqs:GetQueueAttributes - sqs:GetQueueUrl - Effect: Deny + - sqs:DeleteMessage + Effect: Allow Principal: "*" Resource: Fn::GetAtt: diff --git a/tests/aws/services/sqs/resource_providers/test_aws_sqs_queue.py b/tests/aws/services/sqs/resource_providers/test_aws_sqs_queue.py new file mode 100644 index 0000000000000..0e8abe5bf31b6 --- /dev/null +++ b/tests/aws/services/sqs/resource_providers/test_aws_sqs_queue.py @@ -0,0 +1,237 @@ +import os + +import pytest +from botocore.exceptions import ClientError + +from localstack.testing.pytest import markers +from localstack.testing.pytest.fixtures import StackDeployError + + +def deploy_stack(deploy_cfn_template, template_filename, **kwargs): + """ + Helper function to deploy a CloudFormation stack using a template file. This exists to reduce + boilerplate in the test cases. + """ + template_path = os.path.join(os.path.dirname(__file__), "templates", template_filename) + return deploy_cfn_template(template_path=template_path, **kwargs) + + +@markers.aws.validated +def test_create_standard_queue_with_required_properties(deploy_cfn_template, aws_client, snapshot): + stack = deploy_stack(deploy_cfn_template, "sqs_standard_queue_required_properties.yml") + (queue_url, queue_arn) = (stack.outputs["QueueUrl"], stack.outputs["QueueArn"]) + + snapshot.match( + "attributes", + aws_client.sqs.get_queue_attributes(QueueUrl=queue_url, AttributeNames=["All"]), + ) + snapshot.add_transformer(snapshot.transform.regex(queue_arn, "")) + + # auto-generated name check + assert "StandardQueue" in queue_url + assert not queue_url.endswith(".fifo") + + +@markers.aws.validated +def test_create_standard_queue_with_all_properties(deploy_cfn_template, aws_client, snapshot): + stack = deploy_stack(deploy_cfn_template, "sqs_standard_queue_all_properties.yml") + (queue_url, queue_arn, dlq_queue_url, dlq_queue_arn) = ( + stack.outputs["QueueUrl"], + stack.outputs["QueueArn"], + stack.outputs["DeadLetterQueueUrl"], + stack.outputs["DeadLetterQueueArn"], + ) + + snapshot.match( + "attributes", + aws_client.sqs.get_queue_attributes(QueueUrl=queue_url, AttributeNames=["All"]), + ) + snapshot.match("tags", aws_client.sqs.list_queue_tags(QueueUrl=queue_url)) + snapshot.match( + "dlq_attributes", + aws_client.sqs.get_queue_attributes( + QueueUrl=dlq_queue_url, AttributeNames=["RedriveAllowPolicy"] + ), + ) + snapshot.add_transformer(snapshot.transform.regex(queue_arn, "")) + snapshot.add_transformer(snapshot.transform.regex(dlq_queue_arn, "")) + + +@markers.aws.validated +def test_create_fifo_queue_with_required_properties(deploy_cfn_template, aws_client, snapshot): + stack = deploy_stack(deploy_cfn_template, "sqs_fifo_queue_required_properties.yml") + (queue_url, queue_arn) = (stack.outputs["QueueUrl"], stack.outputs["QueueArn"]) + + snapshot.match( + "attributes", + aws_client.sqs.get_queue_attributes(QueueUrl=queue_url, AttributeNames=["All"]), + ) + snapshot.add_transformer(snapshot.transform.regex(queue_arn, "")) + + # auto-generated name check + assert "FifoQueue" in queue_url + assert queue_url.endswith(".fifo") + + +@markers.aws.validated +def test_create_fifo_queue_with_all_properties(deploy_cfn_template, aws_client, snapshot): + stack = deploy_stack(deploy_cfn_template, "sqs_fifo_queue_all_properties.yml") + (queue_url, queue_arn, dlq_queue_url, dlq_queue_arn) = ( + stack.outputs["QueueUrl"], + stack.outputs["QueueArn"], + stack.outputs["DeadLetterQueueUrl"], + stack.outputs["DeadLetterQueueArn"], + ) + + snapshot.match( + "attributes", + aws_client.sqs.get_queue_attributes(QueueUrl=queue_url, AttributeNames=["All"]), + ) + snapshot.match("tags", aws_client.sqs.list_queue_tags(QueueUrl=queue_url)) + snapshot.match( + "dlq_attributes", + aws_client.sqs.get_queue_attributes( + QueueUrl=dlq_queue_url, AttributeNames=["RedriveAllowPolicy"] + ), + ) + snapshot.add_transformer(snapshot.transform.regex(queue_arn, "")) + snapshot.add_transformer(snapshot.transform.regex(dlq_queue_arn, "")) + + +@markers.aws.validated +def test_update_standard_queue_modify_properties_in_place( + deploy_cfn_template, aws_client, snapshot +): + stack = deploy_stack(deploy_cfn_template, "sqs_standard_queue_all_properties.yml") + queue_url = stack.outputs["QueueUrl"] + + # Update the stack to add optional properties + updated_stack = deploy_stack( + deploy_cfn_template, + "sqs_standard_queue_all_properties_variant_1.yml", + is_update=True, + stack_name=stack.stack_name, + ) + updated_queue_url = updated_stack.outputs["QueueUrl"] + assert queue_url == updated_queue_url + + snapshot.match( + "updated_attributes", + aws_client.sqs.get_queue_attributes(QueueUrl=updated_queue_url, AttributeNames=["All"]), + ) + snapshot.match("updated_tags", aws_client.sqs.list_queue_tags(QueueUrl=updated_queue_url)) + snapshot.add_transformer(snapshot.transform.regex(stack.outputs["QueueArn"], "")) + snapshot.add_transformer( + snapshot.transform.regex(stack.outputs["DeadLetterQueueArn"], "") + ) + + +@markers.aws.validated +def test_update_standard_queue_add_properties_with_replacement( + deploy_cfn_template, aws_client, snapshot +): + stack = deploy_stack(deploy_cfn_template, "sqs_standard_queue_all_properties.yml") + (queue_url, dlq_queue_url) = (stack.outputs["QueueUrl"], stack.outputs["DeadLetterQueueUrl"]) + + # Update the stack to rename the queue - this will cause the resource to be replaced, rather than + # updating the existing queue in place + updated_stack = deploy_stack( + deploy_cfn_template, + "sqs_standard_queue_all_properties_variant_2.yml", + is_update=True, + stack_name=stack.stack_name, + ) + (updated_queue_url, updated_dlq_queue_url) = ( + updated_stack.outputs["QueueUrl"], + updated_stack.outputs["DeadLetterQueueUrl"], + ) + assert queue_url != updated_queue_url + assert dlq_queue_url == updated_dlq_queue_url + + snapshot.match( + "updated_attributes", + aws_client.sqs.get_queue_attributes(QueueUrl=updated_queue_url, AttributeNames=["All"]), + ) + snapshot.match("updated_tags", aws_client.sqs.list_queue_tags(QueueUrl=updated_queue_url)) + snapshot.add_transformer(snapshot.transform.key_value("deadLetterTargetArn", "")) + + # confirm that the original queue has been deleted + with pytest.raises(ClientError) as exc: + aws_client.sqs.get_queue_attributes(QueueUrl=queue_url, AttributeNames=["All"]) + snapshot.match("error", exc.value.response) + + +@markers.aws.validated +def test_update_standard_queue_remove_some_properties(deploy_cfn_template, aws_client, snapshot): + stack = deploy_stack(deploy_cfn_template, "sqs_standard_queue_all_properties.yml") + queue_url = stack.outputs["QueueUrl"] + + # Update the stack with modified properties + updated_stack = deploy_stack( + deploy_cfn_template, + "sqs_standard_queue_some_properties.yml", + is_update=True, + stack_name=stack.stack_name, + ) + updated_queue_url = updated_stack.outputs["QueueUrl"] + assert queue_url == updated_queue_url + + snapshot.match( + "updated_attributes", + aws_client.sqs.get_queue_attributes(QueueUrl=updated_queue_url, AttributeNames=["All"]), + ) + snapshot.match("updated_tags", aws_client.sqs.list_queue_tags(QueueUrl=updated_queue_url)) + snapshot.add_transformer(snapshot.transform.regex(stack.outputs["QueueArn"], "")) + snapshot.add_transformer(snapshot.transform.key_value("deadLetterTargetArn", "")) + + +@markers.aws.validated +def test_update_completely_remove_resource(deploy_cfn_template, aws_client, snapshot): + stack = deploy_stack(deploy_cfn_template, "sqs_standard_queue_all_properties.yml") + queue_url = stack.outputs["QueueUrl"] + + # Delete the queue by updating the stack to remove the resource + deploy_stack( + deploy_cfn_template, + "sqs_standard_queue_no_resource.yml", + is_update=True, + stack_name=stack.stack_name, + ) + + # expect an exception to be thrown because the resource is deleted + with pytest.raises(ClientError) as exc: + aws_client.sqs.get_queue_attributes(QueueUrl=queue_url, AttributeNames=["All"]) + snapshot.match("error", exc.value.response) + + +@markers.aws.validated +def test_update_standard_queue_without_explicit_name(deploy_cfn_template, aws_client, snapshot): + stack = deploy_stack(deploy_cfn_template, "sqs_standard_queue_required_properties.yml") + queue_url = stack.outputs["QueueUrl"] + + # Update the stack to add optional properties, but expect the same queue name to be used. + updated_stack = deploy_stack( + deploy_cfn_template, + "sqs_standard_queue_some_properties_without_name.yml", + is_update=True, + stack_name=stack.stack_name, + ) + updated_queue_url = updated_stack.outputs["QueueUrl"] + assert queue_url == updated_queue_url + + snapshot.match( + "updated_attributes", + aws_client.sqs.get_queue_attributes(QueueUrl=updated_queue_url, AttributeNames=["All"]), + ) + snapshot.add_transformer(snapshot.transform.regex(stack.outputs["QueueArn"], "")) + + +@pytest.mark.skip(reason="SQS service in LocalStack does not correctly fail on invalid parameters") +@markers.aws.needs_fixing +def test_error_invalid_parameter(deploy_cfn_template, aws_client, snapshot): + with pytest.raises(StackDeployError) as exc: + deploy_stack( + deploy_cfn_template, + "sqs_standard_queue_all_properties_with_error.yml", + ) + snapshot.match("error", exc.value) diff --git a/tests/aws/services/sqs/resource_providers/test_aws_sqs_queue.snapshot.json b/tests/aws/services/sqs/resource_providers/test_aws_sqs_queue.snapshot.json new file mode 100644 index 0000000000000..393a4ded0e75a --- /dev/null +++ b/tests/aws/services/sqs/resource_providers/test_aws_sqs_queue.snapshot.json @@ -0,0 +1,334 @@ +{ + "tests/aws/services/sqs/resource_providers/test_aws_sqs_queue.py::test_create_standard_queue_with_required_properties": { + "recorded-date": "07-12-2025, 19:18:58", + "recorded-content": { + "attributes": { + "Attributes": { + "ApproximateNumberOfMessages": "0", + "ApproximateNumberOfMessagesDelayed": "0", + "ApproximateNumberOfMessagesNotVisible": "0", + "CreatedTimestamp": "timestamp", + "DelaySeconds": "0", + "LastModifiedTimestamp": "timestamp", + "MaximumMessageSize": "1048576", + "MessageRetentionPeriod": "345600", + "QueueArn": "", + "ReceiveMessageWaitTimeSeconds": "0", + "SqsManagedSseEnabled": "true", + "VisibilityTimeout": "30" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/sqs/resource_providers/test_aws_sqs_queue.py::test_create_standard_queue_with_all_properties": { + "recorded-date": "07-12-2025, 19:20:20", + "recorded-content": { + "attributes": { + "Attributes": { + "ApproximateNumberOfMessages": "0", + "ApproximateNumberOfMessagesDelayed": "0", + "ApproximateNumberOfMessagesNotVisible": "0", + "CreatedTimestamp": "timestamp", + "DelaySeconds": "17", + "KmsDataKeyReusePeriodSeconds": "297", + "KmsMasterKeyId": "alias/aws/sqs", + "LastModifiedTimestamp": "timestamp", + "MaximumMessageSize": "1234", + "MessageRetentionPeriod": "13579", + "QueueArn": "", + "ReceiveMessageWaitTimeSeconds": "18", + "RedrivePolicy": { + "deadLetterTargetArn": "", + "maxReceiveCount": 7 + }, + "SqsManagedSseEnabled": "false", + "VisibilityTimeout": "45" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "tags": { + "Tags": { + "Department": "Finance", + "Environment": "Production" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "dlq_attributes": { + "Attributes": { + "RedriveAllowPolicy": { + "redrivePermission": "allowAll" + } + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/sqs/resource_providers/test_aws_sqs_queue.py::test_create_fifo_queue_with_required_properties": { + "recorded-date": "07-12-2025, 19:22:08", + "recorded-content": { + "attributes": { + "Attributes": { + "ApproximateNumberOfMessages": "0", + "ApproximateNumberOfMessagesDelayed": "0", + "ApproximateNumberOfMessagesNotVisible": "0", + "ContentBasedDeduplication": "false", + "CreatedTimestamp": "timestamp", + "DeduplicationScope": "queue", + "DelaySeconds": "0", + "FifoQueue": "true", + "FifoThroughputLimit": "perQueue", + "LastModifiedTimestamp": "timestamp", + "MaximumMessageSize": "1048576", + "MessageRetentionPeriod": "345600", + "QueueArn": "", + "ReceiveMessageWaitTimeSeconds": "0", + "SqsManagedSseEnabled": "true", + "VisibilityTimeout": "30" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/sqs/resource_providers/test_aws_sqs_queue.py::test_create_fifo_queue_with_all_properties": { + "recorded-date": "07-12-2025, 19:23:29", + "recorded-content": { + "attributes": { + "Attributes": { + "ApproximateNumberOfMessages": "0", + "ApproximateNumberOfMessagesDelayed": "0", + "ApproximateNumberOfMessagesNotVisible": "0", + "ContentBasedDeduplication": "true", + "CreatedTimestamp": "timestamp", + "DeduplicationScope": "messageGroup", + "DelaySeconds": "13", + "FifoQueue": "true", + "FifoThroughputLimit": "perMessageGroupId", + "KmsDataKeyReusePeriodSeconds": "200", + "KmsMasterKeyId": "alias/aws/sqs", + "LastModifiedTimestamp": "timestamp", + "MaximumMessageSize": "3232", + "MessageRetentionPeriod": "13425", + "QueueArn": "", + "ReceiveMessageWaitTimeSeconds": "15", + "RedrivePolicy": { + "deadLetterTargetArn": "", + "maxReceiveCount": 7 + }, + "SqsManagedSseEnabled": "false", + "VisibilityTimeout": "42" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "tags": { + "Tags": { + "Department": "Finance", + "Environment": "Production" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "dlq_attributes": { + "Attributes": { + "RedriveAllowPolicy": { + "redrivePermission": "allowAll" + } + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/sqs/resource_providers/test_aws_sqs_queue.py::test_update_standard_queue_modify_properties_in_place": { + "recorded-date": "07-12-2025, 19:26:04", + "recorded-content": { + "updated_attributes": { + "Attributes": { + "ApproximateNumberOfMessages": "0", + "ApproximateNumberOfMessagesDelayed": "0", + "ApproximateNumberOfMessagesNotVisible": "0", + "CreatedTimestamp": "timestamp", + "DelaySeconds": "16", + "LastModifiedTimestamp": "timestamp", + "MaximumMessageSize": "4321", + "MessageRetentionPeriod": "17539", + "QueueArn": "", + "ReceiveMessageWaitTimeSeconds": "17", + "RedrivePolicy": { + "deadLetterTargetArn": "", + "maxReceiveCount": 3 + }, + "SqsManagedSseEnabled": "true", + "VisibilityTimeout": "40" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "updated_tags": { + "Tags": { + "Department": "Sales", + "Environment": "Staging" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/sqs/resource_providers/test_aws_sqs_queue.py::test_update_standard_queue_add_properties_with_replacement": { + "recorded-date": "07-12-2025, 19:29:14", + "recorded-content": { + "updated_attributes": { + "Attributes": { + "ApproximateNumberOfMessages": "0", + "ApproximateNumberOfMessagesDelayed": "0", + "ApproximateNumberOfMessagesNotVisible": "0", + "CreatedTimestamp": "timestamp", + "DelaySeconds": "15", + "LastModifiedTimestamp": "timestamp", + "MaximumMessageSize": "4444", + "MessageRetentionPeriod": "17777", + "QueueArn": "arn::sqs::111111111111:MyStandardQueueWithADifferentName", + "ReceiveMessageWaitTimeSeconds": "14", + "RedrivePolicy": { + "deadLetterTargetArn": "<:1>", + "maxReceiveCount": 4 + }, + "SqsManagedSseEnabled": "true", + "VisibilityTimeout": "35" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "updated_tags": { + "Tags": { + "Department": "Marketing", + "Environment": "Pre-Test" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "error": { + "Error": { + "Code": "AWS.SimpleQueueService.NonExistentQueue", + "Message": "The specified queue does not exist.", + "QueryErrorCode": "QueueDoesNotExist", + "Type": "Sender" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/sqs/resource_providers/test_aws_sqs_queue.py::test_update_standard_queue_remove_some_properties": { + "recorded-date": "07-12-2025, 19:31:50", + "recorded-content": { + "updated_attributes": { + "Attributes": { + "ApproximateNumberOfMessages": "0", + "ApproximateNumberOfMessagesDelayed": "0", + "ApproximateNumberOfMessagesNotVisible": "0", + "CreatedTimestamp": "timestamp", + "DelaySeconds": "17", + "LastModifiedTimestamp": "timestamp", + "MaximumMessageSize": "262144", + "MessageRetentionPeriod": "345600", + "QueueArn": "", + "ReceiveMessageWaitTimeSeconds": "0", + "RedrivePolicy": { + "deadLetterTargetArn": "<:1>", + "maxReceiveCount": 7 + }, + "SqsManagedSseEnabled": "false", + "VisibilityTimeout": "43" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "updated_tags": { + "Tags": { + "Environment": "Pre-Production" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/sqs/resource_providers/test_aws_sqs_queue.py::test_update_completely_remove_resource": { + "recorded-date": "07-12-2025, 19:34:25", + "recorded-content": { + "error": { + "Error": { + "Code": "AWS.SimpleQueueService.NonExistentQueue", + "Message": "The specified queue does not exist.", + "QueryErrorCode": "QueueDoesNotExist", + "Type": "Sender" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/sqs/resource_providers/test_aws_sqs_queue.py::test_update_standard_queue_without_explicit_name": { + "recorded-date": "07-12-2025, 19:36:25", + "recorded-content": { + "updated_attributes": { + "Attributes": { + "ApproximateNumberOfMessages": "0", + "ApproximateNumberOfMessagesDelayed": "0", + "ApproximateNumberOfMessagesNotVisible": "0", + "CreatedTimestamp": "timestamp", + "DelaySeconds": "0", + "LastModifiedTimestamp": "timestamp", + "MaximumMessageSize": "4321", + "MessageRetentionPeriod": "17539", + "QueueArn": "", + "ReceiveMessageWaitTimeSeconds": "17", + "SqsManagedSseEnabled": "true", + "VisibilityTimeout": "30" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + } +} diff --git a/tests/aws/services/sqs/resource_providers/test_aws_sqs_queue.validation.json b/tests/aws/services/sqs/resource_providers/test_aws_sqs_queue.validation.json new file mode 100644 index 0000000000000..70aab5d6bd816 --- /dev/null +++ b/tests/aws/services/sqs/resource_providers/test_aws_sqs_queue.validation.json @@ -0,0 +1,83 @@ +{ + "tests/aws/services/sqs/resource_providers/test_aws_sqs_queue.py::test_create_fifo_queue_with_all_properties": { + "last_validated_date": "2025-12-07T19:24:35+00:00", + "durations_in_seconds": { + "setup": 0.0, + "call": 45.71, + "teardown": 66.19, + "total": 111.9 + } + }, + "tests/aws/services/sqs/resource_providers/test_aws_sqs_queue.py::test_create_fifo_queue_with_required_properties": { + "last_validated_date": "2025-12-07T19:22:43+00:00", + "durations_in_seconds": { + "setup": 0.0, + "call": 41.17, + "teardown": 35.19, + "total": 76.36 + } + }, + "tests/aws/services/sqs/resource_providers/test_aws_sqs_queue.py::test_create_standard_queue_with_all_properties": { + "last_validated_date": "2025-12-07T19:21:27+00:00", + "durations_in_seconds": { + "setup": 0.0, + "call": 46.56, + "teardown": 66.83, + "total": 113.39 + } + }, + "tests/aws/services/sqs/resource_providers/test_aws_sqs_queue.py::test_create_standard_queue_with_required_properties": { + "last_validated_date": "2025-12-07T19:19:33+00:00", + "durations_in_seconds": { + "setup": 1.3, + "call": 44.27, + "teardown": 34.85, + "total": 80.42 + } + }, + "tests/aws/services/sqs/resource_providers/test_aws_sqs_queue.py::test_update_completely_remove_resource": { + "last_validated_date": "2025-12-07T19:35:02+00:00", + "durations_in_seconds": { + "setup": 0.0, + "call": 88.6, + "teardown": 36.74, + "total": 125.34 + } + }, + "tests/aws/services/sqs/resource_providers/test_aws_sqs_queue.py::test_update_standard_queue_add_properties_with_replacement": { + "last_validated_date": "2025-12-07T19:30:21+00:00", + "durations_in_seconds": { + "setup": 0.0, + "call": 123.16, + "teardown": 67.12, + "total": 190.28 + } + }, + "tests/aws/services/sqs/resource_providers/test_aws_sqs_queue.py::test_update_standard_queue_modify_properties_in_place": { + "last_validated_date": "2025-12-07T19:27:11+00:00", + "durations_in_seconds": { + "setup": 0.0, + "call": 88.67, + "teardown": 67.42, + "total": 156.09 + } + }, + "tests/aws/services/sqs/resource_providers/test_aws_sqs_queue.py::test_update_standard_queue_remove_some_properties": { + "last_validated_date": "2025-12-07T19:32:56+00:00", + "durations_in_seconds": { + "setup": 0.0, + "call": 88.91, + "teardown": 65.94, + "total": 154.85 + } + }, + "tests/aws/services/sqs/resource_providers/test_aws_sqs_queue.py::test_update_standard_queue_without_explicit_name": { + "last_validated_date": "2025-12-07T19:37:01+00:00", + "durations_in_seconds": { + "setup": 0.0, + "call": 83.75, + "teardown": 35.43, + "total": 119.18 + } + } +} diff --git a/tests/aws/services/sqs/resource_providers/test_aws_sqs_queuepolicy.py b/tests/aws/services/sqs/resource_providers/test_aws_sqs_queuepolicy.py new file mode 100644 index 0000000000000..dfed6a331df6a --- /dev/null +++ b/tests/aws/services/sqs/resource_providers/test_aws_sqs_queuepolicy.py @@ -0,0 +1,62 @@ +import os + +from localstack.testing.pytest import markers +from localstack.utils.sync import wait_until + + +def deploy_stack(deploy_cfn_template, template_filename, **kwargs): + """ + Helper function to deploy a CloudFormation stack using a template file. This exists to reduce + boilerplate in the test cases. + """ + template_path = os.path.join(os.path.dirname(__file__), "templates", template_filename) + return deploy_cfn_template(template_path=template_path, **kwargs) + + +@markers.aws.validated +def test_sqs_queue_policy(deploy_cfn_template, aws_client, snapshot): + result = deploy_stack(deploy_cfn_template, "sqs_with_queuepolicy.yml") + queue_url = result.outputs["QueueUrlOutput"] + + snapshot.match( + "policy", aws_client.sqs.get_queue_attributes(QueueUrl=queue_url, AttributeNames=["Policy"]) + ) + snapshot.add_transformer(snapshot.transform.key_value("Resource")) + + +@markers.aws.validated +def test_update_sqs_queuepolicy(deploy_cfn_template, aws_client, snapshot): + stack = deploy_stack(deploy_cfn_template, "sqs_with_queuepolicy.yml") + policy = aws_client.sqs.get_queue_attributes( + QueueUrl=stack.outputs["QueueUrlOutput"], AttributeNames=["Policy"] + ) + snapshot.match("policy1", policy["Attributes"]["Policy"]) + + updated_stack = deploy_stack( + deploy_cfn_template, + "sqs_with_queuepolicy_updated.yml", + is_update=True, + stack_name=stack.stack_name, + ) + + def check_policy_updated(): + policy_updated = aws_client.sqs.get_queue_attributes( + QueueUrl=updated_stack.outputs["QueueUrlOutput"], AttributeNames=["Policy"] + ) + assert policy_updated["Attributes"]["Policy"] != policy["Attributes"]["Policy"] + return policy_updated + + wait_until(check_policy_updated) + + policy = aws_client.sqs.get_queue_attributes( + QueueUrl=updated_stack.outputs["QueueUrlOutput"], AttributeNames=["Policy"] + ) + + snapshot.match("policy2", policy["Attributes"]["Policy"]) + snapshot.add_transformer(snapshot.transform.cloudformation_api()) + + +# TODO: add the following test cases: +# - update to add two additional queues (1 -> 3) +# - update to remove two of the queues (3 -> 1) +# - Update to remove queuepolicy from the template. diff --git a/tests/aws/services/sqs/resource_providers/test_aws_sqs_queuepolicy.snapshot.json b/tests/aws/services/sqs/resource_providers/test_aws_sqs_queuepolicy.snapshot.json new file mode 100644 index 0000000000000..a78d2dd9d29f2 --- /dev/null +++ b/tests/aws/services/sqs/resource_providers/test_aws_sqs_queuepolicy.snapshot.json @@ -0,0 +1,66 @@ +{ + "tests/aws/services/sqs/resource_providers/test_aws_sqs_queuepolicy.py::test_sqs_queue_policy": { + "recorded-date": "07-12-2025, 18:56:21", + "recorded-content": { + "policy": { + "Attributes": { + "Policy": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": [ + "sqs:SendMessage", + "sqs:GetQueueAttributes", + "sqs:GetQueueUrl" + ], + "Resource": "" + } + ] + } + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/sqs/resource_providers/test_aws_sqs_queuepolicy.py::test_update_sqs_queuepolicy": { + "recorded-date": "07-12-2025, 18:58:54", + "recorded-content": { + "policy1": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": [ + "sqs:SendMessage", + "sqs:GetQueueAttributes", + "sqs:GetQueueUrl" + ], + "Resource": "arn::sqs::111111111111:" + } + ] + }, + "policy2": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": [ + "sqs:SendMessage", + "sqs:GetQueueAttributes", + "sqs:GetQueueUrl", + "sqs:DeleteMessage" + ], + "Resource": "arn::sqs::111111111111:" + } + ] + } + } + } +} diff --git a/tests/aws/services/sqs/resource_providers/test_aws_sqs_queuepolicy.validation.json b/tests/aws/services/sqs/resource_providers/test_aws_sqs_queuepolicy.validation.json new file mode 100644 index 0000000000000..4e3c984576684 --- /dev/null +++ b/tests/aws/services/sqs/resource_providers/test_aws_sqs_queuepolicy.validation.json @@ -0,0 +1,11 @@ +{ + "tests/aws/services/sqs/resource_providers/test_aws_sqs_queuepolicy.py::test_sqs_queue_policy": { + "last_validated_date": "2025-12-07T18:57:27+00:00", + "durations_in_seconds": { + "setup": 1.1, + "call": 44.01, + "teardown": 66.41, + "total": 111.52 + } + } +} diff --git a/tests/aws/services/sqs/test_sqs.py b/tests/aws/services/sqs/test_sqs.py index 5580968f6264e..8867b9ae99182 100644 --- a/tests/aws/services/sqs/test_sqs.py +++ b/tests/aws/services/sqs/test_sqs.py @@ -4648,6 +4648,29 @@ def test_sse_queue_attributes(self, sqs_create_queue, snapshot, aws_sqs_client): ) snapshot.match("sse_sqs_attributes", response) + @markers.aws.validated + @markers.snapshot.skip_snapshot_verify(paths=["$..Attributes.SqsManagedSseEnabled"]) + def test_set_queue_attributes_default_values(self, sqs_create_queue, snapshot, aws_sqs_client): + queue_url = sqs_create_queue() + response = aws_sqs_client.get_queue_attributes(QueueUrl=queue_url, AttributeNames=["All"]) + snapshot.match("get-queue-attributes-initial-values", response) + + updated_attributes = { + "KmsMasterKeyId": "testKeyId", + "KmsDataKeyReusePeriodSeconds": "6000", + } + aws_sqs_client.set_queue_attributes(QueueUrl=queue_url, Attributes=updated_attributes) + response = aws_sqs_client.get_queue_attributes(QueueUrl=queue_url, AttributeNames=["All"]) + snapshot.match("get-queue-attributes-after-update", response) + + default_attributes = { + "KmsMasterKeyId": "", + "KmsDataKeyReusePeriodSeconds": "300", + } + aws_sqs_client.set_queue_attributes(QueueUrl=queue_url, Attributes=default_attributes) + response = aws_sqs_client.get_queue_attributes(QueueUrl=queue_url, AttributeNames=["All"]) + snapshot.match("get-queue-attributes-after-set-to-defaults", response) + @pytest.mark.skip(reason="validation currently not implemented in localstack") @markers.aws.validated def test_sse_kms_and_sqs_are_mutually_exclusive( diff --git a/tests/aws/services/sqs/test_sqs.snapshot.json b/tests/aws/services/sqs/test_sqs.snapshot.json index 947712e9394f6..cbf0839eaf311 100644 --- a/tests/aws/services/sqs/test_sqs.snapshot.json +++ b/tests/aws/services/sqs/test_sqs.snapshot.json @@ -4801,5 +4801,139 @@ } } } + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_set_queue_attributes_default_values[sqs]": { + "recorded-date": "10-12-2025, 18:38:55", + "recorded-content": { + "get-queue-attributes-initial-values": { + "Attributes": { + "ApproximateNumberOfMessages": "0", + "ApproximateNumberOfMessagesDelayed": "0", + "ApproximateNumberOfMessagesNotVisible": "0", + "CreatedTimestamp": "timestamp", + "DelaySeconds": "0", + "LastModifiedTimestamp": "timestamp", + "MaximumMessageSize": "1048576", + "MessageRetentionPeriod": "345600", + "QueueArn": "arn::sqs::111111111111:", + "ReceiveMessageWaitTimeSeconds": "0", + "SqsManagedSseEnabled": "true", + "VisibilityTimeout": "30" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "get-queue-attributes-after-update": { + "Attributes": { + "ApproximateNumberOfMessages": "0", + "ApproximateNumberOfMessagesDelayed": "0", + "ApproximateNumberOfMessagesNotVisible": "0", + "CreatedTimestamp": "timestamp", + "DelaySeconds": "0", + "KmsDataKeyReusePeriodSeconds": "6000", + "KmsMasterKeyId": "testKeyId", + "LastModifiedTimestamp": "timestamp", + "MaximumMessageSize": "1048576", + "MessageRetentionPeriod": "345600", + "QueueArn": "arn::sqs::111111111111:", + "ReceiveMessageWaitTimeSeconds": "0", + "SqsManagedSseEnabled": "false", + "VisibilityTimeout": "30" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "get-queue-attributes-after-set-to-defaults": { + "Attributes": { + "ApproximateNumberOfMessages": "0", + "ApproximateNumberOfMessagesDelayed": "0", + "ApproximateNumberOfMessagesNotVisible": "0", + "CreatedTimestamp": "timestamp", + "DelaySeconds": "0", + "LastModifiedTimestamp": "timestamp", + "MaximumMessageSize": "1048576", + "MessageRetentionPeriod": "345600", + "QueueArn": "arn::sqs::111111111111:", + "ReceiveMessageWaitTimeSeconds": "0", + "SqsManagedSseEnabled": "false", + "VisibilityTimeout": "30" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_set_queue_attributes_default_values[sqs_query]": { + "recorded-date": "10-12-2025, 18:38:57", + "recorded-content": { + "get-queue-attributes-initial-values": { + "Attributes": { + "ApproximateNumberOfMessages": "0", + "ApproximateNumberOfMessagesDelayed": "0", + "ApproximateNumberOfMessagesNotVisible": "0", + "CreatedTimestamp": "timestamp", + "DelaySeconds": "0", + "LastModifiedTimestamp": "timestamp", + "MaximumMessageSize": "1048576", + "MessageRetentionPeriod": "345600", + "QueueArn": "arn::sqs::111111111111:", + "ReceiveMessageWaitTimeSeconds": "0", + "SqsManagedSseEnabled": "true", + "VisibilityTimeout": "30" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "get-queue-attributes-after-update": { + "Attributes": { + "ApproximateNumberOfMessages": "0", + "ApproximateNumberOfMessagesDelayed": "0", + "ApproximateNumberOfMessagesNotVisible": "0", + "CreatedTimestamp": "timestamp", + "DelaySeconds": "0", + "KmsDataKeyReusePeriodSeconds": "6000", + "KmsMasterKeyId": "testKeyId", + "LastModifiedTimestamp": "timestamp", + "MaximumMessageSize": "1048576", + "MessageRetentionPeriod": "345600", + "QueueArn": "arn::sqs::111111111111:", + "ReceiveMessageWaitTimeSeconds": "0", + "SqsManagedSseEnabled": "false", + "VisibilityTimeout": "30" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "get-queue-attributes-after-set-to-defaults": { + "Attributes": { + "ApproximateNumberOfMessages": "0", + "ApproximateNumberOfMessagesDelayed": "0", + "ApproximateNumberOfMessagesNotVisible": "0", + "CreatedTimestamp": "timestamp", + "DelaySeconds": "0", + "LastModifiedTimestamp": "timestamp", + "MaximumMessageSize": "1048576", + "MessageRetentionPeriod": "345600", + "QueueArn": "arn::sqs::111111111111:", + "ReceiveMessageWaitTimeSeconds": "0", + "SqsManagedSseEnabled": "false", + "VisibilityTimeout": "30" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/sqs/test_sqs.validation.json b/tests/aws/services/sqs/test_sqs.validation.json index 31d6e8e01eb2e..c86ed337b2b42 100644 --- a/tests/aws/services/sqs/test_sqs.validation.json +++ b/tests/aws/services/sqs/test_sqs.validation.json @@ -755,6 +755,24 @@ "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_set_empty_redrive_policy[sqs_query]": { "last_validated_date": "2024-08-20T14:14:11+00:00" }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_set_queue_attributes_default_values[sqs]": { + "last_validated_date": "2025-12-10T18:38:55+00:00", + "durations_in_seconds": { + "setup": 2.41, + "call": 2.34, + "teardown": 0.29, + "total": 5.04 + } + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_set_queue_attributes_default_values[sqs_query]": { + "last_validated_date": "2025-12-10T18:38:57+00:00", + "durations_in_seconds": { + "setup": 0.01, + "call": 2.21, + "teardown": 0.3, + "total": 2.52 + } + }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_set_unsupported_attribute_fifo[sqs]": { "last_validated_date": "2024-05-14T22:23:46+00:00" }, diff --git a/tests/aws/templates/sqs_fifo_autogenerate_name.yaml b/tests/aws/templates/sqs_fifo_autogenerate_name.yaml deleted file mode 100644 index 62e85cf2aa551..0000000000000 --- a/tests/aws/templates/sqs_fifo_autogenerate_name.yaml +++ /dev/null @@ -1,22 +0,0 @@ -Parameters: - IsFifo: - Type: String - -Conditions: - IsFifo: !Equals [ !Ref IsFifo, "true"] - -Resources: - FooQueueA2A23E59: - Type: AWS::SQS::Queue - Properties: - ContentBasedDeduplication: !If [ IsFifo, "true", !Ref AWS::NoValue ] - FifoQueue: !If [ IsFifo, "true", !Ref AWS::NoValue ] - VisibilityTimeout: 300 - UpdateReplacePolicy: Delete - DeletionPolicy: Delete -Outputs: - FooQueueName: - Value: - Fn::GetAtt: - - FooQueueA2A23E59 - - QueueName diff --git a/tests/aws/templates/sqs_fifo_queue.yml b/tests/aws/templates/sqs_fifo_queue.yml deleted file mode 100644 index 6d1077a421c4c..0000000000000 --- a/tests/aws/templates/sqs_fifo_queue.yml +++ /dev/null @@ -1,18 +0,0 @@ -AWSTemplateFormatVersion: 2010-09-09 -Parameters: - QueueName: - Type: String - Default: "test-queue" - Description: "Name of the SQS queue" -Resources: - FifoQueue: - Type: 'AWS::SQS::Queue' - Properties: - QueueName: !Sub "${QueueName}.fifo" - ContentBasedDeduplication: "false" - FifoQueue: "true" - -Outputs: - QueueURL: - Value: !GetAtt FifoQueue.QueueName - Description: "URL of the SQS queue" diff --git a/tests/aws/templates/sqs_queue_update_no_change.yml b/tests/aws/templates/sqs_queue_update_no_change.yml deleted file mode 100644 index dad49efefd6fb..0000000000000 --- a/tests/aws/templates/sqs_queue_update_no_change.yml +++ /dev/null @@ -1,29 +0,0 @@ -Parameters: - AddBucket: - Type: String - AllowedValues: - - "true" - - "false" - - BucketName: - Type: String - -Conditions: - ShouldDeployBucket: !Equals ["true", !Ref AddBucket] - -Resources: - Queue: - Type: AWS::SQS::Queue - - Bucket: - Type: AWS::S3::Bucket - Condition: ShouldDeployBucket - Properties: - BucketName: !Ref BucketName - -Outputs: - QueueUrl: - Value: !Ref Queue - - QueueArn: - Value: !GetAtt Queue.Arn diff --git a/tests/unit/services/cloudformation/test_provider_utils.py b/tests/unit/services/cloudformation/test_provider_utils.py index 78ee6b73727d6..dad72ee3534de 100644 --- a/tests/unit/services/cloudformation/test_provider_utils.py +++ b/tests/unit/services/cloudformation/test_provider_utils.py @@ -200,3 +200,17 @@ def test_lower_camelcase_to_pascalcase_skip_keys(self): original_dict, skip_keys={"Configuration"} ) assert converted_dict == target_dict + + def test_resource_tags_to_remove_or_update(self): + previous = [ + {"Key": "k1", "Value": "v1"}, + {"Key": "k2", "Value": "v2"}, + {"Key": "k3", "Value": "v3"}, + {"Key": "k4", "Value": "v4"}, + ] + desired = [{"Key": "k2", "Value": "v2-updated"}, {"Key": "k3", "Value": "v3"}] + + to_remove, to_update = utils.resource_tags_to_remove_or_update(previous, desired) + + assert sorted(to_remove) == ["k1", "k4"] + assert to_update == {"k2": "v2-updated", "k3": "v3"}