Repository: getmoto/moto (python)

Issue:
[BUG] route53 tagging for hostedzones & list_tags_for_resources does not work properly
There are two bugs for route53.

To recreate, add `assert len(response["ResourceTagSets"]) == 2` in test_route53.py -> test_list_tags_for_resources() above `for set in response["ResourceTagSets"]:` on line 588.

list_tags_for_resources() doesn't actually return anything and it's stems from 3 bugs.

1. The **list_or_change_tags_for_resource_request** api doesn't decode the url encoding in the ID. Thus, characters like "/" are encoded when tags are added. This impacts hostedzones which have slashes in the ID.  **list_tags_for_resources** the id are not encoded, so the key is never found and returns no results.
2. The list of ids isn't capture correctly, it's passing a dictionary instead of the list of ids.
3. The template for list_tags_for_resources doesn't work as expected and needs refactoring.

The test-case mentioned doesn't test what you expect and thus passes incorrectly.

Pull request description:
[BUG FIX] route53 list_tags_for_resources no results & hostedzone ids not decoding properly
Resolves #8130 

Adds additional test scenarios

Patch:
diff --git a/moto/networkmanager/models.py b/moto/networkmanager/models.py
index 238b29c8a..a60a3886d 100644
--- a/moto/networkmanager/models.py
+++ b/moto/networkmanager/models.py
@@ -270,6 +270,11 @@ class NetworkManagerBackend(BaseBackend):
         except (KeyError, ValueError, AttributeError):
             raise ResourceNotFound(arn)
 
+    def update_resource_state(self, resource_arn: str, state: str) -> None:
+        # Acceptable states: PENDING, AVAILABLE, DELETING, UPDATING
+        resource = self._get_resource_from_arn(resource_arn)
+        resource.state = state
+
     def create_global_network(
         self,
         description: Optional[str],
diff --git a/moto/networkmanager/responses.py b/moto/networkmanager/responses.py
index d64cf8e1a..443db2e72 100644
--- a/moto/networkmanager/responses.py
+++ b/moto/networkmanager/responses.py
@@ -27,7 +27,11 @@ class NetworkManagerResponse(BaseResponse):
             description=description,
             tags=tags,
         )
-        return json.dumps(dict(GlobalNetwork=global_network.to_dict()))
+        resp_dict = dict(GlobalNetwork=global_network.to_dict())
+        self.networkmanager_backend.update_resource_state(
+            global_network.global_network_arn, "AVAILABLE"
+        )
+        return json.dumps(resp_dict)
 
     def create_core_network(self) -> str:
         params = json.loads(self.body)
@@ -43,7 +47,11 @@ class NetworkManagerResponse(BaseResponse):
             policy_document=policy_document,
             client_token=client_token,
         )
-        return json.dumps(dict(CoreNetwork=core_network.to_dict()))
+        resp_dict = dict(CoreNetwork=core_network.to_dict())
+        self.networkmanager_backend.update_resource_state(
+            core_network.core_network_arn, "AVAILABLE"
+        )
+        return json.dumps(resp_dict)
 
     def delete_core_network(self) -> str:
         core_network_id = unquote(self.path.split("/")[-1])
@@ -122,7 +130,9 @@ class NetworkManagerResponse(BaseResponse):
             location=location,
             tags=tags,
         )
-        return json.dumps(dict(Site=site.to_dict()))
+        resp_dict = dict(Site=site.to_dict())
+        self.networkmanager_backend.update_resource_state(site.site_arn, "AVAILABLE")
+        return json.dumps(resp_dict)
 
     def delete_site(self) -> str:
         global_network_id = unquote(self.path.split("/")[-3])
@@ -166,7 +176,9 @@ class NetworkManagerResponse(BaseResponse):
             site_id=site_id,
             tags=tags,
         )
-        return json.dumps(dict(Link=link.to_dict()))
+        resp_dict = dict(Link=link.to_dict())
+        self.networkmanager_backend.update_resource_state(link.link_arn, "AVAILABLE")
+        return json.dumps(resp_dict)
 
     def get_links(self) -> str:
         params = self._get_params()
@@ -222,7 +234,11 @@ class NetworkManagerResponse(BaseResponse):
             site_id=site_id,
             tags=tags,
         )
-        return json.dumps(dict(Device=device.to_dict()))
+        resp_dict = dict(Device=device.to_dict())
+        self.networkmanager_backend.update_resource_state(
+            device.device_arn, "AVAILABLE"
+        )
+        return json.dumps(resp_dict)
 
     def get_devices(self) -> str:
         params = self._get_params()
diff --git a/moto/route53/models.py b/moto/route53/models.py
index 4fe146320..dc8e4e21e 100644
--- a/moto/route53/models.py
+++ b/moto/route53/models.py
@@ -647,10 +647,12 @@ class Route53Backend(BaseBackend):
             return self.resource_tags[resource_id]
         return {}
 
