Repository: getmoto/moto
Pull request #8122 (2024-09-13 10:38:21)

Firehose: Add Snowflake Destination
So far we cannot use moto to test Firehose Service with the Snowflake destination

Example:
```python
@pytest.fixture(autouse=True, scope="class")
    def firehose_client_inst(self):
        with mock_aws():
            firehose_client = boto3.client("firehose", region_name="eu-west-1")

            for firehose_stream_name in FIREHOSE_STREAM_NAMES:
                firehose_client.create_delivery_stream(
                    DeliveryStreamName=firehose_stream_name,
                    SnowflakeDestinationConfiguration={
                        "RoleARN": f"arn:aws:iam::{DEFAULT_ACCOUNT_ID}:role/firehose_delivery_role",
                        "AccountUrl": "fake-account.eu-west-1.snowflakecomputing.com",
                        "Database": "myDatabase",
                        "Schema": "mySchema",
                        "Table": "myTable",
                        "S3BackupMode": "FailedDataOnly",
                        "S3Configuration": {
                            "RoleARN": f"arn:aws:iam::{DEFAULT_ACCOUNT_ID}:role/firehose_delivery_role",
                            "BucketARN": "arn:aws:s3:::kinesis-test"
                        }
                    }
                )

            yield firehose_client
```

Output error:
```
botocore.errorfactory.InvalidArgumentException: An error occurred (InvalidArgumentException) when calling the CreateDeliveryStream operation: Exactly one destination configuration is supported for a Firehose
```

