Repository: getmoto/moto
Pull request #8143 (2024-09-23 18:00:07)

[BUG Fix] route53 - list_tags_for_resources: Resolve when a single ID is provided
Fixes two bugs.

1. When list_tags_for_resource is passed only a single ID, the request is a string and not a list
2. The regex for deriving resource_type was always None


Patch:
diff --git a/moto/resourcegroupstaggingapi/models.py b/moto/resourcegroupstaggingapi/models.py
index f704b8609..b9bad8419 100644
--- a/moto/resourcegroupstaggingapi/models.py
+++ b/moto/resourcegroupstaggingapi/models.py
@@ -663,12 +663,27 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend):
                     "ResourceARN": f"arn:{self.partition}:ec2:{self.region_name}:{self.account_id}:vpc/{vpc.id}",
                     "Tags": tags,
                 }
+        # VPC Subnet
+        if (
+            not resource_type_filters
+            or "ec2" in resource_type_filters
+            or "ec2:subnet" in resource_type_filters
+        ):
+            for subnet in self.ec2_backend.subnets.values():
+                for subnet_id in subnet.keys():
+                    tags = format_tags(self.ec2_backend.tags.get(subnet_id, {}))
+                    if not tags or not tag_filter(tags):
+                        continue
+                    yield {
+                        "ResourceARN": f"arn:{self.partition}:ec2:{self.region_name}:{self.account_id}:subnet/{subnet_id}",
+                        "Tags": tags,
+                    }
+
         # VPC Customer Gateway
         # VPC DHCP Option Set
         # VPC Internet Gateway
         # VPC Network ACL
         # VPC Route Table
-        # VPC Subnet
         # VPC Virtual Private Gateway
         # VPC VPN Connection
 
diff --git a/moto/route53/responses.py b/moto/route53/responses.py
index 6150ae02d..56cf9a987 100644
--- a/moto/route53/responses.py
+++ b/moto/route53/responses.py
@@ -373,13 +373,16 @@ class Route53(BaseResponse):
             return 200, headers, template.render()
 
     def list_tags_for_resources(self) -> str:
-        if id_matcher := re.search(r"/tags/[-a-z]+/(.+)", self.parsed_url.path):
+        if id_matcher := re.search(r"/tags/(.+)", self.parsed_url.path):
             resource_type = unquote(id_matcher.group(1))
         else:
             resource_type = ""
         resource_ids = xmltodict.parse(self.body)["ListTagsForResourcesRequest"][
             "ResourceIds"
         ]["ResourceId"]
+        # If only one resource id is passed, it is a string, not a list
+        if not isinstance(resource_ids, list):
+            resource_ids = [resource_ids]
         tag_sets = self.backend.list_tags_for_resources(resource_ids=resource_ids)
         template = Template(LIST_TAGS_FOR_RESOURCES_RESPONSE)
         return template.render(tag_sets=tag_sets, resource_type=resource_type)


Test patch:
diff --git a/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py b/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py
index b71734ef2..05e656675 100644
--- a/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py
+++ b/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py
@@ -332,20 +332,32 @@ def test_get_resources_ec2():
 def test_get_resources_ec2_vpc():
     ec2 = boto3.resource("ec2", region_name="us-west-2")
     vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16")
-    ec2.create_tags(Resources=[vpc.id], Tags=[{"Key": "test", "Value": "test"}])
-
-    def assert_response(resp):
-        results = resp.get("ResourceTagMappingList", [])
-        assert len(results) == 1
-        assert vpc.id in results[0]["ResourceARN"]
+    ec2.create_tags(Resources=[vpc.id], Tags=[{"Key": "test", "Value": "test_vpc"}])
+    subnet = ec2.create_subnet(VpcId=vpc.id, CidrBlock="10.0.1.0/24")
+    ec2.create_tags(
+        Resources=[subnet.id], Tags=[{"Key": "test", "Value": "test_subnet"}]
+    )
 
     rtapi = boto3.client("resourcegroupstaggingapi", region_name="us-west-2")
+    # Check that we have one entry for VPC, one for the subnet
     resp = rtapi.get_resources(ResourceTypeFilters=["ec2"])
-    assert_response(resp)
+    assert len(resp["ResourceTagMappingList"]) == 2
+
+    # 1 Entry for VPC
     resp = rtapi.get_resources(ResourceTypeFilters=["ec2:vpc"])
-    assert_response(resp)
-    resp = rtapi.get_resources(TagFilters=[{"Key": "test", "Values": ["test"]}])
-    assert_response(resp)
+    assert len(resp["ResourceTagMappingList"]) == 1
+    assert "vpc/" in resp["ResourceTagMappingList"][0]["ResourceARN"]
+    resp = rtapi.get_resources(TagFilters=[{"Key": "test", "Values": ["test_vpc"]}])
+    assert len(resp["ResourceTagMappingList"]) == 1
+    assert "vpc/" in resp["ResourceTagMappingList"][0]["ResourceARN"]
+
+    # 1 Entry for Subnet
+    resp = rtapi.get_resources(ResourceTypeFilters=["ec2:subnet"])
+    assert len(resp["ResourceTagMappingList"]) == 1
+    assert "subnet/" in resp["ResourceTagMappingList"][0]["ResourceARN"]
+    resp = rtapi.get_resources(TagFilters=[{"Key": "test", "Values": ["test_subnet"]}])
+    assert len(resp["ResourceTagMappingList"]) == 1
+    assert "subnet/" in resp["ResourceTagMappingList"][0]["ResourceARN"]
 
 
 @mock_aws
diff --git a/tests/test_route53/test_route53.py b/tests/test_route53/test_route53.py
index 309333f3f..9e3d60d0b 100644
--- a/tests/test_route53/test_route53.py
+++ b/tests/test_route53/test_route53.py
@@ -634,6 +634,7 @@ def test_list_tags_for_resources():
     assert len(response["ResourceTagSets"]) == 2
     for set in response["ResourceTagSets"]:
         assert set["ResourceId"] in (zone1_id, zone2_id)
+        assert set["ResourceType"] == "hostedzone"
         if set["ResourceId"] == zone1_id:
             assert tag1 in set["Tags"]
             assert tag2 in set["Tags"]
@@ -645,6 +646,19 @@ def test_list_tags_for_resources():
             assert tag3 in set["Tags"]
             assert tag4 in set["Tags"]
 
+    # Test hostedzone with single resource
+    response = conn.list_tags_for_resources(
+        ResourceIds=[zone1_id], ResourceType="hostedzone"
+    )
+    assert len(response["ResourceTagSets"]) == 1
+    for set in response["ResourceTagSets"]:
+        assert set["ResourceId"] == zone1_id
+        assert set["ResourceType"] == "hostedzone"
+        assert tag1 in set["Tags"]
+        assert tag2 in set["Tags"]
+        assert tag3 not in set["Tags"]
+        assert tag4 not in set["Tags"]
+
     # Test healthcheck
     response = conn.list_tags_for_resources(
         ResourceIds=[healthcheck1_id, healthcheck2_id], ResourceType="healthcheck"
@@ -652,6 +666,7 @@ def test_list_tags_for_resources():
     assert len(response["ResourceTagSets"]) == 2
     for set in response["ResourceTagSets"]:
         assert set["ResourceId"] in (healthcheck1_id, healthcheck2_id)
+        assert set["ResourceType"] == "healthcheck"
         if set["ResourceId"] == healthcheck1_id:
             assert tag1 in set["Tags"]
             assert tag2 in set["Tags"]