Repository: getmoto/moto
Pull request #8822 (2025-04-17 06:39:41)

add support for Textract SNS integration
Relates to https://github.com/localstack/localstack/issues/12372 


Patch:
diff --git a/moto/config/resources/aws_managed_rules.json b/moto/config/resources/aws_managed_rules.json
index 46b2fcf7d..4afd4eef3 100644
--- a/moto/config/resources/aws_managed_rules.json
+++ b/moto/config/resources/aws_managed_rules.json
@@ -997,12 +997,6 @@
       "Resource Types": "AWS::ElasticBeanstalk::Environment",
       "Trigger type": "Configuration changes"
     },
-    "BEANSTALK_LOGS_TO_CLOUDWATCH": {
-      "AWS Region": "All supported AWS regions except China (Beijing), Asia Pacific (Thailand), Asia Pacific (Jakarta), Middle East (UAE), Asia Pacific (Hyderabad), Asia Pacific (Osaka), Asia Pacific (Malaysia), Asia Pacific (Melbourne), AWS GovCloud (US-East), AWS GovCloud (US-West), Mexico (Central), Israel (Tel Aviv), Canada West (Calgary), Europe (Spain), China (Ningxia), Europe (Zurich) Region",
-      "Parameters": [],
-      "Resource Types": "AWS::ElasticBeanstalk::Environment",
-      "Trigger type": "Configuration changes"
-    },
     "CASSANDRA_KEYSPACE_TAGGED": {
       "AWS Region": "Only available in Europe (Stockholm), Middle East (Bahrain), Asia Pacific (Mumbai), Europe (Paris), US East (Ohio), Europe (Ireland), Europe (Frankfurt), South America (Sao Paulo), Asia Pacific (Hong Kong), US East (N. Virginia), Asia Pacific (Seoul), Europe (London), Asia Pacific (Tokyo), US West (Oregon), US West (N. California), Asia Pacific (Singapore), Asia Pacific (Sydney), Canada (Central) Region",
       "Parameters": [
@@ -1556,6 +1550,18 @@
       "Resource Types": "AWS::Cognito::UserPool",
       "Trigger type": "Configuration changes"
     },
+    "COGNITO_USER_POOL_TAGGED": {
+      "AWS Region": "All supported AWS regions except China (Beijing), Asia Pacific (Thailand), Asia Pacific (Malaysia), AWS GovCloud (US-East), AWS GovCloud (US-West), Mexico (Central), China (Ningxia) Region",
+      "Parameters": [
+        {
+          "Name": "requiredKeyTags",
+          "Optional": true,
+          "Type": "CSV"
+        }
+      ],
+      "Resource Types": "AWS::Cognito::UserPool",
+      "Trigger type": "Configuration changes"
+    },
     "CONNECT_INSTANCE_LOGGING_ENABLED": {
       "AWS Region": "Only available in Africa (Cape Town), Europe (Frankfurt), US East (N. Virginia), Asia Pacific (Seoul), Europe (London), Asia Pacific (Tokyo), US West (Oregon), Asia Pacific (Singapore), Asia Pacific (Sydney), Canada (Central) Region",
       "Parameters": [],
@@ -1822,7 +1828,13 @@
     },
     "DOCDB_CLUSTER_ENCRYPTED_IN_TRANSIT": {
       "AWS Region": "All supported AWS regions except Europe (Stockholm), Middle East (Bahrain), Asia Pacific (Thailand), Asia Pacific (Jakarta), Africa (Cape Town), Asia Pacific (Osaka), Asia Pacific (Malaysia), Asia Pacific (Melbourne), Mexico (Central), US West (N. California), Israel (Tel Aviv), Canada West (Calgary), Europe (Spain), Europe (Zurich) Region",
-      "Parameters": [],
+      "Parameters": [
+        {
+          "Name": "excludeTlsParameters",
+          "Optional": true,
+          "Type": "CSV"
+        }
+      ],
       "Resource Types": "AWS::RDS::DBCluster",
       "Trigger type": "Periodic"
     },
@@ -2757,9 +2769,15 @@
       "Resource Types": "AWS::ECR::Repository",
       "Trigger type": "Configuration changes"
     },