Tests OK on my project if I modify Moto lib locally.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/8122?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 94.47%. Comparing base [(`45114d2`)](https://app.codecov.io/gh/getmoto/moto/commit/45114d259699ec67079eb6bc144f54c4726b30d2?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) to head [(`1c05032`)](https://app.codecov.io/gh/getmoto/moto/commit/1c050327905007ee4ab0f9b4f7c8cfb3a6e7c794?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).

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


```diff
@@           Coverage Diff           @@
##           master    #8122   +/-   ##
=======================================
  Coverage   94.47%   94.47%           
=======================================
  Files        1141     1141           
  Lines       98313    98313           
=======================================
  Hits        92881    92881           
  Misses       5432     5432           
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/8122/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Coverage Δ | |
|---|---|---|
| [servertests](https://app.codecov.io/gh/getmoto/moto/pull/8122/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `28.94% <ø> (ø)` | |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/8122/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `94.44% <ø> (ø)` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/getmoto/moto/pull/8122?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).

sp-bdanies: Not sure if I can do something to fix the failing check :/ 

Patch:
diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index 8b0aedcf9..d5b35d291 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -296,7 +296,7 @@
 
 ## appconfig
 <details>
-<summary>33% implemented</summary>
+<summary>34% implemented</summary>
 
 - [X] create_application
 - [X] create_configuration_profile
@@ -312,7 +312,6 @@
 - [ ] delete_extension
 - [ ] delete_extension_association
 - [X] delete_hosted_configuration_version
-- [ ] get_account_settings
 - [X] get_application
 - [ ] get_configuration
 - [X] get_configuration_profile
@@ -335,7 +334,6 @@
 - [ ] stop_deployment
 - [X] tag_resource
 - [X] untag_resource
-- [ ] update_account_settings
 - [X] update_application
 - [X] update_configuration_profile
 - [ ] update_deployment_strategy
@@ -364,50 +362,6 @@
 - [ ] untag_resource
 </details>
 
-## appmesh
-<details>
-<summary>57% implemented</summary>
-
-- [ ] create_gateway_route
-- [X] create_mesh
-- [X] create_route
-- [ ] create_virtual_gateway
-- [X] create_virtual_node
-- [X] create_virtual_router
-- [ ] create_virtual_service
-- [ ] delete_gateway_route
-- [X] delete_mesh
-- [X] delete_route
-- [ ] delete_virtual_gateway
-- [X] delete_virtual_node
-- [X] delete_virtual_router
-- [ ] delete_virtual_service
-- [ ] describe_gateway_route
-- [X] describe_mesh
-- [X] describe_route
-- [ ] describe_virtual_gateway
-- [X] describe_virtual_node
-- [X] describe_virtual_router
-- [ ] describe_virtual_service
-- [ ] list_gateway_routes
-- [X] list_meshes
-- [X] list_routes
-- [X] list_tags_for_resource
-- [ ] list_virtual_gateways
-- [X] list_virtual_nodes
-- [X] list_virtual_routers
-- [ ] list_virtual_services
-- [X] tag_resource
-- [ ] untag_resource
-- [ ] update_gateway_route
-- [X] update_mesh
-- [X] update_route
-- [ ] update_virtual_gateway
-- [X] update_virtual_node
-- [X] update_virtual_router
-- [ ] update_virtual_service
-</details>
-
 ## appsync
 <details>
 <summary>23% implemented</summary>
@@ -753,50 +707,37 @@
 
 ## bedrock
 <details>
-<summary>28% implemented</summary>
+<summary>39% implemented</summary>
 
-- [ ] batch_delete_evaluation_job
 - [ ] create_evaluation_job
 - [ ] create_guardrail
 - [ ] create_guardrail_version
 - [ ] create_model_copy_job
 - [X] create_model_customization_job
-- [ ] create_model_import_job
-- [ ] create_model_invocation_job
 - [ ] create_provisioned_model_throughput
 - [X] delete_custom_model
 - [ ] delete_guardrail
-- [ ] delete_imported_model
 - [X] delete_model_invocation_logging_configuration
 - [ ] delete_provisioned_model_throughput
 - [X] get_custom_model
 - [ ] get_evaluation_job
 - [ ] get_foundation_model
 - [ ] get_guardrail
-- [ ] get_imported_model
-- [ ] get_inference_profile
 - [ ] get_model_copy_job
 - [X] get_model_customization_job
-- [ ] get_model_import_job
-- [ ] get_model_invocation_job
 - [X] get_model_invocation_logging_configuration
 - [ ] get_provisioned_model_throughput
 - [X] list_custom_models
 - [ ] list_evaluation_jobs
 - [ ] list_foundation_models
 - [ ] list_guardrails
-- [ ] list_imported_models
-- [ ] list_inference_profiles
 - [ ] list_model_copy_jobs
 - [X] list_model_customization_jobs
-- [ ] list_model_import_jobs
-- [ ] list_model_invocation_jobs
 - [ ] list_provisioned_model_throughputs
 - [X] list_tags_for_resource
 - [X] put_model_invocation_logging_configuration
 - [ ] stop_evaluation_job
 - [X] stop_model_customization_job
-- [ ] stop_model_invocation_job
 - [X] tag_resource
 - [X] untag_resource
 - [ ] update_guardrail
@@ -3328,7 +3269,7 @@
 
 ## elbv2
 <details>
-<summary>64% implemented</summary>
+<summary>67% implemented</summary>
 
 - [X] add_listener_certificates
 - [X] add_tags
@@ -3346,7 +3287,6 @@
 - [ ] delete_trust_store
 - [X] deregister_targets
 - [ ] describe_account_limits
-- [ ] describe_listener_attributes
 - [X] describe_listener_certificates
 - [X] describe_listeners
 - [X] describe_load_balancer_attributes
@@ -3364,7 +3304,6 @@
 - [ ] get_trust_store_ca_certificates_bundle
 - [ ] get_trust_store_revocation_content
 - [X] modify_listener
-- [ ] modify_listener_attributes
 - [X] modify_load_balancer_attributes
 - [X] modify_rule
 - [X] modify_target_group
@@ -4976,7 +4915,7 @@
 
 ## lambda
 <details>
-<summary>63% implemented</summary>
+<summary>65% implemented</summary>
 
 - [ ] add_layer_version_permission
 - [X] add_permission
@@ -5004,7 +4943,6 @@
 - [X] get_function_concurrency
 - [ ] get_function_configuration
 - [X] get_function_event_invoke_config
-- [ ] get_function_recursion_config
 - [X] get_function_url_config
 - [X] get_layer_version
 - [ ] get_layer_version_by_arn
@@ -5032,7 +4970,6 @@
 - [ ] put_function_code_signing_config
 - [X] put_function_concurrency
 - [X] put_function_event_invoke_config
-- [ ] put_function_recursion_config
 - [ ] put_provisioned_concurrency_config
 - [ ] put_runtime_management_config
 - [ ] remove_layer_version_permission
@@ -5053,7 +4990,7 @@
 <summary>47% implemented</summary>
 
 - [ ] associate_kms_key
-- [X] cancel_export_task
+- [ ] cancel_export_task
 - [ ] create_delivery
 - [X] create_export_task
 - [ ] create_log_anomaly_detector
@@ -5075,7 +5012,6 @@
 - [X] delete_retention_policy
 - [X] delete_subscription_filter
 - [ ] describe_account_policies
-- [ ] describe_configuration_templates
 - [ ] describe_deliveries
 - [ ] describe_delivery_destinations
 - [ ] describe_delivery_sources
@@ -5126,7 +5062,6 @@
 - [X] untag_log_group
 - [X] untag_resource
 - [ ] update_anomaly
-- [ ] update_delivery_configuration
 - [ ] update_log_anomaly_detector
 </details>
 
@@ -5165,7 +5100,7 @@
 
 ## mediaconnect
 <details>
-<summary>34% implemented</summary>
+<summary>35% implemented</summary>
 
 - [ ] add_bridge_outputs
 - [ ] add_bridge_sources
@@ -5183,7 +5118,6 @@
 - [ ] describe_bridge
 - [X] describe_flow
 - [ ] describe_flow_source_metadata
-- [ ] describe_flow_source_thumbnail
 - [ ] describe_gateway
 - [ ] describe_gateway_instance
 - [ ] describe_offering
@@ -5382,6 +5316,50 @@
 - [X] put_object
 </details>
 
+## memorydb
+<details>
+<summary>34% implemented</summary>
+
+- [ ] batch_update_cluster
+- [ ] copy_snapshot
+- [ ] create_acl
+- [X] create_cluster
+- [ ] create_parameter_group
+- [X] create_snapshot
+- [X] create_subnet_group
+- [ ] create_user
+- [ ] delete_acl
+- [X] delete_cluster
+- [ ] delete_parameter_group
+- [X] delete_snapshot
+- [X] delete_subnet_group
+- [ ] delete_user
+- [ ] describe_acls
+- [X] describe_clusters
+- [ ] describe_engine_versions
+- [ ] describe_events
+- [ ] describe_parameter_groups
+- [ ] describe_parameters
+- [ ] describe_reserved_nodes
+- [ ] describe_reserved_nodes_offerings
+- [ ] describe_service_updates
+- [X] describe_snapshots
+- [X] describe_subnet_groups
+- [ ] describe_users
+- [ ] failover_shard
+- [ ] list_allowed_node_type_updates
+- [X] list_tags
+- [ ] purchase_reserved_nodes_offering
+- [ ] reset_parameter_group
+- [X] tag_resource
+- [X] untag_resource
+- [ ] update_acl
+- [X] update_cluster
+- [ ] update_parameter_group
+- [ ] update_subnet_group
+- [ ] update_user
+</details>
+
 ## meteringmarketplace
 <details>
 <summary>25% implemented</summary>
@@ -5954,7 +5932,6 @@
 - [ ] update_dataset
 - [ ] update_metric_attribution
 - [ ] update_recommender
-- [ ] update_solution
 </details>
 
 ## pinpoint
@@ -6100,32 +6077,6 @@
 - [ ] synthesize_speech
 </details>
 
-## qldb
-<details>
-<summary>30% implemented</summary>
-
-- [ ] cancel_journal_kinesis_stream
-- [X] create_ledger
-- [X] delete_ledger
-- [ ] describe_journal_kinesis_stream
-- [ ] describe_journal_s3_export
-- [X] describe_ledger
-- [ ] export_journal_to_s3
-- [ ] get_block
-- [ ] get_digest
-- [ ] get_revision
-- [ ] list_journal_kinesis_streams_for_ledger
-- [ ] list_journal_s3_exports
-- [ ] list_journal_s3_exports_for_ledger
-- [ ] list_ledgers
-- [X] list_tags_for_resource
-- [ ] stream_journal_to_kinesis
-- [X] tag_resource
-- [ ] untag_resource
-- [X] update_ledger
-- [ ] update_ledger_permissions_mode
-</details>
-
 ## quicksight
 <details>
 <summary>7% implemented</summary>
@@ -7124,7 +7075,7 @@
 
 ## s3
 <details>
-<summary>71% implemented</summary>
+<summary>68% implemented</summary>
 
 - [X] abort_multipart_upload
 - [X] complete_multipart_upload
@@ -7149,7 +7100,7 @@
 - [X] delete_object_tagging
 - [X] delete_objects
 - [X] delete_public_access_block
-- [X] get_bucket_accelerate_configuration
+- [ ] get_bucket_accelerate_configuration
 - [X] get_bucket_acl
 - [ ] get_bucket_analytics_configuration
 - [X] get_bucket_cors
@@ -7188,7 +7139,7 @@
 - [ ] list_bucket_metrics_configurations
 - [X] list_buckets
 - [ ] list_directory_buckets
-- [X] list_multipart_uploads
+- [ ] list_multipart_uploads
 - [X] list_object_versions
 - [X] list_objects
 - [X] list_objects_v2
@@ -7212,7 +7163,7 @@
 - [ ] put_bucket_request_payment
 - [X] put_bucket_tagging
 - [X] put_bucket_versioning
-- [X] put_bucket_website
+- [ ] put_bucket_website
 - [X] put_object
 - [X] put_object_acl
 - [X] put_object_legal_hold
@@ -7298,7 +7249,6 @@
 - [ ] list_access_grants_locations
 - [ ] list_access_points
 - [ ] list_access_points_for_object_lambda
-- [ ] list_caller_access_grants
 - [ ] list_jobs
 - [ ] list_multi_region_access_points
 - [ ] list_regional_buckets
@@ -7983,7 +7933,7 @@
 
 ## shield
 <details>
-<summary>25% implemented</summary>
+<summary>19% implemented</summary>
 
 - [ ] associate_drt_log_bucket
 - [ ] associate_drt_role
@@ -7991,7 +7941,7 @@
 - [ ] associate_proactive_engagement_details
 - [X] create_protection
 - [ ] create_protection_group
-- [X] create_subscription
+- [ ] create_subscription
 - [X] delete_protection
 - [ ] delete_protection_group
 - [ ] delete_subscription
@@ -8001,7 +7951,7 @@
 - [ ] describe_emergency_contact_settings
 - [X] describe_protection
 - [ ] describe_protection_group
-- [X] describe_subscription
+- [ ] describe_subscription
 - [ ] disable_application_layer_automatic_response
 - [ ] disable_proactive_engagement
 - [ ] disassociate_drt_log_bucket
@@ -8505,27 +8455,6 @@
 - [ ] update_adapter
 </details>
 
-## timestream-query
-<details>
-<summary>40% implemented</summary>
-
-- [ ] cancel_query
-- [X] create_scheduled_query
-- [X] delete_scheduled_query
-- [ ] describe_account_settings
-- [X] describe_endpoints
-- [X] describe_scheduled_query
-- [ ] execute_scheduled_query
-- [ ] list_scheduled_queries
-- [ ] list_tags_for_resource
-- [ ] prepare_query
-- [X] query
-- [ ] tag_resource
-- [ ] untag_resource
-- [ ] update_account_settings
-- [X] update_scheduled_query
-</details>
-
 ## timestream-write
 <details>
 <summary>78% implemented</summary>
@@ -8835,6 +8764,7 @@
 - application-insights
 - application-signals
 - applicationcostprofiler
+- appmesh
 - apprunner
 - appstream
 - apptest
@@ -8874,6 +8804,7 @@
 - codeguru-reviewer
 - codeguru-security
 - codeguruprofiler
+- codestar
 - codestar-connections
 - codestar-notifications
 - cognito-sync
@@ -8978,7 +8909,6 @@
 - mediapackagev2
 - mediatailor
 - medical-imaging
-- memorydb
 - mgh
 - mgn
 - migration-hub-refactor-spaces
@@ -9001,7 +8931,6 @@
 - payment-cryptography-data
 - pca-connector-ad
 - pca-connector-scep
-- pcs
 - personalize-events
 - personalize-runtime
 - pi
@@ -9015,6 +8944,7 @@
 - qapps
 - qbusiness
 - qconnect
+- qldb
 - qldb-session
 - rbin
 - redshift-serverless
@@ -9055,6 +8985,7 @@
 - synthetics
 - taxsettings
 - timestream-influxdb
+- timestream-query
 - tnb
 - translate
 - trustedadvisor
diff --git a/docs/docs/services/memorydb.rst b/docs/docs/services/memorydb.rst
new file mode 100644
index 000000000..e36276f40
--- /dev/null
+++ b/docs/docs/services/memorydb.rst
@@ -0,0 +1,57 @@
+.. _implementedservice_memorydb:
+
+.. |start-h3| raw:: html
+
+    <h3>
+
+.. |end-h3| raw:: html
+
+    </h3>
+
+========
+memorydb
+========
+
+.. autoclass:: moto.memorydb.models.MemoryDBBackend
+
+|start-h3| Implemented features for this service |end-h3|
+
+- [ ] batch_update_cluster
+- [ ] copy_snapshot
+- [ ] create_acl
+- [X] create_cluster
+- [ ] create_parameter_group
+- [X] create_snapshot
+- [X] create_subnet_group
+- [ ] create_user
+- [ ] delete_acl
+- [X] delete_cluster
+- [ ] delete_parameter_group
+- [X] delete_snapshot
+- [X] delete_subnet_group
+- [ ] delete_user
+- [ ] describe_acls
+- [X] describe_clusters
+- [ ] describe_engine_versions
+- [ ] describe_events
+- [ ] describe_parameter_groups
+- [ ] describe_parameters
+- [ ] describe_reserved_nodes
+- [ ] describe_reserved_nodes_offerings
+- [ ] describe_service_updates
+- [X] describe_snapshots
+- [X] describe_subnet_groups
+- [ ] describe_users
+- [ ] failover_shard
+- [ ] list_allowed_node_type_updates
+- [X] list_tags
+- [ ] purchase_reserved_nodes_offering
+- [ ] reset_parameter_group
+- [X] tag_resource
+- [X] untag_resource
+- [ ] update_acl
+- [X] update_cluster
+- [ ] update_parameter_group
+- [ ] update_subnet_group
+- [ ] update_user
+
diff --git a/moto/apigateway/models.py b/moto/apigateway/models.py
index 9ca56026a..08d89884b 100644
--- a/moto/apigateway/models.py
+++ b/moto/apigateway/models.py
@@ -71,6 +71,7 @@ from .exceptions import (
 from .utils import create_id, to_path
 
 STAGE_URL = "https://{api_id}.execute-api.{region_name}.amazonaws.com/{stage_name}"
+PATCH_OPERATIONS = ["add", "remove", "replace", "move", "copy", "test"]
 
 
 class Deployment(CloudFormationModel):
@@ -1020,6 +1021,7 @@ class RestAPI(CloudFormationModel):
     PROP_POLICY = "policy"
     PROP_DISABLE_EXECUTE_API_ENDPOINT = "disableExecuteApiEndpoint"
     PROP_MINIMUM_COMPRESSION_SIZE = "minimumCompressionSize"
+    PROP_ROOT_RESOURCE_ID = "rootResourceId"
 
     # operations
     OPERATION_ADD = "add"
@@ -1064,6 +1066,7 @@ class RestAPI(CloudFormationModel):
         self.models: Dict[str, Model] = {}
         self.request_validators: Dict[str, RequestValidator] = {}
         self.default = self.add_child("/")  # Add default child
+        self.root_resource_id = self.default.id
 
     def __repr__(self) -> str:
         return str(self.id)
@@ -1082,6 +1085,7 @@ class RestAPI(CloudFormationModel):
             self.PROP_POLICY: self.policy,
             self.PROP_DISABLE_EXECUTE_API_ENDPOINT: self.disableExecuteApiEndpoint,
             self.PROP_MINIMUM_COMPRESSION_SIZE: self.minimum_compression_size,
+            self.PROP_ROOT_RESOURCE_ID: self.root_resource_id,
         }
 
     def apply_patch_operations(self, patch_operations: List[Dict[str, Any]]) -> None:
@@ -1529,6 +1533,54 @@ class GatewayResponse(BaseModel):
         return dct
 
 
+class Account(BaseModel):
+    def __init__(self) -> None:
+        self.cloudwatch_role_arn: Optional[str] = None
+        self.throttle_settings: Dict[str, Any] = {
+            "burstLimit": 5000,
+            "rateLimit": 10000.0,
+        }
+        self.features: Optional[List[str]] = None
+        self.api_key_version: str = "1"
+
+    def apply_patch_operations(
+        self, patch_operations: List[Dict[str, Any]]
+    ) -> "Account":
+        for op in patch_operations:
+            if "/cloudwatchRoleArn" in op["path"]:
+                self.cloudwatch_role_arn = op["value"]
+            elif "/features" in op["path"]:
+                if op["op"] == "add":
+                    if self.features is None:
+                        self.features = [op["value"]]
+                    else:
+                        self.features.append(op["value"])
+                elif op["op"] == "remove":
+                    if op["value"] == "UsagePlans":
+                        raise BadRequestException(
+                            "Usage Plans cannot be disabled once enabled"
+                        )
+                    if self.features is not None:
+                        self.features.remove(op["value"])
+                else:
+                    raise NotImplementedError(
+                        f'Patch operation "{op["op"]}" for "/features" not implemented'
+                    )
+            else:
+                raise NotImplementedError(
+                    f'Patch operation "{op["op"]}" for "{op["path"]}" not implemented'
+                )
+        return self
+
+    def to_json(self) -> Dict[str, Any]:
+        return {
+            "cloudwatchRoleArn": self.cloudwatch_role_arn,
+            "throttleSettings": self.throttle_settings,
+            "features": self.features,
+            "apiKeyVersion": self.api_key_version,
+        }
+
+
 class APIGatewayBackend(BaseBackend):
     """
     API Gateway mock.
@@ -1558,6 +1610,7 @@ class APIGatewayBackend(BaseBackend):
 
     def __init__(self, region_name: str, account_id: str):
         super().__init__(region_name, account_id)
+        self.account: Account = Account()
         self.apis: Dict[str, RestAPI] = {}
         self.keys: Dict[str, ApiKey] = {}
         self.usage_plans: Dict[str, UsagePlan] = {}
@@ -2485,5 +2538,12 @@ class APIGatewayBackend(BaseBackend):
         api = self.get_rest_api(rest_api_id)
         api.delete_gateway_response(response_type)
 
+    def update_account(self, patch_operations: List[Dict[str, Any]]) -> Account:
+        account = self.account.apply_patch_operations(patch_operations)
+        return account
+
+    def get_account(self) -> Account:
+        return self.account
+
 
 apigateway_backends = BackendDict(APIGatewayBackend, "apigateway")
diff --git a/moto/apigateway/responses.py b/moto/apigateway/responses.py
index 127128419..8ffa618b9 100644
--- a/moto/apigateway/responses.py
+++ b/moto/apigateway/responses.py
@@ -851,3 +851,12 @@ class APIGatewayResponse(BaseResponse):
             rest_api_id=rest_api_id, response_type=response_type
         )
         return 202, {}, json.dumps(dict())
+
+    def update_account(self) -> str:
+        patch_operations = self._get_param("patchOperations")
+        account = self.backend.update_account(patch_operations)
+        return json.dumps(account.to_json())
+
+    def get_account(self) -> str:
+        account = self.backend.get_account()
+        return json.dumps(account.to_json())
diff --git a/moto/apigateway/urls.py b/moto/apigateway/urls.py
index 9bcd317af..fbdabf6fd 100644
--- a/moto/apigateway/urls.py
+++ b/moto/apigateway/urls.py
@@ -41,6 +41,7 @@ url_paths = {
     "{0}/restapis/(?P<api_id>[^/]+)/gatewayresponses/(?P<response_type>[^/]+)/?$": APIGatewayResponse.dispatch,
     "{0}/vpclinks$": APIGatewayResponse.dispatch,
     "{0}/vpclinks/(?P<vpclink_id>[^/]+)": APIGatewayResponse.dispatch,
+    "{0}/account$": APIGatewayResponse.dispatch,
 }
 
 # Also manages the APIGatewayV2
diff --git a/moto/backend_index.py b/moto/backend_index.py
index 027edb94b..46d10cae8 100644
--- a/moto/backend_index.py
+++ b/moto/backend_index.py
@@ -116,6 +116,7 @@ backend_url_patterns = [
     ("mediapackage", re.compile("https?://mediapackage\\.(.+)\\.amazonaws.com")),
     ("mediastore", re.compile("https?://mediastore\\.(.+)\\.amazonaws\\.com")),
     ("mediastoredata", re.compile("https?://data\\.mediastore\\.(.+)\\.amazonaws.com")),
+    ("memorydb", re.compile("https?://memory-db\\.(.+)\\.amazonaws\\.com")),
     (
         "meteringmarketplace",
         re.compile("https?://metering.marketplace.(.+).amazonaws.com"),
@@ -212,5 +213,6 @@ backend_url_patterns = [
     ("transfer", re.compile("https?://transfer\\.(.+)\\.amazonaws\\.com")),
     ("wafv2", re.compile("https?://wafv2\\.(.+)\\.amazonaws.com")),
     ("workspaces", re.compile("https?://workspaces\\.(.+)\\.amazonaws\\.com")),
+    ("workspacesweb", re.compile("https?://workspaces-web\\.(.+)\\.amazonaws\\.com")),
     ("xray", re.compile("https?://xray\\.(.+)\\.amazonaws.com")),
 ]
diff --git a/moto/backends.py b/moto/backends.py
index 15d8242ae..bb70dda5a 100644
--- a/moto/backends.py
+++ b/moto/backends.py
@@ -92,6 +92,7 @@ if TYPE_CHECKING:
     from moto.mediapackage.models import MediaPackageBackend
     from moto.mediastore.models import MediaStoreBackend
     from moto.mediastoredata.models import MediaStoreDataBackend
+    from moto.memorydb.models import MemoryDBBackend
     from moto.meteringmarketplace.models import MeteringMarketplaceBackend
     from moto.moto_api._internal.models import MotoAPIBackend
     from moto.mq.models import MQBackend
@@ -147,6 +148,7 @@ if TYPE_CHECKING:
     from moto.transfer.models import TransferBackend
     from moto.wafv2.models import WAFV2Backend
     from moto.workspaces.models import WorkSpacesBackend
+    from moto.workspacesweb.models import WorkSpacesWebBackend
     from moto.xray.models import XRayBackend
 
 
@@ -267,6 +269,7 @@ SERVICE_NAMES = Union[
     "Literal['medialive']",
     "Literal['mediapackage']",
     "Literal['mediastore']",
+    "Literal['memorydb']",
     "Literal['mediastore-data']",
     "Literal['meteringmarketplace']",
     "Literal['moto_api']",
@@ -325,6 +328,7 @@ SERVICE_NAMES = Union[
     "Literal['transfer']",
     "Literal['wafv2']",
     "Literal['workspaces']",
+    "Literal['workspaces-web']",
     "Literal['xray']",
 ]
 
@@ -562,6 +566,8 @@ def get_backend(
     name: "Literal['mediastore-data']",
 ) -> "BackendDict[MediaStoreDataBackend]": ...
 @overload
+def get_backend(name: "Literal['memorydb']") -> "BackendDict[MemoryDBBackend]": ...
+@overload
 def get_backend(
     name: "Literal['meteringmarketplace']",
 ) -> "BackendDict[MeteringMarketplaceBackend]": ...
@@ -728,6 +734,10 @@ def get_backend(name: "Literal['wafv2']") -> "BackendDict[WAFV2Backend]": ...
 @overload
 def get_backend(name: "Literal['workspaces']") -> "BackendDict[WorkSpacesBackend]": ...
 @overload
+def get_backend(
+    name: "Literal['workspaces-web']",
+) -> "BackendDict[WorkSpacesWebBackend]": ...
+@overload
 def get_backend(name: "Literal['xray']") -> "BackendDict[XRayBackend]": ...
 
 
diff --git a/moto/firehose/models.py b/moto/firehose/models.py
index 2af38c387..45165953b 100644
--- a/moto/firehose/models.py
+++ b/moto/firehose/models.py
@@ -47,6 +47,7 @@ DESTINATION_TYPES_TO_NAMES = {
     "http_endpoint": "HttpEndpoint",
     "elasticsearch": "Elasticsearch",
     "redshift": "Redshift",
+    "snowflake": "Snowflake",
     "splunk": "Splunk",  # Unimplemented
 }
 
@@ -200,6 +201,7 @@ class FirehoseBackend(BaseBackend):
         elasticsearch_destination_configuration: Dict[str, Any],
         splunk_destination_configuration: Dict[str, Any],
         http_endpoint_destination_configuration: Dict[str, Any],
+        snowflake_destination_configuration: Dict[str, Any],
         tags: List[Dict[str, str]],
     ) -> str:
         """Create a Kinesis Data Firehose delivery stream."""
@@ -600,6 +602,7 @@ class FirehoseBackend(BaseBackend):
         elasticsearch_destination_update: Dict[str, Any],
         splunk_destination_update: Dict[str, Any],
         http_endpoint_destination_update: Dict[str, Any],
+        snowflake_destination_configuration: Dict[str, Any],
     ) -> None:
         (dest_name, dest_config) = find_destination_config_in_args(locals())
 
diff --git a/moto/firehose/responses.py b/moto/firehose/responses.py
index 0a1314634..669f82f17 100644
--- a/moto/firehose/responses.py
+++ b/moto/firehose/responses.py
@@ -32,6 +32,7 @@ class FirehoseResponse(BaseResponse):
             self._get_param("ElasticsearchDestinationConfiguration"),
             self._get_param("SplunkDestinationConfiguration"),
             self._get_param("HttpEndpointDestinationConfiguration"),
+            self._get_param("SnowflakeDestinationConfiguration"),
             self._get_param("Tags"),
         )
         return json.dumps({"DeliveryStreamARN": delivery_stream_arn})
@@ -109,6 +110,7 @@ class FirehoseResponse(BaseResponse):
             self._get_param("ElasticsearchDestinationUpdate"),
             self._get_param("SplunkDestinationUpdate"),
             self._get_param("HttpEndpointDestinationUpdate"),
+            self._get_param("SnowflakeDestinationConfiguration"),
         )
         return json.dumps({})
 
diff --git a/moto/memorydb/__init__.py b/moto/memorydb/__init__.py
new file mode 100644
index 000000000..01ab867bb
--- /dev/null
+++ b/moto/memorydb/__init__.py
@@ -0,0 +1,1 @@
+from .models import memorydb_backends  # noqa: F401
diff --git a/moto/memorydb/exceptions.py b/moto/memorydb/exceptions.py
new file mode 100644
index 000000000..2d0c9cf4a
--- /dev/null
+++ b/moto/memorydb/exceptions.py
@@ -0,0 +1,59 @@
+"""Exceptions raised by the memorydb service."""
+
+from typing import List
+
+from moto.core.exceptions import JsonRESTError
+
+
+class MemoryDBClientError(JsonRESTError):
+    code = 400
+
+
+class ClusterAlreadyExistsFault(MemoryDBClientError):
+    def __init__(self, msg: str):
+        super().__init__("ClusterAlreadyExistsFault", msg)
+
+
+class InvalidSubnetError(MemoryDBClientError):
+    def __init__(self, subnet_identifier: List[str]):
+        super().__init__("InvalidSubnetError", f"Subnet {subnet_identifier} not found.")
+
+
+class SubnetGroupAlreadyExistsFault(MemoryDBClientError):
+    def __init__(self, msg: str):
+        super().__init__("SubnetGroupAlreadyExistsFault", msg)
+
+
+class ClusterNotFoundFault(MemoryDBClientError):
+    def __init__(self, msg: str):
+        super().__init__("ClusterNotFoundFault", msg)
+
+
+class SnapshotAlreadyExistsFault(MemoryDBClientError):
+    def __init__(self, msg: str):
+        super().__init__("SnapshotAlreadyExistsFault", msg)
+
+
+class SnapshotNotFoundFault(MemoryDBClientError):
+    def __init__(self, msg: str):
+        super().__init__("SnapshotNotFoundFault", msg)
+
+
+class SubnetGroupNotFoundFault(MemoryDBClientError):
+    def __init__(self, msg: str):
+        super().__init__("SubnetGroupNotFoundFault", msg)
+
+
+class TagNotFoundFault(MemoryDBClientError):
+    def __init__(self, msg: str):
+        super().__init__("TagNotFoundFault", msg)
+
+
+class InvalidParameterValueException(MemoryDBClientError):
+    def __init__(self, msg: str):
+        super().__init__("InvalidParameterValueException", msg)
+
+
+class SubnetGroupInUseFault(MemoryDBClientError):
+    def __init__(self, msg: str):
+        super().__init__("SubnetGroupInUseFault", msg)
diff --git a/moto/memorydb/models.py b/moto/memorydb/models.py
new file mode 100644
index 000000000..d52b2f850
--- /dev/null
+++ b/moto/memorydb/models.py
@@ -0,0 +1,664 @@
+"""MemoryDBBackend class with methods for supported APIs."""
+
+import copy
+import random
+from datetime import datetime
+from typing import Any, Dict, List, Optional
+
+from moto.core.base_backend import BackendDict, BaseBackend
+from moto.core.common_models import BaseModel
+from moto.ec2 import ec2_backends
+from moto.utilities.tagging_service import TaggingService
+
+from .exceptions import (
+    ClusterAlreadyExistsFault,
+    ClusterNotFoundFault,
+    InvalidParameterValueException,
+    InvalidSubnetError,
+    SnapshotAlreadyExistsFault,
+    SnapshotNotFoundFault,
+    SubnetGroupAlreadyExistsFault,
+    SubnetGroupInUseFault,
+    SubnetGroupNotFoundFault,
+    TagNotFoundFault,
+)
+
+
+class MemoryDBCluster(BaseModel):
+    def __init__(
+        self,
+        cluster_name: str,
+        node_type: str,
+        parameter_group_name: str,
+        description: str,
+        num_shards: int,
+        num_replicas_per_shard: int,
+        subnet_group_name: str,
+        vpc_id: str,
+        maintenance_window: str,
+        port: int,
+        sns_topic_arn: str,
+        kms_key_id: str,
+        snapshot_arns: List[str],
+        snapshot_name: str,
+        snapshot_retention_limit: int,
+        snapshot_window: str,
+        acl_name: str,
+        engine_version: str,
+        region: str,
+        account_id: str,
+        security_group_ids: List[str],
+        auto_minor_version_upgrade: bool,
+        data_tiering: bool,
+        tls_enabled: bool,
+    ):
+        self.cluster_name = cluster_name
+        self.node_type = node_type
+        # Default is set to 'default.memorydb-redis7'.
+        self.parameter_group_name = parameter_group_name or "default.memorydb-redis7"
+        # Setting it to 'in-sync', other option are 'active' or 'applying'.
+        self.parameter_group_status = "in-sync"
+        self.description = description
+        self.num_shards = num_shards or 1  # Default shards is set to 1
+        # Defaults to 1 (i.e. 2 nodes per shard).
+        self.num_replicas_per_shard = num_replicas_per_shard or 1
+        self.subnet_group_name = subnet_group_name
+        self.vpc_id = vpc_id
+        self.maintenance_window = maintenance_window or "wed:08:00-wed:09:00"
+        self.port = port or 6379  # Default is set to 6379
+        self.sns_topic_arn = sns_topic_arn
+        self.tls_enabled = tls_enabled or True
+        self.kms_key_id = kms_key_id
+        self.snapshot_arns = snapshot_arns
+        self.snapshot_name = snapshot_name
+        self.snapshot_retention_limit = snapshot_retention_limit or 0
+        self.snapshot_window = snapshot_window or "03:00-04:00"
+        # When tlsenable is set to false, the acl_name must be open-access.
+        self.acl_name = "open-access" if not tls_enabled else acl_name
+        self.region = region
+        self.engine_version = engine_version
+        if engine_version == "7.0":
+            self.engine_patch_version = "7.0.7"
+        elif engine_version == "6.2":
+            self.engine_patch_version = "6.2.6"
+        else:
+            self.engine_version = "7.1"  # Default is '7.1'.
+            self.engine_patch_version = "7.1.1"
+        self.auto_minor_version_upgrade = auto_minor_version_upgrade or True
+        self.data_tiering = "true" if data_tiering else "false"
+        # The initial status of the cluster will be set to 'creating'."
+        self.status = (
+            # Set to 'available', other options are 'creating', 'Updating'.
+            "available"
+        )
+        self.pending_updates: Dict[Any, Any] = {}  # TODO
+        self.shards = self.get_shard_details()
+
+        self.availability_mode = (
+            "SingleAZ" if self.num_replicas_per_shard == 0 else "MultiAZ"
+        )
+        self.cluster_endpoint = {
+            "Address": f"clustercfg.{self.cluster_name}.aoneci.memorydb.{region}.amazonaws.com",
+            "Port": self.port,
+        }
+        self.security_group_ids = security_group_ids or []
+        self.security_groups = []
+        for sg in self.security_group_ids:
+            security_group = {"SecurityGroupId": sg, "Status": "active"}
+            self.security_groups.append(security_group)
+        self.arn = f"arn:aws:memorydb:{region}:{account_id}:cluster/{self.cluster_name}"
+        self.sns_topic_status = "active" if self.sns_topic_arn else ""
+
+    def get_shard_details(self) -> List[Dict[str, Any]]:
+        shards = []
+        for i in range(self.num_shards):
+            shard_name = f"{i+1:04}"
+            num_nodes = self.num_replicas_per_shard + 1
+            nodes = []
+            azs = ["a", "b", "c", "d"]
+            for n in range(num_nodes):
+                node_name = f"{self.cluster_name}-{shard_name}-{n+1:03}"
+                node = {
+                    "Name": node_name,
+                    "Status": "available",
+                    "AvailabilityZone": f"{self.region}{random.choice(azs)}",
+                    "CreateTime": datetime.now().strftime(
+                        "%Y-%m-%dT%H:%M:%S.000%f+0000"
+                    ),
+                    "Endpoint": {
+                        "Address": f"{node_name}.{self.cluster_name}.aoneci.memorydb.{self.region}.amazonaws.com",
+                        "Port": self.port,
+                    },
+                }
+                nodes.append(node)
+
+            shard = {
+                "Name": shard_name,
+                # Set to 'available', other options are 'creating', 'modifying' , 'deleting'.
+                "Status": "available",
+                "Slots": f"0-{str(random.randint(10000,99999))}",
+                "Nodes": nodes,
+                "NumberOfNodes": num_nodes,
+            }
+            shards.append(shard)
+        return shards
+
+    def update(
+        self,
+        description: Optional[str],
+        security_group_ids: Optional[List[str]],
+        maintenance_window: Optional[str],
+        sns_topic_arn: Optional[str],
+        sns_topic_status: Optional[str],
+        parameter_group_name: Optional[str],
+        snapshot_window: Optional[str],
+        snapshot_retention_limit: Optional[int],
+        node_type: Optional[str],
+        engine_version: Optional[str],
+        replica_configuration: Optional[Dict[str, int]],
+        shard_configuration: Optional[Dict[str, int]],
+        acl_name: Optional[str],
+    ) -> None:
+        if description is not None:
+            self.description = description
+        if security_group_ids is not None:
+            self.security_group_ids = security_group_ids
+        if maintenance_window is not None:
+            self.maintenance_window = maintenance_window
+        if sns_topic_arn is not None:
+            self.sns_topic_arn = sns_topic_arn
+        if sns_topic_status is not None:
+            self.sns_topic_status = sns_topic_status
+        if parameter_group_name is not None:
+            self.parameter_group_name = parameter_group_name
+        if snapshot_window is not None:
+            self.snapshot_window = snapshot_window
+        if snapshot_retention_limit is not None:
+            self.snapshot_retention_limit = snapshot_retention_limit
+        if node_type is not None:
+            self.node_type = node_type
+        if engine_version is not None:
+            self.engine_version = engine_version
+        if replica_configuration is not None:
+            self.num_replicas_per_shard = replica_configuration["ReplicaCount"]
+            self.shards = self.get_shard_details()  # update shards and nodes
+        if shard_configuration is not None:
+            self.num_shards = shard_configuration["ShardCount"]
+            self.shards = self.get_shard_details()  # update shards and nodes
+        if acl_name is not None:
+            self.acl_name = acl_name
+
+    def to_dict(self) -> Dict[str, Any]:
+        dct = {
+            "Name": self.cluster_name,
+            "Description": self.description,
+            "Status": self.status,
+            "PendingUpdates": self.pending_updates,
+            "NumberOfShards": self.num_shards,
+            "AvailabilityMode": self.availability_mode,
+            "ClusterEndpoint": self.cluster_endpoint,
+            "NodeType": self.node_type,
+            "EngineVersion": self.engine_version,
+            "EnginePatchVersion": self.engine_patch_version,
+            "ParameterGroupName": self.parameter_group_name,
+            "ParameterGroupStatus": self.parameter_group_status,
+            "SecurityGroups": self.security_groups,
+            "SubnetGroupName": self.subnet_group_name,
+            "TLSEnabled": self.tls_enabled,
+            "KmsKeyId": self.kms_key_id,
+            "ARN": self.arn,
+            "SnsTopicArn": self.sns_topic_arn,
+            "SnsTopicStatus": self.sns_topic_status,
+            "MaintenanceWindow": self.maintenance_window,
+            "SnapshotWindow": self.snapshot_window,
+            "ACLName": self.acl_name,
+            "AutoMinorVersionUpgrade": self.auto_minor_version_upgrade,
+            "DataTiering": self.data_tiering,
+        }
+        dct_items = {k: v for k, v in dct.items() if v}
+        dct_items["SnapshotRetentionLimit"] = self.snapshot_retention_limit
+        return dct_items
+
+    def to_desc_dict(self) -> Dict[str, Any]:
+        dct = self.to_dict()
+        dct["Shards"] = self.shards
+        return dct
+
+
+class MemoryDBSubnetGroup(BaseModel):
+    def __init__(
+        self,
+        region_name: str,
+        account_id: str,
+        ec2_backend: Any,
+        subnet_group_name: str,
+        description: str,
+        subnet_ids: List[str],
+        tags: Optional[List[Dict[str, str]]] = None,
+    ):
+        self.ec2_backend = ec2_backend
+        self.subnet_group_name = subnet_group_name
+        self.description = description
+        self.subnet_ids = subnet_ids
+        if not self.subnets:
+            raise InvalidSubnetError(subnet_ids)
+        self.arn = f"arn:aws:memorydb:{region_name}:{account_id}:subnetgroup/{subnet_group_name}"
+
+    @property
+    def subnets(self) -> Any:  # type: ignore[misc]
+        return self.ec2_backend.describe_subnets(filters={"subnet-id": self.subnet_ids})
+
+    @property
+    def vpc_id(self) -> str:
+        return self.subnets[0].vpc_id
+
+    def to_dict(self) -> Dict[str, Any]:
+        return {
+            "Name": self.subnet_group_name,
+            "Description": self.description,
+            "VpcId": self.vpc_id,
+            "Subnets": [
+                {
+                    "Identifier": subnet.id,
+                    "AvailabilityZone": {"Name": subnet.availability_zone},
+                }
+                for subnet in self.subnets
+            ],
+            "ARN": self.arn,
+        }
+
+
+class MemoryDBSnapshot(BaseModel):
+    def __init__(
+        self,
+        account_id: str,
+        region_name: str,
+        cluster: MemoryDBCluster,
+        snapshot_name: str,
+        kms_key_id: Optional[str],
+        tags: Optional[List[Dict[str, str]]],
+        source: Optional[str],
+    ):
+        self.cluster = copy.copy(cluster)
+        self.cluster_name = self.cluster.cluster_name
+        self.snapshot_name = snapshot_name
+        self.status = "available"
+        self.source = source
+        self.kms_key_id = kms_key_id if kms_key_id else cluster.kms_key_id
+        self.arn = (
+            f"arn:aws:memorydb:{region_name}:{account_id}:snapshot/{snapshot_name}"
+        )
+        self.vpc_id = self.cluster.vpc_id
+        self.shards = []
+        for i in self.cluster.shards:
+            shard = {
+                "Name": i["Name"],
+                "Configuration": {
+                    "Slots": i["Slots"],
+                    "ReplicaCount": self.cluster.num_replicas_per_shard,
+                },
+                "Size": "11 MB",
+                "SnapshotCreationTime": datetime.now().strftime(
+                    "%Y-%m-%dT%H:%M:%S.000%f+0000"
+                ),
+            }
+            self.shards.append(shard)
+
+    def to_dict(self) -> Dict[str, Any]:
+        dct = {
+            "Name": self.snapshot_name,
+            "Status": self.status,
+            "Source": self.source,
+            "KmsKeyId": self.kms_key_id,
+            "ARN": self.arn,
+            "ClusterConfiguration": {
+                "Name": self.cluster_name,
+                "Description": self.cluster.description,
+                "NodeType": self.cluster.node_type,
+                "EngineVersion": self.cluster.engine_version,
+                "MaintenanceWindow": self.cluster.maintenance_window,
+                "TopicArn": self.cluster.sns_topic_arn,
+                "Port": self.cluster.port,
+                "ParameterGroupName": self.cluster.parameter_group_name,
+                "SubnetGroupName": self.cluster.subnet_group_name,
+                "VpcId": self.vpc_id,
+                "SnapshotRetentionLimit": self.cluster.snapshot_retention_limit,
+                "SnapshotWindow": self.cluster.snapshot_window,
+                "NumShards": self.cluster.num_shards,
+            },
+            "DataTiering": self.cluster.data_tiering,
+        }
+        return {k: v for k, v in dct.items() if v}
+
+    def to_desc_dict(self) -> Dict[str, Any]:
+        dct = self.to_dict()
+        dct["ClusterConfiguration"]["Shards"] = self.shards
+        return dct
+
+
+class MemoryDBBackend(BaseBackend):
+    """Implementation of MemoryDB APIs."""
+
+    def __init__(self, region_name: str, account_id: str):
+        super().__init__(region_name, account_id)
+
+        self.ec2_backend = ec2_backends[account_id][region_name]
+        self.clusters: Dict[str, MemoryDBCluster] = dict()
+        self.subnet_groups: Dict[str, MemoryDBSubnetGroup] = {
+            "default": MemoryDBSubnetGroup(
+                region_name,
+                account_id,
+                self.ec2_backend,
+                "default",
+                "Default MemoryDB Subnet Group",
+                self.get_default_subnets(),
+            )
+        }
+        self.snapshots: Dict[str, MemoryDBSnapshot] = dict()
+        self.tagger = TaggingService()
+
+    def get_default_subnets(self) -> List[str]:
+        default_subnets = self.ec2_backend.describe_subnets(
+            filters={"default-for-az": "true"}
+        )
+        default_subnet_ids = [i.id for i in default_subnets]
+        return default_subnet_ids
+
+    def _list_arns(self) -> List[str]:
+        return [cluster.arn for cluster in self.clusters.values()]
+
+    def create_cluster(
+        self,
+        cluster_name: str,
+        node_type: str,
+        parameter_group_name: str,
+        description: str,
+        subnet_group_name: str,
+        security_group_ids: List[str],
+        maintenance_window: str,
+        port: int,
+        sns_topic_arn: str,
+        tls_enabled: bool,
+        kms_key_id: str,
+        snapshot_arns: List[str],
+        snapshot_name: str,
+        snapshot_retention_limit: int,
+        tags: List[Dict[str, str]],
+        snapshot_window: str,
+        acl_name: str,
+        engine_version: str,
+        auto_minor_version_upgrade: bool,
+        data_tiering: bool,
+        num_shards: int,
+        num_replicas_per_shard: int,
+    ) -> MemoryDBCluster:
+        if cluster_name in self.clusters:
+            raise ClusterAlreadyExistsFault(
+                msg="Cluster with specified name already exists."
+            )
+
+        subnet_group_name = subnet_group_name or "default"
+        subnet_group = self.subnet_groups[subnet_group_name]
+        vpc_id = subnet_group.vpc_id
+        cluster = MemoryDBCluster(
+            cluster_name=cluster_name,
+            node_type=node_type,
+            parameter_group_name=parameter_group_name,
+            description=description,
+            num_shards=num_shards,
+            num_replicas_per_shard=num_replicas_per_shard,
+            subnet_group_name=subnet_group_name,
+            vpc_id=vpc_id,
+            security_group_ids=security_group_ids,
+            maintenance_window=maintenance_window,
+            port=port,
+            sns_topic_arn=sns_topic_arn,
+            tls_enabled=tls_enabled,
+            kms_key_id=kms_key_id,
+            snapshot_arns=snapshot_arns,
+            snapshot_name=snapshot_name,
+            snapshot_retention_limit=snapshot_retention_limit,
+            snapshot_window=snapshot_window,
+            acl_name=acl_name,
+            engine_version=engine_version,
+            auto_minor_version_upgrade=auto_minor_version_upgrade,
+            data_tiering=data_tiering,
+            region=self.region_name,
+            account_id=self.account_id,
+        )
+        self.clusters[cluster.cluster_name] = cluster
+        self.tag_resource(cluster.arn, tags)
+        return cluster
+
+    def create_subnet_group(
+        self,
+        subnet_group_name: str,
+        description: str,
+        subnet_ids: List[str],
+        tags: Optional[List[Dict[str, str]]] = None,
+    ) -> MemoryDBSubnetGroup:
+        if subnet_group_name in self.subnet_groups:
+            raise SubnetGroupAlreadyExistsFault(
+                msg=f"Subnet group {subnet_group_name} already exists."
+            )
+        subnet_group = MemoryDBSubnetGroup(
+            self.region_name,
+            self.account_id,
+            self.ec2_backend,
+            subnet_group_name,
+            description,
+            subnet_ids,
+            tags,
+        )
+        self.subnet_groups[subnet_group_name] = subnet_group
+        return subnet_group
+
+    def create_snapshot(
+        self,
+        cluster_name: str,
+        snapshot_name: str,
+        kms_key_id: Optional[str] = None,
+        tags: Optional[List[Dict[str, str]]] = None,
+        source: str = "manual",
+    ) -> MemoryDBSnapshot:
+        if cluster_name not in self.clusters:
+            raise ClusterNotFoundFault(msg=f"Cluster not found: {cluster_name}")
+        cluster = self.clusters[cluster_name]
+        if snapshot_name in self.snapshots:
+            raise SnapshotAlreadyExistsFault(
+                msg="Snapshot with specified name already exists."
+            )
+
+        snapshot = MemoryDBSnapshot(
+            account_id=self.account_id,
+            region_name=self.region_name,
+            cluster=cluster,
+            snapshot_name=snapshot_name,
+            kms_key_id=kms_key_id,
+            tags=tags,
+            source=source,
+        )
+        self.snapshots[snapshot_name] = snapshot
+        return snapshot
+
+    def describe_clusters(
+        self, cluster_name: Optional[str] = None
+    ) -> List[MemoryDBCluster]:
+        if cluster_name:
+            if cluster_name in self.clusters:
+                cluster = self.clusters[cluster_name]
+                return list([cluster])
+            else:
+                raise ClusterNotFoundFault(msg=f"Cluster {cluster_name} not found")
+        clusters = list(self.clusters.values())
+        return clusters
+
+    def describe_snapshots(
+        self,
+        cluster_name: Optional[str] = None,
+        snapshot_name: Optional[str] = None,
+        source: Optional[str] = None,
+    ) -> List[MemoryDBSnapshot]:
+        sources = ["automated", "manual"] if source is None else [source]
+
+        if cluster_name and snapshot_name:
+            for snapshot in list(self.snapshots.values()):
+                if (
+                    snapshot.cluster_name == cluster_name
+                    and snapshot.snapshot_name == snapshot_name
+                    and snapshot.source in sources
+                ):
+                    return [snapshot]
+                raise SnapshotNotFoundFault(
+                    msg=f"Snapshot with name {snapshot_name} not found"
+                )
+
+        if cluster_name:
+            snapshots = [
+                snapshot
+                for snapshot in self.snapshots.values()
+                if (snapshot.cluster_name == cluster_name)
+                and (snapshot.source in sources)
+            ]
+            return snapshots
+
+        if snapshot_name:
+            snapshots = [
+                snapshot
+                for snapshot in self.snapshots.values()
+                if (snapshot.snapshot_name == snapshot_name)
+                and (snapshot.source in sources)
+            ]
+            if snapshots:
+                return snapshots
+            raise SnapshotNotFoundFault(
+                msg=f"Snapshot with name {snapshot_name} not found"
+            )
+
+        snapshots = [
+            snapshot
+            for snapshot in self.snapshots.values()
+            if snapshot.source in sources
+        ]
+        return snapshots
+
+    def describe_subnet_groups(
+        self, subnet_group_name: str
+    ) -> List[MemoryDBSubnetGroup]:
+        if subnet_group_name:
+            if subnet_group_name in self.subnet_groups:
+                return list([self.subnet_groups[subnet_group_name]])
+            raise SubnetGroupNotFoundFault(
+                msg=f"Subnet group {subnet_group_name} not found."
+            )
+
+        subnet_groups = list(self.subnet_groups.values())
+        return subnet_groups
+
+    def list_tags(self, resource_arn: str) -> List[Dict[str, str]]:
+        if resource_arn not in self._list_arns():
+            cluster_name = resource_arn.split("/")[-1]
+            raise ClusterNotFoundFault(f"{cluster_name} is not present")
+        return self.tagger.list_tags_for_resource(arn=resource_arn)["Tags"]
+
+    def tag_resource(
+        self, resource_arn: str, tags: List[Dict[str, str]]
+    ) -> List[Dict[str, str]]:
+        if resource_arn not in self._list_arns():
+            cluster_name = resource_arn.split("/")[-1]
+            raise ClusterNotFoundFault(f"{cluster_name} is not present")
+        self.tagger.tag_resource(resource_arn, tags)
+        return self.tagger.list_tags_for_resource(arn=resource_arn)["Tags"]
+
+    def untag_resource(
+        self, resource_arn: str, tag_keys: List[str]
+    ) -> List[Dict[str, str]]:
+        if resource_arn not in self._list_arns():
+            cluster_name = resource_arn.split("/")[-1]
+            raise ClusterNotFoundFault(f"{cluster_name} is not present")
+        list_tags = self.list_tags(resource_arn=resource_arn)
+        list_keys = [i["Key"] for i in list_tags]
+        invalid_keys = [key for key in tag_keys if key not in list_keys]
+        if invalid_keys:
+            raise TagNotFoundFault(msg=f"These tags are not present : {[invalid_keys]}")
+        self.tagger.untag_resource_using_names(resource_arn, tag_keys)
+        return self.tagger.list_tags_for_resource(arn=resource_arn)["Tags"]
+
+    def update_cluster(
+        self,
+        cluster_name: str,
+        description: Optional[str],
+        security_group_ids: Optional[List[str]],
+        maintenance_window: Optional[str],
+        sns_topic_arn: Optional[str],
+        sns_topic_status: Optional[str],
+        parameter_group_name: Optional[str],
+        snapshot_window: Optional[str],
+        snapshot_retention_limit: Optional[int],
+        node_type: Optional[str],
+        engine_version: Optional[str],
+        replica_configuration: Optional[Dict[str, int]],
+        shard_configuration: Optional[Dict[str, int]],
+        acl_name: Optional[str],
+    ) -> MemoryDBCluster:
+        if cluster_name in self.clusters:
+            cluster = self.clusters[cluster_name]
+            cluster.update(
+                description,
+                security_group_ids,
+                maintenance_window,
+                sns_topic_arn,
+                sns_topic_status,
+                parameter_group_name,
+                snapshot_window,
+                snapshot_retention_limit,
+                node_type,
+                engine_version,
+                replica_configuration,
+                shard_configuration,
+                acl_name,
+            )
+            return cluster
+        raise ClusterNotFoundFault(msg="Cluster not found.")
+
+    def delete_cluster(
+        self, cluster_name: str, final_snapshot_name: Optional[str]
+    ) -> MemoryDBCluster:
+        if cluster_name in self.clusters:
+            cluster = self.clusters[cluster_name]
+            cluster.status = "deleting"
+            if final_snapshot_name is not None:  # create snapshot
+                self.create_snapshot(
+                    cluster_name=cluster_name,
+                    snapshot_name=final_snapshot_name,
+                    source="manual",
+                )
+            return self.clusters.pop(cluster_name)
+        raise ClusterNotFoundFault(cluster_name)
+
+    def delete_snapshot(self, snapshot_name: str) -> MemoryDBSnapshot:
+        if snapshot_name in self.snapshots:
+            snapshot = self.snapshots[snapshot_name]
+            snapshot.status = "deleting"
+            return self.snapshots.pop(snapshot_name)
+        raise SnapshotNotFoundFault(snapshot_name)
+
+    def delete_subnet_group(self, subnet_group_name: str) -> MemoryDBSubnetGroup:
+        if subnet_group_name in self.subnet_groups:
+            if subnet_group_name == "default":
+                raise InvalidParameterValueException(
+                    msg="default is reserved and cannot be modified."
+                )
+            if subnet_group_name in [
+                c.subnet_group_name for c in self.clusters.values()
+            ]:
+                raise SubnetGroupInUseFault(
+                    msg=f"Subnet group {subnet_group_name} is currently in use by a cluster."
+                )
+            return self.subnet_groups.pop(subnet_group_name)
+        raise SubnetGroupNotFoundFault(
+            msg=f"Subnet group {subnet_group_name} not found."
+        )
+
+
+memorydb_backends = BackendDict(MemoryDBBackend, "memorydb")
diff --git a/moto/memorydb/responses.py b/moto/memorydb/responses.py
new file mode 100644
index 000000000..4f4674255
--- /dev/null
+++ b/moto/memorydb/responses.py
@@ -0,0 +1,229 @@
+"""Handles incoming memorydb requests, invokes methods, returns responses."""
+
+import json
+
+from moto.core.responses import BaseResponse
+
+from .models import MemoryDBBackend, memorydb_backends
+
+
+class MemoryDBResponse(BaseResponse):
+    """Handler for MemoryDB requests and responses."""
+
+    def __init__(self) -> None:
+        super().__init__(service_name="memorydb")
+
+    @property
+    def memorydb_backend(self) -> MemoryDBBackend:
+        """Return backend instance specific for this region."""
+        return memorydb_backends[self.current_account][self.region]
+
+    def create_cluster(self) -> str:
+        params = json.loads(self.body)
+        cluster_name = params.get("ClusterName")
+        node_type = params.get("NodeType")
+        parameter_group_name = params.get("ParameterGroupName")
+        description = params.get("Description")
+        num_shards = params.get("NumShards")
+        num_replicas_per_shard = params.get("NumReplicasPerShard")
+        subnet_group_name = params.get("SubnetGroupName")
+        security_group_ids = params.get("SecurityGroupIds")
+        maintenance_window = params.get("MaintenanceWindow")
+        port = params.get("Port")
+        sns_topic_arn = params.get("SnsTopicArn")
+        tls_enabled = params.get("TLSEnabled")
+        kms_key_id = params.get("KmsKeyId")
+        snapshot_arns = params.get("SnapshotArns")
+        snapshot_name = params.get("SnapshotName")
+        snapshot_retention_limit = params.get("SnapshotRetentionLimit")
+        tags = params.get("Tags")
+        snapshot_window = params.get("SnapshotWindow")
+        acl_name = params.get("ACLName")
+        engine_version = params.get("EngineVersion")
+        auto_minor_version_upgrade = params.get("AutoMinorVersionUpgrade")
+        data_tiering = params.get("DataTiering")
+        cluster = self.memorydb_backend.create_cluster(
+            cluster_name=cluster_name,
+            node_type=node_type,
+            parameter_group_name=parameter_group_name,
+            description=description,
+            num_shards=num_shards,
+            num_replicas_per_shard=num_replicas_per_shard,
+            subnet_group_name=subnet_group_name,
+            security_group_ids=security_group_ids,
+            maintenance_window=maintenance_window,
+            port=port,
+            sns_topic_arn=sns_topic_arn,
+            tls_enabled=tls_enabled,
+            kms_key_id=kms_key_id,
+            snapshot_arns=snapshot_arns,
+            snapshot_name=snapshot_name,
+            snapshot_retention_limit=snapshot_retention_limit,
+            tags=tags,
+            snapshot_window=snapshot_window,
+            acl_name=acl_name,
+            engine_version=engine_version,
+            auto_minor_version_upgrade=auto_minor_version_upgrade,
+            data_tiering=data_tiering,
+        )
+        return json.dumps(dict(Cluster=cluster.to_dict()))
+
+    def create_subnet_group(self) -> str:
+        params = json.loads(self.body)
+        subnet_group_name = params.get("SubnetGroupName")
+        description = params.get("Description")
+        subnet_ids = params.get("SubnetIds")
+        tags = params.get("Tags")
+        subnet_group = self.memorydb_backend.create_subnet_group(
+            subnet_group_name=subnet_group_name,
+            description=description,
+            subnet_ids=subnet_ids,
+            tags=tags,
+        )
+        return json.dumps(dict(SubnetGroup=subnet_group.to_dict()))
+
+    def create_snapshot(self) -> str:
+        params = json.loads(self.body)
+        cluster_name = params.get("ClusterName")
+        snapshot_name = params.get("SnapshotName")
+        kms_key_id = params.get("KmsKeyId")
+        tags = params.get("Tags")
+        snapshot = self.memorydb_backend.create_snapshot(
+            cluster_name=cluster_name,
+            snapshot_name=snapshot_name,
+            kms_key_id=kms_key_id,
+            tags=tags,
+        )
+        return json.dumps(dict(Snapshot=snapshot.to_dict()))
+
+    def describe_clusters(self) -> str:
+        params = json.loads(self.body)
+        cluster_name = params.get("ClusterName")
+        show_shard_details = params.get("ShowShardDetails")
+        clusters = self.memorydb_backend.describe_clusters(
+            cluster_name=cluster_name,
+        )
+        return json.dumps(
+            dict(
+                Clusters=[
+                    cluster.to_desc_dict() if show_shard_details else cluster.to_dict()
+                    for cluster in clusters
+                ]
+            )
+        )
+
+    def describe_snapshots(self) -> str:
+        params = json.loads(self.body)
+        cluster_name = params.get("ClusterName")
+        snapshot_name = params.get("SnapshotName")
+        source = params.get("Source")
+        show_detail = params.get("ShowDetail")
+        snapshots = self.memorydb_backend.describe_snapshots(
+            cluster_name=cluster_name,
+            snapshot_name=snapshot_name,
+            source=source,
+        )
+        return json.dumps(
+            dict(
+                Snapshots=[
+                    snapshot.to_desc_dict() if show_detail else snapshot.to_dict()
+                    for snapshot in snapshots
+                ]
+            )
+        )
+
+    def describe_subnet_groups(self) -> str:
+        params = json.loads(self.body)
+        subnet_group_name = params.get("SubnetGroupName")
+        subnet_groups = self.memorydb_backend.describe_subnet_groups(
+            subnet_group_name=subnet_group_name,
+        )
+        return json.dumps(dict(SubnetGroups=[sg.to_dict() for sg in subnet_groups]))
+
+    def list_tags(self) -> str:
+        params = json.loads(self.body)
+        resource_arn = params.get("ResourceArn")
+        tag_list = self.memorydb_backend.list_tags(
+            resource_arn=resource_arn,
+        )
+        return json.dumps(dict(TagList=tag_list))
+
+    def tag_resource(self) -> str:
+        params = json.loads(self.body)
+        resource_arn = params.get("ResourceArn")
+        tags = params.get("Tags")
+        tag_list = self.memorydb_backend.tag_resource(
+            resource_arn=resource_arn,
+            tags=tags,
+        )
+        return json.dumps(dict(TagList=tag_list))
+
+    def untag_resource(self) -> str:
+        params = json.loads(self.body)
+        resource_arn = params.get("ResourceArn")
+        tag_keys = params.get("TagKeys")
+        tag_list = self.memorydb_backend.untag_resource(
+            resource_arn=resource_arn,
+            tag_keys=tag_keys,
+        )
+        return json.dumps(dict(TagList=tag_list))
+
+    def update_cluster(self) -> str:
+        params = json.loads(self.body)
+        cluster_name = params.get("ClusterName")
+        description = params.get("Description")
+        security_group_ids = params.get("SecurityGroupIds")
+        maintenance_window = params.get("MaintenanceWindow")
+        sns_topic_arn = params.get("SnsTopicArn")
+        sns_topic_status = params.get("SnsTopicStatus")
+        parameter_group_name = params.get("ParameterGroupName")
+        snapshot_window = params.get("SnapshotWindow")
+        snapshot_retention_limit = params.get("SnapshotRetentionLimit")
+        node_type = params.get("NodeType")
+        engine_version = params.get("EngineVersion")
+        replica_configuration = params.get("ReplicaConfiguration")
+        shard_configuration = params.get("ShardConfiguration")
+        acl_name = params.get("ACLName")
+        cluster = self.memorydb_backend.update_cluster(
+            cluster_name=cluster_name,
+            description=description,
+            security_group_ids=security_group_ids,
+            maintenance_window=maintenance_window,
+            sns_topic_arn=sns_topic_arn,
+            sns_topic_status=sns_topic_status,
+            parameter_group_name=parameter_group_name,
+            snapshot_window=snapshot_window,
+            snapshot_retention_limit=snapshot_retention_limit,
+            node_type=node_type,
+            engine_version=engine_version,
+            replica_configuration=replica_configuration,
+            shard_configuration=shard_configuration,
+            acl_name=acl_name,
+        )
+        return json.dumps(dict(Cluster=cluster.to_dict()))
+
+    def delete_cluster(self) -> str:
+        params = json.loads(self.body)
+        cluster_name = params.get("ClusterName")
+        final_snapshot_name = params.get("FinalSnapshotName")
+        cluster = self.memorydb_backend.delete_cluster(
+            cluster_name=cluster_name,
+            final_snapshot_name=final_snapshot_name,
+        )
+        return json.dumps(dict(Cluster=cluster.to_dict()))
+
+    def delete_snapshot(self) -> str:
+        params = json.loads(self.body)
+        snapshot_name = params.get("SnapshotName")
+        snapshot = self.memorydb_backend.delete_snapshot(
+            snapshot_name=snapshot_name,
+        )
+        return json.dumps(dict(Snapshot=snapshot.to_dict()))
+
+    def delete_subnet_group(self) -> str:
+        params = json.loads(self.body)
+        subnet_group_name = params.get("SubnetGroupName")
+        subnet_group = self.memorydb_backend.delete_subnet_group(
+            subnet_group_name=subnet_group_name,
+        )
+        return json.dumps(dict(SubnetGroup=subnet_group.to_dict()))
diff --git a/moto/memorydb/urls.py b/moto/memorydb/urls.py
new file mode 100644
index 000000000..c9118d329
--- /dev/null
+++ b/moto/memorydb/urls.py
@@ -0,0 +1,11 @@
+"""memorydb base URL and path."""
+
+from .responses import MemoryDBResponse
+
+url_bases = [
+    r"https?://memory-db\.(.+)\.amazonaws\.com",
+]
+
+url_paths = {
+    "{0}/$": MemoryDBResponse.dispatch,
+}
diff --git a/moto/moto_server/werkzeug_app.py b/moto/moto_server/werkzeug_app.py
index 3449955be..1daf65613 100644
--- a/moto/moto_server/werkzeug_app.py
+++ b/moto/moto_server/werkzeug_app.py
@@ -163,6 +163,8 @@ class DomainDispatcherApplication:
             host = "s3control"
         elif service == "ses" and path.startswith("/v2/"):
             host = "sesv2"
+        elif service == "memorydb":
+            host = f"memory-db.{region}.amazonaws.com"
         else:
             host = f"{service}.{region}.amazonaws.com"
 
diff --git a/moto/resourcegroupstaggingapi/models.py b/moto/resourcegroupstaggingapi/models.py
index 5c4432e4f..f704b8609 100644
--- a/moto/resourcegroupstaggingapi/models.py
+++ b/moto/resourcegroupstaggingapi/models.py
@@ -28,6 +28,7 @@ from moto.ssm.models import SimpleSystemManagerBackend, ssm_backends
 from moto.utilities.tagging_service import TaggingService
 from moto.utilities.utils import get_partition
 from moto.workspaces.models import WorkSpacesBackend, workspaces_backends
+from moto.workspacesweb.models import WorkSpacesWebBackend, workspacesweb_backends
 
 # Left: EC2 ElastiCache RDS ELB CloudFront Lambda EMR Glacier Kinesis Redshift Route53
 # StorageGateway DynamoDB MachineLearning ACM DirectConnect DirectoryService CloudHSM
@@ -135,6 +136,13 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend):
             return workspaces_backends[self.account_id][self.region_name]
         return None
 
+    @property
+    def workspacesweb_backends(self) -> Optional[WorkSpacesWebBackend]:
+        # Workspaces service has limited region availability
+        if self.region_name in workspaces_backends[self.account_id].regions:
+            return workspacesweb_backends[self.account_id][self.region_name]
+        return None
+
     @property
     def sagemaker_backend(self) -> SageMakerModelBackend:
         return sagemaker_backends[self.account_id][self.region_name]
@@ -626,6 +634,19 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend):
                     "Tags": tags,
                 }
 
+        # Workspaces Web
+        if self.workspacesweb_backends and (
+            not resource_type_filters or "workspaces-web" in resource_type_filters
+        ):
+            for portal in self.workspacesweb_backends.portals.values():
+                tags = format_tag_keys(portal.tags, ["Key", "Value"])  # type: ignore
+                if not tags or not tag_filter(tags):
+                    continue
+                yield {
+                    "ResourceARN": f"arn:{get_partition(self.region_name)}:workspaces-web:{self.region_name}:{self.account_id}:portal/{portal.portal_id}",
+                    "Tags": tags,
+                }
+
         # VPC
         if (
             not resource_type_filters
@@ -1028,6 +1049,13 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend):
                 self.rds_backend.add_tags_to_resource(
                     arn, TaggingService.convert_dict_to_tags_input(tags)
                 )
+            elif arn.startswith(
+                f"arn:{get_partition(self.region_name)}:workspaces-web:"
+            ):
+                resource_id = arn.split("/")[-1]
+                self.workspacesweb_backends.create_tags(  # type: ignore[union-attr]
+                    resource_id, TaggingService.convert_dict_to_tags_input(tags)
+                )
             elif arn.startswith(f"arn:{get_partition(self.region_name)}:workspaces:"):
                 resource_id = arn.split("/")[-1]
                 self.workspaces_backend.create_tags(  # type: ignore[union-attr]
diff --git a/moto/workspacesweb/__init__.py b/moto/workspacesweb/__init__.py
new file mode 100644
index 000000000..9192321e7
--- /dev/null
+++ b/moto/workspacesweb/__init__.py
@@ -0,0 +1,1 @@
+from .models import workspacesweb_backends  # noqa: F401
diff --git a/moto/workspacesweb/exceptions.py b/moto/workspacesweb/exceptions.py
new file mode 100644
index 000000000..718d0982e
--- /dev/null
+++ b/moto/workspacesweb/exceptions.py
@@ -0,0 +1,1 @@
+"""Exceptions raised by the workspacesweb service."""
diff --git a/moto/workspacesweb/models.py b/moto/workspacesweb/models.py
new file mode 100644
index 000000000..2cd4612d4
--- /dev/null
+++ b/moto/workspacesweb/models.py
@@ -0,0 +1,290 @@
+"""WorkSpacesWebBackend class with methods for supported APIs."""
+
+import datetime
+import uuid
+from typing import Any, Dict, List, Optional, Tuple
+
+from moto.core.base_backend import BackendDict, BaseBackend
+from moto.core.common_models import BaseModel
+from moto.utilities.utils import get_partition
+
+
+class FakeNetworkSettings(BaseModel):
+    def __init__(
+        self,
+        security_group_ids: List[str],
+        subnet_ids: List[str],
+        tags: Dict[str, str],
+        vpc_id: str,
+        region_name: str,
+        account_id: str,
+    ):
+        self.network_settings_id = str(uuid.uuid4())
+        self.arn = self.arn_formatter(self.network_settings_id, account_id, region_name)
+        self.security_group_ids = security_group_ids
+        self.subnet_ids = subnet_ids
+        self.tags = tags
+        self.vpc_id = vpc_id
+        self.associated_portal_arns: List[str] = []
+
+    def arn_formatter(self, _id: str, account_id: str, region_name: str) -> str:
+        return f"arn:{get_partition(region_name)}:workspaces-web:{region_name}:{account_id}:network-settings/{_id}"
+
+    def to_dict(self) -> Dict[str, Any]:
+        return {
+            "associatedPortalArns": self.associated_portal_arns,
+            "networkSettingsArn": self.arn,
+            "securityGroupIds": self.security_group_ids,
+            "subnetIds": self.subnet_ids,
+            "Tags": self.tags,
+            "vpcId": self.vpc_id,
+        }
+
+
+class FakeBrowserSettings(BaseModel):
+    def __init__(
+        self,
+        additional_encryption_context: Any,
+        browser_policy: str,
+        client_token: str,
+        customer_managed_key: str,
+        tags: Dict[str, str],
+        region_name: str,
+        account_id: str,
+    ):
+        self.browser_settings_id = str(uuid.uuid4())
+        self.arn = self.arn_formatter(self.browser_settings_id, account_id, region_name)
+        self.additional_encryption_context = additional_encryption_context
+        self.browser_policy = browser_policy
+        self.client_token = client_token
+        self.customer_managed_key = customer_managed_key
+        self.tags = tags
+        self.associated_portal_arns: List[str] = []
+
+    def arn_formatter(self, _id: str, account_id: str, region_name: str) -> str:
+        return f"arn:{get_partition(region_name)}:workspaces-web:{region_name}:{account_id}:browser-settings/{_id}"
+
+    def to_dict(self) -> Dict[str, Any]:
+        return {
+            "associatedPortalArns": self.associated_portal_arns,
+            "browserSettingsArn": self.arn,
+            "additionalEncryptionContext": self.additional_encryption_context,
+            "browserPolicy": self.browser_policy,
+            "customerManagedKey": self.customer_managed_key,
+            "tags": self.tags,
+        }
+
+
+class FakePortal(BaseModel):
+    def __init__(
+        self,
+        additional_encryption_context: Any,
+        authentication_type: str,
+        client_token: str,
+        customer_managed_key: str,
+        display_name: str,
+        instance_type: str,
+        max_concurrent_sessions: str,
+        tags: Dict[str, str],
+        region_name: str,
+        account_id: str,
+    ):
+        self.portal_id = str(uuid.uuid4())
+        self.arn = self.arn_formatter(self.portal_id, account_id, region_name)
+        self.additional_encryption_context = additional_encryption_context
+        self.authentication_type = authentication_type
+        self.client_token = client_token
+        self.customer_managed_key = customer_managed_key
+        self.display_name = display_name
+        self.instance_type = instance_type
+        self.max_concurrent_sessions = max_concurrent_sessions
+        self.tags = tags
+        self.portal_endpoint = f"{self.portal_id}.portal.aws"
+        self.browser_type = "Chrome"
+        self.creation_time = datetime.datetime.now().isoformat()
+        self.status = "CREATED"
+        self.renderer_type = "AppStream"
+        self.status_reason = "TestStatusReason"
+        self.browser_settings_arn: Optional[str] = None
+        self.network_settings_arn: Optional[str] = None
+        self.trust_store_arn: Optional[str] = None
+        self.ip_access_settings_arn: Optional[str] = None
+        self.user_access_logging_settings_arn: Optional[str] = None
+        self.user_settings_arn: Optional[str] = None
+
+    def arn_formatter(self, _id: str, account_id: str, region_name: str) -> str:
+        return f"arn:{get_partition(region_name)}:workspaces-web:{region_name}:{account_id}:portal/{_id}"
+
+    def to_dict(self) -> Dict[str, Any]:
+        return {
+            "additionalEncryptionContext": self.additional_encryption_context,
+            "authenticationType": self.authentication_type,
+            "browserSettingsArn": self.browser_settings_arn,
+            "browserType": self.browser_type,
+            "creationDate": self.creation_time,
+            "customerManagedKey": self.customer_managed_key,
+            "displayName": self.display_name,
+            "instanceType": self.instance_type,
+            "ipAccessSettingsArn": self.ip_access_settings_arn,
+            "maxConcurrentSessions": self.max_concurrent_sessions,
+            "networkSettingsArn": self.network_settings_arn,
+            "portalArn": self.arn,
+            "portalEndpoint": self.portal_endpoint,
+            "portalStatus": self.status,
+            "rendererType": self.renderer_type,
+            "statusReason": self.status_reason,
+            "trustStoreArn": self.trust_store_arn,
+            "userAccessLoggingSettingsArn": self.user_access_logging_settings_arn,
+            "userSettingsArn": self.user_settings_arn,
+            "tags": self.tags,
+        }
+
+
+class WorkSpacesWebBackend(BaseBackend):
+    """Implementation of WorkSpacesWeb APIs."""
+
+    def __init__(self, region_name: str, account_id: str):
+        super().__init__(region_name, account_id)
+        self.network_settings: Dict[str, FakeNetworkSettings] = {}
+        self.browser_settings: Dict[str, FakeBrowserSettings] = {}
+        self.portals: Dict[str, FakePortal] = {}
+
+    def create_network_settings(
+        self,
+        security_group_ids: List[str],
+        subnet_ids: List[str],
+        tags: Dict[str, str],
+        vpc_id: str,
+    ) -> str:
+        network_settings_object = FakeNetworkSettings(
+            security_group_ids,
+            subnet_ids,
+            tags,
+            vpc_id,
+            self.region_name,
+            self.account_id,
+        )
+        self.network_settings[network_settings_object.arn] = network_settings_object
+        return network_settings_object.arn
+
+    def list_network_settings(self) -> List[Dict[str, str]]:
+        return [
+            {"networkSettingsArn": network_setting.arn, "vpcId": network_setting.vpc_id}
+            for network_setting in self.network_settings.values()
+        ]
+
+    def get_network_settings(self, network_settings_arn: str) -> Dict[str, Any]:
+        return self.network_settings[network_settings_arn].to_dict()
+
+    def delete_network_settings(self, network_settings_arn: str) -> None:
+        self.network_settings.pop(network_settings_arn)
+
+    def create_browser_settings(
+        self,
+        additional_encryption_context: Any,
+        browser_policy: str,
+        client_token: str,
+        customer_managed_key: str,
+        tags: Dict[str, str],
+    ) -> str:
+        browser_settings_object = FakeBrowserSettings(
+            additional_encryption_context,
+            browser_policy,
+            client_token,
+            customer_managed_key,
+            tags,
+            self.region_name,
+            self.account_id,
+        )
+        self.browser_settings[browser_settings_object.arn] = browser_settings_object
+        return browser_settings_object.arn
+
+    def list_browser_settings(self) -> List[Dict[str, str]]:
+        return [
+            {"browserSettingsArn": browser_setting.arn}
+            for browser_setting in self.browser_settings.values()
+        ]
+
+    def get_browser_settings(self, browser_settings_arn: str) -> Dict[str, Any]:
+        return self.browser_settings[browser_settings_arn].to_dict()
+
+    def delete_browser_settings(self, browser_settings_arn: str) -> None:
+        self.browser_settings.pop(browser_settings_arn)
+
+    def create_portal(
+        self,
+        additional_encryption_context: Any,
+        authentication_type: str,
+        client_token: str,
+        customer_managed_key: str,
+        display_name: str,
+        instance_type: str,
+        max_concurrent_sessions: str,
+        tags: Dict[str, str],
+    ) -> Tuple[str, str]:
+        portal_object = FakePortal(
+            additional_encryption_context,
+            authentication_type,
+            client_token,
+            customer_managed_key,
+            display_name,
+            instance_type,
+            max_concurrent_sessions,
+            tags,
+            self.region_name,
+            self.account_id,
+        )
+        self.portals[portal_object.arn] = portal_object
+        return portal_object.arn, portal_object.portal_endpoint
+
+    def list_portals(self) -> List[Dict[str, Any]]:
+        return [
+            {
+                "authenticationType": portal.authentication_type,
+                "browserSettingsArn": portal.browser_settings_arn,
+                "browserType": portal.browser_type,
+                "creationDate": portal.creation_time,
+                "customerManagedKey": portal.customer_managed_key,
+                "displayName": portal.display_name,
+                "instanceType": portal.instance_type,
+                "ipAccessSettingsArn": portal.ip_access_settings_arn,
+                "maxConcurrentSessions": portal.max_concurrent_sessions,
+                "networkSettingsArn": portal.network_settings_arn,
+                "portalArn": portal.arn,
+                "portalEndpoint": portal.portal_endpoint,
+                "portalStatus": portal.status,
+                "rendererType": portal.renderer_type,
+                "statusReason": portal.status_reason,
+                "trustStoreArn": portal.trust_store_arn,
+                "userAccessLoggingSettingsArn": portal.user_access_logging_settings_arn,
+                "userSettingsArn": portal.user_settings_arn,
+            }
+            for portal in self.portals.values()
+        ]
+
+    def get_portal(self, portal_arn: str) -> Dict[str, Any]:
+        return self.portals[portal_arn].to_dict()
+
+    def delete_portal(self, portal_arn: str) -> None:
+        self.portals.pop(portal_arn)
+
+    def associate_browser_settings(
+        self, browser_settings_arn: str, portal_arn: str
+    ) -> Tuple[str, str]:
+        browser_settings_object = self.browser_settings[browser_settings_arn]
+        portal_object = self.portals[portal_arn]
+        browser_settings_object.associated_portal_arns.append(portal_arn)
+        portal_object.browser_settings_arn = browser_settings_arn
+        return browser_settings_arn, portal_arn
+
+    def associate_network_settings(
+        self, network_settings_arn: str, portal_arn: str
+    ) -> Tuple[str, str]:
+        network_settings_object = self.network_settings[network_settings_arn]
+        portal_object = self.portals[portal_arn]
+        network_settings_object.associated_portal_arns.append(portal_arn)
+        portal_object.network_settings_arn = network_settings_arn
+        return network_settings_arn, portal_arn
+
+
+workspacesweb_backends = BackendDict(WorkSpacesWebBackend, "workspaces-web")
diff --git a/moto/workspacesweb/responses.py b/moto/workspacesweb/responses.py
new file mode 100644
index 000000000..dcda234f3
--- /dev/null
+++ b/moto/workspacesweb/responses.py
@@ -0,0 +1,185 @@
+"""Handles incoming workspacesweb requests, invokes methods, returns responses."""
+
+import json
+from typing import Any
+from urllib.parse import unquote
+
+from moto.core.responses import TYPE_RESPONSE, BaseResponse
+
+from .models import WorkSpacesWebBackend, workspacesweb_backends
+
+
+class WorkSpacesWebResponse(BaseResponse):
+    """Handler for WorkSpacesWeb requests and responses."""
+
+    def __init__(self) -> None:
+        super().__init__(service_name="workspaces-web")
+
+    @property
+    def workspacesweb_backend(self) -> WorkSpacesWebBackend:
+        """Return backend instance specific for this region."""
+        return workspacesweb_backends[self.current_account][self.region]
+
+    @staticmethod
+    def network_settings(request: Any, full_url: str, headers: Any) -> TYPE_RESPONSE:  # type: ignore[misc]
+        handler = WorkSpacesWebResponse()
+        handler.setup_class(request, full_url, headers)
+        if request.method == "GET":
+            return handler.get_network_settings()
+        else:
+            return handler.delete_network_settings()
+
+    @staticmethod
+    def browser_settings(request: Any, full_url: str, headers: Any) -> TYPE_RESPONSE:  # type: ignore[misc]
+        handler = WorkSpacesWebResponse()
+        handler.setup_class(request, full_url, headers)
+        if request.method == "GET":
+            return handler.get_browser_settings()
+        else:
+            return handler.delete_browser_settings()
+
+    @staticmethod
+    def portal(request: Any, full_url: str, headers: Any) -> TYPE_RESPONSE:  # type: ignore[misc]
+        handler = WorkSpacesWebResponse()
+        handler.setup_class(request, full_url, headers)
+        if request.method == "GET":
+            return handler.get_portal()
+        else:
+            return handler.delete_portal()
+
+    def create_browser_settings(self) -> str:
+        additional_encryption_context = self._get_param("additionalEncryptionContext")
+        browser_policy = self._get_param("browserPolicy")
+        client_token = self._get_param("clientToken")
+        customer_managed_key = self._get_param("customerManagedKey")
+        tags = self._get_param("tags")
+        browser_settings_arn = self.workspacesweb_backend.create_browser_settings(
+            additional_encryption_context=additional_encryption_context,
+            browser_policy=browser_policy,
+            client_token=client_token,
+            customer_managed_key=customer_managed_key,
+            tags=tags,
+        )
+        return json.dumps(dict(browserSettingsArn=browser_settings_arn))
+
+    def create_network_settings(self) -> str:
+        security_group_ids = self._get_param("securityGroupIds")
+        subnet_ids = self._get_param("subnetIds")
+        tags = self._get_param("tags")
+        vpc_id = self._get_param("vpcId")
+        network_settings_arn = self.workspacesweb_backend.create_network_settings(
+            security_group_ids=security_group_ids,
+            subnet_ids=subnet_ids,
+            tags=tags,
+            vpc_id=vpc_id,
+        )
+        return json.dumps(dict(networkSettingsArn=network_settings_arn))
+
+    def get_network_settings(self) -> TYPE_RESPONSE:
+        network_settings_arn = unquote(
+            self.parsed_url.path.split("/networkSettings/")[-1]
+        )
+        network_settings = self.workspacesweb_backend.get_network_settings(
+            network_settings_arn=network_settings_arn,
+        )
+        return 200, {}, json.dumps(dict(networkSettings=network_settings))
+
+    def create_portal(self) -> str:
+        additional_encryption_context = self._get_param("additionalEncryptionContext")
+        authentication_type = self._get_param("authenticationType")
+        client_token = self._get_param("clientToken")
+        customer_managed_key = self._get_param("customerManagedKey")
+        display_name = self._get_param("displayName")
+        instance_type = self._get_param("instanceType")
+        max_concurrent_sessions = self._get_param("maxConcurrentSessions")
+        tags = self._get_param("tags")
+        portal_arn, portal_endpoint = self.workspacesweb_backend.create_portal(
+            additional_encryption_context=additional_encryption_context,
+            authentication_type=authentication_type,
+            client_token=client_token,
+            customer_managed_key=customer_managed_key,
+            display_name=display_name,
+            instance_type=instance_type,
+            max_concurrent_sessions=max_concurrent_sessions,
+            tags=tags,
+        )
+        return json.dumps(dict(portalArn=portal_arn, portalEndpoint=portal_endpoint))
+
+    def list_browser_settings(self) -> str:
+        browser_settings = self.workspacesweb_backend.list_browser_settings()
+        return json.dumps(dict(browserSettings=browser_settings))
+
+    def list_network_settings(self) -> str:
+        network_settings = self.workspacesweb_backend.list_network_settings()
+        return json.dumps(dict(networkSettings=network_settings))
+
+    def list_portals(self) -> str:
+        portals = self.workspacesweb_backend.list_portals()
+        return json.dumps(dict(portals=portals))
+
+    def get_browser_settings(self) -> TYPE_RESPONSE:
+        browser_settings_arn = unquote(
+            self.parsed_url.path.split("/browserSettings/")[-1]
+        )
+        browser_settings = self.workspacesweb_backend.get_browser_settings(
+            browser_settings_arn=browser_settings_arn,
+        )
+        return 200, {}, json.dumps(dict(browserSettings=browser_settings))
+
+    def delete_browser_settings(self) -> TYPE_RESPONSE:
+        browser_settings_arn = unquote(
+            self.parsed_url.path.split("/browserSettings/")[-1]
+        )
+        self.workspacesweb_backend.delete_browser_settings(
+            browser_settings_arn=browser_settings_arn
+        )
+        return 200, {}, "{}"
+
+    def delete_network_settings(self) -> TYPE_RESPONSE:
+        network_settings_arn = unquote(
+            self.parsed_url.path.split("/networkSettings/")[-1]
+        )
+        self.workspacesweb_backend.delete_network_settings(
+            network_settings_arn=network_settings_arn,
+        )
+        return 200, {}, "{}"
+
+    def get_portal(self) -> TYPE_RESPONSE:
+        portal_arn = unquote(self.parsed_url.path.split("/portals/")[-1])
+        portal = self.workspacesweb_backend.get_portal(portal_arn=portal_arn)
+        return 200, {}, json.dumps(dict(portal=portal))
+
+    def delete_portal(self) -> TYPE_RESPONSE:
+        portal_arn = unquote(self.parsed_url.path.split("/portals/")[-1])
+        self.workspacesweb_backend.delete_portal(portal_arn=portal_arn)
+        return 200, {}, "{}"
+
+    def associate_browser_settings(self) -> str:
+        browser_settings_arn = unquote(self._get_param("browserSettingsArn"))
+        portal_arn = unquote(
+            self.parsed_url.path.split("/portals/")[-1].split("/browserSettings")[0]
+        )
+        browser_settings_arn, portal_arn = (
+            self.workspacesweb_backend.associate_browser_settings(
+                browser_settings_arn=browser_settings_arn,
+                portal_arn=portal_arn,
+            )
+        )
+        return json.dumps(
+            dict(browserSettingsArn=browser_settings_arn, portalArn=portal_arn)
+        )
+
+    def associate_network_settings(self) -> str:
+        network_settings_arn = unquote(self._get_param("networkSettingsArn"))
+        portal_arn = unquote(
+            self.parsed_url.path.split("/portals/")[-1].split("/networkSettings")[0]
+        )
+        network_settings_arn, portal_arn = (
+            self.workspacesweb_backend.associate_network_settings(
+                network_settings_arn=network_settings_arn,
+                portal_arn=portal_arn,
+            )
+        )
+        return json.dumps(
+            dict(networkSettingsArn=network_settings_arn, portalArn=portal_arn)
+        )
diff --git a/moto/workspacesweb/urls.py b/moto/workspacesweb/urls.py
new file mode 100644
index 000000000..6fe82a99e
--- /dev/null
+++ b/moto/workspacesweb/urls.py
@@ -0,0 +1,18 @@
+"""workspacesweb base URL and path."""
+
+from .responses import WorkSpacesWebResponse
+
+url_bases = [
+    r"https?://workspaces-web\.(.+)\.amazonaws\.com",
+]
+
+url_paths = {
+    "{0}/browserSettings$": WorkSpacesWebResponse.dispatch,
+    "{0}/networkSettings$": WorkSpacesWebResponse.dispatch,
+    "{0}/portals$": WorkSpacesWebResponse.dispatch,
+    "{0}/browserSettings/(?P<browserSettingsArn>.+)$": WorkSpacesWebResponse.browser_settings,
+    "{0}/networkSettings/(?P<networkSettingsArn>.+)$": WorkSpacesWebResponse.network_settings,
+    "{0}/portals/(?P<portalArn>[^/]+)portal/(?P<uuid>[^/]+)$": WorkSpacesWebResponse.portal,
+    "{0}/portals/(?P<portalArn>.*)/browserSettings$": WorkSpacesWebResponse.dispatch,
+    "{0}/portals/(?P<portalArn>.*)/networkSettings$": WorkSpacesWebResponse.dispatch,
+}


Test patch:
diff --git a/tests/test_apigateway/test_apigateway.py b/tests/test_apigateway/test_apigateway.py
index 62e40a123..2b0a3fb06 100644
--- a/tests/test_apigateway/test_apigateway.py
+++ b/tests/test_apigateway/test_apigateway.py
@@ -19,6 +19,7 @@ def test_create_and_get_rest_api():
         name="my_api", description="this is my api", disableExecuteApiEndpoint=True
     )
     api_id = response["id"]
+    root_resource_id = response["rootResourceId"]
 
     response = client.get_rest_api(restApiId=api_id)
 
@@ -34,6 +35,7 @@ def test_create_and_get_rest_api():
         "endpointConfiguration": {"types": ["EDGE"]},
         "tags": {},
         "disableExecuteApiEndpoint": True,
+        "rootResourceId": root_resource_id,
     }
 
 
@@ -42,6 +44,7 @@ def test_update_rest_api():
     client = boto3.client("apigateway", region_name="us-west-2")
     response = client.create_rest_api(name="my_api", description="this is my api")
     api_id = response["id"]
+    root_resource_id = response["rootResourceId"]
     patchOperations = [
         {"op": "replace", "path": "/name", "value": "new-name"},
         {"op": "replace", "path": "/description", "value": "new-description"},
@@ -71,6 +74,7 @@ def test_update_rest_api():
         "endpointConfiguration": {"types": ["EDGE"]},
         "tags": {},
         "disableExecuteApiEndpoint": True,
+        "rootResourceId": root_resource_id,
     }
     # should fail with wrong apikeysoruce
     patchOperations = [
@@ -2486,3 +2490,80 @@ def test_update_path_mapping_with_unknown_stage():
     assert ex.value.response["Error"]["Message"] == "Invalid stage identifier specified"
     assert ex.value.response["Error"]["Code"] == "BadRequestException"
     assert ex.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400
+
+
+@mock_aws
+def test_update_account():
+    client = boto3.client("apigateway", region_name="eu-west-1")
+
+    patch_operations = [
+        {
+            "op": "replace",
+            "path": "/cloudwatchRoleArn",
+            "value": "arn:aws:iam:123456789012:role/moto-test-apigw-role-1",
+        },
+        {"op": "add", "path": "/features", "value": "UsagePlans"},
+        {"op": "add", "path": "/features", "value": "TestFeature"},
+    ]
+
+    account = client.update_account(patchOperations=patch_operations)
+
+    assert (
+        account["cloudwatchRoleArn"]
+        == "arn:aws:iam:123456789012:role/moto-test-apigw-role-1"
+    )
+    assert account["features"] == ["UsagePlans", "TestFeature"]
+
+    patch_operations = [
+        {
+            "op": "replace",
+            "path": "/cloudwatchRoleArn",
+            "value": "arn:aws:iam:123456789012:role/moto-test-apigw-role-2",
+        },
+        {"op": "remove", "path": "/features", "value": "TestFeature"},
+    ]
+
+    account = client.update_account(patchOperations=patch_operations)
+
+    assert (
+        account["cloudwatchRoleArn"]
+        == "arn:aws:iam:123456789012:role/moto-test-apigw-role-2"
+    )
+    assert account["throttleSettings"]["burstLimit"] == 5000
+    assert account["throttleSettings"]["rateLimit"] == 10000.0
+    assert account["apiKeyVersion"] == "1"
+    assert account["features"] == ["UsagePlans"]
+
+
+@mock_aws
+def test_update_account_error():
+    client = boto3.client("apigateway", region_name="eu-west-1")
+    patch_operations = [
+        {
+            "op": "remove",
+            "path": "/features",
+            "value": "UsagePlans",
+        },
+    ]
+
+    with pytest.raises(ClientError) as ex:
+        client.update_account(patchOperations=patch_operations)
+
+    assert (
+        ex.value.response["Error"]["Message"]
+        == "Usage Plans cannot be disabled once enabled"
+    )
+    assert ex.value.response["Error"]["Code"] == "BadRequestException"
+    assert ex.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400
+
+
+@mock_aws
+def test_get_account():
+    client = boto3.client("apigateway", region_name="eu-west-1")
+    account = client.get_account()
+
+    assert account["throttleSettings"]["burstLimit"] == 5000
+    assert account["throttleSettings"]["rateLimit"] == 10000.0
+    assert account["apiKeyVersion"] == "1"
+    assert "features" not in account
+    assert "cloudwatchRoleArn" not in account
diff --git a/tests/test_firehose/test_firehose_destination_types.py b/tests/test_firehose/test_firehose_destination_types.py
index 265395d52..caec1af6c 100644
--- a/tests/test_firehose/test_firehose_destination_types.py
+++ b/tests/test_firehose/test_firehose_destination_types.py
@@ -102,6 +102,69 @@ def create_http_delivery_stream(client, stream_name):
     )
 
 
+def create_snowflake_delivery_stream(client, stream_name):
+    """Return delivery stream ARN of a Snowflake destination."""
+    return client.create_delivery_stream(
+        DeliveryStreamName=stream_name,
+        DeliveryStreamType="DirectPut",
+        SnowflakeDestinationConfiguration={
+            "RoleARN": f"arn:aws:iam::{ACCOUNT_ID}:role/firehose_delivery_role",
+            "AccountUrl": f"fake-account.{TEST_REGION}.snowflakecomputing.com",
+            "Database": "myDatabase",
+            "Schema": "mySchema",
+            "Table": "myTable",
+            "S3BackupMode": "FailedDataOnly",
+            "S3Configuration": {
+                "RoleARN": f"arn:aws:iam::{ACCOUNT_ID}:role/firehose_delivery_role",
+                "BucketARN": "arn:aws:s3:::firehose-test",
+            },
+        },
+    )
+
+
+@mock_aws
+def test_create_snowflake_delivery_stream():
+    """Verify fields of a Snowflake delivery stream."""
+    client = boto3.client("firehose", region_name=TEST_REGION)
+
+    stream_name = f"stream_{mock_random.get_random_hex(6)}"
+    response = create_snowflake_delivery_stream(client, stream_name)
+    stream_arn = response["DeliveryStreamARN"]
+
+    response = client.describe_delivery_stream(DeliveryStreamName=stream_name)
+    stream_description = response["DeliveryStreamDescription"]
+
+    # Sure and Freezegun don't play nicely together
+    stream_description.pop("CreateTimestamp")
+    stream_description.pop("LastUpdateTimestamp")
+
+    assert stream_description == {
+        "DeliveryStreamName": stream_name,
+        "DeliveryStreamARN": stream_arn,
+        "DeliveryStreamStatus": "ACTIVE",
+        "DeliveryStreamType": "DirectPut",
+        "Destinations": [
+            {
+                "DestinationId": "destinationId-000000000001",
+                "SnowflakeDestinationDescription": {
+                    "AccountUrl": f"fake-account.{TEST_REGION}.snowflakecomputing.com",
+                    "Database": "myDatabase",
+                    "RoleARN": f"arn:aws:iam::{ACCOUNT_ID}:role/firehose_delivery_role",
+                    "S3BackupMode": "FailedDataOnly",
+                    "S3DestinationDescription": {
+                        "RoleARN": f"arn:aws:iam::{ACCOUNT_ID}:role/firehose_delivery_role",
+                        "BucketARN": "arn:aws:s3:::firehose-test",
+                    },
+                    "Schema": "mySchema",
+                    "Table": "myTable",
+                },
+            },
+        ],
+        "HasMoreDestinations": False,
+        "VersionId": "1",
+    }
+
+
 @mock_aws
 def test_create_redshift_delivery_stream():
     """Verify fields of a Redshift delivery stream."""
diff --git a/tests/test_memorydb/__init__.py b/tests/test_memorydb/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/test_memorydb/test_memorydb.py b/tests/test_memorydb/test_memorydb.py
new file mode 100644
index 000000000..8b4ff0c5c
--- /dev/null
+++ b/tests/test_memorydb/test_memorydb.py
@@ -0,0 +1,661 @@
+"""Unit tests for memorydb-supported APIs."""
+
+import boto3
+import pytest
+from botocore.exceptions import ClientError
+
+from moto import mock_aws
+from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
+
+# See our Development Tips on writing tests for hints on how to write good tests:
+# http://docs.getmoto.org/en/latest/docs/contributing/development_tips/tests.html
+
+
+def create_subnet_group(client, region_name):
+    """Return valid Subnet group."""
+    ec2 = boto3.resource("ec2", region_name=region_name)
+    vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16")
+    subnet1 = ec2.create_subnet(VpcId=vpc.id, CidrBlock="10.0.0.0/24")
+    subnet2 = ec2.create_subnet(VpcId=vpc.id, CidrBlock="10.0.1.0/24")
+    subnet_group = client.create_subnet_group(
+        SubnetGroupName="my_subnet_group",
+        Description="This is my subnet group",
+        SubnetIds=[subnet1.id, subnet2.id],
+    )
+    return subnet_group
+
+
+@mock_aws
+def test_create_cluster():
+    client = boto3.client("memorydb", region_name="ap-southeast-1")
+    resp = client.create_cluster(
+        ClusterName="test-memory-db",
+        NodeType="db.t4g.small",
+        ACLName="open-access",
+    )
+    cluster = resp["Cluster"]
+    assert "Name" in cluster
+    assert "Status" in cluster
+    assert "NumberOfShards" in cluster
+
+
+@mock_aws
+def test_create_duplicate_cluster_fails():
+    client = boto3.client("memorydb", region_name="ap-southeast-1")
+    client.create_cluster(
+        ClusterName="foo-bar",
+        NodeType="db.t4g.small",
+        ACLName="open-access",
+    )
+    with pytest.raises(ClientError) as ex:
+        client.create_cluster(
+            ClusterName="foo-bar", NodeType="db.t4g.small", ACLName="open-access"
+        )
+    err = ex.value.response["Error"]
+    assert err["Code"] == "ClusterAlreadyExistsFault"
+
+
+@mock_aws
+def test_create_subnet_group():
+    client = boto3.client("memorydb", region_name="ap-southeast-1")
+    subnet_group = create_subnet_group(client, "ap-southeast-1")
+    sg = subnet_group["SubnetGroup"]
+    assert "Name" in sg
+    assert "Description" in sg
+    assert "VpcId" in sg
+    assert "Subnets" in sg
+    assert "ARN" in sg
+
+
+@mock_aws
+def test_create_cluster_with_subnet_group():
+    client = boto3.client("memorydb", region_name="ap-southeast-1")
+    subnet_group = create_subnet_group(client, "ap-southeast-1")
+    resp = client.create_cluster(
+        ClusterName="test-memory-db",
+        NodeType="db.t4g.small",
+        SubnetGroupName=subnet_group["SubnetGroup"]["Name"],
+        ACLName="open-access",
+    )
+    subnet_group = resp["Cluster"]["SubnetGroupName"] == "my_subnet_group"
+
+
+@mock_aws
+def test_create_duplicate_subnet_group_fails():
+    client = boto3.client("memorydb", region_name="ap-southeast-1")
+    create_subnet_group(client, "ap-southeast-1")
+    with pytest.raises(ClientError) as ex:
+        create_subnet_group(client, "ap-southeast-1")
+    err = ex.value.response["Error"]
+    assert err["Code"] == "SubnetGroupAlreadyExistsFault"
+
+
+@mock_aws
+def test_create_invalid_subnet_group_fails():
+    client = boto3.client("memorydb", region_name="ap-southeast-1")
+    with pytest.raises(ClientError) as ex:
+        client.create_subnet_group(SubnetGroupName="foo-bar", SubnetIds=["foo", "bar"])
+    err = ex.value.response["Error"]
+    assert err["Code"] == "InvalidSubnetError"
+
+
+@mock_aws
+def test_create_snapshot():
+    client = boto3.client("memorydb", region_name="ap-southeast-1")
+    subnet_group = create_subnet_group(client, "ap-southeast-1")
+    cluster = client.create_cluster(
+        ClusterName="test-memory-db",
+        Description="Test memorydb cluster",
+        NodeType="db.t4g.small",
+        SubnetGroupName=subnet_group["SubnetGroup"]["Name"],
+        ACLName="open-access",
+    )
+    resp = client.create_snapshot(
+        ClusterName=cluster["Cluster"]["Name"],
+        SnapshotName="my-snapshot-1",
+        KmsKeyId=f"arn:aws:kms:ap-southeast-1:{ACCOUNT_ID}:key/51d81fab-b138-4bd2-8a09-07fd6d37224d",
+        Tags=[
+            {"Key": "foo", "Value": "bar"},
+        ],
+    )
+    snapshot = resp["Snapshot"]
+    assert "Name" in snapshot
+    assert "Status" in snapshot
+    assert "Source" in snapshot
+    assert "KmsKeyId" in snapshot
+    assert "ARN" in snapshot
+    assert "ClusterConfiguration" in snapshot
+    assert "DataTiering" in snapshot
+
+
+@mock_aws
+def test_create_snapshot_with_non_existing_cluster_fails():
+    client = boto3.client("memorydb", region_name="ap-southeast-1")
+    with pytest.raises(ClientError) as ex:
+        client.create_snapshot(ClusterName="foobar", SnapshotName="my-snapshot-1")
+    err = ex.value.response["Error"]
+    assert err["Code"] == "ClusterNotFoundFault"
+
+
+@mock_aws
+def test_create_duplicate_snapshot_fails():
+    client = boto3.client("memorydb", region_name="ap-southeast-1")
+    cluster = client.create_cluster(
+        ClusterName="test-memory-db",
+        NodeType="db.t4g.small",
+        ACLName="open-access",
+    )
+    client.create_snapshot(
+        ClusterName=cluster["Cluster"]["Name"], SnapshotName="my-snapshot-1"
+    )
+    with pytest.raises(ClientError) as ex:
+        client.create_snapshot(
+            ClusterName=cluster["Cluster"]["Name"], SnapshotName="my-snapshot-1"
+        )
+    err = ex.value.response["Error"]
+    assert err["Code"] == "SnapshotAlreadyExistsFault"
+
+
+@mock_aws
+def test_describe_clusters():
+    client = boto3.client("memorydb", region_name="ap-southeast-1")
+    for i in range(1, 3):
+        client.create_cluster(
+            ClusterName=f"test-memory-db-{i}",
+            NodeType="db.t4g.small",
+            ACLName="open-access",
+        )
+    resp = client.describe_clusters()
+    assert "Clusters" in resp
+    assert len(resp["Clusters"]) == 2
+    assert "Shards" not in resp["Clusters"][0]
+
+
+@mock_aws
+def test_describe_clusters_with_shard_details():
+    client = boto3.client("memorydb", region_name="ap-southeast-1")
+    for i in range(1, 3):
+        client.create_cluster(
+            ClusterName=f"test-memory-db-{i}",
+            NodeType="db.t4g.small",
+            ACLName="open-access",
+        )
+    resp = client.describe_clusters(
+        ClusterName="test-memory-db-1",
+        ShowShardDetails=True,
+    )
+    assert resp["Clusters"][0]["Name"] == "test-memory-db-1"
+    assert len(resp["Clusters"]) == 1
+    assert "Shards" in resp["Clusters"][0]
+
+
+@mock_aws
+def test_describe_clusters_with_cluster_name():
+    client = boto3.client("memorydb", region_name="ap-southeast-1")
+    for i in range(1, 3):
+        client.create_cluster(
+            ClusterName=f"test-memory-db-{i}",
+            NodeType="db.t4g.small",
+            ACLName="open-access",
+        )
+    resp = client.describe_clusters(
+        ClusterName="test-memory-db-1",
+    )
+    assert resp["Clusters"][0]["Name"] == "test-memory-db-1"
+    assert len(resp["Clusters"]) == 1
+
+
+@mock_aws
+def test_describe_snapshots():
+    client = boto3.client("memorydb", region_name="eu-west-1")
+    for i in range(1, 3):
+        client.create_cluster(
+            ClusterName=f"test-memory-db-{i}",
+            NodeType="db.t4g.small",
+            ACLName="open-access",
+        )
+        client.create_snapshot(
+            ClusterName=f"test-memory-db-{i}", SnapshotName=f"my-snapshot-{i}"
+        )
+    resp = client.describe_snapshots()
+    assert "Snapshots" in resp
+    assert len(resp["Snapshots"]) == 2
+    assert resp["Snapshots"][0]["Name"] == "my-snapshot-1"
+
+
+@mock_aws
+def test_describe_snapshots_with_cluster_name():
+    client = boto3.client("memorydb", region_name="eu-west-1")
+    for i in range(1, 3):
+        client.create_cluster(
+            ClusterName=f"test-memory-db-{i}",
+            NodeType="db.t4g.small",
+            ACLName="open-access",
+        )
+        client.create_snapshot(
+            ClusterName=f"test-memory-db-{i}", SnapshotName=f"my-snapshot-{i}"
+        )
+    resp = client.describe_snapshots(ClusterName="test-memory-db-2")
+    assert len(resp["Snapshots"]) == 1
+    assert resp["Snapshots"][0]["ClusterConfiguration"]["Name"] == "test-memory-db-2"
+    assert "Shards" not in resp["Snapshots"][0]["ClusterConfiguration"]
+
+
+@mock_aws
+def test_describe_snapshots_with_shard_details():
+    client = boto3.client("memorydb", region_name="eu-west-1")
+    for i in range(1, 3):
+        client.create_cluster(
+            ClusterName=f"test-memory-db-{i}",
+            NodeType="db.t4g.small",
+            ACLName="open-access",
+        )
+        client.create_snapshot(
+            ClusterName=f"test-memory-db-{i}", SnapshotName=f"my-snapshot-{i}"
+        )
+    resp = client.describe_snapshots(ClusterName="test-memory-db-2", ShowDetail=True)
+    assert len(resp["Snapshots"]) == 1
+    assert resp["Snapshots"][0]["ClusterConfiguration"]["Name"] == "test-memory-db-2"
+    assert "Shards" in resp["Snapshots"][0]["ClusterConfiguration"]
+
+
+@mock_aws
+def test_describe_snapshots_with_snapshot_name():
+    client = boto3.client("memorydb", region_name="eu-west-1")
+    for i in range(1, 3):
+        client.create_cluster(
+            ClusterName=f"test-memory-db-{i}",
+            NodeType="db.t4g.small",
+            ACLName="open-access",
+        )
+        client.create_snapshot(
+            ClusterName=f"test-memory-db-{i}", SnapshotName=f"my-snapshot-{i}"
+        )
+    resp = client.describe_snapshots(
+        SnapshotName="my-snapshot-1",
+    )
+    assert len(resp["Snapshots"]) == 1
+    assert resp["Snapshots"][0]["Name"] == "my-snapshot-1"
+
+
+@mock_aws
+def test_describe_snapshots_with_snapshot_and_cluster():
+    client = boto3.client("memorydb", region_name="eu-west-1")
+
+    client.create_cluster(
+        ClusterName="test-memory-db", NodeType="db.t4g.small", ACLName="open-access"
+    )
+    for i in range(1, 3):
+        client.create_snapshot(
+            ClusterName="test-memory-db", SnapshotName=f"my-snapshot-{i}"
+        )
+    resp = client.describe_snapshots(
+        ClusterName="test-memory-db",
+        SnapshotName="my-snapshot-1",
+    )
+    assert len(resp["Snapshots"]) == 1
+    assert resp["Snapshots"][0]["Name"] == "my-snapshot-1"
+
+
+@mock_aws
+def test_describe_snapshots_with_invalid_cluster():
+    client = boto3.client("memorydb", region_name="eu-west-1")
+
+    resp = client.describe_snapshots(
+        ClusterName="foobar",
+    )
+    assert len(resp["Snapshots"]) == 0
+
+
+@mock_aws
+def test_describe_snapshots_invalid_snapshot_fails():
+    client = boto3.client("memorydb", region_name="eu-west-1")
+
+    with pytest.raises(ClientError) as ex:
+        client.describe_snapshots(SnapshotName="foobar")
+    err = ex.value.response["Error"]
+    assert err["Code"] == "SnapshotNotFoundFault"
+
+
+@mock_aws
+def test_describe_snapshots_with_cluster_and_invalid_snapshot_fails():
+    client = boto3.client("memorydb", region_name="eu-west-1")
+
+    client.create_cluster(
+        ClusterName="test-memory-db", NodeType="db.t4g.small", ACLName="open-access"
+    )
+    client.create_snapshot(ClusterName="test-memory-db", SnapshotName="my-snapshot")
+
+    with pytest.raises(ClientError) as ex:
+        client.describe_snapshots(ClusterName="test-memory-db", SnapshotName="foobar")
+    err = ex.value.response["Error"]
+    assert err["Code"] == "SnapshotNotFoundFault"
+
+
+@mock_aws
+def test_describe_subnet_groups():
+    client = boto3.client("memorydb", region_name="eu-west-1")
+    ec2 = boto3.resource("ec2", region_name="eu-west-1")
+    vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16")
+    subnet1 = ec2.create_subnet(VpcId=vpc.id, CidrBlock="10.0.0.0/24")
+    subnet2 = ec2.create_subnet(VpcId=vpc.id, CidrBlock="10.0.1.0/24")
+    for i in range(1, 3):
+        client.create_subnet_group(
+            SubnetGroupName=f"my_subnet_group-{i}",
+            Description="This is my subnet group",
+            SubnetIds=[subnet1.id, subnet2.id],
+        )
+    resp = client.describe_subnet_groups()
+    assert "SubnetGroups" in resp
+    assert len(resp["SubnetGroups"]) == 3  # Including default subnet group
+
+
+@mock_aws
+def test_describe_subnet_groups_with_subnet_group_name():
+    client = boto3.client("memorydb", region_name="eu-west-1")
+    ec2 = boto3.resource("ec2", region_name="eu-west-1")
+    vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16")
+    subnet1 = ec2.create_subnet(VpcId=vpc.id, CidrBlock="10.0.0.0/24")
+    subnet2 = ec2.create_subnet(VpcId=vpc.id, CidrBlock="10.0.1.0/24")
+    for i in range(1, 3):
+        client.create_subnet_group(
+            SubnetGroupName=f"my_subnet_group-{i}",
+            Description="This is my subnet group",
+            SubnetIds=[subnet1.id, subnet2.id],
+        )
+    resp = client.describe_subnet_groups(SubnetGroupName="my_subnet_group-1")
+    assert len(resp["SubnetGroups"]) == 1
+    assert resp["SubnetGroups"][0]["Name"] == "my_subnet_group-1"
+
+
+@mock_aws
+def test_describe_subnet_groups_invalid_subnetgroupname_fails():
+    client = boto3.client("memorydb", region_name="eu-west-1")
+    with pytest.raises(ClientError) as ex:
+        client.describe_subnet_groups(SubnetGroupName="foobar")
+    err = ex.value.response["Error"]
+    assert err["Code"] == "SubnetGroupNotFoundFault"
+
+
+@mock_aws
+def test_list_tags():
+    client = boto3.client("memorydb", region_name="us-east-2")
+    cluster = client.create_cluster(
+        ClusterName="test-memory-db",
+        NodeType="db.t4g.small",
+        ACLName="open-access",
+        Tags=[
+            {"Key": "foo", "Value": "bar"},
+        ],
+    )
+    resp = client.list_tags(ResourceArn=cluster["Cluster"]["ARN"])
+    assert "TagList" in resp
+    assert len(resp["TagList"]) == 1
+    assert "foo" in resp["TagList"][0]["Key"]
+    assert "bar" in resp["TagList"][0]["Value"]
+
+
+@mock_aws
+def test_list_tags_invalid_cluster_fails():
+    client = boto3.client("memorydb", region_name="us-east-2")
+    with pytest.raises(ClientError) as ex:
+        client.list_tags(
+            ResourceArn=f"arn:aws:memorydb:us-east-1:{ACCOUNT_ID}:cluster/foobar",
+        )
+    err = ex.value.response["Error"]
+    assert err["Code"] == "ClusterNotFoundFault"
+
+
+@mock_aws
+def test_tag_resource():
+    client = boto3.client("memorydb", region_name="us-east-2")
+    cluster = client.create_cluster(
+        ClusterName="test-memory-db",
+        NodeType="db.t4g.small",
+        ACLName="open-access",
+        Tags=[
+            {"Key": "key1", "Value": "value1"},
+        ],
+    )
+    resp = client.tag_resource(
+        ResourceArn=cluster["Cluster"]["ARN"],
+        Tags=[
+            {"Key": "key2", "Value": "value2"},
+        ],
+    )
+    assert "TagList" in resp
+    assert len(resp["TagList"]) == 2
+    assert "key2" in resp["TagList"][1]["Key"]
+    assert "value2" in resp["TagList"][1]["Value"]
+
+
+@mock_aws
+def test_tag_resource_invalid_cluster_fails():
+    client = boto3.client("memorydb", region_name="us-east-2")
+    with pytest.raises(ClientError) as ex:
+        client.tag_resource(
+            ResourceArn=f"arn:aws:memorydb:us-east-1:{ACCOUNT_ID}:cluster/foobar",
+            Tags=[{"Key": "key2", "Value": "value2"}],
+        )
+    err = ex.value.response["Error"]
+    assert err["Code"] == "ClusterNotFoundFault"
+
+
+@mock_aws
+def test_untag_resource():
+    client = boto3.client("memorydb", region_name="us-east-2")
+    cluster = client.create_cluster(
+        ClusterName="test-memory-db",
+        NodeType="db.t4g.small",
+        ACLName="open-access",
+        Tags=[{"Key": "key1", "Value": "value1"}, {"Key": "key2", "Value": "value2"}],
+    )
+    resp = client.untag_resource(
+        ResourceArn=cluster["Cluster"]["ARN"],
+        TagKeys=[
+            "key1",
+        ],
+    )
+    assert "TagList" in resp
+    assert len(resp["TagList"]) == 1
+    assert "key2" in resp["TagList"][0]["Key"]
+    assert "value2" in resp["TagList"][0]["Value"]
+
+
+@mock_aws
+def test_untag_resource_invalid_cluster_fails():
+    client = boto3.client("memorydb", region_name="us-east-2")
+    with pytest.raises(ClientError) as ex:
+        client.untag_resource(
+            ResourceArn=f"arn:aws:memorydb:us-east-1:{ACCOUNT_ID}:cluster/foobar",
+            TagKeys=["key1"],
+        )
+    err = ex.value.response["Error"]
+    assert err["Code"] == "ClusterNotFoundFault"
+
+
+@mock_aws
+def test_untag_resource_invalid_keys_fails():
+    client = boto3.client("memorydb", region_name="us-east-2")
+    cluster = client.create_cluster(
+        ClusterName="test-memory-db",
+        NodeType="db.t4g.small",
+        ACLName="open-access",
+        Tags=[{"Key": "key1", "Value": "value1"}, {"Key": "key2", "Value": "value2"}],
+    )
+    with pytest.raises(ClientError) as ex:
+        client.untag_resource(
+            ResourceArn=cluster["Cluster"]["ARN"], TagKeys=["key3", "key4"]
+        )
+    err = ex.value.response["Error"]
+    assert err["Code"] == "TagNotFoundFault"
+
+
+@mock_aws
+def test_update_cluster_replica_count():
+    client = boto3.client("memorydb", region_name="eu-west-1")
+    cluster = client.create_cluster(
+        ClusterName="test-memory-db",
+        NodeType="db.t4g.small",
+        ACLName="open-access",
+    )
+    desc_before_update = client.describe_clusters(ShowShardDetails=True)
+    assert desc_before_update["Clusters"][0]["Shards"][0]["NumberOfNodes"] == 2
+    client.update_cluster(
+        ClusterName=cluster["Cluster"]["Name"],
+        Description="Good cluster",
+        MaintenanceWindow="thu:23:00-thu:01:30",
+        ReplicaConfiguration={"ReplicaCount": 2},
+    )
+    desc_after_update = client.describe_clusters(ShowShardDetails=True)
+    cluster_after_update = desc_after_update["Clusters"][0]
+    assert cluster_after_update["Description"] == "Good cluster"
+    assert cluster_after_update["MaintenanceWindow"] == "thu:23:00-thu:01:30"
+    assert cluster_after_update["Shards"][0]["NumberOfNodes"] == 3
+
+
+@mock_aws
+def test_update_cluster_shards():
+    client = boto3.client("memorydb", region_name="eu-west-1")
+    cluster = client.create_cluster(
+        ClusterName="test-memory-db",
+        NodeType="db.t4g.small",
+        ACLName="open-access",
+    )
+    desc_before_update = client.describe_clusters(ShowShardDetails=True)
+    assert desc_before_update["Clusters"][0]["NumberOfShards"] == 1
+    client.update_cluster(
+        ClusterName=cluster["Cluster"]["Name"],
+        ShardConfiguration={"ShardCount": 2},
+    )
+    desc_after_update = client.describe_clusters(ShowShardDetails=True)
+    assert desc_after_update["Clusters"][0]["NumberOfShards"] == 2
+
+
+@mock_aws
+def test_update_invalid_cluster_fails():
+    client = boto3.client("memorydb", region_name="eu-west-1")
+    with pytest.raises(ClientError) as ex:
+        client.update_cluster(
+            ClusterName="foobar",
+            Description="Good cluster",
+        )
+    err = ex.value.response["Error"]
+    assert err["Code"] == "ClusterNotFoundFault"
+
+
+@mock_aws
+def test_delete_cluster():
+    client = boto3.client("memorydb", region_name="eu-west-1")
+    cluster = client.create_cluster(
+        ClusterName="test-memory-db",
+        NodeType="db.t4g.small",
+        ACLName="open-access",
+    )
+    desc_resp_before = client.describe_clusters()
+    assert len(desc_resp_before["Clusters"]) == 1
+    resp = client.delete_cluster(
+        ClusterName=cluster["Cluster"]["Name"],
+    )
+    assert resp["Cluster"]["Name"] == cluster["Cluster"]["Name"]
+    desc_resp_after = client.describe_clusters()
+    assert len(desc_resp_after["Clusters"]) == 0
+
+
+@mock_aws
+def test_delete_cluster_with_snapshot():
+    client = boto3.client("memorydb", region_name="eu-west-1")
+    cluster = client.create_cluster(
+        ClusterName="test-memory-db",
+        NodeType="db.t4g.small",
+        ACLName="open-access",
+    )
+    desc_resp_before = client.describe_snapshots()
+    assert len(desc_resp_before["Snapshots"]) == 0
+    resp = client.delete_cluster(
+        ClusterName=cluster["Cluster"]["Name"],
+        FinalSnapshotName="test-memory-db-snapshot",
+    )
+    assert resp["Cluster"]["Name"] == cluster["Cluster"]["Name"]
+    desc_resp_after = client.describe_snapshots()
+    assert len(desc_resp_after["Snapshots"]) == 1
+    assert desc_resp_after["Snapshots"][0]["Name"] == "test-memory-db-snapshot"
+
+
+@mock_aws
+def test_delete_invalid_cluster_fails():
+    client = boto3.client("memorydb", region_name="eu-west-1")
+    with pytest.raises(ClientError) as ex:
+        client.delete_cluster(
+            ClusterName="foobar",
+        )
+    err = ex.value.response["Error"]
+    assert err["Code"] == "ClusterNotFoundFault"
+
+
+@mock_aws
+def test_delete_snapshot():
+    client = boto3.client("memorydb", region_name="us-east-2")
+    cluster = client.create_cluster(
+        ClusterName="test-memory-db",
+        NodeType="db.t4g.small",
+        ACLName="open-access",
+    )
+    snapshot = client.create_snapshot(
+        ClusterName=cluster["Cluster"]["Name"],
+        SnapshotName="my-snapshot-1",
+    )
+    desc_resp_before = client.describe_snapshots()
+    assert len(desc_resp_before["Snapshots"]) == 1
+    resp = client.delete_snapshot(SnapshotName=snapshot["Snapshot"]["Name"])
+    assert "Snapshot" in resp
+    desc_resp_after = client.describe_snapshots()
+    assert len(desc_resp_after["Snapshots"]) == 0
+
+
+@mock_aws
+def test_delete_invalid_snapshot_fails():
+    client = boto3.client("memorydb", region_name="us-east-2")
+    with pytest.raises(ClientError) as ex:
+        client.delete_snapshot(SnapshotName="foobar")
+    err = ex.value.response["Error"]
+    assert err["Code"] == "SnapshotNotFoundFault"
+
+
+@mock_aws
+def test_delete_subnet_group():
+    client = boto3.client("memorydb", region_name="us-east-2")
+    subnet_group = create_subnet_group(client, "us-east-2")
+    sg = subnet_group["SubnetGroup"]
+    response = client.describe_subnet_groups()
+    assert len(response["SubnetGroups"]) == 2
+    resp = client.delete_subnet_group(SubnetGroupName=sg["Name"])
+    assert "SubnetGroup" in resp
+    response = client.describe_subnet_groups()
+    assert len(response["SubnetGroups"]) == 1  # default subnet group
+
+
+@mock_aws
+def test_delete_subnet_group_default_fails():
+    client = boto3.client("memorydb", region_name="us-east-2")
+
+    with pytest.raises(ClientError) as ex:
+        client.delete_subnet_group(SubnetGroupName="default")
+    err = ex.value.response["Error"]
+    assert err["Code"] == "InvalidParameterValueException"
+
+
+@mock_aws
+def test_delete_subnet_group_in_use_fails():
+    client = boto3.client("memorydb", region_name="us-east-2")
+    subnet_group = create_subnet_group(client, "us-east-2")
+    client.create_cluster(
+        ClusterName="test-memory-db",
+        NodeType="db.t4g.small",
+        SubnetGroupName=subnet_group["SubnetGroup"]["Name"],
+        ACLName="open-access",
+    )
+    with pytest.raises(ClientError) as ex:
+        client.delete_subnet_group(SubnetGroupName=subnet_group["SubnetGroup"]["Name"])
+    err = ex.value.response["Error"]
+    assert err["Code"] == "SubnetGroupInUseFault"
diff --git a/tests/test_workspacesweb/__init__.py b/tests/test_workspacesweb/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/test_workspacesweb/test_workspacesweb.py b/tests/test_workspacesweb/test_workspacesweb.py
new file mode 100644
index 000000000..854a7599b
--- /dev/null
+++ b/tests/test_workspacesweb/test_workspacesweb.py
@@ -0,0 +1,271 @@
+"""Unit tests for workspacesweb-supported APIs."""
+
+import re
+
+import boto3
+
+from moto import mock_aws
+
+# See our Development Tips on writing tests for hints on how to write good tests:
+# http://docs.getmoto.org/en/latest/docs/contributing/development_tips/tests.html
+
+FAKE_SECURITY_GROUP_IDS = ["sg-0123456789abcdef0"]
+FAKE_SUBNET_IDS = ["subnet-0123456789abcdef0", "subnet-abcdef0123456789"]
+FAKE_TAGS = [
+    {"Key": "TestKey", "Value": "TestValue"},
+]
+FAKE_VPC_ID = "vpc-0123456789abcdef0"
+FAKE_KMS_KEY_ID = "abcd1234-5678-90ab-cdef-FAKEKEY"
+
+
+@mock_aws
+def test_create_network_settings():
+    client = boto3.client("workspaces-web", region_name="eu-west-1")
+    response = client.create_network_settings(
+        securityGroupIds=FAKE_SECURITY_GROUP_IDS,
+        subnetIds=FAKE_SUBNET_IDS,
+        tags=FAKE_TAGS,
+        vpcId=FAKE_VPC_ID,
+    )
+    network_settings_arn = response["networkSettingsArn"]
+    arn_regex = r"^arn:aws:workspaces-web:eu-west-1:\d{12}:network-settings/[a-f0-9-]+$"
+    assert re.match(arn_regex, network_settings_arn) is not None
+
+
+@mock_aws
+def test_list_network_settings():
+    client = boto3.client("workspaces-web", region_name="ap-southeast-1")
+    arn = client.create_network_settings(
+        securityGroupIds=FAKE_SECURITY_GROUP_IDS,
+        subnetIds=FAKE_SUBNET_IDS,
+        tags=FAKE_TAGS,
+        vpcId=FAKE_VPC_ID,
+    )["networkSettingsArn"]
+    resp = client.list_network_settings()
+    assert resp["networkSettings"][0]["networkSettingsArn"] == arn
+    assert len(resp["networkSettings"]) == 1
+
+
+@mock_aws
+def test_get_network_settings():
+    client = boto3.client("workspaces-web", region_name="ap-southeast-1")
+    arn = client.create_network_settings(
+        securityGroupIds=FAKE_SECURITY_GROUP_IDS,
+        subnetIds=FAKE_SUBNET_IDS,
+        tags=FAKE_TAGS,
+        vpcId=FAKE_VPC_ID,
+    )["networkSettingsArn"]
+    resp = client.get_network_settings(networkSettingsArn=arn)["networkSettings"]
+    assert resp["networkSettingsArn"] == arn
+
+
+@mock_aws
+def test_delete_network_settings():
+    client = boto3.client("workspaces-web", region_name="eu-west-1")
+    arn = client.create_network_settings(
+        securityGroupIds=FAKE_SECURITY_GROUP_IDS,
+        subnetIds=FAKE_SUBNET_IDS,
+        tags=FAKE_TAGS,
+        vpcId=FAKE_VPC_ID,
+    )["networkSettingsArn"]
+    client.delete_network_settings(networkSettingsArn=arn)
+    resp = client.list_network_settings()
+    assert resp["networkSettings"] == [], "Expected no network settings"
+
+
+@mock_aws
+def test_create_browser_settings():
+    client = boto3.client("workspaces-web", region_name="eu-west-1")
+    response = client.create_browser_settings(
+        additionalEncryptionContext={"Key1": "Value1", "Key2": "Value2"},
+        browserPolicy="TestBrowserPolicy",
+        clientToken="TestClient",
+        customerManagedKey=FAKE_KMS_KEY_ID,
+        tags=FAKE_TAGS,
+    )
+    browser_settings_arn = response["browserSettingsArn"]
+    arn_regex = r"^arn:aws:workspaces-web:eu-west-1:\d{12}:browser-settings/[a-f0-9-]+$"
+    assert re.match(arn_regex, browser_settings_arn) is not None
+
+
+@mock_aws
+def test_list_browser_settings():
+    client = boto3.client("workspaces-web", region_name="eu-west-1")
+    arn = client.create_browser_settings(
+        additionalEncryptionContext={"Key1": "Value1", "Key2": "Value2"},
+        browserPolicy="TestBrowserPolicy",
+        clientToken="TestClient",
+        customerManagedKey=FAKE_KMS_KEY_ID,
+        tags=FAKE_TAGS,
+    )["browserSettingsArn"]
+    resp = client.list_browser_settings()
+    assert resp["browserSettings"][0]["browserSettingsArn"] == arn
+
+
+@mock_aws
+def test_get_browser_settings():
+    client = boto3.client("workspaces-web", region_name="eu-west-1")
+    arn = client.create_browser_settings(
+        additionalEncryptionContext={"Key1": "Value1", "Key2": "Value2"},
+        browserPolicy="TestBrowserPolicy",
+        clientToken="TestClient",
+        customerManagedKey=FAKE_KMS_KEY_ID,
+        tags=FAKE_TAGS,
+    )["browserSettingsArn"]
+    resp = client.get_browser_settings(browserSettingsArn=arn)["browserSettings"]
+    assert resp["browserSettingsArn"] == arn
+
+
+@mock_aws
+def test_delete_browser_settings():
+    client = boto3.client("workspaces-web", region_name="eu-west-1")
+    arn = client.create_browser_settings(
+        additionalEncryptionContext={"Key1": "Value1", "Key2": "Value2"},
+        browserPolicy="TestBrowserPolicy",
+        clientToken="TestClient",
+        customerManagedKey=FAKE_KMS_KEY_ID,
+        tags=FAKE_TAGS,
+    )["browserSettingsArn"]
+    client.delete_browser_settings(browserSettingsArn=arn)
+    resp = client.list_browser_settings()
+    assert resp["browserSettings"] == []
+
+
+@mock_aws
+def test_create_portal():
+    client = boto3.client("workspaces-web", region_name="eu-west-1")
+    resp = client.create_portal(
+        additionalEncryptionContext={"Key1": "Value1", "Key2": "Value2"},
+        authenticationType="Standard",
+        clientToken="TestClient",
+        customerManagedKey=FAKE_KMS_KEY_ID,
+        displayName="TestDisplayName",
+        instanceType="TestInstanceType",
+        maxConcurrentSessions=5,
+        tags=FAKE_TAGS,
+    )
+    assert resp["portalArn"]
+    assert resp["portalEndpoint"]
+
+
+@mock_aws
+def test_list_portals():
+    client = boto3.client("workspaces-web", region_name="eu-west-1")
+    arn = client.create_portal(
+        additionalEncryptionContext={"Key1": "Value1", "Key2": "Value2"},
+        authenticationType="Standard",
+        clientToken="TestClient",
+        customerManagedKey=FAKE_KMS_KEY_ID,
+        displayName="TestDisplayName",
+        instanceType="TestInstanceType",
+        maxConcurrentSessions=5,
+        tags=FAKE_TAGS,
+    )["portalArn"]
+    resp = client.list_portals()
+    assert resp["portals"][0]["portalArn"] == arn
+    assert len(resp["portals"]) == 1
+    assert resp["portals"][0]["authenticationType"] == "Standard"
+
+
+@mock_aws
+def test_get_portal():
+    client = boto3.client("workspaces-web", region_name="eu-west-1")
+    arn = client.create_portal(
+        additionalEncryptionContext={"Key1": "Value1", "Key2": "Value2"},
+        authenticationType="Standard",
+        clientToken="TestClient",
+        customerManagedKey=FAKE_KMS_KEY_ID,
+        displayName="TestDisplayName",
+        instanceType="TestInstanceType",
+        maxConcurrentSessions=5,
+        tags=FAKE_TAGS,
+    )["portalArn"]
+    resp = client.get_portal(portalArn=arn)["portal"]
+    assert resp["portalArn"] == arn, f"Expected ARN {arn} in response"
+    assert (
+        resp["authenticationType"] == "Standard"
+    ), "Expected authentication type in response"
+    assert (
+        resp["instanceType"] == "TestInstanceType"
+    ), "Expected instance type in response"
+    assert (
+        resp["maxConcurrentSessions"] == 5
+    ), "Expected max concurrent sessions in response"
+
+
+@mock_aws
+def test_delete_portal():
+    client = boto3.client("workspaces-web", region_name="eu-west-1")
+    arn = client.create_portal(
+        additionalEncryptionContext={"Key1": "Value1", "Key2": "Value2"},
+        authenticationType="Standard",
+        clientToken="TestClient",
+        customerManagedKey=FAKE_KMS_KEY_ID,
+        displayName="TestDisplayName",
+        instanceType="TestInstanceType",
+        maxConcurrentSessions=5,
+        tags=FAKE_TAGS,
+    )["portalArn"]
+    client.delete_portal(portalArn=arn)
+    assert len(client.list_portals()["portals"]) == 0, "Expected no portals"
+
+
+@mock_aws
+def test_associate_browser_settings():
+    client = boto3.client("workspaces-web", region_name="eu-west-1")
+    browser_settings_arn = client.create_browser_settings(
+        additionalEncryptionContext={"Key1": "Value1", "Key2": "Value2"},
+        browserPolicy="TestBrowserPolicy",
+        clientToken="TestClient",
+        customerManagedKey=FAKE_KMS_KEY_ID,
+        tags=FAKE_TAGS,
+    )["browserSettingsArn"]
+    portal_arn = client.create_portal(
+        additionalEncryptionContext={"Key1": "Value1", "Key2": "Value2"},
+        authenticationType="Standard",
+        clientToken="TestClient",
+        customerManagedKey=FAKE_KMS_KEY_ID,
+        displayName="TestDisplayName",
+        instanceType="TestInstanceType",
+        maxConcurrentSessions=5,
+        tags=FAKE_TAGS,
+    )["portalArn"]
+    client.associate_browser_settings(
+        browserSettingsArn=browser_settings_arn, portalArn=portal_arn
+    )
+    resp = client.get_portal(portalArn=portal_arn)["portal"]
+    assert resp["browserSettingsArn"] == browser_settings_arn
+    resp = client.get_browser_settings(browserSettingsArn=browser_settings_arn)[
+        "browserSettings"
+    ]
+    assert resp["associatedPortalArns"] == [portal_arn]
+
+
+@mock_aws
+def test_associate_network_settings():
+    client = boto3.client("workspaces-web", region_name="eu-west-1")
+    network_arn = client.create_network_settings(
+        securityGroupIds=FAKE_SECURITY_GROUP_IDS,
+        subnetIds=FAKE_SUBNET_IDS,
+        tags=FAKE_TAGS,
+        vpcId=FAKE_VPC_ID,
+    )["networkSettingsArn"]
+    portal_arn = client.create_portal(
+        additionalEncryptionContext={"Key1": "Value1", "Key2": "Value2"},
+        authenticationType="Standard",
+        clientToken="TestClient",
+        customerManagedKey=FAKE_KMS_KEY_ID,
+        displayName="TestDisplayName",
+        instanceType="TestInstanceType",
+        maxConcurrentSessions=5,
+        tags=FAKE_TAGS,
+    )["portalArn"]
+    client.associate_network_settings(
+        networkSettingsArn=network_arn, portalArn=portal_arn
+    )
+    resp = client.get_portal(portalArn=portal_arn)["portal"]
+    assert resp["networkSettingsArn"] == network_arn
+    resp = client.get_network_settings(networkSettingsArn=network_arn)[
+        "networkSettings"
+    ]
+    assert resp["associatedPortalArns"] == [portal_arn]