Repository: getmoto/moto
Pull request #6455 (2023-06-28 15:27:26)

Lambda: create list_aliases functionality
- Creates the [list_aliases](https://boto3.amazonaws.com/v1/documentation/api/1.26.85/reference/services/lambda/client/list_aliases.html) functionality
- Redesign internal storage of aliases to correct mistaken implementation (Duplicate alias names could be created under the same lambda pointing to different versions)
- Adds tests for new functionality
- Worth noting I've not included functionality to filter by "version", can be implemented if needed

freshollie: Let me know if you want docs updated as part of this PR

Patch:
diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index 763cb8036..d2def447b 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -204,7 +204,7 @@
 
 ## apigatewayv2
 <details>
-<summary>69% implemented</summary>
+<summary>75% implemented</summary>
 
 - [X] create_api
 - [X] create_api_mapping
@@ -216,7 +216,7 @@
 - [X] create_model
 - [X] create_route
 - [X] create_route_response
-- [ ] create_stage
+- [X] create_stage
 - [X] create_vpc_link
 - [ ] delete_access_log_settings
 - [X] delete_api
@@ -232,7 +232,7 @@
 - [X] delete_route_request_parameter
 - [X] delete_route_response
 - [ ] delete_route_settings
-- [ ] delete_stage
+- [X] delete_stage
 - [X] delete_vpc_link
 - [ ] export_api
 - [X] get_api
@@ -256,8 +256,8 @@
 - [X] get_route_response
 - [ ] get_route_responses
 - [X] get_routes
-- [ ] get_stage
-- [ ] get_stages
+- [X] get_stage
+- [X] get_stages
 - [X] get_tags
 - [X] get_vpc_link
 - [X] get_vpc_links
@@ -6784,7 +6784,7 @@
 - [ ] deregister_managed_instance
 - [ ] deregister_patch_baseline_for_patch_group
 - [X] deregister_target_from_maintenance_window
-- [ ] deregister_task_from_maintenance_window
+- [X] deregister_task_from_maintenance_window
 - [ ] describe_activations
 - [ ] describe_association
 - [ ] describe_association_execution_targets
@@ -6807,7 +6807,7 @@
 - [ ] describe_maintenance_window_executions
 - [ ] describe_maintenance_window_schedule
 - [X] describe_maintenance_window_targets
-- [ ] describe_maintenance_window_tasks
+- [X] describe_maintenance_window_tasks
 - [X] describe_maintenance_windows
 - [ ] describe_maintenance_windows_for_target
 - [ ] describe_ops_items
@@ -6868,7 +6868,7 @@
 - [ ] register_default_patch_baseline
 - [ ] register_patch_baseline_for_patch_group
 - [X] register_target_with_maintenance_window
-- [ ] register_task_with_maintenance_window
+- [X] register_task_with_maintenance_window
 - [X] remove_tags_from_resource
 - [ ] reset_service_setting
 - [ ] resume_session
diff --git a/docs/docs/configuration/environment_variables.rst b/docs/docs/configuration/environment_variables.rst
index 657031637..a717abe92 100644
--- a/docs/docs/configuration/environment_variables.rst
+++ b/docs/docs/configuration/environment_variables.rst
@@ -24,6 +24,8 @@ The following is a non-exhaustive list of the environment variables that can be
 +-------------------------------+----------+-----------+-------------------------------------------------------------------------------------------------+
 | MOTO_ALLOW_NONEXISTENT_REGION | bool     | False     |                                                                                                 |
 +-------------------------------+----------+-----------+-------------------------------------------------------------------------------------------------+
+| MOTO_ENABLE_ISO_REGIONS       | bool     | False     | Enable the mocking of ISO regions (ISO, ISO-B, ISO-E, ISO-F)                                    |
++-------------------------------+----------+-----------+-------------------------------------------------------------------------------------------------+
 |                               |          |           |                                                                                                 |
 +-------------------------------+----------+-----------+-------------------------------------------------------------------------------------------------+
 | MOTO_S3_CUSTOM_ENDPOINTS      | str      |           | See :ref:`s3`.                                                                                  |
diff --git a/docs/docs/services/apigatewayv2.rst b/docs/docs/services/apigatewayv2.rst
index 452254ce5..7be34f0ff 100644
--- a/docs/docs/services/apigatewayv2.rst
+++ b/docs/docs/services/apigatewayv2.rst
@@ -46,7 +46,7 @@ apigatewayv2
         The following parameters are not yet implemented: ResponseModels, ResponseParameters
         
 
-- [ ] create_stage
+- [X] create_stage
 - [X] create_vpc_link
 - [ ] delete_access_log_settings
 - [X] delete_api
@@ -62,7 +62,7 @@ apigatewayv2
 - [X] delete_route_request_parameter
 - [X] delete_route_response
 - [ ] delete_route_settings
-- [ ] delete_stage
+- [X] delete_stage
 - [X] delete_vpc_link
 - [ ] export_api
 - [X] get_api
@@ -102,8 +102,8 @@ apigatewayv2
         Pagination is not yet implemented
         
 
-- [ ] get_stage
-- [ ] get_stages
+- [X] get_stage
+- [X] get_stages
 - [X] get_tags
 - [X] get_vpc_link
 - [X] get_vpc_links
diff --git a/moto/apigatewayv2/exceptions.py b/moto/apigatewayv2/exceptions.py
index 1a63b65a9..44ef021d6 100644
--- a/moto/apigatewayv2/exceptions.py
+++ b/moto/apigatewayv2/exceptions.py
@@ -123,3 +123,13 @@ class ApiMappingNotFound(APIGatewayV2Error):
             "NotFoundException",
             "The api mapping resource specified in the request was not found.",
         )
+
+
+class StageNotFound(APIGatewayV2Error):
+    code = 404
+
+    def __init__(self) -> None:
+        super().__init__(
+            "NotFoundException",
+            "Invalid stage identifier specified",
+        )
diff --git a/moto/apigatewayv2/models.py b/moto/apigatewayv2/models.py
index 7842e5a2b..5ec2b3992 100644
--- a/moto/apigatewayv2/models.py
+++ b/moto/apigatewayv2/models.py
@@ -22,9 +22,57 @@ from .exceptions import (
     VpcLinkNotFound,
     DomainNameNotFound,
     DomainNameAlreadyExists,
+    StageNotFound,
 )
 
 
+class Stage(BaseModel):
+    def __init__(self, api: "Api", config: Dict[str, Any]):
+        self.config = config
+        self.name = config["stageName"]
+        if api.protocol_type == "HTTP":
+            self.default_route_settings = config.get(
+                "defaultRouteSettings", {"detailedMetricsEnabled": False}
+            )
+        elif api.protocol_type == "WEBSOCKET":
+            self.default_route_settings = config.get(
+                "defaultRouteSettings",
+                {
+                    "dataTraceEnabled": False,
+                    "detailedMetricsEnabled": False,
+                    "loggingLevel": "OFF",
+                },
+            )
+        self.access_log_settings = config.get("accessLogSettings")
+        self.auto_deploy = config.get("autoDeploy")
+        self.client_certificate_id = config.get("clientCertificateId")
+        self.description = config.get("description")
+        self.route_settings = config.get("routeSettings", {})
+        self.stage_variables = config.get("stageVariables", {})
+        self.tags = config.get("tags", {})
+        self.created = self.updated = unix_time()
+
+    def to_json(self) -> Dict[str, Any]:
+        dct = {
+            "stageName": self.name,
+            "defaultRouteSettings": self.default_route_settings,
+            "createdDate": self.created,
+            "lastUpdatedDate": self.updated,
+            "routeSettings": self.route_settings,
+            "stageVariables": self.stage_variables,
+            "tags": self.tags,
+        }
+        if self.access_log_settings:
+            dct["accessLogSettings"] = self.access_log_settings
+        if self.auto_deploy is not None:
+            dct["autoDeploy"] = self.auto_deploy
+        if self.client_certificate_id:
+            dct["clientCertificateId"] = self.client_certificate_id
+        if self.description:
+            dct["description"] = self.description
+        return dct
+
+
 class Authorizer(BaseModel):
     def __init__(
         self,
@@ -541,6 +589,7 @@ class Api(BaseModel):
         self.integrations: Dict[str, Integration] = dict()
         self.models: Dict[str, Model] = dict()
         self.routes: Dict[str, Route] = dict()
+        self.stages: Dict[str, Stage] = dict()
 
         self.arn = f"arn:aws:apigateway:{region}::/apis/{self.api_id}"
         self.backend.tag_resource(self.arn, tags)
@@ -550,6 +599,7 @@ class Api(BaseModel):
         self.integrations.clear()
         self.models.clear()
         self.routes.clear()
+        self.stages.clear()
 
     def delete_cors_configuration(self) -> None:
         self.cors_configuration = None
@@ -962,6 +1012,19 @@ class Api(BaseModel):
         route = self.get_route(route_id)
         return route.get_route_response(route_response_id)
 
+    def create_stage(self, config: Dict[str, Any]) -> Stage:
+        stage = Stage(api=self, config=config)
+        self.stages[stage.name] = stage
+        return stage
+
+    def get_stage(self, stage_name: str) -> Stage:
+        if stage_name not in self.stages:
+            raise StageNotFound
+        return self.stages[stage_name]
+
+    def delete_stage(self, stage_name: str) -> None:
+        self.stages.pop(stage_name, None)
+
     def to_json(self) -> Dict[str, Any]:
         return {
             "apiId": self.api_id,
@@ -1599,7 +1662,6 @@ class ApiGatewayV2Backend(BaseBackend):
         mutual_tls_authentication: Dict[str, str],
         tags: Dict[str, str],
     ) -> DomainName:
-
         if domain_name in self.domain_names.keys():
             raise DomainNameAlreadyExists
 
@@ -1701,5 +1763,21 @@ class ApiGatewayV2Backend(BaseBackend):
 
         del self.api_mappings[api_mapping_id]
 
+    def create_stage(self, api_id: str, config: Dict[str, Any]) -> Stage:
+        api = self.get_api(api_id)
+        return api.create_stage(config)
+
+    def get_stage(self, api_id: str, stage_name: str) -> Stage:
+        api = self.get_api(api_id)
+        return api.get_stage(stage_name)
+
+    def delete_stage(self, api_id: str, stage_name: str) -> None:
+        api = self.get_api(api_id)
+        api.delete_stage(stage_name)
+
+    def get_stages(self, api_id: str) -> List[Stage]:
+        api = self.get_api(api_id)
+        return list(api.stages.values())
+
 
 apigatewayv2_backends = BackendDict(ApiGatewayV2Backend, "apigatewayv2")
diff --git a/moto/apigatewayv2/responses.py b/moto/apigatewayv2/responses.py
index 91ac11669..82f1c143c 100644
--- a/moto/apigatewayv2/responses.py
+++ b/moto/apigatewayv2/responses.py
@@ -212,6 +212,22 @@ class ApiGatewayV2Response(BaseResponse):
         if request.method == "DELETE":
             return self.delete_api_mapping()
 
+    def stages(self, request: Any, full_url: str, headers: Any) -> TYPE_RESPONSE:  # type: ignore[return]
+        self.setup_class(request, full_url, headers)
+
+        if self.method == "POST":
+            return self.create_stage()
+        if self.method == "GET":
+            return self.get_stages()
+
+    def stage(self, request: Any, full_url: str, headers: Any) -> TYPE_RESPONSE:  # type: ignore[return]
+        self.setup_class(request, full_url, headers)
+
+        if self.method == "GET":
+            return self.get_stage()
+        if self.method == "DELETE":
+            return self.delete_stage()
+
     def create_api(self) -> TYPE_RESPONSE:
         params = json.loads(self.body)
 
@@ -854,3 +870,26 @@ class ApiGatewayV2Response(BaseResponse):
             domain_name=domain_name,
         )
         return 204, {}, ""