-    def list_tags_for_resources(self, resource_ids: List[str]) -> Dict[str, str]:
-        resources = {}
+    def list_tags_for_resources(self, resource_ids: List[str]) -> List[Dict[str, Any]]:
+        resources = []
         for id in resource_ids:
-            resources.update(self.list_tags_for_resource(id))
+            resource_set = {"ResourceId": id, "Tags": {}}
+            resource_set["Tags"] = self.list_tags_for_resource(id)
+            resources.append(resource_set)
         return resources
 
     def list_resource_record_sets(
diff --git a/moto/route53/responses.py b/moto/route53/responses.py
index 9dbc16423..6150ae02d 100644
--- a/moto/route53/responses.py
+++ b/moto/route53/responses.py
@@ -2,7 +2,7 @@
 
 import re
 from typing import Any
-from urllib.parse import parse_qs
+from urllib.parse import parse_qs, unquote
 
 import xmltodict
 from jinja2 import Template
@@ -346,8 +346,10 @@ class Route53(BaseResponse):
     ) -> TYPE_RESPONSE:
         self.setup_class(request, full_url, headers)
 
-        id_ = self.parsed_url.path.split("/")[-1]
-        type_ = self.parsed_url.path.split("/")[-2]
+        id_matcher = re.search(r"/tags/([-a-z]+)/(.+)", self.parsed_url.path)
+        assert id_matcher
+        type_ = id_matcher.group(1)
+        id_ = unquote(id_matcher.group(2))
 
         if request.method == "GET":
             tags = self.backend.list_tags_for_resource(id_)
@@ -371,12 +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):
+            resource_type = unquote(id_matcher.group(1))
+        else:
+            resource_type = ""
         resource_ids = xmltodict.parse(self.body)["ListTagsForResourcesRequest"][
             "ResourceIds"
-        ]
+        ]["ResourceId"]
         tag_sets = self.backend.list_tags_for_resources(resource_ids=resource_ids)
         template = Template(LIST_TAGS_FOR_RESOURCES_RESPONSE)
-        return template.render(tag_sets)
+        return template.render(tag_sets=tag_sets, resource_type=resource_type)
 
     def get_change(self) -> str:
         change_id = self.parsed_url.path.rstrip("/").rsplit("/", 1)[1]
