Repository: getmoto/moto
Pull request #7571 (2024-04-05 16:45:09)

SQS: sqs q name validation
Closes https://github.com/getmoto/moto/issues/7559

rafcio19: @bblommers do you know why these tests are failing? Works locally for me, except some SSO tests  🤔 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ebae891fc..8be2c2af6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,24 @@
 Moto Changelog
 ==============
 
+5.0.5
+-----
+Docker Digest for 5.0.5: _sha256:b95cf0d65557475f29e7256938028eef352e23acafe8e07c071cd58b67c44708_
+
+    New Methods:
+        * SecretsManager:
+            * batch_get_secret_value()
+
+    Miscellaneous:
+        * ApplicationAutoscaling: put_scaling_policy() now generates CW alarms for DynamoDB and ECS
+        * DynamoDB: Fix pagination for scan()/query()
+        * DynamoDB: batch_write_item() now validates for duplicate DELETE or PUT requests
+        * Events: put_events() now validates that input-values cannot be empty
+        * IOT: create_topic_rule() now validates name-format
+        * ResourceGroupsTaggingAPI: tag_resources() now supports RDS snapshots
+        * SFN: Fixed a bug where the executionInput was double encoded
+
+
 5.0.4
 -----
 Docker Digest for 5.0.4: _sha256:e13e917e563bd1e3bb745b0ce914bdcc3bd4577542e13e1468eac5078774b2aa_
diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index 0238a60f1..116ce7282 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -7143,9 +7143,9 @@
 
 ## secretsmanager
 <details>
-<summary>86% implemented</summary>
+<summary>91% implemented</summary>
 
-- [ ] batch_get_secret_value
+- [X] batch_get_secret_value
 - [X] cancel_rotate_secret
 - [X] create_secret
 - [X] delete_resource_policy
diff --git a/docs/docs/services/application-autoscaling.rst b/docs/docs/services/application-autoscaling.rst
index 46d0ed411..f71fe88ac 100644
--- a/docs/docs/services/application-autoscaling.rst
+++ b/docs/docs/services/application-autoscaling.rst
@@ -17,11 +17,7 @@ application-autoscaling
 - [X] delete_scaling_policy
 - [X] delete_scheduled_action
 - [X] deregister_scalable_target
-  Registers or updates a scalable target.
-
 - [X] describe_scalable_targets
-  Describe scalable targets.
-
 - [ ] describe_scaling_activities
 - [X] describe_scaling_policies
 - [X] describe_scheduled_actions
@@ -33,8 +29,6 @@ application-autoscaling
 - [X] put_scaling_policy
 - [X] put_scheduled_action
 - [X] register_scalable_target
-  Registers or updates a scalable target.
-
 - [ ] tag_resource
 - [ ] untag_resource
 
diff --git a/docs/docs/services/secretsmanager.rst b/docs/docs/services/secretsmanager.rst
index 0fdaad7b5..98bb72286 100644
--- a/docs/docs/services/secretsmanager.rst
+++ b/docs/docs/services/secretsmanager.rst
@@ -14,7 +14,7 @@ secretsmanager
 
 |start-h3| Implemented features for this service |end-h3|
 
-- [ ] batch_get_secret_value
+- [X] batch_get_secret_value
 - [X] cancel_rotate_secret
 - [X] create_secret
 - [X] delete_resource_policy
diff --git a/moto/__init__.py b/moto/__init__.py
index f52c14b40..10bb8c7d3 100644
--- a/moto/__init__.py
+++ b/moto/__init__.py
@@ -1,4 +1,4 @@
 from moto.core.decorator import mock_aws as mock_aws
 
 __title__ = "moto"
-__version__ = "5.0.5.dev"
+__version__ = "5.0.6.dev"
diff --git a/moto/cognitoidp/models.py b/moto/cognitoidp/models.py
index e1dd68cc9..2b8917197 100644
--- a/moto/cognitoidp/models.py
+++ b/moto/cognitoidp/models.py
@@ -1964,7 +1964,7 @@ class CognitoIdpBackend(BaseBackend):
             if not refresh_token:
                 raise ResourceNotFoundError(refresh_token)
 
-            res = user_pool.refresh_tokens[refresh_token]
+            res = user_pool.refresh_tokens.get(refresh_token)
             if res is None:
                 raise NotAuthorizedError("Refresh Token has been revoked")
 
diff --git a/moto/iot/models.py b/moto/iot/models.py
index 7b0e2a48c..2193a960c 100644
--- a/moto/iot/models.py
+++ b/moto/iot/models.py
@@ -1796,6 +1796,9 @@ class IoTBackend(BaseBackend):
         return self.rules[rule_name].to_get_dict()
 
     def create_topic_rule(self, rule_name: str, sql: str, **kwargs: Any) -> None:
