Repository: getmoto/moto
Pull request #8310 (2024-11-13 14:11:02)

IoT: Add get_indexing_configuration(), update_indexing_configuration()
I'd like to contribute two IoT calls with a very simple implementation: `get_indexing_configuration()` and `update_indexing_configuration()`


Patch:
diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index e5ee66fff..2349a0b5f 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -4628,7 +4628,7 @@
 - [ ] get_buckets_aggregation
 - [ ] get_cardinality
 - [ ] get_effective_policies
-- [ ] get_indexing_configuration
+- [X] get_indexing_configuration
 - [X] get_job_document
 - [ ] get_logging_options
 - [ ] get_ota_update
@@ -4742,7 +4742,7 @@
 - [ ] update_dynamic_thing_group
 - [ ] update_event_configurations
 - [ ] update_fleet_metric
-- [ ] update_indexing_configuration
+- [X] update_indexing_configuration
 - [ ] update_job
 - [ ] update_mitigation_action
 - [ ] update_package
diff --git a/moto/iot/models.py b/moto/iot/models.py
index a92765c95..d3efc19d6 100644
--- a/moto/iot/models.py
+++ b/moto/iot/models.py
@@ -3,6 +3,7 @@ import json
 import re
 import time
 from collections import OrderedDict
+from dataclasses import asdict, dataclass, field, replace
 from datetime import datetime, timedelta
 from json import JSONDecodeError
 from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Pattern, Tuple
@@ -1027,6 +1028,83 @@ class FakeRoleAlias(CloudFormationModel):
         iot_backend.delete_role_alias(role_alias_name=role_alias_name)
 
 
+@dataclass(frozen=True)
+class FakeConfigField:
+    name: str
+    type: str
+
+
+@dataclass(frozen=True)
+class FakeThingGroupIndexingConfiguration:
+    customFields: List[FakeConfigField] = field(default_factory=list)
+    managedFields: List[FakeConfigField] = field(default_factory=list)
+    thingGroupIndexingMode: str = "OFF"
+
+
+@dataclass(frozen=True)
+class FakeThingIndexingConfigurationFilterGeoLocations:
+    name: str
+    order: str
+
+
+@dataclass(frozen=True)
+class FakeThingIndexingConfigurationFilter:
+    geoLocations: List[FakeThingIndexingConfigurationFilterGeoLocations] = field(
+        default_factory=list
+    )
+    namedShadowNames: List[str] = field(default_factory=list)
+
+
+@dataclass(frozen=True)
+class FakeThingIndexingConfiguration:
+    customFields: List[FakeConfigField] = field(default_factory=list)
+    managedFields: List[FakeConfigField] = field(default_factory=list)
+    filter: FakeThingIndexingConfigurationFilter = field(
+        default_factory=FakeThingIndexingConfigurationFilter
+    )
+    deviceDefenderIndexingMode: str = "OFF"
+    namedShadowIndexingMode: str = "OFF"
+    thingConnectivityIndexingMode: str = "OFF"
+    thingIndexingMode: str = "OFF"
+
+
+@dataclass(frozen=True)
+class FakeIndexingConfigurationData:
+    thingGroupIndexingConfiguration: FakeThingGroupIndexingConfiguration = field(
+        default_factory=FakeThingGroupIndexingConfiguration
+    )
+    thingIndexingConfiguration: FakeThingIndexingConfiguration = field(
+        default_factory=FakeThingIndexingConfiguration
+    )
+
+
+class FakeIndexingConfiguration(BaseModel):
+    def __init__(self, region_name: str, account_id: str) -> None:
+        self.region_name = region_name
+        self.account_id = account_id
+        self.configuration = FakeIndexingConfigurationData()
+
+    def to_dict(self) -> Dict[str, Any]:
+        return asdict(self.configuration)
+
+    def update_configuration(
+        self,
+        thingIndexingConfiguration: Dict[str, Any],
+        thingGroupIndexingConfiguration: Dict[str, Any],
+    ) -> None:
+        self.configuration = replace(
+            self.configuration,
+            thingIndexingConfiguration=replace(
+                self.configuration.thingIndexingConfiguration,
+                **thingIndexingConfiguration,
+            ),
+            thingGroupIndexingConfiguration=replace(
+                self.configuration.thingGroupIndexingConfiguration,
+                **thingGroupIndexingConfiguration,
+            ),
+        )
+
+
 class IoTBackend(BaseBackend):
     def __init__(self, region_name: str, account_id: str):
         super().__init__(region_name, account_id)
@@ -1048,6 +1126,7 @@ class IoTBackend(BaseBackend):
         self.role_aliases: Dict[str, FakeRoleAlias] = OrderedDict()
         self.endpoint: Optional[FakeEndpoint] = None
         self.domain_configurations: Dict[str, FakeDomainConfiguration] = OrderedDict()
