Repository: getmoto/moto
Pull request #8108 (2024-09-09 18:21:53)

feat: Implementation of  memorydb api
This is the initial moto implementation of AWS MemoryDB API.


Patch:
diff --git a/.github/workflows/data-update_config-managed-rules.yml b/.github/workflows/data-update_config-managed-rules.yml
index fd27f0e54..16504d8e1 100644
--- a/.github/workflows/data-update_config-managed-rules.yml
+++ b/.github/workflows/data-update_config-managed-rules.yml
@@ -37,7 +37,7 @@ jobs:
         cd scripts
         ./pull_down_aws_managed_rules.py
     - name: Create PR
-      uses: peter-evans/create-pull-request@v6
+      uses: peter-evans/create-pull-request@v7
       with:
         branch: 'chore-update-config-managed-rules-${{ github.run_number }}'
         title: 'chore: update Config Managed Rules'
diff --git a/.github/workflows/data-update_ec2-instance-offerings.yml b/.github/workflows/data-update_ec2-instance-offerings.yml
index 2cdca875f..8655b6208 100644
--- a/.github/workflows/data-update_ec2-instance-offerings.yml
+++ b/.github/workflows/data-update_ec2-instance-offerings.yml
@@ -42,7 +42,7 @@ jobs:
         pip install boto3
         scripts/ec2_get_instance_type_offerings.py
     - name: Create PR
-      uses: peter-evans/create-pull-request@v6
+      uses: peter-evans/create-pull-request@v7
       with:
         branch: 'chore-update-ec2-offerings-${{ github.run_number }}'
         title: 'chore: update EC2 Instance Offerings'
diff --git a/.github/workflows/data-update_ec2-instance-types.yml b/.github/workflows/data-update_ec2-instance-types.yml
index de1c06481..64c3b9d37 100644
--- a/.github/workflows/data-update_ec2-instance-types.yml
+++ b/.github/workflows/data-update_ec2-instance-types.yml
@@ -42,7 +42,7 @@ jobs:
         pip install boto3
         scripts/get_instance_info.py
     - name: Create PR
-      uses: peter-evans/create-pull-request@v6
+      uses: peter-evans/create-pull-request@v7
       with:
         branch: 'chore-update-ec2-types-${{ github.run_number }}'
         title: 'chore: update EC2 Instance Types'
diff --git a/.github/workflows/data-update_iam-managed-policies.yml b/.github/workflows/data-update_iam-managed-policies.yml
index 87b78764f..fcfd65ed8 100644
--- a/.github/workflows/data-update_iam-managed-policies.yml
+++ b/.github/workflows/data-update_iam-managed-policies.yml
@@ -42,7 +42,7 @@ jobs:
         pip install -r requirements-dev.txt
         make aws_managed_policies
     - name: Create PR
-      uses: peter-evans/create-pull-request@v6
+      uses: peter-evans/create-pull-request@v7
       with:
         branch: 'chore-update-iam-managed-policies-${{ github.run_number }}'
         title: 'chore: update IAM Managed Policies'
diff --git a/.github/workflows/data-update_ssm-default-amis.yml b/.github/workflows/data-update_ssm-default-amis.yml
index 30ed7193c..f22b38467 100644
--- a/.github/workflows/data-update_ssm-default-amis.yml
+++ b/.github/workflows/data-update_ssm-default-amis.yml
@@ -43,7 +43,7 @@ jobs:
         pip install moto[all]
         python scripts/ssm_get_default_amis.py
     - name: Create PR
-      uses: peter-evans/create-pull-request@v6
+      uses: peter-evans/create-pull-request@v7
       with:
         branch: 'chore-update-ssm-amis-${{ github.run_number }}'
         title: "chore: update SSM Instance AMI's"
diff --git a/.github/workflows/data-update_ssm-default-parameters.yml b/.github/workflows/data-update_ssm-default-parameters.yml
index 8bf4f6f42..1a1d398d6 100644
--- a/.github/workflows/data-update_ssm-default-parameters.yml
+++ b/.github/workflows/data-update_ssm-default-parameters.yml
@@ -43,7 +43,7 @@ jobs:
         pip install moto[all]
         python scripts/ssm_get_default_params.py
     - name: Create PR
-      uses: peter-evans/create-pull-request@v6
+      uses: peter-evans/create-pull-request@v7
       with:
         branch: 'chore-update-ssm-params-${{ github.run_number }}'
         title: "chore: update SSM Instance parameters"
diff --git a/.github/workflows/data-update_ssm-optimized-amis.yml b/.github/workflows/data-update_ssm-optimized-amis.yml
index d15d3d168..96a9600e7 100644
--- a/.github/workflows/data-update_ssm-optimized-amis.yml
+++ b/.github/workflows/data-update_ssm-optimized-amis.yml
@@ -43,7 +43,7 @@ jobs:
         pip install moto[all]
         python scripts/ssm_get_optimized_amis.py
     - name: Create PR
-      uses: peter-evans/create-pull-request@v6
+      uses: peter-evans/create-pull-request@v7
       with:
         branch: 'chore-update-ssm-amis-${{ github.run_number }}'
         title: "chore: update SSM Instance AMI's"
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/appsync/exceptions.py b/moto/appsync/exceptions.py
index 7e1069e3c..d3ffd2a97 100644
--- a/moto/appsync/exceptions.py
+++ b/moto/appsync/exceptions.py
@@ -23,6 +23,17 @@ class GraphQLSchemaException(AppSyncExceptions):
         self.description = json.dumps({"message": self.message})
 
 
+class GraphqlAPICacheNotFound(AppSyncExceptions):
+    code = 404
+
+    def __init__(self, op: str):
+        super().__init__(
+            "NotFoundException",
+            f"Unable to {op} the cache as it doesn't exist, please create the cache first.",
+        )
+        self.description = json.dumps({"message": self.message})
+
+
 class BadRequestException(AppSyncExceptions):
     def __init__(self, message: str):
         super().__init__("BadRequestException", message)
diff --git a/moto/appsync/models.py b/moto/appsync/models.py
index f0e0345f6..c2dd460a9 100644
--- a/moto/appsync/models.py
+++ b/moto/appsync/models.py
@@ -10,7 +10,12 @@ from moto.moto_api._internal import mock_random
 from moto.utilities.tagging_service import TaggingService
 from moto.utilities.utils import get_partition
 
-from .exceptions import BadRequestException, GraphqlAPINotFound, GraphQLSchemaException
+from .exceptions import (
+    BadRequestException,
+    GraphqlAPICacheNotFound,
+    GraphqlAPINotFound,
+    GraphQLSchemaException,
+)
 
 # AWS custom scalars and directives
 # https://github.com/dotansimha/graphql-code-generator/discussions/4311#discussioncomment-2921796
@@ -132,6 +137,49 @@ class GraphqlAPIKey(BaseModel):
         }
 
 
