Repository: getmoto/moto (python)

Issue:
ECR only describe_images returns all the tags for given image
If image has multiple tags, only the describe_images operation returns them. Other ones, like list_images return only the latest tag.

Pull request description:
ECR: list_images() should list images with multiple tags separately
Fixes #8269 

The `list_images()`-method returns a `dict(imageDigest=..., imageTag=...)`, which means that we images may be listed multiple times if they have multiple tags.

The original implementation ([added 7 years ago!](https://github.com/getmoto/moto/pull/961)) didn't account for that, and would only return every image with it's latest tag and ignore other tags.

Patch:
diff --git a/CLOUDFORMATION_COVERAGE.md b/CLOUDFORMATION_COVERAGE.md
index b7853a20f..34bcedc0a 100644
--- a/CLOUDFORMATION_COVERAGE.md
+++ b/CLOUDFORMATION_COVERAGE.md
@@ -277,6 +277,11 @@ Please let us know if you'd like support for a resource not yet listed here.
    - [x] update implemented
    - [x] delete implemented
    - [x] Fn::GetAtt implemented
+- AWS::IoT::RoleAlias:
+   - [x] create implemented
+   - [x] update implemented
+   - [x] delete implemented
+   - [x] Fn::GetAtt implemented
 - AWS::KMS::Key:
    - [x] create implemented
    - [ ] update implemented
diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index 1626ede03..e5ee66fff 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -4539,7 +4539,7 @@
 - [ ] create_provisioning_claim
 - [ ] create_provisioning_template
 - [ ] create_provisioning_template_version
-- [ ] create_role_alias
+- [X] create_role_alias
 - [ ] create_scheduled_audit
 - [ ] create_security_profile
 - [ ] create_stream
@@ -4572,7 +4572,7 @@
 - [ ] delete_provisioning_template
 - [ ] delete_provisioning_template_version
 - [ ] delete_registration_code
-- [ ] delete_role_alias
+- [X] delete_role_alias
 - [ ] delete_scheduled_audit
 - [ ] delete_security_profile
 - [ ] delete_stream
@@ -4609,7 +4609,7 @@
 - [ ] describe_mitigation_action
 - [ ] describe_provisioning_template
 - [ ] describe_provisioning_template_version
-- [ ] describe_role_alias
+- [X] describe_role_alias
 - [ ] describe_scheduled_audit
 - [ ] describe_security_profile
 - [ ] describe_stream
@@ -4682,7 +4682,7 @@
 - [ ] list_provisioning_template_versions
 - [ ] list_provisioning_templates
 - [ ] list_related_resources_for_audit_finding
-- [ ] list_role_aliases
+- [X] list_role_aliases
 - [ ] list_sbom_validation_results
 - [ ] list_scheduled_audits
 - [ ] list_security_profiles
@@ -4749,7 +4749,7 @@
 - [ ] update_package_configuration
 - [ ] update_package_version
 - [ ] update_provisioning_template
-- [ ] update_role_alias
+- [X] update_role_alias
 - [ ] update_scheduled_audit
 - [ ] update_security_profile
 - [ ] update_stream
diff --git a/moto/ecr/models.py b/moto/ecr/models.py
index 71207497b..70f325814 100644
--- a/moto/ecr/models.py
+++ b/moto/ecr/models.py
@@ -337,15 +337,6 @@ class Image(BaseObject):
             k: v for k, v in response_object.items() if v is not None and v != [None]
         }
 
-    @property
-    def response_list_object(self) -> Dict[str, Any]:  # type: ignore[misc]
-        response_object = self.gen_response_object()
-        response_object["imageTag"] = self.image_tag
-        response_object["imageDigest"] = self.get_image_digest()
-        return {
-            k: v for k, v in response_object.items() if v is not None and v != [None]
-        }
-
     @property
     def response_describe_object(self) -> Dict[str, Any]:  # type: ignore[misc]
         response_object = self.gen_response_object()
diff --git a/moto/ecr/responses.py b/moto/ecr/responses.py
index 9ec4220ec..3e114a905 100644
--- a/moto/ecr/responses.py
+++ b/moto/ecr/responses.py
@@ -69,9 +69,11 @@ class ECRResponse(BaseResponse):
         repository_str = self._get_param("repositoryName")
         registry_id = self._get_param("registryId")
         images = self.ecr_backend.list_images(repository_str, registry_id)
-        return json.dumps(
-            {"imageIds": [image.response_list_object for image in images]}
-        )
+        resp = []
+        for image in images:
+            for tag in image.image_tags or [None]:  # type: ignore
+                resp.append({"imageDigest": image.image_digest, "imageTag": tag})
+        return json.dumps({"imageIds": resp})
 
     def describe_images(self) -> str:
         repository_str = self._get_param("repositoryName")
diff --git a/moto/iot/models.py b/moto/iot/models.py
index 87746f6a4..a92765c95 100644
--- a/moto/iot/models.py
+++ b/moto/iot/models.py
@@ -48,6 +48,7 @@ class FakeThing(CloudFormationModel):
         region_name: str,
     ):
         self.region_name = region_name
