Repository: getmoto/moto
Pull request #8277 (2024-11-01 19:04:05)

CloudFormation models for some IoT objects
I'd like to contribute CloudFormation models for AWS IoT Core service.
Initial models for:
* AWS::IoT::Thing
* AWS::IoT::ThingType
* AWS::IoT::Policy

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/8277?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
Attention: Patch coverage is `91.53846%` with `11 lines` in your changes missing coverage. Please review.
> Project coverage is 94.55%. Comparing base [(`5f4c48b`)](https://app.codecov.io/gh/getmoto/moto/commit/5f4c48b9ef8d61cacd17c653af6462eaf77fc371?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) to head [(`c10a308`)](https://app.codecov.io/gh/getmoto/moto/commit/c10a308248dc1e488b6c044564a4eeada44f977d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).

| [Files with missing lines](https://app.codecov.io/gh/getmoto/moto/pull/8277?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Patch % | Lines |
|---|---|---|
| [moto/iot/models.py](https://app.codecov.io/gh/getmoto/moto/pull/8277?src=pr&el=tree&filepath=moto%2Fiot%2Fmodels.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#diff-bW90by9pb3QvbW9kZWxzLnB5) | 91.47% | [11 Missing :warning: ](https://app.codecov.io/gh/getmoto/moto/pull/8277?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #8277      +/-   ##
==========================================
- Coverage   94.56%   94.55%   -0.01%     
==========================================
  Files        1158     1158              
  Lines      100606   100732     +126     
==========================================
+ Hits        95134    95249     +115     
- Misses       5472     5483      +11     
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/8277/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Coverage Δ | |
|---|---|---|
| [servertests](https://app.codecov.io/gh/getmoto/moto/pull/8277/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `28.87% <37.69%> (+<0.01%)` | :arrow_up: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/8277/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `94.52% <91.53%> (-0.01%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/getmoto/moto/pull/8277?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).


Patch:
diff --git a/CLOUDFORMATION_COVERAGE.md b/CLOUDFORMATION_COVERAGE.md
index f74dc3052..b7853a20f 100644
--- a/CLOUDFORMATION_COVERAGE.md
+++ b/CLOUDFORMATION_COVERAGE.md
@@ -262,6 +262,21 @@ 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::Thing:
+   - [x] create implemented
+   - [x] update implemented
+   - [x] delete implemented
+   - [x] Fn::GetAtt implemented
+- AWS::IoT::ThingType:
+   - [x] create implemented
+   - [x] update implemented
+   - [x] delete implemented
+   - [x] Fn::GetAtt implemented
+- AWS::IoT::Policy:
+   - [x] create implemented
+   - [x] update implemented
+   - [x] delete implemented
+   - [x] Fn::GetAtt implemented
 - AWS::KMS::Key:
    - [x] create implemented
    - [ ] update implemented
diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py
index dcebd31ed..807942e05 100644
--- a/moto/cloudformation/parsing.py
+++ b/moto/cloudformation/parsing.py
@@ -47,6 +47,7 @@ from moto.elbv2 import models as elbv2_models  # noqa  # pylint: disable=all
 from moto.emr import models as emr_models  # noqa  # pylint: disable=all
 from moto.events import models as events_models  # noqa  # pylint: disable=all
 from moto.iam import models as iam_models  # noqa  # pylint: disable=all
+from moto.iot import models as iot_models  # noqa  # pylint: disable=all
 from moto.kinesis import models as kinesis_models  # noqa  # pylint: disable=all
 from moto.kms import models as kms_models  # noqa  # pylint: disable=all
 from moto.rds import models as rds_models  # noqa  # pylint: disable=all
diff --git a/moto/iot/models.py b/moto/iot/models.py
index b46d53e41..87746f6a4 100644
--- a/moto/iot/models.py
+++ b/moto/iot/models.py
@@ -1,8 +1,10 @@
 import hashlib
+import json
 import re
 import time
 from collections import OrderedDict
 from datetime import datetime, timedelta
+from json import JSONDecodeError
 from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Pattern, Tuple
 
 from cryptography import x509
@@ -12,7 +14,7 @@ from cryptography.hazmat.primitives import hashes, serialization
 from cryptography.hazmat.primitives.asymmetric import rsa
 
 from moto.core.base_backend import BackendDict, BaseBackend
-from moto.core.common_models import BaseModel
+from moto.core.common_models import BaseModel, CloudFormationModel
 from moto.core.utils import utcnow
 from moto.moto_api._internal import mock_random as random
 from moto.settings import iot_use_valid_cert
@@ -36,7 +38,7 @@ if TYPE_CHECKING:
     from moto.iotdata.models import FakeShadow
 
 
-class FakeThing(BaseModel):
+class FakeThing(CloudFormationModel):
     def __init__(
         self,
         thing_name: str,
@@ -98,8 +100,101 @@ class FakeThing(BaseModel):
             obj["thingId"] = self.thing_id
         return obj
 
+    @staticmethod
+    def cloudformation_name_type() -> str:
+        return "Thing"
+
+    @staticmethod
+    def cloudformation_type() -> str:
+        return "AWS::IoT::Thing"
+
+    @classmethod
+    def has_cfn_attr(cls, attr: str) -> bool:
+        return attr in [
+            "Arn",
+            "Id",
+        ]
+
+    def get_cfn_attribute(self, attribute_name: str) -> Any:
+        from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
+
+        if attribute_name == "Arn":
+            return self.arn
+        elif attribute_name == "Id":
+            return self.thing_id
+        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,
+    ) -> "FakeThing":
+        iot_backend = iot_backends[account_id][region_name]
+        properties = cloudformation_json["Properties"]
+
+        thing_name = properties.get("ThingName", resource_name)
+        attribute_payload = properties.get("AttributePayload", "")
+        try:
+            attributes = json.loads(attribute_payload)
+        except JSONDecodeError:
+            attributes = None
+
+        return iot_backend.create_thing(
+            thing_name=thing_name, thing_type_name="", attribute_payload=attributes
+        )
+
+    @classmethod
+    def update_from_cloudformation_json(  # type: ignore[misc]
+        cls,
+        original_resource: "FakeThing",
+        new_resource_name: str,
+        cloudformation_json: Any,
+        account_id: str,
+        region_name: str,
+    ) -> "FakeThing":
+        iot_backend = iot_backends[account_id][region_name]
+        properties = cloudformation_json["Properties"]
+        thing_name = properties.get("ThingName", new_resource_name)
+        attribute_payload = properties.get("AttributePayload", "")
+        try:
+            attributes = json.loads(attribute_payload)
+        except JSONDecodeError:
+            attributes = None
+
+        if thing_name != original_resource.thing_name:
+            iot_backend.delete_thing(original_resource.thing_name)
+            return cls.create_from_cloudformation_json(
+                new_resource_name, cloudformation_json, account_id, region_name
+            )
+        else:
+            iot_backend.update_thing(
+                thing_name=thing_name,
+                thing_type_name="",
+                attribute_payload=attributes,
+                remove_thing_type=False,
+            )
+            return original_resource
 
-class FakeThingType(BaseModel):
+    @classmethod
+    def delete_from_cloudformation_json(  # type: ignore[misc]
+        cls,
+        resource_name: str,
+        cloudformation_json: Any,
+        account_id: str,
+        region_name: str,
+    ) -> None:
+        iot_backend = iot_backends[account_id][region_name]
+        properties = cloudformation_json["Properties"]
+        thing_name = properties.get("ThingName", resource_name)
+
+        iot_backend.delete_thing(thing_name=thing_name)
+
+
+class FakeThingType(CloudFormationModel):
     def __init__(
         self,
         thing_type_name: str,
@@ -125,6 +220,80 @@ class FakeThingType(BaseModel):
             "thingTypeArn": self.arn,
         }
 
+    @staticmethod
+    def cloudformation_name_type() -> str:
+        return "ThingType"
+
+    @staticmethod
+    def cloudformation_type() -> str:
+        return "AWS::IoT::ThingType"
+
+    @classmethod
+    def has_cfn_attr(cls, attr: str) -> bool:
+        return attr in [
+            "Arn",
+            "Id",
+        ]
+
+    def get_cfn_attribute(self, attribute_name: str) -> Any:
+        from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
+
+        if attribute_name == "Arn":
+            return self.arn
+        elif attribute_name == "Id":
+            return self.thing_type_id
+        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,
+    ) -> "FakeThingType":
+        iot_backend = iot_backends[account_id][region_name]
+        properties = cloudformation_json["Properties"]
+
+        thing_type_name = properties.get("ThingTypeName", resource_name)
+        thing_type_properties = properties.get("ThingTypeProperties", {})
+
+        type_name, type_arn = iot_backend.create_thing_type(
+            thing_type_name=thing_type_name, thing_type_properties=thing_type_properties
+        )
+        return iot_backend.thing_types[type_arn]
+
+    @classmethod
+    def update_from_cloudformation_json(  # type: ignore[misc]
+        cls,
+        original_resource: "FakeThingType",
+        new_resource_name: str,
+        cloudformation_json: Any,
+        account_id: str,
+        region_name: str,
+    ) -> "FakeThingType":
+        iot_backend = iot_backends[account_id][region_name]
+
+        iot_backend.delete_thing_type(thing_type_name=original_resource.thing_type_name)
+        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"]
+        thing_type_name = properties.get("ThingTypeName", resource_name)
+
+        iot_backend = iot_backends[account_id][region_name]
+        iot_backend.delete_thing_type(thing_type_name=thing_type_name)
+
 
 class FakeThingGroup(BaseModel):
     def __init__(
@@ -283,7 +452,7 @@ class FakeCaCertificate(FakeCertificate):
         self.registration_config = registration_config
 
 
-class FakePolicy(BaseModel):
+class FakePolicy(CloudFormationModel):
     def __init__(
         self,
         name: str,
@@ -320,6 +489,90 @@ class FakePolicy(BaseModel):
     def to_dict(self) -> Dict[str, str]:
         return {"policyName": self.name, "policyArn": self.arn}
 
+    @staticmethod
+    def cloudformation_name_type() -> str:
+        return "Policy"
+
+    @staticmethod
+    def cloudformation_type() -> str:
+        return "AWS::IoT::Policy"
+
+    @classmethod
+    def has_cfn_attr(cls, attr: str) -> bool:
+        return attr in [
+            "Arn",
+            "Id",
+        ]
+
+    def get_cfn_attribute(self, attribute_name: str) -> Any:
+        from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
+
+        if attribute_name == "Arn":
+            return self.arn
+        elif attribute_name == "Id":
+            return self.name + "_" + str(self._max_version_id)
+        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,
+    ) -> "FakePolicy":
+        iot_backend = iot_backends[account_id][region_name]
+        properties = cloudformation_json["Properties"]
+
+        policy_name = properties.get("PolicyName", resource_name)
+        policy_document = properties.get("PolicyDocument", {})
+
+        return iot_backend.create_policy(
+            policy_name=policy_name, policy_document=policy_document
+        )
+
+    @classmethod
+    def update_from_cloudformation_json(  # type: ignore[misc]
+        cls,
+        original_resource: "FakePolicy",
+        new_resource_name: str,
+        cloudformation_json: Any,
+        account_id: str,
+        region_name: str,
+    ) -> "FakePolicy":
+        iot_backend = iot_backends[account_id][region_name]
+        properties = cloudformation_json["Properties"]
+        new_policy_name = properties.get("PolicyName", new_resource_name)
+        policy_document = properties.get("PolicyDocument", {})
+
+        if original_resource.name != new_policy_name:
+            iot_backend.delete_policy(policy_name=original_resource.name)
+            return iot_backend.create_policy(
+                policy_name=new_policy_name, policy_document=policy_document
+            )
+        else:
+            iot_backend.create_policy_version(
+                policy_name=original_resource.name,
+                policy_document=policy_document,
+                set_as_default=True,
+            )
+            return original_resource
+
+    @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"]
+        policy_name = properties.get("PolicyName", resource_name)
+
+        iot_backend = iot_backends[account_id][region_name]
+        iot_backend.delete_policy(policy_name=policy_name)
+
 
 class FakePolicyVersion:
     def __init__(


Test patch:
diff --git a/tests/test_iot/test_iot_cloudformation.py b/tests/test_iot/test_iot_cloudformation.py
new file mode 100644
index 000000000..d30fbdbc2
--- /dev/null
+++ b/tests/test_iot/test_iot_cloudformation.py
@@ -0,0 +1,554 @@
+import json
+
+import boto3
+
+from moto import mock_aws
+
+TEST_REGION = "us-west-1"
+
+
+@mock_aws
+def test_create_thing_with_simple_cloudformation():
+    # given
+    stack_name = "test_stack"
+    template = {
+        "AWSTemplateFormatVersion": "2010-09-09",
+        "Description": "IOT Thing CloudFormation",
+        "Resources": {
+            "testThing": {
+                "Type": "AWS::IoT::Thing",
+            },
+        },
+        "Outputs": {
+            "ThingArn": {"Value": {"Fn::GetAtt": ["testThing", "Arn"]}},
+            "ThingId": {"Value": {"Fn::GetAtt": ["testThing", "Id"]}},
+        },
+    }
+
+    # when
+    template_json = json.dumps(template)
+    cfn_conn = boto3.client("cloudformation", region_name=TEST_REGION)
+    cfn_conn.create_stack(StackName=stack_name, TemplateBody=template_json)
+
+    # then check list of things
+    iot_conn = boto3.client("iot", region_name=TEST_REGION)
+    things = iot_conn.list_things()["things"]
+    assert len(things) == 1
+    assert things[0]["thingName"].startswith("test_stack-testThing-")
+    assert things[0]["thingArn"].startswith(
+        "arn:aws:iot:us-west-1:123456789012:thing/test_stack-testThing-"
+    )
+    assert things[0]["attributes"] == {}
+
+
+@mock_aws
+def test_create_thing_with_attributes_through_cloudformation():
+    # given
+    test_thing_name = "Test_Thing"
+    stack_name = "test_stack"
+
+    thing_attributes = {"attributes": {"attr1": "value1", "attr2": "value2"}}
+    template = {
+        "AWSTemplateFormatVersion": "2010-09-09",
+        "Description": "IOT Thing CloudFormation",
+        "Resources": {
+            "testThing": {
+                "Type": "AWS::IoT::Thing",
+                "Properties": {
+                    "ThingName": test_thing_name,
+                    "AttributePayload": json.dumps(thing_attributes),
+                },
+            },
+        },
+        "Outputs": {
+            "ThingArn": {"Value": {"Fn::GetAtt": ["testThing", "Arn"]}},
+            "ThingId": {"Value": {"Fn::GetAtt": ["testThing", "Id"]}},
+        },
+    }
+
+    # when
+    template_json = json.dumps(template)
+    cfn_conn = boto3.client("cloudformation", region_name=TEST_REGION)
+    cfn_conn.create_stack(StackName=stack_name, TemplateBody=template_json)
+
+    # then check list of things
+    iot_conn = boto3.client("iot", region_name=TEST_REGION)
+    assert len(iot_conn.list_things()["things"]) == 1
+
+    # describe thing
+    resp = iot_conn.describe_thing(thingName=test_thing_name)
+    assert resp["thingName"] == test_thing_name
+    assert resp["thingArn"] == "arn:aws:iot:us-west-1:123456789012:thing/Test_Thing"
+    assert resp["attributes"] == thing_attributes["attributes"]
+
+    # Check stack outputs
+    stack = cfn_conn.describe_stacks(StackName=stack_name)["Stacks"][0]
+    outputs = {
+        Output["OutputKey"]: Output["OutputValue"] for Output in stack["Outputs"]
+    }
+    assert outputs["ThingArn"] == resp["thingArn"]
+    assert outputs["ThingId"] == resp["thingId"]
+
+
+@mock_aws
+def test_update_thing_name_should_recreate_thing_and_change_id():
+    # given
+    test_thing_name = "Test_Thing"
+    updated_thing_name = "Updated_Thing"
+    stack_name = "test_stack"
+
+    first_template = {
+        "AWSTemplateFormatVersion": "2010-09-09",
+        "Description": "IOT Thing CloudFormation",
+        "Resources": {
+            "testThing": {
+                "Type": "AWS::IoT::Thing",
+                "Properties": {"ThingName": test_thing_name},
+            },
+        },
+    }
+    updated_template = {
+        "AWSTemplateFormatVersion": "2010-09-09",
+        "Description": "IOT Thing CloudFormation",
+        "Resources": {
+            "testThing": {
+                "Type": "AWS::IoT::Thing",
+                "Properties": {"ThingName": updated_thing_name},
+            },
+        },
+    }
+
+    # when
+    cfn_conn = boto3.client("cloudformation", region_name=TEST_REGION)
+    cfn_conn.create_stack(StackName=stack_name, TemplateBody=json.dumps(first_template))
+
+    # then check list of things and the only one thing
+    iot_conn = boto3.client("iot", region_name=TEST_REGION)
+    assert len(iot_conn.list_things()["things"]) == 1
+    first_resp = iot_conn.describe_thing(thingName=test_thing_name)
+    assert first_resp["thingName"] == test_thing_name
+    assert (
+        first_resp["thingArn"] == "arn:aws:iot:us-west-1:123456789012:thing/Test_Thing"
+    )
+
+    # and
+    cfn_conn.update_stack(
+        StackName=stack_name, TemplateBody=json.dumps(updated_template)
+    )
+    assert len(iot_conn.list_things()["things"]) == 1
+    updated_resp = iot_conn.describe_thing(thingName=updated_thing_name)
+    assert updated_resp["thingName"] == updated_thing_name
+    assert (
+        updated_resp["thingArn"]
+        == "arn:aws:iot:us-west-1:123456789012:thing/Updated_Thing"
+    )
+
+    # and it's a different thing
+    assert first_resp["thingId"] != updated_resp["thingId"]
+
+
+@mock_aws
+def test_delete_resource_should_delete_thing():
+    # given
+    test_thing_name = "Test_Thing"
+    stack_name = "test_stack"
+
+    first_template = {
+        "AWSTemplateFormatVersion": "2010-09-09",
+        "Description": "IOT Thing CloudFormation",
+        "Resources": {
+            "testThing": {
+                "Type": "AWS::IoT::Thing",
+                "Properties": {
+                    "ThingName": test_thing_name,
+                },
+            },
+        },
+    }
+
+    # when
+    cfn_conn = boto3.client("cloudformation", region_name=TEST_REGION)
+    cfn_conn.create_stack(StackName=stack_name, TemplateBody=json.dumps(first_template))
+
+    # then check list of things and the only one thing
+    iot_conn = boto3.client("iot", region_name=TEST_REGION)
+    assert len(iot_conn.list_things()["things"]) == 1
+
+    # and
+    cfn_conn.delete_stack(StackName=stack_name)
+    assert len(iot_conn.list_things()["things"]) == 0
+
+
+@mock_aws
+def test_create_thingtype_with_simple_cloudformation():
+    # given
+    stack_name = "test_stack"
+    template = {
+        "AWSTemplateFormatVersion": "2010-09-09",
+        "Description": "IOT ThingType CloudFormation",
+        "Resources": {
+            "testThingType": {
+                "Type": "AWS::IoT::ThingType",
+            },
+        },
+        "Outputs": {
+            "ThingTypeArn": {"Value": {"Fn::GetAtt": ["testThingType", "Arn"]}},
+            "ThingTypeId": {"Value": {"Fn::GetAtt": ["testThingType", "Id"]}},
+        },
+    }
+
+    # 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)
+    thing_types = iot_conn.list_thing_types()["thingTypes"]
+    assert len(thing_types) == 1
+    assert thing_types[0]["thingTypeName"].startswith("test_stack-testThingType-")
+    assert thing_types[0]["thingTypeArn"].startswith(
+        "arn:aws:iot:us-west-1:123456789012:thingtype/test_stack-testThingType-"
+    )
+
+    # and check if they are the same as in a stack outputs
+    stack = cfn_conn.describe_stacks(StackName=stack_name)["Stacks"][0]
+    outputs = {
+        Output["OutputKey"]: Output["OutputValue"] for Output in stack["Outputs"]
+    }
+    assert outputs["ThingTypeArn"] == thing_types[0]["thingTypeArn"]
+
+
+@mock_aws
+def test_update_thingtype_using_cloudformation_should_recreate_id():
+    # given
+    stack_name = "test_stack"
+    initial_template = {
+        "AWSTemplateFormatVersion": "2010-09-09",
+        "Description": "IOT ThingType CloudFormation",
+        "Resources": {
+            "testThingType": {
+                "Type": "AWS::IoT::ThingType",
+                "Properties": {"ThingTypeName": "TestThing1"},
+            },
+        },
+    }
+
+    updated_template = {
+        "AWSTemplateFormatVersion": "2010-09-09",
+        "Description": "IOT ThingType CloudFormation",
+        "Resources": {
+            "testThingType": {
+                "Type": "AWS::IoT::ThingType",
+                "Properties": {"ThingTypeName": "TestThing2"},
+            },
+        },
+    }
+
+    # 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 things
+    iot_conn = boto3.client("iot", region_name=TEST_REGION)
+    initial_thing_types = iot_conn.list_thing_types()["thingTypes"]
+    assert len(initial_thing_types) == 1
+    assert initial_thing_types[0]["thingTypeName"] == "TestThing1"
+
+    # then update
+    cfn_conn.update_stack(
+        StackName=stack_name, TemplateBody=json.dumps(updated_template)
+    )
+    updated_thing_types = iot_conn.list_thing_types()["thingTypes"]
+    assert len(updated_thing_types) == 1
+    assert updated_thing_types[0]["thingTypeName"] == "TestThing2"
+
+    # and ids are different
+    assert (
+        initial_thing_types[0]["thingTypeArn"] != updated_thing_types[0]["thingTypeArn"]
+    )
+
+
+@mock_aws
+def test_cloudformation_stack_delete_should_remove_thingtype():
+    # given
+    stack_name = "test_stack"
+    template = {
+        "AWSTemplateFormatVersion": "2010-09-09",
+        "Description": "IOT ThingType CloudFormation",
+        "Resources": {
+            "testThingType": {
+                "Type": "AWS::IoT::ThingType",
+                "Properties": {"ThingTypeName": "TestThingType"},
+            },
+        },
+    }
+
+    # 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 len(iot_conn.list_thing_types()["thingTypes"]) == 1
+
+    # then update
+    cfn_conn.delete_stack(StackName=stack_name)
+    assert len(iot_conn.list_thing_types()["thingTypes"]) == 0
+
+
+@mock_aws
+def test_create_policy_with_simple_cloudformation():
+    # given
+    stack_name = "test_stack"
+    policy_name = "Test_Policy"
+    template = {
+        "AWSTemplateFormatVersion": "2010-09-09",
+        "Description": "IOT Policy CloudFormation",
+        "Resources": {
+            "testPolicy": {
+                "Type": "AWS::IoT::Policy",
+                "Properties": {
+                    "PolicyName": policy_name,
+                    "PolicyDocument": json.dumps(
+                        {
+                            "Version": "2012-10-17",
+                            "Statement": [
+                                {
+                                    "Effect": "Allow",
+                                    "Action": ["iot:Connect"],
+                                    "Resource": [
+                                        "arn:aws:iot:us-east-1:123456789012:client/client1"
+                                    ],
+                                }
+                            ],
+                        }
+                    ),
+                },
+            },
+        },
+        "Outputs": {
+            "PolicyArn": {"Value": {"Fn::GetAtt": ["testPolicy", "Arn"]}},
+            "PolicyId": {"Value": {"Fn::GetAtt": ["testPolicy", "Id"]}},
+        },
+    }
+
+    # 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)
+    policies = iot_conn.list_policies()["policies"]
+    assert len(policies) == 1
+    assert policies[0]["policyName"] == policy_name
+    assert (
+        policies[0]["policyArn"]
+        == "arn:aws:iot:us-west-1:123456789012:policy/Test_Policy"
+    )
+
+    # 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["PolicyArn"] == policies[0]["policyArn"]
+
+
+@mock_aws
+def test_update_policy_with_the_same_name_should_update_policy_document():
+    # given
+    stack_name = "test_stack"
+    policy_name = "Test_Policy"
+    initial_template = {
+        "AWSTemplateFormatVersion": "2010-09-09",
+        "Description": "IOT Policy CloudFormation",
+        "Resources": {
+            "testPolicy": {
+                "Type": "AWS::IoT::Policy",
+                "Properties": {
+                    "PolicyName": policy_name,
+                    "PolicyDocument": json.dumps(
+                        {
+                            "Version": "2012-10-17",
+                            "Statement": [
+                                {
+                                    "Effect": "Allow",
+                                    "Action": ["iot:Connect"],
+                                    "Resource": [
+                                        "arn:aws:iot:us-east-1:123456789012:client/client1"
+                                    ],
+                                }
+                            ],
+                        }
+                    ),
+                },
+            },
+        },
+    }
+    updated_template = {
+        "AWSTemplateFormatVersion": "2010-09-09",
+        "Description": "IOT Policy CloudFormation",
+        "Resources": {
+            "testPolicy": {
+                "Type": "AWS::IoT::Policy",
+                "Properties": {
+                    "PolicyName": policy_name,
+                    "PolicyDocument": json.dumps(
+                        {
+                            "Version": "2012-10-17",
+                            "Statement": [
+                                {
+                                    "Effect": "Allow",
+                                    "Action": ["iot:Connect", "iot:Subscribe"],
+                                    "Resource": ["*"],
+                                }
+                            ],
+                        }
+                    ),
+                },
+            },
+        },
+    }
+    # when
+    cfn_conn = boto3.client("cloudformation", region_name=TEST_REGION)
+    cfn_conn.create_stack(
+        StackName=stack_name, TemplateBody=json.dumps(initial_template)
+    )
+
+    # then update stack
+    cfn_conn.update_stack(
+        StackName=stack_name, TemplateBody=json.dumps(updated_template)
+    )
+
+    iot_conn = boto3.client("iot", region_name=TEST_REGION)
+    policy = iot_conn.get_policy(policyName=policy_name)
+
+    assert policy["policyName"] == policy_name
+    policyDocument = json.loads(policy["policyDocument"])
+    assert policyDocument["Statement"][0]["Resource"] == ["*"]
+
+
+@mock_aws
+def test_update_policy_with_different_name_should_recreate_whole_policy():
+    # given
+    stack_name = "test_stack"
+    initial_policy_name = "Test_Policy"
+    updated_policy_name = "New_Test_Policy"
+    initial_template = {
+        "AWSTemplateFormatVersion": "2010-09-09",
+        "Description": "IOT Policy CloudFormation",
+        "Resources": {
+            "testPolicy": {
+                "Type": "AWS::IoT::Policy",
+                "Properties": {
+                    "PolicyName": initial_policy_name,
+                    "PolicyDocument": json.dumps(
+                        {
+                            "Version": "2012-10-17",
+                            "Statement": [
+                                {
+                                    "Effect": "Allow",
+                                    "Action": ["iot:Connect"],
+                                    "Resource": [
+                                        "arn:aws:iot:us-east-1:123456789012:client/client1"
+                                    ],
+                                }
+                            ],
+                        }
+                    ),
+                },
+            },
+        },
+    }
+    updated_template = {
+        "AWSTemplateFormatVersion": "2010-09-09",
+        "Description": "IOT Policy CloudFormation",
+        "Resources": {
+            "testPolicy": {
+                "Type": "AWS::IoT::Policy",
+                "Properties": {
+                    "PolicyName": updated_policy_name,
+                    "PolicyDocument": json.dumps(
+                        {
+                            "Version": "2012-10-17",
+                            "Statement": [
+                                {
+                                    "Effect": "Allow",
+                                    "Action": ["iot:Connect", "iot:Subscribe"],
+                                    "Resource": ["*"],
+                                }
+                            ],
+                        }
+                    ),
+                },
+            },
+        },
+    }
+    # when
+    cfn_conn = boto3.client("cloudformation", region_name=TEST_REGION)
+    cfn_conn.create_stack(
+        StackName=stack_name, TemplateBody=json.dumps(initial_template)
+    )
+    iot_conn = boto3.client("iot", region_name=TEST_REGION)
+    policies = iot_conn.list_policies()["policies"]
+    assert len(policies) == 1
+    assert policies[0]["policyName"] == initial_policy_name
+
+    # then update stack
+    cfn_conn.update_stack(
+        StackName=stack_name, TemplateBody=json.dumps(updated_template)
+    )
+
+    policies = iot_conn.list_policies()["policies"]
+    assert len(policies) == 1
+    assert policies[0]["policyName"] == updated_policy_name
+
+    policy = iot_conn.get_policy(policyName=updated_policy_name)
+
+    assert policy["policyName"] == updated_policy_name
+    policyDocument = json.loads(policy["policyDocument"])
+    assert policyDocument["Statement"][0]["Resource"] == ["*"]
+
+
+@mock_aws
+def test_delete_stack_should_delete_policy():
+    # given
+    stack_name = "test_stack"
+    policy_name = "Test_Policy"
+    template = {
+        "AWSTemplateFormatVersion": "2010-09-09",
+        "Description": "IOT Policy CloudFormation",
+        "Resources": {
+            "testPolicy": {
+                "Type": "AWS::IoT::Policy",
+                "Properties": {
+                    "PolicyName": policy_name,
+                    "PolicyDocument": json.dumps(
+                        {
+                            "Version": "2012-10-17",
+                            "Statement": [
+                                {
+                                    "Effect": "Allow",
+                                    "Action": ["iot:Connect"],
+                                    "Resource": [
+                                        "arn:aws:iot:us-east-1:123456789012:client/client1"
+                                    ],
+                                }
+                            ],
+                        }
+                    ),
+                },
+            },
+        },
+    }
+    # when
+    cfn_conn = boto3.client("cloudformation", region_name=TEST_REGION)
+    cfn_conn.create_stack(StackName=stack_name, TemplateBody=json.dumps(template))
+    iot_conn = boto3.client("iot", region_name=TEST_REGION)
+    assert len(iot_conn.list_policies()["policies"]) == 1
+
+    # then delete stack
+    cfn_conn.delete_stack(StackName=stack_name)
+    assert len(iot_conn.list_policies()["policies"]) == 0