+        if not re.match("^[a-zA-Z0-9_]+$", rule_name):
+            msg = f"1 validation error detected: Value '{rule_name}' at 'ruleName' failed to satisfy constraint: Member must satisfy regular expression pattern: ^[a-zA-Z0-9_]+$"
+            raise InvalidRequestException(msg)
         if rule_name in self.rules:
             raise ResourceAlreadyExistsException(
                 "Rule with given name already exists", "", self.rules[rule_name].arn
diff --git a/moto/resourcegroupstaggingapi/models.py b/moto/resourcegroupstaggingapi/models.py
index c8f48559a..657a74cdb 100644
--- a/moto/resourcegroupstaggingapi/models.py
+++ b/moto/resourcegroupstaggingapi/models.py
@@ -122,8 +122,11 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend):
         return dynamodb_backends[self.account_id][self.region_name]
 
     @property
-    def workspaces_backend(self) -> WorkSpacesBackend:
-        return workspaces_backends[self.account_id][self.region_name]
+    def workspaces_backend(self) -> Optional[WorkSpacesBackend]:
+        # Workspaces service has limited region availability
+        if self.region_name in workspaces_backends[self.account_id].regions:
+            return workspaces_backends[self.account_id][self.region_name]
+        return None
 
     def _get_resources_generator(
         self,
@@ -537,7 +540,9 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend):
                 }
 
         # Workspaces
-        if not resource_type_filters or "workspaces" in resource_type_filters:
+        if self.workspaces_backend and (
+            not resource_type_filters or "workspaces" in resource_type_filters
+        ):
             for ws in self.workspaces_backend.workspaces.values():
                 tags = format_tag_keys(ws.tags, ["Key", "Value"])
                 if not tags or not tag_filter(
@@ -551,7 +556,9 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend):
                 }
 
         # Workspace Directories
-        if not resource_type_filters or "workspaces-directory" in resource_type_filters:
+        if self.workspaces_backend and (
+            not resource_type_filters or "workspaces-directory" in resource_type_filters
+        ):
             for wd in self.workspaces_backend.workspace_directories.values():
                 tags = format_tag_keys(wd.tags, ["Key", "Value"])
                 if not tags or not tag_filter(
@@ -565,7 +572,9 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend):
                 }
 
         # Workspace Images
-        if not resource_type_filters or "workspaces-image" in resource_type_filters:
+        if self.workspaces_backend and (
+            not resource_type_filters or "workspaces-image" in resource_type_filters
+        ):
             for wi in self.workspaces_backend.workspace_images.values():
                 tags = format_tag_keys(wi.tags, ["Key", "Value"])
                 if not tags or not tag_filter(
@@ -926,18 +935,18 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend):
             "ErrorMessage": "Service not yet supported",
         }
         for arn in resource_arns:
-            if arn.startswith("arn:aws:rds:"):
+            if arn.startswith("arn:aws:rds:") or arn.startswith("arn:aws:snapshot:"):
                 self.rds_backend.add_tags_to_resource(
                     arn, TaggingService.convert_dict_to_tags_input(tags)
                 )
             elif arn.startswith("arn:aws:workspaces:"):
                 resource_id = arn.split("/")[-1]
-                self.workspaces_backend.create_tags(
+                self.workspaces_backend.create_tags(  # type: ignore[union-attr]
                     resource_id, TaggingService.convert_dict_to_tags_input(tags)
                 )
             elif arn.startswith("arn:aws:logs:"):
                 self.logs_backend.tag_resource(arn, tags)
-            if arn.startswith("arn:aws:dynamodb"):
+            elif arn.startswith("arn:aws:dynamodb"):
                 self.dynamodb_backend.tag_resource(
                     arn, TaggingService.convert_dict_to_tags_input(tags)
                 )
diff --git a/moto/sqs/models.py b/moto/sqs/models.py
index 3b6f3efb6..17b4384df 100644
--- a/moto/sqs/models.py
+++ b/moto/sqs/models.py
@@ -314,6 +314,22 @@ class Queue(CloudFormationModel):
         ):
             raise InvalidAttributeValue("MaximumMessageSize")
 
+    @property
+    def name(self) -> str:
+        return self._name
+
+    @name.setter
+    def name(self, value: str) -> None:
+        # https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_CreateQueue.html#SQS-CreateQueue-request-QueueName
+        if re.match(r"^[a-zA-Z0-9\_-]{1,80}$", value) or re.match(
+            r"^[a-zA-Z0-9\_-]{1,75}(\.fifo)?$", value
+        ):
+            self._name = value
+        else:
+            raise InvalidParameterValue(
+                "Can only include alphanumeric characters, hyphens, or underscores. 1 to 80 in length"
+            )
+
     @property
     def pending_messages(self) -> Set[Message]:
         return self._pending_messages