+class APICache(BaseModel):
+    def __init__(
+        self,
+        ttl: int,
+        api_caching_behavior: str,
+        type_: str,
+        transit_encryption_enabled: Optional[bool] = None,
+        at_rest_encryption_enabled: Optional[bool] = None,
+        health_metrics_config: Optional[str] = None,
+    ):
+        self.ttl = ttl
+        self.api_caching_behavior = api_caching_behavior
+        self.type = type_
+        self.transit_encryption_enabled = transit_encryption_enabled or False
+        self.at_rest_encryption_enabled = at_rest_encryption_enabled or False
+        self.health_metrics_config = health_metrics_config or "DISABLED"
+        self.status = "AVAILABLE"
+
+    def update(
+        self,
+        ttl: int,
+        api_caching_behavior: str,
+        type: str,
+        health_metrics_config: Optional[str] = None,
+    ) -> None:
+        self.ttl = ttl
+        self.api_caching_behavior = api_caching_behavior
+        self.type = type
+        if health_metrics_config is not None:
+            self.health_metrics_config = health_metrics_config
+
+    def to_json(self) -> Dict[str, Any]:
+        return {
+            "ttl": self.ttl,
+            "transitEncryptionEnabled": self.transit_encryption_enabled,
+            "atRestEncryptionEnabled": self.at_rest_encryption_enabled,
+            "apiCachingBehavior": self.api_caching_behavior,
+            "type": self.type,
+            "healthMetricsConfig": self.health_metrics_config,
+            "status": self.status,
+        }
+
+
 class GraphqlAPI(BaseModel):
     def __init__(
         self,
@@ -162,6 +210,8 @@ class GraphqlAPI(BaseModel):
 
         self.api_keys: Dict[str, GraphqlAPIKey] = dict()
 
+        self.api_cache: Optional[APICache] = None
+
     def update(
         self,
         name: str,
@@ -223,6 +273,38 @@ class GraphqlAPI(BaseModel):
         graphql_type["format"] = type_format  # type: ignore[index]
         return graphql_type
 
+    def create_api_cache(
+        self,
+        ttl: int,
+        api_caching_behavior: str,
+        type: str,
+        transit_encryption_enabled: Optional[bool] = None,
+        at_rest_encryption_enabled: Optional[bool] = None,
+        health_metrics_config: Optional[str] = None,
+    ) -> APICache:
+        self.api_cache = APICache(
+            ttl,
+            api_caching_behavior,
+            type,
+            transit_encryption_enabled,
+            at_rest_encryption_enabled,
+            health_metrics_config,
+        )
+        return self.api_cache
+
+    def update_api_cache(
+        self,
+        ttl: int,
+        api_caching_behavior: str,
+        type: str,
+        health_metrics_config: Optional[str] = None,
+    ) -> APICache:
+        self.api_cache.update(ttl, api_caching_behavior, type, health_metrics_config)  # type: ignore[union-attr]
+        return self.api_cache  # type: ignore[return-value]
+
+    def delete_api_cache(self) -> None:
+        self.api_cache = None
+
     def to_json(self) -> Dict[str, Any]:
         return {
             "name": self.name,
@@ -374,5 +456,71 @@ class AppSyncBackend(BaseBackend):
     def get_type(self, api_id: str, type_name: str, type_format: str) -> Any:
         return self.graphql_apis[api_id].get_type(type_name, type_format)
 
+    def get_api_cache(self, api_id: str) -> APICache:
+        if api_id not in self.graphql_apis:
+            raise GraphqlAPINotFound(api_id)
+        api_cache = self.graphql_apis[api_id].api_cache
+        if api_cache is None:
+            raise GraphqlAPICacheNotFound("get")
+        return api_cache
+
+    def delete_api_cache(self, api_id: str) -> None:
+        if api_id not in self.graphql_apis:
+            raise GraphqlAPINotFound(api_id)
+        if self.graphql_apis[api_id].api_cache is None:
+            raise GraphqlAPICacheNotFound("delete")
+        self.graphql_apis[api_id].delete_api_cache()
+        return
+
+    def create_api_cache(
+        self,
+        api_id: str,
+        ttl: int,
+        api_caching_behavior: str,
+        type: str,
+        transit_encryption_enabled: Optional[bool] = None,
+        at_rest_encryption_enabled: Optional[bool] = None,
+        health_metrics_config: Optional[str] = None,
+    ) -> APICache:
+        if api_id not in self.graphql_apis:
+            raise GraphqlAPINotFound(api_id)
+        graphql_api = self.graphql_apis[api_id]
+        if graphql_api.api_cache is not None:
+            raise BadRequestException(message="The API has already enabled caching.")
+        api_cache = graphql_api.create_api_cache(
+            ttl,
+            api_caching_behavior,
+            type,
+            transit_encryption_enabled,
+            at_rest_encryption_enabled,
+            health_metrics_config,
+        )
+        return api_cache
+
+    def update_api_cache(
+        self,
+        api_id: str,
+        ttl: int,
+        api_caching_behavior: str,
+        type: str,
+        health_metrics_config: Optional[str] = None,
+    ) -> APICache:
+        if api_id not in self.graphql_apis:
+            raise GraphqlAPINotFound(api_id)
+        graphql_api = self.graphql_apis[api_id]
+        if graphql_api.api_cache is None:
+            raise GraphqlAPICacheNotFound("update")
+        api_cache = graphql_api.update_api_cache(
+            ttl, api_caching_behavior, type, health_metrics_config
+        )
+        return api_cache
+
+    def flush_api_cache(self, api_id: str) -> None:
+        if api_id not in self.graphql_apis:
+            raise GraphqlAPINotFound(api_id)
+        if self.graphql_apis[api_id].api_cache is None:
+            raise GraphqlAPICacheNotFound("flush")
+        return
+
 
 appsync_backends = BackendDict(AppSyncBackend, "appsync")
diff --git a/moto/appsync/responses.py b/moto/appsync/responses.py
index 570472f78..c2096000e 100644
--- a/moto/appsync/responses.py
+++ b/moto/appsync/responses.py
@@ -192,3 +192,60 @@ class AppSyncResponse(BaseResponse):
             format_=format_, include_directives=include_directives
         )
         return schema
+
+    def get_api_cache(self) -> str:
+        api_id = self.path.split("/")[-2]
+        api_cache = self.appsync_backend.get_api_cache(
+            api_id=api_id,
+        )
+        return json.dumps(dict(apiCache=api_cache.to_json()))
+
+    def delete_api_cache(self) -> str:
+        api_id = self.path.split("/")[-2]
+        self.appsync_backend.delete_api_cache(
+            api_id=api_id,
+        )
+        return "{}"
+
+    def create_api_cache(self) -> str:
+        params = json.loads(self.body)
+        api_id = self.path.split("/")[-2]
+        ttl = params.get("ttl")
+        transit_encryption_enabled = params.get("transitEncryptionEnabled")
+        at_rest_encryption_enabled = params.get("atRestEncryptionEnabled")
+        api_caching_behavior = params.get("apiCachingBehavior")
+        type = params.get("type")
+        health_metrics_config = params.get("healthMetricsConfig")
+        api_cache = self.appsync_backend.create_api_cache(
+            api_id=api_id,
+            ttl=ttl,
+            transit_encryption_enabled=transit_encryption_enabled,
+            at_rest_encryption_enabled=at_rest_encryption_enabled,
+            api_caching_behavior=api_caching_behavior,
+            type=type,
+            health_metrics_config=health_metrics_config,
+        )
+        return json.dumps(dict(apiCache=api_cache.to_json()))
+
+    def update_api_cache(self) -> str:
+        api_id = self.path.split("/")[-3]
+        params = json.loads(self.body)
+        ttl = params.get("ttl")
+        api_caching_behavior = params.get("apiCachingBehavior")
+        type = params.get("type")
+        health_metrics_config = params.get("healthMetricsConfig")
+        api_cache = self.appsync_backend.update_api_cache(
+            api_id=api_id,
+            ttl=ttl,
+            api_caching_behavior=api_caching_behavior,
+            type=type,
+            health_metrics_config=health_metrics_config,
+        )
+        return json.dumps(dict(apiCache=api_cache.to_json()))
+
+    def flush_api_cache(self) -> str:
+        api_id = self.path.split("/")[-2]
+        self.appsync_backend.flush_api_cache(
+            api_id=api_id,
+        )
+        return "{}"
diff --git a/moto/appsync/urls.py b/moto/appsync/urls.py
index 5dfe9ff55..b1d43d7a9 100644
--- a/moto/appsync/urls.py
+++ b/moto/appsync/urls.py
@@ -17,4 +17,7 @@ url_paths = {
     "{0}/v1/tags/(?P<resource_arn>.+)$": AppSyncResponse.dispatch,
     "{0}/v1/tags/(?P<resource_arn_pt1>.+)/(?P<resource_arn_pt2>.+)$": AppSyncResponse.dispatch,
     "{0}/v1/apis/(?P<api_id>[^/]+)/types/(?P<type_name>.+)$": AppSyncResponse.dispatch,
+    "{0}/v1/apis/(?P<apiId>.*)/ApiCaches$": AppSyncResponse.dispatch,
+    "{0}/v1/apis/(?P<apiId>.*)/ApiCaches/update$": AppSyncResponse.dispatch,
+    "{0}/v1/apis/(?P<apiId>.*)/FlushCache$": AppSyncResponse.dispatch,
 }
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/dynamodb/exceptions.py b/moto/dynamodb/exceptions.py
index ae7c94f62..4d4d327e0 100644
--- a/moto/dynamodb/exceptions.py
+++ b/moto/dynamodb/exceptions.py
@@ -305,11 +305,11 @@ class DuplicateUpdateExpression(InvalidUpdateExpression):
         )
 
 
-class TooManyAddClauses(InvalidUpdateExpression):
-    msg = 'The "ADD" section can only be used once in an update expression;'
-
-    def __init__(self) -> None:
-        super().__init__(self.msg)
+class TooManyClauses(InvalidUpdateExpression):
+    def __init__(self, _type: str) -> None:
+        super().__init__(
+            f'The "{_type}" section can only be used once in an update expression;'
+        )
 
 
 class ResourceNotFoundException(JsonRESTError):
diff --git a/moto/dynamodb/parsing/ast_nodes.py b/moto/dynamodb/parsing/ast_nodes.py
index 95875d90d..28985bf1a 100644
--- a/moto/dynamodb/parsing/ast_nodes.py
+++ b/moto/dynamodb/parsing/ast_nodes.py
@@ -8,7 +8,7 @@ from moto.dynamodb.models import DynamoType
 from ..exceptions import (
     DuplicateUpdateExpression,
     MockValidationException,
-    TooManyAddClauses,
+    TooManyClauses,
 )
 
 
@@ -29,9 +29,10 @@ class Node(metaclass=abc.ABCMeta):
 
     def validate(self, limit_set_actions: bool = False) -> None:
         if self.type == "UpdateExpression":
-            nr_of_clauses = len(self.find_clauses([UpdateExpressionAddClause]))
-            if nr_of_clauses > 1:
-                raise TooManyAddClauses()
+            if len(self.find_clauses([UpdateExpressionAddClause])) > 1:
+                raise TooManyClauses("ADD")
+            if len(self.find_clauses([UpdateExpressionRemoveClause])) > 1:
+                raise TooManyClauses("REMOVE")
             set_actions = self.find_clauses([UpdateExpressionSetAction])
             # set_attributes = ["attr", "map.attr", attr.list[2], ..]
             set_attributes = [s.children[0].to_str() for s in set_actions]
@@ -176,8 +177,11 @@ class UpdateExpressionRemoveAction(UpdateExpressionClause):
 
     def __lt__(self, other):
         self_value = self._get_value()
-
-        return self_value < other._get_value()
+        other_value = other._get_value()
+        if isinstance(self_value, int) and isinstance(other_value, int):
+            return self_value < other_value
+        else:
+            return str(self_value) < str(other_value)
 
 
 class UpdateExpressionAddActions(UpdateExpressionClause):
diff --git a/moto/iam/models.py b/moto/iam/models.py
index 172e55aa8..3e06a6cfb 100644
--- a/moto/iam/models.py
+++ b/moto/iam/models.py
@@ -223,6 +223,7 @@ class SAMLProvider(BaseModel):
         saml_metadata_document: Optional[str] = None,
     ):
         self.account_id = account_id
