Repository: getmoto/moto (python)

Issue:
moto/core/responses.py `_get_multi_param` bug

> Guidance on **how to reproduce the issue**. Ideally, this should be a
     *small* code sample that can be run immediately by the maintainers.
     Failing that, let us know what you're doing, how often it happens, what
     environment you're using, etc. Be thorough: it prevents us needing to ask
     further questions.
     
- try to execute this code:
```python
fleet_request = {
        'LaunchTemplateConfigs': [
            {
                'LaunchTemplateSpecification': {
                    'LaunchTemplateName': 'test-template-name',
                    'Version': '$Default'
                },
                'Overrides': [ { "InstanceType": "t3.nano" } ]
            }
        ]
    }
ec2_client.create_fleet(**fleet_request)
```

>  Tell us **what you expected to happen**. When we run your example code,
     what are we expecting to happen? What does "success" look like for your
     code?


https://github.com/getmoto/moto/blob/1b91aee1de3d26902c5e79a3adefb8f391fb9876/moto/ec2/responses/fleets.py#L33
```python
# Expected:
(Pdb) print(launch_template_configs)
[{'LaunchTemplateSpecification': {'LaunchTemplateName': 'test-template-name', 'Version': '$Default'}, 'Overrides': [ {'InstanceType': 't3.nano'} ]}]
```

>   Tell us **what actually happens**. It's not helpful for you to say "it
     doesn't work" or "it fails". Tell us *how* it fails: do you get an
     exception? A hang? How was the actual result different from your expected
     result?

```python
# Actually happens, see Overrides section
(Pdb) print(launch_template_configs)
[{'LaunchTemplateSpecification': {'LaunchTemplateName': 'test-template-name', 'Version': '$Default'}, 'Overrides': [ 't3.nano' ]}]
```

https://github.com/getmoto/moto/blob/1b91aee1de3d26902c5e79a3adefb8f391fb9876/moto/core/responses.py#L701
```python
(Pdb) print(self.querystring)
OrderedDict({'Action': ['CreateFleet'],
 'Version': ['2016-11-15'],
 'Type': ['request'],
 'ValidUntil': ['2024-11-14T08:57:24.289912Z'],
 /* ... cropped ... */
 'LaunchTemplateConfigs.1.LaunchTemplateSpecification.LaunchTemplateName': ['test-template-name'],
 'LaunchTemplateConfigs.1.LaunchTemplateSpecification.Version': ['$Default'],
 'LaunchTemplateConfigs.1.Overrides.1.InstanceType': ['t3.nano']
 /* ... cropped ... */})
```

```python
# how it fails
            launch_template_data = launch_template.latest_version().data
            new_launch_template = launch_template_data.copy()
            if config.get("Overrides"):
                for override in config["Overrides"]:
>                   new_launch_template.update(override)
E                   ValueError: dictionary update sequence element #0 has length 1; 2 is required

.venv/lib/python3.13/site-packages/moto/ec2/models/fleets.py:73: ValueError
```

   

>  Tell us **what version of Moto you're using**, and
     **how you installed it**. Tell us whether you're using standalone server
     mode or the Python mocks. If you are using the Python mocks, include the
     version of boto/boto3/botocore.

- `5.0.20`


Pull request description:
EC2: create_fleet() now correctly handles Overrides with a single value
Fixes #8311 

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/ec2/responses/fleets.py b/moto/ec2/responses/fleets.py
index e63a74322..ff44fa84b 100644
--- a/moto/ec2/responses/fleets.py
+++ b/moto/ec2/responses/fleets.py
@@ -33,6 +33,22 @@ class Fleets(EC2BaseResponse):
         launch_template_configs = self._get_multi_param(
             param_prefix="LaunchTemplateConfigs"
         )
