Repository: getmoto/moto
Pull request #8211 (2024-10-07 08:36:35)

QuickSight finalize user and group handling
__Enhancements:__
* Added all missing user handling functions
* Added all missing group handling functions
* Added pagination support for all user and group list functions
* Added search support for groups

__Fixes:__
* Use function to get URL path parameters


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/8211?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
Attention: Patch coverage is `96.83258%` with `7 lines` in your changes missing coverage. Please review.
> Project coverage is 94.54%. Comparing base [(`43410b9`)](https://app.codecov.io/gh/getmoto/moto/commit/43410b987e1a5a172a7d429d68f5c94105154476?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) to head [(`aee59ba`)](https://app.codecov.io/gh/getmoto/moto/commit/aee59bab9f20275157fcfd387b680f08a496e7d0?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).
> Report is 2 commits behind head on master.

| [Files with missing lines](https://app.codecov.io/gh/getmoto/moto/pull/8211?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Patch % | Lines |
|---|---|---|
| [moto/quicksight/utils.py](https://app.codecov.io/gh/getmoto/moto/pull/8211?src=pr&el=tree&filepath=moto%2Fquicksight%2Futils.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#diff-bW90by9xdWlja3NpZ2h0L3V0aWxzLnB5) | 91.30% | [4 Missing :warning: ](https://app.codecov.io/gh/getmoto/moto/pull/8211?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) |
| [moto/quicksight/exceptions.py](https://app.codecov.io/gh/getmoto/moto/pull/8211?src=pr&el=tree&filepath=moto%2Fquicksight%2Fexceptions.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#diff-bW90by9xdWlja3NpZ2h0L2V4Y2VwdGlvbnMucHk=) | 75.00% | [3 Missing :warning: ](https://app.codecov.io/gh/getmoto/moto/pull/8211?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) |

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


```diff
@@           Coverage Diff            @@
##           master    #8211    +/-   ##
========================================
  Coverage   94.53%   94.54%            
========================================
  Files        1155     1157     +2     
  Lines       99707    99905   +198     
========================================
+ Hits        94261    94452   +191     
- Misses       5446     5453     +7     
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/8211/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Coverage Δ | |
|---|---|---|
| [servertests](https://app.codecov.io/gh/getmoto/moto/pull/8211/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `28.81% <0.00%> (-0.04%)` | :arrow_down: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/8211/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `94.51% <96.83%> (+<0.01%)` | :arrow_up: |

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

</details>

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


Patch:
diff --git a/moto/quicksight/data_models.py b/moto/quicksight/data_models.py
new file mode 100644
index 000000000..d7a2cde81
--- /dev/null
+++ b/moto/quicksight/data_models.py
@@ -0,0 +1,120 @@
+from typing import Any, Dict, List, Union
+
+from moto.core.common_models import BaseModel
+from moto.moto_api._internal import mock_random as random
+from moto.utilities.utils import get_partition
+
+
+class QuicksightDataSet(BaseModel):
+    def __init__(self, account_id: str, region: str, _id: str, name: str):
+        self.arn = f"arn:{get_partition(region)}:quicksight:{region}:{account_id}:data-set/{_id}"
+        self._id = _id
+        self.name = name
+        self.region = region
+        self.account_id = account_id
+
+    def to_json(self) -> Dict[str, Any]:
+        return {
+            "Arn": self.arn,
+            "DataSetId": self._id,
+            "IngestionArn": f"arn:{get_partition(self.region)}:quicksight:{self.region}:{self.account_id}:ingestion/tbd",
+        }
+
+
+class QuicksightIngestion(BaseModel):
+    def __init__(
+        self, account_id: str, region: str, data_set_id: str, ingestion_id: str
+    ):
+        self.arn = f"arn:{get_partition(region)}:quicksight:{region}:{account_id}:data-set/{data_set_id}/ingestions/{ingestion_id}"
+        self.ingestion_id = ingestion_id
+
+    def to_json(self) -> Dict[str, Any]:
+        return {
+            "Arn": self.arn,
+            "IngestionId": self.ingestion_id,
+            "IngestionStatus": "INITIALIZED",
+        }
+
+
+class QuicksightMembership(BaseModel):
+    def __init__(self, account_id: str, region: str, group: str, user: str):
+        self.group = group
+        self.user = user
+        self.arn = f"arn:{get_partition(region)}:quicksight:{region}:{account_id}:group/default/{group}/{user}"
+
+    def to_json(self) -> Dict[str, str]:
+        return {"Arn": self.arn, "MemberName": self.user}
+
+
+class QuicksightGroup(BaseModel):
+    def __init__(
+        self,
+        region: str,
+        group_name: str,
+        description: str,
+        aws_account_id: str,
+        namespace: str,
+    ):
+        self.arn = f"arn:{get_partition(region)}:quicksight:{region}:{aws_account_id}:group/default/{group_name}"
+        self.group_name = group_name
+        self.description = description
+        self.aws_account_id = aws_account_id
+        self.namespace = namespace
+        self.region = region
+
+        self.members: Dict[str, QuicksightMembership] = dict()
+
+    def add_member(self, member_name: str) -> QuicksightMembership:
+        membership = QuicksightMembership(
+            self.aws_account_id, self.region, self.group_name, member_name
+        )
+        self.members[member_name] = membership
+        return membership
+
+    def delete_member(self, user_name: str) -> None:
+        self.members.pop(user_name, None)
+
+    def get_member(self, user_name: str) -> Union[QuicksightMembership, None]:
+        return self.members.get(user_name, None)
+
+    def list_members(self) -> List[QuicksightMembership]:
+        return list(self.members.values())
+
+    def to_json(self) -> Dict[str, Any]:
+        return {
+            "Arn": self.arn,
+            "GroupName": self.group_name,
+            "Description": self.description,
+            "PrincipalId": self.aws_account_id,
+            "Namespace": self.namespace,
+        }
+
+
+class QuicksightUser(BaseModel):
+    def __init__(
+        self,
+        account_id: str,
+        region: str,
+        email: str,
+        identity_type: str,
+        username: str,
+        user_role: str,
+    ):
+        self.arn = f"arn:{get_partition(region)}:quicksight:{region}:{account_id}:user/default/{username}"
+        self.email = email
+        self.identity_type = identity_type
+        self.username = username
+        self.user_role = user_role
+        self.active = False
+        self.principal_id = random.get_random_hex(10)
+
+    def to_json(self) -> Dict[str, Any]:
+        return {
+            "Arn": self.arn,
+            "Email": self.email,
+            "IdentityType": self.identity_type,
+            "Role": self.user_role,
+            "UserName": self.username,
+            "Active": self.active,
+            "PrincipalId": self.principal_id,
+        }
diff --git a/moto/quicksight/exceptions.py b/moto/quicksight/exceptions.py
index b9b571e46..08e6d3210 100644
--- a/moto/quicksight/exceptions.py
+++ b/moto/quicksight/exceptions.py
@@ -6,3 +6,23 @@ from moto.core.exceptions import JsonRESTError
 class ResourceNotFoundException(JsonRESTError):
     def __init__(self, msg: str):
         super().__init__("ResourceNotFoundException", msg)
+
+
+class InvalidParameterValueException(JsonRESTError):
+    def __init__(self, msg: str):
+        super().__init__("InvalidParameterValueException", msg)
+
+
+class ValidationException(JsonRESTError):
+    def __init__(self, msg: str):
+        super().__init__("ValidationException", msg)
+
+
+class SchemaException(JsonRESTError):
+    def __init__(self, msg: str):
+        super().__init__("SchemaException", msg)
+
+
+class ParamValidationError(JsonRESTError):
+    def __init__(self, msg: str):
+        super().__init__("ParamValidationError", msg)
diff --git a/moto/quicksight/models.py b/moto/quicksight/models.py
index e7f0f8b98..b5f5f3a95 100644
--- a/moto/quicksight/models.py
+++ b/moto/quicksight/models.py
@@ -1,132 +1,23 @@
-from typing import Any, Dict, Iterable
+from typing import Dict, List
 
 from moto.core.base_backend import BackendDict, BaseBackend
-from moto.core.common_models import BaseModel
-from moto.moto_api._internal import mock_random as random
-from moto.utilities.utils import get_partition
-
+from moto.utilities.paginator import paginate
+
+from .data_models import (
+    QuicksightDataSet,
+    QuicksightGroup,
+    QuicksightIngestion,
+    QuicksightMembership,
+    QuicksightUser,
+)
 from .exceptions import ResourceNotFoundException
+from .utils import PAGINATION_MODEL, QuicksightSearchFilterFactory
 
 
 def _create_id(aws_account_id: str, namespace: str, _id: str) -> str:
     return f"{aws_account_id}:{namespace}:{_id}"
 
 
-class QuicksightDataSet(BaseModel):
-    def __init__(self, account_id: str, region: str, _id: str, name: str):
-        self.arn = f"arn:{get_partition(region)}:quicksight:{region}:{account_id}:data-set/{_id}"
-        self._id = _id
-        self.name = name
-        self.region = region
-        self.account_id = account_id
-
-    def to_json(self) -> Dict[str, Any]:
-        return {
-            "Arn": self.arn,
-            "DataSetId": self._id,
-            "IngestionArn": f"arn:{get_partition(self.region)}:quicksight:{self.region}:{self.account_id}:ingestion/tbd",
-        }
-
-
-class QuicksightIngestion(BaseModel):
-    def __init__(
-        self, account_id: str, region: str, data_set_id: str, ingestion_id: str
-    ):
-        self.arn = f"arn:{get_partition(region)}:quicksight:{region}:{account_id}:data-set/{data_set_id}/ingestions/{ingestion_id}"
-        self.ingestion_id = ingestion_id
-
-    def to_json(self) -> Dict[str, Any]:
-        return {
-            "Arn": self.arn,
-            "IngestionId": self.ingestion_id,
-            "IngestionStatus": "INITIALIZED",
-        }
-
-
-class QuicksightMembership(BaseModel):
-    def __init__(self, account_id: str, region: str, group: str, user: str):
-        self.group = group
-        self.user = user
-        self.arn = f"arn:{get_partition(region)}:quicksight:{region}:{account_id}:group/default/{group}/{user}"
-
-    def to_json(self) -> Dict[str, str]:
-        return {"Arn": self.arn, "MemberName": self.user}
-
-
-class QuicksightGroup(BaseModel):
-    def __init__(
-        self,
-        region: str,
-        group_name: str,
-        description: str,
-        aws_account_id: str,
-        namespace: str,
-    ):
-        self.arn = f"arn:{get_partition(region)}:quicksight:{region}:{aws_account_id}:group/default/{group_name}"
-        self.group_name = group_name
-        self.description = description
-        self.aws_account_id = aws_account_id
-        self.namespace = namespace
-        self.region = region
-
-        self.members: Dict[str, QuicksightMembership] = dict()
-
-    def add_member(self, user_name: str) -> QuicksightMembership:
-        membership = QuicksightMembership(
-            self.aws_account_id, self.region, self.group_name, user_name
-        )
-        self.members[user_name] = membership
-        return membership
-
-    def delete_member(self, user_name: str) -> None:
-        self.members.pop(user_name, None)
-
-    def get_member(self, user_name: str) -> QuicksightMembership:
-        return self.members[user_name]
-
-    def list_members(self) -> Iterable[QuicksightMembership]:
-        return self.members.values()
-
-    def to_json(self) -> Dict[str, Any]:
-        return {
-            "Arn": self.arn,
-            "GroupName": self.group_name,
-            "Description": self.description,
-            "PrincipalId": self.aws_account_id,
-            "Namespace": self.namespace,
-        }
-
-
-class QuicksightUser(BaseModel):
-    def __init__(
-        self,
-        account_id: str,
-        region: str,
-        email: str,
-        identity_type: str,
-        username: str,
-        user_role: str,
-    ):
-        self.arn = f"arn:{get_partition(region)}:quicksight:{region}:{account_id}:user/default/{username}"
-        self.email = email
-        self.identity_type = identity_type
-        self.username = username
-        self.user_role = user_role
-        self.active = False
-        self.principal_id = random.get_random_hex(10)
-
-    def to_json(self) -> Dict[str, Any]:
-        return {
-            "Arn": self.arn,
-            "Email": self.email,
-            "IdentityType": self.identity_type,
-            "Role": self.user_role,
-            "UserName": self.username,
-            "Active": self.active,
-            "PrincipalId": self.principal_id,
-        }
-
-
 class QuickSightBackend(BaseBackend):
     """Implementation of QuickSight APIs."""
 
@@ -155,10 +46,10 @@ class QuickSightBackend(BaseBackend):
         return group
 
     def create_group_membership(
-        self, aws_account_id: str, namespace: str, group_name: str, user_name: str
+        self, aws_account_id: str, namespace: str, group_name: str, member_name: str
     ) -> QuicksightMembership:
         group = self.describe_group(aws_account_id, namespace, group_name)
-        return group.add_member(user_name)
+        return group.add_member(member_name)
 
     def create_ingestion(
         self, data_set_id: str, ingestion_id: str
@@ -190,10 +81,13 @@ class QuickSightBackend(BaseBackend):
         return self.groups[_id]
 
     def describe_group_membership(
-        self, aws_account_id: str, namespace: str, group_name: str, user_name: str
+        self, aws_account_id: str, namespace: str, group_name: str, member_name: str
     ) -> QuicksightMembership:
         group = self.describe_group(aws_account_id, namespace, group_name)
-        return group.get_member(user_name)
+        member = group.get_member(member_name)
+        if member is None:
+            raise ResourceNotFoundException(f"Member {member_name} not found")
+        return member
 
     def describe_user(
         self, aws_account_id: str, namespace: str, user_name: str
@@ -203,35 +97,51 @@ class QuickSightBackend(BaseBackend):
             raise ResourceNotFoundException(f"User {user_name} not found")
         return self.users[_id]
 
-    def list_groups(
-        self, aws_account_id: str, namespace: str
-    ) -> Iterable[QuicksightGroup]:
-        """
-        The NextToken and MaxResults parameters are not yet implemented
-        """
+    @paginate(pagination_model=PAGINATION_MODEL)
+    def list_groups(self, aws_account_id: str, namespace: str) -> List[QuicksightGroup]:
         id_for_ns = _create_id(aws_account_id, namespace, _id="")
         return [
             group for _id, group in self.groups.items() if _id.startswith(id_for_ns)
         ]
 
+    @paginate(pagination_model=PAGINATION_MODEL)
+    def search_groups(
+        self, aws_account_id: str, namespace: str, filters: List[Dict[str, str]]
+    ) -> List[QuicksightGroup]:
+        id_for_ns = _create_id(aws_account_id, namespace, _id="")
+        filter_list = QuicksightSearchFilterFactory.validate_and_create_filter(
+            model_type=QuicksightGroup, input=filters
+        )
+        return [
+            group
+            for _id, group in self.groups.items()
+            if _id.startswith(id_for_ns) and filter_list.match(group)
+        ]
+
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_group_memberships(
         self, aws_account_id: str, namespace: str, group_name: str
-    ) -> Iterable[QuicksightMembership]:
-        """
-        The NextToken and MaxResults parameters are not yet implemented
-        """
+    ) -> List[QuicksightMembership]:
         group = self.describe_group(aws_account_id, namespace, group_name)
         return group.list_members()
 
-    def list_users(
-        self, aws_account_id: str, namespace: str
-    ) -> Iterable[QuicksightUser]:
-        """
-        The NextToken and MaxResults parameters are not yet implemented
-        """
+    @paginate(pagination_model=PAGINATION_MODEL)
+    def list_users(self, aws_account_id: str, namespace: str) -> List[QuicksightUser]:
         id_for_ns = _create_id(aws_account_id, namespace, _id="")
         return [user for _id, user in self.users.items() if _id.startswith(id_for_ns)]
 
+    @paginate(pagination_model=PAGINATION_MODEL)
+    def list_user_groups(
+        self, aws_account_id: str, namespace: str, user_name: str
+    ) -> List[QuicksightGroup]:
+        id_for_ns = _create_id(aws_account_id, namespace, _id="")
+        group_list: Dict[str, QuicksightGroup] = {}
+        # Loop through all groups and check if the user is member.
+        for id, group in self.groups.items():
+            if group.get_member(user_name):
+                group_list[id] = group
+        return [group for _id, group in group_list.items() if _id.startswith(id_for_ns)]
+
     def register_user(
         self,
         identity_type: str,
@@ -246,7 +156,7 @@ class QuickSightBackend(BaseBackend):
         IamArn, SessionName, CustomsPermissionsName, ExternalLoginFederationProviderType, CustomFederationProviderUrl, ExternalLoginId
         """
         user = QuicksightUser(
-            account_id=self.account_id,
+            account_id=aws_account_id,
             region=self.region_name,
             email=email,
             identity_type=identity_type,
@@ -257,6 +167,19 @@ class QuickSightBackend(BaseBackend):
         self.users[_id] = user
         return user
 
+    def update_user(
+        self,
+        aws_account_id: str,
+        namespace: str,
+        user_name: str,
+        email: str,
+        user_role: str,
+    ) -> QuicksightUser:
+        user = self.describe_user(aws_account_id, namespace, user_name)
+        user.email = email
+        user.user_role = user_role
+        return user
+
     def update_group(
         self, aws_account_id: str, namespace: str, group_name: str, description: str
     ) -> QuicksightGroup:
diff --git a/moto/quicksight/responses.py b/moto/quicksight/responses.py
index 9d7caf36c..e7e1ade76 100644
--- a/moto/quicksight/responses.py
+++ b/moto/quicksight/responses.py
@@ -29,8 +29,8 @@ class QuickSightResponse(BaseResponse):
         params = json.loads(self.body)
         group_name = params.get("GroupName")
         description = params.get("Description")
-        aws_account_id = self.path.split("/")[-4]
-        namespace = self.path.split("/")[-2]
+        aws_account_id = self._get_param("AwsAccountId")
+        namespace = self._get_param("Namespace")
         group = self.quicksight_backend.create_group(
             group_name=group_name,
             description=description,
@@ -40,59 +40,96 @@ class QuickSightResponse(BaseResponse):
         return json.dumps(dict(Group=group.to_json()))
 
     def create_group_membership(self) -> str:
-        aws_account_id = self.path.split("/")[-7]
-        namespace = self.path.split("/")[-5]
-        group_name = self.path.split("/")[-3]
-        user_name = unquote(self.path.split("/")[-1])
+        aws_account_id = self._get_param("AwsAccountId")
+        namespace = self._get_param("Namespace")
+        group_name = unquote(self._get_param("GroupName"))
+        member_name = unquote(self._get_param("MemberName"))
         member = self.quicksight_backend.create_group_membership(
-            aws_account_id, namespace, group_name, user_name
+            aws_account_id, namespace, group_name, member_name
         )
         return json.dumps({"GroupMember": member.to_json()})
 
     def create_ingestion(self) -> str:
-        data_set_id = self.path.split("/")[-3]
-        ingestion_id = self.path.split("/")[-1]
+        data_set_id = self._get_param("DataSetId")
+        ingestion_id = self._get_param("IngestionId")
         ingestion = self.quicksight_backend.create_ingestion(data_set_id, ingestion_id)
         return json.dumps(ingestion.to_json())
 
     def describe_group_membership(self) -> str:
-        aws_account_id = self.path.split("/")[-7]
-        namespace = self.path.split("/")[-5]
-        group_name = self.path.split("/")[-3]
-        user_name = unquote(self.path.split("/")[-1])
+        aws_account_id = self._get_param("AwsAccountId")
+        namespace = self._get_param("Namespace")
+        group_name = unquote(self._get_param("GroupName"))
+        member_name = unquote(self._get_param("MemberName"))
         member = self.quicksight_backend.describe_group_membership(
-            aws_account_id, namespace, group_name, user_name
+            aws_account_id, namespace, group_name, member_name
         )
         return json.dumps({"GroupMember": member.to_json()})
 
     def list_groups(self) -> str:
-        aws_account_id = self.path.split("/")[-4]
-        namespace = self.path.split("/")[-2]
-        groups = self.quicksight_backend.list_groups(aws_account_id, namespace)
-        return json.dumps(dict(GroupList=[g.to_json() for g in groups]))
+        max_results = self._get_int_param("max-results")
+        next_token = self._get_param("next-token")
+        aws_account_id = self._get_param("AwsAccountId")
+        namespace = self._get_param("Namespace")
+        groups, next_token = self.quicksight_backend.list_groups(
+            aws_account_id, namespace, max_results=max_results, next_token=next_token
+        )
+        return json.dumps(
+            {"NextToken": next_token, "GroupList": [g.to_json() for g in groups]}
+        )
 
     def list_group_memberships(self) -> str:
-        aws_account_id = self.path.split("/")[-6]
-        namespace = self.path.split("/")[-4]
-        group_name = self.path.split("/")[-2]
-        members = self.quicksight_backend.list_group_memberships(
-            aws_account_id, namespace, group_name
+        max_results = self._get_int_param("max-results")
+        next_token = self._get_param("next-token")
+        aws_account_id = self._get_param("AwsAccountId")
+        namespace = self._get_param("Namespace")
+        group_name = unquote(self._get_param("GroupName"))
+        members, next_token = self.quicksight_backend.list_group_memberships(
+            aws_account_id,
+            namespace,
+            group_name,
+            max_results=max_results,
+            next_token=next_token,
+        )
+        return json.dumps(
+            {"NextToken": next_token, "GroupMemberList": [m.to_json() for m in members]}
         )
-        return json.dumps({"GroupMemberList": [m.to_json() for m in members]})
 
     def list_users(self) -> str:
-        aws_account_id = self.path.split("/")[-4]
-        namespace = self.path.split("/")[-2]
-        users = self.quicksight_backend.list_users(aws_account_id, namespace)
-        return json.dumps(dict(UserList=[u.to_json() for u in users]))
+        max_results = self._get_int_param("max-results")
+        next_token = self._get_param("next-token")
+        aws_account_id = self._get_param("AwsAccountId")
+        namespace = self._get_param("Namespace")
+        users, next_token = self.quicksight_backend.list_users(
+            aws_account_id, namespace, max_results=max_results, next_token=next_token
+        )
+        return json.dumps(
+            {"NextToken": next_token, "UserList": [u.to_json() for u in users]}
+        )
+
+    def list_user_groups(self) -> str:
+        max_results = self._get_int_param("max-results")
+        next_token = self._get_param("next-token")
+        aws_account_id = self._get_param("AwsAccountId")
+        namespace = self._get_param("Namespace")
+        user_name = unquote(self._get_param("UserName"))
+        groups, next_token = self.quicksight_backend.list_user_groups(
+            aws_account_id,
+            namespace,
+            user_name,
+            max_results=max_results,
+            next_token=next_token,
+        )
+        return json.dumps(
+            {"NextToken": next_token, "GroupList": [g.to_json() for g in groups]}
+        )
 
     def register_user(self) -> str:
         params = json.loads(self.body)
         identity_type = params.get("IdentityType")
         email = params.get("Email")
         user_role = params.get("UserRole")
-        aws_account_id = self.path.split("/")[-4]
-        namespace = self.path.split("/")[-2]
+        aws_account_id = self._get_param("AwsAccountId")
+        namespace = self._get_param("Namespace")
         user_name = params.get("UserName")
         user = self.quicksight_backend.register_user(
             identity_type=identity_type,
@@ -104,10 +141,23 @@ class QuickSightResponse(BaseResponse):
         )
         return json.dumps(dict(User=user.to_json(), UserInvitationUrl="TBD"))
 
+    def update_user(self) -> str:
+        aws_account_id = self._get_param("AwsAccountId")
+        namespace = self._get_param("Namespace")
+        user_name = unquote(self._get_param("UserName"))
+        body = json.loads(self.body)
+        email = body.get("Email", None)
+        user_role = body.get("Role", None)
+
+        user = self.quicksight_backend.update_user(
+            aws_account_id, namespace, user_name, email, user_role
+        )
+        return json.dumps(dict(User=user.to_json()))
+
     def describe_group(self) -> str:
-        aws_account_id = self.path.split("/")[-5]
-        namespace = self.path.split("/")[-3]
-        group_name = self.path.split("/")[-1]
+        aws_account_id = self._get_param("AwsAccountId")
+        namespace = self._get_param("Namespace")
+        group_name = unquote(self._get_param("GroupName"))
 
         group = self.quicksight_backend.describe_group(
             aws_account_id, namespace, group_name
@@ -115,9 +165,9 @@ class QuickSightResponse(BaseResponse):
         return json.dumps(dict(Group=group.to_json()))
 
     def describe_user(self) -> str:
-        aws_account_id = self.path.split("/")[-5]
-        namespace = self.path.split("/")[-3]
-        user_name = unquote(self.path.split("/")[-1])
+        aws_account_id = self._get_param("AwsAccountId")
+        namespace = self._get_param("Namespace")
+        user_name = unquote(self._get_param("UserName"))
 
         user = self.quicksight_backend.describe_user(
             aws_account_id, namespace, user_name
@@ -125,28 +175,46 @@ class QuickSightResponse(BaseResponse):
         return json.dumps(dict(User=user.to_json()))
 
     def delete_group(self) -> TYPE_RESPONSE:
-        aws_account_id = self.path.split("/")[-5]
-        namespace = self.path.split("/")[-3]
-        group_name = self.path.split("/")[-1]
+        aws_account_id = self._get_param("AwsAccountId")
+        namespace = self._get_param("Namespace")
+        group_name = unquote(self._get_param("GroupName"))
 
         self.quicksight_backend.delete_group(aws_account_id, namespace, group_name)
         return 204, {"status": 204}, json.dumps({"Status": 204})
 
     def delete_user(self) -> TYPE_RESPONSE:
-        aws_account_id = self.path.split("/")[-5]
-        namespace = self.path.split("/")[-3]
-        user_name = unquote(self.path.split("/")[-1])
+        aws_account_id = self._get_param("AwsAccountId")
+        namespace = self._get_param("Namespace")
+        user_name = unquote(self._get_param("UserName"))
 
         self.quicksight_backend.delete_user(aws_account_id, namespace, user_name)
         return 204, {"status": 204}, json.dumps({"Status": 204})
 
     def update_group(self) -> str:
-        aws_account_id = self.path.split("/")[-5]
-        namespace = self.path.split("/")[-3]
-        group_name = self.path.split("/")[-1]
+        aws_account_id = self._get_param("AwsAccountId")
+        namespace = self._get_param("Namespace")
+        group_name = unquote(self._get_param("GroupName"))
         description = json.loads(self.body).get("Description")
 
         group = self.quicksight_backend.update_group(
             aws_account_id, namespace, group_name, description
         )
         return json.dumps(dict(Group=group.to_json()))
+
+    def search_groups(self) -> str:
+        max_results = self._get_int_param("max-results")
+        next_token = self._get_param("next-token")
+        aws_account_id = self._get_param("AwsAccountId")
+        namespace = self._get_param("Namespace")
+        body = json.loads(self.body)
+
+        groups, next_token = self.quicksight_backend.search_groups(
+            aws_account_id,
+            namespace,
+            body.get("Filters", None),
+            max_results=max_results,
+            next_token=next_token,
+        )
+        return json.dumps(
+            {"NextToken": next_token, "GroupList": [g.to_json() for g in groups]}
+        )
diff --git a/moto/quicksight/urls.py b/moto/quicksight/urls.py
index 5b0f037e8..e88f5e6ae 100644
--- a/moto/quicksight/urls.py
+++ b/moto/quicksight/urls.py
@@ -16,4 +16,6 @@ url_paths = {
     r"{0}/accounts/(?P<account_id>[\d]+)/namespaces/(?P<namespace>[a-zA-Z0-9._-]+)/groups/(?P<groupname>[^/]+)/members/(?P<username>[^/]+)$": QuickSightResponse.dispatch,
     r"{0}/accounts/(?P<account_id>[\d]+)/namespaces/(?P<namespace>[a-zA-Z0-9._-]+)/users$": QuickSightResponse.dispatch,
     r"{0}/accounts/(?P<account_id>[\d]+)/namespaces/(?P<namespace>[a-zA-Z0-9._-]+)/users/(?P<username>[^/]+)$": QuickSightResponse.dispatch,
+    r"{0}/accounts/(?P<account_id>[\d]+)/namespaces/(?P<namespace>[a-zA-Z0-9._-]+)/users/(?P<username>[^/]+)/groups$": QuickSightResponse.dispatch,
+    r"{0}/accounts/(?P<account_id>[\d]+)/namespaces/(?P<namespace>[a-zA-Z0-9._-]+)/groups-search$": QuickSightResponse.dispatch,
 }
diff --git a/moto/quicksight/utils.py b/moto/quicksight/utils.py
new file mode 100644
index 000000000..ad23c192b
--- /dev/null
+++ b/moto/quicksight/utils.py
@@ -0,0 +1,141 @@
+from typing import Any, Dict, List, Type, Union
+
+from jsonschema import validate
+from jsonschema.exceptions import SchemaError, ValidationError
+
+from moto.core.common_models import BaseModel
+
+from .data_models import QuicksightGroup
+from .exceptions import ParamValidationError, SchemaException, ValidationException
+
+PAGINATION_MODEL = {
+    "list_users": {
+        "input_token": "next_token",
+        "limit_key": "max_results",
+        "limit_default": 100,  # This should be the sum of the directory limits
+        "unique_attribute": "arn",
+    },
+    "list_user_groups": {
+        "input_token": "next_token",
+        "limit_key": "max_results",
+        "limit_default": 100,  # This should be the sum of the directory limits
+        "unique_attribute": "arn",
+    },
+    "list_groups": {
+        "input_token": "next_token",
+        "limit_key": "max_results",
+        "limit_default": 100,  # This should be the sum of the directory limits
+        "unique_attribute": "arn",
+    },
+    "list_group_memberships": {
+        "input_token": "next_token",
+        "limit_key": "max_results",
+        "limit_default": 100,  # This should be the sum of the directory limits
+        "unique_attribute": "arn",
+    },
+    "search_groups": {
+        "input_token": "next_token",
+        "limit_key": "max_results",
+        "limit_default": 100,  # This should be the sum of the directory limits
+        "unique_attribute": "arn",
+    },
+}
+
+
+class QuicksightBaseSearchFilter(BaseModel):
+    """Base Search Filter."""
+
+    schema: Dict[str, Any] = {
+        "$schema": "https://json-schema.org/draft/2020-12/schema",
+    }
+
+    def __init__(self, operator: str, name: str, value: str):
+        self.operator = operator
+        self.name = name
+        self.value = value
+
+    def match(self, input: BaseModel) -> bool:
+        raise NotImplementedError
+
+
+class QuicksightGroupSearchFilter(QuicksightBaseSearchFilter):
+    """Group Search Filter."""
+
+    schema = {
+        "$schema": "https://json-schema.org/draft/2020-12/schema",
+        "type": "array",
+        "items": {
+            "type": "object",
+            "properties": {
+                "Operator": {"type": "string", "enum": ["StringEquals", "StartsWith"]},
+                "Name": {"type": "string", "enum": ["GROUP_NAME"]},
+                "Value": {"type": "string"},
+            },
+            "required": ["Operator", "Name", "Value"],
+        },
+        "minItems": 1,
+        "maxItems": 1,
+    }
+
+    def __init__(self, operator: str, name: str, value: str):
+        super().__init__(operator, name, value)
+
+    def match(self, input: BaseModel) -> bool:
+        if isinstance(input, QuicksightGroup):
+            if self.name == "GROUP_NAME" and self.operator == "StringEquals":
+                return input.group_name.lower() == self.value.lower()
+            if self.name == "GROUP_NAME" and self.operator == "StartsWith":
+                return input.group_name.lower().startswith(self.value.lower())
+        return False
+
+    @classmethod
+    def parse_filter(cls, filter: Dict[str, str]) -> QuicksightBaseSearchFilter:
+        return QuicksightGroupSearchFilter(
+            operator=filter.get("Operator", ""),
+            name=filter.get("Name", ""),
+            value=filter.get("Value", ""),
+        )
+
+
+class QuicksightSearchFilterList:
+    """Generic QuickSight Search Filter List."""
+
+    def __init__(self, filters: List[QuicksightBaseSearchFilter]):
+        self.filters: List[QuicksightBaseSearchFilter] = filters
+
+    def match(self, input: BaseModel) -> bool:
+        return any([filter.match(input) for filter in self.filters])
+
+
+class QuicksightSearchFilterFactory:
+    """Creates the appropriate search filter."""
+
+    @classmethod
+    def validate_and_create_filter(
+        cls, model_type: Type[BaseModel], input: Union[List[Dict[str, str]], None]
+    ) -> QuicksightSearchFilterList:
+        if issubclass(model_type, QuicksightGroup):
+            if input is None:
+                # Should never happen as there are other validations before but just in case.
+                raise ParamValidationError(
+                    'Missing required parameter in input: "Filters"'
+                )
+            try:
+                validate(instance=input, schema=QuicksightGroupSearchFilter.schema)
+                return QuicksightSearchFilterList(
+                    filters=[
+                        QuicksightGroupSearchFilter.parse_filter(filter)
+                        for filter in input
+                    ]
+                )
+            except ValidationError as err:
+                raise ValidationException(err.message)
+
+            except SchemaError as err:
+                # This exception can happen only, when the schema defined in moto is wrong.
+                # The schema is not presented by the user.
+                raise SchemaException(f"Schema definition wrong in moto: {err.message}")
+
+        raise NotImplementedError(
+            f"Filter for '{model_type.__name__}' not implemented."
+        )


Test patch:
diff --git a/tests/test_quicksight/test_quicksight_groups.py b/tests/test_quicksight/test_quicksight_groups.py
index 802515172..24e2bd455 100644
--- a/tests/test_quicksight/test_quicksight_groups.py
+++ b/tests/test_quicksight/test_quicksight_groups.py
@@ -11,62 +11,103 @@ from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
 # http://docs.getmoto.org/en/latest/docs/contributing/development_tips/tests.html
 
 
+@pytest.mark.parametrize(
+    "request_params",
+    [
+        {
+            "GroupName": "mygroup",
+            "Description": "my new fancy group",
+        },
+        {
+            "GroupName": "users@munich",
+            "Description": "all munich users",
+        },
+    ],
+)
 @mock_aws
-def test_create_group():
+def test_create_group(request_params):
     client = boto3.client("quicksight", region_name="us-west-2")
     resp = client.create_group(
         AwsAccountId=ACCOUNT_ID,
         Namespace="default",
-        GroupName="mygroup",
-        Description="my new fancy group",
+        GroupName=request_params["GroupName"],
+        Description=request_params["Description"],
     )
 
     assert "Group" in resp
 
     assert resp["Group"]["Arn"] == (
-        f"arn:aws:quicksight:us-west-2:{ACCOUNT_ID}:group/default/mygroup"
+        f"arn:aws:quicksight:us-west-2:{ACCOUNT_ID}:group/default/{request_params['GroupName']}"
     )
-    assert resp["Group"]["GroupName"] == "mygroup"
-    assert resp["Group"]["Description"] == "my new fancy group"
+    assert resp["Group"]["GroupName"] == request_params["GroupName"]
+    assert resp["Group"]["Description"] == request_params["Description"]
     assert resp["Group"]["PrincipalId"] == f"{ACCOUNT_ID}"
 
 
+@pytest.mark.parametrize(
+    "request_params",
+    [
+        {
+            "GroupName": "mygroup",
+            "Description": "my new fancy group",
+        },
+        {
+            "GroupName": "users@munich",
+            "Description": "all munich users",
+        },
+    ],
+)
 @mock_aws
-def test_describe_group():
+def test_describe_group(request_params):
     client = boto3.client("quicksight", region_name="us-west-2")
     client.create_group(
         AwsAccountId=ACCOUNT_ID,
         Namespace="default",
-        GroupName="mygroup",
-        Description="my new fancy group",
+        GroupName=request_params["GroupName"],
+        Description=request_params["Description"],
     )
 
     resp = client.describe_group(
-        GroupName="mygroup", AwsAccountId=ACCOUNT_ID, Namespace="default"
+        GroupName=request_params["GroupName"],
+        AwsAccountId=ACCOUNT_ID,
+        Namespace="default",
     )
 
     assert "Group" in resp
 
     assert resp["Group"]["Arn"] == (
-        f"arn:aws:quicksight:us-west-2:{ACCOUNT_ID}:group/default/mygroup"
+        f"arn:aws:quicksight:us-west-2:{ACCOUNT_ID}:group/default/{request_params['GroupName']}"
     )
-    assert resp["Group"]["GroupName"] == "mygroup"
-    assert resp["Group"]["Description"] == "my new fancy group"
+    assert resp["Group"]["GroupName"] == request_params["GroupName"]
+    assert resp["Group"]["Description"] == request_params["Description"]
     assert resp["Group"]["PrincipalId"] == f"{ACCOUNT_ID}"
 
 
+@pytest.mark.parametrize(
+    "request_params",
+    [
+        {
+            "GroupName": "mygroup",
+            "Description": "my new fancy group",
+        },
+        {
+            "GroupName": "users@munich",
+            "Description": "all munich users",
+        },
+    ],
+)
 @mock_aws
-def test_update_group():
+def test_update_group(request_params):
     client = boto3.client("quicksight", region_name="us-west-2")
     client.create_group(
         AwsAccountId=ACCOUNT_ID,
         Namespace="default",
-        GroupName="mygroup",
+        GroupName=request_params["GroupName"],
         Description="desc1",
     )
 
     resp = client.update_group(
-        GroupName="mygroup",
+        GroupName=request_params["GroupName"],
         AwsAccountId=ACCOUNT_ID,
         Namespace="default",
         Description="desc2",
@@ -74,35 +115,54 @@ def test_update_group():
     assert resp["Group"]["Description"] == "desc2"
 
     resp = client.describe_group(
-        GroupName="mygroup", AwsAccountId=ACCOUNT_ID, Namespace="default"
+        GroupName=request_params["GroupName"],
+        AwsAccountId=ACCOUNT_ID,
+        Namespace="default",
     )
 
     assert "Group" in resp
     assert resp["Group"]["Arn"] == (
-        f"arn:aws:quicksight:us-west-2:{ACCOUNT_ID}:group/default/mygroup"
+        f"arn:aws:quicksight:us-west-2:{ACCOUNT_ID}:group/default/{request_params['GroupName']}"
     )
-    assert resp["Group"]["GroupName"] == "mygroup"
+    assert resp["Group"]["GroupName"] == request_params["GroupName"]
     assert resp["Group"]["Description"] == "desc2"
     assert resp["Group"]["PrincipalId"] == f"{ACCOUNT_ID}"
 
 
+@pytest.mark.parametrize(
+    "request_params",
+    [
+        {
+            "GroupName": "mygroup",
+            "Description": "my new fancy group",
+        },
+        {
+            "GroupName": "users@munich",
+            "Description": "all munich users",
+        },
+    ],
+)
 @mock_aws
-def test_delete_group():
+def test_delete_group(request_params):
     client = boto3.client("quicksight", region_name="us-east-2")
     client.create_group(
         AwsAccountId=ACCOUNT_ID,
         Namespace="default",
-        GroupName="mygroup",
-        Description="my new fancy group",
+        GroupName=request_params["GroupName"],
+        Description=request_params["Description"],
     )
 
     client.delete_group(
-        GroupName="mygroup", AwsAccountId=ACCOUNT_ID, Namespace="default"
+        GroupName=request_params["GroupName"],
+        AwsAccountId=ACCOUNT_ID,
+        Namespace="default",
     )
 
     with pytest.raises(ClientError) as exc:
         client.describe_group(
-            GroupName="mygroup", AwsAccountId=ACCOUNT_ID, Namespace="default"
+            GroupName=request_params["GroupName"],
+            AwsAccountId=ACCOUNT_ID,
+            Namespace="default",
         )
     err = exc.value.response["Error"]
     assert err["Code"] == "ResourceNotFoundException"
@@ -121,8 +181,9 @@ def test_list_groups__initial():
 def test_list_groups():
     client = boto3.client("quicksight", region_name="us-east-1")
     for i in range(4):
+        group_name = f"group{i}" if i < 2 else f"group{i}@test"
         client.create_group(
-            AwsAccountId=ACCOUNT_ID, Namespace="default", GroupName=f"group{i}"
+            AwsAccountId=ACCOUNT_ID, Namespace="default", GroupName=group_name
         )
 
     resp = client.list_groups(AwsAccountId=ACCOUNT_ID, Namespace="default")
@@ -137,7 +198,201 @@ def test_list_groups():
     } in resp["GroupList"]
 
     assert {
-        "Arn": f"arn:aws:quicksight:us-east-1:{ACCOUNT_ID}:group/default/group3",
-        "GroupName": "group3",
+        "Arn": f"arn:aws:quicksight:us-east-1:{ACCOUNT_ID}:group/default/group3@test",
+        "GroupName": "group3@test",
         "PrincipalId": ACCOUNT_ID,
     } in resp["GroupList"]
+
+
+@mock_aws
+def test_list_groups__paginated():
+    client = boto3.client("quicksight", region_name="us-east-1")
+    for i in range(125):
+        client.create_group(
+            AwsAccountId=ACCOUNT_ID, Namespace="default", GroupName=f"group{i}"
+        )
+    # default pagesize is 100
+    page1 = client.list_groups(AwsAccountId=ACCOUNT_ID, Namespace="default")
+    assert len(page1["GroupList"]) == 100
+    assert "NextToken" in page1
+
+    # We can ask for a smaller pagesize
+    page2 = client.list_groups(
+        AwsAccountId=ACCOUNT_ID,
+        Namespace="default",
+        MaxResults=15,
+        NextToken=page1["NextToken"],
+    )
+    assert len(page2["GroupList"]) == 15
+    assert "NextToken" in page2
+
+    # We could request all of them in one go
+    all_users = client.list_groups(
+        AwsAccountId=ACCOUNT_ID,
+        Namespace="default",
+        MaxResults=1000,
+    )
+    length = len(all_users["GroupList"])
+    # We don't know exactly how much workspaces there are, because we are running multiple tests at the same time
+    assert length >= 125
+
+
+@mock_aws
+def test_search_groups():
+    client = boto3.client("quicksight", region_name="us-east-1")
+    for i in range(4):
+        group_name = f"group{i}" if i < 2 else f"test{i}@test"
+        client.create_group(
+            AwsAccountId=ACCOUNT_ID, Namespace="default", GroupName=group_name
+        )
+
+    resp = client.search_groups(
+        AwsAccountId=ACCOUNT_ID,
+        Namespace="default",
+        Filters=[
+            {"Operator": "StringEquals", "Name": "GROUP_NAME", "Value": "group1"},
+        ],
+    )
+
+    assert len(resp["GroupList"]) == 1
+    assert resp["Status"] == 200
+
+    assert {
+        "Arn": f"arn:aws:quicksight:us-east-1:{ACCOUNT_ID}:group/default/group1",
+        "GroupName": "group1",
+        "PrincipalId": ACCOUNT_ID,
+    } in resp["GroupList"]
+
+
+@mock_aws
+def test_search_groups__paginated():
+    client = boto3.client("quicksight", region_name="us-east-1")
+    for i in range(250):
+        group_name = f"group{i}" if i % 2 else f"test{i}@test"
+        client.create_group(
+            AwsAccountId=ACCOUNT_ID, Namespace="default", GroupName=group_name
+        )
+
+    # default pagesize is 100
+    page1 = client.search_groups(
+        AwsAccountId=ACCOUNT_ID,
+        Namespace="default",
+        Filters=[
+            {"Operator": "StartsWith", "Name": "GROUP_NAME", "Value": "group"},
+        ],
+    )
+    assert len(page1["GroupList"]) == 100
+    assert "NextToken" in page1
+
+    # We can ask for a smaller pagesize
+    page2 = client.search_groups(
+        AwsAccountId=ACCOUNT_ID,
+        Namespace="default",
+        Filters=[
+            {"Operator": "StartsWith", "Name": "GROUP_NAME", "Value": "group"},
+        ],
+        MaxResults=15,
+        NextToken=page1["NextToken"],
+    )
+    assert len(page2["GroupList"]) == 15
+    assert "NextToken" in page2
+
+    # We could request all of them in one go
+    all_users = client.search_groups(
+        AwsAccountId=ACCOUNT_ID,
+        Namespace="default",
+        Filters=[
+            {"Operator": "StartsWith", "Name": "GROUP_NAME", "Value": "group"},
+        ],
+        MaxResults=1000,
+    )
+    length = len(all_users["GroupList"])
+    # We don't know exactly how much workspaces there are, because we are running multiple tests at the same time
+    assert length >= 125
+
+
+@mock_aws
+def test_list_groups__diff_account_region():
+    ACCOUNT_ID_2 = "998877665544"
+    client_us = boto3.client("quicksight", region_name="us-east-2")
+    client_eu = boto3.client("quicksight", region_name="eu-west-1")
+    resp = client_us.create_group(
+        AwsAccountId=ACCOUNT_ID,
+        Namespace="default",
+        GroupName="group_us_1",
+        Description="Group in US Account 1",
+    )
+    resp = client_us.create_group(
+        AwsAccountId=ACCOUNT_ID_2,
+        Namespace="default",
+        GroupName="group_us_2",
+        Description="Group in US Account 2",
+    )
+    resp = client_eu.create_group(
+        AwsAccountId=ACCOUNT_ID,
+        Namespace="default",
+        GroupName="group_eu_1",
+        Description="Group in EU Account 1",
+    )
+
+    # Return Account 1, Region US
+    resp = client_us.list_groups(AwsAccountId=ACCOUNT_ID, Namespace="default")
+    assert len(resp["GroupList"]) == 1
+    assert resp["Status"] == 200
+
+    resp["GroupList"][0].pop("PrincipalId")
+
+    assert resp["GroupList"][0] == {
+        "Arn": f"arn:aws:quicksight:us-east-2:{ACCOUNT_ID}:group/default/group_us_1",
+        "GroupName": "group_us_1",
+        "Description": "Group in US Account 1",
+    }
+
+    # Return Account 2, Region US
+    resp = client_us.list_groups(AwsAccountId=ACCOUNT_ID_2, Namespace="default")
+
+    assert len(resp["GroupList"]) == 1
+    assert resp["Status"] == 200
+
+    resp["GroupList"][0].pop("PrincipalId")
+
+    assert resp["GroupList"][0] == {
+        "Arn": f"arn:aws:quicksight:us-east-2:{ACCOUNT_ID_2}:group/default/group_us_2",
+        "GroupName": "group_us_2",
+        "Description": "Group in US Account 2",
+    }
+
+    # Return Account 1, Region EU
+    resp = client_eu.list_groups(AwsAccountId=ACCOUNT_ID, Namespace="default")
+
+    assert len(resp["GroupList"]) == 1
+    assert resp["Status"] == 200
+
+    resp["GroupList"][0].pop("PrincipalId")
+
+    assert resp["GroupList"][0] == {
+        "Arn": f"arn:aws:quicksight:eu-west-1:{ACCOUNT_ID}:group/default/group_eu_1",
+        "GroupName": "group_eu_1",
+        "Description": "Group in EU Account 1",
+    }
+
+
+@mock_aws
+def test_search_groups__check_exceptions():
+    client = boto3.client("quicksight", region_name="us-east-1")
+    # Just do an exception test. No need to create a group first.
+
+    with pytest.raises(ClientError) as exc:
+        client.search_groups(
+            AwsAccountId=ACCOUNT_ID,
+            Namespace="default",
+            Filters=[
+                {
+                    "Operator": "StringEquals",
+                    "Name": "GROUP_DESCRIPTION",
+                    "Value": "My Group 1",
+                },
+            ],
+        )
+    err = exc.value.response["Error"]
+    assert err["Code"] == "ValidationException"
diff --git a/tests/test_quicksight/test_quicksight_users.py b/tests/test_quicksight/test_quicksight_users.py
index d1c35b944..c4250447e 100644
--- a/tests/test_quicksight/test_quicksight_users.py
+++ b/tests/test_quicksight/test_quicksight_users.py
@@ -145,6 +145,61 @@ def test_delete_user__quicksight(request_params):
     assert err["Code"] == "ResourceNotFoundException"
 
 
+@pytest.mark.parametrize(
+    "request_params",
+    [
+        {
+            "Email": "fakeemail@example.com",
+            "UserName": "tfacctestm9hpsr970z",
+            "UserRole": "READER",
+            "IdentityType": "QUICKSIGHT",
+        },
+        {
+            "Email": "authoremail@example.com",
+            "UserName": "authoremail@example.com",
+            "UserRole": "AUTHOR",
+            "IdentityType": "IAM",
+        },
+    ],
+)
+@mock_aws
+def test_update_user(request_params):
+    client = boto3.client("quicksight", region_name="us-west-2")
+    client.register_user(
+        AwsAccountId=ACCOUNT_ID,
+        Namespace="default",
+        Email=request_params["Email"],
+        IdentityType=request_params["IdentityType"],
+        UserName=request_params["UserName"],
+        UserRole=request_params["UserRole"],
+    )
+
+    resp = client.update_user(
+        UserName=request_params["UserName"],
+        AwsAccountId=ACCOUNT_ID,
+        Namespace="default",
+        Email=request_params["Email"] + "_modified",
+        Role="AUTHOR",
+    )
+    assert resp["User"]["Email"] == request_params["Email"] + "_modified"
+
+    resp = client.describe_user(
+        UserName=request_params["UserName"],
+        AwsAccountId=ACCOUNT_ID,
+        Namespace="default",
+    )
+
+    assert "User" in resp
+    assert resp["User"]["Arn"] == (
+        f"arn:aws:quicksight:us-west-2:{ACCOUNT_ID}:user/default/{request_params['UserName']}"
+    )
+    assert resp["User"]["UserName"] == request_params["UserName"]
+    assert resp["User"]["Email"] == request_params["Email"] + "_modified"
+    assert resp["User"]["Role"] == "AUTHOR"
+    assert resp["User"]["IdentityType"] == request_params["IdentityType"]
+    assert resp["User"]["Active"] is False
+
+
 @mock_aws
 def test_list_users__initial():
     client = boto3.client("quicksight", region_name="us-east-2")
@@ -202,6 +257,45 @@ def test_list_users():
     } in resp["UserList"]
 
 
+@mock_aws
+def test_list_users__paginated():
+    client = boto3.client("quicksight", region_name="us-east-2")
+    for i in range(125):
+        client.register_user(
+            AwsAccountId=ACCOUNT_ID,
+            Namespace="default",
+            Email=f"fakeemail{i}@example.com",
+            IdentityType="QUICKSIGHT",
+            UserName=f"fake{i}",
+            UserRole="READER",
+        )
+
+    # default pagesize is 100
+    page1 = client.list_users(AwsAccountId=ACCOUNT_ID, Namespace="default")
+    assert len(page1["UserList"]) == 100
+    assert "NextToken" in page1
+
+    # We can ask for a smaller pagesize
+    page2 = client.list_users(
+        AwsAccountId=ACCOUNT_ID,
+        Namespace="default",
+        MaxResults=15,
+        NextToken=page1["NextToken"],
+    )
+    assert len(page2["UserList"]) == 15
+    assert "NextToken" in page2
+
+    # We could request all of them in one go
+    all_users = client.list_users(
+        AwsAccountId=ACCOUNT_ID,
+        Namespace="default",
+        MaxResults=1000,
+    )
+    length = len(all_users["UserList"])
+    # We don't know exactly how much workspaces there are, because we are running multiple tests at the same time
+    assert length >= 125
+
+
 @pytest.mark.parametrize(
     "request_params",
     [
@@ -217,7 +311,7 @@ def test_list_users():
             "UserName": "authoremail@example.com",
             "UserRole": "AUTHOR",
             "IdentityType": "IAM",
-            "GroupName": "group.2",
+            "GroupName": "group@test",
         },
     ],
 )
@@ -267,7 +361,7 @@ def test_create_group_membership(request_params):
             "UserName": "authoremail@example.com",
             "UserRole": "AUTHOR",
             "IdentityType": "IAM",
-            "GroupName": "group.2",
+            "GroupName": "group@test",
         },
     ],
 )
@@ -439,3 +533,336 @@ def test_list_group_memberships__after_deleting_user():
         GroupName="group1", AwsAccountId=ACCOUNT_ID, Namespace="default"
     )
     assert len(resp["GroupMemberList"]) == 2
+
+
+@mock_aws
+def test_list_group_memberships__check_exceptions():
+    client = boto3.client("quicksight", region_name="us-east-2")
+    client.register_user(
+        AwsAccountId=ACCOUNT_ID,
+        Namespace="default",
+        Email="fakeemail@example.com",
+        IdentityType="QUICKSIGHT",
+        UserName="user",
+        UserRole="READER",
+    )
+
+    client.create_group(AwsAccountId=ACCOUNT_ID, Namespace="default", GroupName="group")
+
+    client.create_group_membership(
+        MemberName="user",
+        GroupName="group",
+        AwsAccountId=ACCOUNT_ID,
+        Namespace="default",
+    )
+
+    # Test for user is member of group
+    resp = client.describe_group_membership(
+        MemberName="user",
+        GroupName="group",
+        AwsAccountId=ACCOUNT_ID,
+        Namespace="default",
+    )
+    assert resp["Status"] == 200
+    assert resp["GroupMember"] == {
+        "Arn": f"arn:aws:quicksight:us-east-2:{ACCOUNT_ID}:group/default/group/user",
+        "MemberName": "user",
+    }
+
+    # Test for user is not member of group
+    with pytest.raises(ClientError) as exc:
+        resp = client.describe_group_membership(
+            MemberName="fake_user",
+            GroupName="group",
+            AwsAccountId=ACCOUNT_ID,
+            Namespace="default",
+        )
+    err = exc.value.response["Error"]
+    assert err["Code"] == "ResourceNotFoundException"
+
+    # Test for non existing group
+    with pytest.raises(ClientError) as exc:
+        resp = client.describe_group_membership(
+            MemberName="user",
+            GroupName="fake_group",
+            AwsAccountId=ACCOUNT_ID,
+            Namespace="default",
+        )
+    err = exc.value.response["Error"]
+    assert err["Code"] == "ResourceNotFoundException"
+
+
+@mock_aws
+def test_list_group_memberships__paginated():
+    client = boto3.client("quicksight", region_name="us-east-2")
+    client.create_group(
+        AwsAccountId=ACCOUNT_ID,
+        Namespace="default",
+        GroupName="group",
+    )
+    for i in range(125):
+        client.register_user(
+            AwsAccountId=ACCOUNT_ID,
+            Namespace="default",
+            Email=f"authoremail{i}@example.com",
+            IdentityType="QUICKSIGHT",
+            UserName=f"user.{i}",
+            UserRole="AUTHOR",
+        )
+        client.create_group_membership(
+            MemberName=f"user.{i}",
+            GroupName="group",
+            AwsAccountId=ACCOUNT_ID,
+            Namespace="default",
+        )
+
+    # default pagesize is 100
+    page1 = client.list_group_memberships(
+        GroupName="group", AwsAccountId=ACCOUNT_ID, Namespace="default"
+    )
+    assert len(page1["GroupMemberList"]) == 100
+    assert "NextToken" in page1
+
+    # We can ask for a smaller pagesize
+    page2 = client.list_group_memberships(
+        GroupName="group",
+        AwsAccountId=ACCOUNT_ID,
+        Namespace="default",
+        MaxResults=15,
+        NextToken=page1["NextToken"],
+    )
+    assert len(page2["GroupMemberList"]) == 15
+    assert "NextToken" in page2
+
+    # We could request all of them in one go
+    all_users = client.list_group_memberships(
+        GroupName="group",
+        AwsAccountId=ACCOUNT_ID,
+        Namespace="default",
+        MaxResults=1000,
+    )
+    length = len(all_users["GroupMemberList"])
+    # We don't know exactly how much workspaces there are, because we are running multiple tests at the same time
+    assert length >= 125
+
+
+@pytest.mark.parametrize(
+    "request_params",
+    [
+        {
+            "Email": "fakeemail@example.com",
+            "UserName": "user.1",
+            "UserRole": "READER",
+            "IdentityType": "QUICKSIGHT",
+            "GroupName1": "group1",
+            "GroupName2": "group2",
+            "GroupName3": "group3",
+        },
+        {
+            "Email": "authoremail@example.com",
+            "UserName": "authoremail@example.com",
+            "UserRole": "AUTHOR",
+            "IdentityType": "IAM",
+            "GroupName1": "group1@test",
+            "GroupName2": "group2@test",
+            "GroupName3": "group3@test",
+        },
+    ],
+)
+@mock_aws
+def test_list_user_groups(request_params):
+    client = boto3.client("quicksight", region_name="us-east-2")
+    client.register_user(
+        AwsAccountId=ACCOUNT_ID,
+        Namespace="default",
+        Email=request_params["Email"],
+        IdentityType=request_params["IdentityType"],
+        UserName=request_params["UserName"],
+        UserRole=request_params["UserRole"],
+    )
+    client.create_group(
+        AwsAccountId=ACCOUNT_ID,
+        Namespace="default",
+        GroupName=request_params["GroupName1"],
+    )
+    client.create_group(
+        AwsAccountId=ACCOUNT_ID,
+        Namespace="default",
+        GroupName=request_params["GroupName2"],
+    )
+    client.create_group(
+        AwsAccountId=ACCOUNT_ID,
+        Namespace="default",
+        GroupName=request_params["GroupName3"],
+    )
+    client.create_group_membership(
+        MemberName=request_params["UserName"],
+        GroupName=request_params["GroupName1"],
+        AwsAccountId=ACCOUNT_ID,
+        Namespace="default",
+    )
+    client.create_group_membership(
+        MemberName=request_params["UserName"],
+        GroupName=request_params["GroupName2"],
+        AwsAccountId=ACCOUNT_ID,
+        Namespace="default",
+    )
+
+    resp = client.list_user_groups(
+        UserName=request_params["UserName"],
+        AwsAccountId=ACCOUNT_ID,
+        Namespace="default",
+    )
+
+    assert resp["GroupList"] == [
+        {
+            "Arn": f"arn:aws:quicksight:us-east-2:{ACCOUNT_ID}:group/default/{request_params['GroupName1']}",
+            "GroupName": request_params["GroupName1"],
+            "PrincipalId": ACCOUNT_ID,
+        },
+        {
+            "Arn": f"arn:aws:quicksight:us-east-2:{ACCOUNT_ID}:group/default/{request_params['GroupName2']}",
+            "GroupName": request_params["GroupName2"],
+            "PrincipalId": ACCOUNT_ID,
+        },
+    ]
+    assert resp["Status"] == 200
+
+
+@mock_aws
+def test_list_user_groups__paginate():
+    client = boto3.client("quicksight", region_name="us-east-2")
+    client.register_user(
+        AwsAccountId=ACCOUNT_ID,
+        Namespace="default",
+        Email="fakeemail@example.com",
+        IdentityType="QUICKSIGHT",
+        UserName="fake_user",
+        UserRole="READER",
+    )
+    for i in range(125):
+        client.create_group(
+            AwsAccountId=ACCOUNT_ID,
+            Namespace="default",
+            GroupName=f"group{i}",
+        )
+
+        client.create_group_membership(
+            MemberName="fake_user",
+            GroupName=f"group{i}",
+            AwsAccountId=ACCOUNT_ID,
+            Namespace="default",
+        )
+
+    # default pagesize is 100
+    page1 = client.list_user_groups(
+        UserName="fake_user",
+        AwsAccountId=ACCOUNT_ID,
+        Namespace="default",
+    )
+    assert len(page1["GroupList"]) == 100
+    assert "NextToken" in page1
+
+    # We can ask for a smaller pagesize
+    page2 = client.list_user_groups(
+        UserName="fake_user",
+        AwsAccountId=ACCOUNT_ID,
+        Namespace="default",
+        MaxResults=15,
+        NextToken=page1["NextToken"],
+    )
+    assert len(page2["GroupList"]) == 15
+    assert "NextToken" in page2
+
+    # We could request all of them in one go
+    all_users = client.list_user_groups(
+        UserName="fake_user",
+        AwsAccountId=ACCOUNT_ID,
+        Namespace="default",
+        MaxResults=1000,
+    )
+    length = len(all_users["GroupList"])
+    # We don't know exactly how much workspaces there are, because we are running multiple tests at the same time
+    assert length >= 125
+
+
+@mock_aws
+def test_list_users__diff_account_region():
+    ACCOUNT_ID_2 = "998877665544"
+    client_us = boto3.client("quicksight", region_name="us-east-2")
+    client_eu = boto3.client("quicksight", region_name="eu-west-1")
+    client_us.register_user(
+        AwsAccountId=ACCOUNT_ID,
+        Namespace="default",
+        Email="fake_us_1@example.com",
+        IdentityType="QUICKSIGHT",
+        UserName="fake_us_1",
+        UserRole="READER",
+    )
+    resp = client_us.register_user(
+        AwsAccountId=ACCOUNT_ID_2,
+        Namespace="default",
+        Email="fake_us_2@example.com",
+        IdentityType="QUICKSIGHT",
+        UserName="fake_us_2",
+        UserRole="AUTHOR",
+    )
+    client_eu.register_user(
+        AwsAccountId=ACCOUNT_ID,
+        Namespace="default",
+        Email="fake_eu_1@example.com",
+        IdentityType="IAM",
+        UserName="fake_eu_1",
+        UserRole="AUTHOR",
+    )
+
+    # Return Account 1, Region US
+    resp = client_us.list_users(AwsAccountId=ACCOUNT_ID, Namespace="default")
+
+    assert len(resp["UserList"]) == 1
+    assert resp["Status"] == 200
+
+    resp["UserList"][0].pop("PrincipalId")
+
+    assert resp["UserList"][0] == {
+        "Arn": f"arn:aws:quicksight:us-east-2:{ACCOUNT_ID}:user/default/fake_us_1",
+        "UserName": "fake_us_1",
+        "Email": "fake_us_1@example.com",
+        "Role": "READER",
+        "IdentityType": "QUICKSIGHT",
+        "Active": False,
+    }
+
+    # Return Account 2, Region US
+    resp = client_us.list_users(AwsAccountId=ACCOUNT_ID_2, Namespace="default")
+
+    assert len(resp["UserList"]) == 1
+    assert resp["Status"] == 200
+
+    resp["UserList"][0].pop("PrincipalId")
+
+    assert resp["UserList"][0] == {
+        "Arn": f"arn:aws:quicksight:us-east-2:{ACCOUNT_ID_2}:user/default/fake_us_2",
+        "UserName": "fake_us_2",
+        "Email": "fake_us_2@example.com",
+        "Role": "AUTHOR",
+        "IdentityType": "QUICKSIGHT",
+        "Active": False,
+    }
+
+    # Return Account 1, Region EU
+    resp = client_eu.list_users(AwsAccountId=ACCOUNT_ID, Namespace="default")
+
+    assert len(resp["UserList"]) == 1
+    assert resp["Status"] == 200
+
+    resp["UserList"][0].pop("PrincipalId")
+
+    assert resp["UserList"][0] == {
+        "Arn": f"arn:aws:quicksight:eu-west-1:{ACCOUNT_ID}:user/default/fake_eu_1",
+        "UserName": "fake_eu_1",
+        "Email": "fake_eu_1@example.com",
+        "Role": "AUTHOR",
+        "IdentityType": "IAM",
+        "Active": False,
+    }