+
+    def create_stage(self) -> TYPE_RESPONSE:
+        api_id = self.path.split("/")[-2]
+        config = json.loads(self.body)
+        stage = self.apigatewayv2_backend.create_stage(api_id, config)
+        return 200, {}, json.dumps(stage.to_json())
+
+    def get_stage(self) -> TYPE_RESPONSE:
+        api_id = self.path.split("/")[-3]
+        stage_name = self.path.split("/")[-1]
+        stage = self.apigatewayv2_backend.get_stage(api_id, stage_name)
+        return 200, {}, json.dumps(stage.to_json())
+
+    def delete_stage(self) -> TYPE_RESPONSE:
+        api_id = self.path.split("/")[-3]
+        stage_name = self.path.split("/")[-1]
+        self.apigatewayv2_backend.delete_stage(api_id, stage_name)
+        return 200, {}, "{}"
+
+    def get_stages(self) -> TYPE_RESPONSE:
+        api_id = self.path.split("/")[-2]
+        stages = self.apigatewayv2_backend.get_stages(api_id)
+        return 200, {}, json.dumps({"items": [st.to_json() for st in stages]})
diff --git a/moto/apigatewayv2/urls.py b/moto/apigatewayv2/urls.py
index b737112e8..7826904a6 100644
--- a/moto/apigatewayv2/urls.py
+++ b/moto/apigatewayv2/urls.py
@@ -26,6 +26,8 @@ url_paths = {
     "{0}/v2/apis/(?P<api_id>[^/]+)/routes/(?P<route_id>[^/]+)/routeresponses$": response_v2.route_responses,
     "{0}/v2/apis/(?P<api_id>[^/]+)/routes/(?P<route_id>[^/]+)/routeresponses/(?P<route_response_id>[^/]+)$": response_v2.route_response,
     "{0}/v2/apis/(?P<api_id>[^/]+)/routes/(?P<route_id>[^/]+)/requestparameters/(?P<request_parameter>[^/]+)$": response_v2.route_request_parameter,
+    "{0}/v2/apis/(?P<api_id>[^/]+)/stages$": response_v2.stages,
+    "{0}/v2/apis/(?P<api_id>[^/]+)/stages/(?P<stage_name>[^/]+)$": response_v2.stage,
     "{0}/v2/tags/(?P<resource_arn>[^/]+)$": response_v2.tags,
     "{0}/v2/tags/(?P<resource_arn_pt1>[^/]+)/apis/(?P<resource_arn_pt2>[^/]+)$": response_v2.tags,
     "{0}/v2/tags/(?P<resource_arn_pt1>[^/]+)/vpclinks/(?P<resource_arn_pt2>[^/]+)$": response_v2.tags,
diff --git a/moto/awslambda/exceptions.py b/moto/awslambda/exceptions.py
index 15b0e0ccd..9107e1ac5 100644
--- a/moto/awslambda/exceptions.py
+++ b/moto/awslambda/exceptions.py
@@ -34,6 +34,13 @@ class PreconditionFailedException(JsonRESTError):
         super().__init__("PreconditionFailedException", message)
 
 
+class ConflictException(LambdaClientError):
+    code = 409
+
+    def __init__(self, message: str):
+        super().__init__("ConflictException", message)
+
+
 class UnknownAliasException(LambdaClientError):
     code = 404
 
diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py
index 04af5c3a5..2aa35c9d0 100644
--- a/moto/awslambda/models.py
+++ b/moto/awslambda/models.py
@@ -36,6 +36,7 @@ from moto.ecr.models import ecr_backends
 from moto.s3.exceptions import MissingBucket, MissingKey
 from moto import settings
 from .exceptions import (
+    ConflictException,
     CrossAccountNotAllowed,
     FunctionUrlConfigNotFound,
     InvalidRoleFormat,
@@ -514,8 +515,6 @@ class LambdaFunction(CloudFormationModel, DockerModel):
 
         self.tags = spec.get("Tags") or dict()
 
-        self._aliases: Dict[str, LambdaAlias] = dict()
-
     def __getstate__(self) -> Dict[str, Any]:
         return {
             k: v
@@ -954,43 +953,6 @@ class LambdaFunction(CloudFormationModel, DockerModel):
     def delete(self, account_id: str, region: str) -> None:
         lambda_backends[account_id][region].delete_function(self.function_name)
 
-    def delete_alias(self, name: str) -> None:
-        self._aliases.pop(name, None)
-
-    def get_alias(self, name: str) -> LambdaAlias:
-        if name in self._aliases:
-            return self._aliases[name]
-        arn = f"arn:aws:lambda:{self.region}:{self.account_id}:function:{self.function_name}:{name}"
-        raise UnknownAliasException(arn)
-
-    def has_alias(self, alias_name: str) -> bool:
-        try:
-            return self.get_alias(alias_name) is not None
-        except UnknownAliasException:
-            return False
-
-    def put_alias(
-        self, name: str, description: str, function_version: str, routing_config: str
-    ) -> LambdaAlias:
-        alias = LambdaAlias(
-            account_id=self.account_id,
-            region=self.region,
-            name=name,
-            function_name=self.function_name,
-            function_version=function_version,
-            description=description,
-            routing_config=routing_config,
-        )
-        self._aliases[name] = alias
-        return alias
-
-    def update_alias(
-        self, name: str, description: str, function_version: str, routing_config: str
-    ) -> LambdaAlias:
-        alias = self.get_alias(name)
-        alias.update(description, function_version, routing_config)
-        return alias
-
     def create_url_config(self, config: Dict[str, Any]) -> "FunctionUrlConfig":
         self.url_config = FunctionUrlConfig(function=self, config=config)
         return self.url_config  # type: ignore[return-value]
@@ -1205,22 +1167,34 @@ class LambdaStorage(object):
         self.region_name = region_name
         self.account_id = account_id
 
+        # function-arn -> alias -> LambdaAlias
+        self._aliases: Dict[str, Dict[str, LambdaAlias]] = defaultdict(lambda: {})
+
     def _get_latest(self, name: str) -> LambdaFunction:
         return self._functions[name]["latest"]
 
     def _get_version(self, name: str, version: str) -> Optional[LambdaFunction]:
         for config in self._functions[name]["versions"]:
-            if str(config.version) == version or config.has_alias(version):
+            if str(config.version) == version:
                 return config
+
         return None
 
-    def delete_alias(self, name: str, function_name: str) -> None:
+    def _get_function_aliases(self, function_name: str) -> Dict[str, LambdaAlias]:
         fn = self.get_function_by_name_or_arn(function_name)
-        return fn.delete_alias(name)
+        return self._aliases[fn.function_arn]
+
+    def delete_alias(self, name: str, function_name: str) -> None:
+        aliases = self._get_function_aliases(function_name)
+        aliases.pop(name, None)
 
     def get_alias(self, name: str, function_name: str) -> LambdaAlias:
-        fn = self.get_function_by_name_or_arn(function_name)
-        return fn.get_alias(name)
+        aliases = self._get_function_aliases(function_name)
+        if name in aliases:
+            return aliases[name]
+
+        arn = f"arn:aws:lambda:{self.region_name}:{self.account_id}:function:{function_name}:{name}"
+        raise UnknownAliasException(arn)
 
     def put_alias(
         self,
@@ -1230,8 +1204,23 @@ class LambdaStorage(object):
         description: str,
         routing_config: str,
     ) -> LambdaAlias:
-        fn = self.get_function_by_name_or_arn(function_name)
-        return fn.put_alias(name, description, function_version, routing_config)
+        fn = self.get_function_by_name_or_arn(function_name, function_version)
+        aliases = self._get_function_aliases(function_name)
+        if name in aliases:
+            arn = f"arn:aws:lambda:{self.region_name}:{self.account_id}:function:{function_name}:{name}"
+            raise ConflictException(f"Alias already exists: {arn}")
+
+        alias = LambdaAlias(
+            account_id=self.account_id,
+            region=self.region_name,
+            name=name,
+            function_name=fn.function_name,
+            function_version=function_version,
+            description=description,
+            routing_config=routing_config,
+        )
+        aliases[name] = alias
+        return alias
 
     def update_alias(
         self,
@@ -1241,8 +1230,13 @@ class LambdaStorage(object):
         description: str,
         routing_config: str,
     ) -> LambdaAlias:
-        fn = self.get_function_by_name_or_arn(function_name)
-        return fn.update_alias(name, description, function_version, routing_config)
+        alias = self.get_alias(name, function_name)
+
+        # errors if new function version doesn't exist
+        self.get_function_by_name_or_arn(function_name, function_version)
+
+        alias.update(description, function_version, routing_config)
+        return alias
 
     def get_function_by_name(
         self, name: str, qualifier: Optional[str] = None
@@ -1256,7 +1250,17 @@ class LambdaStorage(object):
         if qualifier.lower() == "$latest":
             return self._functions[name]["latest"]
 
-        return self._get_version(name, qualifier)
+        found_version = self._get_version(name, qualifier)
+        if found_version:
+            return found_version
+
+        aliases = self._get_function_aliases(name)
+
+        if qualifier in aliases:
+            alias = aliases[qualifier]
+            return self._get_version(name, alias.function_version)
+
+        return None
 
     def list_versions_by_function(self, name: str) -> Iterable[LambdaFunction]:
         if name not in self._functions:
@@ -1266,6 +1270,10 @@ class LambdaStorage(object):
         latest.function_arn += ":$LATEST"
         return [latest] + self._functions[name]["versions"]
 
+    def list_aliases(self, function_name: str) -> Iterable[LambdaAlias]:
+        aliases = self._get_function_aliases(function_name)
+        return sorted(aliases.values(), key=lambda alias: alias.name)
+
     def get_arn(self, arn: str) -> Optional[LambdaFunction]:
         # Function ARN may contain an alias
         # arn:aws:lambda:region:account_id:function:<fn_name>:<alias_name>
@@ -1375,6 +1383,8 @@ class LambdaStorage(object):
                 ):
                     del self._functions[name]
 
+        self._aliases[function.function_arn] = {}
+
     def all(self) -> Iterable[LambdaFunction]:
         result = []
 
@@ -1706,6 +1716,9 @@ class LambdaBackend(BaseBackend):
     def list_versions_by_function(self, function_name: str) -> Iterable[LambdaFunction]:
         return self._lambdas.list_versions_by_function(function_name)
 
+    def list_aliases(self, function_name: str) -> Iterable[LambdaAlias]:
+        return self._lambdas.list_aliases(function_name)
+
     def get_event_source_mapping(self, uuid: str) -> Optional[EventSourceMapping]:
         return self._event_source_mappings.get(uuid)
 
diff --git a/moto/awslambda/responses.py b/moto/awslambda/responses.py
index 5810935c4..0df05b90e 100644
--- a/moto/awslambda/responses.py
+++ b/moto/awslambda/responses.py
@@ -46,8 +46,13 @@ class LambdaResponse(BaseResponse):
 
     def aliases(self, request: Any, full_url: str, headers: Any) -> TYPE_RESPONSE:  # type: ignore[return]
         self.setup_class(request, full_url, headers)
+
         if request.method == "POST":
             return self._create_alias()
+        elif request.method == "GET":
+            path = request.path if hasattr(request, "path") else path_url(request.url)
+            function_name = path.split("/")[-2]
+            return self._list_aliases(function_name)
 
     def alias(self, request: Any, full_url: str, headers: Any) -> TYPE_RESPONSE:  # type: ignore[return]
         self.setup_class(request, full_url, headers)
@@ -296,6 +301,16 @@ class LambdaResponse(BaseResponse):
 
         return 200, {}, json.dumps(result)
 
+    def _list_aliases(self, function_name: str) -> TYPE_RESPONSE:
+        result: Dict[str, Any] = {"Aliases": []}
+
+        aliases = self.backend.list_aliases(function_name)
+        for alias in aliases:
+            json_data = alias.to_json()
+            result["Aliases"].append(json_data)
+
+        return 200, {}, json.dumps(result)
+
     def _create_function(self) -> TYPE_RESPONSE:
         fn = self.backend.create_function(self.json_body)
         config = fn.get_configuration(on_create=True)
diff --git a/moto/core/base_backend.py b/moto/core/base_backend.py
index b5ce8a377..10ea6a225 100644
--- a/moto/core/base_backend.py
+++ b/moto/core/base_backend.py
@@ -5,7 +5,7 @@ from functools import lru_cache
 from threading import RLock
 from typing import Any, List, Dict, Optional, ClassVar, TypeVar, Iterator
 from uuid import uuid4
-from moto.settings import allow_unknown_region
+from moto.settings import allow_unknown_region, enable_iso_regions
 from .model_instances import model_data
 from .utils import convert_regex_to_flask_path
 
@@ -56,13 +56,16 @@ class BaseBackend:
         for url_base in url_bases:
             # The default URL_base will look like: http://service.[..].amazonaws.com/...
             # This extension ensures support for the China & ISO regions
-            alt_dns_suffixes = {
-                "cn": "amazonaws.com.cn",
-                "iso": "c2s.ic.gov",
-                "isob": "sc2s.sgov.gov",
-                "isoe": "cloud.adc-e.uk",
-                "isof": "csp.hci.ic.gov",
-            }
+            alt_dns_suffixes = {"cn": "amazonaws.com.cn"}
+            if enable_iso_regions():
+                alt_dns_suffixes.update(
+                    {
+                        "iso": "c2s.ic.gov",
+                        "isob": "sc2s.sgov.gov",
+                        "isoe": "cloud.adc-e.uk",
+                        "isof": "csp.hci.ic.gov",
+                    }
+                )
 
             for url_path, handler in unformatted_paths.items():
                 url = url_path.format(url_base)
diff --git a/moto/core/custom_responses_mock.py b/moto/core/custom_responses_mock.py
index d7c5107ee..b9ff346f1 100644
--- a/moto/core/custom_responses_mock.py
+++ b/moto/core/custom_responses_mock.py
@@ -23,6 +23,11 @@ class CallbackResponse(responses.CallbackResponse):
     Need to subclass so we can change a couple things
     """
 
+    def __eq__(self, other: Any) -> bool:
+        if isinstance(other, CallbackResponse):
+            return self.method == other.method and self.url.pattern == other.url.pattern  # type: ignore
+        return super().__eq__(other)
+
     def get_response(self, request: Any) -> responses.HTTPResponse:
         """
         Need to override this so we can pass decode_content=False
diff --git a/moto/core/responses_custom_registry.py b/moto/core/responses_custom_registry.py
index 30f800b51..f21a8bda4 100644
--- a/moto/core/responses_custom_registry.py
+++ b/moto/core/responses_custom_registry.py
@@ -1,6 +1,7 @@
 # This will only exist in responses >= 0.17
 import responses
-from typing import Any, List, Tuple, Optional
+from collections import defaultdict
+from typing import Any, Dict, List, Tuple, Optional
 from .custom_responses_mock import CallbackResponse, not_implemented_callback
 
 
@@ -9,15 +10,25 @@ class CustomRegistry(responses.registries.FirstMatchRegistry):
     Custom Registry that returns requests in an order that makes sense for Moto:
      - Implemented callbacks take precedence over non-implemented-callbacks
      - CallbackResponses are not discarded after first use - users can mock the same URL as often as they like
+     - CallbackResponses are persisted in a dictionary, with the request-method as key
+       This reduces the number of possible responses that we need to search
     """
 
+    def __init__(self) -> None:
+        self._registered: Dict[str, List[responses.BaseResponse]] = defaultdict(list)
+
     def add(self, response: responses.BaseResponse) -> responses.BaseResponse:
-        if response not in self.registered:
-            super().add(response)
+        if response not in self._registered[response.method]:
+            self._registered[response.method].append(response)
         return response
 
+    def reset(self) -> None:
+        self._registered.clear()
+
     def find(self, request: Any) -> Tuple[Optional[responses.BaseResponse], List[str]]:
-        all_possibles = responses._default_mock._registry.registered + self.registered
+        all_possibles = responses._default_mock._registry.registered
+        # We don't have to search through all possible methods - only the ones registered for this particular method
+        all_possibles.extend(self._registered[request.method])
         found = []
         match_failed_reasons = []
         for response in all_possibles:
diff --git a/moto/ec2/models/security_groups.py b/moto/ec2/models/security_groups.py
index 1e458e751..0e3c91665 100644
--- a/moto/ec2/models/security_groups.py
+++ b/moto/ec2/models/security_groups.py
@@ -550,16 +550,19 @@ class SecurityGroupBackend:
 
     def describe_security_group_rules(
         self, group_ids: Optional[List[str]] = None, filters: Any = None
-    ) -> List[SecurityRule]:
+    ) -> Dict[str, List[SecurityRule]]:
         matches = self.describe_security_groups(group_ids=group_ids, filters=filters)
         if not matches:
             raise InvalidSecurityGroupNotFoundError(
                 "No security groups found matching the filters provided."
             )
-        rules = []
+        rules = {}
         for group in matches:
-            rules.extend(group.ingress_rules)
-            rules.extend(group.egress_rules)
+            group_rules = []
+            group_rules.extend(group.ingress_rules)
+            group_rules.extend(group.egress_rules)
+            if group_rules:
+                rules[group.group_id] = group_rules
 
         return rules
 
diff --git a/moto/ec2/responses/security_groups.py b/moto/ec2/responses/security_groups.py
index 2742f6518..04f2f8b54 100644
--- a/moto/ec2/responses/security_groups.py
+++ b/moto/ec2/responses/security_groups.py
@@ -254,22 +254,25 @@ DESCRIBE_SECURITY_GROUP_RULES_RESPONSE = """
 <DescribeSecurityGroupRulesResponse xmlns="http://ec2.amazonaws.com/doc/2016-11-15/">
   <requestId>{{ request_id }}</requestId>
   <securityGroupRuleSet>
-    {% for rule in rules %}
-    <item>
-        {% if rule.from_port is not none %}
-        <fromPort>{{ rule.from_port }}</fromPort>
-        {% endif %}
-        {% if rule.to_port is not none %}
-          <toPort>{{ rule.to_port }}</toPort>
-        {% endif %}
-        {% if rule.ip_ranges %}
-          <cidrIpv4>{{ rule.ip_ranges[0]['CidrIp'] }}</cidrIpv4>
-        {% endif %}
-        <ipProtocol>{{ rule.ip_protocol }}</ipProtocol>
-        <groupOwnerId>{{ rule.owner_id }}</groupOwnerId>
-        <isEgress>{{ 'true' if rule.is_egress else 'false' }}</isEgress>
-        <securityGroupRuleId>{{ rule.id }}</securityGroupRuleId>
-    </item>
+    {% for group, rule_list in rules.items() %}
+        {% for rule in rule_list %}
+            <item>
+                {% if rule.from_port is not none %}
+                <fromPort>{{ rule.from_port }}</fromPort>
+                {% endif %}
+                {% if rule.to_port is not none %}
+                  <toPort>{{ rule.to_port }}</toPort>
+                {% endif %}
+                {% if rule.ip_ranges %}
+                  <cidrIpv4>{{ rule.ip_ranges[0]['CidrIp'] }}</cidrIpv4>
+                {% endif %}
+                <ipProtocol>{{ rule.ip_protocol }}</ipProtocol>
+                <groupId>{{ group }}</groupId>
+                <groupOwnerId>{{ rule.owner_id }}</groupOwnerId>
+                <isEgress>{{ 'true' if rule.is_egress else 'false' }}</isEgress>
+                <securityGroupRuleId>{{ rule.id }}</securityGroupRuleId>
+            </item>
+        {% endfor %}
     {% endfor %}
   </securityGroupRuleSet>
 </DescribeSecurityGroupRulesResponse>"""
diff --git a/moto/settings.py b/moto/settings.py
index 5ca0e6b13..2c7974879 100644
--- a/moto/settings.py
+++ b/moto/settings.py
@@ -145,3 +145,7 @@ def get_docker_host() -> str:
 
 def get_cognito_idp_user_pool_id_strategy() -> Optional[str]:
     return os.environ.get("MOTO_COGNITO_IDP_USER_POOL_ID_STRATEGY")
+
+
+def enable_iso_regions() -> bool:
+    return os.environ.get("MOTO_ENABLE_ISO_REGIONS", "false").lower() == "true"
diff --git a/moto/ssm/models.py b/moto/ssm/models.py
index f3c8734b0..d88deb808 100644
--- a/moto/ssm/models.py
+++ b/moto/ssm/models.py
@@ -975,6 +975,65 @@ class FakeMaintenanceWindowTarget:
         return str(random.uuid4())
 
 
+class FakeMaintenanceWindowTask:
+    def __init__(
+        self,
+        window_id: str,
+        targets: Optional[List[Dict[str, Any]]],
+        task_arn: str,
+        service_role_arn: Optional[str],
+        task_type: str,
+        task_parameters: Optional[Dict[str, Any]],
+        task_invocation_parameters: Optional[Dict[str, Any]],
+        priority: Optional[int],
+        max_concurrency: Optional[str],
+        max_errors: Optional[str],
+        logging_info: Optional[Dict[str, Any]],
+        name: Optional[str],
+        description: Optional[str],
+        cutoff_behavior: Optional[str],
+        alarm_configurations: Optional[Dict[str, Any]],
+    ):
+        self.window_task_id = FakeMaintenanceWindowTask.generate_id()
+        self.window_id = window_id
+        self.task_type = task_type
+        self.task_arn = task_arn
+        self.service_role_arn = service_role_arn
+        self.task_parameters = task_parameters
+        self.priority = priority
+        self.max_concurrency = max_concurrency
+        self.max_errors = max_errors
+        self.logging_info = logging_info
+        self.name = name
+        self.description = description
+        self.targets = targets
+        self.task_invocation_parameters = task_invocation_parameters
+        self.cutoff_behavior = cutoff_behavior
+        self.alarm_configurations = alarm_configurations
+
+    def to_json(self) -> Dict[str, Any]:
+        return {
+            "WindowId": self.window_id,
+            "WindowTaskId": self.window_task_id,
+            "TaskType": self.task_type,
+            "TaskArn": self.task_arn,
+            "ServiceRoleArn": self.service_role_arn,
+            "TaskParameters": self.task_parameters,
+            "Priority": self.priority,
+            "MaxConcurrency": self.max_concurrency,
+            "MaxErrors": self.max_errors,
+            "LoggingInfo": self.logging_info,
+            "Name": self.name,
+            "Description": self.description,
+            "Targets": self.targets,
+            "TaskInvocationParameters": self.task_invocation_parameters,
+        }
+
+    @staticmethod
+    def generate_id() -> str:
+        return str(random.uuid4())
+
+
 def _maintenance_window_target_filter_match(
     filters: Optional[List[Dict[str, Any]]], target: FakeMaintenanceWindowTarget
 ) -> bool:
@@ -983,6 +1042,14 @@ def _maintenance_window_target_filter_match(
     return False
 
 
+def _maintenance_window_task_filter_match(
+    filters: Optional[List[Dict[str, Any]]], task: FakeMaintenanceWindowTask
+) -> bool:
+    if not filters and task:
+        return True
+    return False
+
+
 class FakeMaintenanceWindow:
     def __init__(
         self,
@@ -1007,6 +1074,7 @@ class FakeMaintenanceWindow:
         self.start_date = start_date
         self.end_date = end_date
         self.targets: Dict[str, FakeMaintenanceWindowTarget] = {}
+        self.tasks: Dict[str, FakeMaintenanceWindowTask] = {}
 
     def to_json(self) -> Dict[str, Any]:
         return {
@@ -2351,5 +2419,62 @@ class SimpleSystemManagerBackend(BaseBackend):
         ]
         return targets
 
+    def register_task_with_maintenance_window(
+        self,
+        window_id: str,
+        targets: Optional[List[Dict[str, Any]]],
+        task_arn: str,
+        service_role_arn: Optional[str],
+        task_type: str,
+        task_parameters: Optional[Dict[str, Any]],
+        task_invocation_parameters: Optional[Dict[str, Any]],
+        priority: Optional[int],
+        max_concurrency: Optional[str],
+        max_errors: Optional[str],
+        logging_info: Optional[Dict[str, Any]],
+        name: Optional[str],
+        description: Optional[str],
+        cutoff_behavior: Optional[str],
+        alarm_configurations: Optional[Dict[str, Any]],
+    ) -> str:
+
+        window = self.get_maintenance_window(window_id)
+        task = FakeMaintenanceWindowTask(
+            window_id,
+            targets,
+            task_arn,
+            service_role_arn,
+            task_type,
+            task_parameters,
+            task_invocation_parameters,
+            priority,
+            max_concurrency,
+            max_errors,
+            logging_info,
+            name,
+            description,
+            cutoff_behavior,
+            alarm_configurations,
+        )
+        window.tasks[task.window_task_id] = task
+        return task.window_task_id
+
+    def describe_maintenance_window_tasks(
+        self, window_id: str, filters: List[Dict[str, Any]]
+    ) -> List[FakeMaintenanceWindowTask]:
+        window = self.get_maintenance_window(window_id)
+        tasks = [
+            task
+            for task in window.tasks.values()
+            if _maintenance_window_task_filter_match(filters, task)
+        ]
+        return tasks
+
+    def deregister_task_from_maintenance_window(
+        self, window_id: str, window_task_id: str
+    ) -> None:
+        window = self.get_maintenance_window(window_id)
+        del window.tasks[window_task_id]
+
 
 ssm_backends = BackendDict(SimpleSystemManagerBackend, "ssm")
diff --git a/moto/ssm/responses.py b/moto/ssm/responses.py
index 3f757f03d..e488b6193 100644
--- a/moto/ssm/responses.py
+++ b/moto/ssm/responses.py
@@ -513,3 +513,42 @@ class SimpleSystemManagerResponse(BaseResponse):
         baseline_id = self._get_param("BaselineId")
         self.ssm_backend.delete_patch_baseline(baseline_id)
         return "{}"
+
+    def register_task_with_maintenance_window(self) -> str:
+        window_task_id = self.ssm_backend.register_task_with_maintenance_window(
+            window_id=self._get_param("WindowId"),
+            targets=self._get_param("Targets"),
+            task_arn=self._get_param("TaskArn"),
+            service_role_arn=self._get_param("ServiceRoleArn"),
+            task_type=self._get_param("TaskType"),
+            task_parameters=self._get_param("TaskParameters"),
+            task_invocation_parameters=self._get_param("TaskInvocationParameters"),
+            priority=self._get_param("Priority"),
+            max_concurrency=self._get_param("MaxConcurrency"),
+            max_errors=self._get_param("MaxErrors"),
+            logging_info=self._get_param("LoggingInfo"),
+            name=self._get_param("Name"),
+            description=self._get_param("Description"),
+            cutoff_behavior=self._get_param("CutoffBehavior"),
+            alarm_configurations=self._get_param("AlarmConfigurations"),
+        )
+        return json.dumps({"WindowTaskId": window_task_id})
+
+    def describe_maintenance_window_tasks(self) -> str:
+        window_id = self._get_param("WindowId")
+        filters = self._get_param("Filters", [])
+        tasks = [
+            task.to_json()
+            for task in self.ssm_backend.describe_maintenance_window_tasks(
+                window_id, filters
+            )
+        ]
+        return json.dumps({"Tasks": tasks})
+
+    def deregister_task_from_maintenance_window(self) -> str:
+        window_id = self._get_param("WindowId")
+        window_task_id = self._get_param("WindowTaskId")
+        self.ssm_backend.deregister_task_from_maintenance_window(
+            window_id, window_task_id
+        )
+        return "{}"


Test patch:
diff --git a/tests/terraformtests/terraform-tests.success.txt b/tests/terraformtests/terraform-tests.success.txt
index bd41fd494..4a5430eb5 100644
--- a/tests/terraformtests/terraform-tests.success.txt
+++ b/tests/terraformtests/terraform-tests.success.txt
@@ -30,6 +30,9 @@ apigatewayv2:
   - TestAccAPIGatewayV2IntegrationResponse
   - TestAccAPIGatewayV2Model
   - TestAccAPIGatewayV2Route
+  - TestAccAPIGatewayV2Stage_basicHTTP
+  - TestAccAPIGatewayV2Stage_basicWebSocket
+  - TestAccAPIGatewayV2Stage_disappears
   - TestAccAPIGatewayV2VPCLink
 appconfig:
   - TestAccAppConfigConfigurationProfileDataSource_basic
diff --git a/tests/test_apigatewayv2/test_apigatewayv2_stages.py b/tests/test_apigatewayv2/test_apigatewayv2_stages.py
new file mode 100644
index 000000000..3dfe47332
--- /dev/null
+++ b/tests/test_apigatewayv2/test_apigatewayv2_stages.py
@@ -0,0 +1,95 @@
+import boto3
+import pytest
+from botocore.exceptions import ClientError
+from moto import mock_apigatewayv2
+
+
+@mock_apigatewayv2
+def test_create_stage__defaults():
+    client = boto3.client("apigatewayv2", "us-east-2")
+
+    api_id = client.create_api(Name="test-api", ProtocolType="HTTP")["ApiId"]
+
+    stages = client.get_stages(ApiId=api_id)
+    assert stages["Items"] == []
+
+    stage = client.create_stage(ApiId=api_id, StageName="my_stage")
+    assert stage["StageName"] == "my_stage"
+
+    resp = client.get_stage(ApiId=api_id, StageName="my_stage")
+    assert resp["StageName"] == "my_stage"
+
+    stages = client.get_stages(ApiId=api_id)
+    assert len(stages["Items"]) == 1
+
+    assert stages["Items"][0]["StageName"] == "my_stage"
+    assert stages["Items"][0]["DefaultRouteSettings"] == {
+        "DetailedMetricsEnabled": False
+    }
+    assert stages["Items"][0]["CreatedDate"]
+    assert stages["Items"][0]["LastUpdatedDate"]
+    assert stages["Items"][0]["RouteSettings"] == {}
+    assert stages["Items"][0]["StageVariables"] == {}
+    assert stages["Items"][0]["Tags"] == {}
+
+    client.delete_stage(ApiId=api_id, StageName="my_stage")
+
+    with pytest.raises(ClientError) as exc:
+        client.get_stage(ApiId=api_id, StageName="my_stage")
+    err = exc.value.response["Error"]
+    assert err["Code"] == "NotFoundException"
+    assert err["Message"] == "Invalid stage identifier specified"
+
+
+@mock_apigatewayv2
+def test_create_stage__defaults_for_websocket_api():
+    client = boto3.client("apigatewayv2", "us-east-2")
+
+    api_id = client.create_api(Name="test-api", ProtocolType="WEBSOCKET")["ApiId"]
+
+    client.create_stage(ApiId=api_id, StageName="my_stage")
+
+    stage = client.get_stage(ApiId=api_id, StageName="my_stage")
+    assert stage["StageName"] == "my_stage"
+    assert stage["DefaultRouteSettings"] == {
+        "DataTraceEnabled": False,
+        "DetailedMetricsEnabled": False,
+        "LoggingLevel": "OFF",
+    }
+
+
+@mock_apigatewayv2
+def test_create_stage():
+    client = boto3.client("apigatewayv2", "us-east-2")
+
+    api_id = client.create_api(Name="test-api", ProtocolType="HTTP")["ApiId"]
+
+    stage = client.create_stage(
+        ApiId=api_id,
+        StageName="my_stage",
+        AccessLogSettings={"Format": "JSON"},
+        AutoDeploy=True,
+        ClientCertificateId="ccid",
+        DefaultRouteSettings={"LoggingLevel": "INFO", "ThrottlingBurstLimit": 9000},
+        Description="my shiny stage",
+        RouteSettings={
+            "route1": {"DataTraceEnabled": True},
+        },
+        StageVariables={"sv1": "val1"},
+        Tags={"k1": "v1"},
+    )
+
+    assert stage["StageName"] == "my_stage"
+    assert stage["AccessLogSettings"] == {"Format": "JSON"}
+    assert stage["AutoDeploy"] is True
+    assert stage["ClientCertificateId"] == "ccid"
+    assert stage["DefaultRouteSettings"] == {
+        "LoggingLevel": "INFO",
+        "ThrottlingBurstLimit": 9000,
+    }
+    assert stage["Description"] == "my shiny stage"
+    assert stage["CreatedDate"]
+    assert stage["LastUpdatedDate"]
+    assert stage["RouteSettings"] == {"route1": {"DataTraceEnabled": True}}
+    assert stage["StageVariables"] == {"sv1": "val1"}
+    assert stage["Tags"] == {"k1": "v1"}
diff --git a/tests/test_awslambda/test_lambda.py b/tests/test_awslambda/test_lambda.py
index e6a1a9e56..0a52ebafc 100644
--- a/tests/test_awslambda/test_lambda.py
+++ b/tests/test_awslambda/test_lambda.py
@@ -1,7 +1,7 @@
 import base64
 import json
 import os
-from unittest import SkipTest
+from unittest import SkipTest, mock
 import boto3
 import hashlib
 import pytest
@@ -25,6 +25,7 @@ _lambda_region = "us-west-2"
 boto3.setup_default_session(region_name=_lambda_region)
 
 
+@mock.patch.dict("os.environ", {"MOTO_ENABLE_ISO_REGIONS": "true"})
 @pytest.mark.parametrize("region", ["us-west-2", "cn-northwest-1", "us-isob-east-1"])
 @mock_lambda
 def test_lambda_regions(region):
@@ -957,6 +958,107 @@ def test_list_versions_by_function():
     )
 
 
+@mock_lambda
+@mock_s3
+def test_list_aliases():
+    bucket_name = str(uuid4())
+    s3_conn = boto3.client("s3", _lambda_region)
+    s3_conn.create_bucket(
+        Bucket=bucket_name,
+        CreateBucketConfiguration={"LocationConstraint": _lambda_region},
+    )
+
+    zip_content = get_test_zip_file2()
+    s3_conn.put_object(Bucket=bucket_name, Key="test.zip", Body=zip_content)
+    conn = boto3.client("lambda", _lambda_region)
+    function_name = str(uuid4())[0:6]
+    function_name2 = str(uuid4())[0:6]
+
+    conn.create_function(
+        FunctionName=function_name,
+        Runtime="python2.7",
+        Role=get_role_name(),
+        Handler="lambda_function.lambda_handler",
+        Code={"S3Bucket": bucket_name, "S3Key": "test.zip"},
+        Description="test lambda function",
+        Timeout=3,
+        MemorySize=128,
+        Publish=True,
+    )
+
+    conn.create_function(
+        FunctionName=function_name2,
+        Runtime="python2.7",
+        Role=get_role_name(),
+        Handler="lambda_function.lambda_handler",
+        Code={"S3Bucket": bucket_name, "S3Key": "test.zip"},
+        Description="test lambda function",
+        Timeout=3,
+        MemorySize=128,
+        Publish=True,
+    )
+
+    first_version = conn.publish_version(FunctionName=function_name)["Version"]
+
+    conn.create_alias(
+        FunctionName=function_name,
+        Name="alias1",
+        FunctionVersion=first_version,
+    )
+
+    conn.update_function_code(FunctionName=function_name, ZipFile=get_test_zip_file1())
+    second_version = conn.publish_version(FunctionName=function_name)["Version"]
+
+    conn.create_alias(
+        FunctionName=function_name,
+        Name="alias2",
+        FunctionVersion=second_version,
+    )
+
+    conn.create_alias(
+        FunctionName=function_name,
+        Name="alias0",
+        FunctionVersion=second_version,
+    )
+
+    aliases = conn.list_aliases(FunctionName=function_name)
+    assert len(aliases["Aliases"]) == 3
+
+    # should be ordered by their alias name (as per SDK response)
+    assert (
+        aliases["Aliases"][0]["AliasArn"]
+        == f"arn:aws:lambda:us-west-2:{ACCOUNT_ID}:function:{function_name}:alias0"
+    )
+    assert aliases["Aliases"][0]["FunctionVersion"] == second_version
+
+    assert (
+        aliases["Aliases"][1]["AliasArn"]
+        == f"arn:aws:lambda:us-west-2:{ACCOUNT_ID}:function:{function_name}:alias1"
+    )
+    assert aliases["Aliases"][1]["FunctionVersion"] == first_version
+
+    assert (
+        aliases["Aliases"][2]["AliasArn"]
+        == f"arn:aws:lambda:us-west-2:{ACCOUNT_ID}:function:{function_name}:alias2"
+    )
+    assert aliases["Aliases"][2]["FunctionVersion"] == second_version
+
+    res = conn.publish_version(FunctionName=function_name2)
+    conn.create_alias(
+        FunctionName=function_name2,
+        Name="alias1",
+        FunctionVersion=res["Version"],
+    )
+
+    aliases = conn.list_aliases(FunctionName=function_name2)
+
+    assert len(aliases["Aliases"]) == 1
+    assert (
+        aliases["Aliases"][0]["AliasArn"]
+        == f"arn:aws:lambda:us-west-2:{ACCOUNT_ID}:function:{function_name2}:alias1"
+    )
+
+
 @mock_lambda
 @mock_s3
 def test_create_function_with_already_exists():
diff --git a/tests/test_awslambda/test_lambda_alias.py b/tests/test_awslambda/test_lambda_alias.py
index a0fd33150..00ade02f7 100644
--- a/tests/test_awslambda/test_lambda_alias.py
+++ b/tests/test_awslambda/test_lambda_alias.py
@@ -10,6 +10,7 @@ from uuid import uuid4
 from .utilities import (
     get_role_name,
     get_test_zip_file1,
+    get_test_zip_file2,
 )
 
 # See our Development Tips on writing tests for hints on how to write good tests:
@@ -150,6 +151,56 @@ def test_get_alias():
     assert "RevisionId" in resp
 
 
+@mock_lambda
+def test_aliases_are_unique_per_function():
+    client = boto3.client("lambda", region_name="us-west-1")
+    function_name = str(uuid4())[0:6]
+    function_name2 = str(uuid4())[0:6]
+
+    client.create_function(
+        FunctionName=function_name,
+        Runtime="python3.7",
+        Role=get_role_name(),
+        Handler="lambda_function.lambda_handler",
+        Code={"ZipFile": get_test_zip_file1()},
+    )
+    client.create_function(
+        FunctionName=function_name2,
+        Runtime="python3.7",
+        Role=get_role_name(),
+        Handler="lambda_function.lambda_handler",
+        Code={"ZipFile": get_test_zip_file1()},
+    )
+
+    client.create_alias(
+        FunctionName=function_name, Name="alias1", FunctionVersion="$LATEST"
+    )
+    client.create_alias(
+        FunctionName=function_name2, Name="alias1", FunctionVersion="$LATEST"
+    )
+
+    client.update_function_code(
+        FunctionName=function_name, ZipFile=get_test_zip_file2()
+    )
+    client.update_function_code(
+        FunctionName=function_name2, ZipFile=get_test_zip_file2()
+    )
+
+    res = client.publish_version(FunctionName=function_name)
+
+    with pytest.raises(ClientError) as exc:
+        client.create_alias(
+            FunctionName=function_name, Name="alias1", FunctionVersion=res["Version"]
+        )
+
+    err = exc.value.response["Error"]
+    assert err["Code"] == "ConflictException"
+    assert (
+        err["Message"]
+        == f"Alias already exists: arn:aws:lambda:us-west-1:{ACCOUNT_ID}:function:{function_name}:alias1"
+    )
+
+
 @mock_lambda
 def test_get_alias_using_function_arn():
     client = boto3.client("lambda", region_name="us-west-1")
@@ -250,10 +301,16 @@ def test_update_alias():
         FunctionName=function_name, Name="alias1", FunctionVersion="$LATEST"
     )
 
+    client.update_function_code(
+        FunctionName=function_name, ZipFile=get_test_zip_file2()
+    )
+
+    new_version = client.publish_version(FunctionName=function_name)["Version"]
+
     resp = client.update_alias(
         FunctionName=function_name,
         Name="alias1",
-        FunctionVersion="1",
+        FunctionVersion=new_version,
         Description="updated desc",
     )
 
@@ -262,11 +319,43 @@ def test_update_alias():
         == f"arn:aws:lambda:us-east-2:{ACCOUNT_ID}:function:{function_name}:alias1"
     )
     assert resp["Name"] == "alias1"
-    assert resp["FunctionVersion"] == "1"
+    assert resp["FunctionVersion"] == new_version
     assert resp["Description"] == "updated desc"
     assert "RevisionId" in resp
 
 
+@mock_lambda
+def test_update_alias_errors_if_version_doesnt_exist():
+    client = boto3.client("lambda", region_name="us-east-2")
+    function_name = str(uuid4())[0:6]
+
+    client.create_function(
+        FunctionName=function_name,
+        Runtime="python3.7",
+        Role=get_role_name(),
+        Handler="lambda_function.lambda_handler",
+        Code={"ZipFile": get_test_zip_file1()},
+    )
+
+    client.create_alias(
+        FunctionName=function_name, Name="alias1", FunctionVersion="$LATEST"
+    )
+
+    with pytest.raises(ClientError) as exc:
+        client.update_alias(
+            FunctionName=function_name,
+            Name="alias1",
+            FunctionVersion="1",
+            Description="updated desc",
+        )
+    err = exc.value.response["Error"]
+    assert err["Code"] == "ResourceNotFoundException"
+    assert (
+        err["Message"]
+        == f"Function not found: arn:aws:lambda:us-east-2:{ACCOUNT_ID}:function:{function_name}:1"
+    )
+
+
 @mock_lambda
 def test_update_alias_routingconfig():
     client = boto3.client("lambda", region_name="us-east-2")
diff --git a/tests/test_budgets/test_budgets.py b/tests/test_budgets/test_budgets.py
index 90e2f8715..a58c7093a 100644
--- a/tests/test_budgets/test_budgets.py
+++ b/tests/test_budgets/test_budgets.py
@@ -2,8 +2,6 @@ import boto3
 import pytest
 
 from botocore.exceptions import ClientError
-import sure  # noqa # pylint: disable=unused-import
-from datetime import datetime
 from moto import mock_budgets
 from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
 
@@ -20,7 +18,7 @@ def test_create_and_describe_budget_minimal_params():
             "BudgetType": "COST",
         },
     )
-    resp["ResponseMetadata"]["HTTPStatusCode"].should.equal(200)
+    assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200
 
     try:
         budget = client.describe_budget(AccountId=ACCOUNT_ID, BudgetName="testbudget")[
@@ -28,42 +26,41 @@ def test_create_and_describe_budget_minimal_params():
         ]
     except OverflowError:
         pytest.skip("This test requires 64-bit time_t")
-    budget.should.have.key("BudgetLimit")
-    budget["BudgetLimit"].should.have.key("Amount")
-    budget["BudgetLimit"]["Amount"].should.equal("10")
-    budget["BudgetLimit"].should.have.key("Unit")
-    budget["BudgetLimit"]["Unit"].should.equal("USD")
-    budget.should.have.key("BudgetName").equals("testbudget")
-    budget.should.have.key("TimeUnit").equals("DAILY")
-    budget.should.have.key("BudgetType").equals("COST")
-    budget.should.have.key("CalculatedSpend")
-    budget["CalculatedSpend"].should.have.key("ActualSpend")
-    budget["CalculatedSpend"]["ActualSpend"].should.equal(
-        {"Amount": "0", "Unit": "USD"}
-    )
-    budget["CalculatedSpend"].should.have.key("ForecastedSpend")
-    budget["CalculatedSpend"]["ForecastedSpend"].should.equal(
-        {"Amount": "0", "Unit": "USD"}
-    )
-    budget.should.have.key("CostTypes")
-    budget["CostTypes"].should.have.key("IncludeCredit").equals(True)
-    budget["CostTypes"].should.have.key("IncludeDiscount").equals(True)
-    budget["CostTypes"].should.have.key("IncludeOtherSubscription").equals(True)
-    budget["CostTypes"].should.have.key("IncludeRecurring").equals(True)
-    budget["CostTypes"].should.have.key("IncludeRefund").equals(True)
-    budget["CostTypes"].should.have.key("IncludeSubscription").equals(True)
-    budget["CostTypes"].should.have.key("IncludeSupport").equals(True)
-    budget["CostTypes"].should.have.key("IncludeTax").equals(True)
-    budget["CostTypes"].should.have.key("IncludeUpfront").equals(True)
-    budget["CostTypes"].should.have.key("UseAmortized").equals(False)
-    budget["CostTypes"].should.have.key("UseBlended").equals(False)
-    budget.should.have.key("LastUpdatedTime").should.be.a(datetime)
-    budget.should.have.key("TimePeriod")
-    budget["TimePeriod"].should.have.key("Start")
-    budget["TimePeriod"]["Start"].should.be.a(datetime)
-    budget["TimePeriod"].should.have.key("End")
-    budget["TimePeriod"]["End"].should.be.a(datetime)
-    budget.should.have.key("TimeUnit").equals("DAILY")
+    assert "BudgetLimit" in budget
+    assert "Amount" in budget["BudgetLimit"]
+    assert budget["BudgetLimit"]["Amount"] == "10"
+    assert "Unit" in budget["BudgetLimit"]
+    assert budget["BudgetLimit"]["Unit"] == "USD"
+    assert budget["BudgetName"] == "testbudget"
+    assert budget["TimeUnit"] == "DAILY"
+    assert budget["BudgetType"] == "COST"
+    assert "CalculatedSpend" in budget
+    assert "ActualSpend" in budget["CalculatedSpend"]
+    assert budget["CalculatedSpend"]["ActualSpend"] == {"Amount": "0", "Unit": "USD"}
+    assert "ForecastedSpend" in budget["CalculatedSpend"]
+    assert budget["CalculatedSpend"]["ForecastedSpend"] == {
+        "Amount": "0",
+        "Unit": "USD",
+    }
+    assert "CostTypes" in budget
+    assert budget["CostTypes"]["IncludeCredit"] is True
+    assert budget["CostTypes"]["IncludeDiscount"] is True
+    assert budget["CostTypes"]["IncludeOtherSubscription"] is True
+    assert budget["CostTypes"]["IncludeRecurring"] is True
+    assert budget["CostTypes"]["IncludeRefund"] is True
+    assert budget["CostTypes"]["IncludeSubscription"] is True
+    assert budget["CostTypes"]["IncludeSupport"] is True
+    assert budget["CostTypes"]["IncludeTax"] is True
+    assert budget["CostTypes"]["IncludeUpfront"] is True
+    assert budget["CostTypes"]["UseAmortized"] is False
+    assert budget["CostTypes"]["UseBlended"] is False
+    assert "LastUpdatedTime" in budget
+    assert "TimePeriod" in budget
+    assert "Start" in budget["TimePeriod"]
+    assert budget["TimePeriod"]["Start"]
+    assert "End" in budget["TimePeriod"]
+    assert budget["TimePeriod"]["End"]
+    assert budget["TimeUnit"] == "DAILY"
 
 
 @mock_budgets
@@ -90,10 +87,8 @@ def test_create_existing_budget():
             },
         )
     err = exc.value.response["Error"]
-    err["Code"].should.equal("DuplicateRecordException")
-    err["Message"].should.equal(
-        "Error creating budget: testb - the budget already exists."
-    )
+    assert err["Code"] == "DuplicateRecordException"
+    assert err["Message"] == "Error creating budget: testb - the budget already exists."
 
 
 @mock_budgets
@@ -105,9 +100,10 @@ def test_create_budget_without_limit_param():
             Budget={"BudgetName": "testb", "TimeUnit": "DAILY", "BudgetType": "COST"},
         )
     err = exc.value.response["Error"]
-    err["Code"].should.equal("InvalidParameterException")
-    err["Message"].should.equal(
-        "Unable to create/update budget - please provide one of the followings: Budget Limit/ Planned Budget Limit/ Auto Adjust Data"
+    assert err["Code"] == "InvalidParameterException"
+    assert (
+        err["Message"]
+        == "Unable to create/update budget - please provide one of the followings: Budget Limit/ Planned Budget Limit/ Auto Adjust Data"
     )
 
 
@@ -117,17 +113,15 @@ def test_describe_unknown_budget():
     with pytest.raises(ClientError) as exc:
         client.describe_budget(AccountId=ACCOUNT_ID, BudgetName="unknown")
     err = exc.value.response["Error"]
-    err["Code"].should.equal("NotFoundException")
-    err["Message"].should.equal(
-        "Unable to get budget: unknown - the budget doesn't exist."
-    )
+    assert err["Code"] == "NotFoundException"
+    assert err["Message"] == "Unable to get budget: unknown - the budget doesn't exist."
 
 
 @mock_budgets
 def test_describe_no_budgets():
     client = boto3.client("budgets", region_name="us-east-1")
     resp = client.describe_budgets(AccountId=ACCOUNT_ID)
-    resp.should.have.key("Budgets").equals([])
+    assert resp["Budgets"] == []
 
 
 @mock_budgets
@@ -147,7 +141,7 @@ def test_create_and_describe_all_budgets():
         res = client.describe_budgets(AccountId=ACCOUNT_ID)
     except OverflowError:
         pytest.skip("This test requires 64-bit time_t")
-    res["Budgets"].should.have.length_of(1)
+    assert len(res["Budgets"]) == 1
 
 
 @mock_budgets
@@ -164,10 +158,10 @@ def test_delete_budget():
     )
 
     resp = client.delete_budget(AccountId=ACCOUNT_ID, BudgetName="b1")
-    resp["ResponseMetadata"]["HTTPStatusCode"].should.equal(200)
+    assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200
 
     res = client.describe_budgets(AccountId=ACCOUNT_ID)
-    res["Budgets"].should.have.length_of(0)
+    assert len(res["Budgets"]) == 0
 
 
 @mock_budgets
@@ -176,7 +170,8 @@ def test_delete_unknown_budget():
     with pytest.raises(ClientError) as exc:
         client.delete_budget(AccountId=ACCOUNT_ID, BudgetName="unknown")
     err = exc.value.response["Error"]
-    err["Code"].should.equal("NotFoundException")
-    err["Message"].should.equal(
-        "Unable to delete budget: unknown - the budget doesn't exist. Try creating it first. "
+    assert err["Code"] == "NotFoundException"
+    assert (
+        err["Message"]
+        == "Unable to delete budget: unknown - the budget doesn't exist. Try creating it first. "
     )
diff --git a/tests/test_budgets/test_notifications.py b/tests/test_budgets/test_notifications.py
index 7e1de0747..f9a306f25 100644
--- a/tests/test_budgets/test_notifications.py
+++ b/tests/test_budgets/test_notifications.py
@@ -2,7 +2,6 @@ import boto3
 import pytest
 
 from botocore.exceptions import ClientError
-import sure  # noqa # pylint: disable=unused-import
 from moto import mock_budgets
 from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
 
@@ -37,13 +36,13 @@ def test_create_and_describe_notification():
     res = client.describe_notifications_for_budget(
         AccountId=ACCOUNT_ID, BudgetName="testbudget"
     )
-    res.should.have.key("Notifications").length_of(1)
+    assert len(res["Notifications"]) == 1
     notification = res["Notifications"][0]
-    notification.should.have.key("NotificationType").should.equal("ACTUAL")
-    notification.should.have.key("ComparisonOperator").should.equal("EQUAL_TO")
-    notification.should.have.key("Threshold").should.equal(123)
-    notification.should.have.key("ThresholdType").should.equal("ABSOLUTE_VALUE")
-    notification.should.have.key("NotificationState").should.equal("ALARM")
+    assert notification["NotificationType"] == "ACTUAL"
+    assert notification["ComparisonOperator"] == "EQUAL_TO"
+    assert notification["Threshold"] == 123
+    assert notification["ThresholdType"] == "ABSOLUTE_VALUE"
+    assert notification["NotificationState"] == "ALARM"
 
 
 @mock_budgets
@@ -85,24 +84,24 @@ def test_create_notification():
         },
         Subscribers=[{"SubscriptionType": "SNS", "Address": "arn:sns:topic:mytopic"}],
     )
-    res["ResponseMetadata"]["HTTPStatusCode"].should.equal(200)
+    assert res["ResponseMetadata"]["HTTPStatusCode"] == 200
 
     res = client.describe_notifications_for_budget(
         AccountId=ACCOUNT_ID, BudgetName="testbudget"
     )
-    res.should.have.key("Notifications").length_of(2)
+    assert len(res["Notifications"]) == 2
     n_1 = res["Notifications"][0]
-    n_1.should.have.key("NotificationType").should.equal("ACTUAL")
-    n_1.should.have.key("ComparisonOperator").should.equal("EQUAL_TO")
-    n_1.should.have.key("Threshold").should.equal(123)
-    n_1.should.have.key("ThresholdType").should.equal("ABSOLUTE_VALUE")
-    n_1.should.have.key("NotificationState").should.equal("ALARM")
+    assert n_1["NotificationType"] == "ACTUAL"
+    assert n_1["ComparisonOperator"] == "EQUAL_TO"
+    assert n_1["Threshold"] == 123
+    assert n_1["ThresholdType"] == "ABSOLUTE_VALUE"
+    assert n_1["NotificationState"] == "ALARM"
     n_2 = res["Notifications"][1]
-    n_2.should.have.key("NotificationType").should.equal("ACTUAL")
-    n_2.should.have.key("ComparisonOperator").should.equal("GREATER_THAN")
-    n_2.should.have.key("Threshold").should.equal(0)
-    n_2.should.have.key("ThresholdType").should.equal("ABSOLUTE_VALUE")
-    n_2.should.have.key("NotificationState").should.equal("OK")
+    assert n_2["NotificationType"] == "ACTUAL"
+    assert n_2["ComparisonOperator"] == "GREATER_THAN"
+    assert n_2["Threshold"] == 0
+    assert n_2["ThresholdType"] == "ABSOLUTE_VALUE"
+    assert n_2["NotificationState"] == "OK"
 
 
 @mock_budgets
@@ -123,10 +122,8 @@ def test_create_notification_unknown_budget():
             Subscribers=[{"SubscriptionType": "EMAIL", "Address": "admin@moto.com"}],
         )
     err = exc.value.response["Error"]
-    err["Code"].should.equal("NotFoundException")
-    err["Message"].should.equal(
-        "Unable to create notification - the budget doesn't exist."
-    )
+    assert err["Code"] == "NotFoundException"
+    assert err["Message"] == "Unable to create notification - the budget doesn't exist."
 
 
 @mock_budgets
@@ -171,7 +168,7 @@ def test_delete_notification():
     res = client.describe_notifications_for_budget(
         AccountId=ACCOUNT_ID, BudgetName="testbudget"
     )
-    res.should.have.key("Notifications").length_of(0)
+    assert len(res["Notifications"]) == 0
 
 
 @mock_budgets
@@ -191,7 +188,5 @@ def test_delete_notification_unknown_budget():
             },
         )
     err = exc.value.response["Error"]
-    err["Code"].should.equal("NotFoundException")
-    err["Message"].should.equal(
-        "Unable to delete notification - the budget doesn't exist."
-    )
+    assert err["Code"] == "NotFoundException"
+    assert err["Message"] == "Unable to delete notification - the budget doesn't exist."
diff --git a/tests/test_budgets/test_server.py b/tests/test_budgets/test_server.py
index 3bfec7297..3f7333939 100644
--- a/tests/test_budgets/test_server.py
+++ b/tests/test_budgets/test_server.py
@@ -1,5 +1,4 @@
 import json
-import sure  # noqa # pylint: disable=unused-import
 
 import moto.server as server
 from moto import mock_budgets
@@ -12,5 +11,5 @@ def test_budgets_describe_budgets():
 
     headers = {"X-Amz-Target": "AWSBudgetServiceGateway.DescribeBudgets"}
     resp = test_client.post("/", headers=headers, json={})
-    resp.status_code.should.equal(200)
-    json.loads(resp.data).should.equal({"Budgets": [], "nextToken": None})
+    assert resp.status_code == 200
+    assert json.loads(resp.data) == {"Budgets": [], "nextToken": None}
diff --git a/tests/test_ce/test_ce.py b/tests/test_ce/test_ce.py
index ee572557e..0b3ff82e1 100644
--- a/tests/test_ce/test_ce.py
+++ b/tests/test_ce/test_ce.py
@@ -1,7 +1,6 @@
 """Unit tests for ce-supported APIs."""
 import boto3
 import pytest
-import sure  # noqa # pylint: disable=unused-import
 
 from botocore.exceptions import ClientError
 from moto import mock_ce
@@ -21,10 +20,8 @@ def test_create_cost_category_definition():
             {"Value": "v", "Rule": {"CostCategories": {"Key": "k", "Values": ["v"]}}}
         ],
     )
-    resp.should.have.key("CostCategoryArn").match(
-        f"arn:aws:ce::{ACCOUNT_ID}:costcategory/"
-    )
-    resp.should.have.key("EffectiveStart")
+    assert resp["CostCategoryArn"].startswith(f"arn:aws:ce::{ACCOUNT_ID}:costcategory/")
+    assert "EffectiveStart" in resp
 
 
 @mock_ce
@@ -38,10 +35,8 @@ def test_create_cost_category_definition_with_effective_start():
         ],
         EffectiveStart="2022-11-01T00:00:00Z",
     )
-    resp.should.have.key("CostCategoryArn").match(
-        f"arn:aws:ce::{ACCOUNT_ID}:costcategory/"
-    )
-    resp.should.have.key("EffectiveStart").equals("2022-11-01T00:00:00Z")
+    assert resp["CostCategoryArn"].startswith(f"arn:aws:ce::{ACCOUNT_ID}:costcategory/")
+    assert resp["EffectiveStart"] == "2022-11-01T00:00:00Z"
 
 
 @mock_ce
@@ -58,13 +53,14 @@ def test_describe_cost_category_definition():
     resp = client.describe_cost_category_definition(CostCategoryArn=ccd_arn)[
         "CostCategory"
     ]
-    resp.should.have.key("Name").equals("ccd")
-    resp.should.have.key("CostCategoryArn").equals(ccd_arn)
-    resp.should.have.key("RuleVersion").equals("CostCategoryExpression.v1")
-    resp.should.have.key("Rules").length_of(1)
-    resp["Rules"][0].should.equal(
-        {"Value": "v", "Rule": {"CostCategories": {"Key": "k", "Values": ["v"]}}}
-    )
+    assert resp["Name"] == "ccd"
+    assert resp["CostCategoryArn"] == ccd_arn
+    assert resp["RuleVersion"] == "CostCategoryExpression.v1"
+    assert len(resp["Rules"]) == 1
+    assert resp["Rules"][0] == {
+        "Value": "v",
+        "Rule": {"CostCategories": {"Key": "k", "Values": ["v"]}},
+    }
 
 
 @mock_ce
@@ -84,8 +80,8 @@ def test_delete_cost_category_definition():
     with pytest.raises(ClientError) as exc:
         client.describe_cost_category_definition(CostCategoryArn=ccd_arn)
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ResourceNotFoundException")
-    err["Message"].should.equal(f"No Cost Categories found with ID {ccd_id}")
+    assert err["Code"] == "ResourceNotFoundException"
+    assert err["Message"] == f"No Cost Categories found with ID {ccd_id}"
 
 
 @mock_ce
@@ -111,16 +107,19 @@ def test_update_cost_category_definition():
     resp = client.describe_cost_category_definition(CostCategoryArn=ccd_arn)[
         "CostCategory"
     ]
-    resp.should.have.key("Name").equals("ccd")
-    resp.should.have.key("CostCategoryArn").equals(ccd_arn)
-    resp.should.have.key("RuleVersion").equals("CostCategoryExpression.v1")
-
-    resp.should.have.key("Rules").length_of(1)
-    resp["Rules"][0].should.equal(
-        {"Value": "v", "Rule": {"CostCategories": {"Key": "k", "Values": ["v"]}}}
-    )
-
-    resp.should.have.key("SplitChargeRules").length_of(1)
-    resp["SplitChargeRules"][0].should.equal(
-        {"Source": "s", "Targets": ["t1"], "Method": "EVEN"}
-    )
+    assert resp["Name"] == "ccd"
+    assert resp["CostCategoryArn"] == ccd_arn
+    assert resp["RuleVersion"] == "CostCategoryExpression.v1"
+
+    assert len(resp["Rules"]) == 1
+    assert resp["Rules"][0] == {
+        "Value": "v",
+        "Rule": {"CostCategories": {"Key": "k", "Values": ["v"]}},
+    }
+
+    assert len(resp["SplitChargeRules"]) == 1
+    assert resp["SplitChargeRules"][0] == {
+        "Source": "s",
+        "Targets": ["t1"],
+        "Method": "EVEN",
+    }
diff --git a/tests/test_ce/test_ce_tags.py b/tests/test_ce/test_ce_tags.py
index a1ee5b43c..ce01b9b01 100644
--- a/tests/test_ce/test_ce_tags.py
+++ b/tests/test_ce/test_ce_tags.py
@@ -1,5 +1,4 @@
 import boto3
-import sure  # noqa # pylint: disable=unused-import
 
 from moto import mock_ce
 
@@ -19,12 +18,12 @@ def test_list_tags_if_none_exist():
     )["CostCategoryArn"]
 
     resp = client.list_tags_for_resource(ResourceArn=arn)
-    resp.should.have.key("ResourceTags").equals([])
+    assert resp["ResourceTags"] == []
 
     client.tag_resource(ResourceArn=arn, ResourceTags=[{"Key": "t1", "Value": "v1"}])
 
     resp = client.list_tags_for_resource(ResourceArn=arn)
-    resp.should.have.key("ResourceTags").equals([{"Key": "t1", "Value": "v1"}])
+    assert resp["ResourceTags"] == [{"Key": "t1", "Value": "v1"}]
 
 
 @mock_ce
@@ -40,16 +39,17 @@ def test_cost_category_tags_workflow():
     )["CostCategoryArn"]
 
     resp = client.list_tags_for_resource(ResourceArn=arn)
-    resp.should.have.key("ResourceTags").equals([{"Key": "t1", "Value": "v1"}])
+    assert resp["ResourceTags"] == [{"Key": "t1", "Value": "v1"}]
 
     client.tag_resource(ResourceArn=arn, ResourceTags=[{"Key": "t2", "Value": "v2"}])
 
     resp = client.list_tags_for_resource(ResourceArn=arn)
-    resp.should.have.key("ResourceTags").equals(
-        [{"Key": "t1", "Value": "v1"}, {"Key": "t2", "Value": "v2"}]
-    )
+    assert resp["ResourceTags"] == [
+        {"Key": "t1", "Value": "v1"},
+        {"Key": "t2", "Value": "v2"},
+    ]
 
     client.untag_resource(ResourceArn=arn, ResourceTagKeys=["t2"])
 
     resp = client.list_tags_for_resource(ResourceArn=arn)
-    resp.should.have.key("ResourceTags").equals([{"Key": "t1", "Value": "v1"}])
+    assert resp["ResourceTags"] == [{"Key": "t1", "Value": "v1"}]
diff --git a/tests/test_databrew/test_databrew_datasets.py b/tests/test_databrew/test_databrew_datasets.py
index 27f16c154..2c501f470 100644
--- a/tests/test_databrew/test_databrew_datasets.py
+++ b/tests/test_databrew/test_databrew_datasets.py
@@ -99,8 +99,8 @@ def test_dataset_list_when_empty():
     client = _create_databrew_client()
 
     response = client.list_datasets()
-    response.should.have.key("Datasets")
-    response["Datasets"].should.have.length_of(0)
+    assert "Datasets" in response
+    assert len(response["Datasets"]) == 0
 
 
 @mock_databrew
@@ -109,9 +109,9 @@ def test_list_datasets_with_max_results():
 
     _create_test_datasets(client, 4)
     response = client.list_datasets(MaxResults=2)
-    response["Datasets"].should.have.length_of(2)
-    response["Datasets"][0].should.have.key("ResourceArn")
-    response.should.have.key("NextToken")
+    assert len(response["Datasets"]) == 2
+    assert "ResourceArn" in response["Datasets"][0]
+    assert "NextToken" in response
 
 
 @mock_databrew
@@ -120,7 +120,7 @@ def test_list_datasets_from_next_token():
     _create_test_datasets(client, 10)
     first_response = client.list_datasets(MaxResults=3)
     response = client.list_datasets(NextToken=first_response["NextToken"])
-    response["Datasets"].should.have.length_of(7)
+    assert len(response["Datasets"]) == 7
 
 
 @mock_databrew
@@ -128,7 +128,7 @@ def test_list_datasets_with_max_results_greater_than_actual_results():
     client = _create_databrew_client()
     _create_test_datasets(client, 4)
     response = client.list_datasets(MaxResults=10)
-    response["Datasets"].should.have.length_of(4)
+    assert len(response["Datasets"]) == 4
 
 
 @mock_databrew
@@ -138,16 +138,17 @@ def test_describe_dataset():
     # region basic test
     response = _create_test_dataset(client)
     dataset = client.describe_dataset(Name=response["Name"])
-    dataset["Name"].should.equal(response["Name"])
-    dataset.should.have.key("ResourceArn").equal(
-        f"arn:aws:databrew:us-west-1:{ACCOUNT_ID}:dataset/{response['Name']}"
+    assert dataset["Name"] == response["Name"]
+    assert (
+        dataset["ResourceArn"]
+        == f"arn:aws:databrew:us-west-1:{ACCOUNT_ID}:dataset/{response['Name']}"
     )
     # endregion
 
     # region JSON test
     response = _create_test_dataset(client, dataset_format="CSV")
     dataset = client.describe_dataset(Name=response["Name"])
-    dataset["Format"].should.equal("CSV")
+    assert dataset["Format"] == "CSV"
     # endregion
 
 
@@ -158,8 +159,8 @@ def test_describe_dataset_that_does_not_exist():
     with pytest.raises(ClientError) as exc:
         client.describe_dataset(Name="DoseNotExist")
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ResourceNotFoundException")
-    err["Message"].should.equal("One or more resources can't be found.")
+    assert err["Code"] == "ResourceNotFoundException"
+    assert err["Message"] == "One or more resources can't be found."
 
 
 @mock_databrew
@@ -171,8 +172,8 @@ def test_create_dataset_that_already_exists():
     with pytest.raises(ClientError) as exc:
         _create_test_dataset(client, dataset_name=response["Name"])
     err = exc.value.response["Error"]
-    err["Code"].should.equal("AlreadyExistsException")
-    err["Message"].should.equal(f"{response['Name']} already exists.")
+    assert err["Code"] == "AlreadyExistsException"
+    assert err["Message"] == f"{response['Name']} already exists."
 
 
 @mock_databrew
@@ -182,7 +183,7 @@ def test_delete_dataset():
 
     # Check dataset exists
     dataset = client.describe_dataset(Name=response["Name"])
-    dataset["Name"].should.equal(response["Name"])
+    assert dataset["Name"] == response["Name"]
 
     # Delete the dataset
     client.delete_dataset(Name=response["Name"])
@@ -192,15 +193,15 @@ def test_delete_dataset():
         client.describe_dataset(Name=response["Name"])
 
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ResourceNotFoundException")
-    err["Message"].should.equal("One or more resources can't be found.")
+    assert err["Code"] == "ResourceNotFoundException"
+    assert err["Message"] == "One or more resources can't be found."
 
     # Check that a dataset that does not exist errors
     with pytest.raises(ClientError) as exc:
         client.delete_dataset(Name=response["Name"])
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ResourceNotFoundException")
-    err["Message"].should.equal("One or more resources can't be found.")
+    assert err["Code"] == "ResourceNotFoundException"
+    assert err["Message"] == "One or more resources can't be found."
 
 
 @mock_databrew
@@ -231,14 +232,15 @@ def test_update_dataset():
             },
         },
     )
-    dataset["Name"].should.equal(response["Name"])
+    assert dataset["Name"] == response["Name"]
 
     # Describe the dataset and check the changes
     dataset = client.describe_dataset(Name=response["Name"])
-    dataset["Name"].should.equal(response["Name"])
-    dataset["Format"].should.equal("TEST")
-    dataset.should.have.key("ResourceArn").equal(
-        f"arn:aws:databrew:us-west-1:{ACCOUNT_ID}:dataset/{response['Name']}"
+    assert dataset["Name"] == response["Name"]
+    assert dataset["Format"] == "TEST"
+    assert (
+        dataset["ResourceArn"]
+        == f"arn:aws:databrew:us-west-1:{ACCOUNT_ID}:dataset/{response['Name']}"
     )
 
 
@@ -272,5 +274,5 @@ def test_update_dataset_that_does_not_exist():
         )
 
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ResourceNotFoundException")
-    err["Message"].should.equal("One or more resources can't be found.")
+    assert err["Code"] == "ResourceNotFoundException"
+    assert err["Message"] == "One or more resources can't be found."
diff --git a/tests/test_databrew/test_databrew_jobs.py b/tests/test_databrew/test_databrew_jobs.py
index d559fe23c..e28744e97 100644
--- a/tests/test_databrew/test_databrew_jobs.py
+++ b/tests/test_databrew/test_databrew_jobs.py
@@ -78,9 +78,9 @@ def test_create_profile_job_that_already_exists():
     with pytest.raises(ClientError) as exc:
         _create_test_profile_job(client, job_name=response["Name"])
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ConflictException")
-    err["Message"].should.equal(f"The job {job_name} profile job already exists.")
-    exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(409)
+    assert err["Code"] == "ConflictException"
+    assert err["Message"] == f"The job {job_name} profile job already exists."
+    assert exc.value.response["ResponseMetadata"]["HTTPStatusCode"] == 409
 
 
 @mock_databrew
@@ -92,9 +92,9 @@ def test_create_recipe_job_that_already_exists():
     with pytest.raises(ClientError) as exc:
         _create_test_recipe_job(client, job_name=response["Name"])
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ConflictException")
-    err["Message"].should.equal(f"The job {job_name} recipe job already exists.")
-    exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(409)
+    assert err["Code"] == "ConflictException"
+    assert err["Message"] == f"The job {job_name} recipe job already exists."
+    assert exc.value.response["ResponseMetadata"]["HTTPStatusCode"] == 409
 
 
 @mock_databrew
@@ -104,12 +104,12 @@ def test_create_recipe_job_with_invalid_encryption_mode():
     with pytest.raises(ClientError) as exc:
         _create_test_recipe_job(client, encryption_mode="INVALID")
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ValidationException")
-    err["Message"].should.equal(
-        "1 validation error detected: Value 'INVALID' at 'encryptionMode' failed to satisfy constraint: "
-        "Member must satisfy enum value set: [SSE-S3, SSE-KMS]"
+    assert err["Code"] == "ValidationException"
+    assert (
+        err["Message"]
+        == "1 validation error detected: Value 'INVALID' at 'encryptionMode' failed to satisfy constraint: Member must satisfy enum value set: [SSE-S3, SSE-KMS]"
     )
-    exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
+    assert exc.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400
 
 
 @mock_databrew
@@ -119,12 +119,12 @@ def test_create_recipe_job_with_invalid_log_subscription_value():
     with pytest.raises(ClientError) as exc:
         _create_test_recipe_job(client, log_subscription="INVALID")
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ValidationException")
-    err["Message"].should.equal(
-        "1 validation error detected: Value 'INVALID' at 'logSubscription' failed to satisfy constraint: "
-        "Member must satisfy enum value set: [ENABLE, DISABLE]"
+    assert err["Code"] == "ValidationException"
+    assert (
+        err["Message"]
+        == "1 validation error detected: Value 'INVALID' at 'logSubscription' failed to satisfy constraint: Member must satisfy enum value set: [ENABLE, DISABLE]"
     )
-    exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
+    assert exc.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400
 
 
 @mock_databrew
@@ -136,9 +136,9 @@ def test_create_recipe_job_with_same_name_as_profile_job():
     with pytest.raises(ClientError) as exc:
         _create_test_recipe_job(client, job_name=response["Name"])
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ConflictException")
-    err["Message"].should.equal(f"The job {job_name} profile job already exists.")
-    exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(409)
+    assert err["Code"] == "ConflictException"
+    assert err["Message"] == f"The job {job_name} profile job already exists."
+    assert exc.value.response["ResponseMetadata"]["HTTPStatusCode"] == 409
 
 
 @mock_databrew
@@ -148,12 +148,12 @@ def test_describe_recipe_job():
     response = _create_test_recipe_job(client)
     job_name = response["Name"]
     job = client.describe_job(Name=job_name)
-    job.should.have.key("Name").equal(response["Name"])
-    job.should.have.key("Type").equal("RECIPE")
-    job.should.have.key("ResourceArn").equal(
-        f"arn:aws:databrew:us-west-1:{ACCOUNT_ID}:job/{job_name}"
+    assert job["Name"] == response["Name"]
+    assert job["Type"] == "RECIPE"
+    assert (
+        job["ResourceArn"] == f"arn:aws:databrew:us-west-1:{ACCOUNT_ID}:job/{job_name}"
     )
-    job["ResponseMetadata"]["HTTPStatusCode"].should.equal(200)
+    assert job["ResponseMetadata"]["HTTPStatusCode"] == 200
 
 
 @mock_databrew
@@ -163,9 +163,9 @@ def test_describe_job_that_does_not_exist():
     with pytest.raises(ClientError) as exc:
         client.describe_job(Name="DoesNotExist")
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ResourceNotFoundException")
-    err["Message"].should.equal("Job DoesNotExist wasn't found.")
-    exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(404)
+    assert err["Code"] == "ResourceNotFoundException"
+    assert err["Message"] == "Job DoesNotExist wasn't found."
+    assert exc.value.response["ResponseMetadata"]["HTTPStatusCode"] == 404
 
 
 @mock_databrew
@@ -175,12 +175,12 @@ def test_describe_job_with_long_name():
     with pytest.raises(ClientError) as exc:
         client.describe_job(Name=name)
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ValidationException")
-    err["Message"].should.equal(
-        f"1 validation error detected: Value '{name}' at 'name' failed to satisfy constraint: "
-        f"Member must have length less than or equal to 240"
+    assert err["Code"] == "ValidationException"
+    assert (
+        err["Message"]
+        == f"1 validation error detected: Value '{name}' at 'name' failed to satisfy constraint: Member must have length less than or equal to 240"
     )
-    exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
+    assert exc.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400
 
 
 @mock_databrew
@@ -195,13 +195,13 @@ def test_update_profile_job():
     update_response = client.update_profile_job(
         Name=job_name, RoleArn="a" * 20, OutputLocation={"Bucket": "b" * 20}
     )
-    update_response.should.have.key("Name").equals(job_name)
-    update_response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200)
+    assert update_response["Name"] == job_name
+    assert update_response["ResponseMetadata"]["HTTPStatusCode"] == 200
 
     # Describe the job to check that RoleArn was updated
     job = client.describe_job(Name=job_name)
-    job.should.have.key("Name").equal(response["Name"])
-    job.should.have.key("RoleArn").equal("a" * 20)
+    assert job["Name"] == response["Name"]
+    assert job["RoleArn"] == "a" * 20
 
 
 @mock_databrew
@@ -214,13 +214,13 @@ def test_update_recipe_job():
 
     # Update the job by changing RoleArn
     update_response = client.update_recipe_job(Name=job_name, RoleArn="a" * 20)
-    update_response.should.have.key("Name").equals(job_name)
-    update_response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200)
+    assert update_response["Name"] == job_name
+    assert update_response["ResponseMetadata"]["HTTPStatusCode"] == 200
 
     # Describe the job to check that RoleArn was updated
     job = client.describe_job(Name=job_name)
-    job.should.have.key("Name").equal(response["Name"])
-    job.should.have.key("RoleArn").equal("a" * 20)
+    assert job["Name"] == response["Name"]
+    assert job["RoleArn"] == "a" * 20
 
 
 @mock_databrew
@@ -232,9 +232,9 @@ def test_update_profile_job_does_not_exist():
             Name="DoesNotExist", RoleArn="a" * 20, OutputLocation={"Bucket": "b" * 20}
         )
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ResourceNotFoundException")
-    err["Message"].should.equal("The job DoesNotExist wasn't found")
-    exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(404)
+    assert err["Code"] == "ResourceNotFoundException"
+    assert err["Message"] == "The job DoesNotExist wasn't found"
+    assert exc.value.response["ResponseMetadata"]["HTTPStatusCode"] == 404
 
 
 @mock_databrew
@@ -244,9 +244,9 @@ def test_update_recipe_job_does_not_exist():
     with pytest.raises(ClientError) as exc:
         client.update_recipe_job(Name="DoesNotExist", RoleArn="a" * 20)
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ResourceNotFoundException")
-    err["Message"].should.equal("The job DoesNotExist wasn't found")
-    exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(404)
+    assert err["Code"] == "ResourceNotFoundException"
+    assert err["Message"] == "The job DoesNotExist wasn't found"
+    assert exc.value.response["ResponseMetadata"]["HTTPStatusCode"] == 404
 
 
 @mock_databrew
@@ -259,16 +259,16 @@ def test_delete_job():
 
     # Delete the job
     response = client.delete_job(Name=job_name)
-    response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200)
-    response["Name"].should.equal(job_name)
+    assert response["ResponseMetadata"]["HTTPStatusCode"] == 200
+    assert response["Name"] == job_name
 
     # Check the job does not exist anymore
     with pytest.raises(ClientError) as exc:
         client.describe_job(Name=job_name)
 
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ResourceNotFoundException")
-    err["Message"].should.equal(f"Job {job_name} wasn't found.")
+    assert err["Code"] == "ResourceNotFoundException"
+    assert err["Message"] == f"Job {job_name} wasn't found."
 
 
 @mock_databrew
@@ -278,11 +278,11 @@ def test_delete_job_does_not_exist():
     # Delete the job
     with pytest.raises(ClientError) as exc:
         client.delete_job(Name="DoesNotExist")
-    exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(404)
+    assert exc.value.response["ResponseMetadata"]["HTTPStatusCode"] == 404
 
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ResourceNotFoundException")
-    err["Message"].should.equal("The job DoesNotExist wasn't found.")
+    assert err["Code"] == "ResourceNotFoundException"
+    assert err["Message"] == "The job DoesNotExist wasn't found."
 
 
 @mock_databrew
@@ -292,12 +292,12 @@ def test_delete_job_with_long_name():
     with pytest.raises(ClientError) as exc:
         client.delete_job(Name=name)
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ValidationException")
-    err["Message"].should.equal(
-        f"1 validation error detected: Value '{name}' at 'name' failed to satisfy constraint: "
-        f"Member must have length less than or equal to 240"
+    assert err["Code"] == "ValidationException"
+    assert (
+        err["Message"]
+        == f"1 validation error detected: Value '{name}' at 'name' failed to satisfy constraint: Member must have length less than or equal to 240"
     )
-    exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
+    assert exc.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400
 
 
 @mock_databrew
@@ -305,9 +305,8 @@ def test_job_list_when_empty():
     client = _create_databrew_client()
 
     response = client.list_jobs()
-    response.should.have.key("Jobs")
-    response["Jobs"].should.have.length_of(0)
-    response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200)
+    assert len(response["Jobs"]) == 0
+    assert response["ResponseMetadata"]["HTTPStatusCode"] == 200
 
 
 @mock_databrew
@@ -316,9 +315,9 @@ def test_list_jobs_with_max_results():
 
     _create_test_recipe_jobs(client, 4)
     response = client.list_jobs(MaxResults=2)
-    response["Jobs"].should.have.length_of(2)
-    response.should.have.key("NextToken")
-    response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200)
+    assert len(response["Jobs"]) == 2
+    assert "NextToken" in response
+    assert response["ResponseMetadata"]["HTTPStatusCode"] == 200
 
 
 @mock_databrew
@@ -327,8 +326,8 @@ def test_list_jobs_from_next_token():
     _create_test_recipe_jobs(client, 10)
     first_response = client.list_jobs(MaxResults=3)
     response = client.list_jobs(NextToken=first_response["NextToken"])
-    response["Jobs"].should.have.length_of(7)
-    response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200)
+    assert len(response["Jobs"]) == 7
+    assert response["ResponseMetadata"]["HTTPStatusCode"] == 200
 
 
 @mock_databrew
@@ -336,8 +335,8 @@ def test_list_jobs_with_max_results_greater_than_actual_results():
     client = _create_databrew_client()
     _create_test_recipe_jobs(client, 4)
     response = client.list_jobs(MaxResults=10)
-    response["Jobs"].should.have.length_of(4)
-    response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200)
+    assert len(response["Jobs"]) == 4
+    assert response["ResponseMetadata"]["HTTPStatusCode"] == 200
 
 
 @mock_databrew
@@ -347,8 +346,8 @@ def test_list_jobs_recipe_and_profile():
     _create_test_recipe_jobs(client, 4)
     _create_test_profile_jobs(client, 2)
     response = client.list_jobs()
-    response["Jobs"].should.have.length_of(6)
-    response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200)
+    assert len(response["Jobs"]) == 6
+    assert response["ResponseMetadata"]["HTTPStatusCode"] == 200
 
 
 @mock_databrew
@@ -361,8 +360,8 @@ def test_list_jobs_dataset_name_filter():
     _create_test_profile_jobs(client, 1)
 
     response = client.list_jobs(DatasetName="TEST")
-    response["Jobs"].should.have.length_of(7)
-    response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200)
+    assert len(response["Jobs"]) == 7
+    assert response["ResponseMetadata"]["HTTPStatusCode"] == 200
 
 
 @mock_databrew
@@ -374,8 +373,8 @@ def test_list_jobs_project_name_filter():
     _create_test_profile_jobs(client, 1)
 
     response = client.list_jobs(ProjectName="TEST_PROJECT")
-    response["Jobs"].should.have.length_of(3)
-    response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200)
+    assert len(response["Jobs"]) == 3
+    assert response["ResponseMetadata"]["HTTPStatusCode"] == 200
 
 
 @mock_databrew
@@ -391,5 +390,5 @@ def test_list_jobs_dataset_name_and_project_name_filter():
     _create_test_profile_jobs(client, 1)
 
     response = client.list_jobs(DatasetName="TEST", ProjectName="TEST_PROJECT")
-    response["Jobs"].should.have.length_of(10)
-    response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200)
+    assert len(response["Jobs"]) == 10
+    assert response["ResponseMetadata"]["HTTPStatusCode"] == 200
diff --git a/tests/test_databrew/test_databrew_recipes.py b/tests/test_databrew/test_databrew_recipes.py
index 60e486cc8..8f9233635 100644
--- a/tests/test_databrew/test_databrew_recipes.py
+++ b/tests/test_databrew/test_databrew_recipes.py
@@ -55,8 +55,8 @@ def test_recipe_list_when_empty():
     client = _create_databrew_client()
 
     response = client.list_recipes()
-    response.should.have.key("Recipes")
-    response["Recipes"].should.have.length_of(0)
+    assert "Recipes" in response
+    assert len(response["Recipes"]) == 0
 
 
 @mock_databrew
@@ -67,14 +67,14 @@ def test_recipe_list_with_invalid_version():
     with pytest.raises(ClientError) as exc:
         client.list_recipes(RecipeVersion=recipe_version)
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ValidationException")
-    err["Message"].should.equal(
-        f"Invalid version {recipe_version}. "
-        "Valid versions are LATEST_PUBLISHED and LATEST_WORKING."
+    assert err["Code"] == "ValidationException"
+    assert (
+        err["Message"]
+        == f"Invalid version {recipe_version}. Valid versions are LATEST_PUBLISHED and LATEST_WORKING."
     )
-    exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
+    assert exc.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ValidationException")
+    assert err["Code"] == "ValidationException"
 
 
 @mock_databrew
@@ -83,8 +83,8 @@ def test_list_recipes_with_max_results():
 
     _create_test_recipes(client, 4)
     response = client.list_recipes(MaxResults=2, RecipeVersion="LATEST_WORKING")
-    response["Recipes"].should.have.length_of(2)
-    response.should.have.key("NextToken")
+    assert len(response["Recipes"]) == 2
+    assert "NextToken" in response
 
 
 @mock_databrew
@@ -95,7 +95,7 @@ def test_list_recipes_from_next_token():
     response = client.list_recipes(
         NextToken=first_response["NextToken"], RecipeVersion="LATEST_WORKING"
     )
-    response["Recipes"].should.have.length_of(7)
+    assert len(response["Recipes"]) == 7
 
 
 @mock_databrew
@@ -103,7 +103,7 @@ def test_list_recipes_with_max_results_greater_than_actual_results():
     client = _create_databrew_client()
     _create_test_recipes(client, 4)
     response = client.list_recipes(MaxResults=10, RecipeVersion="LATEST_WORKING")
-    response["Recipes"].should.have.length_of(4)
+    assert len(response["Recipes"]) == 4
 
 
 @mock_databrew
@@ -111,7 +111,7 @@ def test_list_recipe_versions_no_recipe():
     client = _create_databrew_client()
     recipe_name = "NotExist"
     response = client.list_recipe_versions(Name=recipe_name)
-    response["Recipes"].should.have.length_of(0)
+    assert len(response["Recipes"]) == 0
 
 
 @mock_databrew
@@ -120,7 +120,7 @@ def test_list_recipe_versions_none_published():
     response = _create_test_recipe(client)
     recipe_name = response["Name"]
     response = client.list_recipe_versions(Name=recipe_name)
-    response["Recipes"].should.have.length_of(0)
+    assert len(response["Recipes"]) == 0
 
 
 @mock_databrew
@@ -130,8 +130,8 @@ def test_list_recipe_versions_one_published():
     recipe_name = response["Name"]
     client.publish_recipe(Name=recipe_name)
     response = client.list_recipe_versions(Name=recipe_name)
-    response["Recipes"].should.have.length_of(1)
-    response["Recipes"][0]["RecipeVersion"].should.equal("1.0")
+    assert len(response["Recipes"]) == 1
+    assert response["Recipes"][0]["RecipeVersion"] == "1.0"
 
 
 @mock_databrew
@@ -142,9 +142,9 @@ def test_list_recipe_versions_two_published():
     client.publish_recipe(Name=recipe_name)
     client.publish_recipe(Name=recipe_name)
     response = client.list_recipe_versions(Name=recipe_name)
-    response["Recipes"].should.have.length_of(2)
-    response["Recipes"][0]["RecipeVersion"].should.equal("1.0")
-    response["Recipes"][1]["RecipeVersion"].should.equal("2.0")
+    assert len(response["Recipes"]) == 2
+    assert response["Recipes"][0]["RecipeVersion"] == "1.0"
+    assert response["Recipes"][1]["RecipeVersion"] == "2.0"
 
 
 @mock_databrew
@@ -156,9 +156,9 @@ def test_describe_recipe_latest_working():
         Name=response["Name"], RecipeVersion="LATEST_WORKING"
     )
 
-    recipe["Name"].should.equal(response["Name"])
-    recipe["Steps"].should.have.length_of(1)
-    recipe["RecipeVersion"].should.equal("0.1")
+    assert recipe["Name"] == response["Name"]
+    assert len(recipe["Steps"]) == 1
+    assert recipe["RecipeVersion"] == "0.1"
 
 
 @mock_databrew
@@ -168,9 +168,9 @@ def test_describe_recipe_with_version():
 
     recipe = client.describe_recipe(Name=response["Name"], RecipeVersion="0.1")
 
-    recipe["Name"].should.equal(response["Name"])
-    recipe["Steps"].should.have.length_of(1)
-    recipe["RecipeVersion"].should.equal("0.1")
+    assert recipe["Name"] == response["Name"]
+    assert len(recipe["Steps"]) == 1
+    assert recipe["RecipeVersion"] == "0.1"
 
 
 @mock_databrew
@@ -183,9 +183,9 @@ def test_describe_recipe_latest_published():
         Name=response["Name"], RecipeVersion="LATEST_PUBLISHED"
     )
 
-    recipe["Name"].should.equal(response["Name"])
-    recipe["Steps"].should.have.length_of(1)
-    recipe["RecipeVersion"].should.equal("1.0")
+    assert recipe["Name"] == response["Name"]
+    assert len(recipe["Steps"]) == 1
+    assert recipe["RecipeVersion"] == "1.0"
 
 
 @mock_databrew
@@ -196,9 +196,9 @@ def test_describe_recipe_implicit_latest_published():
     client.publish_recipe(Name=response["Name"])
     recipe = client.describe_recipe(Name=response["Name"])
 
-    recipe["Name"].should.equal(response["Name"])
-    recipe["Steps"].should.have.length_of(1)
-    recipe["RecipeVersion"].should.equal("1.0")
+    assert recipe["Name"] == response["Name"]
+    assert len(recipe["Steps"]) == 1
+    assert recipe["RecipeVersion"] == "1.0"
 
 
 @mock_databrew
@@ -208,11 +208,12 @@ def test_describe_recipe_that_does_not_exist():
     with pytest.raises(ClientError) as exc:
         client.describe_recipe(Name="DoseNotExist")
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ResourceNotFoundException")
-    err["Message"].should.equal(
-        "The recipe DoseNotExist for version LATEST_PUBLISHED wasn't found."
+    assert err["Code"] == "ResourceNotFoundException"
+    assert (
+        err["Message"]
+        == "The recipe DoseNotExist for version LATEST_PUBLISHED wasn't found."
     )
-    exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(404)
+    assert exc.value.response["ResponseMetadata"]["HTTPStatusCode"] == 404
 
 
 @mock_databrew
@@ -222,12 +223,12 @@ def test_describe_recipe_with_long_name():
     with pytest.raises(ClientError) as exc:
         client.describe_recipe(Name=name)
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ValidationException")
-    err["Message"].should.equal(
-        f"1 validation error detected: Value '{name}' at 'name' failed to satisfy constraint: "
-        f"Member must have length less than or equal to 255"
+    assert err["Code"] == "ValidationException"
+    assert (
+        err["Message"]
+        == f"1 validation error detected: Value '{name}' at 'name' failed to satisfy constraint: Member must have length less than or equal to 255"
     )
-    exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
+    assert exc.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400
 
 
 @mock_databrew
@@ -237,12 +238,12 @@ def test_describe_recipe_with_long_version():
     with pytest.raises(ClientError) as exc:
         client.describe_recipe(Name="AnyName", RecipeVersion=version)
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ValidationException")
-    err["Message"].should.equal(
-        f"1 validation error detected: Value '{version}' at 'recipeVersion' failed to satisfy constraint: "
-        f"Member must have length less than or equal to 16"
+    assert err["Code"] == "ValidationException"
+    assert (
+        err["Message"]
+        == f"1 validation error detected: Value '{version}' at 'recipeVersion' failed to satisfy constraint: Member must have length less than or equal to 16"
     )
-    exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
+    assert exc.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400
 
 
 @mock_databrew
@@ -253,9 +254,9 @@ def test_describe_recipe_with_invalid_version():
     with pytest.raises(ClientError) as exc:
         client.describe_recipe(Name=name, RecipeVersion=version)
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ValidationException")
-    err["Message"].should.equal(f"Recipe {name} version {version} isn't valid.")
-    exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
+    assert err["Code"] == "ValidationException"
+    assert err["Message"] == f"Recipe {name} version {version} isn't valid."
+    assert exc.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400
 
 
 @mock_databrew
@@ -289,15 +290,15 @@ def test_update_recipe():
         ],
     )
 
-    recipe["Name"].should.equal(response["Name"])
+    assert recipe["Name"] == response["Name"]
 
     # Describe the recipe and check the changes
     recipe = client.describe_recipe(
         Name=response["Name"], RecipeVersion="LATEST_WORKING"
     )
-    recipe["Name"].should.equal(response["Name"])
-    recipe["Steps"].should.have.length_of(1)
-    recipe["Steps"][0]["Action"]["Parameters"]["removeCustomValue"].should.equal("true")
+    assert recipe["Name"] == response["Name"]
+    assert len(recipe["Steps"]) == 1
+    assert recipe["Steps"][0]["Action"]["Parameters"]["removeCustomValue"] == "true"
 
 
 @mock_databrew
@@ -310,14 +311,14 @@ def test_update_recipe_description():
         Name=response["Name"], Steps=[], Description=description
     )
 
-    recipe["Name"].should.equal(response["Name"])
+    assert recipe["Name"] == response["Name"]
 
     # Describe the recipe and check the changes
     recipe = client.describe_recipe(
         Name=response["Name"], RecipeVersion="LATEST_WORKING"
     )
-    recipe["Name"].should.equal(response["Name"])
-    recipe["Description"].should.equal(description)
+    assert recipe["Name"] == response["Name"]
+    assert recipe["Description"] == description
 
 
 @mock_databrew
@@ -328,9 +329,9 @@ def test_update_recipe_invalid():
     with pytest.raises(ClientError) as exc:
         client.update_recipe(Name=recipe_name)
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ResourceNotFoundException")
-    err["Message"].should.equal(f"The recipe {recipe_name} wasn't found")
-    exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(404)
+    assert err["Code"] == "ResourceNotFoundException"
+    assert err["Message"] == f"The recipe {recipe_name} wasn't found"
+    assert exc.value.response["ResponseMetadata"]["HTTPStatusCode"] == 404
 
 
 @mock_databrew
@@ -342,9 +343,9 @@ def test_create_recipe_that_already_exists():
     with pytest.raises(ClientError) as exc:
         _create_test_recipe(client, recipe_name=response["Name"])
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ConflictException")
-    err["Message"].should.equal(f"The recipe {recipe_name} already exists")
-    exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(409)
+    assert err["Code"] == "ConflictException"
+    assert err["Message"] == f"The recipe {recipe_name} already exists"
+    assert exc.value.response["ResponseMetadata"]["HTTPStatusCode"] == 409
 
 
 @mock_databrew
@@ -358,29 +359,29 @@ def test_publish_recipe():
     with pytest.raises(ClientError) as exc:
         recipe = client.describe_recipe(Name=recipe_name)
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ResourceNotFoundException")
+    assert err["Code"] == "ResourceNotFoundException"
 
     dt_before_publish = datetime.now().astimezone()
 
     # Publish the recipe
     publish_response = client.publish_recipe(Name=recipe_name, Description="1st desc")
-    publish_response["Name"].should.equal(recipe_name)
+    assert publish_response["Name"] == recipe_name
 
     # Recipe is now published, so check we can retrieve the published version
     recipe = client.describe_recipe(Name=recipe_name)
-    recipe["Description"].should.equal("1st desc")
-    recipe["RecipeVersion"].should.equal("1.0")
-    recipe["PublishedDate"].should.be.greater_than(dt_before_publish)
+    assert recipe["Description"] == "1st desc"
+    assert recipe["RecipeVersion"] == "1.0"
+    assert recipe["PublishedDate"] > dt_before_publish
     first_published_date = recipe["PublishedDate"]
 
     # Publish the recipe a 2nd time
     publish_response = client.publish_recipe(Name=recipe_name, Description="2nd desc")
-    publish_response["Name"].should.equal(recipe_name)
+    assert publish_response["Name"] == recipe_name
 
     recipe = client.describe_recipe(Name=recipe_name)
-    recipe["Description"].should.equal("2nd desc")
-    recipe["RecipeVersion"].should.equal("2.0")
-    recipe["PublishedDate"].should.be.greater_than(first_published_date)
+    assert recipe["Description"] == "2nd desc"
+    assert recipe["RecipeVersion"] == "2.0"
+    assert recipe["PublishedDate"] > first_published_date
 
 
 @mock_databrew
@@ -389,8 +390,8 @@ def test_publish_recipe_that_does_not_exist():
     with pytest.raises(ClientError) as exc:
         client.publish_recipe(Name="DoesNotExist")
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ResourceNotFoundException")
-    exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(404)
+    assert err["Code"] == "ResourceNotFoundException"
+    assert exc.value.response["ResponseMetadata"]["HTTPStatusCode"] == 404
 
 
 @mock_databrew
@@ -400,14 +401,14 @@ def test_publish_long_recipe_name():
     with pytest.raises(ClientError) as exc:
         client.publish_recipe(Name=name)
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ValidationException")
-    err["Message"].should.equal(
-        f"1 validation error detected: Value '{name}' at 'name' failed to satisfy constraint: "
-        f"Member must have length less than or equal to 255"
+    assert err["Code"] == "ValidationException"
+    assert (
+        err["Message"]
+        == f"1 validation error detected: Value '{name}' at 'name' failed to satisfy constraint: Member must have length less than or equal to 255"
     )
-    exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
+    assert exc.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ValidationException")
+    assert err["Code"] == "ValidationException"
 
 
 @mock_databrew
@@ -419,8 +420,8 @@ def test_delete_recipe_version():
     with pytest.raises(ClientError) as exc:
         client.describe_recipe(Name=recipe_name)
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ResourceNotFoundException")
-    exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(404)
+    assert err["Code"] == "ResourceNotFoundException"
+    assert exc.value.response["ResponseMetadata"]["HTTPStatusCode"] == 404
 
 
 @mock_databrew
@@ -433,10 +434,10 @@ def test_delete_recipe_version_published():
     with pytest.raises(ClientError) as exc:
         client.describe_recipe(Name=recipe_name)
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ResourceNotFoundException")
-    exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(404)
+    assert err["Code"] == "ResourceNotFoundException"
+    assert exc.value.response["ResponseMetadata"]["HTTPStatusCode"] == 404
     recipe = client.describe_recipe(Name=recipe_name, RecipeVersion="1.1")
-    recipe["RecipeVersion"].should.equal("1.1")
+    assert recipe["RecipeVersion"] == "1.1"
 
 
 @mock_databrew
@@ -448,11 +449,11 @@ def test_delete_recipe_version_latest_working_after_publish():
     with pytest.raises(ClientError) as exc:
         client.delete_recipe_version(Name=recipe_name, RecipeVersion="LATEST_WORKING")
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ValidationException")
-    err["Message"].should.equal(
-        "Recipe version LATEST_WORKING is not allowed to be deleted"
+    assert err["Code"] == "ValidationException"
+    assert (
+        err["Message"] == "Recipe version LATEST_WORKING is not allowed to be deleted"
     )
-    exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
+    assert exc.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400
 
 
 @mock_databrew
@@ -464,9 +465,9 @@ def test_delete_recipe_version_latest_working_numeric_after_publish():
     with pytest.raises(ClientError) as exc:
         client.delete_recipe_version(Name=recipe_name, RecipeVersion="1.1")
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ValidationException")
-    err["Message"].should.equal("Recipe version 1.1 is not allowed to be deleted")
-    exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
+    assert err["Code"] == "ValidationException"
+    assert err["Message"] == "Recipe version 1.1 is not allowed to be deleted"
+    assert exc.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400
 
 
 @mock_databrew
@@ -479,11 +480,11 @@ def test_delete_recipe_version_invalid_version_string():
     with pytest.raises(ClientError) as exc:
         client.delete_recipe_version(Name=recipe_name, RecipeVersion=recipe_version)
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ValidationException")
-    err["Message"].should.equal(
-        f"Recipe {recipe_name} version {recipe_version} is invalid."
+    assert err["Code"] == "ValidationException"
+    assert (
+        err["Message"] == f"Recipe {recipe_name} version {recipe_version} is invalid."
     )
-    exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
+    assert exc.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400
 
 
 @mock_databrew
@@ -496,11 +497,11 @@ def test_delete_recipe_version_invalid_version_length():
     with pytest.raises(ClientError) as exc:
         client.delete_recipe_version(Name=recipe_name, RecipeVersion=recipe_version)
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ValidationException")
-    err["Message"].should.equal(
-        f"Recipe {recipe_name} version {recipe_version} is invalid."
+    assert err["Code"] == "ValidationException"
+    assert (
+        err["Message"] == f"Recipe {recipe_name} version {recipe_version} is invalid."
     )
-    exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(400)
+    assert exc.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400
 
 
 @mock_databrew
@@ -510,9 +511,9 @@ def test_delete_recipe_version_unknown_recipe():
     with pytest.raises(ClientError) as exc:
         client.delete_recipe_version(Name=recipe_name, RecipeVersion="1.1")
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ResourceNotFoundException")
-    err["Message"].should.equal(f"The recipe {recipe_name} wasn't found")
-    exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(404)
+    assert err["Code"] == "ResourceNotFoundException"
+    assert err["Message"] == f"The recipe {recipe_name} wasn't found"
+    assert exc.value.response["ResponseMetadata"]["HTTPStatusCode"] == 404
 
 
 @mock_databrew
@@ -524,8 +525,9 @@ def test_delete_recipe_version_unknown_version():
     with pytest.raises(ClientError) as exc:
         client.delete_recipe_version(Name=recipe_name, RecipeVersion=recipe_version)
     err = exc.value.response["Error"]
-    err["Code"].should.equal("ResourceNotFoundException")
-    err["Message"].should.equal(
-        f"The recipe {recipe_name} version {recipe_version} wasn't found."
+    assert err["Code"] == "ResourceNotFoundException"
+    assert (
+        err["Message"]
+        == f"The recipe {recipe_name} version {recipe_version} wasn't found."
     )
-    exc.value.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(404)
+    assert exc.value.response["ResponseMetadata"]["HTTPStatusCode"] == 404
diff --git a/tests/test_databrew/test_databrew_rulesets.py b/tests/test_databrew/test_databrew_rulesets.py
index a59a38c01..70ba06a36 100644
--- a/tests/test_databrew/test_databrew_rulesets.py
+++ b/tests/test_databrew/test_databrew_rulesets.py
@@ -8,8 +8,7 @@ from moto import mock_databrew
 
 
 def _create_databrew_client():
-    client = boto3.client("databrew", region_name="us-west-1")
-    return client
+    return boto3.client("databrew", region_name="us-west-1")
 
 
 def _create_test_ruleset(client, tags=None, ruleset_name=None):
@@ -46,8 +45,8 @@ def test_ruleset_list_when_empty():
     client = _create_databrew_client()
 
     response = client.list_rulesets()
-    response.should.have.key("Rulesets")
-    response["Rulesets"].should.have.length_of(0)
+    assert "Rulesets" in response
+    assert len(response["Rulesets"]) == 0
 
 
 @mock_databrew
@@ -56,8 +55,8 @@ def test_list_ruleset_with_max_results():
 
     _create_test_rulesets(client, 4)
     response = client.list_rulesets(MaxResults=2)
-    response["Rulesets"].should.have.length_of(2)
-    response.should.have.key("NextToken")
+    assert len(response["Rulesets"]) == 2
+    assert "NextToken" in response
 
 
 @mock_databrew
@@ -66,7 +65,7 @@ def test_list_rulesets_from_next_token():
     _create_test_rulesets(client, 10)
     first_response = client.list_rulesets(MaxResults=3)
     response = client.list_rulesets(NextToken=first_response["NextToken"])
-    response["Rulesets"].should.have.length_of(7)
+    assert len(response["Rulesets"]) == 7
 
 
 @mock_databrew
@@ -74,7 +73,7 @@ def test_list_rulesets_with_max_results_greater_than_actual_results():
     client = _create_databrew_client()
     _create_test_rulesets(client, 4)
     response = client.list_rulesets(MaxResults=10)
-    response["Rulesets"].should.have.length_of(4)
+    assert len(response["Rulesets"]) == 4
 
 
 @mock_databrew
@@ -84,9 +83,9 @@ def test_describe_ruleset():
 
     ruleset = client.describe_ruleset(Name=response["Name"])
 
-    ruleset["Name"].should.equal(response["Name"])
-    ruleset["Rules"].should.have.length_of(1)
-    response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200)
+    assert ruleset["Name"] == response["Name"]
+    assert len(ruleset["Rules"]) == 1
+    assert response["ResponseMetadata"]["HTTPStatusCode"] == 200
 
 
 @mock_databrew
@@ -96,8 +95,8 @@ def test_describe_ruleset_that_does_not_exist():
     with pytest.raises(ClientError) as exc:
         client.describe_ruleset(Name="DoseNotExist")
     err = exc.value.response["Error"]
-    err["Code"].should.equal("EntityNotFoundException")
-    err["Message"].should.equal("Ruleset DoseNotExist not found.")
+    assert err["Code"] == "EntityNotFoundException"
+    assert err["Message"] == "Ruleset DoseNotExist not found."
 
 
 @mock_databrew
@@ -109,8 +108,8 @@ def test_create_ruleset_that_already_exists():
     with pytest.raises(ClientError) as exc:
         _create_test_ruleset(client, ruleset_name=response["Name"])
     err = exc.value.response["Error"]
-    err["Code"].should.equal("AlreadyExistsException")
-    err["Message"].should.equal("Ruleset already exists.")
+    assert err["Code"] == "AlreadyExistsException"
+    assert err["Message"] == "Ruleset already exists."
 
 
 @mock_databrew
@@ -121,27 +120,27 @@ def test_delete_ruleset():
 
     # Check ruleset exists
     ruleset = client.describe_ruleset(Name=ruleset_name)
-    ruleset["Name"].should.equal(response["Name"])
+    assert ruleset["Name"] == response["Name"]
 
     # Delete the ruleset
     response = client.delete_ruleset(Name=ruleset_name)
-    response["ResponseMetadata"]["HTTPStatusCode"].should.equal(200)
-    response["Name"].should.equal(ruleset_name)
+    assert response["ResponseMetadata"]["HTTPStatusCode"] == 200
+    assert response["Name"] == ruleset_name
 
     # Check it does not exist anymore
     with pytest.raises(ClientError) as exc:
         client.describe_ruleset(Name=ruleset_name)
 
     err = exc.value.response["Error"]
-    err["Code"].should.equal("EntityNotFoundException")
-    err["Message"].should.equal(f"Ruleset {ruleset_name} not found.")
+    assert err["Code"] == "EntityNotFoundException"
+    assert err["Message"] == f"Ruleset {ruleset_name} not found."
 
     # Check that a ruleset that does not exist errors
     with pytest.raises(ClientError) as exc:
         client.delete_ruleset(Name=ruleset_name)
     err = exc.value.response["Error"]
-    err["Code"].should.equal("EntityNotFoundException")
-    err["Message"].should.equal(f"Ruleset {ruleset_name} not found.")
+    assert err["Code"] == "EntityNotFoundException"
+    assert err["Message"] == f"Ruleset {ruleset_name} not found."
 
 
 @mock_databrew
@@ -166,10 +165,10 @@ def test_update_ruleset():
             }
         ],
     )
-    ruleset["Name"].should.equal(response["Name"])
+    assert ruleset["Name"] == response["Name"]
 
     # Describe the ruleset and check the changes
     ruleset = client.describe_ruleset(Name=response["Name"])
-    ruleset["Name"].should.equal(response["Name"])
-    ruleset["Rules"].should.have.length_of(1)
-    ruleset["Rules"][0]["SubstitutionMap"][":val1"].should.equal("10")
+    assert ruleset["Name"] == response["Name"]
+    assert len(ruleset["Rules"]) == 1
+    assert ruleset["Rules"][0]["SubstitutionMap"][":val1"] == "10"
diff --git a/tests/test_ec2/test_security_groups.py b/tests/test_ec2/test_security_groups.py
index c7cc6dc87..70d859d8e 100644
--- a/tests/test_ec2/test_security_groups.py
+++ b/tests/test_ec2/test_security_groups.py
@@ -586,6 +586,7 @@ def test_create_and_describe_security_grp_rule():
     assert rules[0]["IsEgress"] is True
     assert rules[0]["IpProtocol"] == "-1"
     assert rules[0]["CidrIpv4"] == "0.0.0.0/0"
+    assert "GroupId" in rules[0]
 
 
 @mock_ec2
diff --git a/tests/test_ssm/test_ssm_maintenance_windows.py b/tests/test_ssm/test_ssm_maintenance_windows.py
index f484a4252..9d0e2ca26 100644
--- a/tests/test_ssm/test_ssm_maintenance_windows.py
+++ b/tests/test_ssm/test_ssm_maintenance_windows.py
@@ -274,3 +274,111 @@ def test_deregister_target_from_maintenance_window():
         WindowId=window_id,
     )
     resp.should.have.key("Targets").should.have.length_of(0)
+
+
+@mock_ssm
+def test_describe_maintenance_window_with_no_task_or_targets():
+    ssm = boto3.client("ssm", region_name="us-east-1")
+
+    resp = ssm.create_maintenance_window(
+        Name="simple-window",
+        Schedule="cron(15 12 * * ? *)",
+        Duration=2,
+        Cutoff=1,
+        AllowUnassociatedTargets=False,
+    )
+    window_id = resp["WindowId"]
+
+    resp = ssm.describe_maintenance_window_tasks(
+        WindowId=window_id,
+    )
+    resp.should.have.key("Tasks").should.have.length_of(0)
+
+    resp = ssm.describe_maintenance_window_targets(
+        WindowId=window_id,
+    )
+    resp.should.have.key("Targets").should.have.length_of(0)
+
+
+@mock_ssm
+def test_register_maintenance_window_task():
+    ssm = boto3.client("ssm", region_name="us-east-1")
+
+    resp = ssm.create_maintenance_window(
+        Name="simple-window",
+        Schedule="cron(15 12 * * ? *)",
+        Duration=2,
+        Cutoff=1,
+        AllowUnassociatedTargets=False,
+    )
+    window_id = resp["WindowId"]
+
+    resp = ssm.register_target_with_maintenance_window(
+        WindowId=window_id,
+        ResourceType="INSTANCE",
+        Targets=[{"Key": "tag:Name", "Values": ["my-instance"]}],
+    )
+    window_target_id = resp["WindowTargetId"]
+
+    resp = ssm.register_task_with_maintenance_window(
+        WindowId=window_id,
+        Targets=[{"Key": "WindowTargetIds", "Values": [window_target_id]}],
+        TaskArn="AWS-RunShellScript",
+        TaskType="RUN_COMMAND",
+        MaxConcurrency="1",
+        MaxErrors="1",
+    )
+
+    resp.should.have.key("WindowTaskId")
+    _id = resp["WindowTaskId"]
+
+    resp = ssm.describe_maintenance_window_tasks(
+        WindowId=window_id,
+    )
+    resp.should.have.key("Tasks").should.have.length_of(1)
+    resp["Tasks"][0].should.have.key("WindowTaskId").equal(_id)
+    resp["Tasks"][0].should.have.key("WindowId").equal(window_id)
+    resp["Tasks"][0].should.have.key("TaskArn").equal("AWS-RunShellScript")
+    resp["Tasks"][0].should.have.key("MaxConcurrency").equal("1")
+    resp["Tasks"][0].should.have.key("MaxErrors").equal("1")
+
+
+@mock_ssm
+def test_deregister_maintenance_window_task():
+    ssm = boto3.client("ssm", region_name="us-east-1")
+
+    resp = ssm.create_maintenance_window(
+        Name="simple-window",
+        Schedule="cron(15 12 * * ? *)",
+        Duration=2,
+        Cutoff=1,
+        AllowUnassociatedTargets=False,
+    )
+    window_id = resp["WindowId"]
+
+    resp = ssm.register_target_with_maintenance_window(
+        WindowId=window_id,
+        ResourceType="INSTANCE",
+        Targets=[{"Key": "tag:Name", "Values": ["my-instance"]}],
+    )
+    window_target_id = resp["WindowTargetId"]
+
+    resp = ssm.register_task_with_maintenance_window(
+        WindowId=window_id,
+        Targets=[{"Key": "WindowTargetIds", "Values": [window_target_id]}],
+        TaskArn="AWS-RunShellScript",
+        TaskType="RUN_COMMAND",
+        MaxConcurrency="1",
+        MaxErrors="1",
+    )
+    window_task_id = resp["WindowTaskId"]
+
+    ssm.deregister_task_from_maintenance_window(
+        WindowId=window_id,
+        WindowTaskId=window_task_id,
+    )
+
+    resp = ssm.describe_maintenance_window_tasks(
+        WindowId=window_id,
+    )
+    resp.should.have.key("Tasks").should.have.length_of(0)
diff --git a/tests/test_stepfunctions/test_stepfunctions.py b/tests/test_stepfunctions/test_stepfunctions.py
index 70d8faa11..b383701bc 100644
--- a/tests/test_stepfunctions/test_stepfunctions.py
+++ b/tests/test_stepfunctions/test_stepfunctions.py
@@ -888,6 +888,7 @@ def test_state_machine_get_execution_history_contains_expected_success_events_wh
     execution_history["events"].should.equal(expected_events)
 
 
+@mock.patch.dict("os.environ", {"MOTO_ENABLE_ISO_REGIONS": "true"})
 @pytest.mark.parametrize(
     "test_region", ["us-west-2", "cn-northwest-1", "us-isob-east-1"]
 )
diff --git a/tests/test_sts/test_sts.py b/tests/test_sts/test_sts.py
index 417089cd5..19ca35b4b 100644
--- a/tests/test_sts/test_sts.py
+++ b/tests/test_sts/test_sts.py
@@ -5,6 +5,7 @@ import boto3
 from botocore.client import ClientError
 from datetime import datetime
 from freezegun import freeze_time
+from unittest.mock import patch
 import pytest
 import sure  # noqa # pylint: disable=unused-import
 
@@ -707,6 +708,7 @@ def test_federation_token_with_too_long_policy():
     )
 
 
+@patch.dict("os.environ", {"MOTO_ENABLE_ISO_REGIONS": "true"})
 @pytest.mark.parametrize("region", ["us-west-2", "cn-northwest-1", "us-isob-east-1"])
 @mock_sts
 def test_sts_regions(region):