Repository: getmoto/moto (python)

Issue:
[Lex] DescribeResourcePolicy resourceArn malformed
When calling LexModelsV2Response describe_resource_policy the clients resourceArn param becomes encoded somewhere inside core. All colons are replaces with "%3A" which works for the URL string but it also converts the actual resourceArn in the response. This seems to only have an effect on actual AWS ARN shapes which is why the test_lexv2models.py test didn't catch it.

I can add an `unquote` to the responses.py to correct the Arn but wanted to raise the issue first, in case it has been seen elsewhere before.


Tested in Boto and Moto. Requests seem to be okay/similar.

Moto API Call Below from client.py

OperationModel(name=DescribeResourcePolicy)
**request_dict**
{'url_path': '/policy/arn%3Aaws%3Alex%3Aus-east-1%3A123456789012%3Abot%2F4edca17c-c6f7-4fde-a879-6538653f8a36/', 'query_string': {}, 'method': 'GET', 'headers': {'User-Agent': 'Boto3/1.37.31 md/Botocore#1.37.31 ua/2.1 os/linux#5.15.167.4-microsoft-standard-WSL2 md/arch#x86_64 lang/python#3.12.3 md/pyimpl#CPython cfg/retry-mode#legacy Botocore/1.37.31'}, 'body': b'', 'url': 'https://models-v2-lex.us-east-1.amazonaws.com/policy/arn%3Aaws%3Alex%3Aus-east-1%3A123456789012%3Abot%2F4edca17c-c6f7-4fde-a879-6538653f8a36/', 'context': {'client_region': 'us-east-1', 'client_config': <botocore.config.Config object at 0x7f00cae32bd0>, 'has_streaming_input': False, 'auth_type': None, 'unsigned_payload': None}}

**parsed_response**
{'ResponseMetadata': {'RequestId': 'crWrO9mCaGSwvBEZgX3DkCvKcRyDsOsin4qvm9rnEUkfQfbpeCL7', 'HTTPStatusCode': 200, 'HTTPHeaders': {'server': 'amazon.com', 'date': 'Sat, 12 Apr 2025 19:41:31 GMT', 'x-amzn-requestid': 'crWrO9mCaGSwvBEZgX3DkCvKcRyDsOsin4qvm9rnEUkfQfbpeCL7'}, 'RetryAttempts': 0}, **'resourceArn': 'arn%3Aaws%3Alex%3Aus-east-1%3A123456789012%3Abot%2F4edca17c-c6f7-4fde-a879-6538653f8a36',** 'policy': '{"Version": "2012-10-17", "Statement": [{"Effect": "Allow", "Principal": {"AWS": "arn:aws:iam::123456789012:root"}, "Action": "lex:*", "Resource": "arn:aws:lex:us-east-1:123456789012:bot/4edca17c-c6f7-4fde-a879-6538653f8a36"}]}', 'revisionId': '87be0f4f-0fa4-400a-8ca7-f7c09006861e'}


[lexv2_RP_testing.txt](https://github.com/user-attachments/files/19726088/lexv2_RP_testing.txt)

Pull request description:
[Lex] resourceArn fix for resource policies
Fixes #8803 

The returned resourceArn param comes encoded, so adding unquote calls to get the parameter format correctly.

Linted, formatted, and tested.

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/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..69fd3a3b8 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]
diff --git a/moto/ses/responses.py b/moto/ses/responses.py
index 3499fcee1..1b6cf00d7 100644
--- a/moto/ses/responses.py
+++ b/moto/ses/responses.py
@@ -392,13 +392,14 @@ 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
         )
 
 
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_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_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