+        self.account_id = account_id
         self.thing_id = str(random.uuid4())
         self.thing_name = thing_name
         self.thing_type = thing_type
@@ -80,6 +81,8 @@ class FakeThing(CloudFormationModel):
         include_default_client_id: bool = False,
         include_connectivity: bool = False,
         include_thing_id: bool = False,
+        include_thing_group_names: bool = False,
+        include_shadows_as_json: bool = False,
     ) -> Dict[str, Any]:
         obj = {
             "thingName": self.thing_name,
@@ -98,6 +101,29 @@ class FakeThing(CloudFormationModel):
             }
         if include_thing_id:
             obj["thingId"] = self.thing_id
+        if include_thing_group_names:
+            iot_backend = iot_backends[self.account_id][self.region_name]
+            obj["thingGroupNames"] = [
+                thing_group.thing_group_name
+                for thing_group in iot_backend.list_thing_groups(None, None, None)
+                if self.arn in thing_group.things
+            ]
+        if include_shadows_as_json:
+            named_shadows = {
+                shadow_name: shadow.to_dict()
+                for shadow_name, shadow in self.thing_shadows.items()
+                if shadow_name is not None
+            }
+            converted_response_format = {
+                shadow_name: {
+                    **shadow_data["state"],
+                    "metadata": shadow_data["metadata"],
+                    "version": shadow_data["version"],
+                    "hasDelta": "delta" in shadow_data["state"],
+                }
+                for shadow_name, shadow_data in named_shadows.items()
+            }
+            obj["shadow"] = json.dumps({"name": converted_response_format})
         return obj
 
     @staticmethod
@@ -899,6 +925,108 @@ class FakeDomainConfiguration(BaseModel):
         }
 
 
