Repository: getmoto/moto
Pull request #7383 (2024-02-22 01:12:14)

ASG: support filters in describe_autoscaling_groups
**Description:**

This PR introduces support for filtering auto-scaling groups by tags in the [describe_auto_scaling_groups ](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/autoscaling/client/describe_auto_scaling_groups.html)method. It enhances the functionality of the method by allowing users to specify filters based on tag keys, tag values, or key-value pairs.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/7383?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`9aef694`)](https://app.codecov.io/gh/getmoto/moto/commit/9aef6944da6a6f1b84be99cefb34f1724e6ca58f?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) 95.88% compared to head [(`1135d3d`)](https://app.codecov.io/gh/getmoto/moto/pull/7383?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) 95.85%.
> Report is 40 commits behind head on master.


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


```diff
@@            Coverage Diff             @@
##           master    #7383      +/-   ##
==========================================
- Coverage   95.88%   95.85%   -0.03%     
==========================================
  Files         843      843              
  Lines       82578    82883     +305     
==========================================
+ Hits        79178    79451     +273     
- Misses       3400     3432      +32     
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/7383/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/7383/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `32.53% <0.00%> (+0.01%)` | :arrow_up: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/7383/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `95.82% <100.00%> (-0.03%)` | :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/7383?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/moto/autoscaling/models.py b/moto/autoscaling/models.py
index e835e71fa..2f27e3610 100644
--- a/moto/autoscaling/models.py
+++ b/moto/autoscaling/models.py
@@ -1190,13 +1190,40 @@ class AutoScalingBackend(BaseBackend):
         return group
 
     def describe_auto_scaling_groups(
-        self, names: List[str]
+        self, names: List[str], filters: Optional[List[Dict[str, str]]] = None
     ) -> List[FakeAutoScalingGroup]:
-        groups = self.autoscaling_groups.values()
+
+        groups = list(self.autoscaling_groups.values())
+
+        if filters:
+            for f in filters:
+                if f["Name"] == "tag-key":
+                    groups = [
+                        group
+                        for group in groups
+                        if any(tag["Key"] in f["Values"] for tag in group.tags)
+                    ]
+                elif f["Name"] == "tag-value":
+                    groups = [
+                        group
+                        for group in groups
+                        if any(tag["Value"] in f["Values"] for tag in group.tags)
+                    ]
+                elif f["Name"].startswith("tag:"):
+                    tag_key = f["Name"][4:]
+                    groups = [
+                        group
+                        for group in groups
+                        if any(
+                            tag["Key"] == tag_key and tag["Value"] in f["Values"]
+                            for tag in group.tags
+                        )
+                    ]
+
         if names:
-            return [group for group in groups if group.name in names]
-        else:
-            return list(groups)
+            groups = [group for group in groups if group.name in names]
+
+        return groups
 
     def delete_auto_scaling_group(self, group_name: str) -> None:
         self.set_desired_capacity(group_name, 0)
diff --git a/moto/autoscaling/responses.py b/moto/autoscaling/responses.py
index d83ca039e..e9aa149ca 100644
--- a/moto/autoscaling/responses.py
+++ b/moto/autoscaling/responses.py
@@ -228,7 +228,10 @@ class AutoScalingResponse(BaseResponse):
     def describe_auto_scaling_groups(self) -> str:
         names = self._get_multi_param("AutoScalingGroupNames.member")
         token = self._get_param("NextToken")
-        all_groups = self.autoscaling_backend.describe_auto_scaling_groups(names)
+        filters = self._get_params().get("Filters", [])
+        all_groups = self.autoscaling_backend.describe_auto_scaling_groups(
+            names, filters=filters
+        )
         all_names = [group.name for group in all_groups]
         if token:
             start = all_names.index(token) + 1
diff --git a/moto/cognitoidp/models.py b/moto/cognitoidp/models.py
index 15416d4e7..0e960764e 100644
--- a/moto/cognitoidp/models.py
+++ b/moto/cognitoidp/models.py
@@ -1466,6 +1466,16 @@ class CognitoIdpBackend(BaseBackend):
                     "Session": session,
                 }
 
+            if user.sms_mfa_enabled and user.preferred_mfa_setting == "SMS_MFA":
+                session = str(random.uuid4())
+                self.sessions[session] = user_pool
+
+                return {
+                    "ChallengeName": "SMS_MFA",
+                    "ChallengeParameters": {},
+                    "Session": session,
+                }
+
             return self._log_user_in(user_pool, client, username)
         elif auth_flow in (AuthFlow.REFRESH_TOKEN, AuthFlow.REFRESH_TOKEN_AUTH):
             refresh_token: str = auth_parameters.get("REFRESH_TOKEN")  # type: ignore[assignment]
@@ -1578,13 +1588,13 @@ class CognitoIdpBackend(BaseBackend):
 
             del self.sessions[session]
             return self._log_user_in(user_pool, client, username)
-        elif challenge_name == "SOFTWARE_TOKEN_MFA":
+        elif challenge_name == "SOFTWARE_TOKEN_MFA" or challenge_name == "SMS_MFA":
             username: str = challenge_responses.get("USERNAME")  # type: ignore[no-redef]
             self.admin_get_user(user_pool.id, username)
 
-            software_token_mfa_code = challenge_responses.get("SOFTWARE_TOKEN_MFA_CODE")
-            if not software_token_mfa_code:
-                raise ResourceNotFoundError(software_token_mfa_code)
+            mfa_code = challenge_responses.get(f"{challenge_name}_CODE")
+            if not mfa_code:
+                raise ResourceNotFoundError(mfa_code)
 
             if client.generate_secret:
                 secret_hash = challenge_responses.get("SECRET_HASH")
diff --git a/moto/dynamodb/models/__init__.py b/moto/dynamodb/models/__init__.py
index c3be0ff5d..0627a238e 100644
--- a/moto/dynamodb/models/__init__.py
+++ b/moto/dynamodb/models/__init__.py
@@ -579,7 +579,11 @@ class DynamoDBBackend(BaseBackend):
                     item = item["Put"]
                     attrs = item["Item"]
                     table_name = item["TableName"]
-                    check_unicity(table_name, item)
+                    table = self.get_table(table_name)
+                    key = {table.hash_key_attr: attrs[table.hash_key_attr]}
+                    if table.range_key_attr is not None:
+                        key[table.range_key_attr] = attrs[table.range_key_attr]
+                    check_unicity(table_name, key)
                     condition_expression = item.get("ConditionExpression", None)
                     expression_attribute_names = item.get(
                         "ExpressionAttributeNames", None


Test patch:
diff --git a/tests/test_autoscaling/test_autoscaling_group_filters.py b/tests/test_autoscaling/test_autoscaling_group_filters.py
new file mode 100644
index 000000000..914c23238
--- /dev/null
+++ b/tests/test_autoscaling/test_autoscaling_group_filters.py
@@ -0,0 +1,116 @@
+import boto3
+
+from moto import mock_aws
+from tests import EXAMPLE_AMI_ID
+
+from .utils import setup_networking
+
+
+@mock_aws
+def test_describe_autoscaling_groups_filter_by_tag_key():
+    subnet = setup_networking()["subnet1"]
+    client = boto3.client("autoscaling", region_name="us-east-1")
+    create_asgs(client, subnet)
+
+    response = client.describe_auto_scaling_groups(
+        Filters=[{"Name": "tag-key", "Values": ["test_key1"]}]
+    )
+    group = response["AutoScalingGroups"][0]
+    tags = group["Tags"]
+
+    assert len(response["AutoScalingGroups"]) == 1
+    assert response["AutoScalingGroups"][0]["AutoScalingGroupName"] == "test_asg1"
+    assert {
+        "Key": "test_key1",
+        "PropagateAtLaunch": False,
+        "ResourceId": "test_asg1",
+        "ResourceType": "auto-scaling-group",
+        "Value": "test_value1",
+    } in tags
+
+
+@mock_aws
+def test_describe_autoscaling_groups_filter_by_tag_value():
+    subnet = setup_networking()["subnet1"]
+    client = boto3.client("autoscaling", region_name="us-east-1")
+    create_asgs(client, subnet)
+
+    response = client.describe_auto_scaling_groups(
+        Filters=[{"Name": "tag-value", "Values": ["test_value1"]}]
+    )
+    group = response["AutoScalingGroups"][0]
+    tags = group["Tags"]
+
+    assert len(response["AutoScalingGroups"]) == 1
+    assert response["AutoScalingGroups"][0]["AutoScalingGroupName"] == "test_asg1"
+    assert {
+        "Key": "test_key1",
+        "PropagateAtLaunch": False,
+        "ResourceId": "test_asg1",
+        "ResourceType": "auto-scaling-group",
+        "Value": "test_value1",
+    } in tags
+
+
+@mock_aws
+def test_describe_autoscaling_groups_filter_by_tag_key_value():
+    subnet = setup_networking()["subnet1"]
+    client = boto3.client("autoscaling", region_name="us-east-1")
+    create_asgs(client, subnet)
+
+    response = client.describe_auto_scaling_groups(
+        Filters=[{"Name": "tag:test_key1", "Values": ["test_value1"]}]
+    )
+    group = response["AutoScalingGroups"][0]
+    tags = group["Tags"]
+
+    assert len(response["AutoScalingGroups"]) == 1
+    assert response["AutoScalingGroups"][0]["AutoScalingGroupName"] == "test_asg1"
+    assert {
+        "Key": "test_key1",
+        "PropagateAtLaunch": False,
+        "ResourceId": "test_asg1",
+        "ResourceType": "auto-scaling-group",
+        "Value": "test_value1",
+    } in tags
+
+
+@mock_aws
+def test_describe_autoscaling_groups_no_filter():
+    subnet = setup_networking()["subnet1"]
+    client = boto3.client("autoscaling", region_name="us-east-1")
+    create_asgs(client, subnet)
+
+    response = client.describe_auto_scaling_groups()
+
+    assert len(response["AutoScalingGroups"]) == 2
+    assert response["AutoScalingGroups"][0]["AutoScalingGroupName"] == "test_asg1"
+    assert response["AutoScalingGroups"][1]["AutoScalingGroupName"] == "test_asg2"
+
+
+def create_asgs(client, subnet):
+    _ = client.create_launch_configuration(
+        LaunchConfigurationName="test_launch_configuration",
+        ImageId=EXAMPLE_AMI_ID,
+        InstanceType="t2.medium",
+    )
+    client.create_auto_scaling_group(
+        AutoScalingGroupName="test_asg1",
+        LaunchConfigurationName="test_launch_configuration",
+        MinSize=0,
+        MaxSize=20,
+        DesiredCapacity=5,
+        Tags=[
+            {"Key": "test_key1", "Value": "test_value1"},
+            {"Key": "test_key2", "Value": "test_value2"},
+        ],
+        VPCZoneIdentifier=subnet,
+    )
+    client.create_auto_scaling_group(
+        AutoScalingGroupName="test_asg2",
+        LaunchConfigurationName="test_launch_configuration",
+        MinSize=0,
+        MaxSize=20,
+        DesiredCapacity=5,
+        VPCZoneIdentifier=subnet,
+    )
diff --git a/tests/test_cognitoidp/test_cognitoidp.py b/tests/test_cognitoidp/test_cognitoidp.py
index 8867282e4..1ff151bfb 100644
--- a/tests/test_cognitoidp/test_cognitoidp.py
+++ b/tests/test_cognitoidp/test_cognitoidp.py
@@ -4454,6 +4454,56 @@ def test_admin_initiate_auth_when_token_totp_enabled():
     assert result["AuthenticationResult"]["TokenType"] == "Bearer"
 
 
+@mock_aws
+def test_admin_initiate_auth_when_sms_mfa_enabled():
+    conn = boto3.client("cognito-idp", "us-west-2")
+
+    result = authentication_flow(conn, "ADMIN_NO_SRP_AUTH")
+    user_pool_id = result["user_pool_id"]
+    username = result["username"]
+    client_id = result["client_id"]
+    password = result["password"]
+
+    # Set MFA SMS methods
+    conn.admin_set_user_mfa_preference(
+        Username=username,
+        UserPoolId=user_pool_id,
+        SMSMfaSettings={"Enabled": True, "PreferredMfa": True},
+    )
+    result = conn.admin_get_user(UserPoolId=user_pool_id, Username=username)
+    assert len(result["UserMFASettingList"]) == 1
+    assert result["PreferredMfaSetting"] == "SMS_MFA"
+
+    result = conn.admin_initiate_auth(
+        UserPoolId=user_pool_id,
+        ClientId=client_id,
+        AuthFlow="ADMIN_NO_SRP_AUTH",
+        AuthParameters={
+            "USERNAME": username,
+            "PASSWORD": password,
+        },
+    )
+
+    assert result["ChallengeName"] == "SMS_MFA"
+    assert result["Session"] != ""
+
+    result = conn.admin_respond_to_auth_challenge(
+        UserPoolId=user_pool_id,
+        ClientId=client_id,
+        ChallengeName="SMS_MFA",
+        Session=result["Session"],
+        ChallengeResponses={
+            "SMS_MFA_CODE": "123456",
+            "USERNAME": username,
+        },
+    )
+
+    assert result["AuthenticationResult"]["IdToken"] != ""
+    assert result["AuthenticationResult"]["AccessToken"] != ""
+    assert result["AuthenticationResult"]["RefreshToken"] != ""
+    assert result["AuthenticationResult"]["TokenType"] == "Bearer"
+
+
 @mock_aws
 def test_admin_setting_mfa_when_token_not_verified():
     conn = boto3.client("cognito-idp", "us-west-2")
diff --git a/tests/test_dynamodb/exceptions/test_dynamodb_transactions.py b/tests/test_dynamodb/exceptions/test_dynamodb_transactions.py
index 61f2c59dd..212739388 100644
--- a/tests/test_dynamodb/exceptions/test_dynamodb_transactions.py
+++ b/tests/test_dynamodb/exceptions/test_dynamodb_transactions.py
@@ -46,6 +46,55 @@ def test_multiple_transactions_on_same_item():
     )
 
 
+@mock_aws
+def test_transact_write_items__put_and_delete_on_same_item():
+    schema = {
+        "KeySchema": [
+            {"AttributeName": "pk", "KeyType": "HASH"},
+            {"AttributeName": "sk", "KeyType": "RANGE"},
+        ],
+        "AttributeDefinitions": [
+            {"AttributeName": "pk", "AttributeType": "S"},
+            {"AttributeName": "sk", "AttributeType": "S"},
+        ],
+    }
+    dynamodb = boto3.client("dynamodb", region_name="us-east-1")
+    dynamodb.create_table(
+        TableName="test-table", BillingMode="PAY_PER_REQUEST", **schema
+    )
+
+    with pytest.raises(ClientError) as exc:
+        dynamodb.transact_write_items(
+            TransactItems=[
+                {
+                    "Put": {
+                        "TableName": "test-table",
+                        "Item": {
+                            "pk": {"S": "test-pk"},
+                            "sk": {"S": "test-sk"},
+                            "field": {"S": "test-field"},
+                        },
+                    }
+                },
+                {
+                    "Delete": {
+                        "TableName": "test-table",
+                        "Key": {
+                            "pk": {"S": "test-pk"},
+                            "sk": {"S": "test-sk"},
+                        },
+                    }
+                },
+            ]
+        )
+    err = exc.value.response["Error"]
+    assert err["Code"] == "ValidationException"
+    assert (
+        err["Message"]
+        == "Transaction request cannot include multiple operations on one item"
+    )
+
+
 @mock_aws
 def test_transact_write_items__too_many_transactions():
     schema = {
diff --git a/tests/test_dynamodb/test_dynamodb.py b/tests/test_dynamodb/test_dynamodb.py
index 26d5e0545..808d48d2f 100644
--- a/tests/test_dynamodb/test_dynamodb.py
+++ b/tests/test_dynamodb/test_dynamodb.py
@@ -3856,24 +3856,27 @@ def test_transact_write_items_conditioncheck_passes():
     dynamodb.create_table(
         TableName="test-table", BillingMode="PAY_PER_REQUEST", **table_schema
     )
-    # Insert an item without email address
-    dynamodb.put_item(TableName="test-table", Item={"id": {"S": "foo"}})
-    # Put an email address, after verifying it doesn't exist yet
+    # Insert an item with email address
+    dynamodb.put_item(
+        TableName="test-table",
+        Item={"id": {"S": "foo"}, "email_address": {"S": "foo@moto.com"}},
+    )
+    # Put a new item, after verifying the exising item also has email address
     dynamodb.transact_write_items(
         TransactItems=[
             {
                 "ConditionCheck": {
                     "Key": {"id": {"S": "foo"}},
                     "TableName": "test-table",
-                    "ConditionExpression": "attribute_not_exists(#e)",
+                    "ConditionExpression": "attribute_exists(#e)",
                     "ExpressionAttributeNames": {"#e": "email_address"},
                 }
             },
             {
                 "Put": {
                     "Item": {
-                        "id": {"S": "foo"},
-                        "email_address": {"S": "test@moto.com"},
+                        "id": {"S": "bar"},
+                        "email_address": {"S": "bar@moto.com"},
                     },
                     "TableName": "test-table",
                 }
@@ -3882,8 +3885,9 @@ def test_transact_write_items_conditioncheck_passes():
     )
     # Assert all are present
     items = dynamodb.scan(TableName="test-table")["Items"]
-    assert len(items) == 1
-    assert items[0] == {"email_address": {"S": "test@moto.com"}, "id": {"S": "foo"}}
+    assert len(items) == 2
+    assert items[0] == {"email_address": {"S": "foo@moto.com"}, "id": {"S": "foo"}}
+    assert items[1] == {"email_address": {"S": "bar@moto.com"}, "id": {"S": "bar"}}
 
 
 @mock_aws
@@ -3896,12 +3900,12 @@ def test_transact_write_items_conditioncheck_fails():
     dynamodb.create_table(
         TableName="test-table", BillingMode="PAY_PER_REQUEST", **table_schema
     )
-    # Insert an item with email address
+    # Insert an item without email address
     dynamodb.put_item(
         TableName="test-table",
-        Item={"id": {"S": "foo"}, "email_address": {"S": "test@moto.com"}},
+        Item={"id": {"S": "foo"}},
     )
-    # Try to put an email address, but verify whether it exists
+    # Try putting a new item, after verifying the exising item also has email address
     # ConditionCheck should fail
     with pytest.raises(ClientError) as ex:
         dynamodb.transact_write_items(
@@ -3910,15 +3914,15 @@ def test_transact_write_items_conditioncheck_fails():
                     "ConditionCheck": {
                         "Key": {"id": {"S": "foo"}},
                         "TableName": "test-table",
-                        "ConditionExpression": "attribute_not_exists(#e)",
+                        "ConditionExpression": "attribute_exists(#e)",
                         "ExpressionAttributeNames": {"#e": "email_address"},
                     }
                 },
                 {
                     "Put": {
                         "Item": {
-                            "id": {"S": "foo"},
-                            "email_address": {"S": "update@moto.com"},
+                            "id": {"S": "bar"},
+                            "email_address": {"S": "bar@moto.com"},
                         },
                         "TableName": "test-table",
                     }
@@ -3929,10 +3933,10 @@ def test_transact_write_items_conditioncheck_fails():
     assert ex.value.response["Error"]["Code"] == "TransactionCanceledException"
     assert ex.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400
 
-    # Assert the original email address is still present
+    # Assert the original item is still present
     items = dynamodb.scan(TableName="test-table")["Items"]
     assert len(items) == 1
-    assert items[0] == {"email_address": {"S": "test@moto.com"}, "id": {"S": "foo"}}
+    assert items[0] == {"id": {"S": "foo"}}
 
 
 @mock_aws