+        # Our multi_param function doesn't understand when to return a list, a string, or a dictionary
+        # So we'll have to help it a little, by explicitly asking for a dict if we're getting anything else back
+        for idx, config in enumerate(launch_template_configs, start=1):
+            if (overrides := config.get("Overrides")) and not isinstance(
+                overrides, dict
+            ):
+                config["Overrides"] = self._get_multi_param(
+                    f"LaunchTemplateConfigs.{idx}.Overrides",
+                    skip_result_conversion=True,
+                )
+            if (spec := config.get("LaunchTemplateSpecification")) and not isinstance(
+                spec, dict
+            ):
+                config["LaunchTemplateSpecification"] = self._get_multi_param_dict(
+                    f"LaunchTemplateConfigs.{idx}.LaunchTemplateSpecification"
+                )
 
         excess_capacity_termination_policy = self._get_param(
             "ExcessCapacityTerminationPolicy"
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({})
diff --git a/moto/s3/responses.py b/moto/s3/responses.py
index eb79a3228..d0c82428a 100644
--- a/moto/s3/responses.py
+++ b/moto/s3/responses.py
@@ -1516,7 +1516,8 @@ class S3Response(BaseResponse):
         return 200, response_headers, key.value
 
     def get_object_attributes(self) -> TYPE_RESPONSE:
-        key, not_modified = self._get_key()
+        # Get the Key, but do not validate StorageClass - we can retrieve the attributes of Glacier-objects
+        key, not_modified = self._get_key(validate_storage_class=False)
         response_headers = self._get_cors_headers_other()
         if not_modified:
             return 304, response_headers, "Not Modified"
@@ -1608,7 +1609,7 @@ class S3Response(BaseResponse):
             ),
         )
 
-    def _get_key(self) -> Tuple[FakeKey, bool]:
+    def _get_key(self, validate_storage_class: bool = True) -> Tuple[FakeKey, bool]:
         key_name = self.parse_key_name()
         version_id = self.querystring.get("versionId", [None])[0]
         if_modified_since = self.headers.get("If-Modified-Since")
@@ -1621,7 +1622,7 @@ class S3Response(BaseResponse):
             raise MissingKey(key=key_name)
         elif key is None:
             raise MissingVersion()
-        if key.storage_class in ARCHIVE_STORAGE_CLASSES:
+        if validate_storage_class and key.storage_class in ARCHIVE_STORAGE_CLASSES:
             if 'ongoing-request="false"' not in key.response_dict.get(
                 "x-amz-restore", ""
             ):


Test patch:
diff --git a/tests/test_ec2/test_fleets.py b/tests/test_ec2/test_fleets.py
index e2db4ceba..a8080ce80 100644
--- a/tests/test_ec2/test_fleets.py
+++ b/tests/test_ec2/test_fleets.py
@@ -328,6 +328,35 @@ def test_create_fleet_request_with_tags():
             assert tag in instance["Tags"]
 
 
+@mock_aws
+@pytest.mark.parametrize(
+    "overrides", [{"InstanceType": "t2.nano"}, {"AvailabilityZone": "us-west-1"}]
+)
+def test_create_fleet_using_launch_template_config__overrides_single(overrides):
+    conn = boto3.client("ec2", region_name="us-east-2")
+
+    template_id, _ = get_launch_template(conn, instance_type="t2.medium")
+
+    fleet_id = conn.create_fleet(
+        LaunchTemplateConfigs=[
+            {
+                "LaunchTemplateSpecification": {"LaunchTemplateId": template_id},
+                # Verify we parse the incoming parameters correctly,
+                # even when only supplying a dictionary with a single key
+                "Overrides": [overrides],
+            },
+        ],
+        TargetCapacitySpecification={
+            "DefaultTargetCapacityType": "spot",
+            "TotalTargetCapacity": 1,
+        },
+    )["FleetId"]
+
+    fleet = conn.describe_fleets(FleetIds=[fleet_id])["Fleets"][0]
+    lt_config = fleet["LaunchTemplateConfigs"][0]
+    assert lt_config["Overrides"] == [overrides]
+
+
 @mock_aws
 def test_create_fleet_using_launch_template_config__overrides():
     conn = boto3.client("ec2", region_name="us-east-2")
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"}
+    ]
diff --git a/tests/test_s3/test_s3_storageclass.py b/tests/test_s3/test_s3_storageclass.py
index f40c97ee6..4ba0b3214 100644
--- a/tests/test_s3/test_s3_storageclass.py
+++ b/tests/test_s3/test_s3_storageclass.py
@@ -269,3 +269,9 @@ def test_s3_get_object_from_glacier():
         "The operation is not valid for the object's storage class"
     )
     assert err["StorageClass"] == "GLACIER"
+
+    # Note that get_object_attributes should work
+    resp = s3_client.get_object_attributes(
+        Bucket=bucket_name, Key="test.txt", ObjectAttributes=["StorageClass"]
+    )
+    assert resp["StorageClass"] == "GLACIER"