+class FakeRoleAlias(CloudFormationModel):
+    def __init__(
+        self,
+        role_alias: str,
+        role_arn: str,
+        account_id: str,
+        region_name: str,
+        credential_duration_seconds: int = 3600,
+    ):
+        self.role_alias = role_alias
+        self.role_arn = role_arn
+        self.credential_duration_seconds = credential_duration_seconds
+        self.account_id = account_id
+        self.region_name = region_name
+        self.creation_date = time.time()
+        self.last_modified_date = self.creation_date
+        self.arn = f"arn:{get_partition(self.region_name)}:iot:{self.region_name}:{self.account_id}:rolealias/{role_alias}"
+
+    def to_dict(self) -> Dict[str, Any]:
+        return {
+            "roleAlias": self.role_alias,
+            "roleAliasArn": self.arn,
+            "roleArn": self.role_arn,
+            "owner": self.account_id,
+            "credentialDurationSeconds": self.credential_duration_seconds,
+            "creationDate": self.creation_date,
+            "lastModifiedDate": self.last_modified_date,
+        }
+
+    @staticmethod
+    def cloudformation_name_type() -> str:
+        return "RoleAlias"
+
+    @staticmethod
+    def cloudformation_type() -> str:
+        return "AWS::IoT::RoleAlias"
+
+    @classmethod
+    def has_cfn_attr(cls, attr: str) -> bool:
+        return attr in [
+            "RoleAliasArn",
+        ]
+
+    def get_cfn_attribute(self, attribute_name: str) -> Any:
+        from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
+
+        if attribute_name == "RoleAliasArn":
+            return self.arn
+        raise UnformattedGetAttTemplateException()
+
+    @classmethod
+    def create_from_cloudformation_json(  # type: ignore[misc]
+        cls,
+        resource_name: str,
+        cloudformation_json: Any,
+        account_id: str,
+        region_name: str,
+        **kwargs: Any,
+    ) -> "FakeRoleAlias":
+        iot_backend = iot_backends[account_id][region_name]
+        properties = cloudformation_json["Properties"]
+
+        role_alias_name = properties.get("RoleAlias", resource_name)
+        role_arn = properties.get("RoleArn")
+        credential_duration_seconds = properties.get("CredentialDurationSeconds", 3600)
+
+        return iot_backend.create_role_alias(
+            role_alias_name=role_alias_name,
+            role_arn=role_arn,
+            credential_duration_seconds=credential_duration_seconds,
+        )
+
+    @classmethod
+    def update_from_cloudformation_json(  # type: ignore[misc]
+        cls,
+        original_resource: "FakeRoleAlias",
+        new_resource_name: str,
+        cloudformation_json: Any,
+        account_id: str,
+        region_name: str,
+    ) -> "FakeRoleAlias":
+        iot_backend = iot_backends[account_id][region_name]
+        iot_backend.delete_role_alias(role_alias_name=original_resource.role_alias)
+        return cls.create_from_cloudformation_json(
+            new_resource_name, cloudformation_json, account_id, region_name
+        )
+
+    @classmethod
+    def delete_from_cloudformation_json(  # type: ignore[misc]
+        cls,
+        resource_name: str,
+        cloudformation_json: Any,
+        account_id: str,
+        region_name: str,
+    ) -> None:
+        properties = cloudformation_json["Properties"]
+        role_alias_name = properties.get("RoleAlias", resource_name)
+
+        iot_backend = iot_backends[account_id][region_name]
+        iot_backend.delete_role_alias(role_alias_name=role_alias_name)
+
+
 class IoTBackend(BaseBackend):
     def __init__(self, region_name: str, account_id: str):
         super().__init__(region_name, account_id)
@@ -917,6 +1045,7 @@ class IoTBackend(BaseBackend):
             OrderedDict()
         )
         self.rules: Dict[str, FakeRule] = OrderedDict()
+        self.role_aliases: Dict[str, FakeRoleAlias] = OrderedDict()
         self.endpoint: Optional[FakeEndpoint] = None
         self.domain_configurations: Dict[str, FakeDomainConfiguration] = OrderedDict()
 
@@ -2195,7 +2324,65 @@ class IoTBackend(BaseBackend):
         things = [
             thing for thing in self.things.values() if thing.matches(query_string)
         ]