+        self.indexing_configuration = FakeIndexingConfiguration(region_name, account_id)
 
     @staticmethod
     def default_vpc_endpoint_service(
@@ -2384,5 +2463,17 @@ class IoTBackend(BaseBackend):
         self.describe_role_alias(role_alias_name=role_alias_name)
         del self.role_aliases[role_alias_name]
 
+    def get_index_configuration(self) -> Dict[str, Any]:
+        return self.indexing_configuration.to_dict()
+
+    def update_indexing_configuration(
+        self,
+        thingIndexingConfiguration: Dict[str, Any],
+        thingGroupIndexingConfiguration: Dict[str, Any],
+    ) -> None:
+        self.indexing_configuration.update_configuration(
+            thingIndexingConfiguration, thingGroupIndexingConfiguration
+        )
+
 
 iot_backends = BackendDict(IoTBackend, "iot")
diff --git a/moto/iot/responses.py b/moto/iot/responses.py
index 7d256dbb6..55842b818 100644
--- a/moto/iot/responses.py
+++ b/moto/iot/responses.py
@@ -826,3 +826,13 @@ class IoTResponse(BaseResponse):
         role_alias_name = self._get_param("roleAlias")
         self.iot_backend.delete_role_alias(role_alias_name=role_alias_name)
         return json.dumps({})
+
+    def get_indexing_configuration(self) -> str:
+        return json.dumps(self.iot_backend.get_index_configuration())
+
+    def update_indexing_configuration(self) -> str:
+        self.iot_backend.update_indexing_configuration(
+            self._get_param("thingIndexingConfiguration", {}),
+            self._get_param("thingGroupIndexingConfiguration", {}),
+        )
+        return json.dumps({})


Test patch:
diff --git a/tests/test_iot/test_iot_indexing_configuration.py b/tests/test_iot/test_iot_indexing_configuration.py
new file mode 100644
index 000000000..d2ec29a58
--- /dev/null
+++ b/tests/test_iot/test_iot_indexing_configuration.py
@@ -0,0 +1,63 @@
+import boto3
+
+from moto import mock_aws
+
+
+@mock_aws
+def test_validate_default_indexing_configuration():
+    client = boto3.client("iot", region_name="us-east-1")
+    indexing_config = client.get_indexing_configuration()
+
+    thingIndexingConfiguration = indexing_config["thingIndexingConfiguration"]
+    assert thingIndexingConfiguration["thingIndexingMode"] in [
+        "OFF",
+        "REGISTRY",
+        "REGISTRY_AND_SHADOW",
+    ]
+
+    thingGroupIndexingConfiguration = indexing_config["thingGroupIndexingConfiguration"]
+    assert thingGroupIndexingConfiguration["thingGroupIndexingMode"] in ["ON", "OFF"]
+
+
+@mock_aws
+def test_update_indexing_mode():
+    client = boto3.client("iot", region_name="us-east-1")
+    client.update_indexing_configuration(
+        thingIndexingConfiguration={
+            "thingIndexingMode": "REGISTRY",
+            "thingConnectivityIndexingMode": "STATUS",
+            "deviceDefenderIndexingMode": "VIOLATIONS",
+            "namedShadowIndexingMode": "ON",
+            "managedFields": [{"name": "field1", "type": "String"}],
+            "customFields": [{"name": "custom_field1", "type": "Boolean"}],
+            "filter": {"namedShadowNames": ["shadow_1", "shadow_2"]},
+        },
+        thingGroupIndexingConfiguration={
+            "thingGroupIndexingMode": "ON",
+            "managedFields": [{"name": "thing_field_1", "type": "String"}],
+            "customFields": [{"name": "thing_custom_field", "type": "Number"}],
+        },
+    )
+
+    indexing_config = client.get_indexing_configuration()
+
+    thingIndexingConfiguration = indexing_config["thingIndexingConfiguration"]
+    assert thingIndexingConfiguration["thingIndexingMode"] == "REGISTRY"
+    assert thingIndexingConfiguration["thingConnectivityIndexingMode"] == "STATUS"
+    assert thingIndexingConfiguration["deviceDefenderIndexingMode"] == "VIOLATIONS"
+    assert thingIndexingConfiguration["namedShadowIndexingMode"] == "ON"
+    assert thingIndexingConfiguration["managedFields"] == [
+        {"name": "field1", "type": "String"}
+    ]
+    assert thingIndexingConfiguration["customFields"] == [
+        {"name": "custom_field1", "type": "Boolean"}
+    ]
+
+    thingGroupIndexingConfiguration = indexing_config["thingGroupIndexingConfiguration"]
+    assert thingGroupIndexingConfiguration["thingGroupIndexingMode"] == "ON"
+    assert thingGroupIndexingConfiguration["managedFields"] == [
+        {"name": "thing_field_1", "type": "String"}
+    ]
+    assert thingGroupIndexingConfiguration["customFields"] == [
+        {"name": "thing_custom_field", "type": "Number"}
+    ]