Repository: getmoto/moto

Issue:
cognito_idp.list_users() does not support prefix operator in Filter parameter
Per [boto3 docs](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/cognito-idp.html#CognitoIdentityProvider.Client.list_users), `cognito-idp` client supports two types of operators in `list_users()` method `Filter` parameter:

> Filter-Type : For an exact match, use =, for example, "given_name = "Jon"". For a prefix ("starts with") match, use ^=, for example, "given_name ^= "Jon"".

However, moto implementations leaves `^=` functionality unsupported. Example based on unit test (last 6 lines were added):
```python
@mock_cognitoidp
def test_list_users():
    conn = boto3.client("cognito-idp", "us-west-2")

    username = str(uuid.uuid4())
    user_pool_id = conn.create_user_pool(PoolName=str(uuid.uuid4()))["UserPool"]["Id"]
    conn.admin_create_user(UserPoolId=user_pool_id, Username=username)
    result = conn.list_users(UserPoolId=user_pool_id)
    result["Users"].should.have.length_of(1)
    result["Users"][0]["Username"].should.equal(username)

    username_bis = str(uuid.uuid4())
    conn.admin_create_user(
        UserPoolId=user_pool_id,
        Username=username_bis,
        UserAttributes=[{"Name": "phone_number", "Value": "+33666666666"}],
    )
    result = conn.list_users(
        UserPoolId=user_pool_id, Filter='phone_number="+33666666666'
    )
    result["Users"].should.have.length_of(1)
    result["Users"][0]["Username"].should.equal(username_bis)

    # checking Filter with space
    result = conn.list_users(
        UserPoolId=user_pool_id, Filter='phone_number = "+33666666666'
    )
    result["Users"].should.have.length_of(1)
    result["Users"][0]["Username"].should.equal(username_bis)

    # checking Filter with prefix operator
    result = conn.list_users(
        UserPoolId=user_pool_id, Filter='phone_number ^= "+336"'
    )
    result["Users"].should.have.length_of(1)  # <-- FAIL
    result["Users"][0]["Username"].should.equal(username_bis)
```

In order to properly reflect behavior of `cognito-idp` client, support for prefix (`^=`) operator in `list_users()` `Filter` parameter should be implemented.

Reporting it, as I'd like to fix it one-shot with #3952 

Please confirm if you're OK with it, fellow maintainers :)


Discussion:
Sounds good to me @k0mmsussert0d!

Resolving patch:
diff --git a/moto/cognitoidp/responses.py b/moto/cognitoidp/responses.py
--- a/moto/cognitoidp/responses.py
+++ b/moto/cognitoidp/responses.py
@@ -2,6 +2,7 @@
 
 import json
 import os
+import re
 
 from moto.core.responses import BaseResponse
 from .models import cognitoidp_backends, find_region_by_value, UserStatus
@@ -332,18 +333,25 @@ def list_users(self):
                 "status": lambda u: "Enabled" if u.enabled else "Disabled",
                 "username": lambda u: u.username,
             }
-            name, value = filt.replace('"', "").replace(" ", "").split("=")
+            comparisons = {"=": lambda x, y: x == y, "^=": lambda x, y: x.startswith(y)}
+
+            match = re.match(r"([\w:]+)\s*(=|\^=)\s*\"(.*)\"", filt)
+            if match:
+                name, op, value = match.groups()
+            else:
+                raise InvalidParameterException("Error while parsing filter")
+            compare = comparisons[op]
             users = [
                 user
                 for user in users
                 if [
                     attr
                     for attr in user.attributes
-                    if attr["Name"] == name and attr["Value"] == value
+                    if attr["Name"] == name and compare(attr["Value"], value)
                 ]
                 or (
                     name in inherent_attributes
-                    and inherent_attributes[name](user) == value
+                    and compare(inherent_attributes[name](user), value)
                 )
             ]
         response = {"Users": [user.to_json(extended=True) for user in users]}


Test patch:
diff --git a/tests/test_cognitoidp/test_cognitoidp.py b/tests/test_cognitoidp/test_cognitoidp.py
--- a/tests/test_cognitoidp/test_cognitoidp.py
+++ b/tests/test_cognitoidp/test_cognitoidp.py
@@ -1209,18 +1209,55 @@ def test_list_users():
         UserAttributes=[{"Name": "phone_number", "Value": "+33666666666"}],
     )
     result = conn.list_users(
-        UserPoolId=user_pool_id, Filter='phone_number="+33666666666'
+        UserPoolId=user_pool_id, Filter='phone_number="+33666666666"'
     )
     result["Users"].should.have.length_of(1)
     result["Users"][0]["Username"].should.equal(username_bis)
 
     # checking Filter with space
     result = conn.list_users(
-        UserPoolId=user_pool_id, Filter='phone_number = "+33666666666'
+        UserPoolId=user_pool_id, Filter='phone_number = "+33666666666"'
     )
     result["Users"].should.have.length_of(1)
     result["Users"][0]["Username"].should.equal(username_bis)
 
+    user0_username = "user0@example.com"
+    conn.admin_create_user(
+        UserPoolId=user_pool_id,
+        Username=user0_username,
+        UserAttributes=[{"Name": "phone_number", "Value": "+48555555555"}],
+    )
+
+    # checking Filter with prefix operator
+    result = conn.list_users(UserPoolId=user_pool_id, Filter='phone_number ^= "+48"')
+    result["Users"].should.have.length_of(1)
+    result["Users"][0]["Username"].should.equal(user0_username)
+
+    # empty value Filter should also be supported
+    result = conn.list_users(UserPoolId=user_pool_id, Filter='family_name=""')
+    result["Users"].should.have.length_of(0)
+
+
+@mock_cognitoidp
+def test_list_users_incorrect_filter():
+    conn = boto3.client("cognito-idp", "us-west-2")
+
+    user_pool_id = conn.create_user_pool(PoolName=str(uuid.uuid4()))["UserPool"]["Id"]
+
+    with pytest.raises(conn.exceptions.InvalidParameterException) as exc:
+        conn.list_users(UserPoolId=user_pool_id, Filter="username = foo")
+    _assert_filter_parsing_error(exc)
+
+    with pytest.raises(conn.exceptions.InvalidParameterException) as exc:
+        conn.list_users(UserPoolId=user_pool_id, Filter="username=")
+    _assert_filter_parsing_error(exc)
+
+
+def _assert_filter_parsing_error(exc):
+    err = exc.value.response["Error"]
+    assert err["Code"].should.equal("InvalidParameterException")
+    assert err["Message"].should.equal("Error while parsing filter")
+
 
 @mock_cognitoidp
 def test_list_users_inherent_attributes():