-        return [t.to_dict(include_connectivity=True) for t in things]
+        return [
+            t.to_dict(
+                include_connectivity=True,
+                include_thing_id=True,
+                include_thing_group_names=True,
+                include_shadows_as_json=True,
+            )
+            for t in things
+        ]
+
+    def create_role_alias(
+        self,
+        role_alias_name: str,
+        role_arn: str,
+        credential_duration_seconds: int = 3600,
+    ) -> FakeRoleAlias:
+        if role_alias_name in self.role_aliases:
+            current_role_alias = self.role_aliases[role_alias_name]
+            raise ResourceAlreadyExistsException(
+                f"RoleAlias cannot be created - already exists (name={role_alias_name})",
+                current_role_alias.role_alias,
+                current_role_alias.arn,
+            )
+        new_role_alias = FakeRoleAlias(
+            role_alias=role_alias_name,
+            role_arn=role_arn,
+            account_id=self.account_id,
+            region_name=self.region_name,
+            credential_duration_seconds=credential_duration_seconds,
+        )
+        self.role_aliases[role_alias_name] = new_role_alias
+        return new_role_alias
+
+    def list_role_aliases(self) -> Iterable[FakeRoleAlias]:
+        return self.role_aliases.values()
+
+    def describe_role_alias(self, role_alias_name: str) -> FakeRoleAlias:
+        if role_alias_name not in self.role_aliases:
+            raise ResourceNotFoundException(
+                f"RoleAlias not found (name= {role_alias_name})",
+            )
+        return self.role_aliases[role_alias_name]
+
+    def update_role_alias(
+        self,
+        role_alias_name: str,
+        role_arn: Optional[str] = None,
+        credential_duration_seconds: Optional[int] = None,
+    ) -> FakeRoleAlias:
+        role_alias = self.describe_role_alias(role_alias_name=role_alias_name)
+        if role_arn:
+            role_alias.role_arn = role_arn
+        if credential_duration_seconds:
+            role_alias.credential_duration_seconds = credential_duration_seconds
+        return role_alias
+
+    def delete_role_alias(self, role_alias_name: str) -> None:
+        self.describe_role_alias(role_alias_name=role_alias_name)
+        del self.role_aliases[role_alias_name]
 
 
 iot_backends = BackendDict(IoTBackend, "iot")
diff --git a/moto/iot/responses.py b/moto/iot/responses.py
index f4bb04e72..7d256dbb6 100644
--- a/moto/iot/responses.py
+++ b/moto/iot/responses.py
@@ -769,3 +769,60 @@ class IoTResponse(BaseResponse):
         query = self._get_param("queryString")
         things = self.iot_backend.search_index(query)
         return json.dumps({"things": things, "thingGroups": []})
+
+    def create_role_alias(self) -> str:
+        role_alias_name = self._get_param("roleAlias")
+        role_arn = self._get_param("roleArn")
+        credential_duration_seconds = self._get_int_param(
+            "credentialDurationSeconds", 3600
+        )
+        created_role_alias = self.iot_backend.create_role_alias(
+            role_alias_name=role_alias_name,
+            role_arn=role_arn,
+            credential_duration_seconds=credential_duration_seconds,
+        )
+        return json.dumps(
+            dict(
+                roleAlias=created_role_alias.role_alias,
+                roleAliasArn=created_role_alias.arn,
+            )
+        )
+
+    def list_role_aliases(self) -> str:
+        # page_size = self._get_int_param("pageSize")
+        # marker = self._get_param("marker")
+        # ascending_order = self._get_param("ascendingOrder")
+        return json.dumps(
+            dict(
+                roleAliases=[_.role_alias for _ in self.iot_backend.list_role_aliases()]
+            )
+        )
+
+    def describe_role_alias(self) -> str:
+        role_alias_name = self._get_param("roleAlias")
+        role_alias = self.iot_backend.describe_role_alias(
+            role_alias_name=role_alias_name
+        )
+        return json.dumps({"roleAliasDescription": role_alias.to_dict()})
+
+    def update_role_alias(self) -> str:
+        role_alias_name = self._get_param("roleAlias")
+        role_arn = self._get_param("roleArn", None)
+        credential_duration_seconds = self._get_int_param(
+            "credentialDurationSeconds", 0
+        )
+
+        role_alias = self.iot_backend.update_role_alias(
+            role_alias_name=role_alias_name,
+            role_arn=role_arn,
+            credential_duration_seconds=credential_duration_seconds,
+        )
+
+        return json.dumps(
+            dict(roleAlias=role_alias.role_alias, roleAliasArn=role_alias.arn)
+        )
+
+    def delete_role_alias(self) -> str:
+        role_alias_name = self._get_param("roleAlias")
+        self.iot_backend.delete_role_alias(role_alias_name=role_alias_name)
+        return json.dumps({})


