fix(listUsers): Add Search User By Phone to User Service V2 (#9052)

# Which Problems Are Solved

Added search by phone to user Service V2.

```
curl --request POST \
  --url https://<zitadel_domain>/v2/users \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <Token>' \
  --header 'Content-Type: application/json' \
  --header 'content-type: application/json' \
  --data '{
  "query": {
    "offset": "0",
    "limit": 100,
    "asc": true
  },
  "sortingColumn": "USER_FIELD_NAME_UNSPECIFIED",
  "queries": [
    {
      "phoneQuery": {
        "number": "+12011223313",
        "method": "TEXT_QUERY_METHOD_EQUALS"
      }
    }
  ]
}'
```

Why?
Searching for a user by phone was missing from User Service V2 and V2
beta.

# How the Problems Are Solved

* Added to the SearchQuery proto 
* Added code to filter users by phone

# Additional Changes

N/A

# Additional Context

Search by phone is present in V3 User Service

---------

Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com>
This commit is contained in:
Harsha Reddy
2025-01-02 15:44:15 +05:30
committed by GitHub
parent 8ec099ae28
commit e1f0d46393
14 changed files with 423 additions and 288 deletions

View File

@@ -5,6 +5,7 @@ package user_test
import (
"context"
"fmt"
"slices"
"testing"
"time"
@@ -33,7 +34,7 @@ func TestServer_GetUserByID(t *testing.T) {
type args struct {
ctx context.Context
req *user.GetUserByIDRequest
dep func(ctx context.Context, username string, request *user.GetUserByIDRequest) (*userAttr, error)
dep func(ctx context.Context, request *user.GetUserByIDRequest) *userAttr
}
tests := []struct {
name string
@@ -48,8 +49,8 @@ func TestServer_GetUserByID(t *testing.T) {
&user.GetUserByIDRequest{
UserId: "",
},
func(ctx context.Context, username string, request *user.GetUserByIDRequest) (*userAttr, error) {
return nil, nil
func(ctx context.Context, request *user.GetUserByIDRequest) *userAttr {
return nil
},
},
wantErr: true,
@@ -61,8 +62,8 @@ func TestServer_GetUserByID(t *testing.T) {
&user.GetUserByIDRequest{
UserId: "unknown",
},
func(ctx context.Context, username string, request *user.GetUserByIDRequest) (*userAttr, error) {
return nil, nil
func(ctx context.Context, request *user.GetUserByIDRequest) *userAttr {
return nil
},
},
wantErr: true,
@@ -72,10 +73,10 @@ func TestServer_GetUserByID(t *testing.T) {
args: args{
IamCTX,
&user.GetUserByIDRequest{},
func(ctx context.Context, username string, request *user.GetUserByIDRequest) (*userAttr, error) {
resp := Instance.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username)
request.UserId = resp.GetUserId()
return &userAttr{resp.GetUserId(), username, nil, resp.GetDetails()}, nil
func(ctx context.Context, request *user.GetUserByIDRequest) *userAttr {
info := createUser(ctx, orgResp.OrganizationId, false)
request.UserId = info.UserID
return &info
},
},
want: &user.GetUserByIDResponse{
@@ -99,7 +100,6 @@ func TestServer_GetUserByID(t *testing.T) {
IsVerified: true,
},
Phone: &user.HumanPhone{
Phone: "+41791234567",
IsVerified: true,
},
},
@@ -116,11 +116,10 @@ func TestServer_GetUserByID(t *testing.T) {
args: args{
IamCTX,
&user.GetUserByIDRequest{},
func(ctx context.Context, username string, request *user.GetUserByIDRequest) (*userAttr, error) {
resp := Instance.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username)
request.UserId = resp.GetUserId()
details := Instance.SetUserPassword(ctx, resp.GetUserId(), integration.UserPassword, true)
return &userAttr{resp.GetUserId(), username, details.GetChangeDate(), resp.GetDetails()}, nil
func(ctx context.Context, request *user.GetUserByIDRequest) *userAttr {
info := createUser(ctx, orgResp.OrganizationId, true)
request.UserId = info.UserID
return &info
},
},
want: &user.GetUserByIDResponse{
@@ -144,7 +143,6 @@ func TestServer_GetUserByID(t *testing.T) {
IsVerified: true,
},
Phone: &user.HumanPhone{
Phone: "+41791234567",
IsVerified: true,
},
PasswordChangeRequired: true,
@@ -161,9 +159,7 @@ func TestServer_GetUserByID(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
username := gofakeit.Email()
userAttr, err := tt.args.dep(tt.args.ctx, username, tt.args.req)
require.NoError(t, err)
userAttr := tt.args.dep(IamCTX, tt.args.req)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.args.ctx, time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
@@ -183,6 +179,7 @@ func TestServer_GetUserByID(t *testing.T) {
tt.want.User.LoginNames = []string{userAttr.Username}
if human := tt.want.User.GetHuman(); human != nil {
human.Email.Email = userAttr.Username
human.Phone.Phone = userAttr.Phone
if tt.want.User.GetHuman().GetPasswordChanged() != nil {
human.PasswordChanged = userAttr.Changed
}
@@ -335,21 +332,60 @@ func TestServer_GetUserByID_Permission(t *testing.T) {
}
}
type userAttrs []userAttr
func (u userAttrs) userIDs() []string {
ids := make([]string, len(u))
for i := range u {
ids[i] = u[i].UserID
}
return ids
}
func (u userAttrs) emails() []string {
emails := make([]string, len(u))
for i := range u {
emails[i] = u[i].Username
}
return emails
}
type userAttr struct {
UserID string
Username string
Phone string
Changed *timestamppb.Timestamp
Details *object.Details
}
func createUsers(ctx context.Context, orgID string, count int, passwordChangeRequired bool) userAttrs {
infos := make([]userAttr, count)
for i := 0; i < count; i++ {
infos[i] = createUser(ctx, orgID, passwordChangeRequired)
}
slices.Reverse(infos)
return infos
}
func createUser(ctx context.Context, orgID string, passwordChangeRequired bool) userAttr {
username := gofakeit.Email()
// used as default country prefix
phone := "+41" + gofakeit.Phone()
resp := Instance.CreateHumanUserVerified(ctx, orgID, username, phone)
info := userAttr{resp.GetUserId(), username, phone, nil, resp.GetDetails()}
if passwordChangeRequired {
details := Instance.SetUserPassword(ctx, resp.GetUserId(), integration.UserPassword, true)
info.Changed = details.GetChangeDate()
}
return info
}
func TestServer_ListUsers(t *testing.T) {
orgResp := Instance.CreateOrganization(IamCTX, fmt.Sprintf("ListUsersOrg-%s", gofakeit.AppName()), gofakeit.Email())
userResp := Instance.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, gofakeit.Email())
type args struct {
ctx context.Context
count int
req *user.ListUsersRequest
dep func(ctx context.Context, usernames []string, request *user.ListUsersRequest) ([]userAttr, error)
ctx context.Context
req *user.ListUsersRequest
dep func(ctx context.Context, request *user.ListUsersRequest) userAttrs
}
tests := []struct {
name string
@@ -361,11 +397,11 @@ func TestServer_ListUsers(t *testing.T) {
name: "list user by id, no permission",
args: args{
UserCTX,
0,
&user.ListUsersRequest{},
func(ctx context.Context, usernames []string, request *user.ListUsersRequest) ([]userAttr, error) {
request.Queries = append(request.Queries, InUserIDsQuery([]string{userResp.UserId}))
return []userAttr{}, nil
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
info := createUser(ctx, orgResp.OrganizationId, false)
request.Queries = append(request.Queries, InUserIDsQuery([]string{info.UserID}))
return []userAttr{}
},
},
want: &user.ListUsersResponse{
@@ -381,22 +417,15 @@ func TestServer_ListUsers(t *testing.T) {
name: "list user by id, ok",
args: args{
IamCTX,
1,
&user.ListUsersRequest{
Queries: []*user.SearchQuery{
OrganizationIdQuery(orgResp.OrganizationId),
},
},
func(ctx context.Context, usernames []string, request *user.ListUsersRequest) ([]userAttr, error) {
infos := make([]userAttr, len(usernames))
userIDs := make([]string, len(usernames))
for i, username := range usernames {
resp := Instance.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username)
userIDs[i] = resp.GetUserId()
infos[i] = userAttr{resp.GetUserId(), username, nil, resp.GetDetails()}
}
request.Queries = append(request.Queries, InUserIDsQuery(userIDs))
return infos, nil
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
info := createUser(ctx, orgResp.OrganizationId, false)
request.Queries = append(request.Queries, InUserIDsQuery([]string{info.UserID}))
return []userAttr{info}
},
},
want: &user.ListUsersResponse{
@@ -422,7 +451,6 @@ func TestServer_ListUsers(t *testing.T) {
IsVerified: true,
},
Phone: &user.HumanPhone{
Phone: "+41791234567",
IsVerified: true,
},
},
@@ -435,23 +463,15 @@ func TestServer_ListUsers(t *testing.T) {
name: "list user by id, passwordChangeRequired, ok",
args: args{
IamCTX,
1,
&user.ListUsersRequest{
Queries: []*user.SearchQuery{
OrganizationIdQuery(orgResp.OrganizationId),
},
},
func(ctx context.Context, usernames []string, request *user.ListUsersRequest) ([]userAttr, error) {
infos := make([]userAttr, len(usernames))
userIDs := make([]string, len(usernames))
for i, username := range usernames {
resp := Instance.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username)
userIDs[i] = resp.GetUserId()
details := Instance.SetUserPassword(ctx, resp.GetUserId(), integration.UserPassword, true)
infos[i] = userAttr{resp.GetUserId(), username, details.GetChangeDate(), resp.GetDetails()}
}
request.Queries = append(request.Queries, InUserIDsQuery(userIDs))
return infos, nil
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
info := createUser(ctx, orgResp.OrganizationId, true)
request.Queries = append(request.Queries, InUserIDsQuery([]string{info.UserID}))
return []userAttr{info}
},
},
want: &user.ListUsersResponse{
@@ -477,7 +497,6 @@ func TestServer_ListUsers(t *testing.T) {
IsVerified: true,
},
Phone: &user.HumanPhone{
Phone: "+41791234567",
IsVerified: true,
},
PasswordChangeRequired: true,
@@ -492,22 +511,15 @@ func TestServer_ListUsers(t *testing.T) {
name: "list user by id multiple, ok",
args: args{
IamCTX,
3,
&user.ListUsersRequest{
Queries: []*user.SearchQuery{
OrganizationIdQuery(orgResp.OrganizationId),
},
},
func(ctx context.Context, usernames []string, request *user.ListUsersRequest) ([]userAttr, error) {
infos := make([]userAttr, len(usernames))
userIDs := make([]string, len(usernames))
for i, username := range usernames {
resp := Instance.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username)
userIDs[i] = resp.GetUserId()
infos[i] = userAttr{resp.GetUserId(), username, nil, resp.GetDetails()}
}
request.Queries = append(request.Queries, InUserIDsQuery(userIDs))
return infos, nil
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
infos := createUsers(ctx, orgResp.OrganizationId, 3, false)
request.Queries = append(request.Queries, InUserIDsQuery(infos.userIDs()))
return infos
},
},
want: &user.ListUsersResponse{
@@ -533,7 +545,6 @@ func TestServer_ListUsers(t *testing.T) {
IsVerified: true,
},
Phone: &user.HumanPhone{
Phone: "+41791234567",
IsVerified: true,
},
},
@@ -554,7 +565,6 @@ func TestServer_ListUsers(t *testing.T) {
IsVerified: true,
},
Phone: &user.HumanPhone{
Phone: "+41791234567",
IsVerified: true,
},
},
@@ -575,7 +585,6 @@ func TestServer_ListUsers(t *testing.T) {
IsVerified: true,
},
Phone: &user.HumanPhone{
Phone: "+41791234567",
IsVerified: true,
},
},
@@ -588,22 +597,15 @@ func TestServer_ListUsers(t *testing.T) {
name: "list user by username, ok",
args: args{
IamCTX,
1,
&user.ListUsersRequest{
Queries: []*user.SearchQuery{
OrganizationIdQuery(orgResp.OrganizationId),
},
},
func(ctx context.Context, usernames []string, request *user.ListUsersRequest) ([]userAttr, error) {
infos := make([]userAttr, len(usernames))
userIDs := make([]string, len(usernames))
for i, username := range usernames {
resp := Instance.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username)
userIDs[i] = resp.GetUserId()
infos[i] = userAttr{resp.GetUserId(), username, nil, resp.GetDetails()}
request.Queries = append(request.Queries, UsernameQuery(username))
}
return infos, nil
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
info := createUser(ctx, orgResp.OrganizationId, false)
request.Queries = append(request.Queries, UsernameQuery(info.Username))
return []userAttr{info}
},
},
want: &user.ListUsersResponse{
@@ -629,7 +631,6 @@ func TestServer_ListUsers(t *testing.T) {
IsVerified: true,
},
Phone: &user.HumanPhone{
Phone: "+41791234567",
IsVerified: true,
},
},
@@ -642,20 +643,15 @@ func TestServer_ListUsers(t *testing.T) {
name: "list user in emails, ok",
args: args{
IamCTX,
1,
&user.ListUsersRequest{
Queries: []*user.SearchQuery{
OrganizationIdQuery(orgResp.OrganizationId),
},
},
func(ctx context.Context, usernames []string, request *user.ListUsersRequest) ([]userAttr, error) {
infos := make([]userAttr, len(usernames))
for i, username := range usernames {
resp := Instance.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username)
infos[i] = userAttr{resp.GetUserId(), username, nil, resp.GetDetails()}
}
request.Queries = append(request.Queries, InUserEmailsQuery(usernames))
return infos, nil
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
info := createUser(ctx, orgResp.OrganizationId, false)
request.Queries = append(request.Queries, InUserEmailsQuery([]string{info.Username}))
return []userAttr{info}
},
},
want: &user.ListUsersResponse{
@@ -681,7 +677,6 @@ func TestServer_ListUsers(t *testing.T) {
IsVerified: true,
},
Phone: &user.HumanPhone{
Phone: "+41791234567",
IsVerified: true,
},
},
@@ -694,20 +689,15 @@ func TestServer_ListUsers(t *testing.T) {
name: "list user in emails multiple, ok",
args: args{
IamCTX,
3,
&user.ListUsersRequest{
Queries: []*user.SearchQuery{
OrganizationIdQuery(orgResp.OrganizationId),
},
},
func(ctx context.Context, usernames []string, request *user.ListUsersRequest) ([]userAttr, error) {
infos := make([]userAttr, len(usernames))
for i, username := range usernames {
resp := Instance.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username)
infos[i] = userAttr{resp.GetUserId(), username, nil, resp.GetDetails()}
}
request.Queries = append(request.Queries, InUserEmailsQuery(usernames))
return infos, nil
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
infos := createUsers(ctx, orgResp.OrganizationId, 3, false)
request.Queries = append(request.Queries, InUserEmailsQuery(infos.emails()))
return infos
},
},
want: &user.ListUsersResponse{
@@ -733,7 +723,6 @@ func TestServer_ListUsers(t *testing.T) {
IsVerified: true,
},
Phone: &user.HumanPhone{
Phone: "+41791234567",
IsVerified: true,
},
},
@@ -754,7 +743,6 @@ func TestServer_ListUsers(t *testing.T) {
IsVerified: true,
},
Phone: &user.HumanPhone{
Phone: "+41791234567",
IsVerified: true,
},
},
@@ -775,7 +763,6 @@ func TestServer_ListUsers(t *testing.T) {
IsVerified: true,
},
Phone: &user.HumanPhone{
Phone: "+41791234567",
IsVerified: true,
},
},
@@ -788,14 +775,13 @@ func TestServer_ListUsers(t *testing.T) {
name: "list user in emails no found, ok",
args: args{
IamCTX,
3,
&user.ListUsersRequest{Queries: []*user.SearchQuery{
OrganizationIdQuery(orgResp.OrganizationId),
InUserEmailsQuery([]string{"notfound"}),
},
},
func(ctx context.Context, usernames []string, request *user.ListUsersRequest) ([]userAttr, error) {
return []userAttr{}, nil
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
return []userAttr{}
},
},
want: &user.ListUsersResponse{
@@ -807,23 +793,64 @@ func TestServer_ListUsers(t *testing.T) {
Result: []*user.User{},
},
},
{
name: "list user phone, ok",
args: args{
IamCTX,
&user.ListUsersRequest{
Queries: []*user.SearchQuery{
OrganizationIdQuery(orgResp.OrganizationId),
},
},
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
info := createUser(ctx, orgResp.OrganizationId, false)
request.Queries = append(request.Queries, PhoneQuery(info.Phone))
return []userAttr{info}
},
},
want: &user.ListUsersResponse{
Details: &object_v2beta.ListDetails{
TotalResult: 1,
Timestamp: timestamppb.Now(),
},
SortingColumn: 0,
Result: []*user.User{
{
State: user.UserState_USER_STATE_ACTIVE,
Type: &user.User_Human{
Human: &user.HumanUser{
Profile: &user.HumanProfile{
GivenName: "Mickey",
FamilyName: "Mouse",
NickName: gu.Ptr("Mickey"),
DisplayName: gu.Ptr("Mickey Mouse"),
PreferredLanguage: gu.Ptr("nl"),
Gender: user.Gender_GENDER_MALE.Enum(),
},
Email: &user.HumanEmail{
IsVerified: true,
},
Phone: &user.HumanPhone{
IsVerified: true,
},
},
},
},
},
},
},
{
name: "list user resourceowner multiple, ok",
args: args{
IamCTX,
3,
&user.ListUsersRequest{},
func(ctx context.Context, usernames []string, request *user.ListUsersRequest) ([]userAttr, error) {
func(ctx context.Context, request *user.ListUsersRequest) userAttrs {
orgResp := Instance.CreateOrganization(ctx, fmt.Sprintf("ListUsersResourceowner-%s", gofakeit.AppName()), gofakeit.Email())
infos := make([]userAttr, len(usernames))
for i, username := range usernames {
resp := Instance.CreateHumanUserVerified(ctx, orgResp.OrganizationId, username)
infos[i] = userAttr{resp.GetUserId(), username, nil, resp.GetDetails()}
}
infos := createUsers(ctx, orgResp.OrganizationId, 3, false)
request.Queries = append(request.Queries, OrganizationIdQuery(orgResp.OrganizationId))
request.Queries = append(request.Queries, InUserEmailsQuery(usernames))
return infos, nil
request.Queries = append(request.Queries, InUserEmailsQuery(infos.emails()))
return infos
},
},
want: &user.ListUsersResponse{
@@ -849,7 +876,6 @@ func TestServer_ListUsers(t *testing.T) {
IsVerified: true,
},
Phone: &user.HumanPhone{
Phone: "+41791234567",
IsVerified: true,
},
},
@@ -870,7 +896,6 @@ func TestServer_ListUsers(t *testing.T) {
IsVerified: true,
},
Phone: &user.HumanPhone{
Phone: "+41791234567",
IsVerified: true,
},
},
@@ -891,7 +916,6 @@ func TestServer_ListUsers(t *testing.T) {
IsVerified: true,
},
Phone: &user.HumanPhone{
Phone: "+41791234567",
IsVerified: true,
},
},
@@ -903,12 +927,7 @@ func TestServer_ListUsers(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
usernames := make([]string, tt.args.count)
for i := 0; i < tt.args.count; i++ {
usernames[i] = gofakeit.Email()
}
infos, err := tt.args.dep(tt.args.ctx, usernames, tt.args.req)
require.NoError(t, err)
infos := tt.args.dep(IamCTX, tt.args.req)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(tt.args.ctx, time.Minute)
require.EventuallyWithT(t, func(ttt *assert.CollectT) {
@@ -934,6 +953,7 @@ func TestServer_ListUsers(t *testing.T) {
tt.want.Result[i].LoginNames = []string{infos[i].Username}
if human := tt.want.Result[i].GetHuman(); human != nil {
human.Email.Email = infos[i].Username
human.Phone.Phone = infos[i].Phone
if tt.want.Result[i].GetHuman().GetPasswordChanged() != nil {
human.PasswordChanged = infos[i].Changed
}
@@ -941,7 +961,7 @@ func TestServer_ListUsers(t *testing.T) {
tt.want.Result[i].Details = detailsV2ToV2beta(infos[i].Details)
}
for i := range tt.want.Result {
assert.Contains(ttt, got.Result, tt.want.Result[i])
assert.EqualExportedValues(ttt, got.Result[i], tt.want.Result[i])
}
}
integration.AssertListDetails(ttt, tt.want, got)
@@ -968,6 +988,15 @@ func InUserEmailsQuery(emails []string) *user.SearchQuery {
}
}
func PhoneQuery(number string) *user.SearchQuery {
return &user.SearchQuery{Query: &user.SearchQuery_PhoneQuery{
PhoneQuery: &user.PhoneQuery{
Number: number,
},
},
}
}
func UsernameQuery(username string) *user.SearchQuery {
return &user.SearchQuery{Query: &user.SearchQuery_UserNameQuery{
UserNameQuery: &user.UserNameQuery{