Repository: getmoto/moto
Pull request #8821 (2025-04-17 06:04:51)

implement UpdateConfigurationSetReputationMetricsEnabled API for SES
See https://github.com/localstack/localstack/issues/5744 

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/8821?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
Attention: Patch coverage is `92.85714%` with `1 line` in your changes missing coverage. Please review.
> Project coverage is 92.82%. Comparing base [(`ec3224f`)](https://app.codecov.io/gh/getmoto/moto/commit/ec3224f772c8c3b46830f99e8dbc31944124e552?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) to head [(`fd056f6`)](https://app.codecov.io/gh/getmoto/moto/commit/fd056f674f122b744b5377b880e8008c8f3e9516?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).

| [Files with missing lines](https://app.codecov.io/gh/getmoto/moto/pull/8821?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Patch % | Lines |
|---|---|---|
| [moto/ses/models.py](https://app.codecov.io/gh/getmoto/moto/pull/8821?src=pr&el=tree&filepath=moto%2Fses%2Fmodels.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#diff-bW90by9zZXMvbW9kZWxzLnB5) | 85.71% | [1 Missing :warning: ](https://app.codecov.io/gh/getmoto/moto/pull/8821?src=pr&el=tree&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    #8821   +/-   ##
=======================================
  Coverage   92.82%   92.82%           
=======================================
  Files        1266     1266           
  Lines      110461   110475   +14     
=======================================
+ Hits       102533   102546   +13     
- Misses       7928     7929    +1     
```

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

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/8821?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/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index 17378c8c7..beb10ffb5 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -9336,7 +9336,7 @@
 - [X] detect_document_text
 - [ ] get_adapter
 - [ ] get_adapter_version
-- [ ] get_document_analysis
+- [X] get_document_analysis
 - [X] get_document_text_detection
 - [ ] get_expense_analysis
 - [ ] get_lending_analysis
@@ -9344,7 +9344,7 @@
 - [ ] list_adapter_versions
 - [ ] list_adapters
 - [ ] list_tags_for_resource
-- [ ] start_document_analysis
+- [X] start_document_analysis
 - [X] start_document_text_detection
 - [ ] start_expense_analysis
 - [ ] start_lending_analysis
diff --git a/docs/docs/services/textract.rst b/docs/docs/services/textract.rst
index 6a16fb11a..664848380 100644
--- a/docs/docs/services/textract.rst
+++ b/docs/docs/services/textract.rst
@@ -26,7 +26,7 @@ textract
 - [X] detect_document_text
 - [ ] get_adapter
 - [ ] get_adapter_version
-- [ ] get_document_analysis
+- [X] get_document_analysis
 - [X] get_document_text_detection
   
         Pagination has not yet been implemented
@@ -38,7 +38,7 @@ textract
 - [ ] list_adapter_versions
 - [ ] list_adapters
 - [ ] list_tags_for_resource
-- [ ] start_document_analysis
+- [X] start_document_analysis
 - [X] start_document_text_detection
   
         The following parameters have not yet been implemented: ClientRequestToken, JobTag, NotificationChannel, OutputConfig, KmsKeyID
diff --git a/moto/batch/models.py b/moto/batch/models.py
index 274737536..d54d55ab0 100644
--- a/moto/batch/models.py
+++ b/moto/batch/models.py
@@ -1344,7 +1344,10 @@ class BatchBackend(BaseBackend):
                     "Error executing request, Exception : Instance role is required."
                 )
             for profile in self.iam_backend.get_instance_profiles():
-                if profile.arn == cr["instanceRole"]:
+                if (
+                    profile.arn == cr["instanceRole"]
+                    or profile.name == cr["instanceRole"]
+                ):
                     break
             else:
                 raise InvalidParameterValueException(
@@ -1549,11 +1552,13 @@ class BatchBackend(BaseBackend):
                 for item in sorted(compute_env_order, key=lambda x: x["order"])
             ]
             env_objects = []
-            # Check each ARN exists, then make a list of compute env's
-            for arn in ordered_compute_environments:
-                env = self.get_compute_environment_by_arn(arn)
+            # Check each compute env exists, then make a list of them
+            for identifier in ordered_compute_environments:
+                env = self.get_compute_environment(identifier)
                 if env is None:
-                    raise ClientException(f"Compute environment {arn} does not exist")
+                    raise ClientException(
+                        f"Compute environment {identifier} does not exist"
+                    )
                 env_objects.append(env)
         except Exception:
             raise ClientException("computeEnvironmentOrder is malformed")
diff --git a/moto/cloudfront/models.py b/moto/cloudfront/models.py
index db90d8ff7..b1b01c8ff 100644
--- a/moto/cloudfront/models.py
+++ b/moto/cloudfront/models.py
@@ -121,9 +121,14 @@ class Logging:
 
 
 class ViewerCertificate:
-    def __init__(self) -> None:
-        self.cloud_front_default_certificate = True
-        self.min_protocol_version = "TLSv1"
+    def __init__(self, config: Dict[str, Any]) -> None:
+        self.cloud_front_default_certificate = (
+            config.get("CloudFrontDefaultCertificate") or True
+        )
+        self.iam_certificate_id = config.get("IAMCertificateId") or ""
+        self.acm_certificate_arn = config.get("ACMCertificateArn") or ""
+        self.ssl_support_method = config.get("SSLSupportMethod") or "sni-only"
+        self.min_protocol_version = config.get("MinimumProtocolVersion") or "TLSv1"
         self.certificate_source = "cloudfront"
 
 
@@ -195,7 +200,9 @@ class DistributionConfig:
         self.custom_error_responses: List[Any] = []
         self.logging = Logging(config.get("Logging") or {})
         self.enabled = config.get("Enabled") or False
-        self.viewer_certificate = ViewerCertificate()
+        self.viewer_certificate = ViewerCertificate(
+            config.get("ViewerCertificate") or {}
+        )
         self.geo_restriction = GeoRestrictions(config.get("Restrictions") or {})
         self.caller_reference = config.get("CallerReference", str(random.uuid4()))
         self.origins = config["Origins"]["Items"]["Origin"]
diff --git a/moto/cloudfront/responses.py b/moto/cloudfront/responses.py
index 9e6daf783..023d10186 100644
--- a/moto/cloudfront/responses.py
+++ b/moto/cloudfront/responses.py
@@ -552,7 +552,7 @@ DIST_CONFIG_TEMPLATE = """
         <CloudFrontDefaultCertificate>{{ 'true' if distribution.distribution_config.viewer_certificate.cloud_front_default_certificate else 'false' }}</CloudFrontDefaultCertificate>
         <IAMCertificateId>{{ distribution.distribution_config.viewer_certificate.iam_certificate_id }}</IAMCertificateId>
         <ACMCertificateArn>{{ distribution.distribution_config.viewer_certificate.acm_certificate_arn }}</ACMCertificateArn>
-        <SSLSupportMethod>{{ SSLSupportMethod }}</SSLSupportMethod>
+        <SSLSupportMethod>{{ distribution.distribution_config.viewer_certificate.ssl_support_method }}</SSLSupportMethod>
         <MinimumProtocolVersion>{{ distribution.distribution_config.viewer_certificate.min_protocol_version }}</MinimumProtocolVersion>
         <Certificate>{{ distribution.distribution_config.viewer_certificate.certificate }}</Certificate>
         <CertificateSource>{{ distribution.distribution_config.viewer_certificate.certificate_source }}</CertificateSource>
diff --git a/moto/lexv2models/responses.py b/moto/lexv2models/responses.py
index 3a5aa7ec6..0bd0608c5 100644
--- a/moto/lexv2models/responses.py
+++ b/moto/lexv2models/responses.py
@@ -178,7 +178,7 @@ class LexModelsV2Response(BaseResponse):
         )
 
     def create_resource_policy(self) -> str:
-        resource_arn = self._get_param("resourceArn")
+        resource_arn = unquote(self._get_param("resourceArn"))
         policy = self._get_param("policy")
         resource_arn, revision_id = self.lexv2models_backend.create_resource_policy(
             resource_arn=resource_arn,
@@ -187,7 +187,7 @@ class LexModelsV2Response(BaseResponse):
         return json.dumps(dict(resourceArn=resource_arn, revisionId=revision_id))
 
     def describe_resource_policy(self) -> str:
-        resource_arn = self._get_param("resourceArn")
+        resource_arn = unquote(self._get_param("resourceArn"))
         resource_arn, policy, revision_id = (
             self.lexv2models_backend.describe_resource_policy(
                 resource_arn=resource_arn,
@@ -198,7 +198,7 @@ class LexModelsV2Response(BaseResponse):
         )
 
     def update_resource_policy(self) -> str:
-        resource_arn = self._get_param("resourceArn")
+        resource_arn = unquote(self._get_param("resourceArn"))
         policy = self._get_param("policy")
         expected_revision_id = self._get_param("expectedRevisionId")
         resource_arn, revision_id = self.lexv2models_backend.update_resource_policy(
@@ -209,7 +209,7 @@ class LexModelsV2Response(BaseResponse):
         return json.dumps(dict(resourceArn=resource_arn, revisionId=revision_id))
 
     def delete_resource_policy(self) -> str:
-        resource_arn = self._get_param("resourceArn")
+        resource_arn = unquote(self._get_param("resourceArn"))
         expected_revision_id = self._get_param("expectedRevisionId")
         resource_arn, revision_id = self.lexv2models_backend.delete_resource_policy(
             resource_arn=resource_arn,
diff --git a/moto/lexv2models/urls.py b/moto/lexv2models/urls.py
index b44742bac..5050b3875 100644
--- a/moto/lexv2models/urls.py
+++ b/moto/lexv2models/urls.py
@@ -12,7 +12,7 @@ url_paths = {
     "{0}/bots/(?P<botId>[^/]+)/$": LexModelsV2Response.dispatch,
     "{0}/bots/(?P<botId>[^/]+)/botaliases/$": LexModelsV2Response.dispatch,
     "{0}/bots/(?P<botId>[^/]+)/botaliases/(?P<botAliasId>[^/]+)/$": LexModelsV2Response.dispatch,
-    "{0}/policy/(?P<resourceArn>[^/]+)/$": LexModelsV2Response.dispatch,
+    "{0}/policy/(?P<resourceArn>.+)/$": LexModelsV2Response.dispatch,
     "{0}/tags/(?P<resourceARN>[^/]+)$": LexModelsV2Response.dispatch,
     "{0}/tags/(?P<arn_prefix>[^/]+)/(?P<bot_id>[^/]+)$": LexModelsV2Response.dispatch,
 }
diff --git a/moto/resourcegroupstaggingapi/models.py b/moto/resourcegroupstaggingapi/models.py
index 71e3147ba..37f6f7cce 100644
--- a/moto/resourcegroupstaggingapi/models.py
+++ b/moto/resourcegroupstaggingapi/models.py
@@ -19,6 +19,10 @@ from moto.glacier.models import GlacierBackend, glacier_backends
 from moto.glue.models import GlueBackend, glue_backends
 from moto.kafka.models import KafkaBackend, kafka_backends
 from moto.kinesis.models import KinesisBackend, kinesis_backends
+from moto.kinesisanalyticsv2.models import (
+    KinesisAnalyticsV2Backend,
+    kinesisanalyticsv2_backends,
+)
 from moto.kms.models import KmsBackend, kms_backends
 from moto.lexv2models.models import LexModelsV2Backend, lexv2models_backends
 from moto.logs.models import LogsBackend, logs_backends
@@ -89,6 +93,10 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend):
     def kinesis_backend(self) -> KinesisBackend:
         return kinesis_backends[self.account_id][self.region_name]
 
+    @property
+    def kinesisanalyticsv2_backend(self) -> KinesisAnalyticsV2Backend:
+        return kinesisanalyticsv2_backends[self.account_id][self.region_name]
+
     @property
     def kms_backend(self) -> KmsBackend:
         return kms_backends[self.account_id][self.region_name]
@@ -510,6 +518,21 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend):
 
         # Kinesis
 
+        # KinesisAnalyticsV2
+        if self.kinesisanalyticsv2_backend and (
+            not resource_type_filters or "kinesisanalyticsv2" in resource_type_filters
+        ):
+            for application in self.kinesisanalyticsv2_backend.applications.values():
+                tags = self.kinesisanalyticsv2_backend.tagger.list_tags_for_resource(
+                    application.application_arn
+                )["Tags"]
+                if not tags or not tag_filter(tags):
+                    continue
+                yield {
+                    "ResourceARN": application.application_arn,
+                    "Tags": tags,
+                }
+
         # KMS
         if not resource_type_filters or "kms" in resource_type_filters:
             for kms_key in self.kms_backend.list_keys():
diff --git a/moto/ses/models.py b/moto/ses/models.py
index 9920f6283..566ecf951 100644
--- a/moto/ses/models.py
+++ b/moto/ses/models.py
@@ -11,6 +11,7 @@ from moto.core.base_backend import BackendDict, BaseBackend
 from moto.core.common_models import BaseModel
 from moto.core.utils import utcnow
 from moto.sns.models import sns_backends
+from moto.utilities.paginator import paginate
 
 from .exceptions import (
     AlreadyExists,
@@ -37,7 +38,7 @@ PAGINATION_MODEL = {
         "input_token": "next_token",
         "limit_key": "max_items",
         "limit_default": 100,
-        "unique_attribute": ["configuration_set_name"],
+        "unique_attribute": "configuration_set_name",
     },
 }
 
@@ -501,10 +502,12 @@ class SESBackend(BaseBackend):
     def delete_configuration_set(self, configuration_set_name: str) -> None:
         self.config_sets.pop(configuration_set_name)
 
-    def list_configuration_sets(
-        self, next_token: Optional[str], max_items: Optional[int]
-    ) -> List[str]:
-        return list(self.config_sets.keys())
+    @paginate(pagination_model=PAGINATION_MODEL)
+    def list_configuration_sets(self) -> List[ConfigurationSet]:
+        return list(self.config_sets.values())
+
+    def _list_all_configuration_sets(self) -> List[ConfigurationSet]:
+        return list(self.config_sets.values())
 
     def create_configuration_set_event_destination(
         self, configuration_set_name: str, event_destination: Dict[str, Any]
@@ -722,5 +725,20 @@ class SESBackend(BaseBackend):
 
         return attributes_by_identity
 
+    def update_configuration_set_reputation_metrics_enabled(
+        self, configuration_set_name: str, enabled: bool
+    ) -> None:
+        """
+        Enable or disable reputation metrics for a configuration set.
+        """
+        if configuration_set_name not in self.config_sets:
+            raise ConfigurationSetDoesNotExist(
+                f"Configuration set <{configuration_set_name}> does not exist"
+            )
+        config_set = self.config_sets[configuration_set_name]
+        if config_set.reputation_options is None:
+            config_set.reputation_options = {}
+        config_set.reputation_options["ReputationMetricsEnabled"] = enabled
+
 
 ses_backends = BackendDict(SESBackend, "ses")
diff --git a/moto/ses/responses.py b/moto/ses/responses.py
index 3499fcee1..38ddc04b6 100644
--- a/moto/ses/responses.py
+++ b/moto/ses/responses.py
@@ -392,15 +392,28 @@ class EmailResponse(BaseResponse):
         params = self._get_params()
         next_token = params.get("NextToken")
         max_items = params.get("MaxItems")
-        configuration_sets = self.backend.list_configuration_sets(
+        configuration_sets, next_token = self.backend.list_configuration_sets(
             next_token=next_token,
             max_items=max_items,
         )
+        config_set_names = [c.configuration_set_name for c in configuration_sets]
         template = self.response_template(LIST_CONFIGURATION_SETS_TEMPLATE)
         return template.render(
-            configuration_sets=configuration_sets, next_token=next_token
+            configuration_sets=config_set_names, next_token=next_token
         )
 
+    def update_configuration_set_reputation_metrics_enabled(self) -> str:
+        configuration_set_name = self._get_param("ConfigurationSetName")
+        enabled = self._get_param("Enabled")
+        self.backend.update_configuration_set_reputation_metrics_enabled(
+            configuration_set_name=configuration_set_name,
+            enabled=enabled,
+        )
+        template = self.response_template(
+            UPDATE_CONFIGURATION_SET_REPUTATION_METRICS_ENABLED_RESPONSE
+        )
+        return template.render()
+
 
 VERIFY_EMAIL_IDENTITY = """<VerifyEmailIdentityResponse xmlns="http://ses.amazonaws.com/doc/2010-12-01/">
   <VerifyEmailIdentityResult/>
@@ -860,3 +873,10 @@ LIST_CONFIGURATION_SETS_TEMPLATE = """<ListConfigurationSetsResponse xmlns="http
     <NextToken>{{ next_token }}</NextToken>
   </ListConfigurationSetsResult>
 </ListConfigurationSetsResponse>"""
+
+UPDATE_CONFIGURATION_SET_REPUTATION_METRICS_ENABLED_RESPONSE = """<UpdateConfigurationSetReputationMetricsEnabledResponse xmlns="http://ses.amazonaws.com/doc/2010-12-01/">
+  <UpdateConfigurationSetReputationMetricsEnabledResult/>
+  <ResponseMetadata>
+    <RequestId>47e0ef1a-9bf2-11e1-9279-0100e8cf109a</RequestId>
+  </ResponseMetadata>
+</UpdateConfigurationSetReputationMetricsEnabledResponse>"""
diff --git a/moto/sesv2/models.py b/moto/sesv2/models.py
index 4925fea61..20e480581 100644
--- a/moto/sesv2/models.py
+++ b/moto/sesv2/models.py
@@ -23,6 +23,12 @@ PAGINATION_MODEL = {
         "limit_default": 100,
         "unique_attribute": "IdentityName",
     },
+    "list_configuration_sets": {
+        "input_token": "next_token",
+        "limit_key": "page_size",
+        "limit_default": 100,
+        "unique_attribute": "configuration_set_name",
+    },
 }
 
 
@@ -309,10 +315,9 @@ class SESV2Backend(BaseBackend):
         )
         return config_set
 
-    def list_configuration_sets(self, next_token: str, page_size: int) -> List[str]:
-        return self.v1_backend.list_configuration_sets(
-            next_token=next_token, max_items=page_size
-        )
+    @paginate(pagination_model=PAGINATION_MODEL)
+    def list_configuration_sets(self) -> List[ConfigurationSet]:
+        return self.v1_backend._list_all_configuration_sets()
 
     def create_dedicated_ip_pool(
         self, pool_name: str, tags: List[Dict[str, str]], scaling_mode: str
diff --git a/moto/sesv2/responses.py b/moto/sesv2/responses.py
index 9a3e91c0e..4a1d2ec0f 100644
--- a/moto/sesv2/responses.py
+++ b/moto/sesv2/responses.py
@@ -183,11 +183,13 @@ class SESV2Response(BaseResponse):
     def list_configuration_sets(self) -> str:
         next_token = self._get_param("NextToken")
         page_size = self._get_param("PageSize")
-        configuration_sets = self.sesv2_backend.list_configuration_sets(
+        configuration_sets, next_token = self.sesv2_backend.list_configuration_sets(
             next_token=next_token, page_size=page_size
         )
+        config_set_names = [c.configuration_set_name for c in configuration_sets]
+
         return json.dumps(
-            dict(ConfigurationSets=configuration_sets, NextToken=next_token)
+            dict(ConfigurationSets=config_set_names, NextToken=next_token)
         )
 
     def create_dedicated_ip_pool(self) -> str:
diff --git a/moto/textract/models.py b/moto/textract/models.py
index 11078421d..86e66307b 100644
--- a/moto/textract/models.py
+++ b/moto/textract/models.py
@@ -1,5 +1,5 @@
 from collections import defaultdict
-from typing import Any, Dict, List
+from typing import Any, Dict, List, Optional
 
 from moto.core.base_backend import BackendDict, BaseBackend
 from moto.core.common_models import BaseModel
@@ -33,6 +33,7 @@ class TextractBackend(BaseBackend):
     def __init__(self, region_name: str, account_id: str):
         super().__init__(region_name, account_id)
         self.async_text_detection_jobs: Dict[str, TextractJob] = defaultdict()
+        self.async_document_analysis_jobs: Dict[str, TextractJob] = defaultdict()
 
     def get_document_text_detection(self, job_id: str) -> TextractJob:
         """
@@ -67,5 +68,32 @@ class TextractBackend(BaseBackend):
         )
         return job_id
 
+    def start_document_analysis(
+        self, document_location: dict[str, Any], feature_types: List[str]
+    ) -> str:
+        """
+        The following parameters have not yet been implemented: ClientRequestToken, JobTag, NotificationChannel, OutputConfig, KmsKeyID
+        """
+        if not document_location or not feature_types:
+            raise InvalidParameterException()
+        job_id = str(mock_random.uuid4())
+        self.async_document_analysis_jobs[job_id] = TextractJob(
+            {
+                "Blocks": TextractBackend.BLOCKS,
+                "DetectDocumentTextModelVersion": "1.0",
+                "DocumentMetadata": {"Pages": TextractBackend.PAGES},
+                "JobStatus": TextractBackend.JOB_STATUS,
+            }
+        )
+        return job_id
+
+    def get_document_analysis(
+        self, job_id: str, max_results: Optional[int], next_token: Optional[str] = None
+    ) -> TextractJob:
+        job = self.async_document_analysis_jobs.get(job_id)
+        if not job:
+            raise InvalidJobIdException()
+        return job
+
 
 textract_backends = BackendDict(TextractBackend, "textract")
diff --git a/moto/textract/responses.py b/moto/textract/responses.py
index adc95b0ff..5aeaeed0f 100644
--- a/moto/textract/responses.py
+++ b/moto/textract/responses.py
@@ -35,3 +35,22 @@ class TextractResponse(BaseResponse):
             document_location=document_location
         )
         return json.dumps(dict(JobId=job_id))
+
+    def start_document_analysis(self) -> str:
+        params = json.loads(self.body)
+        document_location = params.get("DocumentLocation")
+        feature_types = params.get("FeatureTypes")
+        job_id = self.textract_backend.start_document_analysis(
+            document_location=document_location, feature_types=feature_types
+        )
+        return json.dumps(dict(JobId=job_id))
+
+    def get_document_analysis(self) -> str:
+        params = json.loads(self.body)
+        job_id = params.get("JobId")
+        max_results = params.get("MaxResults")
+        next_token = params.get("NextToken")
+        job = self.textract_backend.get_document_analysis(
+            job_id=job_id, max_results=max_results, next_token=next_token
+        ).to_dict()
+        return json.dumps(job)


Test patch:
diff --git a/tests/test_batch/test_batch_compute_envs.py b/tests/test_batch/test_batch_compute_envs.py
index 43129da92..37b6f43b9 100644
--- a/tests/test_batch/test_batch_compute_envs.py
+++ b/tests/test_batch/test_batch_compute_envs.py
@@ -9,10 +9,16 @@ from . import _get_clients, _setup
 
 
 @mock_aws
-def test_create_managed_compute_environment():
+@pytest.mark.parametrize("use_arn_for_instance_role", [True, False])
+def test_create_managed_compute_environment(use_arn_for_instance_role):
     ec2_client, iam_client, ecs_client, _, batch_client = _get_clients()
     _, subnet_id, sg_id, iam_arn = _setup(ec2_client, iam_client)
 
+    instance_role = (
+        iam_arn.replace("role", "instance-profile")
+        if use_arn_for_instance_role
+        else iam_arn.split("/")[-1]
+    )
     compute_name = str(uuid4())
     resp = batch_client.create_compute_environment(
         computeEnvironmentName=compute_name,
@@ -28,7 +34,7 @@ def test_create_managed_compute_environment():
             "subnets": [subnet_id],
             "securityGroupIds": [sg_id],
             "ec2KeyPair": "string",
-            "instanceRole": iam_arn.replace("role", "instance-profile"),
+            "instanceRole": instance_role,
             "tags": {"string": "string"},
             "bidPercentage": 123,
             "spotIamFleetRole": "string",
diff --git a/tests/test_batch/test_batch_tags_job_queue.py b/tests/test_batch/test_batch_tags_job_queue.py
index c3d624f8b..4fccacf82 100644
--- a/tests/test_batch/test_batch_tags_job_queue.py
+++ b/tests/test_batch/test_batch_tags_job_queue.py
@@ -1,12 +1,15 @@
 from uuid import uuid4
 
+import pytest
+
 from moto import mock_aws
 
 from . import _get_clients, _setup
 
 
 @mock_aws
-def test_create_job_queue_with_tags():
+@pytest.mark.parametrize("use_compute_env_arn", [True, False])
+def test_create_job_queue_with_tags(use_compute_env_arn):
     ec2_client, iam_client, _, _, batch_client = _get_clients()
     _, _, _, iam_arn = _setup(ec2_client, iam_client)
 
@@ -17,14 +20,18 @@ def test_create_job_queue_with_tags():
         state="ENABLED",
         serviceRole=iam_arn,
     )
-    arn = resp["computeEnvironmentArn"]
+    compute_env_identifier = (
+        resp["computeEnvironmentArn"] if use_compute_env_arn else compute_name
+    )
 
     jq_name = str(uuid4())[0:6]
     resp = batch_client.create_job_queue(
         jobQueueName=jq_name,
         state="ENABLED",
         priority=123,
-        computeEnvironmentOrder=[{"order": 123, "computeEnvironment": arn}],
+        computeEnvironmentOrder=[
+            {"order": 123, "computeEnvironment": compute_env_identifier}
+        ],
         tags={"k1": "v1", "k2": "v2"},
     )
     assert "jobQueueArn" in resp
diff --git a/tests/test_cloudfront/test_cloudfront.py b/tests/test_cloudfront/test_cloudfront.py
index 9475c9886..0d352903c 100644
--- a/tests/test_cloudfront/test_cloudfront.py
+++ b/tests/test_cloudfront/test_cloudfront.py
@@ -116,6 +116,9 @@ def test_update_distribution():
     assert cert["CloudFrontDefaultCertificate"] is True
     assert cert["MinimumProtocolVersion"] == "TLSv1"
     assert cert["CertificateSource"] == "cloudfront"
+    assert cert["IAMCertificateId"] == ""
+    assert cert["ACMCertificateArn"] == ""
+    assert cert["SSLSupportMethod"] == "sni-only"
 
     restriction = config["Restrictions"]["GeoRestriction"]
     assert restriction["RestrictionType"] == "none"
diff --git a/tests/test_cloudfront/test_cloudfront_distributions.py b/tests/test_cloudfront/test_cloudfront_distributions.py
index 1fa488207..2c2fb85b0 100644
--- a/tests/test_cloudfront/test_cloudfront_distributions.py
+++ b/tests/test_cloudfront/test_cloudfront_distributions.py
@@ -115,7 +115,7 @@ def test_create_distribution_s3_minimum(region, partition):
         "CloudFrontDefaultCertificate": True,
         "IAMCertificateId": "",
         "MinimumProtocolVersion": "TLSv1",
-        "SSLSupportMethod": "",
+        "SSLSupportMethod": "sni-only",
     }
 
     restriction = config["Restrictions"]["GeoRestriction"]
diff --git a/tests/test_kinesisanalyticsv2/test_kinesisanalyticsv2.py b/tests/test_kinesisanalyticsv2/test_kinesisanalyticsv2.py
index eb79c9e7a..421a99d8c 100644
--- a/tests/test_kinesisanalyticsv2/test_kinesisanalyticsv2.py
+++ b/tests/test_kinesisanalyticsv2/test_kinesisanalyticsv2.py
@@ -381,3 +381,24 @@ def test_list_applications():
     assert app_summaries[0].get("ApplicationStatus") == "STARTING"
     assert app_summaries[0].get("ApplicationVersionId") == 1
     assert app_summaries[0].get("RuntimeEnvironment") == "FLINK-1_20"
+
+
+@mock_aws
+def test_resource_groups_tagging_api():
+    region = "us-east-2"
+    client = boto3.client("kinesisanalyticsv2", region_name=region)
+    rtapi_client = boto3.client("resourcegroupstaggingapi", region_name=region)
+
+    app_resp = client.create_application(
+        ApplicationName="test_application",
+        RuntimeEnvironment="FLINK-1_20",
+        ServiceExecutionRole=f"arn:aws:iam::{ACCOUNT_ID}:role/application_role",
+        Tags=FAKE_TAGS,
+    )
+    app = app_resp.get("ApplicationDetail")
+    app_arn = app.get("ApplicationARN")
+
+    resp = rtapi_client.get_resources(ResourceTypeFilters=["kinesisanalyticsv2"])
+    assert len(resp["ResourceTagMappingList"]) == 1
+    assert resp["ResourceTagMappingList"][0]["ResourceARN"] == app_arn
+    assert resp["ResourceTagMappingList"][0]["Tags"] == FAKE_TAGS
diff --git a/tests/test_lexv2models/test_lexv2models.py b/tests/test_lexv2models/test_lexv2models.py
index 2359429af..8c0a7fa21 100644
--- a/tests/test_lexv2models/test_lexv2models.py
+++ b/tests/test_lexv2models/test_lexv2models.py
@@ -346,39 +346,41 @@ def test_bot_alias():
 @mock_aws
 def test_resource_policy():
     client = boto3.client("lexv2-models", region_name="eu-west-1")
+
+    arn = "arn:aws:lex:eu-west-1:123456789012:bot/MyLexBot"
+
     resp = client.create_resource_policy(
-        resourceArn="test_resource_arn",
+        resourceArn=arn,
         policy="test_resource_policy",
     )
-    assert resp["resourceArn"] == "test_resource_arn"
+
+    assert resp["resourceArn"] == arn
     assert resp.get("revisionId")
 
-    desc_resp = client.describe_resource_policy(
-        resourceArn="test_resource_arn",
-    )
-    assert desc_resp["resourceArn"] == "test_resource_arn"
+    desc_resp = client.describe_resource_policy(resourceArn=arn)
+    assert desc_resp["resourceArn"] == arn
     assert desc_resp["policy"] == "test_resource_policy"
     assert desc_resp.get("revisionId")
 
     update_resp = client.update_resource_policy(
-        resourceArn="test_resource_arn",
+        resourceArn=arn,
         policy="test_resource_policy_updated",
         expectedRevisionId=resp["revisionId"],
     )
 
-    assert update_resp["resourceArn"] == "test_resource_arn"
+    assert update_resp["resourceArn"] == arn
     assert update_resp["revisionId"] != resp["revisionId"]
 
     delete_resp = client.delete_resource_policy(
-        resourceArn="test_resource_arn",
+        resourceArn=arn,
         expectedRevisionId=update_resp["revisionId"],
     )
-    assert delete_resp["resourceArn"] == "test_resource_arn"
+    assert delete_resp["resourceArn"] == arn
 
 
 @mock_aws
 def test_tag_resource():
-    sts = boto3.client("sts")
+    sts = boto3.client("sts", "eu-west-1")
     account_id = sts.get_caller_identity()["Account"]
     region_name = "eu-west-1"
 
diff --git a/tests/test_ses/test_ses_boto3.py b/tests/test_ses/test_ses_boto3.py
index c5426958d..f35075da5 100644
--- a/tests/test_ses/test_ses_boto3.py
+++ b/tests/test_ses/test_ses_boto3.py
@@ -1553,3 +1553,24 @@ def test_get_identity_verification_attributes():
         attributes["VerificationAttributes"]["foo@bar.com"]["VerificationStatus"]
         == "Success"
     )
+
+
+@mock_aws
+def test_update_configuration_set_reputation_metrics():
+    conn = boto3.client("ses", region_name="us-east-1")
+
+    # Create a configuration set first
+    conn.create_configuration_set(ConfigurationSet={"Name": "test-config-set"})
+
+    # Enable reputation metrics
+    response = conn.update_configuration_set_reputation_metrics_enabled(
+        ConfigurationSetName="test-config-set", Enabled=True
+    )
+    assert response["ResponseMetadata"]["HTTPStatusCode"] == 200
+
+    # Test with non-existent configuration set
+    with pytest.raises(ClientError) as e:
+        conn.update_configuration_set_reputation_metrics_enabled(
+            ConfigurationSetName="non-existent", Enabled=True
+        )
+    assert e.value.response["Error"]["Code"] == "ConfigurationSetDoesNotExist"
diff --git a/tests/test_sesv2/test_sesv2.py b/tests/test_sesv2/test_sesv2.py
index c4fef0c53..7428023d0 100644
--- a/tests/test_sesv2/test_sesv2.py
+++ b/tests/test_sesv2/test_sesv2.py
@@ -713,7 +713,7 @@ def test_list_configuration_sets():
     # Validate
     assert len(config_sets["ConfigurationSets"]) == 2
     for c in config_sets["ConfigurationSets"]:
-        assert c in {name_v1, name_v2}
+        assert c in [name_v1, name_v2]
 
 
 @mock_aws
diff --git a/tests/test_textract/test_server.py b/tests/test_textract/test_server.py
index 0bf2ffc25..09c9224c0 100644
--- a/tests/test_textract/test_server.py
+++ b/tests/test_textract/test_server.py
@@ -93,3 +93,48 @@ def test_textract_get_text_detection_without_job_id():
     data = json.loads(resp.data.decode("utf-8"))
     assert data["__type"] == "InvalidJobIdException"
     assert data["message"] == "An invalid job identifier was passed."
+
+
+@mock_aws
+def test_textract_start_document_analysis():
+    backend = server.create_backend_app("textract")
+    test_client = backend.test_client()
+
+    headers = {"X-Amz-Target": "X-Amz-Target=Textract.StartDocumentAnalysis"}
+    request_body = {
+        "DocumentLocation": {
+            "S3Object": {"Bucket": "bucket", "Name": "name", "Version": "version"}
+        },
+        "FeatureTypes": ["TABLES", "FORMS"],
+    }
+
+    resp = test_client.post("/", headers=headers, json=request_body)
+    data = json.loads(resp.data.decode("utf-8"))
+    assert resp.status_code == 200
+    assert isinstance(data["JobId"], str)
+
+
+@mock_aws
+def test_textract_get_document_analysis():
+    backend = server.create_backend_app("textract")
+    test_client = backend.test_client()
+
+    headers = {"X-Amz-Target": "X-Amz-Target=Textract.StartDocumentAnalysis"}
+    request_body = {
+        "DocumentLocation": {
+            "S3Object": {"Bucket": "bucket", "Name": "name", "Version": "version"}
+        },
+        "FeatureTypes": ["TABLES", "FORMS"],
+    }
+
+    resp = test_client.post("/", headers=headers, json=request_body)
+    start_job_data = json.loads(resp.data.decode("utf-8"))
+
+    headers = {"X-Amz-Target": "X-Amz-Target=Textract.GetDocumentAnalysis"}
+    request_body = {
+        "JobId": start_job_data["JobId"],
+    }
+    resp = test_client.post("/", headers=headers, json=request_body)
+    assert resp.status_code == 200
+    data = json.loads(resp.data.decode("utf-8"))
+    assert data["JobStatus"] == "SUCCEEDED"
diff --git a/tests/test_textract/test_textract.py b/tests/test_textract/test_textract.py
index 7306a3a48..023b66bb7 100644
--- a/tests/test_textract/test_textract.py
+++ b/tests/test_textract/test_textract.py
@@ -85,3 +85,62 @@ def test_detect_document_text():
     )
     assert isinstance(result["Blocks"], list)
     assert result["DetectDocumentTextModelVersion"] == "1.0"
+
+
+@mock_aws
+def test_get_document_analysis():
+    if settings.TEST_SERVER_MODE:
+        raise SkipTest("Cannot set textract backend values in server mode")
+
+    TextractBackend.JOB_STATUS = "SUCCEEDED"
+    TextractBackend.PAGES = randint(5, 500)
+    TextractBackend.BLOCKS = [
+        {
+            "Text": "This is a test",
+            "Id": "0",
+            "Confidence": "100",
+            "Geometry": {
+                "BoundingBox": {
+                    "Width": "0.5",
+                    "Height": "0.5",
+                    "Left": "0.5",
+                    "Top": "0.5",
+                },
+            },
+        }
+    ]
+
+    client = boto3.client("textract", region_name="us-east-1")
+    job = client.start_document_analysis(
+        DocumentLocation={"S3Object": {"Bucket": "bucket", "Name": "name"}},
+        FeatureTypes=["TABLES", "FORMS"],
+    )
+
+    resp = client.get_document_analysis(JobId=job["JobId"], MaxResults=1000)
+
+    assert resp["Blocks"][0]["Text"] == "This is a test"
+    assert resp["Blocks"][0]["Id"] == "0"
+    assert resp["Blocks"][0]["Confidence"] == "100"
+    assert resp["Blocks"][0]["Geometry"]["BoundingBox"]["Width"] == "0.5"
+    assert resp["Blocks"][0]["Geometry"]["BoundingBox"]["Height"] == "0.5"
+    assert resp["Blocks"][0]["Geometry"]["BoundingBox"]["Left"] == "0.5"
+    assert resp["Blocks"][0]["Geometry"]["BoundingBox"]["Top"] == "0.5"
+    assert resp["JobStatus"] == "SUCCEEDED"
+    assert resp["DocumentMetadata"]["Pages"] == TextractBackend.PAGES
+
+
+@mock_aws
+def test_start_document_analysis():
+    client = boto3.client("textract", region_name="us-east-1")
+    resp = client.start_document_analysis(
+        DocumentLocation={"S3Object": {"Bucket": "bucket", "Name": "name"}},
+        FeatureTypes=["TABLES", "FORMS"],
+        ClientRequestToken="unique-token",
+        JobTag="test-job",
+        NotificationChannel={
+            "SNSTopicArn": "arn:aws:sns:us-east-1:123456789012:AmazonTextractTopic",
+            "RoleArn": "arn:aws:iam::123456789012:role/TextractRole",
+        },
+    )
+
+    assert "JobId" in resp