Test patch:
diff --git a/tests/test_ecr/test_ecr_boto3.py b/tests/test_ecr/test_ecr_boto3.py
index a810718c4..8086df85e 100644
--- a/tests/test_ecr/test_ecr_boto3.py
+++ b/tests/test_ecr/test_ecr_boto3.py
@@ -677,6 +677,35 @@ def test_list_images():
     assert "sha" in response["imageIds"][0]["imageDigest"]
 
 
+@mock_aws
+def test_list_same_image_with_multiple_tags():
+    ecr_client = boto3.client("ecr", "us-east-1")
+
+    ecr_client.create_repository(repositoryName="reponame")
+
+    manifest = json.dumps(_create_image_manifest())
+
+    ecr_client.put_image(
+        repositoryName="reponame", imageTag="tag1", imageManifest=manifest
+    )
+    ecr_client.put_image(
+        repositoryName="reponame", imageTag="tag2", imageManifest=manifest
+    )
+
+    images = ecr_client.list_images(repositoryName="reponame")["imageIds"]
+    assert len(images) == 2
+    assert images[0]["imageDigest"] == images[1]["imageDigest"]
+    assert images[0]["imageTag"] == "tag1"
+    assert images[1]["imageTag"] == "tag2"
+
+    # Ensure tag is not required
+    manifest2 = _create_image_manifest()
+    ecr_client.put_image(repositoryName="reponame", imageManifest=json.dumps(manifest2))
+
+    images = ecr_client.list_images(repositoryName="reponame")["imageIds"]
+    assert {"imageDigest": manifest2["config"]["digest"]} in images
+
+
 @mock_aws
 def test_list_images_from_repository_that_doesnt_exist():
     client = boto3.client("ecr", region_name=ECR_REGION)
diff --git a/tests/test_iot/test_iot_cloudformation.py b/tests/test_iot/test_iot_cloudformation.py
index d30fbdbc2..27199d2cb 100644
--- a/tests/test_iot/test_iot_cloudformation.py
+++ b/tests/test_iot/test_iot_cloudformation.py
@@ -552,3 +552,136 @@ def test_delete_stack_should_delete_policy():
     # then delete stack
     cfn_conn.delete_stack(StackName=stack_name)
     assert len(iot_conn.list_policies()["policies"]) == 0