@@ -448,7 +464,9 @@ class Queue(CloudFormationModel):
         tags_dict = tags_from_cloudformation_tags_list(tags)
 
         # Could be passed as an integer - just treat it as a string
-        resource_name = str(resource_name)
+        # take first 80 characters of the q name, more is invalid
+        # https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_CreateQueue.html#SQS-CreateQueue-request-QueueName
+        resource_name = str(resource_name)[0:80]
 
         sqs_backend = sqs_backends[account_id][region_name]
         return sqs_backend.create_queue(
diff --git a/setup.cfg b/setup.cfg
index 8296c53a2..1594df76a 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -54,7 +54,7 @@ all =
     openapi-spec-validator>=0.5.0
     pyparsing>=3.0.7
     jsondiff>=1.1.2
-    py-partiql-parser==0.5.2
+    py-partiql-parser==0.5.4
     aws-xray-sdk!=0.96,>=0.93
     setuptools
     multipart
@@ -69,7 +69,7 @@ proxy =
     openapi-spec-validator>=0.5.0
     pyparsing>=3.0.7
     jsondiff>=1.1.2
-    py-partiql-parser==0.5.2
+    py-partiql-parser==0.5.4
     aws-xray-sdk!=0.96,>=0.93
     setuptools
     multipart
@@ -84,7 +84,7 @@ server =
     openapi-spec-validator>=0.5.0
     pyparsing>=3.0.7
     jsondiff>=1.1.2
-    py-partiql-parser==0.5.2
+    py-partiql-parser==0.5.4
     aws-xray-sdk!=0.96,>=0.93
     setuptools
     flask!=2.2.0,!=2.2.1
@@ -119,7 +119,7 @@ cloudformation =
     openapi-spec-validator>=0.5.0
     pyparsing>=3.0.7
     jsondiff>=1.1.2
-    py-partiql-parser==0.5.2
+    py-partiql-parser==0.5.4
     aws-xray-sdk!=0.96,>=0.93
     setuptools
 cloudfront =
@@ -141,10 +141,10 @@ dms =
 ds =
 dynamodb =
     docker>=3.0.0
-    py-partiql-parser==0.5.2
+    py-partiql-parser==0.5.4
 dynamodbstreams =
     docker>=3.0.0
-    py-partiql-parser==0.5.2
+    py-partiql-parser==0.5.4
 ebs =
 ec2 =
 ec2instanceconnect =
@@ -208,15 +208,15 @@ resourcegroupstaggingapi =
     openapi-spec-validator>=0.5.0
     pyparsing>=3.0.7
     jsondiff>=1.1.2
-    py-partiql-parser==0.5.2
+    py-partiql-parser==0.5.4
 route53 =
 route53resolver =
 s3 =
     PyYAML>=5.1
-    py-partiql-parser==0.5.2
+    py-partiql-parser==0.5.4
 s3crc32c =
     PyYAML>=5.1
-    py-partiql-parser==0.5.2
+    py-partiql-parser==0.5.4
     crc32c
 s3control =
 sagemaker =


Test patch:
diff --git a/tests/test_cloudformation/test_stack_parsing.py b/tests/test_cloudformation/test_stack_parsing.py
index 5585d5c8e..0188adaee 100644
--- a/tests/test_cloudformation/test_stack_parsing.py
+++ b/tests/test_cloudformation/test_stack_parsing.py
@@ -105,7 +105,7 @@ sub_template = {
         "Queue1": {
             "Type": "AWS::SQS::Queue",
             "Properties": {
-                "QueueName": {"Fn::Sub": "${AWS::StackName}-queue-${!Literal}"},
+                "QueueName": {"Fn::Sub": "${AWS::StackName}-queue"},
                 "VisibilityTimeout": 60,
             },
         },
diff --git a/tests/test_iot/test_iot_topic_rules.py b/tests/test_iot/test_iot_topic_rules.py
index 89a425e22..f088f9fd4 100644
--- a/tests/test_iot/test_iot_topic_rules.py
+++ b/tests/test_iot/test_iot_topic_rules.py
@@ -4,7 +4,7 @@ from botocore.exceptions import ClientError
 
 from moto import mock_aws
 
-name = "my-rule"
+name = "myrule"
 payload = {
     "sql": "SELECT * FROM 'topic/*' WHERE something > 0",
     "actions": [
@@ -19,6 +19,17 @@ payload = {
 }
 
 
+@mock_aws
+@pytest.mark.parametrize("invalid_name", ["t-r", "t.r", "t r"])
+def test_topic_rule_create_with_invalid_name(invalid_name):
+    client = boto3.client("iot", region_name="us-east-1")
+    with pytest.raises(ClientError) as exc:
+        client.create_topic_rule(ruleName=invalid_name, topicRulePayload=payload)
+    err = exc.value.response["Error"]
+    assert err["Code"] == "InvalidRequestException"
+    assert "Member must satisfy regular expression pattern" in err["Message"]
+
+
 @mock_aws
 def test_topic_rule_create():
     client = boto3.client("iot", region_name="ap-northeast-1")
@@ -41,11 +52,11 @@ def test_topic_rule_list():
     assert len(res["rules"]) == 0
 
     client.create_topic_rule(ruleName=name, topicRulePayload=payload)
-    client.create_topic_rule(ruleName="my-rule-2", topicRulePayload=payload)
+    client.create_topic_rule(ruleName=f"{name}2", topicRulePayload=payload)
 
     res = client.list_topic_rules()
     assert len(res["rules"]) == 2
-    for rule, rule_name in zip(res["rules"], [name, "my-rule-2"]):
+    for rule, rule_name in zip(res["rules"], [name, f"{name}2"]):
         assert rule["ruleName"] == rule_name
         assert rule["createdAt"] is not None
         assert rule["ruleArn"] is not None
diff --git a/tests/test_resourcegroupstaggingapi/test_resourcegroupstagging_glue.py b/tests/test_resourcegroupstaggingapi/test_resourcegroupstagging_glue.py
index b6165fa97..627a2cd31 100644
--- a/tests/test_resourcegroupstaggingapi/test_resourcegroupstagging_glue.py
+++ b/tests/test_resourcegroupstaggingapi/test_resourcegroupstagging_glue.py
@@ -8,7 +8,7 @@ from moto.core import DEFAULT_ACCOUNT_ID
 
 @mock_aws
 def test_glue_jobs():
-    glue = boto3.client("glue", region_name="us-west-2")
+    glue = boto3.client("glue", region_name="us-west-1")
     tag_key = str(uuid4())[0:6]
     tag_val = str(uuid4())[0:6]
     job_name = glue.create_job(
@@ -17,9 +17,9 @@ def test_glue_jobs():
         Command={"Name": "test_command"},
         Tags={tag_key: tag_val},
     )["Name"]
-    job_arn = f"arn:aws:glue:us-west-2:{DEFAULT_ACCOUNT_ID}:job/{job_name}"
+    job_arn = f"arn:aws:glue:us-west-1:{DEFAULT_ACCOUNT_ID}:job/{job_name}"
 
-    rtapi = boto3.client("resourcegroupstaggingapi", region_name="us-west-2")
+    rtapi = boto3.client("resourcegroupstaggingapi", region_name="us-west-1")
     resources = rtapi.get_resources(ResourceTypeFilters=["glue"])[
         "ResourceTagMappingList"
     ]
diff --git a/tests/test_sqs/test_sqs.py b/tests/test_sqs/test_sqs.py
index 7b6993fe5..c57286b23 100644
--- a/tests/test_sqs/test_sqs.py
+++ b/tests/test_sqs/test_sqs.py
@@ -60,6 +60,30 @@ def test_create_fifo_queue_fail():
         raise RuntimeError("Should have raised InvalidParameterValue Exception")
 
 
+@mock_aws
+@pytest.mark.parametrize(
+    "queue_name",
+    [
+        "",
+        "ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp",
+        "/my/test",
+        "!@£$%^&*()queue",
+    ],
+)
+def test_create_fifo_queue_invalid_name(queue_name):
+    sqs = boto3.client("sqs", region_name=REGION)
+
+    with pytest.raises(ClientError) as ex:
+        sqs.create_queue(QueueName=queue_name)
+
+    err = ex.value.response["Error"]
+    assert err["Code"] == "InvalidParameterValue"
+    assert (
+        err["Message"]
+        == "Can only include alphanumeric characters, hyphens, or underscores. 1 to 80 in length"
+    )
+
+
 @mock_aws
 def test_create_queue_with_same_attributes():
     sqs = boto3.client("sqs", region_name=REGION)
@@ -171,10 +195,19 @@ def test_create_fifo_queue_with_high_throughput():
 
 
 @mock_aws
-def test_create_queue():
+@pytest.mark.parametrize(
+    "q_name",
+    [
+        str(uuid4())[0:6],
+        "name_with_underscores",
+        "name-with-hyphens",
+        "Name-with_all_the_THings",
+    ],
+    ids=["random", "underscores", "hyphens", "combined"],
+)
+def test_create_queue(q_name):
     sqs = boto3.resource("sqs", region_name=REGION)
 
-    q_name = str(uuid4())[0:6]
     new_queue = sqs.create_queue(QueueName=q_name)
     assert q_name in getattr(new_queue, "url")