Repository: getmoto/moto
Pull request #7161 (2023-12-27 07:31:12)

 Subject: [PR] Adding Rekognition compare_faces, detect_labels and detect_text, Textract detect_document_text
Hi,

I hope this message finds you well. I've just submitted a pull request that includes the following enhancements

Added support for 
**Rekognition:** compare_faces, detect_labels, detect_text
**Textract:** detect_document_text

Thanks

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/7161?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`e5f9621`)](https://app.codecov.io/gh/getmoto/moto/commit/e5f962193cb12e696bd6ec641844c4d09b5fb87c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) 95.90% compared to head [(`601dd49`)](https://app.codecov.io/gh/getmoto/moto/pull/7161?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) 95.90%.


<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           master    #7161   +/-   ##
=======================================
  Coverage   95.90%   95.90%           
=======================================
  Files         840      840           
  Lines       82438    82470   +32     
=======================================
+ Hits        79060    79093   +33     
+ Misses       3378     3377    -1     
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/7161/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/7161/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `35.49% <43.75%> (+<0.01%)` | :arrow_up: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/7161/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `95.84% <100.00%> (+<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/7161?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).


Patch:
diff --git a/docs/docs/faq.rst b/docs/docs/faq.rst
index 5380569e3..cc6271b05 100644
--- a/docs/docs/faq.rst
+++ b/docs/docs/faq.rst
@@ -19,6 +19,14 @@ Is Moto concurrency safe?
 
 No. Moto is not designed for multithreaded access/multiprocessing.
 
+Moto's internal state, encompassing resource creations and modifications, is managed within the context of the thread in which it is invoked. In a multithreading environment, Python's threading model involves creating a new thread that initially copies the global context from the main thread. However, it's crucial to understand that this copy is a one-time snapshot at the moment of thread creation.
+
+If Moto is employed to mock AWS services in a secondary thread, resources created or modified within that thread may not be visible to other threads or the main thread. The initial copy of the global context to the new thread means that the secondary thread depends on the state from the main thread in a read-only manner. Subsequent modifications or creations within the secondary thread do not propagate back to the main thread or affect the global context of other threads.
+
+For example, if you are using Moto to mock the AWS Cognito IDP service and create users, and these operations are performed in a thread separate from the main application logic, the application may not be able to see the users created in the secondary thread. This limitation arises because, in Python, the new thread's initial state is a copy of the main thread's state, and any changes in one thread do not automatically reflect in other threads and vice versa.
+
+It is essential to consider this behavior when using Moto in scenarios involving multiple threads. For tests that require resource modifications, such as creating or updating AWS resources, it is recommended to ensure that Moto operations are performed within the same thread context as the application or test logic to ensure consistent behavior.
+
 Why am I getting RUST errors when installing Moto?
 ####################################################
 
diff --git a/moto/cognitoidp/models.py b/moto/cognitoidp/models.py
index 577e194de..f0cc9082f 100644
--- a/moto/cognitoidp/models.py
+++ b/moto/cognitoidp/models.py
@@ -909,7 +909,7 @@ class CognitoResourceServer(BaseModel):
             "Name": self.name,
         }
 
-        if len(self.scopes) != 0:
+        if self.scopes:
             res.update({"Scopes": self.scopes})
 
         return res
diff --git a/moto/rekognition/models.py b/moto/rekognition/models.py
index d5589f9e5..aff6e3687 100644
--- a/moto/rekognition/models.py
+++ b/moto/rekognition/models.py
@@ -46,6 +46,30 @@ class RekognitionBackend(BaseBackend):
             self._text_model_version(),
         )
 
+    def compare_faces(
+        self,
+    ) -> Tuple[List[Dict[str, Any]], str, str, List[Dict[str, Any]], Dict[str, Any],]:
+        return (
+            self._face_matches(),
+            "ROTATE_90",
+            "ROTATE_90",
+            self._unmatched_faces(),
+            self.source_image_face(),
+        )
+
+    def detect_labels(self) -> Tuple[List[Dict[str, Any]], Dict[str, Any], str]:
+        return (
+            self._mobile_phone_label(),
+            self._image_properties(),
+            "3.0",
+        )
+
+    def detect_text(self) -> Tuple[List[Dict[str, Any]], str]:
+        return (
+            self._detect_text_text_detections(),
+            "3.0",
+        )
+
     # private
 
     def _job_id(self) -> str:
@@ -339,5 +363,391 @@ class RekognitionBackend(BaseBackend):
             },
         ]
 
+    def _face_matches(self) -> List[Dict[str, Any]]:
+        return [
+            {
+                "Face": {
+                    "BoundingBox": {
+                        "Width": 0.5521978139877319,
+                        "Top": 0.1203877404332161,
+                        "Left": 0.23626373708248138,
+                        "Height": 0.3126954436302185,
+                    },
+                    "Confidence": 99.98751068115234,
+                    "Pose": {
+                        "Yaw": -82.36799621582031,
+                        "Roll": -62.13221740722656,
+                        "Pitch": 0.8652129173278809,
+                    },
+                    "Quality": {
+                        "Sharpness": 99.99880981445312,
+                        "Brightness": 54.49755096435547,
+                    },
+                    "Landmarks": [
+                        {
+                            "Y": 0.2996366024017334,
+                            "X": 0.41685718297958374,
+                            "Type": "eyeLeft",
+                        },
+                        {
+                            "Y": 0.2658946216106415,
+                            "X": 0.4414493441581726,
+                            "Type": "eyeRight",
+                        },
+                        {
+                            "Y": 0.3465650677680969,
+                            "X": 0.48636093735694885,
+                            "Type": "nose",
+                        },
+                        {
+                            "Y": 0.30935320258140564,
+                            "X": 0.6251809000968933,
+                            "Type": "mouthLeft",
+                        },
+                        {
+                            "Y": 0.26942989230155945,
+                            "X": 0.6454493403434753,
+                            "Type": "mouthRight",
+                        },
+                    ],
+                },
+                "Similarity": 100.0,
+            }
+        ]
+
+    def _unmatched_faces(self) -> List[Dict[str, Any]]:
+        return [
+            {
+                "BoundingBox": {
+                    "Width": 0.4890109896659851,
+                    "Top": 0.6566604375839233,
+                    "Left": 0.10989011079072952,
+                    "Height": 0.278298944234848,
+                },
+                "Confidence": 99.99992370605469,
+                "Pose": {
+                    "Yaw": 51.51519012451172,
+                    "Roll": -110.32493591308594,
+                    "Pitch": -2.322134017944336,
+                },
+                "Quality": {
+                    "Sharpness": 99.99671173095703,
+                    "Brightness": 57.23163986206055,
+                },
+                "Landmarks": [
+                    {
+                        "Y": 0.8288310766220093,
+                        "X": 0.3133862614631653,
+                        "Type": "eyeLeft",
+                    },
+                    {
+                        "Y": 0.7632885575294495,
+                        "X": 0.28091415762901306,
+                        "Type": "eyeRight",
+                    },
+                    {"Y": 0.7417283654212952, "X": 0.3631140887737274, "Type": "nose"},
+                    {
+                        "Y": 0.8081989884376526,
+                        "X": 0.48565614223480225,
+                        "Type": "mouthLeft",
+                    },
+                    {
+                        "Y": 0.7548204660415649,
+                        "X": 0.46090251207351685,
+                        "Type": "mouthRight",
+                    },
+                ],
+            }
+        ]
+
+    def source_image_face(self) -> Dict[str, Any]:
+        return {
+            "BoundingBox": {
+                "Width": 0.5521978139877319,
+                "Top": 0.1203877404332161,
+                "Left": 0.23626373708248138,
+                "Height": 0.3126954436302185,
+            },
+            "Confidence": 99.98751068115234,
+        }
+
+    def _mobile_phone_label(self) -> List[Dict[str, Any]]:
+        return [
+            {
+                "Name": "Mobile Phone",
+                "Parents": [{"Name": "Phone"}],
+                "Aliases": [{"Name": "Cell Phone"}],
+                "Categories": [{"Name": "Technology and Computing"}],
+                "Confidence": 99.9364013671875,
+                "Instances": [
+                    {
+                        "BoundingBox": {
+                            "Width": 0.26779675483703613,
+                            "Height": 0.8562285900115967,
+                            "Left": 0.3604024350643158,
+                            "Top": 0.09245597571134567,
+                        },
+                        "Confidence": 99.9364013671875,
+                        "DominantColors": [
+                            {
+                                "Red": 120,
+                                "Green": 137,
+                                "Blue": 132,
+                                "HexCode": "3A7432",
+                                "SimplifiedColor": "red",
+                                "CssColor": "fuscia",
+                                "PixelPercentage": 40.10,
+                            }
+                        ],
+                    }
+                ],
+            }
+        ]
+
+    def _image_properties(self) -> Dict[str, Any]:
+        return {
+            "ImageProperties": {
+                "Quality": {
+                    "Brightness": 40,
+                    "Sharpness": 40,
+                    "Contrast": 24,
+                },
+                "DominantColors": [
+                    {
+                        "Red": 120,
+                        "Green": 137,
+                        "Blue": 132,
+                        "HexCode": "3A7432",
+                        "SimplifiedColor": "red",
+                        "CssColor": "fuscia",
+                        "PixelPercentage": 40.10,
+                    }
+                ],
+                "Foreground": {
+                    "Quality": {
+                        "Brightness": 40,
+                        "Sharpness": 40,
+                    },
+                    "DominantColors": [
+                        {
+                            "Red": 200,
+                            "Green": 137,
+                            "Blue": 132,
+                            "HexCode": "3A7432",
+                            "CSSColor": "",
+                            "SimplifiedColor": "red",
+                            "PixelPercentage": 30.70,
+                        }
+                    ],
+                },
+                "Background": {
+                    "Quality": {
+                        "Brightness": 40,
+                        "Sharpness": 40,
+                    },
+                    "DominantColors": [
+                        {
+                            "Red": 200,
+                            "Green": 137,
+                            "Blue": 132,
+                            "HexCode": "3A7432",
+                            "CSSColor": "",
+                            "SimplifiedColor": "Red",
+                            "PixelPercentage": 10.20,
+                        }
+                    ],
+                },
+            }
+        }
+
+    def _detect_text_text_detections(self) -> List[Dict[str, Any]]:
+        return [
+            {
+                "Confidence": 99.35693359375,
+                "DetectedText": "IT'S",
+                "Geometry": {
+                    "BoundingBox": {
+                        "Height": 0.09988046437501907,
+                        "Left": 0.6684935688972473,
+                        "Top": 0.18226495385169983,
+                        "Width": 0.1461552083492279,
+                    },
+                    "Polygon": [
+                        {"X": 0.6684935688972473, "Y": 0.1838926374912262},
+                        {"X": 0.8141663074493408, "Y": 0.18226495385169983},
+                        {"X": 0.8146487474441528, "Y": 0.28051772713661194},
+                        {"X": 0.6689760088920593, "Y": 0.2821454107761383},
+                    ],
+                },
+                "Id": 0,
+                "Type": "LINE",
+            },
+            {
+                "Confidence": 99.6207275390625,
+                "DetectedText": "MONDAY",
+                "Geometry": {
+                    "BoundingBox": {
+                        "Height": 0.11442459374666214,
+                        "Left": 0.5566731691360474,
+                        "Top": 0.3525116443634033,
+                        "Width": 0.39574965834617615,
+                    },
+                    "Polygon": [
+                        {"X": 0.5566731691360474, "Y": 0.353712260723114},
+                        {"X": 0.9522717595100403, "Y": 0.3525116443634033},
+                        {"X": 0.9524227976799011, "Y": 0.4657355844974518},
+                        {"X": 0.5568241477012634, "Y": 0.46693623065948486},
+                    ],
+                },
+                "Id": 1,
+                "Type": "LINE",
+            },
+            {
+                "Confidence": 99.6160888671875,
+                "DetectedText": "but keep",
+                "Geometry": {
+                    "BoundingBox": {
+                        "Height": 0.08314694464206696,
+                        "Left": 0.6398131847381592,
+                        "Top": 0.5267938375473022,
+                        "Width": 0.2021435648202896,
+                    },
+                    "Polygon": [
+                        {"X": 0.640289306640625, "Y": 0.5267938375473022},
+                        {"X": 0.8419567942619324, "Y": 0.5295097827911377},
+                        {"X": 0.8414806723594666, "Y": 0.609940767288208},
+                        {"X": 0.6398131847381592, "Y": 0.6072247624397278},
+                    ],
+                },
+                "Id": 2,
+                "Type": "LINE",
+            },
+            {
+                "Confidence": 88.95134735107422,
+                "DetectedText": "Smiling",
+                "Geometry": {
+                    "BoundingBox": {
+                        "Height": 0.4326171875,
+                        "Left": 0.46289217472076416,
+                        "Top": 0.5634765625,
+                        "Width": 0.5371078252792358,
+                    },
+                    "Polygon": [
+                        {"X": 0.46289217472076416, "Y": 0.5634765625},
+                        {"X": 1.0, "Y": 0.5634765625},
+                        {"X": 1.0, "Y": 0.99609375},
+                        {"X": 0.46289217472076416, "Y": 0.99609375},
+                    ],
+                },
+                "Id": 3,
+                "Type": "LINE",
+            },
+            {
+                "Confidence": 99.35693359375,
+                "DetectedText": "IT'S",
+                "Geometry": {
+                    "BoundingBox": {
+                        "Height": 0.09988046437501907,
+                        "Left": 0.6684935688972473,
+                        "Top": 0.18226495385169983,
+                        "Width": 0.1461552083492279,
+                    },
+                    "Polygon": [
+                        {"X": 0.6684935688972473, "Y": 0.1838926374912262},
+                        {"X": 0.8141663074493408, "Y": 0.18226495385169983},
+                        {"X": 0.8146487474441528, "Y": 0.28051772713661194},
+                        {"X": 0.6689760088920593, "Y": 0.2821454107761383},
+                    ],
+                },
+                "Id": 4,
+                "ParentId": 0,
+                "Type": "WORD",
+            },
+            {
+                "Confidence": 99.6207275390625,
+                "DetectedText": "MONDAY",
+                "Geometry": {
+                    "BoundingBox": {
+                        "Height": 0.11442466825246811,
+                        "Left": 0.5566731691360474,
+                        "Top": 0.35251158475875854,
+                        "Width": 0.39574965834617615,
+                    },
+                    "Polygon": [
+                        {"X": 0.5566731691360474, "Y": 0.3537122905254364},
+                        {"X": 0.9522718787193298, "Y": 0.35251158475875854},
+                        {"X": 0.9524227976799011, "Y": 0.4657355546951294},
+                        {"X": 0.5568241477012634, "Y": 0.46693626046180725},
+                    ],
+                },
+                "Id": 5,
+                "ParentId": 1,
+                "Type": "WORD",
+            },
+            {
+                "Confidence": 99.96778869628906,
+                "DetectedText": "but",
+                "Geometry": {
+                    "BoundingBox": {
+                        "Height": 0.0625,
+                        "Left": 0.6402802467346191,
+                        "Top": 0.5283203125,
+                        "Width": 0.08027780801057816,
+                    },
+                    "Polygon": [
+                        {"X": 0.6402802467346191, "Y": 0.5283203125},
+                        {"X": 0.7205580472946167, "Y": 0.5283203125},
+                        {"X": 0.7205580472946167, "Y": 0.5908203125},
+                        {"X": 0.6402802467346191, "Y": 0.5908203125},
+                    ],
+                },
+                "Id": 6,
+                "ParentId": 2,
+                "Type": "WORD",
+            },
+            {
+                "Confidence": 99.26438903808594,
+                "DetectedText": "keep",
+                "Geometry": {
+                    "BoundingBox": {
+                        "Height": 0.0818721204996109,
+                        "Left": 0.7344760298728943,
+                        "Top": 0.5280686020851135,
+                        "Width": 0.10748066753149033,
+                    },
+                    "Polygon": [
+                        {"X": 0.7349520921707153, "Y": 0.5280686020851135},
+                        {"X": 0.8419566750526428, "Y": 0.5295097827911377},
+                        {"X": 0.8414806127548218, "Y": 0.6099407076835632},
+                        {"X": 0.7344760298728943, "Y": 0.6084995269775391},
+                    ],
+                },
+                "Id": 7,
+                "ParentId": 2,
+                "Type": "WORD",
+            },
+            {
+                "Confidence": 88.95134735107422,
+                "DetectedText": "Smiling",
+                "Geometry": {
+                    "BoundingBox": {
+                        "Height": 0.4326171875,
+                        "Left": 0.46289217472076416,
+                        "Top": 0.5634765625,
+                        "Width": 0.5371078252792358,
+                    },
+                    "Polygon": [
+                        {"X": 0.46289217472076416, "Y": 0.5634765625},
+                        {"X": 1.0, "Y": 0.5634765625},
+                        {"X": 1.0, "Y": 0.99609375},
+                        {"X": 0.46289217472076416, "Y": 0.99609375},
+                    ],
+                },
+                "Id": 8,
+                "ParentId": 3,
+                "Type": "WORD",
+            },
+        ]
+
 
 rekognition_backends = BackendDict(RekognitionBackend, "rekognition")
diff --git a/moto/rekognition/responses.py b/moto/rekognition/responses.py
index 37af625b3..a18e912c7 100644
--- a/moto/rekognition/responses.py
+++ b/moto/rekognition/responses.py
@@ -58,6 +58,51 @@ class RekognitionResponse(BaseResponse):
             )
         )
 
+    def compare_faces(self) -> str:
+        (
+            face_matches,
+            source_image_orientation_correction,
+            target_image_orientation_correction,
+            unmatched_faces,
+            source_image_face,
+        ) = self.rekognition_backend.compare_faces()
+
+        return json.dumps(
+            dict(
+                FaceMatches=face_matches,
+                SourceImageOrientationCorrection=source_image_orientation_correction,
+                TargetImageOrientationCorrection=target_image_orientation_correction,
+                UnmatchedFaces=unmatched_faces,
+                SourceImageFace=source_image_face,
+            )
+        )
+
+    def detect_labels(self) -> str:
+        (
+            labels,
+            image_properties,
+            label_model_version,
+        ) = self.rekognition_backend.detect_labels()
+        return json.dumps(
+            dict(
+                Labels=labels,
+                ImageProperties=image_properties,
+                LabelModelVersion=label_model_version,
+            )
+        )
+
+    def detect_text(self) -> str:
+        (
+            text_detections,
+            text_model_version,
+        ) = self.rekognition_backend.detect_text()
+        return json.dumps(
+            dict(
+                TextDetections=text_detections,
+                TextModelVersion=text_model_version,
+            )
+        )
+
     def start_face_search(self) -> TYPE_RESPONSE:
         headers = {"Content-Type": "application/x-amz-json-1.1"}
         job_id = self.rekognition_backend.start_face_search()
diff --git a/moto/scheduler/exceptions.py b/moto/scheduler/exceptions.py
index 0e180c2c1..fcf49f736 100644
--- a/moto/scheduler/exceptions.py
+++ b/moto/scheduler/exceptions.py
@@ -8,8 +8,10 @@ class ScheduleExists(JsonRESTError):
 
 
 class ScheduleNotFound(JsonRESTError):
-    def __init__(self) -> None:
-        super().__init__("ResourceNotFoundException", "Schedule not found")
+    def __init__(self, name: str) -> None:
+        super().__init__(
+            "ResourceNotFoundException", f"Schedule {name} does not exist."
+        )
 
 
 class ScheduleGroupNotFound(JsonRESTError):
diff --git a/moto/scheduler/models.py b/moto/scheduler/models.py
index bd440a9f8..fbbd57f7c 100644
--- a/moto/scheduler/models.py
+++ b/moto/scheduler/models.py
@@ -108,10 +108,12 @@ class ScheduleGroup(BaseModel):
 
     def get_schedule(self, name: str) -> Schedule:
         if name not in self.schedules:
-            raise ScheduleNotFound
+            raise ScheduleNotFound(name)
         return self.schedules[name]
 
     def delete_schedule(self, name: str) -> None:
+        if name not in self.schedules:
+            raise ScheduleNotFound(name)
         self.schedules.pop(name)
 
     def to_dict(self) -> Dict[str, Any]:
diff --git a/moto/textract/models.py b/moto/textract/models.py
index 361a2e543..1cc2dd9f5 100644
--- a/moto/textract/models.py
+++ b/moto/textract/models.py
@@ -42,6 +42,13 @@ class TextractBackend(BaseBackend):
             raise InvalidJobIdException()
         return job
 
+    def detect_document_text(self) -> Dict[str, Any]:
+        return {
+            "Blocks": TextractBackend.BLOCKS,
+            "DetectDocumentTextModelVersion": "1.0",
+            "DocumentMetadata": {"Pages": TextractBackend.PAGES},
+        }
+
     def start_document_text_detection(self, document_location: str) -> str:
         """
         The following parameters have not yet been implemented: ClientRequestToken, JobTag, NotificationChannel, OutputConfig, KmsKeyID
diff --git a/moto/textract/responses.py b/moto/textract/responses.py
index d723c1f8e..421f188de 100644
--- a/moto/textract/responses.py
+++ b/moto/textract/responses.py
@@ -23,6 +23,10 @@ class TextractResponse(BaseResponse):
         job = self.textract_backend.get_document_text_detection(job_id=job_id).to_dict()
         return json.dumps(job)
 
+    def detect_document_text(self) -> str:
+        result = self.textract_backend.detect_document_text()
+        return json.dumps(result)
+
     def start_document_text_detection(self) -> str:
         params = json.loads(self.body)
         document_location = params.get("DocumentLocation")


Test patch:
diff --git a/tests/test_cognitoidp/test_cognitoidp.py b/tests/test_cognitoidp/test_cognitoidp.py
index 9b5b1db1f..ed2851096 100644
--- a/tests/test_cognitoidp/test_cognitoidp.py
+++ b/tests/test_cognitoidp/test_cognitoidp.py
@@ -3578,6 +3578,26 @@ def test_create_resource_server():
     assert ex.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400
 
 
+@mock_cognitoidp
+def test_create_resource_server_with_no_scopes():
+    client = boto3.client("cognito-idp", "us-west-2")
+    name = str(uuid.uuid4())
+    res = client.create_user_pool(PoolName=name)
+
+    user_pool_id = res["UserPool"]["Id"]
+    identifier = "http://localhost.localdomain"
+    name = "local server"
+
+    res = client.create_resource_server(
+        UserPoolId=user_pool_id, Identifier=identifier, Name=name
+    )
+
+    assert res["ResourceServer"]["UserPoolId"] == user_pool_id
+    assert res["ResourceServer"]["Identifier"] == identifier
+    assert res["ResourceServer"]["Name"] == name
+    assert "Scopes" not in res["ResourceServer"]
+
+
 @mock_cognitoidp
 def test_describe_resource_server():
 
diff --git a/tests/test_rekognition/test_rekognition.py b/tests/test_rekognition/test_rekognition.py
index 0e9bd3119..7b1070462 100644
--- a/tests/test_rekognition/test_rekognition.py
+++ b/tests/test_rekognition/test_rekognition.py
@@ -43,6 +43,48 @@ def test_start_text_detection():
     assert "JobId" in resp
 
 
+@mock_rekognition
+def test_compare_faces():
+    client = boto3.client("rekognition", region_name="ap-southeast-1")
+    sourceimage = {
+        "S3Object": {"Bucket": "string", "Name": "string", "Version": "string"}
+    }
+    targetimage = {
+        "S3Object": {"Bucket": "string", "Name": "string", "Version": "string"}
+    }
+
+    resp = client.compare_faces(
+        SimilarityThreshold=80, SourceImage=sourceimage, TargetImage=targetimage
+    )
+
+    assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200
+    assert "FaceMatches" in resp
+
+
+@mock_rekognition
+def test_detect_labels():
+    client = boto3.client("rekognition", region_name="ap-southeast-1")
+
+    resp = client.detect_labels(
+        Image={"S3Object": {"Bucket": "string", "Name": "name.jpg"}}, MaxLabels=10
+    )
+
+    assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200
+    assert "Labels" in resp
+
+
+@mock_rekognition
+def test_detect_text():
+    client = boto3.client("rekognition", region_name="ap-southeast-1")
+
+    resp = client.detect_text(
+        Image={"S3Object": {"Bucket": "string", "Name": "name.jpg"}}
+    )
+
+    assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200
+    assert "TextDetections" in resp
+
+
 @mock_rekognition
 def test_get_face_search():
     client = boto3.client("rekognition", region_name="us-east-2")
diff --git a/tests/test_scheduler/test_scheduler.py b/tests/test_scheduler/test_scheduler.py
index 7abab8173..c613cb5f6 100644
--- a/tests/test_scheduler/test_scheduler.py
+++ b/tests/test_scheduler/test_scheduler.py
@@ -175,6 +175,17 @@ def test_get_schedule_for_unknown_group():
     assert err["Code"] == "ResourceNotFoundException"
 
 
+@mock_scheduler
+def test_get_schedule_for_none_existing_schedule():
+    client = boto3.client("scheduler", region_name="eu-west-1")
+
+    with pytest.raises(ClientError) as exc:
+        client.get_schedule(Name="my-schedule")
+    err = exc.value.response["Error"]
+    assert err["Code"] == "ResourceNotFoundException"
+    assert err["Message"] == "Schedule my-schedule does not exist."
+
+
 @mock_scheduler
 def test_list_schedules():
     client = boto3.client("scheduler", region_name="eu-west-1")
@@ -206,3 +217,14 @@ def test_list_schedules():
 
     schedules = client.list_schedules(State="ENABLED")["Schedules"]
     assert len(schedules) == 4
+
+
+@mock_scheduler
+def test_delete_schedule_for_none_existing_schedule():
+    client = boto3.client("scheduler", region_name="eu-west-1")
+
+    with pytest.raises(ClientError) as exc:
+        client.delete_schedule(Name="my-schedule")
+    err = exc.value.response["Error"]
+    assert err["Code"] == "ResourceNotFoundException"
+    assert err["Message"] == "Schedule my-schedule does not exist."
diff --git a/tests/test_textract/test_server.py b/tests/test_textract/test_server.py
index 72dc62d7a..d84f4a0b1 100644
--- a/tests/test_textract/test_server.py
+++ b/tests/test_textract/test_server.py
@@ -23,6 +23,22 @@ def test_textract_start_text_detection():
     assert isinstance(data["JobId"], str)
 
 
+@mock_textract
+def test_detect_document_text():
+    backend = server.create_backend_app("textract")
+    test_client = backend.test_client()
+    headers = {"X-Amz-Target": "X-Amz-Target=Textract.DetectDocumentText"}
+    request_body = {
+        "DocumentLocation": {
+            "S3Object": {"Bucket": "bucket", "Name": "name", "Version": "version"}
+        }
+    }
+    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["Blocks"], list)
+
+
 @mock_textract
 def test_textract_start_text_detection_without_document_location():
     backend = server.create_backend_app("textract")
diff --git a/tests/test_textract/test_textract.py b/tests/test_textract/test_textract.py
index b994516f8..818d9580a 100644
--- a/tests/test_textract/test_textract.py
+++ b/tests/test_textract/test_textract.py
@@ -84,3 +84,18 @@ def test_get_document_text_detection_without_document_location():
         'Parameter validation failed:\nMissing required parameter in input: "DocumentLocation"'
         in e.value.args
     )
+
+
+@mock_textract
+def test_detect_document_text():
+    client = boto3.client("textract", region_name="us-east-1")
+    result = client.detect_document_text(
+        Document={
+            "S3Object": {
+                "Bucket": "bucket",
+                "Name": "name.jpg",
+            }
+        }
+    )
+    assert isinstance(result["Blocks"], list)
+    assert result["DetectDocumentTextModelVersion"] == "1.0"