+
+
+@mock_aws
+def test_create_role_alias_with_simple_cloudformation():
+    # given
+    stack_name = "test_stack"
+    template = {
+        "AWSTemplateFormatVersion": "2010-09-09",
+        "Description": "IOT RoleAlias CloudFormation",
+        "Resources": {
+            "testRoleAlias": {
+                "Type": "AWS::IoT::RoleAlias",
+                "Properties": {
+                    "RoleAlias": "TestRoleAlias",
+                    "RoleArn": "arn:aws:iam::123456789012:role/my-role",
+                },
+            },
+        },
+        "Outputs": {
+            "RoleAliasArn": {
+                "Value": {"Fn::GetAtt": ["testRoleAlias", "RoleAliasArn"]}
+            },
+        },
+    }
+
+    # when
+    cfn_conn = boto3.client("cloudformation", region_name=TEST_REGION)
+    cfn_conn.create_stack(StackName=stack_name, TemplateBody=json.dumps(template))
+
+    # then check list of things
+    iot_conn = boto3.client("iot", region_name=TEST_REGION)
+    assert iot_conn.list_role_aliases()["roleAliases"] == ["TestRoleAlias"]
+
+    # then check stack
+    stack = cfn_conn.describe_stacks(StackName=stack_name)["Stacks"][0]
+    outputs = {
+        Output["OutputKey"]: Output["OutputValue"] for Output in stack["Outputs"]
+    }
+    assert (
+        outputs["RoleAliasArn"]
+        == "arn:aws:iot:us-west-1:123456789012:rolealias/TestRoleAlias"
+    )
+
+
+@mock_aws
+def test_update_role_alias_with_cloudformation():
+    # given
+    stack_name = "test_stack"
+    initial_template = {
+        "AWSTemplateFormatVersion": "2010-09-09",
+        "Description": "IOT RoleAlias CloudFormation",
+        "Resources": {
+            "testRoleAlias": {
+                "Type": "AWS::IoT::RoleAlias",
+                "Properties": {"RoleArn": "arn:aws:iam::123456789012:role/my-role"},
+            },
+        },
+    }
+    updated_template = {
+        "AWSTemplateFormatVersion": "2010-09-09",
+        "Description": "IOT RoleAlias CloudFormation",
+        "Resources": {
+            "testRoleAlias": {
+                "Type": "AWS::IoT::RoleAlias",
+                "Properties": {
+                    "RoleAlias": "TestRoleAlias",
+                    "RoleArn": "arn:aws:iam::123456789012:role/other-role",
+                    "CredentialDurationSeconds": 1234,
+                },
+            },
+        },
+    }
+
+    # when
+    cfn_conn = boto3.client("cloudformation", region_name=TEST_REGION)
+    cfn_conn.create_stack(
+        StackName=stack_name, TemplateBody=json.dumps(initial_template)
+    )
+
+    # then check list of role aliases
+    iot_conn = boto3.client("iot", region_name=TEST_REGION)
+    role_aliases = iot_conn.list_role_aliases()["roleAliases"]
+    assert len(role_aliases) == 1
+    assert role_aliases[0].startswith("test_stack-testRoleAlias-")
+
+    # then check stack
+    cfn_conn.update_stack(
+        StackName=stack_name, TemplateBody=json.dumps(updated_template)
+    )
+    assert iot_conn.list_role_aliases()["roleAliases"] == ["TestRoleAlias"]
+
+    # and describe that role alias
+    role_alias = iot_conn.describe_role_alias(roleAlias="TestRoleAlias")
+    assert role_alias["roleAliasDescription"]["roleAlias"] == "TestRoleAlias"
+    assert (
+        role_alias["roleAliasDescription"]["roleAliasArn"]
+        == "arn:aws:iot:us-west-1:123456789012:rolealias/TestRoleAlias"
+    )
+    assert (
+        role_alias["roleAliasDescription"]["roleArn"]
+        == "arn:aws:iam::123456789012:role/other-role"
+    )
+
+
+@mock_aws
+def test_delete_role_alias_with_cloudformation():
+    # given
+    stack_name = "test_stack"
+    template = {
+        "AWSTemplateFormatVersion": "2010-09-09",
+        "Description": "IOT RoleAlias CloudFormation",
+        "Resources": {
+            "testRoleAlias": {
+                "Type": "AWS::IoT::RoleAlias",
+                "Properties": {
+                    "RoleAlias": "TestRoleAlias",
+                    "RoleArn": "arn:aws:iam::123456789012:role/my-role",
+                },
+            },
+        },
+    }
+
+    # when
+    cfn_conn = boto3.client("cloudformation", region_name=TEST_REGION)
+    cfn_conn.create_stack(StackName=stack_name, TemplateBody=json.dumps(template))
+
+    # then check list of role aliases
+    iot_conn = boto3.client("iot", region_name=TEST_REGION)
+    assert len(iot_conn.list_role_aliases()["roleAliases"]) == 1
+
+    # then check stack
+    cfn_conn.delete_stack(StackName=stack_name)
+    assert iot_conn.list_role_aliases()["roleAliases"] == []
diff --git a/tests/test_iot/test_iot_rolealias.py b/tests/test_iot/test_iot_rolealias.py
new file mode 100644
index 000000000..387e4e49c
--- /dev/null
+++ b/tests/test_iot/test_iot_rolealias.py
@@ -0,0 +1,128 @@
+import boto3
+import pytest
+
+from moto import mock_aws
+
+
+@pytest.fixture(name="client")
+def client_fixture():
+    with mock_aws():
+        yield boto3.client("iot", region_name="eu-west-1")
+
+
+def test_create_role_alias(client):
+    role_alias_name = "test-role-alias"
+    create_response = client.create_role_alias(
+        roleAlias=role_alias_name,
+        roleArn="arn:aws:iam::123456789012:role/my-role",
+        credentialDurationSeconds=1234,
+    )
+
+    assert create_response["roleAlias"] == role_alias_name
+    assert (
+        create_response["roleAliasArn"]
+        == f"arn:aws:iot:eu-west-1:123456789012:rolealias/{role_alias_name}"
+    )
+
+    assert len(client.list_role_aliases()["roleAliases"]) == 1
+
+
+def test_create_role_alias_twice(client):
+    role_alias_name = "test-role-alias"
+    create_response = client.create_role_alias(
+        roleAlias=role_alias_name,
+        roleArn="arn:aws:iam::123456789012:role/my-role",
+        credentialDurationSeconds=1234,
+    )
+
+    assert create_response["roleAlias"] == role_alias_name
+    assert (
+        create_response["roleAliasArn"]
+        == f"arn:aws:iot:eu-west-1:123456789012:rolealias/{role_alias_name}"
+    )
+
+    with pytest.raises(client.exceptions.ResourceAlreadyExistsException):
+        client.create_role_alias(
+            roleAlias=role_alias_name,
+            roleArn="arn:aws:iam::123456789012:role/my-role",
+            credentialDurationSeconds=1234,
+        )
+
+
+def test_list_role_aliases(client):
+    client.create_role_alias(
+        roleAlias="test-role-alias", roleArn="arn:aws:iam::123456789012:role/my-role"
+    )
+    client.create_role_alias(
+        roleAlias="another_role_alias",
+        roleArn="arn:aws:iam::123456789012:role/my-role",
+    )
+
+    response = client.list_role_aliases()
+
+    assert response["roleAliases"] == ["test-role-alias", "another_role_alias"]
+
+
+def test_delete_role_alias(client):
+    role_alias_name = "test-role-alias"
+
+    client.create_role_alias(
+        roleAlias=role_alias_name, roleArn="arn:aws:iam::123456789012:role/my-role"
+    )
+    assert len(client.list_role_aliases()["roleAliases"]) == 1
+
+    client.delete_role_alias(roleAlias=role_alias_name)
+    assert len(client.list_role_aliases()["roleAliases"]) == 0
+
+
+def test_delete_nonexistent_role_alias(client):
+    with pytest.raises(client.exceptions.ResourceNotFoundException):
+        client.delete_role_alias(roleAlias="test_role_alias")
+
+
+def test_describe_role_alias(client):
+    role_alias_name = "test-role-alias"
+    client.create_role_alias(
+        roleAlias=role_alias_name, roleArn="arn:aws:iam::123456789012:role/my-role"
+    )
+
+    response = client.describe_role_alias(roleAlias=role_alias_name)
+    assert response["roleAliasDescription"]["roleAlias"] == role_alias_name
+    assert (
+        response["roleAliasDescription"]["roleAliasArn"]
+        == f"arn:aws:iot:eu-west-1:123456789012:rolealias/{role_alias_name}"
+    )
+    assert (
+        response["roleAliasDescription"]["roleArn"]
+        == "arn:aws:iam::123456789012:role/my-role"
+    )
+    assert response["roleAliasDescription"]["credentialDurationSeconds"] == 3600
+    assert "owner" in response["roleAliasDescription"]
+    assert "creationDate" in response["roleAliasDescription"]
+    assert "lastModifiedDate" in response["roleAliasDescription"]
+
+
+def test_update_role_alias(client):
+    role_alias_name = "test-role-alias"
+    client.create_role_alias(
+        roleAlias=role_alias_name,
+        roleArn="arn:aws:iam::123456789012:role/my-role",
+        credentialDurationSeconds=1234,
+    )
+    client.update_role_alias(
+        roleAlias=role_alias_name,
+        roleArn="arn:aws:iam::123456789012:role/other-role",
+        credentialDurationSeconds=2345,
+    )
+    response = client.describe_role_alias(roleAlias=role_alias_name)
+
+    assert response["roleAliasDescription"]["roleAlias"] == role_alias_name
+    assert (
+        response["roleAliasDescription"]["roleAliasArn"]
+        == f"arn:aws:iot:eu-west-1:123456789012:rolealias/{role_alias_name}"
+    )
+    assert (
+        response["roleAliasDescription"]["roleArn"]
+        == "arn:aws:iam::123456789012:role/other-role"
+    )
+    assert response["roleAliasDescription"]["credentialDurationSeconds"] == 2345
diff --git a/tests/test_iot/test_iot_search.py b/tests/test_iot/test_iot_search.py
index 50c8884d2..5c0a501cf 100644
--- a/tests/test_iot/test_iot_search.py
+++ b/tests/test_iot/test_iot_search.py
@@ -1,3 +1,5 @@
+import json
+
 import boto3
 import pytest
 
@@ -31,6 +33,48 @@ def test_search_things(query_string, results):
     for thing in resp["things"]:
         del thing["connectivity"]["timestamp"]
         assert thing["connectivity"] == {"connected": True}
+        assert "thingId" in thing
+
+
+@mock_aws
+def test_search_things_include_group_names():
+    client = boto3.client("iot", region_name="ap-northeast-1")
+
+    thing_name = "test-thing-name"
+    client.create_thing(thingName=thing_name)
+
+    client.create_thing_group(thingGroupName="TestGroup1")
+    client.create_thing_group(thingGroupName="AnotherGroup")
+    client.create_thing_group(thingGroupName="GroupWithoutMembers")
+
+    client.add_thing_to_thing_group(thingName=thing_name, thingGroupName="TestGroup1")
+    client.add_thing_to_thing_group(thingName=thing_name, thingGroupName="AnotherGroup")
+
+    resp = client.search_index(queryString=f"thingName:{thing_name}")
+    assert len(resp["things"]) == 1
+    assert resp["things"][0]["thingGroupNames"] == ["TestGroup1", "AnotherGroup"]
+
+
+@mock_aws
+def test_search_things_include_named_shadow():
+    iot_client = boto3.client("iot", region_name="ap-northeast-1")
+    iotdata_client = boto3.client("iot-data", region_name="ap-northeast-1")
+    raw_payload = b'{"state": {"desired": {"led": "on"}, "reported": {"led": "off"}}}'
+
+    thing_name = "test-thing-name"
+    iot_client.create_thing(thingName=thing_name)
+    iotdata_client.update_thing_shadow(
+        thingName=thing_name, shadowName="test_shadow", payload=raw_payload
+    )
+
+    resp = iot_client.search_index(queryString=f"thingName:{thing_name}")
+
+    assert len(resp["things"]) == 1
+    shadow = json.loads(resp["things"][0]["shadow"])
+
+    assert shadow["name"]["test_shadow"]["desired"] == {"led": "on"}
+    assert shadow["name"]["test_shadow"]["reported"] == {"led": "off"}
+    assert shadow["name"]["test_shadow"]["hasDelta"]
 
 
 @mock_aws