-    "ECR_REPOSITORY_LIFECYCLE_POLICY_CONFIGURED": {
-      "AWS Region": "All supported AWS regions except Asia Pacific (Thailand), Asia Pacific (Hyderabad), Asia Pacific (Malaysia), Asia Pacific (Melbourne), AWS GovCloud (US-East), AWS GovCloud (US-West), Mexico (Central), Israel (Tel Aviv), Canada West (Calgary), Europe (Spain), Europe (Zurich) Region",
-      "Parameters": [],
+    "ECR_REPOSITORY_TAGGED": {
+      "AWS Region": "All supported AWS regions except China (Beijing), Asia Pacific (Thailand), Asia Pacific (Malaysia), AWS GovCloud (US-East), AWS GovCloud (US-West), Mexico (Central), China (Ningxia) Region",
+      "Parameters": [
+        {
+          "Name": "requiredKeyTags",
+          "Optional": true,
+          "Type": "CSV"
+        }
+      ],
       "Resource Types": "AWS::ECR::Repository",
       "Trigger type": "Configuration changes"
     },
@@ -3261,12 +3279,6 @@
       "Resource Types": "AWS::Elasticsearch::Domain",
       "Trigger type": "Configuration changes"
     },
-    "ELASTICSEARCH_UPDATE_CHECK": {
-      "AWS Region": "All supported AWS regions except China (Beijing), Asia Pacific (Thailand), Africa (Cape Town), Middle East (UAE), Asia Pacific (Hyderabad), Asia Pacific (Osaka), Asia Pacific (Malaysia), Asia Pacific (Melbourne), Europe (Milan), AWS GovCloud (US-East), AWS GovCloud (US-West), Mexico (Central), Israel (Tel Aviv), Canada West (Calgary), Europe (Spain), China (Ningxia), Europe (Zurich) Region",
-      "Parameters": [],
-      "Resource Types": "AWS::Elasticsearch::Domain",
-      "Trigger type": "Configuration changes"
-    },
     "ELASTIC_BEANSTALK_LOGS_TO_CLOUDWATCH": {
       "AWS Region": "All supported AWS regions except China (Beijing), Asia Pacific (Thailand), Middle East (UAE), Asia Pacific (Hyderabad), Asia Pacific (Malaysia), Asia Pacific (Melbourne), AWS GovCloud (US-East), AWS GovCloud (US-West), Mexico (Central), Israel (Tel Aviv), Canada West (Calgary), Europe (Spain), China (Ningxia), Europe (Zurich) Region",
       "Parameters": [
@@ -3544,85 +3556,6 @@
       "Resource Types": "AWS::FIS::ExperimentTemplate",
       "Trigger type": "Configuration changes"
     },
-    "FMS_NETWORK_FIREWALL_RESOURCE_CHECK": {
-      "AWS Region": "All supported AWS regions",
-      "Parameters": [
-        {
-          "Name": "policyId",
-          "Optional": false,
-          "Type": "String"
-        },
-        {
-          "Name": "policyVersion",
-          "Optional": false,
-          "Type": "String"
-        },
-        {
-          "Name": "resourceTags",
-          "Optional": true,
-          "Type": "String"
-        },
-        {
-          "Name": "excludeResourceTags",
-          "Optional": true,
-          "Type": "boolean"
-        },
-        {
-          "Name": "resourceTagLogicalOperator",
-          "Optional": true,
-          "Type": "String"
-        }
-      ],
-      "Resource Types": "AWS::NetworkFirewall::Firewall, AWS::NetworkFirewall::FirewallPolicy, AWS::NetworkFirewall::RuleGroup, AWS::EC2::RouteTable, AWS::EC2::Subnet, AWS::EC2::InternetGateway, AWS::EC2::VPC, AWS::EC2::VPCEndpoint, AWS::EC2::NetworkAcl",
-      "Trigger type": "Configuration changes"
-    },
-    "FMS_SECURITY_GROUP_AUDIT_POLICY_CHECK": {
-      "AWS Region": "All supported AWS regions",
-      "Parameters": [
-        {
-          "Name": "masterSecurityGroupsIds",
-          "Optional": false,
-          "Type": "String"
-        },
-        {
-          "Name": "inScope",
-          "Optional": false,
-          "Type": "String"
-        },
-        {
-          "Name": "resourceTags",
-          "Optional": false,
-          "Type": "String"
-        },
-        {
-          "Name": "excludeResourceTags",
-          "Optional": false,
-          "Type": "boolean"
-        },
-        {
-          "Name": "resourceTypes",
-          "Optional": false,
-          "Type": "String"
-        },
-        {
-          "Name": "fmsRemediationEnabled",
-          "Optional": false,
-          "Type": "boolean"
-        },
-        {
-          "Name": "allowSecurityGroup",
-          "Optional": false,
-          "Type": "boolean"
-        },
-        {
-          "Name": "resourceTagLogicalOperator",
-          "Optional": true,
-          "Type": "String"
-        }
-      ],
-      "Resource Types": "AWS::EC2::SecurityGroup, AWS::EC2::Instance, AWS::EC2::NetworkInterface",
-      "Trigger type": "Configuration changes"
-    },
     "FMS_SHIELD_RESOURCE_POLICY_CHECK": {
       "AWS Region": "All supported AWS regions except China (Beijing), Asia Pacific (Thailand), Asia Pacific (Jakarta), Asia Pacific (Malaysia), Mexico (Central), Canada West (Calgary), China (Ningxia) Region",
       "Parameters": [
@@ -5635,6 +5568,18 @@
       "Resource Types": "AWS::Redshift::Cluster, AWS::Redshift::ClusterParameterGroup",
       "Trigger type": "Configuration changes"
     },
+    "REDSHIFT_SERVERLESS_DEFAULT_ADMIN_CHECK": {
+      "AWS Region": "All supported AWS regions except Middle East (Bahrain), Asia Pacific (Thailand), Africa (Cape Town), Asia Pacific (Hyderabad), Asia Pacific (Osaka), Asia Pacific (Malaysia), Asia Pacific (Melbourne), Europe (Milan), Mexico (Central), Canada West (Calgary) Region",
+      "Parameters": [],
+      "Resource Types": "AWS::RedshiftServerless::Namespace",
+      "Trigger type": "Periodic"
+    },
+    "REDSHIFT_SERVERLESS_DEFAULT_DB_NAME_CHECK": {
+      "AWS Region": "All supported AWS regions except Middle East (Bahrain), Asia Pacific (Thailand), Africa (Cape Town), Asia Pacific (Hyderabad), Asia Pacific (Osaka), Asia Pacific (Malaysia), Asia Pacific (Melbourne), Europe (Milan), Mexico (Central), Canada West (Calgary) Region",
+      "Parameters": [],
+      "Resource Types": "AWS::RedshiftServerless::Namespace",
+      "Trigger type": "Periodic"
+    },
     "REDSHIFT_SERVERLESS_NAMESPACE_CMK_ENCRYPTION": {
       "AWS Region": "All supported AWS regions except Middle East (Bahrain), Asia Pacific (Thailand), Africa (Cape Town), Asia Pacific (Hyderabad), Asia Pacific (Osaka), Asia Pacific (Malaysia), Asia Pacific (Melbourne), Europe (Milan), Mexico (Central), Canada West (Calgary) Region",
       "Parameters": [
@@ -6036,6 +5981,18 @@
       "Resource Types": "AWS::S3::Bucket",
       "Trigger type": "Configuration changes"
     },
+    "S3_BUCKET_TAGGED": {
+      "AWS Region": "All supported AWS regions except China (Beijing), Asia Pacific (Thailand), Asia Pacific (Malaysia), AWS GovCloud (US-East), AWS GovCloud (US-West), Mexico (Central), China (Ningxia) Region",
+      "Parameters": [
+        {
+          "Name": "requiredKeyTags",
+          "Optional": true,
+          "Type": "CSV"
+        }
+      ],
+      "Resource Types": "AWS::S3::Bucket",
+      "Trigger type": "Configuration changes"
+    },
     "S3_BUCKET_VERSIONING_ENABLED": {
       "AWS Region": "All supported AWS regions",
       "Parameters": [
@@ -6261,6 +6218,18 @@
       "Resource Types": "AWS::SageMaker::Domain",
       "Trigger type": "Configuration changes"
     },
+    "SAGEMAKER_DOMAIN_TAGGED": {
+      "AWS Region": "All supported AWS regions except China (Beijing), Asia Pacific (Thailand), Middle East (UAE), Asia Pacific (Hyderabad), Asia Pacific (Malaysia), Asia Pacific (Melbourne), AWS GovCloud (US-East), AWS GovCloud (US-West), Mexico (Central), Israel (Tel Aviv), Canada West (Calgary), Europe (Spain), China (Ningxia), Europe (Zurich) Region",
+      "Parameters": [
+        {
+          "Name": "requiredKeyTags",
+          "Optional": true,
+          "Type": "CSV"
+        }
+      ],
+      "Resource Types": "AWS::SageMaker::Domain",
+      "Trigger type": "Configuration changes"
+    },
     "SAGEMAKER_ENDPOINT_CONFIGURATION_KMS_KEY_CONFIGURED": {
       "AWS Region": "All supported AWS regions except Asia Pacific (Thailand), Asia Pacific (Malaysia), Asia Pacific (Melbourne), Mexico (Central) Region",
       "Parameters": [
@@ -6279,6 +6248,18 @@
       "Resource Types": "AWS::SageMaker::EndpointConfig",
       "Trigger type": "Periodic"
     },
+    "SAGEMAKER_FEATURE_GROUP_TAGGED": {
+      "AWS Region": "All supported AWS regions except China (Beijing), Asia Pacific (Thailand), Middle East (UAE), Asia Pacific (Hyderabad), Asia Pacific (Malaysia), Asia Pacific (Melbourne), AWS GovCloud (US-East), AWS GovCloud (US-West), Mexico (Central), Canada West (Calgary), China (Ningxia), Europe (Zurich) Region",
+      "Parameters": [
+        {
+          "Name": "requiredKeyTags",
+          "Optional": true,
+          "Type": "CSV"
+        }
+      ],
+      "Resource Types": "AWS::SageMaker::FeatureGroup",
+      "Trigger type": "Configuration changes"
+    },
     "SAGEMAKER_IMAGE_DESCRIPTION": {
       "AWS Region": "All supported AWS regions except China (Beijing), Asia Pacific (Thailand), Asia Pacific (Hyderabad), Asia Pacific (Malaysia), Asia Pacific (Melbourne), AWS GovCloud (US-East), AWS GovCloud (US-West), Mexico (Central), Canada West (Calgary), China (Ningxia), Europe (Zurich) Region",
       "Parameters": [],
@@ -6840,6 +6821,11 @@
           "Name": "vpcIds",
           "Optional": true,
           "Type": "CSV"
+        },
+        {
+          "Name": "scopeConfigResourceTypes",
+          "Optional": true,
+          "Type": "CSV"
         }
       ],
       "Resource Types": "AWS::EC2::VPC",
diff --git a/moto/ec2/models/iam_instance_profile.py b/moto/ec2/models/iam_instance_profile.py
index 199e98bc9..7efbb2e4e 100644
--- a/moto/ec2/models/iam_instance_profile.py
+++ b/moto/ec2/models/iam_instance_profile.py
@@ -114,6 +114,11 @@ class IamInstanceProfileAssociationBackend:
                 )
                 del self.iam_instance_profile_associations[association_key]
                 # Deleting once and avoiding `RuntimeError: dictionary changed size during iteration`
+
+                iam_instance_profile_association.ec2_backend.modify_instance_attribute(
+                    association_key, "iam_instance_profile", None
+                )
+
                 break
 
         if not iam_instance_profile_association:
diff --git a/moto/ec2/models/instances.py b/moto/ec2/models/instances.py
index b752806a8..89b829961 100644
--- a/moto/ec2/models/instances.py
+++ b/moto/ec2/models/instances.py
@@ -878,18 +878,20 @@ class InstanceBackend:
         :return: A list with instance objects
         """
         result = []
-
+        all_instance_ids = {}
         for reservation in self.all_reservations():
             for instance in reservation.instances:
-                if instance.id in instance_ids:
-                    if instance.applies(filters):
-                        result.append(instance)
-
-        if instance_ids and len(instance_ids) > len(result):
-            result_ids = [i.id for i in result]
-            missing_instance_ids = [i for i in instance_ids if i not in result_ids]
-            raise InvalidInstanceIdError(missing_instance_ids)
-
+                all_instance_ids[instance.id] = instance
+        not_found_instance_ids = []
+        for instance_id in instance_ids:
+            if instance_id not in all_instance_ids:
+                not_found_instance_ids.append(instance_id)
+                continue
+            instance = all_instance_ids[instance_id]
+            if instance.applies(filters):
+                result.append(instance)
+        if not_found_instance_ids:
+            raise InvalidInstanceIdError(not_found_instance_ids)
         return result
 
     def get_instance_by_id(self, instance_id: str) -> Optional[Instance]:
@@ -939,7 +941,10 @@ class InstanceBackend:
         self, instance_ids: List[str], include_all_instances: bool, filters: Any
     ) -> List[Instance]:
         if instance_ids:
-            return self.get_multi_instances_by_id(instance_ids, filters)
+            instances = self.get_multi_instances_by_id(instance_ids, filters)
+            if include_all_instances:
+                return instances
+            return [instance for instance in instances if instance.is_running()]
         elif include_all_instances:
             return self.all_instances(filters)
         else:
diff --git a/moto/textract/models.py b/moto/textract/models.py
index 86e66307b..f37c69dd2 100644
--- a/moto/textract/models.py
+++ b/moto/textract/models.py
@@ -1,9 +1,13 @@
+import json
+import time
 from collections import defaultdict
 from typing import Any, Dict, List, Optional
 
 from moto.core.base_backend import BackendDict, BaseBackend
 from moto.core.common_models import BaseModel
 from moto.moto_api._internal import mock_random
+from moto.sns import sns_backends
+from moto.sns.exceptions import TopicNotFound
 
 from .exceptions import InvalidJobIdException, InvalidParameterException
 
@@ -16,12 +20,53 @@ class TextractJobStatus:
 
 
 class TextractJob(BaseModel):
-    def __init__(self, job: Dict[str, Any]):
+    def __init__(
+        self, job: Dict[str, Any], notification_channel: Optional[Dict[str, str]] = None
+    ):
         self.job = job
+        self.notification_channel = notification_channel
+        self.job_id = str(mock_random.uuid4())
 
     def to_dict(self) -> Dict[str, Any]:
         return self.job
 
+    def send_completion_notification(
+        self, account_id: str, region_name: str, document_location: Dict[str, Any]
+    ) -> None:
+        if not self.notification_channel:
+            return
+
+        topic_arn = self.notification_channel.get("SNSTopicArn")
+        if not topic_arn:
+            return
+
+        # Convert document_location from {'S3Object': {'Bucket': '...', 'Name': '...'}} format
+        # to {'S3Bucket': '...', 'S3ObjectName': '...'} format as per AWS docs
+        s3_object = document_location.get("S3Object", {})
+        doc_location = {
+            "S3Bucket": s3_object.get("Bucket", ""),
+            "S3ObjectName": s3_object.get("Name", ""),
+        }
+
+        notification = {
+            "JobId": self.job_id,
+            "Status": self.job["JobStatus"],
+            "API": "StartDocumentTextDetection",
+            "JobTag": "",  # Not implemented yet
+            "Timestamp": int(time.time() * 1000),  # Convert to milliseconds
+            "DocumentLocation": doc_location,
+        }
+
+        sns_backend = sns_backends[account_id][region_name]
+        try:
+            sns_backend.publish(
+                message=json.dumps(notification),  # SNS requires message to be a string
+                arn=topic_arn,
+                subject="Amazon Textract Job Completion",
+            )
+        except TopicNotFound:
+            pass
+
 
 class TextractBackend(BaseBackend):
     """Implementation of Textract APIs."""
@@ -51,22 +96,35 @@ class TextractBackend(BaseBackend):
             "DocumentMetadata": {"Pages": TextractBackend.PAGES},
         }
 
-    def start_document_text_detection(self, document_location: str) -> str:
+    def start_document_text_detection(
+        self,
+        document_location: Dict[str, Any],
+        notification_channel: Optional[Dict[str, str]] = None,
+    ) -> str:
         """
-        The following parameters have not yet been implemented: ClientRequestToken, JobTag, NotificationChannel, OutputConfig, KmsKeyID
+        The following parameters have not yet been implemented: ClientRequestToken, JobTag, OutputConfig, KmsKeyID
         """
         if not document_location:
             raise InvalidParameterException()
-        job_id = str(mock_random.uuid4())
-        self.async_text_detection_jobs[job_id] = TextractJob(
+
+        job = TextractJob(
             {
                 "Blocks": TextractBackend.BLOCKS,
                 "DetectDocumentTextModelVersion": "1.0",
                 "DocumentMetadata": {"Pages": TextractBackend.PAGES},
                 "JobStatus": TextractBackend.JOB_STATUS,
-            }
+            },
+            notification_channel=notification_channel,
         )
-        return job_id
+
+        self.async_text_detection_jobs[job.job_id] = job
+
+        # Send completion notification since we're mocking an immediate completion
+        job.send_completion_notification(
+            self.account_id, self.region_name, document_location
+        )
+
+        return job.job_id
 
     def start_document_analysis(
         self, document_location: dict[str, Any], feature_types: List[str]
diff --git a/moto/textract/responses.py b/moto/textract/responses.py
index 5aeaeed0f..9198e5c7d 100644
--- a/moto/textract/responses.py
+++ b/moto/textract/responses.py
@@ -31,8 +31,10 @@ class TextractResponse(BaseResponse):
     def start_document_text_detection(self) -> str:
         params = json.loads(self.body)
         document_location = params.get("DocumentLocation")
+        notification_channel = params.get("NotificationChannel")
         job_id = self.textract_backend.start_document_text_detection(
-            document_location=document_location
+            document_location=document_location,
+            notification_channel=notification_channel,
         )
         return json.dumps(dict(JobId=job_id))
 


Test patch:
diff --git a/tests/test_ec2/test_iam_integration.py b/tests/test_ec2/test_iam_integration.py
index 8651d739c..e3c6c9a16 100644
--- a/tests/test_ec2/test_iam_integration.py
+++ b/tests/test_ec2/test_iam_integration.py
@@ -272,6 +272,11 @@ def test_disassociate():
         a["IamInstanceProfile"]["Arn"] for a in associations
     ]
 
+    reservations = client.describe_instances(InstanceIds=[instance_id])["Reservations"]
+    instances = reservations[0]["Instances"]
+
+    assert "IamInstanceProfile" in instances[0]
+
     disassociation = client.disassociate_iam_instance_profile(
         AssociationId=association["IamInstanceProfileAssociation"]["AssociationId"]
     )
@@ -288,6 +293,11 @@ def test_disassociate():
         a["IamInstanceProfile"]["Arn"] for a in associations
     ]
 
+    reservations = client.describe_instances(InstanceIds=[instance_id])["Reservations"]
+    instances = reservations[0]["Instances"]
+
+    assert "IamInstanceProfile" not in instances[0]
+
 
 @mock_aws
 def test_invalid_disassociate():
diff --git a/tests/test_ec2/test_instances.py b/tests/test_ec2/test_instances.py
index c704585d8..ca88fc21b 100644
--- a/tests/test_ec2/test_instances.py
+++ b/tests/test_ec2/test_instances.py
@@ -2064,6 +2064,66 @@ def test_describe_instance_status_with_non_running_instances():
     assert status3["InstanceState"] == {"Code": 16, "Name": "running"}
 
 
+@mock_aws
+def test_describe_instance_status_with_partial_instance_ids():
+    client = boto3.client("ec2", region_name="us-east-1")
+    ec2 = boto3.resource("ec2", region_name="us-east-1")
+    reservation = ec2.create_instances(ImageId=EXAMPLE_AMI_ID, MinCount=3, MaxCount=3)
+    instance1, instance2, instance3 = reservation
+
+    all_status = client.describe_instance_status(
+        InstanceIds=[instance1.id, instance3.id]
+    )["InstanceStatuses"]
+    assert instance1.id in [status["InstanceId"] for status in all_status]
+    assert instance2.id not in [status["InstanceId"] for status in all_status]
+    assert instance3.id in [status["InstanceId"] for status in all_status]
+
+
+@mock_aws
+def test_describe_instance_status_with_partial_instance_id_stopped():
+    client = boto3.client("ec2", region_name="us-east-1")
+    ec2 = boto3.resource("ec2", region_name="us-east-1")
+    reservation = ec2.create_instances(ImageId=EXAMPLE_AMI_ID, MinCount=3, MaxCount=3)
+    instance1, instance2, instance3 = reservation
+    instance1.stop()
+
+    all_running_status = client.describe_instance_status(
+        InstanceIds=[instance1.id, instance2.id, instance3.id]
+    )["InstanceStatuses"]
+    assert instance1.id not in [status["InstanceId"] for status in all_running_status]
+    assert instance2.id in [status["InstanceId"] for status in all_running_status]
+    assert instance3.id in [status["InstanceId"] for status in all_running_status]
+
+    all_status = client.describe_instance_status(
+        InstanceIds=[instance1.id, instance2.id, instance3.id], IncludeAllInstances=True
+    )["InstanceStatuses"]
+    assert instance1.id in [status["InstanceId"] for status in all_status]
+    assert instance2.id in [status["InstanceId"] for status in all_status]
+    assert instance3.id in [status["InstanceId"] for status in all_status]
+
+
+@mock_aws
+def test_describe_instance_status_with_stopped_instance_filter_and_instance_ids():
+    client = boto3.client("ec2", region_name="us-east-1")
+    ec2 = boto3.resource("ec2", region_name="us-east-1")
+    reservation = ec2.create_instances(ImageId=EXAMPLE_AMI_ID, MinCount=1, MaxCount=1)
+    instance1 = reservation[0]
+    instance1.stop()
+
+    stopped_status = client.describe_instance_status(
+        InstanceIds=[instance1.id],
+        Filters=[{"Name": "instance-state-name", "Values": ["stopped"]}],
+    )["InstanceStatuses"]
+    assert instance1.id not in [status["InstanceId"] for status in stopped_status]
+
+    all_status = client.describe_instance_status(
+        InstanceIds=[instance1.id],
+        Filters=[{"Name": "instance-state-name", "Values": ["stopped"]}],
+        IncludeAllInstances=True,
+    )["InstanceStatuses"]
+    assert instance1.id in [status["InstanceId"] for status in all_status]
+
+
 @mock_aws
 def test_get_instance_by_security_group():
     client = boto3.client("ec2", region_name="us-east-1")
diff --git a/tests/test_textract/test_textract.py b/tests/test_textract/test_textract.py
index 023b66bb7..ffce481c4 100644
--- a/tests/test_textract/test_textract.py
+++ b/tests/test_textract/test_textract.py
@@ -1,3 +1,4 @@
+import json
 from random import randint
 from unittest import SkipTest
 
@@ -87,6 +88,61 @@ def test_detect_document_text():
     assert result["DetectDocumentTextModelVersion"] == "1.0"
 
 
+@mock_aws
+def test_start_document_text_detection_with_sns_notification():
+    # Create SNS topic
+    sns = boto3.client("sns", region_name="us-east-1")
+    topic = sns.create_topic(Name="test-topic")
+    topic_arn = topic["TopicArn"]
+
+    # Create SQS queue and subscribe to SNS topic to verify notifications
+    sqs = boto3.client("sqs", region_name="us-east-1")
+    queue = sqs.create_queue(QueueName="test-queue")
+    queue_url = queue["QueueUrl"]
+
+    # Get queue ARN
+    queue_attrs = sqs.get_queue_attributes(
+        QueueUrl=queue_url, AttributeNames=["QueueArn"]
+    )
+    queue_arn = queue_attrs["Attributes"]["QueueArn"]
+
+    # Subscribe SQS to SNS
+    sns.subscribe(TopicArn=topic_arn, Protocol="sqs", Endpoint=queue_arn)
+
+    # Start Textract job with notification
+    client = boto3.client("textract", region_name="us-east-1")
+    document_location = {"S3Object": {"Bucket": "bucket", "Name": "name"}}
+    resp = client.start_document_text_detection(
+        DocumentLocation=document_location,
+        NotificationChannel={
+            "SNSTopicArn": topic_arn,
+            "RoleArn": "arn:aws:iam::123456789012:role/service-role/TextractRole",
+        },
+    )
+
+    assert "JobId" in resp
+    job_id = resp["JobId"]
+
+    # Check if notification was sent to SQS
+    messages = sqs.receive_message(QueueUrl=queue_url, MaxNumberOfMessages=1)
+    assert "Messages" in messages
+    message = messages["Messages"][0]
+
+    # Parse SNS message from SQS
+    body = json.loads(message["Body"])
+    notification = json.loads(body["Message"])
+
+    # Verify notification contents
+    assert notification["JobId"] == job_id
+    assert notification["Status"] == "SUCCEEDED"
+    assert notification["API"] == "StartDocumentTextDetection"
+    assert "Timestamp" in notification
+    assert notification["DocumentLocation"] == {
+        "S3Bucket": document_location["S3Object"]["Bucket"],
+        "S3ObjectName": document_location["S3Object"]["Name"],
+    }
+
+
 @mock_aws
 def test_get_document_analysis():
     if settings.TEST_SERVER_MODE: