Repository: getmoto/moto
Pull request #8575 (2025-02-07 15:20:24)

SES: Fix AlreadyExists and DoesNotExist error messages
[CreateReceiptRule](https://docs.aws.amazon.com/ses/latest/APIReference/API_CreateReceiptRule.html) and [CreateReceiptRuleSet](https://docs.aws.amazon.com/ses/latest/APIReference/API_CreateReceiptRuleSet.html) should raise `AlreadyExists` error according to the docs.

 Also, updating messages for `RuleDoesNotExist` and `RuleSetDoesNotExist` to match what AWS returns.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/8575?dropdown=coverage&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:
> Project coverage is 92.65%. Comparing base [(`98e2730`)](https://app.codecov.io/gh/getmoto/moto/commit/98e273086f475635fa77669fb4e40edcb0120431?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) to head [(`9d7ff53`)](https://app.codecov.io/gh/getmoto/moto/commit/9d7ff53655c2b50afb770ad16dc9d99d4eebb79e?dropdown=coverage&el=desc&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    #8575      +/-   ##
==========================================
- Coverage   92.65%   92.65%   -0.01%     
==========================================
  Files        1231     1231              
  Lines      106851   106848       -3     
==========================================
- Hits        99005    99002       -3     
  Misses       7846     7846              
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/8575/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/8575/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `28.13% <14.28%> (-0.01%)` | :arrow_down: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/8575/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `92.63% <100.00%> (-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/8575?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/moto/ec2/responses/_base_response.py b/moto/ec2/responses/_base_response.py
index da11941fa..c4a45e508 100644
--- a/moto/ec2/responses/_base_response.py
+++ b/moto/ec2/responses/_base_response.py
@@ -2,7 +2,7 @@ from typing import Any, Dict, Optional
 
 from moto.core.responses import BaseResponse
 
-from ..exceptions import EmptyTagSpecError, InvalidParameter
+from ..exceptions import EC2ClientError, EmptyTagSpecError, InvalidParameter
 from ..utils import convert_tag_spec
 
 
@@ -15,9 +15,14 @@ class EC2BaseResponse(BaseResponse):
 
     def _filters_from_querystring(self) -> Dict[str, str]:
         # [{"Name": x1, "Value": y1}, ..]
-        _filters = self._get_multi_param("Filter.")
+        _filters = self._get_multi_param("Filter.", skip_result_conversion=True)
         # return {x1: y1, ...}
-        return {f["Name"]: f["Value"] for f in _filters}
+        try:
+            return {f["Name"]: f.get("Value", []) for f in _filters}
+        except KeyError:
+            raise EC2ClientError(
+                "InvalidParameterValue", "The filter 'null' is invalid."
+            )
 
     def _parse_tag_specification(
         self, expected_type: Optional[str] = None
diff --git a/moto/ses/exceptions.py b/moto/ses/exceptions.py
index ccb5da15f..fba9bdc92 100644
--- a/moto/ses/exceptions.py
+++ b/moto/ses/exceptions.py
@@ -56,14 +56,9 @@ class TemplateDoesNotExist(SesError):
         super().__init__("TemplateDoesNotExist", message)
 
 
-class RuleSetNameAlreadyExists(SesError):
+class AlreadyExists(SesError):
     def __init__(self, message: str):
-        super().__init__("RuleSetNameAlreadyExists", message)
-
-
-class RuleAlreadyExists(SesError):
-    def __init__(self, message: str):
-        super().__init__("RuleAlreadyExists", message)
+        super().__init__("AlreadyExists", message)
 
 
 class RuleSetDoesNotExist(SesError):
diff --git a/moto/ses/models.py b/moto/ses/models.py
index 396bfd365..9920f6283 100644
--- a/moto/ses/models.py
+++ b/moto/ses/models.py
@@ -13,16 +13,15 @@ from moto.core.utils import utcnow
 from moto.sns.models import sns_backends
 
 from .exceptions import (
+    AlreadyExists,
     ConfigurationSetAlreadyExists,
     ConfigurationSetDoesNotExist,
     EventDestinationAlreadyExists,
     InvalidParameterValue,
     InvalidRenderingParameterException,
     MessageRejectedError,
-    RuleAlreadyExists,
     RuleDoesNotExist,
     RuleSetDoesNotExist,
-    RuleSetNameAlreadyExists,
     TemplateDoesNotExist,
     TemplateNameAlreadyExists,
     ValidationError,
@@ -616,15 +615,15 @@ class SESBackend(BaseBackend):
 
     def create_receipt_rule_set(self, rule_set_name: str) -> None:
         if self.receipt_rule_set.get(rule_set_name) is not None:
-            raise RuleSetNameAlreadyExists("Duplicate Receipt Rule Set Name.")
+            raise AlreadyExists(f"Rule set already exists: {rule_set_name}")
         self.receipt_rule_set[rule_set_name] = []
 
     def create_receipt_rule(self, rule_set_name: str, rule: Dict[str, Any]) -> None:
         rule_set = self.receipt_rule_set.get(rule_set_name)
         if rule_set is None:
-            raise RuleSetDoesNotExist("Invalid Rule Set Name.")
+            raise RuleSetDoesNotExist(f"Rule set does not exist: {rule_set_name}")
         if rule in rule_set:
-            raise RuleAlreadyExists("Duplicate Rule Name.")
+            raise AlreadyExists(f"Rule already exists: {rule['name']}")
         rule_set.append(rule)
         self.receipt_rule_set[rule_set_name] = rule_set
 
@@ -642,13 +641,13 @@ class SESBackend(BaseBackend):
         rule_set = self.receipt_rule_set.get(rule_set_name)
 
         if rule_set is None:
-            raise RuleSetDoesNotExist("Invalid Rule Set Name.")
+            raise RuleSetDoesNotExist(f"Rule set does not exist: {rule_set_name}")
 
         for receipt_rule in rule_set:
             if receipt_rule["name"] == rule_name:
                 return receipt_rule
 
-        raise RuleDoesNotExist("Invalid Rule Name.")
+        raise RuleDoesNotExist(f"Rule does not exist: {rule_name}")
 
     def update_receipt_rule(self, rule_set_name: str, rule: Dict[str, Any]) -> None:
         rule_set = self.receipt_rule_set.get(rule_set_name)
diff --git a/moto/utilities/utils.py b/moto/utilities/utils.py
index 1b4ccb53f..44908f86e 100644
--- a/moto/utilities/utils.py
+++ b/moto/utilities/utils.py
@@ -98,6 +98,10 @@ def filter_resources(
                 ):
                     result.remove(resource)
                     break
+            elif attrs[0] in filters:
+                # In case the filter exists but the value of the filter is empty, the filter shouldn't match
+                result.remove(resource)
+                break
     return result
 
 


Test patch:
diff --git a/tests/test_ec2/test_amis.py b/tests/test_ec2/test_amis.py
index 23a01c0de..b7d60b25d 100644
--- a/tests/test_ec2/test_amis.py
+++ b/tests/test_ec2/test_amis.py
@@ -1337,6 +1337,32 @@ def test_ami_filter_by_unknown_ownerid():
     assert len(images) == 0
 
 
+@mock_aws()
+def test_ami_filter_without_value():
+    client = boto3.client("ec2", region_name="us-east-1")
+    ec2_resource = boto3.resource("ec2", region_name="us-west-1")
+
+    image_name = str(uuid4())[0:12]
+
+    instance = ec2_resource.create_instances(
+        ImageId=EXAMPLE_AMI_ID, MinCount=1, MaxCount=1
+    )[0]
+    instance.create_image(Name=image_name)
+
+    images = client.describe_images(Filters=[{"Name": "owner-alias", "Values": []}])[
+        "Images"
+    ]
+    assert len(images) == 0
+
+    images = client.describe_images(Filters=[{"Name": "owner-alias"}])["Images"]
+    assert len(images) == 0
+
+    with pytest.raises(ClientError) as err:
+        client.describe_images(Filters=[{"Values": ["123"]}])
+
+    assert err.value.response["Error"]["Code"] == "InvalidParameterValue"
+
+
 @mock_aws
 def test_describe_images_dryrun():
     client = boto3.client("ec2", region_name="us-east-1")
diff --git a/tests/test_ec2/test_availability_zones_and_regions.py b/tests/test_ec2/test_availability_zones_and_regions.py
index d092bcde4..6724c4c06 100644
--- a/tests/test_ec2/test_availability_zones_and_regions.py
+++ b/tests/test_ec2/test_availability_zones_and_regions.py
@@ -77,6 +77,17 @@ def test_availability_zones__parameters():
     assert len(zones) == 1
     assert zones[0]["ZoneId"] == "use1-az1"
 
+    # This shouldn't raise an unhandled exception
+    zones = us_east.describe_availability_zones(Filters=[{"Name": "state"}])[
+        "AvailabilityZones"
+    ]
+    assert len(zones) == 0
+
+    zones = us_east.describe_availability_zones(
+        Filters=[{"Name": "state", "Values": []}]
+    )["AvailabilityZones"]
+    assert len(zones) == 0
+
 
 @mock_aws
 def test_describe_availability_zones_dryrun():
diff --git a/tests/test_ec2/test_subnets.py b/tests/test_ec2/test_subnets.py
index 850277ed3..e83c26668 100644
--- a/tests/test_ec2/test_subnets.py
+++ b/tests/test_ec2/test_subnets.py
@@ -330,6 +330,16 @@ def test_get_subnets_filtering():
         with pytest.raises(NotImplementedError):
             client.describe_subnets(Filters=filters)
 
+    # Filter without a Value.
+    subnets_with_invalid_filter = client.describe_subnets(Filters=[{"Name": "vpc-id"}])[
+        "Subnets"
+    ]
+    assert len(subnets_with_invalid_filter) == 0
+    subnets_with_invalid_filter = client.describe_subnets(
+        Filters=[{"Name": "vpc-id", "Values": []}]
+    )["Subnets"]
+    assert len(subnets_with_invalid_filter) == 0
+
 
 @mock_aws
 def test_create_subnet_response_fields():
diff --git a/tests/test_ses/test_ses_boto3.py b/tests/test_ses/test_ses_boto3.py
index 427c62849..c5426958d 100644
--- a/tests/test_ses/test_ses_boto3.py
+++ b/tests/test_ses/test_ses_boto3.py
@@ -636,7 +636,10 @@ def test_create_receipt_rule_set():
     with pytest.raises(ClientError) as ex:
         conn.create_receipt_rule_set(RuleSetName="testRuleSet")
 
-    assert ex.value.response["Error"]["Code"] == "RuleSetNameAlreadyExists"
+    assert ex.value.response["Error"]["Code"] == "AlreadyExists"
+    assert (
+        ex.value.response["Error"]["Message"] == "Rule set already exists: testRuleSet"
+    )
 
 
 @mock_aws
@@ -704,7 +707,8 @@ def test_create_receipt_rule():
             },
         )
 
-    assert ex.value.response["Error"]["Code"] == "RuleAlreadyExists"
+    assert ex.value.response["Error"]["Code"] == "AlreadyExists"
+    assert ex.value.response["Error"]["Message"] == "Rule already exists: testRule"
 
     with pytest.raises(ClientError) as ex:
         conn.create_receipt_rule(
@@ -736,6 +740,10 @@ def test_create_receipt_rule():
         )
 
     assert ex.value.response["Error"]["Code"] == "RuleSetDoesNotExist"
+    assert (
+        ex.value.response["Error"]["Message"]
+        == "Rule set does not exist: InvalidRuleSetaName"
+    )
 
 
 @mock_aws
@@ -897,6 +905,10 @@ def test_describe_receipt_rule():
         conn.describe_receipt_rule(RuleSetName="invalidRuleSetName", RuleName=rule_name)
 
     assert error.value.response["Error"]["Code"] == "RuleSetDoesNotExist"
+    assert (
+        error.value.response["Error"]["Message"]
+        == "Rule set does not exist: invalidRuleSetName"
+    )
 
     with pytest.raises(ClientError) as error:
         conn.describe_receipt_rule(
@@ -904,6 +916,10 @@ def test_describe_receipt_rule():
         )
 
     assert error.value.response["Error"]["Code"] == "RuleDoesNotExist"
+    assert (
+        error.value.response["Error"]["Message"]
+        == "Rule does not exist: invalidRuleName"
+    )
 
 
 @mock_aws