Repository: getmoto/moto
Pull request #8266 (2024-10-28 20:54:33)

Update sagemaker-runtime.rst
sync call was using wrong client


Patch:
diff --git a/docs/docs/services/sagemaker-runtime.rst b/docs/docs/services/sagemaker-runtime.rst
index 8c3e1fbb7..20849463f 100644
--- a/docs/docs/services/sagemaker-runtime.rst
+++ b/docs/docs/services/sagemaker-runtime.rst
@@ -46,7 +46,7 @@ sagemaker-runtime
                 json=expected_results,
             )
 
-            client = boto3.client("sagemaker", region_name="us-east-1")
+            client = boto3.client("sagemaker-runtime", region_name="us-east-1")
             details = client.invoke_endpoint(EndpointName="asdf", Body="qwer")
 
         
diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py
index 480d648c3..fde8bfc8c 100644
--- a/moto/awslambda/models.py
+++ b/moto/awslambda/models.py
@@ -2286,7 +2286,7 @@ class LambdaBackend(BaseBackend):
             "Records": [
                 {
                     "eventID": item.to_json()["eventID"],
-                    "eventName": "INSERT",
+                    "eventName": item.to_json()["eventName"],
                     "eventVersion": item.to_json()["eventVersion"],
                     "eventSource": item.to_json()["eventSource"],
                     "awsRegion": self.region_name,
diff --git a/moto/utilities/utils.py b/moto/utilities/utils.py
index d0ad79d65..1b4ccb53f 100644
--- a/moto/utilities/utils.py
+++ b/moto/utilities/utils.py
@@ -3,6 +3,7 @@ import hashlib
 import json
 import pkgutil
 from typing import Any, Dict, Iterator, List, MutableMapping, Optional, Tuple, TypeVar
+from uuid import UUID
 
 DEFAULT_PARTITION = "aws"
 REGION_PREFIX_TO_PARTITION = {
@@ -67,6 +68,14 @@ def merge_multiple_dicts(*args: Any) -> Dict[str, Any]:
     return result
 
 
+def is_valid_uuid(uuid: str, version: int = 4) -> bool:
+    try:
+        UUID(uuid, version=version)
+        return True
+    except ValueError:
+        return False
+
+
 RESOURCE_TYPE = TypeVar("RESOURCE_TYPE")
 
 
diff --git a/moto/wafv2/exceptions.py b/moto/wafv2/exceptions.py
index 6a9810eeb..76d69f44a 100644
--- a/moto/wafv2/exceptions.py
+++ b/moto/wafv2/exceptions.py
@@ -1,3 +1,5 @@
+from typing import Optional
+
 from moto.core.exceptions import JsonRESTError
 
 
@@ -13,6 +15,26 @@ class WAFV2DuplicateItemException(WAFv2ClientError):
         )
 
 
+class WAFV2InsufficientInformationException(WAFv2ClientError):
+    def __init__(
+        self,
+        name: Optional[str],
+        scope: Optional[str],
+        id: Optional[str],
+        arn: Optional[str],
+    ) -> None:
+        super().__init__(
+            "AcessDeniedException",
+            (
+                "Critical information is missing in your request: "
+                f"GetRuleGroupRequest(name={name if name else 'null'}, "
+                f"scope={scope if scope else 'null'}, "
+                f"id={id if id else 'null'}, "
+                f"aRN={arn if arn is not None else 'null'})."
+            ),
+        )
+
+
 class WAFNonexistentItemException(WAFv2ClientError):
     def __init__(self) -> None:
         super().__init__(
diff --git a/moto/wafv2/models.py b/moto/wafv2/models.py
index 5623e8138..4c0e1b87b 100644
--- a/moto/wafv2/models.py
+++ b/moto/wafv2/models.py
@@ -1,10 +1,13 @@
 import re
 from collections import OrderedDict
-from typing import TYPE_CHECKING, Any, Dict, List, Optional
+from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
 
 from moto.core.base_backend import BackendDict, BaseBackend
 from moto.core.common_models import BaseModel
-from moto.core.utils import iso_8601_datetime_with_milliseconds
+from moto.core.utils import (
+    camelcase_to_underscores,
+    iso_8601_datetime_with_milliseconds,
+)
 from moto.moto_api._internal import mock_random
 from moto.utilities.tagging_service import TaggingService
 from moto.utilities.utils import ARN_PARTITION_REGEX, PARTITION_NAMES
@@ -13,8 +16,9 @@ from .exceptions import (
     WAFNonexistentItemException,
     WAFOptimisticLockException,
     WAFV2DuplicateItemException,
+    WAFV2InsufficientInformationException,
 )
-from .utils import make_arn_for_ip_set, make_arn_for_wacl
+from .utils import make_arn_for_ip_set, make_arn_for_rule_group, make_arn_for_wacl
 
 if TYPE_CHECKING:
     from moto.apigateway.models import Stage
@@ -28,7 +32,49 @@ APIGATEWAY_REGEX = (
 )
 
 
-# TODO: Add remaining properties
+class FakeRule(BaseModel):
+    def __init__(
+        self,
+        name: str,
+        priority: int,
+        statement: Dict[str, Any],
+        visibility_config: Dict[str, Union[str, bool]],
+        action: Optional[Dict[str, Any]] = None,
+        captcha_config: Optional[Dict[str, Dict[str, int]]] = None,
+        challenge_config: Optional[Dict[str, Dict[str, int]]] = None,
+        override_action: Optional[Dict[str, Any]] = None,
+        rule_labels: Optional[List[Dict[str, str]]] = None,
+    ):
+        self.name = name
+        self.priority = priority
+        self.statement = statement
+        self.visibility_config = visibility_config
+        self.action = action
+        self.captcha_config = captcha_config
+        self.challenge_config = challenge_config
+        self.override_action = override_action
+        self.rule_labels = rule_labels
+
+    def get_consumed_label(self) -> Optional[str]:
+        return self.statement.get("LabelMatchStatement", {}).get("Key")
+
+    def get_available_labels(self) -> Optional[List[str]]:
+        return [r["Name"] for r in self.rule_labels] if self.rule_labels else None
+
+    def to_dict(self) -> Dict[str, Any]:
+        return {
+            "Name": self.name,
+            "Action": self.action,
+            "Priority": self.priority,
+            "Statement": self.statement,
+            "VisibilityConfig": self.visibility_config,
+            "CaptchaConfig": self.captcha_config,
+            "ChallengeConfig": self.challenge_config,
+            "OverrideAction": self.override_action,
+            "RuleLabels": self.rule_labels,
+        }
+
+
 class FakeWebACL(BaseModel):
     """
     https://docs.aws.amazon.com/waf/latest/APIReference/API_WebACL.html
@@ -37,31 +83,55 @@ class FakeWebACL(BaseModel):
     def __init__(
         self,
         name: str,
+        account: str,
         arn: str,
         wacl_id: str,
         visibility_config: Dict[str, Any],
         default_action: Dict[str, Any],
         description: Optional[str],
-        rules: List[Dict[str, Any]],
+        rules: Optional[List[FakeRule]],
+        association_config: Optional[Dict[str, Any]] = None,
+        captcha_config: Optional[Dict[str, Any]] = None,
+        challenge_config: Optional[Dict[str, Any]] = None,
+        custom_response_bodies: Optional[Dict[str, Any]] = None,
+        token_domains: Optional[List[str]] = None,
     ):
         self.name = name
+        self.account = account
         self.created_time = iso_8601_datetime_with_milliseconds()
         self.id = wacl_id
         self.arn = arn
         self.description = description or ""
         self.capacity = 3
-        self.rules = rules
+        self.rules = rules or []
         self.visibility_config = visibility_config
         self.default_action = default_action
-        self.lock_token = str(mock_random.uuid4())[0:6]
+        self.lock_token = self._generate_lock_token()
         self.associated_resources: List[str] = []
+        self.association_config = association_config
+        self.captcha_config = captcha_config
+        self.challenge_config = challenge_config
+        self.custom_response_bodies = custom_response_bodies
+        self.token_domains = token_domains
+        self.label_namespace = self._get_label_namespace()
+
+    def _generate_lock_token(self) -> str:
+        return str(mock_random.uuid4())
+
+    def _get_label_namespace(self) -> str:
+        return f"awswaf:{self.account}:webacl:{self.name}:"
 
     def update(
         self,
         default_action: Optional[Dict[str, Any]],
-        rules: Optional[List[Dict[str, Any]]],
+        rules: Optional[List[FakeRule]],
         description: Optional[str],
         visibility_config: Optional[Dict[str, Any]],
+        custom_response_bodies: Optional[Dict[str, Any]],
+        captcha_config: Optional[Dict[str, Any]],
+        challenge_config: Optional[Dict[str, Any]],
+        token_domains: Optional[List[str]],
+        association_config: Optional[Dict[str, Any]],
     ) -> None:
         if default_action is not None:
             self.default_action = default_action
@@ -71,18 +141,45 @@ class FakeWebACL(BaseModel):
             self.description = description
         if visibility_config is not None:
             self.visibility_config = visibility_config
-        self.lock_token = str(mock_random.uuid4())[0:6]
-
-    def to_dict(self) -> Dict[str, Any]:
+        if custom_response_bodies is not None:
+            self.custom_response_bodies = custom_response_bodies
+        if captcha_config is not None:
+            self.captcha_config = captcha_config
+        if challenge_config is not None:
+            self.challenge_config = challenge_config
+        if token_domains is not None:
+            self.token_domains = token_domains
+        if association_config is not None:
+            self.association_config = association_config
+        self.lock_token = self._generate_lock_token()
+
+    def to_short_dict(self) -> Dict[str, Any]:
         # Format for summary https://docs.aws.amazon.com/waf/latest/APIReference/API_CreateWebACL.html (response syntax section)
         return {
             "ARN": self.arn,
             "Description": self.description,
             "Id": self.id,
             "Name": self.name,
-            "Rules": self.rules,
+            "LockToken": self.lock_token,
+        }
+
+    def to_dict(self) -> Dict[str, Any]:
+        return {
+            "Name": self.name,
+            "Id": self.id,
+            "ARN": self.arn,
+            "Description": self.description,
+            "Rules": [r.to_dict() for r in self.rules],
             "DefaultAction": self.default_action,
             "VisibilityConfig": self.visibility_config,
+            "Capacity": self.capacity,
+            "LockToken": self.lock_token,
+            "AssociationConfig": self.association_config,
+            "CaptchaConfig": self.captcha_config,
+            "ChallengeConfig": self.challenge_config,
+            "CustomResponseBodies": self.custom_response_bodies,
+            "TokenDomains": self.token_domains,
+            "LabelNamespace": self.label_namespace,
         }
 
 
@@ -130,7 +227,7 @@ class FakeIPSet(BaseModel):
         }
 
 
-class FakeLoggingConfiguration:
+class FakeLoggingConfiguration(BaseModel):
     def __init__(
         self,
         arn: str,
@@ -155,6 +252,108 @@ class FakeLoggingConfiguration:
         }
 
 
+class FakeRuleGroup(BaseModel):
+    def __init__(
+        self,
+        account: str,
+        name: str,
+        id: str,
+        arn: str,
+        scope: str,
+        capacity: int,
+        visibility_config: Dict[str, Any],
+        description: Optional[str],
+        rules: Optional[List[FakeRule]],
+        custom_response_bodies: Optional[Dict[str, Any]] = None,
+    ):
+        self.account = account
+        self.name = name
+        self.id = id
+        self.arn = arn
+        self.lock_token = self._generate_lock_token()
+        self.scope = scope
+        self.capacity = capacity
+        self.description = description or ""
+        self.rules = rules or []
+        self.visibility_config = visibility_config
+        self.custom_response_bodies = custom_response_bodies
+        self.label_namespace = self._get_label_namespace()
+        self.available_labels = self._get_available_labels()
+        self.consumed_labels = self._get_consumed_labels()
+
+    def _generate_lock_token(self) -> str:
+        return str(mock_random.uuid4())
+
+    def _get_available_labels(self) -> Optional[List[Dict[str, str]]]:
+        return (
+            [
+                {"Name": f"{self.label_namespace}{label}"}
+                for rule in self.rules
+                for label in (rule.get_available_labels() or [])
+            ]
+            if self.rules
+            else None
+        )
+
+    def _get_consumed_labels(self) -> Optional[List[Dict[str, str]]]:
+        return (
+            [
+                {"Name": f"{self.label_namespace}{label}"}
+                for rule in self.rules
+                if (label := rule.get_consumed_label())
+            ]
+            if self.rules
+            else None
+        )
+
+    def _get_label_namespace(self) -> str:
+        return f"awswaf:{self.account}:rulegroup:{self.name}:"
+
+    def update(
+        self,
+        description: Optional[str],
+        rules: Optional[List[FakeRule]],
+        visibility_config: Optional[Dict[str, Any]],
+        custom_response_bodies: Optional[Dict[str, Any]],
+    ) -> str:
+        if description is not None:
+            self.description = description
+        if rules is not None:
+            self.rules = rules
+        if visibility_config is not None:
+            self.visibility_config = visibility_config
+        if custom_response_bodies is not None:
+            self.custom_response_bodies = custom_response_bodies
+
+        self.lock_token = self._generate_lock_token()
+        return self.lock_token
+
+    def to_short_dict(self) -> Dict[str, Any]:
+        return {
+            "Name": self.name,
+            "Id": self.id,
+            "Description": self.description,
+            "LockToken": self.lock_token,
+            "ARN": self.arn,
+        }
+
+    def to_dict(self) -> Dict[str, Any]:
+        return {
+            "Name": self.name,
+            "Id": self.id,
+            "Capacity": self.capacity,
+            "ARN": self.arn,
+            "Description": self.description,
+            "Rules": [r.to_dict() for r in self.rules],
+            "VisibilityConfig": self.visibility_config,
+            "LabelNamespace": self.label_namespace,
+            "AvailableLabels": self.available_labels,
+            "ConsumedLabels": self.consumed_labels,
+            "CustomResponseBodies": self.custom_response_bodies,
+            "LockToken": self.lock_token,
+        }
+
+
 class WAFV2Backend(BaseBackend):
     """
     https://docs.aws.amazon.com/waf/latest/APIReference/API_Operations_AWS_WAFV2.html
@@ -165,6 +364,7 @@ class WAFV2Backend(BaseBackend):
         self.wacls: Dict[str, FakeWebACL] = OrderedDict()
         self.ip_sets: Dict[str, FakeIPSet] = OrderedDict()
         self.logging_configurations: Dict[str, FakeLoggingConfiguration] = OrderedDict()
+        self.rule_groups: Dict[str, FakeRuleGroup] = OrderedDict()
         self.tagging_service = TaggingService()
         # TODO: self.load_balancers = OrderedDict()
 
@@ -207,6 +407,9 @@ class WAFV2Backend(BaseBackend):
         except:  # noqa: E722 Do not use bare except
             return None
 
+    def _generate_id(self) -> str:
+        return str(mock_random.uuid4())
+
     def create_web_acl(
         self,
         name: str,
@@ -216,36 +419,59 @@ class WAFV2Backend(BaseBackend):
         description: str,
         tags: List[Dict[str, str]],
         rules: List[Dict[str, Any]],
+        association_config: Optional[Dict[str, Any]],
+        captcha_config: Optional[Dict[str, Any]],
+        challenge_config: Optional[Dict[str, Any]],
+        custom_response_bodies: Optional[Dict[str, Any]],
+        token_domains: Optional[List[str]],
     ) -> FakeWebACL:
-        """
-        The following parameters are not yet implemented: CustomResponseBodies, CaptchaConfig
-        """
-        wacl_id = str(mock_random.uuid4())
+        wacl_id = self._generate_id()
         arn = make_arn_for_wacl(
             name=name,
             account_id=self.account_id,
             region_name=self.region_name,
-            wacl_id=wacl_id,
+            _id=wacl_id,
             scope=scope,
         )
         if arn in self.wacls or self._is_duplicate_name(name):
             raise WAFV2DuplicateItemException()
+        rule_objs = [
+            FakeRule(**{camelcase_to_underscores(k): v for k, v in rule.items()})
+            for rule in rules
+        ]
         new_wacl = FakeWebACL(
-            name, arn, wacl_id, visibility_config, default_action, description, rules
+            name=name,
+            account=self.account_id,
+            arn=arn,
+            wacl_id=wacl_id,
+            visibility_config=visibility_config,
+            default_action=default_action,
+            description=description,
+            rules=rule_objs,
+            association_config=association_config,
+            captcha_config=captcha_config,
+            challenge_config=challenge_config,
+            custom_response_bodies=custom_response_bodies,
+            token_domains=token_domains,
         )
         self.wacls[arn] = new_wacl
         self.tag_resource(arn, tags)
         return new_wacl
 
-    def delete_web_acl(self, name: str, _id: str) -> None:
-        """
-        The LockToken-parameter is not yet implemented
-        """
-        self.wacls = {
-            arn: wacl
-            for arn, wacl in self.wacls.items()
-            if wacl.name != name and wacl.id != _id
-        }
+    def delete_web_acl(self, name: str, scope: str, _id: str, lock_token: str) -> None:
+        arn = make_arn_for_wacl(
+            name=name,
+            account_id=self.account_id,
+            region_name=self.region_name,
+            _id=_id,
+            scope=scope,
+        )
+        if arn not in self.wacls:
+            raise WAFNonexistentItemException
+        wacl = self.wacls[arn]
+        if wacl.lock_token != lock_token:
+            raise WAFOptimisticLockException()
+        self.wacls.pop(arn)
 
     def get_web_acl(self, name: str, _id: str) -> FakeWebACL:
         for wacl in self.wacls.values():
@@ -254,14 +480,17 @@ class WAFV2Backend(BaseBackend):
         raise WAFNonexistentItemException
 
     def list_web_acls(self) -> List[Dict[str, Any]]:
-        return [wacl.to_dict() for wacl in self.wacls.values()]
+        return [wacl.to_short_dict() for wacl in self.wacls.values()]
 
     def _is_duplicate_name(self, name: str) -> bool:
-        allWaclNames = set(wacl.name for wacl in self.wacls.values())
-        return name in allWaclNames
+        all_wacl_names = set(wacl.name for wacl in self.wacls.values())
+        return name in all_wacl_names
 
-    def list_rule_groups(self) -> List[Any]:
-        return []
+    def list_rule_groups(self, scope: str) -> List[Any]:
+        rule_groups = [
+            group for group in self.rule_groups.values() if group.scope == scope
+        ]
+        return rule_groups
 
     def list_tags_for_resource(self, arn: str) -> List[Dict[str, str]]:
         """
@@ -283,12 +512,35 @@ class WAFV2Backend(BaseBackend):
         rules: Optional[List[Dict[str, Any]]],
         description: Optional[str],
         visibility_config: Optional[Dict[str, Any]],
+        lock_token: str,
+        custom_response_bodies: Optional[Dict[str, Any]],
+        captcha_config: Optional[Dict[str, Any]],
+        challenge_config: Optional[Dict[str, Any]],
+        token_domains: Optional[List[str]],
+        association_config: Optional[Dict[str, Any]],
     ) -> str:
-        """
-        The following parameters are not yet implemented: LockToken, CustomResponseBodies, CaptchaConfig
-        """
         acl = self.get_web_acl(name, _id)
-        acl.update(default_action, rules, description, visibility_config)
+        if acl.lock_token != lock_token:
+            raise WAFOptimisticLockException()
+        rule_objs = (
+            [
+                FakeRule(**{camelcase_to_underscores(k): v for k, v in rule.items()})
+                for rule in rules
+            ]
+            if rules
+            else None
+        )
+        acl.update(
+            default_action,
+            rule_objs,
+            description,
+            visibility_config,
+            custom_response_bodies,
+            captcha_config,
+            challenge_config,
+            token_domains,
+            association_config,
+        )
         return acl.lock_token
 
     def create_ip_set(
@@ -300,7 +552,7 @@ class WAFV2Backend(BaseBackend):
         addresses: List[str],
         tags: List[Dict[str, str]],
     ) -> FakeIPSet:
-        ip_set_id = str(mock_random.uuid4())
+        ip_set_id = self._generate_id()
         arn = make_arn_for_ip_set(
             name=name,
             account_id=self.account_id,
@@ -425,5 +677,110 @@ class WAFV2Backend(BaseBackend):
             if f":{scope}:" in arn
         ]
 
+    def create_rule_group(
+        self,
+        name: str,
+        scope: str,
+        capacity: int,
+        description: Optional[str],
+        rules: Optional[List[Dict[str, Any]]],
+        visibility_config: Dict[str, Union[bool, str]],
+        tags: Optional[List[Dict[str, str]]],
+        custom_response_bodies: Optional[Dict[str, str]],
+    ) -> FakeRuleGroup:
+        id = self._generate_id()
+        arn = make_arn_for_rule_group(
+            name, self.account_id, self.region_name, id, scope
+        )
+        if name in set(group.name for group in self.rule_groups.values()):
+            raise WAFV2DuplicateItemException()
+        rules_objs = (
+            [
+                FakeRule(**{camelcase_to_underscores(k): v for k, v in rule.items()})
+                for rule in rules
+            ]
+            if rules
+            else None
+        )
+        rule_group = FakeRuleGroup(
+            name=name,
+            account=self.account_id,
+            id=id,
+            arn=arn,
+            scope=scope,
+            capacity=capacity,
+            visibility_config=visibility_config,
+            description=description,
+            rules=rules_objs,
+            custom_response_bodies=custom_response_bodies,
+        )
+        self.rule_groups[arn] = rule_group
+        self.tagging_service.tag_resource(arn, tags)
+        return rule_group
+
+    def update_rule_group(
+        self,
+        name: str,
+        scope: str,
+        id: str,
+        description: Optional[str],
+        rules: Optional[List[Dict[str, Any]]],
+        visibility_config: Dict[str, Any],
+        lock_token: str,
+        custom_response_bodies: Optional[Dict[str, Any]],
+    ) -> FakeRuleGroup:
+        arn = make_arn_for_rule_group(
+            name, self.account_id, self.region_name, id, scope
+        )
+        if not (group := self.rule_groups.get(arn)):
+            raise WAFNonexistentItemException()
+        if group.lock_token != lock_token:
+            raise WAFOptimisticLockException()
+        rules_objs = (
+            [
+                FakeRule(**{camelcase_to_underscores(k): v for k, v in rule.items()})
+                for rule in rules
+            ]
+            if rules
+            else None
+        )
+        group.update(description, rules_objs, visibility_config, custom_response_bodies)
+
+        return group
+
+    def delete_rule_group(
+        self, name: str, scope: str, id: str, lock_token: str
+    ) -> None:
+        arn = make_arn_for_rule_group(
+            name, self.account_id, self.region_name, id, scope
+        )
+        if not (group := self.rule_groups.get(arn)):
+            raise WAFNonexistentItemException()
+        if group.lock_token != lock_token:
+            raise WAFOptimisticLockException()
+        self.rule_groups.pop(arn)
+        return
+
+    def get_rule_group(
+        self,
+        name: Optional[str],
+        scope: Optional[str],
+        id: Optional[str],
+        arn: Optional[str],
+    ) -> FakeRuleGroup:
+        if not arn and not (name and scope and id):
+            raise WAFV2InsufficientInformationException(name, scope, id, arn)
+        else:
+            arn = arn or make_arn_for_rule_group(
+                name,  # type: ignore[arg-type]
+                self.account_id,
+                self.region_name,
+                id,  # type: ignore[arg-type]
+                scope,  # type: ignore[arg-type]
+            )
+        if not (group := self.rule_groups.get(arn)):
+            raise WAFNonexistentItemException()
+        return group
+
 
 wafv2_backends = BackendDict(WAFV2Backend, "wafv2", additional_regions=PARTITION_NAMES)
diff --git a/moto/wafv2/responses.py b/moto/wafv2/responses.py
index 68b496059..b8b03c22e 100644
--- a/moto/wafv2/responses.py
+++ b/moto/wafv2/responses.py
@@ -47,6 +47,11 @@ class WAFV2Response(BaseResponse):
         description = body.get("Description")
         tags = body.get("Tags", [])
         rules = body.get("Rules", [])
+        association_config = body.get("AssociationConfig")
+        captcha_config = body.get("CaptchaConfig")
+        challenge_config = body.get("ChallengeConfig")
+        custom_response_bodies = body.get("CustomResponseBodies")
+        token_domains = body.get("TokenDomains")
         web_acl = self.wafv2_backend.create_web_acl(
             name,
             body["VisibilityConfig"],
@@ -55,8 +60,13 @@ class WAFV2Response(BaseResponse):
             description,
             tags,
             rules,
+            association_config,
+            captcha_config,
+            challenge_config,
+            custom_response_bodies,
+            token_domains,
         )
-        response = {"Summary": web_acl.to_dict()}
+        response = {"Summary": web_acl.to_short_dict()}
         response_headers = {"Content-Type": "application/json"}
         return 200, response_headers, json.dumps(response)
 
@@ -66,7 +76,8 @@ class WAFV2Response(BaseResponse):
             self.region = GLOBAL_REGION
         name = self._get_param("Name")
         _id = self._get_param("Id")
-        self.wafv2_backend.delete_web_acl(name, _id)
+        lock_token = self._get_param("LockToken")
+        self.wafv2_backend.delete_web_acl(name, scope, _id, lock_token)
         response_headers = {"Content-Type": "application/json"}
         return 200, response_headers, "{}"
 
@@ -96,8 +107,8 @@ class WAFV2Response(BaseResponse):
         scope = self._get_param("Scope")
         if scope == "CLOUDFRONT":
             self.region = GLOBAL_REGION
-        rule_groups = self.wafv2_backend.list_rule_groups()
-        response = {"RuleGroups": [rg.to_dict() for rg in rule_groups]}
+        rule_groups = self.wafv2_backend.list_rule_groups(scope)
+        response = {"RuleGroups": [rg.to_short_dict() for rg in rule_groups]}
         response_headers = {"Content-Type": "application/json"}
         return 200, response_headers, json.dumps(response)
 
@@ -136,10 +147,27 @@ class WAFV2Response(BaseResponse):
         rules = body.get("Rules")
         description = body.get("Description")
         visibility_config = body.get("VisibilityConfig")
-        lock_token = self.wafv2_backend.update_web_acl(
-            name, _id, default_action, rules, description, visibility_config
+        lock_token = body.get("LockToken")
+        custom_response_bodies = body.get("CustomResponseBodies")
+        captcha_config = body.get("CaptchaConfig")
+        challenge_config = body.get("ChallengeConfig")
+        token_domains = body.get("TokenDomains")
+        association_config = body.get("AssociationConfig")
+        new_lock_token = self.wafv2_backend.update_web_acl(
+            name,
+            _id,
+            default_action,
+            rules,
+            description,
+            visibility_config,
+            lock_token,
+            custom_response_bodies,
+            captcha_config,
+            challenge_config,
+            token_domains,
+            association_config,
         )
-        return 200, {}, json.dumps({"NextLockToken": lock_token})
+        return 200, {}, json.dumps({"NextLockToken": new_lock_token})
 
     def create_ip_set(self) -> TYPE_RESPONSE:
         body = json.loads(self.body)
@@ -316,6 +344,86 @@ class WAFV2Response(BaseResponse):
         self.wafv2_backend.delete_logging_configuration(resource_arn)
         return 200, {}, "{}"
 
+    def create_rule_group(self) -> TYPE_RESPONSE:
+        name = self._get_param("Name")
+        scope = self._get_param("Scope")
+        capacity = self._get_param("Capacity")
+        description = self._get_param("Description")
+        rules = self._get_param("Rules", [])
+        visibility_config = self._get_param("VisibilityConfig")
+        tags = self._get_param("Tags")
+        custom_response_bodies = self._get_param("CustomResponseBodies")
+        if scope == "CLOUDFRONT":
+            self.region = GLOBAL_REGION
+        group = self.wafv2_backend.create_rule_group(
+            name=name,
+            scope=scope,
+            capacity=capacity,
+            description=description,
+            rules=rules,
+            visibility_config=visibility_config,
+            tags=tags,
+            custom_response_bodies=custom_response_bodies,
+        )
+        return 200, {}, json.dumps(dict(Summary=group.to_short_dict()))
+
+    def update_rule_group(self) -> TYPE_RESPONSE:
+        name = self._get_param("Name")
+        scope = self._get_param("Scope")
+        id = self._get_param("Id")
+        description = self._get_param("Description")
+        rules = self._get_param("Rules")
+        visibility_config = self._get_param("VisibilityConfig")
+        lock_token = self._get_param("LockToken")
+        custom_response_bodies = self._get_param("CustomResponseBodies")
+        if scope == "CLOUDFRONT":
+            self.region = GLOBAL_REGION
+        updated_group = self.wafv2_backend.update_rule_group(
+            name=name,
+            scope=scope,
+            id=id,
+            description=description,
+            rules=rules,
+            visibility_config=visibility_config,
+            lock_token=lock_token,
+            custom_response_bodies=custom_response_bodies,
+        )
+        return 200, {}, json.dumps(dict(NextLockToken=updated_group.lock_token))
+
+    def delete_rule_group(self) -> TYPE_RESPONSE:
+        name = self._get_param("Name")
+        scope = self._get_param("Scope")
+        id = self._get_param("Id")
+        lock_token = self._get_param("LockToken")
+        if scope == "CLOUDFRONT":
+            self.region = GLOBAL_REGION
+        self.wafv2_backend.delete_rule_group(
+            name=name,
+            scope=scope,
+            id=id,
+            lock_token=lock_token,
+        )
+        return 200, {}, json.dumps(dict())
+
+    def get_rule_group(self) -> TYPE_RESPONSE:
+        name = self._get_param("Name")
+        scope = self._get_param("Scope")
+        id = self._get_param("Id")
+        arn = self._get_param("ARN")
+        if scope == "CLOUDFRONT" or (
+            isinstance(arn, str) and arn.split(":")[3] == GLOBAL_REGION
+        ):
+            self.region = GLOBAL_REGION
+        rule_group = self.wafv2_backend.get_rule_group(
+            name=name,
+            scope=scope,
+            id=id,
+            arn=arn,
+        )
+        group_dict = rule_group.to_dict()
+        lock_token = group_dict.pop("LockToken")
+        return 200, {}, json.dumps(dict(RuleGroup=group_dict, LockToken=lock_token))
+
 
 # notes about region and scope
 # --scope = CLOUDFRONT is ALWAYS us-east-1 (but we use "global" instead to differentiate between REGIONAL us-east-1)
diff --git a/moto/wafv2/urls.py b/moto/wafv2/urls.py
index 86725f2b2..e3908a1df 100644
--- a/moto/wafv2/urls.py
+++ b/moto/wafv2/urls.py
@@ -6,4 +6,5 @@ url_bases = [
 
 url_paths = {
     "{0}/": WAFV2Response.dispatch,
+    "{0}/$": WAFV2Response.dispatch,
 }
diff --git a/moto/wafv2/utils.py b/moto/wafv2/utils.py
index 9bb7b4753..0be4a57dc 100644
--- a/moto/wafv2/utils.py
+++ b/moto/wafv2/utils.py
@@ -1,23 +1,6 @@
-from moto.utilities.utils import PARTITION_NAMES, get_partition
-
-
-def make_arn_for_wacl(
-    name: str, account_id: str, region_name: str, wacl_id: str, scope: str
-) -> str:
-    """https://docs.aws.amazon.com/waf/latest/developerguide/how-aws-waf-works.html - explains --scope (cloudfront vs regional)"""
-    return make_arn(name, account_id, region_name, wacl_id, scope, "webacl")
-
-
-def make_arn_for_ip_set(
-    name: str, account_id: str, region_name: str, _id: str, scope: str
-) -> str:
-    return make_arn(name, account_id, region_name, _id, scope, "ipset")
-
+from functools import partial
 
-def make_arn_for_logging_configuration(
-    name: str, account_id: str, region_name: str, _id: str, scope: str
-) -> str:
-    return make_arn(name, account_id, region_name, _id, scope, "logingconfiguration")
+from moto.utilities.utils import PARTITION_NAMES, get_partition
 
 
 def make_arn(
@@ -35,3 +18,9 @@ def make_arn(
     )
 
     return f"arn:{partition}:wafv2:{region_name}:{account_id}:{scope}/{resource}/{name}/{_id}"
+
+
+make_arn_for_wacl = partial(make_arn, resource="webacl")
+make_arn_for_ip_set = partial(make_arn, resource="ipset")
+make_arn_for_logging_configuration = partial(make_arn, resource="loggingconfiguration")
+make_arn_for_rule_group = partial(make_arn, resource="rulegroup")


Test patch:
diff --git a/tests/test_wafv2/test_wafv2.py b/tests/test_wafv2/test_wafv2.py
index 0646d2fe1..5c8af466f 100644
--- a/tests/test_wafv2/test_wafv2.py
+++ b/tests/test_wafv2/test_wafv2.py
@@ -41,6 +41,38 @@ def test_create_web_acl(region, partition):
 @mock_aws
 def test_create_web_acl_with_all_arguments():
     client = boto3.client("wafv2", region_name="us-east-2")
+    rule_1 = {
+        "Action": {"Allow": {}},
+        "Name": "tf-acc-test-8205974093017792151-2",
+        "Priority": 10,
+        "Statement": {"GeoMatchStatement": {"CountryCodes": ["US", "NL"]}},
+        "VisibilityConfig": {
+            "CloudWatchMetricsEnabled": False,
+            "MetricName": "tf-acc-test-8205974093017792151-2",
+            "SampledRequestsEnabled": False,
+        },
+    }
+    rule_2 = {
+        "Action": {"Count": {}},
+        "Name": "tf-acc-test-8205974093017792151-1",
+        "Priority": 5,
+        "Statement": {
+            "SizeConstraintStatement": {
+                "ComparisonOperator": "LT",
+                "FieldToMatch": {"QueryString": {}},
+                "Size": 50,
+                "TextTransformations": [
+                    {"Priority": 2, "Type": "CMD_LINE"},
+                    {"Priority": 5, "Type": "NONE"},
+                ],
+            }
+        },
+        "VisibilityConfig": {
+            "CloudWatchMetricsEnabled": False,
+            "MetricName": "tf-acc-test-8205974093017792151-1",
+            "SampledRequestsEnabled": False,
+        },
+    }
     web_acl_id = client.create_web_acl(
         Name="test",
         Scope="CLOUDFRONT",
@@ -51,39 +83,35 @@ def test_create_web_acl_with_all_arguments():
             "CloudWatchMetricsEnabled": False,
             "MetricName": "idk",
         },
-        Rules=[
-            {
-                "Action": {"Allow": {}},
-                "Name": "tf-acc-test-8205974093017792151-2",
-                "Priority": 10,
-                "Statement": {"GeoMatchStatement": {"CountryCodes": ["US", "NL"]}},
-                "VisibilityConfig": {
-                    "CloudWatchMetricsEnabled": False,
-                    "MetricName": "tf-acc-test-8205974093017792151-2",
-                    "SampledRequestsEnabled": False,
+        AssociationConfig={
+            "RequestBody": {
+                "Test": {
+                    "DefaultSizeInspectionLimit": "KB_64",
                 },
             },
-            {
-                "Action": {"Count": {}},
-                "Name": "tf-acc-test-8205974093017792151-1",
-                "Priority": 5,
-                "Statement": {
-                    "SizeConstraintStatement": {
-                        "ComparisonOperator": "LT",
-                        "FieldToMatch": {"QueryString": {}},
-                        "Size": 50,
-                        "TextTransformations": [
-                            {"Priority": 2, "Type": "CMD_LINE"},
-                            {"Priority": 5, "Type": "NONE"},
-                        ],
-                    }
-                },
-                "VisibilityConfig": {
-                    "CloudWatchMetricsEnabled": False,
-                    "MetricName": "tf-acc-test-8205974093017792151-1",
-                    "SampledRequestsEnabled": False,
-                },
+        },
+        CustomResponseBodies={
+            "Test": {
+                "Content": "test",
+                "ContentType": "TEXT_PLAIN",
             },
+        },
+        CaptchaConfig={
+            "ImmunityTimeProperty": {"ImmunityTime": 60},
+        },
+        ChallengeConfig={
+            "ImmunityTimeProperty": {"ImmunityTime": 60},
+        },
+        TokenDomains=[
+            "test1.com",
+            "test2.com",
+        ],
+        Tags=[
+            {"Key": "TestKey", "Value": "TestValue"},
+        ],
+        Rules=[
+            rule_1,
+            rule_2,
         ],
     )["Summary"]["Id"]
 
@@ -96,6 +124,27 @@ def test_create_web_acl_with_all_arguments():
         "MetricName": "idk",
     }
     assert len(wacl["Rules"]) == 2
+    assert rule_1 in wacl["Rules"] and rule_2 in wacl["Rules"]
+    assert wacl["AssociationConfig"] == {
+        "RequestBody": {
+            "Test": {
+                "DefaultSizeInspectionLimit": "KB_64",
+            },
+        },
+    }
+    assert wacl["CustomResponseBodies"] == {
+        "Test": {
+            "Content": "test",
+            "ContentType": "TEXT_PLAIN",
+        },
+    }
+    assert wacl["CaptchaConfig"] == {
+        "ImmunityTimeProperty": {"ImmunityTime": 60},
+    }
+    assert wacl["ChallengeConfig"] == {
+        "ImmunityTimeProperty": {"ImmunityTime": 60},
+    }
+    assert wacl["TokenDomains"] == ["test1.com", "test2.com"]
 
 
 @mock_aws
@@ -106,6 +155,7 @@ def test_get_web_acl():
     wacl = conn.get_web_acl(Name="John", Scope="REGIONAL", Id=web_acl_id)["WebACL"]
     assert wacl["Name"] == "John"
     assert wacl["Id"] == web_acl_id
+    assert wacl["LabelNamespace"] == f"awswaf:{ACCOUNT_ID}:webacl:John:"
 
 
 @mock_aws
@@ -129,17 +179,23 @@ def test_list_web_acl():
 @mock_aws
 def test_delete_web_acl():
     conn = boto3.client("wafv2", region_name="us-east-1")
-    wacl_id = conn.create_web_acl(**CREATE_WEB_ACL_BODY("Daphne", "REGIONAL"))[
-        "Summary"
-    ]["Id"]
+    wacl = conn.create_web_acl(**CREATE_WEB_ACL_BODY("Daphne", "REGIONAL"))["Summary"]
 
-    conn.delete_web_acl(Name="Daphne", Id=wacl_id, Scope="REGIONAL", LockToken="n/a")
+    with pytest.raises(ClientError) as e:
+        conn.delete_web_acl(
+            Name="Daphne", Id=wacl["Id"], Scope="REGIONAL", LockToken="123"
+        )
+    assert e.value.response["Error"]["Code"] == "WAFOptimisticLockException"
+
+    conn.delete_web_acl(
+        Name="Daphne", Id=wacl["Id"], Scope="REGIONAL", LockToken=wacl["LockToken"]
+    )
 
     res = conn.list_web_acls(**LIST_WEB_ACL_BODY("REGIONAL"))
     assert len(res["WebACLs"]) == 0
 
     with pytest.raises(ClientError) as exc:
-        conn.get_web_acl(Name="Daphne", Scope="REGIONAL", Id=wacl_id)
+        conn.get_web_acl(Name="Daphne", Scope="REGIONAL", Id=wacl["Id"])
     err = exc.value.response["Error"]
     assert err["Code"] == "WAFNonexistentItemException"
     assert err["Message"] == (
@@ -150,14 +206,12 @@ def test_delete_web_acl():
 @mock_aws
 def test_update_web_acl():
     conn = boto3.client("wafv2", region_name="us-east-1")
-    wacl_id = conn.create_web_acl(**CREATE_WEB_ACL_BODY("Daphne", "REGIONAL"))[
-        "Summary"
-    ]["Id"]
+    wacl = conn.create_web_acl(**CREATE_WEB_ACL_BODY("Daphne", "REGIONAL"))["Summary"]
 
     resp = conn.update_web_acl(
         Name="Daphne",
         Scope="REGIONAL",
-        Id=wacl_id,
+        Id=wacl["Id"],
         DefaultAction={"Block": {"CustomResponse": {"ResponseCode": 412}}},
         Description="updated_desc",
         Rules=[
@@ -172,7 +226,7 @@ def test_update_web_acl():
                 },
             }
         ],
-        LockToken="n/a",
+        LockToken=wacl["LockToken"],
         VisibilityConfig={
             "SampledRequestsEnabled": True,
             "CloudWatchMetricsEnabled": True,
@@ -181,7 +235,22 @@ def test_update_web_acl():
     )
     assert "NextLockToken" in resp
 
-    acl = conn.get_web_acl(Name="Daphne", Scope="REGIONAL", Id=wacl_id)["WebACL"]
+    with pytest.raises(ClientError) as e:
+        conn.update_web_acl(
+            Name="Daphne",
+            Scope="REGIONAL",
+            Id=wacl["Id"],
+            DefaultAction={"Block": {"CustomResponse": {"ResponseCode": 412}}},
+            LockToken="123",
+            VisibilityConfig={
+                "SampledRequestsEnabled": True,
+                "CloudWatchMetricsEnabled": True,
+                "MetricName": "updated",
+            },
+        )
+    assert e.value.response["Error"]["Code"] == "WAFOptimisticLockException"
+
+    acl = conn.get_web_acl(Name="Daphne", Scope="REGIONAL", Id=wacl["Id"])["WebACL"]
     assert acl["Description"] == "updated_desc"
     assert acl["DefaultAction"] == {"Block": {"CustomResponse": {"ResponseCode": 412}}}
     assert acl["Rules"] == [
@@ -347,4 +416,4 @@ def test_logging_configuration_crud():
         wafv2_client.get_logging_configuration(
             ResourceArn=logging_configuration["ResourceArn"]
         )
-    e.value.response["Error"]["Code"] == "WAFNonexistentItemException"
+    assert e.value.response["Error"]["Code"] == "WAFNonexistentItemException"
diff --git a/tests/test_wafv2/test_wafv2_rules.py b/tests/test_wafv2/test_wafv2_rules.py
index 43e764d78..1d4f7fba2 100644
--- a/tests/test_wafv2/test_wafv2_rules.py
+++ b/tests/test_wafv2/test_wafv2_rules.py
@@ -1,10 +1,358 @@
 import boto3
+import pytest
+from botocore.exceptions import ClientError
 
 from moto import mock_aws
+from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
+from moto.utilities.utils import is_valid_uuid
+
+
+@mock_aws
+def test_create_rule_group():
+    client = boto3.client("wafv2", region_name="ap-southeast-1")
+    resp = client.create_rule_group(
+        Capacity=100,
+        Name="test-group",
+        Scope="REGIONAL",
+        VisibilityConfig={
+            "SampledRequestsEnabled": False,
+            "CloudWatchMetricsEnabled": False,
+            "MetricName": "test-group",
+        },
+    )["Summary"]
+    assert resp["ARN"].startswith(
+        f"arn:aws:wafv2:ap-southeast-1:{ACCOUNT_ID}:regional/rulegroup/test-group/"
+    )
+    assert resp["Name"] == "test-group"
+    assert resp["Description"] == ""
+    assert is_valid_uuid(resp["Id"])
+    assert is_valid_uuid(resp["LockToken"])
+    assert "VisibilityConfig" not in resp
+    assert "Rules" not in resp
+
+    with pytest.raises(ClientError) as e:
+        client.create_rule_group(
+            Capacity=100,
+            Name="test-group",
+            Scope="REGIONAL",
+            VisibilityConfig={
+                "SampledRequestsEnabled": False,
+                "CloudWatchMetricsEnabled": False,
+                "MetricName": "test-group",
+            },
+        )
+    assert e.value.response["Error"]["Code"] == "WafV2DuplicateItem"
+
+
+@mock_aws
+def test_update_rule_group():
+    client = boto3.client("wafv2", region_name="ap-southeast-1")
+    group = client.create_rule_group(
+        Capacity=100,
+        Name="test-group",
+        Scope="REGIONAL",
+        VisibilityConfig={
+            "SampledRequestsEnabled": False,
+            "CloudWatchMetricsEnabled": False,
+            "MetricName": "test-group",
+        },
+    )["Summary"]
+
+    test_rule = {
+        "Name": "test-rule",
+        "Priority": 0,
+        "Statement": {
+            "LabelMatchStatement": {
+                "Scope": "LABEL",
+                "Key": "testlabelmatch",
+            }
+        },
+        "Action": {
+            "Allow": {},
+        },
+        "VisibilityConfig": {
+            "SampledRequestsEnabled": True,
+            "CloudWatchMetricsEnabled": True,
+            "MetricName": "test-rule",
+        },
+        "CaptchaConfig": {
+            "ImmunityTimeProperty": {
+                "ImmunityTime": 60,
+            }
+        },
+        "ChallengeConfig": {
+            "ImmunityTimeProperty": {
+                "ImmunityTime": 60,
+            }
+        },
+        "OverrideAction": {
+            "Count": {},
+            "None": {},
+        },
+        "RuleLabels": [
+            {
+                "Name": "testlabel",
+            },
+        ],
+    }
+
+    resp = client.update_rule_group(
+        Name="test-group",
+        Scope="REGIONAL",
+        Id=group["Id"],
+        Rules=[
+            test_rule,
+        ],
+        LockToken=group["LockToken"],
+        VisibilityConfig={
+            "SampledRequestsEnabled": True,
+            "CloudWatchMetricsEnabled": True,
+            "MetricName": "new-metric",
+        },
+        Description="test",
+        CustomResponseBodies={
+            "Test": {
+                "Content": "ohmylookatthiscontent",
+                "ContentType": "TEXT_PLAIN",
+            },
+        },
+    )
+    assert (
+        is_valid_uuid(resp["NextLockToken"])
+        and resp["NextLockToken"] != group["LockToken"]
+    )
+    updated_group = client.get_rule_group(ARN=group["ARN"])["RuleGroup"]
+    assert updated_group["VisibilityConfig"] == {
+        "SampledRequestsEnabled": True,
+        "CloudWatchMetricsEnabled": True,
+        "MetricName": "new-metric",
+    }
+    assert updated_group["Description"] == "test"
+    assert updated_group["CustomResponseBodies"] == {
+        "Test": {
+            "Content": "ohmylookatthiscontent",
+            "ContentType": "TEXT_PLAIN",
+        },
+    }
+    assert updated_group["Rules"][0] == test_rule
+
+    with pytest.raises(ClientError) as e:
+        client.update_rule_group(
+            Name="test-group",
+            Scope="REGIONAL",
+            Id=group["Id"],
+            LockToken="123",
+            VisibilityConfig={
+                "SampledRequestsEnabled": False,
+                "CloudWatchMetricsEnabled": False,
+                "MetricName": "new-metric",
+            },
+        )
+    assert e.value.response["Error"]["Code"] == "WAFOptimisticLockException"
+
+    with pytest.raises(ClientError) as e:
+        client.update_rule_group(
+            Name="bad-group",
+            Scope="REGIONAL",
+            Id=group["Id"],
+            LockToken=group["LockToken"],
+            VisibilityConfig={
+                "SampledRequestsEnabled": False,
+                "CloudWatchMetricsEnabled": False,
+                "MetricName": "new-metric",
+            },
+        )
+    assert e.value.response["Error"]["Code"] == "WAFNonexistentItemException"
+
+
+@mock_aws
+def test_delete_rule_group():
+    client = boto3.client("wafv2", region_name="eu-west-1")
+    group = client.create_rule_group(
+        Capacity=100,
+        Name="test-group-1",
+        Scope="REGIONAL",
+        VisibilityConfig={
+            "SampledRequestsEnabled": False,
+            "CloudWatchMetricsEnabled": False,
+            "MetricName": "test-group-1",
+        },
+    )["Summary"]
+    with pytest.raises(ClientError) as e:
+        client.delete_rule_group(
+            Name="test-group-1",
+            Scope="CLOUDFRONT",
+            Id=group["Id"],
+            LockToken=group["LockToken"],
+        )
+    assert e.value.response["Error"]["Code"] == "WAFNonexistentItemException"
+
+    with pytest.raises(ClientError) as e:
+        client.delete_rule_group(
+            Name="test-group-1",
+            Scope="REGIONAL",
+            Id=group["Id"],
+            LockToken="1234567890",
+        )
+    assert e.value.response["Error"]["Code"] == "WAFOptimisticLockException"
+
+    assert len(client.list_rule_groups(Scope="REGIONAL")["RuleGroups"]) == 1
+    client.delete_rule_group(
+        Name="test-group-1",
+        Scope="REGIONAL",
+        Id=group["Id"],
+        LockToken=group["LockToken"],
+    )
+    assert len(client.list_rule_groups(Scope="REGIONAL")["RuleGroups"]) == 0
+
+
+@mock_aws
+def test_get_rule_group():
+    client = boto3.client("wafv2", region_name="us-east-2")
+    test_rule = {
+        "Name": "test-rule",
+        "Priority": 0,
+        "Statement": {
+            "LabelMatchStatement": {
+                "Scope": "LABEL",
+                "Key": "testlabelmatch",
+            }
+        },
+        "Action": {
+            "Allow": {},
+        },
+        "VisibilityConfig": {
+            "SampledRequestsEnabled": True,
+            "CloudWatchMetricsEnabled": True,
+            "MetricName": "test-rule",
+        },
+        "CaptchaConfig": {
+            "ImmunityTimeProperty": {
+                "ImmunityTime": 60,
+            }
+        },
+        "ChallengeConfig": {
+            "ImmunityTimeProperty": {
+                "ImmunityTime": 60,
+            }
+        },
+        "OverrideAction": {
+            "Count": {},
+            "None": {},
+        },
+        "RuleLabels": [
+            {
+                "Name": "testlabel",
+            },
+        ],
+    }
+
+    new_group = client.create_rule_group(
+        Capacity=100,
+        Name="test-group-all-params",
+        Description="test",
+        Scope="CLOUDFRONT",
+        VisibilityConfig={
+            "SampledRequestsEnabled": True,
+            "CloudWatchMetricsEnabled": True,
+            "MetricName": "test-group",
+        },
+        Rules=[
+            test_rule,
+        ],
+        CustomResponseBodies={
+            "Test": {
+                "Content": "ohmylookatthiscontent",
+                "ContentType": "TEXT_PLAIN",
+            },
+        },
+        Tags=[
+            {
+                "Key": "test-key",
+                "Value": "test-value",
+            },
+        ],
+    )["Summary"]
+    resp = client.get_rule_group(
+        Id=new_group["Id"], Scope="CLOUDFRONT", Name="test-group-all-params"
+    )
+    group = resp["RuleGroup"]
+    assert is_valid_uuid(resp["LockToken"])
+    assert is_valid_uuid(group["Id"])
+    assert group["ARN"] == new_group["ARN"]
+    assert group["Capacity"] == 100
+    assert group["CustomResponseBodies"] == {
+        "Test": {
+            "Content": "ohmylookatthiscontent",
+            "ContentType": "TEXT_PLAIN",
+        },
+    }
+    assert group["Description"] == "test"
+    assert group["Id"] == new_group["Id"]
+    assert group["Name"] == "test-group-all-params"
+    assert (
+        group["LabelNamespace"] == f"awswaf:{ACCOUNT_ID}:rulegroup:{new_group['Name']}:"
+    )
+    assert group["AvailableLabels"] == [{"Name": f"{group['LabelNamespace']}testlabel"}]
+    assert group["ConsumedLabels"] == [
+        {"Name": f"{group['LabelNamespace']}testlabelmatch"}
+    ]
+    assert group["Name"] == "test-group-all-params"
+    assert group["Rules"][0] == test_rule
+    assert group["VisibilityConfig"] == {
+        "SampledRequestsEnabled": True,
+        "CloudWatchMetricsEnabled": True,
+        "MetricName": "test-group",
+    }
+
+    with pytest.raises(ClientError) as e:
+        client.get_rule_group(
+            ARN=f"arn:aws:wafv2:ap-southeast-1:{ACCOUNT_ID}:regional/rulegroup/test-group/1234567890)"
+        )
+    assert e.value.response["Error"]["Code"] == "WAFNonexistentItemException"
+
+    with pytest.raises(ClientError) as e:
+        client.get_rule_group(Name="test-group")
+    assert e.value.response["Error"]["Code"] == "AcessDeniedException"
+    assert (
+        e.value.response["Error"]["Message"]
+        == "Critical information is missing in your request: GetRuleGroupRequest(name=test-group, scope=null, id=null, aRN=null)."
+    )
 
 
 @mock_aws
 def test_list_rule_groups():
     client = boto3.client("wafv2", region_name="us-east-2")
-    resp = client.list_rule_groups(Scope="CLOUDFRONT")
-    assert resp["RuleGroups"] == []
+    assert len(client.list_rule_groups(Scope="REGIONAL")["RuleGroups"]) == 0
+    regional_group = client.create_rule_group(
+        Capacity=100,
+        Name="test-group-1",
+        Scope="REGIONAL",
+        VisibilityConfig={
+            "SampledRequestsEnabled": False,
+            "CloudWatchMetricsEnabled": False,
+            "MetricName": "test-group-1",
+        },
+    )["Summary"]
+    cf_group = client.create_rule_group(
+        Capacity=100,
+        Name="test-group-2",
+        Scope="CLOUDFRONT",
+        VisibilityConfig={
+            "SampledRequestsEnabled": True,
+            "CloudWatchMetricsEnabled": True,
+            "MetricName": "test-group-2",
+        },
+    )["Summary"]
+
+    reg_groups = client.list_rule_groups(Scope="REGIONAL")["RuleGroups"]
+    cf_groups = client.list_rule_groups(Scope="CLOUDFRONT")["RuleGroups"]
+    assert len(reg_groups) == len(cf_groups) == 1
+    regional = reg_groups[0]
+    assert regional["ARN"] == regional_group["ARN"]
+    assert cf_groups[0]["ARN"] == cf_group["ARN"]
+    assert regional["Name"] == "test-group-1"
+    assert "Scope" not in regional
+    assert is_valid_uuid(regional["LockToken"])
+    assert is_valid_uuid(regional["Id"]) and regional["Id"] == regional_group["Id"]
+    assert "VisibilityConfig" not in regional