@@ -494,10 +500,10 @@ LIST_TAGS_FOR_RESOURCES_RESPONSE = """
     <ResourceTagSets>
         {% for set in tag_sets %}
         <ResourceTagSet>
-            <ResourceType>{{set.resource_type}}</ResourceType>
-            <ResourceId>{{set.resource_id}}</ResourceId>
+            <ResourceType>{{resource_type}}</ResourceType>
+            <ResourceId>{{set["ResourceId"]}}</ResourceId>
             <Tags>
-                {% for key, value in set.tags.items() %}
+                {% for key, value in set["Tags"].items() %}
                 <Tag>
                     <Key>{{key}}</Key>
                     <Value>{{value}}</Value>


Test patch:
diff --git a/tests/test_events/test_events_integration.py b/tests/test_events/test_events_integration.py
index 074c5b76b..7e03c74e3 100644
--- a/tests/test_events/test_events_integration.py
+++ b/tests/test_events/test_events_integration.py
@@ -1,9 +1,11 @@
+import contextlib
 import json
 import os
 from datetime import datetime
 from unittest import SkipTest, mock
 
 import boto3
+import pytest
 
 from moto import mock_aws, settings
 from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
@@ -110,16 +112,23 @@ def test_send_to_sqs_fifo_queue():
 
     # when
     event_time = datetime(2021, 1, 1, 12, 23, 34)
-    client_events.put_events(
-        Entries=[
-            {
-                "Time": event_time,
-                "Source": "source",
-                "DetailType": "type",
-                "Detail": json.dumps({"key": "value"}),
-            }
-        ]
+    # Catch and validate a UserWarning when running locally.
+    context = (
+        pytest.warns(UserWarning, match="you must enable content-based deduplication")
+        if not settings.TEST_SERVER_MODE
+        else contextlib.nullcontext()
     )
+    with context:
+        client_events.put_events(
+            Entries=[
+                {
+                    "Time": event_time,
+                    "Source": "source",
+                    "DetailType": "type",
+                    "Detail": json.dumps({"key": "value"}),
+                }
+            ]
+        )
 
     # then
     response = client_sqs.receive_message(
diff --git a/tests/test_networkmanager/test_networkmanager.py b/tests/test_networkmanager/test_networkmanager.py
index 0a35d08c5..09bfc2663 100644
--- a/tests/test_networkmanager/test_networkmanager.py
+++ b/tests/test_networkmanager/test_networkmanager.py
@@ -36,6 +36,10 @@ def test_create_global_network():
     assert global_network["Tags"] == [{"Key": "Name", "Value": "TestNetwork"}]
     assert global_network["State"] == "PENDING"
 
+    global_network = client.describe_global_networks()["GlobalNetworks"][0]
+    assert global_network["GlobalNetworkId"] == resp["GlobalNetwork"]["GlobalNetworkId"]
+    assert global_network["State"] == "AVAILABLE"
+
 
 @mock_aws
 def test_create_core_network():
diff --git a/tests/test_route53/test_route53.py b/tests/test_route53/test_route53.py
index cc34720a6..309333f3f 100644
--- a/tests/test_route53/test_route53.py
+++ b/tests/test_route53/test_route53.py
@@ -553,6 +553,7 @@ def test_list_or_change_tags_for_resource_request():
 @mock_aws
 def test_list_tags_for_resources():
     conn = boto3.client("route53", region_name="us-east-1")
+    # Create two hosted zones
     zone1 = conn.create_hosted_zone(
         Name="testdns1.aws.com", CallerReference=str(hash("foo"))
     )
@@ -562,11 +563,48 @@ def test_list_tags_for_resources():
     )
     zone2_id = zone2["HostedZone"]["Id"]
 
-    # confirm this works for resources with zero tags
+    # Create two healthchecks
+    health_check1 = conn.create_health_check(
+        CallerReference="foo",
+        HealthCheckConfig={
+            "IPAddress": "192.0.2.44",
+            "Port": 123,
+            "Type": "HTTP",
+            "ResourcePath": "/",
+            "RequestInterval": 30,
+            "FailureThreshold": 123,
+            "HealthThreshold": 123,
+        },
+    )
+    healthcheck1_id = health_check1["HealthCheck"]["Id"]
+
+    health_check2 = conn.create_health_check(
+        CallerReference="bar",
+        HealthCheckConfig={
+            "IPAddress": "192.0.2.44",
+            "Port": 123,
+            "Type": "HTTP",
+            "ResourcePath": "/",
+            "RequestInterval": 30,
+            "FailureThreshold": 123,
+            "HealthThreshold": 123,
+        },
+    )
+    healthcheck2_id = health_check2["HealthCheck"]["Id"]
+
+    # confirm this works for resources with zero tags for hostedzone
     response = conn.list_tags_for_resources(
         ResourceIds=[zone1_id, zone2_id], ResourceType="hostedzone"
     )
 
+    for set in response["ResourceTagSets"]:
+        assert set["Tags"] == []
+
+    # confirm this works for resources with zero tags for healthchecks
+    response = conn.list_tags_for_resources(
+        ResourceIds=[healthcheck1_id, healthcheck2_id], ResourceType="healthcheck"
+    )
+
     for set in response["ResourceTagSets"]:
         assert set["Tags"] == []
 
@@ -582,14 +620,46 @@ def test_list_tags_for_resources():
         ResourceType="hostedzone", ResourceId=zone2_id, AddTags=[tag3, tag4]
     )
 
+    conn.change_tags_for_resource(
+        ResourceType="healthcheck", ResourceId=healthcheck1_id, AddTags=[tag1, tag2]
+    )
+    conn.change_tags_for_resource(
+        ResourceType="healthcheck", ResourceId=healthcheck2_id, AddTags=[tag3, tag4]
+    )
+
+    # Test hostedzone
     response = conn.list_tags_for_resources(
         ResourceIds=[zone1_id, zone2_id], ResourceType="hostedzone"
     )
+    assert len(response["ResourceTagSets"]) == 2
     for set in response["ResourceTagSets"]:
+        assert set["ResourceId"] in (zone1_id, zone2_id)
         if set["ResourceId"] == zone1_id:
             assert tag1 in set["Tags"]
             assert tag2 in set["Tags"]
+            assert tag3 not in set["Tags"]
+            assert tag4 not in set["Tags"]
         elif set["ResourceId"] == zone2_id:
+            assert tag1 not in set["Tags"]
+            assert tag2 not in set["Tags"]
+            assert tag3 in set["Tags"]
+            assert tag4 in set["Tags"]
+
+    # Test healthcheck
+    response = conn.list_tags_for_resources(
+        ResourceIds=[healthcheck1_id, healthcheck2_id], ResourceType="healthcheck"
+    )
+    assert len(response["ResourceTagSets"]) == 2
+    for set in response["ResourceTagSets"]:
+        assert set["ResourceId"] in (healthcheck1_id, healthcheck2_id)
+        if set["ResourceId"] == healthcheck1_id:
+            assert tag1 in set["Tags"]
+            assert tag2 in set["Tags"]
+            assert tag3 not in set["Tags"]
+            assert tag4 not in set["Tags"]
+        elif set["ResourceId"] == healthcheck2_id:
+            assert tag1 not in set["Tags"]
+            assert tag2 not in set["Tags"]
             assert tag3 in set["Tags"]
             assert tag4 in set["Tags"]