+        self.create_date = utcnow()
         self.region_name = region_name
         self.name = name
         self.saml_metadata_document = saml_metadata_document
@@ -231,6 +232,10 @@ class SAMLProvider(BaseModel):
     def arn(self) -> str:
         return f"arn:{get_partition(self.region_name)}:iam::{self.account_id}:saml-provider/{self.name}"
 
+    @property
+    def created_iso_8601(self) -> str:
+        return iso_8601_datetime_without_milliseconds(self.create_date)
+
 
 class OpenIDConnectProvider(BaseModel):
     def __init__(
diff --git a/moto/iam/responses.py b/moto/iam/responses.py
index cb20c45fd..1b2be7c1f 100644
--- a/moto/iam/responses.py
+++ b/moto/iam/responses.py
@@ -1,3 +1,5 @@
+from xml.sax.saxutils import escape
+
 from moto.core.responses import BaseResponse
 
 from .models import IAMBackend, User, iam_backends
@@ -858,7 +860,10 @@ class IamResponse(BaseResponse):
 
     def create_saml_provider(self) -> str:
         saml_provider_name = self._get_param("Name")
-        saml_metadata_document = self._get_param("SAMLMetadataDocument")
+        _saml_metadata_document = self._get_param("SAMLMetadataDocument")
+        saml_metadata_document = escape(
+            _saml_metadata_document, entities={"'": "&apos;", '"': "&quot;"}
+        )
         saml_provider = self.backend.create_saml_provider(
             saml_provider_name, saml_metadata_document
         )
@@ -868,7 +873,10 @@ class IamResponse(BaseResponse):
 
     def update_saml_provider(self) -> str:
         saml_provider_arn = self._get_param("SAMLProviderArn")
-        saml_metadata_document = self._get_param("SAMLMetadataDocument")
+        _saml_metadata_document = self._get_param("SAMLMetadataDocument")
+        saml_metadata_document = escape(
+            _saml_metadata_document, entities={"'": "&apos;", '"': "&quot;"}
+        )
         saml_provider = self.backend.update_saml_provider(
             saml_provider_arn, saml_metadata_document
         )
@@ -2481,7 +2489,7 @@ LIST_SAML_PROVIDERS_TEMPLATE = """<ListSAMLProvidersResponse xmlns="https://iam.
 
 GET_SAML_PROVIDER_TEMPLATE = """<GetSAMLProviderResponse xmlns="https://iam.amazonaws.com/doc/2010-05-08/">
 <GetSAMLProviderResult>
-  <CreateDate>2012-05-09T16:27:11Z</CreateDate>
+  <CreateDate>{{ saml_provider.created_iso_8601 }}</CreateDate>
   <ValidUntil>2015-12-31T21:59:59Z</ValidUntil>
   <SAMLMetadataDocument>{{ saml_provider.saml_metadata_document }}</SAMLMetadataDocument>
 </GetSAMLProviderResult>
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,
+}
diff --git a/other_langs/terraform/iam/data.tf b/other_langs/terraform/iam/data.tf
new file mode 100644
index 000000000..083e23c72
--- /dev/null
+++ b/other_langs/terraform/iam/data.tf
@@ -0,0 +1,20 @@
+data "aws_caller_identity" "this" {}
+
+data "aws_iam_policy_document" "this" {
+  statement {
+    actions = ["sts:AssumeRoleWithSAML"]
+    principals {
+      type        = "Federated"
+      identifiers = [aws_iam_saml_provider.this.arn]
+    }
+    condition {
+      test     = "StringEquals"
+      variable = "SAML:aud"
+      values   = ["https://signin.aws.amazon.com/saml"]
+    }
+  }
+}
+
+data "aws_iam_saml_provider" "this" {
+  arn = aws_iam_saml_provider.this.arn
+}
diff --git a/other_langs/terraform/iam/iam.tf b/other_langs/terraform/iam/iam.tf
index 1a7710d9e..812a1e536 100644
--- a/other_langs/terraform/iam/iam.tf
+++ b/other_langs/terraform/iam/iam.tf
@@ -21,4 +21,9 @@ resource "aws_iam_policy" "policy" {
   tags = {
     t1 = "v1"
   }
-}
\ No newline at end of file
+}
+
+resource "aws_iam_saml_provider" "this" {
+  name                   = "sso-test"
+  saml_metadata_document = file("${path.module}/sample-metadata.xml")
+}
diff --git a/other_langs/terraform/iam/output.tf b/other_langs/terraform/iam/output.tf
new file mode 100644
index 000000000..6f6f6a881
--- /dev/null
+++ b/other_langs/terraform/iam/output.tf
@@ -0,0 +1,11 @@
+output "saml_provider_create_date" {
+  value = data.aws_iam_saml_provider.this.create_date
+}
+
+output "saml_provider_create_name" {
+  value = data.aws_iam_saml_provider.this.name
+}
+
+output "saml_provider_saml_metadata_document" {
+  value = data.aws_iam_saml_provider.this.saml_metadata_document
+}
diff --git a/other_langs/terraform/iam/providers.tf b/other_langs/terraform/iam/providers.tf
index 3e11097e7..b6d68e709 100644
--- a/other_langs/terraform/iam/providers.tf
+++ b/other_langs/terraform/iam/providers.tf
@@ -7,6 +7,7 @@ provider "aws" {
 
   endpoints {
     iam = "http://localhost:5000"
+    sts = "http://localhost:5000"
   }
 
   access_key = "my-access-key"
diff --git a/other_langs/terraform/iam/sample-metadata.xml b/other_langs/terraform/iam/sample-metadata.xml
new file mode 100644
index 000000000..6ff6e914a
--- /dev/null
+++ b/other_langs/terraform/iam/sample-metadata.xml
@@ -0,0 +1,76 @@
+<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" entityID="urn:amazon:webservices" validUntil="2025-08-30T00:00:00Z">
+  <SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol" WantAssertionsSigned="true">
+    <KeyDescriptor use="signing">
+      <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+        <ds:X509Data>
+          <ds:X509Certificate>MIIDcTCCAlmgAwIBAgIIGPlChvWsb9QwDQYJKoZIhvcNAQELBQAwZzEfMB0GA1UE AxMWdXJuOmFtYXpvbjp3ZWJzZXJ2aWNlczEiMCAGA1UEChMZQW1hem9uIFdlYiBT ZXJ2aWNlcywgSW5jLjETMBEGA1UECBMKV2FzaGluZ3RvbjELMAkGA1UEBhMCVVMw HhcNMjQwODMwMDAwMDAwWhcNMjUwODMwMDAwMDAwWjBnMR8wHQYDVQQDExZ1cm46 YW1hem9uOndlYnNlcnZpY2VzMSIwIAYDVQQKExlBbWF6b24gV2ViIFNlcnZpY2Vz LCBJbmMuMRMwEQYDVQQIEwpXYXNoaW5ndG9uMQswCQYDVQQGEwJVUzCCASIwDQYJ KoZIhvcNAQEBBQADggEPADCCAQoCggEBAK2IcN8WBhWhmwR5AOHJ7LIyl+f7ZiYM OjzsNZeOhvD6t8fIPqXg2Kyt2ixCCB16AQsif/bxYAoqqwrVkuBwG8V9CrDDuvXX 3WL7+hvn/JzmjyTeZxy34ySMppSvR3JNtps1C/stTs90KKAKfP56wg1RIEfA6dWX S/Ebd05Jx1miZb2j2+rX1C8Oc6RhOhBCHbbR/4fzFXxdocs4Krq6r+So2XQ8JChC 6E/c092/hCTBXtBj0D8Lk1DJWI1avMzRbrm53wH+1qTzoZMZu66gsuvWhbeH7B/Q kixxpb2IUNYNIT5A4g8tzzQLeGaMUHpvY8fuzRkX/8Zrd8CQjveOdPUCAwEAAaMh MB8wHQYDVR0OBBYEFCM8vneP4Ut+aXPdeCYsbFkUVMpeMA0GCSqGSIb3DQEBCwUA A4IBAQBCz1qeNzA4R/iXrqswc98bZXT7oj1lzkwRaDy0jsfsgyGwHIJ5VOMxnvFH 564zGlbljCOxrZG9FtdrutYGLHaLJLZdHKH2XQy2stxcP6pnZ29LyAMoIP6fq08M 1tzW0u0fMdC2Hpgi8QHGgguKW1AFFK3vj/sW1PWuMzTwB7ur5BpgMo4Wk074HIRT Z8B1RkY13EnFy7IIEIdreKLY3xwrpUZYnW0Em705T3gt0UrFKfzxrTjMPL3yXzgk wkAvGzGJZtdZ0FNc6ecGVuWkbJ7wvbYh2j557gbk9s0F+24103bfRyyM2jj7cgnK ij1wVZYobnMgnQI+ikrd7ksym6QN</ds:X509Certificate>
+        </ds:X509Data>
+      </ds:KeyInfo>
+    </KeyDescriptor>
+    <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
+    <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</NameIDFormat>
+    <NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>
+    <NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</NameIDFormat>
+    <NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName</NameIDFormat>
+    <NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName</NameIDFormat>
+    <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos</NameIDFormat>
+    <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:entity</NameIDFormat>
+    <AssertionConsumerService index="1" isDefault="true" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="2" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://eu-north-1.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="3" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://me-south-1.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="4" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://ap-south-1.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="5" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://eu-west-3.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="6" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://ap-southeast-3.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="7" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://us-east-2.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="8" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://af-south-1.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="9" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://eu-west-1.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="10" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://eu-central-1.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="11" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sa-east-1.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="12" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://ap-east-1.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="13" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://ap-northeast-2.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="14" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://ap-northeast-3.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="15" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://eu-west-2.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="16" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://eu-south-1.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="17" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://ap-northeast-1.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="18" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://us-west-2.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="19" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://us-west-1.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="20" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://ap-southeast-1.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="21" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://ap-southeast-2.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="22" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://ca-central-1.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="23" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://me-central-1.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="24" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://eu-central-2.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="25" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://eu-south-2.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="26" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://ap-south-2.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="27" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://ap-southeast-4.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="28" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://il-central-1.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="29" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://ca-west-1.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="30" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://ap-southeast-5.signin.aws.amazon.com/saml"/>
+    <AttributeConsumingService index="1">
+      <ServiceName xml:lang="en">AWS Management Console Single Sign-On</ServiceName>
+      <RequestedAttribute isRequired="true" Name="https://aws.amazon.com/SAML/Attributes/Role" FriendlyName="RoleEntitlement"/>
+      <RequestedAttribute isRequired="true" Name="https://aws.amazon.com/SAML/Attributes/RoleSessionName" FriendlyName="RoleSessionName"/>
+      <RequestedAttribute isRequired="false" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.1" FriendlyName="eduPersonAffiliation"/>
+      <RequestedAttribute isRequired="false" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.2" FriendlyName="eduPersonNickname"/>
+      <RequestedAttribute isRequired="false" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.3" FriendlyName="eduPersonOrgDN"/>
+      <RequestedAttribute isRequired="false" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.4" FriendlyName="eduPersonOrgUnitDN"/>
+      <RequestedAttribute isRequired="false" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.5" FriendlyName="eduPersonPrimaryAffiliation"/>
+      <RequestedAttribute isRequired="false" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.6" FriendlyName="eduPersonPrincipalName"/>
+      <RequestedAttribute isRequired="false" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.7" FriendlyName="eduPersonEntitlement"/>
+      <RequestedAttribute isRequired="false" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.8" FriendlyName="eduPersonPrimaryOrgUnitDN"/>
+      <RequestedAttribute isRequired="false" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.9" FriendlyName="eduPersonScopedAffiliation"/>
+      <RequestedAttribute isRequired="false" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.10" FriendlyName="eduPersonTargetedID"/>
+      <RequestedAttribute isRequired="false" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.11" FriendlyName="eduPersonAssurance"/>
+      <RequestedAttribute isRequired="false" Name="urn:oid:1.3.6.1.4.1.5923.1.2.1.2" FriendlyName="eduOrgHomePageURI"/>
+      <RequestedAttribute isRequired="false" Name="urn:oid:1.3.6.1.4.1.5923.1.2.1.3" FriendlyName="eduOrgIdentityAuthNPolicyURI"/>
+      <RequestedAttribute isRequired="false" Name="urn:oid:1.3.6.1.4.1.5923.1.2.1.4" FriendlyName="eduOrgLegalName"/>
+      <RequestedAttribute isRequired="false" Name="urn:oid:1.3.6.1.4.1.5923.1.2.1.5" FriendlyName="eduOrgSuperiorURI"/>
+      <RequestedAttribute isRequired="false" Name="urn:oid:1.3.6.1.4.1.5923.1.2.1.6" FriendlyName="eduOrgWhitePagesURI"/>
+      <RequestedAttribute isRequired="false" Name="urn:oid:2.5.4.3" FriendlyName="cn"/>
+    </AttributeConsumingService>
+  </SPSSODescriptor>
+  <Organization>
+    <OrganizationName xml:lang="en">Amazon Web Services, Inc.</OrganizationName>
+    <OrganizationDisplayName xml:lang="en">AWS</OrganizationDisplayName>
+    <OrganizationURL xml:lang="en">https://aws.amazon.com</OrganizationURL>
+  </Organization>
+</EntityDescriptor>


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_appsync/test_appsync.py b/tests/test_appsync/test_appsync.py
index 67f99bbc7..5f43b7a01 100644
--- a/tests/test_appsync/test_appsync.py
+++ b/tests/test_appsync/test_appsync.py
@@ -162,3 +162,299 @@ def test_list_graphql_apis():
 
     resp = client.list_graphql_apis()
     assert len(resp["graphqlApis"]) == 3
+
+
+@mock_aws
+def test_get_api_cache():
+    client = boto3.client("appsync", region_name="ap-southeast-1")
+    api_id = client.create_graphql_api(name="api1", authenticationType="API_KEY")[
+        "graphqlApi"
+    ]["apiId"]
+
+    client.create_api_cache(
+        apiId=api_id,
+        ttl=300,
+        apiCachingBehavior="FULL_REQUEST_CACHING",
+        type="T2_SMALL",
+        transitEncryptionEnabled=False,
+        atRestEncryptionEnabled=False,
+        healthMetricsConfig="DISABLED",
+    )
+
+    cache = client.get_api_cache(apiId=api_id)["apiCache"]
+
+    assert cache["ttl"] == 300
+    assert cache["apiCachingBehavior"] == "FULL_REQUEST_CACHING"
+    assert cache["transitEncryptionEnabled"] is False
+    assert cache["atRestEncryptionEnabled"] is False
+    assert cache["type"] == "T2_SMALL"
+    assert cache["status"] == "AVAILABLE"
+    assert cache["healthMetricsConfig"] == "DISABLED"
+
+
+@mock_aws
+def test_get_api_cache_error():
+    client = boto3.client("appsync", region_name="ap-southeast-1")
+
+    with pytest.raises(ClientError) as exc:
+        client.get_api_cache(apiId="unknown")
+    err = exc.value.response["Error"]
+
+    assert err["Code"] == "NotFoundException"
+    assert err["Message"] == "GraphQL API unknown not found."
+
+    api_id = client.create_graphql_api(name="api1", authenticationType="API_KEY")[
+        "graphqlApi"
+    ]["apiId"]
+
+    with pytest.raises(ClientError) as exc:
+        client.get_api_cache(apiId=api_id)
+    err = exc.value.response["Error"]
+
+    assert err["Code"] == "NotFoundException"
+    assert err["Message"] == (
+        "Unable to get the cache as it doesn't exist, please create the cache first."
+    )
+
+
+@mock_aws
+def test_delete_api_cache():
+    client = boto3.client("appsync", region_name="eu-west-1")
+    api_id = client.create_graphql_api(name="api1", authenticationType="API_KEY")[
+        "graphqlApi"
+    ]["apiId"]
+
+    client.create_api_cache(
+        apiId=api_id,
+        ttl=300,
+        apiCachingBehavior="FULL_REQUEST_CACHING",
+        type="T2_SMALL",
+    )
+
+    client.delete_api_cache(apiId=api_id)
+
+    with pytest.raises(ClientError) as exc:
+        client.get_api_cache(apiId=api_id)
+    err = exc.value.response["Error"]
+
+    assert err["Code"] == "NotFoundException"
+    assert err["Message"] == (
+        "Unable to get the cache as it doesn't exist, please create the cache first."
+    )
+
+
+@mock_aws
+def test_delete_api_cache_error():
+    client = boto3.client("appsync", region_name="eu-west-1")
+
+    with pytest.raises(ClientError) as exc:
+        client.delete_api_cache(apiId="unknown")
+    err = exc.value.response["Error"]
+
+    assert err["Code"] == "NotFoundException"
+    assert err["Message"] == "GraphQL API unknown not found."
+
+    api_id = client.create_graphql_api(name="api1", authenticationType="API_KEY")[
+        "graphqlApi"
+    ]["apiId"]
+
+    with pytest.raises(ClientError) as exc:
+        client.delete_api_cache(apiId=api_id)
+    err = exc.value.response["Error"]
+
+    assert err["Code"] == "NotFoundException"
+    assert err["Message"] == (
+        "Unable to delete the cache as it doesn't exist, please create the cache first."
+    )
+
+
+@mock_aws
+def test_create_api_cache():
+    client = boto3.client("appsync", region_name="eu-west-1")
+    api_id = client.create_graphql_api(name="api1", authenticationType="API_KEY")[
+        "graphqlApi"
+    ]["apiId"]
+
+    cache = client.create_api_cache(
+        apiId=api_id,
+        ttl=300,
+        apiCachingBehavior="FULL_REQUEST_CACHING",
+        type="T2_SMALL",
+    )["apiCache"]
+
+    assert cache["ttl"] == 300
+    assert cache["apiCachingBehavior"] == "FULL_REQUEST_CACHING"
+    assert cache["transitEncryptionEnabled"] is False
+    assert cache["atRestEncryptionEnabled"] is False
+    assert cache["type"] == "T2_SMALL"
+    assert cache["status"] == "AVAILABLE"
+    assert cache["healthMetricsConfig"] == "DISABLED"
+
+
+@mock_aws
+def test_create_api_cache_advanced():
+    client = boto3.client("appsync", region_name="eu-west-1")
+    api_id = client.create_graphql_api(name="api2", authenticationType="API_KEY")[
+        "graphqlApi"
+    ]["apiId"]
+    cache = client.create_api_cache(
+        apiId=api_id,
+        ttl=500,
+        apiCachingBehavior="PER_RESOLVER_CACHING",
+        type="R4_4XLARGE",
+        transitEncryptionEnabled=True,
+        atRestEncryptionEnabled=True,
+        healthMetricsConfig="ENABLED",
+    )["apiCache"]
+    assert cache["ttl"] == 500
+    assert cache["apiCachingBehavior"] == "PER_RESOLVER_CACHING"
+    assert cache["transitEncryptionEnabled"] is True
+    assert cache["atRestEncryptionEnabled"] is True
+    assert cache["type"] == "R4_4XLARGE"
+    assert cache["status"] == "AVAILABLE"
+    assert cache["healthMetricsConfig"] == "ENABLED"
+
+
+@mock_aws
+def test_create_api_cache_error():
+    client = boto3.client("appsync", region_name="ap-southeast-1")
+    with pytest.raises(ClientError) as exc:
+        client.create_api_cache(
+            apiId="unknown",
+            ttl=100,
+            apiCachingBehavior="FULL_REQUEST_CACHING",
+            type="SMALL",
+        )
+    err = exc.value.response["Error"]
+
+    assert err["Code"] == "NotFoundException"
+    assert err["Message"] == "GraphQL API unknown not found."
+
+    api_id = client.create_graphql_api(name="api1", authenticationType="API_KEY")[
+        "graphqlApi"
+    ]["apiId"]
+
+    client.create_api_cache(
+        apiId=api_id,
+        ttl=300,
+        apiCachingBehavior="FULL_REQUEST_CACHING",
+        type="T2_SMALL",
+    )
+    with pytest.raises(ClientError) as exc:
+        client.create_api_cache(
+            apiId=api_id,
+            ttl=100,
+            apiCachingBehavior="FULL_REQUEST_CACHING",
+            type="SMALL",
+        )
+    err = exc.value.response["Error"]
+
+    assert err["Code"] == "BadRequestException"
+    assert err["Message"] == "The API has already enabled caching."
+
+
+@mock_aws
+def test_update_api_cache():
+    client = boto3.client("appsync", region_name="eu-west-1")
+    api_id = client.create_graphql_api(name="api1", authenticationType="API_KEY")[
+        "graphqlApi"
+    ]["apiId"]
+
+    client.create_api_cache(
+        apiId=api_id,
+        ttl=300,
+        apiCachingBehavior="FULL_REQUEST_CACHING",
+        type="T2_SMALL",
+    )["apiCache"]
+
+    cache = client.update_api_cache(
+        apiId=api_id,
+        ttl=500,
+        apiCachingBehavior="PER_RESOLVER_CACHING",
+        type="R4_4XLARGE",
+    )["apiCache"]
+    assert cache["ttl"] == 500
+    assert cache["apiCachingBehavior"] == "PER_RESOLVER_CACHING"
+    assert cache["transitEncryptionEnabled"] is False
+    assert cache["atRestEncryptionEnabled"] is False
+    assert cache["type"] == "R4_4XLARGE"
+    assert cache["status"] == "AVAILABLE"
+    assert cache["healthMetricsConfig"] == "DISABLED"
+
+    cache = client.update_api_cache(
+        apiId=api_id,
+        ttl=300,
+        apiCachingBehavior="FULL_REQUEST_CACHING",
+        type="SMALL",
+        healthMetricsConfig="ENABLED",
+    )["apiCache"]
+
+    assert cache["healthMetricsConfig"] == "ENABLED"
+
+
+@mock_aws
+def test_update_api_cache_error():
+    client = boto3.client("appsync", region_name="eu-west-1")
+    with pytest.raises(ClientError) as exc:
+        client.update_api_cache(
+            apiId="unknown",
+            ttl=100,
+            apiCachingBehavior="FULL_REQUEST_CACHING",
+            type="SMALL",
+        )
+    err = exc.value.response["Error"]
+
+    assert err["Code"] == "NotFoundException"
+    assert err["Message"] == "GraphQL API unknown not found."
+
+    api_id = client.create_graphql_api(name="api1", authenticationType="API_KEY")[
+        "graphqlApi"
+    ]["apiId"]
+
+    with pytest.raises(ClientError) as exc:
+        client.update_api_cache(
+            apiId=api_id,
+            ttl=100,
+            apiCachingBehavior="FULL_REQUEST_CACHING",
+            type="SMALL",
+        )
+    err = exc.value.response["Error"]
+
+    assert err["Code"] == "NotFoundException"
+    assert (
+        err["Message"]
+        == "Unable to update the cache as it doesn't exist, please create the cache first."
+    )
+
+
+@mock_aws
+def test_flush_api_cache():
+    client = boto3.client("appsync", region_name="us-east-2")
+    with pytest.raises(ClientError) as exc:
+        client.flush_api_cache(apiId="unknown")
+    err = exc.value.response["Error"]
+
+    assert err["Code"] == "NotFoundException"
+    assert err["Message"] == "GraphQL API unknown not found."
+
+    api_id = client.create_graphql_api(name="api1", authenticationType="API_KEY")[
+        "graphqlApi"
+    ]["apiId"]
+
+    with pytest.raises(ClientError) as exc:
+        client.flush_api_cache(apiId=api_id)
+    err = exc.value.response["Error"]
+
+    assert err["Code"] == "NotFoundException"
+    assert (
+        err["Message"]
+        == "Unable to flush the cache as it doesn't exist, please create the cache first."
+    )
+
+    client.create_api_cache(
+        apiId=api_id,
+        ttl=300,
+        apiCachingBehavior="FULL_REQUEST_CACHING",
+        type="T2_SMALL",
+    )["apiCache"]
+    client.flush_api_cache(apiId=api_id)
diff --git a/tests/test_dynamodb/test_dynamodb_update_expressions.py b/tests/test_dynamodb/test_dynamodb_update_expressions.py
index 3add47cfd..b14e66987 100644
--- a/tests/test_dynamodb/test_dynamodb_update_expressions.py
+++ b/tests/test_dynamodb/test_dynamodb_update_expressions.py
@@ -225,3 +225,65 @@ def test_update_item_with_empty_expression(table_name=None):
     assert (
         err["Message"] == "Invalid UpdateExpression: The expression can not be empty;"
     )
+
+
+@pytest.mark.aws_verified
+@dynamodb_aws_verified()
+def test_update_expression_with_multiple_remove_clauses(table_name=None):
+    ddb_client = boto3.client("dynamodb", "us-east-1")
+    payload = {
+        "pk": {"S": "primary_key"},
+        "current_user": {"M": {"name": {"S": "John"}, "surname": {"S": "Doe"}}},
+        "user_list": {
+            "L": [
+                {"M": {"name": {"S": "John"}, "surname": {"S": "Doe"}}},
+                {"M": {"name": {"S": "Jane"}, "surname": {"S": "Smith"}}},
+            ]
+        },
+        "some_param": {"NULL": True},
+    }
+    ddb_client.put_item(TableName=table_name, Item=payload)
+    with pytest.raises(ClientError) as exc:
+        ddb_client.update_item(
+            TableName=table_name,
+            Key={"pk": {"S": "primary_key"}},
+            UpdateExpression="REMOVE #ulist[0] SET current_user = :current_user REMOVE some_param",
+            ExpressionAttributeNames={"#ulist": "user_list"},
+            ExpressionAttributeValues={":current_user": {"M": {"name": {"S": "Jane"}}}},
+        )
+    err = exc.value.response["Error"]
+    assert err["Code"] == "ValidationException"
+    assert (
+        err["Message"]
+        == 'Invalid UpdateExpression: The "REMOVE" section can only be used once in an update expression;'
+    )
+
+
+@pytest.mark.aws_verified
+@dynamodb_aws_verified()
+def test_update_expression_remove_list_and_attribute(table_name=None):
+    ddb_client = boto3.client("dynamodb", "us-east-1")
+    payload = {
+        "pk": {"S": "primary_key"},
+        "user_list": {
+            "L": [
+                {"M": {"name": {"S": "John"}, "surname": {"S": "Doe"}}},
+                {"M": {"name": {"S": "Jane"}, "surname": {"S": "Smith"}}},
+            ]
+        },
+        "some_param": {"NULL": True},
+    }
+    ddb_client.put_item(TableName=table_name, Item=payload)
+    ddb_client.update_item(
+        TableName=table_name,
+        Key={"pk": {"S": "primary_key"}},
+        UpdateExpression="REMOVE #ulist[0], some_param",
+        ExpressionAttributeNames={"#ulist": "user_list"},
+    )
+    item = ddb_client.get_item(
+        TableName=table_name, Key={"pk": {"S": "primary_key"}, "sk": {"S": "sort_key"}}
+    )["Item"]
+    assert item == {
+        "user_list": {"L": [{"M": {"name": {"S": "Jane"}, "surname": {"S": "Smith"}}}]},
+        "pk": {"S": "primary_key"},
+    }
diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py
index 310d547cf..cab288905 100644
--- a/tests/test_iam/test_iam.py
+++ b/tests/test_iam/test_iam.py
@@ -67,6 +67,84 @@ MOCK_POLICY_3 = """
 }
 """
 
+MOCK_SAML_SAMPLE_METADATA = """<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" entityID="urn:amazon:webservices" validUntil="2025-08-30T00:00:00Z">
+  <SPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol" WantAssertionsSigned="true">
+    <KeyDescriptor use="signing">
+      <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
+        <ds:X509Data>
+          <ds:X509Certificate>MIIDcTCCAlmgAwIBAgIIGPlChvWsb9QwDQYJKoZIhvcNAQELBQAwZzEfMB0GA1UE AxMWdXJuOmFtYXpvbjp3ZWJzZXJ2aWNlczEiMCAGA1UEChMZQW1hem9uIFdlYiBT ZXJ2aWNlcywgSW5jLjETMBEGA1UECBMKV2FzaGluZ3RvbjELMAkGA1UEBhMCVVMw HhcNMjQwODMwMDAwMDAwWhcNMjUwODMwMDAwMDAwWjBnMR8wHQYDVQQDExZ1cm46 YW1hem9uOndlYnNlcnZpY2VzMSIwIAYDVQQKExlBbWF6b24gV2ViIFNlcnZpY2Vz LCBJbmMuMRMwEQYDVQQIEwpXYXNoaW5ndG9uMQswCQYDVQQGEwJVUzCCASIwDQYJ KoZIhvcNAQEBBQADggEPADCCAQoCggEBAK2IcN8WBhWhmwR5AOHJ7LIyl+f7ZiYM OjzsNZeOhvD6t8fIPqXg2Kyt2ixCCB16AQsif/bxYAoqqwrVkuBwG8V9CrDDuvXX 3WL7+hvn/JzmjyTeZxy34ySMppSvR3JNtps1C/stTs90KKAKfP56wg1RIEfA6dWX S/Ebd05Jx1miZb2j2+rX1C8Oc6RhOhBCHbbR/4fzFXxdocs4Krq6r+So2XQ8JChC 6E/c092/hCTBXtBj0D8Lk1DJWI1avMzRbrm53wH+1qTzoZMZu66gsuvWhbeH7B/Q kixxpb2IUNYNIT5A4g8tzzQLeGaMUHpvY8fuzRkX/8Zrd8CQjveOdPUCAwEAAaMh MB8wHQYDVR0OBBYEFCM8vneP4Ut+aXPdeCYsbFkUVMpeMA0GCSqGSIb3DQEBCwUA A4IBAQBCz1qeNzA4R/iXrqswc98bZXT7oj1lzkwRaDy0jsfsgyGwHIJ5VOMxnvFH 564zGlbljCOxrZG9FtdrutYGLHaLJLZdHKH2XQy2stxcP6pnZ29LyAMoIP6fq08M 1tzW0u0fMdC2Hpgi8QHGgguKW1AFFK3vj/sW1PWuMzTwB7ur5BpgMo4Wk074HIRT Z8B1RkY13EnFy7IIEIdreKLY3xwrpUZYnW0Em705T3gt0UrFKfzxrTjMPL3yXzgk wkAvGzGJZtdZ0FNc6ecGVuWkbJ7wvbYh2j557gbk9s0F+24103bfRyyM2jj7cgnK ij1wVZYobnMgnQI+ikrd7ksym6QN</ds:X509Certificate>
+        </ds:X509Data>
+      </ds:KeyInfo>
+    </KeyDescriptor>
+    <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
+    <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</NameIDFormat>
+    <NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</NameIDFormat>
+    <NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</NameIDFormat>
+    <NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName</NameIDFormat>
+    <NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName</NameIDFormat>
+    <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos</NameIDFormat>
+    <NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:entity</NameIDFormat>
+    <AssertionConsumerService index="1" isDefault="true" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="2" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://eu-north-1.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="3" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://me-south-1.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="4" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://ap-south-1.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="5" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://eu-west-3.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="6" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://ap-southeast-3.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="7" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://us-east-2.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="8" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://af-south-1.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="9" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://eu-west-1.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="10" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://eu-central-1.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="11" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://sa-east-1.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="12" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://ap-east-1.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="13" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://ap-northeast-2.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="14" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://ap-northeast-3.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="15" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://eu-west-2.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="16" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://eu-south-1.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="17" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://ap-northeast-1.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="18" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://us-west-2.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="19" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://us-west-1.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="20" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://ap-southeast-1.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="21" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://ap-southeast-2.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="22" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://ca-central-1.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="23" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://me-central-1.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="24" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://eu-central-2.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="25" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://eu-south-2.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="26" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://ap-south-2.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="27" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://ap-southeast-4.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="28" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://il-central-1.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="29" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://ca-west-1.signin.aws.amazon.com/saml"/>
+    <AssertionConsumerService index="30" isDefault="false" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://ap-southeast-5.signin.aws.amazon.com/saml"/>
+    <AttributeConsumingService index="1">
+      <ServiceName xml:lang="en">AWS Management Console Single Sign-On</ServiceName>
+      <RequestedAttribute isRequired="true" Name="https://aws.amazon.com/SAML/Attributes/Role" FriendlyName="RoleEntitlement"/>
+      <RequestedAttribute isRequired="true" Name="https://aws.amazon.com/SAML/Attributes/RoleSessionName" FriendlyName="RoleSessionName"/>
+      <RequestedAttribute isRequired="false" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.1" FriendlyName="eduPersonAffiliation"/>
+      <RequestedAttribute isRequired="false" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.2" FriendlyName="eduPersonNickname"/>
+      <RequestedAttribute isRequired="false" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.3" FriendlyName="eduPersonOrgDN"/>
+      <RequestedAttribute isRequired="false" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.4" FriendlyName="eduPersonOrgUnitDN"/>
+      <RequestedAttribute isRequired="false" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.5" FriendlyName="eduPersonPrimaryAffiliation"/>
+      <RequestedAttribute isRequired="false" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.6" FriendlyName="eduPersonPrincipalName"/>
+      <RequestedAttribute isRequired="false" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.7" FriendlyName="eduPersonEntitlement"/>
+      <RequestedAttribute isRequired="false" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.8" FriendlyName="eduPersonPrimaryOrgUnitDN"/>
+      <RequestedAttribute isRequired="false" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.9" FriendlyName="eduPersonScopedAffiliation"/>
+      <RequestedAttribute isRequired="false" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.10" FriendlyName="eduPersonTargetedID"/>
+      <RequestedAttribute isRequired="false" Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.11" FriendlyName="eduPersonAssurance"/>
+      <RequestedAttribute isRequired="false" Name="urn:oid:1.3.6.1.4.1.5923.1.2.1.2" FriendlyName="eduOrgHomePageURI"/>
+      <RequestedAttribute isRequired="false" Name="urn:oid:1.3.6.1.4.1.5923.1.2.1.3" FriendlyName="eduOrgIdentityAuthNPolicyURI"/>
+      <RequestedAttribute isRequired="false" Name="urn:oid:1.3.6.1.4.1.5923.1.2.1.4" FriendlyName="eduOrgLegalName"/>
+      <RequestedAttribute isRequired="false" Name="urn:oid:1.3.6.1.4.1.5923.1.2.1.5" FriendlyName="eduOrgSuperiorURI"/>
+      <RequestedAttribute isRequired="false" Name="urn:oid:1.3.6.1.4.1.5923.1.2.1.6" FriendlyName="eduOrgWhitePagesURI"/>
+      <RequestedAttribute isRequired="false" Name="urn:oid:2.5.4.3" FriendlyName="cn"/>
+    </AttributeConsumingService>
+  </SPSSODescriptor>
+  <Organization>
+    <OrganizationName xml:lang="en">Amazon Web Services, Inc.</OrganizationName>
+    <OrganizationDisplayName xml:lang="en">AWS</OrganizationDisplayName>
+    <OrganizationURL xml:lang="en">https://aws.amazon.com</OrganizationURL>
+  </Organization>
+</EntityDescriptor>
+"""
+
 MOCK_STS_EC2_POLICY_DOCUMENT = """{
       "Version": "2012-10-17",
       "Statement": [
@@ -2554,6 +2632,34 @@ def test_create_saml_provider():
     )
 
 
+@mock_aws()
+def test_create_saml_provider_with_samlmetadatadocument():
+    conn = boto3.client("iam", region_name="us-east-1")
+    response = conn.create_saml_provider(
+        Name="TestSAMLProvider", SAMLMetadataDocument=MOCK_SAML_SAMPLE_METADATA
+    )
+    assert (
+        response["SAMLProviderArn"]
+        == f"arn:aws:iam::{ACCOUNT_ID}:saml-provider/TestSAMLProvider"
+    )
+
+
+@mock_aws()
+def test_update_saml_provider_with_samlmetadatadocument():
+    conn = boto3.client("iam", region_name="us-east-1")
+    sam_provider_create = conn.create_saml_provider(
+        Name="TestSAMLProvider", SAMLMetadataDocument="a" * 1024
+    )
+    saml_provider_update = conn.update_saml_provider(
+        SAMLMetadataDocument=MOCK_SAML_SAMPLE_METADATA,
+        SAMLProviderArn=sam_provider_create["SAMLProviderArn"],
+    )
+    response = conn.get_saml_provider(
+        SAMLProviderArn=saml_provider_update["SAMLProviderArn"]
+    )
+    assert response["SAMLMetadataDocument"] == MOCK_SAML_SAMPLE_METADATA
+
+
 @mock_aws()
 def test_get_saml_provider():
     conn = boto3.client("iam", region_name="us-east-1")
@@ -2566,6 +2672,18 @@ def test_get_saml_provider():
     assert response["SAMLMetadataDocument"] == "a" * 1024
 
 
+@mock_aws()
+def test_get_saml_provider_with_samlmetadatadocument():
+    conn = boto3.client("iam", region_name="us-east-1")
+    saml_provider_create = conn.create_saml_provider(
+        Name="TestSAMLProvider", SAMLMetadataDocument=MOCK_SAML_SAMPLE_METADATA
+    )
+    response = conn.get_saml_provider(
+        SAMLProviderArn=saml_provider_create["SAMLProviderArn"]
+    )
+    assert response["SAMLMetadataDocument"] == MOCK_SAML_SAMPLE_METADATA
+
+
 @mock_aws()
 def test_list_saml_providers():
     conn = boto3.client("iam", region_name="us-east-1")
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]