Repository: getmoto/moto
Pull request #6910 (2023-10-13 13:06:00)

[iot] update_thing(): delete empty attributes with merge=true
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/iot/client/update_thing.html
- To remove an attribute, call UpdateThing with an empty attribute value.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/6910?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
> Merging [#6910](https://app.codecov.io/gh/getmoto/moto/pull/6910?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) (9e7bee6) into [master](https://app.codecov.io/gh/getmoto/moto/commit/f59e178f272003ba39694d68390611e118d8eaa9?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) (f59e178) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

```diff
@@           Coverage Diff           @@
##           master    #6910   +/-   ##
=======================================
  Coverage   95.82%   95.82%           
=======================================
  Files         826      826           
  Lines       81060    81060           
=======================================
+ Hits        77674    77675    +1     
+ Misses       3386     3385    -1     
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/6910/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/6910/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `36.74% <0.00%> (ø)` | |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/6910/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `95.76% <100.00%> (+<0.01%)` | :arrow_up: |

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.

| [Files](https://app.codecov.io/gh/getmoto/moto/pull/6910?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Coverage Δ | |
|---|---|---|
| [moto/iot/models.py](https://app.codecov.io/gh/getmoto/moto/pull/6910?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#diff-bW90by9pb3QvbW9kZWxzLnB5) | `93.38% <100.00%> (+0.11%)` | :arrow_up: |



Patch:
diff --git a/moto/iot/models.py b/moto/iot/models.py
index 14289a00d..1ebce5eaa 100644
--- a/moto/iot/models.py
+++ b/moto/iot/models.py
@@ -883,7 +883,7 @@ class IoTBackend(BaseBackend):
             if not do_merge:
                 thing.attributes = attributes
             else:
-                thing.attributes.update(attributes)
+                thing.attributes = {k: v for k, v in attributes.items() if v}
 
     def create_keys_and_certificate(
         self, set_as_active: bool
@@ -1307,7 +1307,6 @@ class IoTBackend(BaseBackend):
         return thing_names
 
     def list_thing_principals(self, thing_name: str) -> List[str]:
-
         things = [_ for _ in self.things.values() if _.thing_name == thing_name]
         if len(things) == 0:
             raise ResourceNotFoundException(


Test patch:
diff --git a/tests/test_iot/test_iot_things.py b/tests/test_iot/test_iot_things.py
index 65f605ea5..773bdab3e 100644
--- a/tests/test_iot/test_iot_things.py
+++ b/tests/test_iot/test_iot_things.py
@@ -54,6 +54,15 @@ def test_update_thing():
     assert res["things"][0]["thingArn"] is not None
     assert res["things"][0]["attributes"]["k1"] == "v1"
 
+    client.update_thing(
+        thingName=name, attributePayload={"attributes": {"k1": ""}, "merge": True}
+    )
+    res = client.list_things()
+    assert len(res["things"]) == 1
+    assert res["things"][0]["thingName"] is not None
+    assert res["things"][0]["thingArn"] is not None
+    assert res["things"][0]["attributes"] == {}
+
 
 @mock_iot
 def test_describe_thing():