Repository: getmoto/moto
Pull request #8826 (2025-04-18 14:26:24)

[Cloudfront] Add TrustedSigners Class
TrustedSigners class added. This allows users to add an AWSAccountNumber to a CacheBehavior/DefaultCacheBehavior and it will auto update the enabled/qty field. Similar to TrustedKeyGroups functionally.

Added a test to support. Format/Checked, tested local and on server.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/8826?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.82%. Comparing base [(`107da42`)](https://app.codecov.io/gh/getmoto/moto/commit/107da425c278a6aece68ee27d72f135494876ad4?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) to head [(`def2b92`)](https://app.codecov.io/gh/getmoto/moto/commit/def2b92083d71e3e20bc13d08f0ccf88fda47ce4?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    #8826   +/-   ##
=======================================
  Coverage   92.82%   92.82%           
=======================================
  Files        1266     1266           
  Lines      110520   110526    +6     
=======================================
+ Hits       102586   102594    +8     
+ Misses       7934     7932    -2     
```

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

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>

Patch:
diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py
index 807942e05..828d8a94b 100644
--- a/moto/cloudformation/parsing.py
+++ b/moto/cloudformation/parsing.py
@@ -82,6 +82,9 @@ NULL_MODELS = [
     "AWS::CloudFormation::WaitConditionHandle",
 ]
 
+# How often CF should attempt to update/delete a resource, before giving up
+MAX_UPDATE_ATTEMPTS = 5
+
 DEFAULT_REGION = "us-east-1"
 
 logger = logging.getLogger("moto")
@@ -842,10 +845,21 @@ class ResourceMap(collections_abc.Mapping):  # type: ignore[type-arg]
     ) -> None:
         resource_names_by_action = self.build_resource_diff(template, parameters)
 
-        for logical_name in resource_names_by_action["Remove"]:
-            resource_json = self._resource_json_map[logical_name]
-            resource = self._parsed_resources[logical_name]
-            self._delete_resource(resource, resource_json)
+        tries = 1
+        while resource_names_by_action["Remove"] and tries < MAX_UPDATE_ATTEMPTS:
+            for logical_name in resource_names_by_action["Remove"].copy():
+                resource_json = self._resource_json_map[logical_name]
+                try:
+                    resource = self._parsed_resources[logical_name]
+                    self._delete_resource(resource, resource_json)
+                except Exception as e:
+                    # skip over dependency violations, and try again in another pass
+                    last_exception = e
+                else:
+                    resource_names_by_action["Remove"].remove(logical_name)
+            tries += 1
+        if tries == MAX_UPDATE_ATTEMPTS:
+            raise last_exception
 
         self._template = template
         if parameters:
@@ -862,7 +876,7 @@ class ResourceMap(collections_abc.Mapping):  # type: ignore[type-arg]
             _ = self[logical_name]
 
         tries = 1
-        while resource_names_by_action["Modify"] and tries < 5:
+        while resource_names_by_action["Modify"] and tries < MAX_UPDATE_ATTEMPTS:
             for logical_name in resource_names_by_action["Modify"].copy():
                 resource_json = self._resource_json_map[logical_name]
                 try:
@@ -881,7 +895,7 @@ class ResourceMap(collections_abc.Mapping):  # type: ignore[type-arg]
                     self._parsed_resources[logical_name] = changed_resource
                     resource_names_by_action["Modify"].remove(logical_name)
             tries += 1
-        if tries == 5:
+        if tries == MAX_UPDATE_ATTEMPTS:
             raise last_exception
 
     def delete(self) -> None:
diff --git a/moto/cloudfront/models.py b/moto/cloudfront/models.py
index b1b01c8ff..f1a9f5d8c 100644
--- a/moto/cloudfront/models.py
+++ b/moto/cloudfront/models.py
@@ -65,6 +65,14 @@ class ForwardedValues:
         self.cookies: List[Dict[str, Any]] = config.get("Cookies") or []
 
 
+class TrustedSigners:
+    def __init__(self, config: Dict[str, Any]):
+        items = config.get("Items") or {}
+        self.acct_nums = items.get("AwsAccountNumber") or []
+        if isinstance(self.acct_nums, str):
+            self.acct_nums = [self.acct_nums]
+
+
 class TrustedKeyGroups:
     def __init__(self, config: Dict[str, Any]):
         items = config.get("Items") or {}
@@ -77,7 +85,7 @@ class DefaultCacheBehaviour:
     def __init__(self, config: Dict[str, Any]):
         self.target_origin_id = config["TargetOriginId"]
         self.trusted_signers_enabled = False
-        self.trusted_signers: List[Any] = []
+        self.trusted_signers = TrustedSigners(config.get("TrustedSigners") or {})
         self.trusted_key_groups = TrustedKeyGroups(config.get("TrustedKeyGroups") or {})
         self.viewer_protocol_policy = config["ViewerProtocolPolicy"]
         methods = config.get("AllowedMethods", {})
diff --git a/moto/cloudfront/responses.py b/moto/cloudfront/responses.py
index 023d10186..0da2fd695 100644
--- a/moto/cloudfront/responses.py
+++ b/moto/cloudfront/responses.py
@@ -325,10 +325,10 @@ DIST_CONFIG_TEMPLATE = """
       <DefaultCacheBehavior>
         <TargetOriginId>{{ distribution.distribution_config.default_cache_behavior.target_origin_id }}</TargetOriginId>
         <TrustedSigners>
-          <Enabled>{{ distribution.distribution_config.default_cache_behavior.trusted_signers_enabled }}</Enabled>
-          <Quantity>{{ distribution.distribution_config.default_cache_behavior.trusted_signers|length }}</Quantity>
+          <Enabled>{{ 'true' if distribution.distribution_config.default_cache_behavior.trusted_signers.acct_nums|length > 0 else 'false' }}</Enabled>          
+          <Quantity>{{ distribution.distribution_config.default_cache_behavior.trusted_signers.acct_nums|length }}</Quantity>
           <Items>
-            {% for aws_account_number  in distribution.distribution_config.default_cache_behavior.trusted_signers %}
+            {% for aws_account_number  in distribution.distribution_config.default_cache_behavior.trusted_signers.acct_nums %}
               <AwsAccountNumber>{{ aws_account_number }}</AwsAccountNumber>
             {% endfor %}
           </Items>
@@ -432,10 +432,10 @@ DIST_CONFIG_TEMPLATE = """
                 <PathPattern>{{ behaviour.path_pattern }}</PathPattern>
                 <TargetOriginId>{{ behaviour.target_origin_id }}</TargetOriginId>
                 <TrustedSigners>
-                  <Enabled>{{ behaviour.trusted_signers.enabled }}</Enabled>
-                  <Quantity>{{ behaviour.trusted_signers | length }}</Quantity>
+                  <Enabled>{{ 'true' if behaviour.trusted_signers.acct_nums|length > 0 else 'false' }}</Enabled>
+                  <Quantity>{{ behaviour.trusted_signers.acct_nums | length }}</Quantity>
                   <Items>
-                    {% for account_nr  in behaviour.trusted_signers %}
+                    {% for account_nr  in behaviour.trusted_signers.acct_nums %}
                       <AwsAccountNumber>{{ account_nr }}</AwsAccountNumber>
                     {% endfor %}
                   </Items>
diff --git a/moto/ec2/models/vpcs.py b/moto/ec2/models/vpcs.py
index 1ea59d839..cc66ad297 100644
--- a/moto/ec2/models/vpcs.py
+++ b/moto/ec2/models/vpcs.py
@@ -775,6 +775,15 @@ class VPCBackend:
         return matches
 
     def delete_vpc(self, vpc_id: str) -> VPC:
+        # Refuse to delete if there are dependent subnets
+        subnets = self.describe_subnets(filters={"vpc-id": vpc_id})  # type: ignore[attr-defined]
+        if subnets:
+            raise DependencyViolationError(
+                f"The vpc '{vpc_id}' has dependencies and cannot be deleted."
+            )
+
+        # TODO: Refuse to delete if there are dependent IGW
+
         # Do not delete if any VPN Gateway is attached
         vpn_gateways = self.describe_vpn_gateways(filters={"attachment.vpc-id": vpc_id})  # type: ignore[attr-defined]
         vpn_gateways = [
diff --git a/moto/rds/models.py b/moto/rds/models.py
index 3a291b6d6..de0855cab 100644
--- a/moto/rds/models.py
+++ b/moto/rds/models.py
@@ -1454,7 +1454,9 @@ class DBInstance(EventMixin, CloudFormationModel, RDSBaseModel):
             self.master_user_secret = MasterUserSecret(
                 self, master_user_secret_kms_key_id
             )
-        elif manage_master_user_password is False:
+        elif manage_master_user_password is False and hasattr(
+            self, "master_user_secret"
+        ):
             self.master_user_secret.delete_secret()
             del self.master_user_secret
 
@@ -3141,7 +3143,9 @@ class RDSBackend(BaseBackend):
             if manage_master_user_password:
                 kms_key_id = kwargs.pop("master_user_secret_kms_key_id", None)
                 cluster.master_user_secret = MasterUserSecret(cluster, kms_key_id)
-            else:
+            elif not manage_master_user_password and hasattr(
+                cluster, "master_user_secret"
+            ):
                 cluster.master_user_secret.delete_secret()
                 del cluster.master_user_secret
 
diff --git a/moto/ses/models.py b/moto/ses/models.py
index 566ecf951..899bf8694 100644
--- a/moto/ses/models.py
+++ b/moto/ses/models.py
@@ -206,6 +206,7 @@ class SESBackend(BaseBackend):
         self.identity_mail_from_domains: Dict[str, Dict[str, Any]] = {}
         self.templates: Dict[str, Dict[str, str]] = {}
         self.receipt_rule_set: Dict[str, List[Dict[str, Any]]] = {}
+        self.dkim_tokens: Dict[str, List[str]] = {}
 
     def _is_verified_address(self, source: str) -> bool:
         _, address = parseaddr(source)
@@ -740,5 +741,42 @@ class SESBackend(BaseBackend):
             config_set.reputation_options = {}
         config_set.reputation_options["ReputationMetricsEnabled"] = enabled
 
+    def get_identity_dkim_attributes(
+        self, identities: List[str]
+    ) -> Dict[str, Dict[str, Any]]:
+        """
+        Returns the current status of Easy DKIM signing for an entity.
+        For domain name identities, this operation also returns the DKIM tokens.
+        """
+        result = {}
+        for identity in identities:
+            is_domain = "@" not in identity
+            dkim_enabled = True
+            verification_status = (
+                "Success"
+                if identity in (self.domains + self.addresses)
+                else "NotStarted"
+            )
+
+            dkim_data = {
+                "DkimEnabled": dkim_enabled,
+                "DkimVerificationStatus": verification_status,
+            }
+
+            # Only include tokens for domain identities
+            if is_domain and verification_status == "Success":
+                if identity not in self.dkim_tokens:
+                    # Generate new DKIM tokens for the domain
+                    self.dkim_tokens[identity] = [
+                        "vvjuipp74whm76gqoni7qmwwn4w4qusjiainivf6sf",
+                        "3frqe7jn4obpuxjpwpolz6ipb3k5nvt2nhjpik2oy",
+                        "wrqplteh7oodxnad7hsl4mixg2uavzneazxv5sxi2",
+                    ]
+                dkim_data["DkimTokens"] = self.dkim_tokens[identity]
+
+            result[identity] = dkim_data
+
+        return result
+
 
 ses_backends = BackendDict(SESBackend, "ses")
diff --git a/moto/ses/responses.py b/moto/ses/responses.py
index 38ddc04b6..b369ba611 100644
--- a/moto/ses/responses.py
+++ b/moto/ses/responses.py
@@ -414,6 +414,12 @@ class EmailResponse(BaseResponse):
         )
         return template.render()
 
+    def get_identity_dkim_attributes(self) -> str:
+        identities = self._get_multi_param("Identities.member.")
+        dkim_attributes = self.backend.get_identity_dkim_attributes(identities)
+        template = self.response_template(GET_IDENTITY_DKIM_ATTRIBUTES_RESPONSE)
+        return template.render(dkim_attributes=dkim_attributes)
+
 
 VERIFY_EMAIL_IDENTITY = """<VerifyEmailIdentityResponse xmlns="http://ses.amazonaws.com/doc/2010-12-01/">
   <VerifyEmailIdentityResult/>
@@ -880,3 +886,29 @@ UPDATE_CONFIGURATION_SET_REPUTATION_METRICS_ENABLED_RESPONSE = """<UpdateConfigu
     <RequestId>47e0ef1a-9bf2-11e1-9279-0100e8cf109a</RequestId>
   </ResponseMetadata>
 </UpdateConfigurationSetReputationMetricsEnabledResponse>"""
+
+GET_IDENTITY_DKIM_ATTRIBUTES_RESPONSE = """<GetIdentityDkimAttributesResponse xmlns="http://ses.amazonaws.com/doc/2010-12-01/">
+  <GetIdentityDkimAttributesResult>
+    <DkimAttributes>
+      {% for identity, attributes in dkim_attributes.items() %}
+      <entry>
+        <key>{{ identity }}</key>
+        <value>
+          <DkimEnabled>{{ attributes.DkimEnabled|lower }}</DkimEnabled>
+          <DkimVerificationStatus>{{ attributes.DkimVerificationStatus }}</DkimVerificationStatus>
+          {% if attributes.get('DkimTokens') %}
+          <DkimTokens>
+            {% for token in attributes.DkimTokens %}
+            <member>{{ token }}</member>
+            {% endfor %}
+          </DkimTokens>
+          {% endif %}
+        </value>
+      </entry>
+      {% endfor %}
+    </DkimAttributes>
+  </GetIdentityDkimAttributesResult>
+  <ResponseMetadata>
+    <RequestId>9662c15b-c469-11e1-99d1-797d6ecd6414</RequestId>
+  </ResponseMetadata>
+</GetIdentityDkimAttributesResponse>"""


Test patch:
diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py
index 26df7c3bc..85f5acd29 100644
--- a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py
+++ b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py
@@ -1361,6 +1361,9 @@ def test_update_stack_deleted_resources_can_reference_deleted_resources():
             "AWSTemplateFormatVersion": "2010-09-09",
             "Parameters": {"TimeoutParameter": {"Default": 61, "Type": "String"}},
             "Resources": {
+                # Note that we're listing the VPC first on purpose, even though the dependency between them
+                # means that the Subnet should be deleted first, before the VPC can be deleted
+                # Our implementation needs to handle deletion of resources regardless of the order
                 "VPC": {
                     "Type": "AWS::EC2::VPC",
                     "Properties": {"CidrBlock": "10.0.0.0/16"},
diff --git a/tests/test_cloudfront/test_cloudfront.py b/tests/test_cloudfront/test_cloudfront.py
index 0d352903c..acbd0f00c 100644
--- a/tests/test_cloudfront/test_cloudfront.py
+++ b/tests/test_cloudfront/test_cloudfront.py
@@ -234,3 +234,47 @@ def test_update_distribution_applies_changes():
         "DistributionConfig"
     ]
     assert actual_dist_config["Enabled"]
+
+
+@mock_aws
+def test_update_distribution_trusted_signers():
+    client = boto3.client("cloudfront", region_name="us-east-1")
+
+    # Create standard distribution
+    config = scaffold.example_distribution_config(ref="ref")
+
+    dist = client.create_distribution(DistributionConfig=config)
+    dist_id = dist["Distribution"]["Id"]
+
+    # Assert that default TrustedSigners 'Enabled' value is false
+    actual_dist_config = client.get_distribution(Id=dist_id)["Distribution"][
+        "DistributionConfig"
+    ]
+    assert (
+        actual_dist_config["DefaultCacheBehavior"]["TrustedSigners"]["Enabled"] is False
+    )
+
+    # Add an item to TrustedSigners
+    get_config_response = client.get_distribution_config(Id=dist_id)
+    actual_dist_config = get_config_response["DistributionConfig"]
+    etag = get_config_response["ETag"]
+    actual_dist_config["DefaultCacheBehavior"]["TrustedSigners"]["Items"] = [
+        "AwsAccountNumber123"
+    ]
+
+    client.update_distribution(
+        DistributionConfig=actual_dist_config, Id=dist_id, IfMatch=etag
+    )
+
+    # Assert that all TrustedSigners fields updated
+    updated_dist_config = client.get_distribution(Id=dist_id)["Distribution"][
+        "DistributionConfig"
+    ]
+    assert (
+        updated_dist_config["DefaultCacheBehavior"]["TrustedSigners"]["Quantity"] == 1
+    )
+    assert updated_dist_config["DefaultCacheBehavior"]["TrustedSigners"]["Enabled"]
+    assert (
+        "AwsAccountNumber123"
+        in updated_dist_config["DefaultCacheBehavior"]["TrustedSigners"]["Items"]
+    )
diff --git a/tests/test_ec2/test_vpcs.py b/tests/test_ec2/test_vpcs.py
index 221b66a31..68e12fe55 100644
--- a/tests/test_ec2/test_vpcs.py
+++ b/tests/test_ec2/test_vpcs.py
@@ -23,6 +23,11 @@ def test_creating_a_vpc_in_empty_region_does_not_make_this_vpc_the_default():
     client = boto3.client("ec2", region_name="eu-north-1")
     all_vpcs = retrieve_all_vpcs(client)
     for vpc in all_vpcs:
+        subnets = client.describe_subnets(
+            Filters=[{"Name": "vpc-id", "Values": [vpc["VpcId"]]}]
+        )["Subnets"]
+        for subnet in subnets:
+            client.delete_subnet(SubnetId=subnet["SubnetId"])
         client.delete_vpc(VpcId=vpc["VpcId"])
     # create vpc
     client.create_vpc(CidrBlock="10.0.0.0/16")
@@ -40,6 +45,11 @@ def test_create_default_vpc():
     client = boto3.client("ec2", region_name="eu-north-1")
     all_vpcs = retrieve_all_vpcs(client)
     for vpc in all_vpcs:
+        subnets = client.describe_subnets(
+            Filters=[{"Name": "vpc-id", "Values": [vpc["VpcId"]]}]
+        )["Subnets"]
+        for subnet in subnets:
+            client.delete_subnet(SubnetId=subnet["SubnetId"])
         client.delete_vpc(VpcId=vpc["VpcId"])
     # create default vpc
     client.create_default_vpc()
@@ -1238,3 +1248,21 @@ def test_describe_prefix_lists():
         ]
     )
     assert len(result_filtered["PrefixLists"]) == 1
+
+
+@mock_aws
+def test_delete_vpc_with_subnet_dependency():
+    client = boto3.client("ec2", region_name="us-east-1")
+    vpc = client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"]
+    subnet = client.create_subnet(VpcId=vpc["VpcId"], CidrBlock="10.0.1.0/24")["Subnet"]
+
+    with pytest.raises(ClientError) as exc:
+        client.delete_vpc(VpcId=vpc["VpcId"])
+    assert exc.value.response["Error"]["Code"] == "DependencyViolation"
+    assert (
+        exc.value.response["Error"]["Message"]
+        == f"The vpc '{vpc['VpcId']}' has dependencies and cannot be deleted."
+    )
+
+    client.delete_subnet(SubnetId=subnet["SubnetId"])
+    client.delete_vpc(VpcId=vpc["VpcId"])
diff --git a/tests/test_rds/test_rds.py b/tests/test_rds/test_rds.py
index 7fdf0446c..a9650033e 100644
--- a/tests/test_rds/test_rds.py
+++ b/tests/test_rds/test_rds.py
@@ -486,6 +486,10 @@ def test_modify_db_instance_manage_master_user_password(
         DBInstanceIdentifier=db_id, ManageMasterUserPassword=False
     )
 
+    retry_revert_modification_response = client.modify_db_instance(
+        DBInstanceIdentifier=db_id, ManageMasterUserPassword=False
+    )
+
     assert db_instance.get("MasterUserSecret") is None
     master_user_secret = modify_response["DBInstance"]["MasterUserSecret"]
     assert len(master_user_secret.keys()) == 3
@@ -510,6 +514,9 @@ def test_modify_db_instance_manage_master_user_password(
         == describe_response["DBInstances"][0]["MasterUserSecret"]["SecretArn"]
     )
     assert revert_modification_response["DBInstance"].get("MasterUserSecret") is None
+    assert (
+        retry_revert_modification_response["DBInstance"].get("MasterUserSecret") is None
+    )
 
 
 @pytest.mark.parametrize("with_apply_immediately", [True, False])
diff --git a/tests/test_rds/test_rds_clusters.py b/tests/test_rds/test_rds_clusters.py
index dd30ab133..1570656c2 100644
--- a/tests/test_rds/test_rds_clusters.py
+++ b/tests/test_rds/test_rds_clusters.py
@@ -189,6 +189,10 @@ def test_modify_db_cluster_manage_master_user_password(client, with_custom_kms_k
         DBClusterIdentifier=cluster_id, ManageMasterUserPassword=False
     )
 
+    retry_revert_modification_response = client.modify_db_cluster(
+        DBClusterIdentifier=cluster_id, ManageMasterUserPassword=False
+    )
+
     assert create_response["DBCluster"].get("MasterUserSecret") is None
     master_user_secret = modify_response["DBCluster"]["MasterUserSecret"]
     assert len(master_user_secret.keys()) == 3
@@ -213,6 +217,9 @@ def test_modify_db_cluster_manage_master_user_password(client, with_custom_kms_k
         == describe_response["DBClusters"][0]["MasterUserSecret"]["SecretArn"]
     )
     assert revert_modification_response["DBCluster"].get("MasterUserSecret") is None
+    assert (
+        retry_revert_modification_response["DBCluster"].get("MasterUserSecret") is None
+    )
 
 
 @pytest.mark.parametrize("with_apply_immediately", [True, False])
diff --git a/tests/test_ses/test_ses_boto3.py b/tests/test_ses/test_ses_boto3.py
index f35075da5..c4a1a4b40 100644
--- a/tests/test_ses/test_ses_boto3.py
+++ b/tests/test_ses/test_ses_boto3.py
@@ -1574,3 +1574,46 @@ def test_update_configuration_set_reputation_metrics():
             ConfigurationSetName="non-existent", Enabled=True
         )
     assert e.value.response["Error"]["Code"] == "ConfigurationSetDoesNotExist"
+
+
+@mock_aws
+def test_get_identity_dkim_attributes():
+    conn = boto3.client("ses", region_name="us-east-1")
+
+    # Create domain identity
+    domain = "example.com"
+    conn.verify_domain_identity(Domain=domain)
+
+    # Create email identity
+    email = "test@example.com"
+    conn.verify_email_identity(EmailAddress=email)
+
+    # Test getting DKIM attributes for both domain and email
+    response = conn.get_identity_dkim_attributes(Identities=[domain, email])
+
+    # Verify response structure
+    attributes = response["DkimAttributes"]
+    assert len(attributes) == 2
+
+    # Verify domain attributes
+    domain_attrs = attributes[domain]
+    assert domain_attrs["DkimEnabled"] is True
+    assert domain_attrs["DkimVerificationStatus"] == "Success"
+    assert len(domain_attrs["DkimTokens"]) == 3
+    for token in domain_attrs["DkimTokens"]:
+        assert isinstance(token, str)
+        assert len(token) > 0
+
+    # Verify email attributes
+    email_attrs = attributes[email]
+    assert email_attrs["DkimEnabled"] is True
+    assert email_attrs["DkimVerificationStatus"] == "Success"
+    assert "DkimTokens" not in email_attrs  # Email identities don't have DKIM tokens
+
+    # Test unverified identity
+    unverified = "unverified.com"
+    response = conn.get_identity_dkim_attributes(Identities=[unverified])
+    unverified_attrs = response["DkimAttributes"][unverified]
+    assert unverified_attrs["DkimEnabled"] is True
+    assert unverified_attrs["DkimVerificationStatus"] == "NotStarted"
+    assert "DkimTokens" not in unverified_attrs