From e1f0d463933a6cd8432863e625627ea12458e289 Mon Sep 17 00:00:00 2001 From: Harsha Reddy Date: Thu, 2 Jan 2025 15:44:15 +0530 Subject: [PATCH] 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:///v2/users \ --header 'Accept: application/json' \ --header 'Authorization: Bearer ' \ --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> --- .../admin/integration_test/iam_member_test.go | 8 +- .../management/integration_test/org_test.go | 8 +- .../user/v2/integration_test/email_test.go | 4 +- .../user/v2/integration_test/idp_link_test.go | 12 +- .../user/v2/integration_test/phone_test.go | 2 +- .../user/v2/integration_test/query_test.go | 340 ++++++++++-------- internal/api/grpc/user/v2/query.go | 6 + .../v2beta/integration_test/email_test.go | 2 +- .../v2beta/integration_test/phone_test.go | 2 +- .../v2beta/integration_test/query_test.go | 275 +++++++------- internal/api/grpc/user/v2beta/query.go | 6 + internal/integration/client.go | 4 +- proto/zitadel/user/v2/query.proto | 21 ++ proto/zitadel/user/v2beta/query.proto | 21 ++ 14 files changed, 423 insertions(+), 288 deletions(-) diff --git a/internal/api/grpc/admin/integration_test/iam_member_test.go b/internal/api/grpc/admin/integration_test/iam_member_test.go index 035cfa9f70..93d4417cba 100644 --- a/internal/api/grpc/admin/integration_test/iam_member_test.go +++ b/internal/api/grpc/admin/integration_test/iam_member_test.go @@ -35,7 +35,7 @@ func TestServer_ListIAMMemberRoles(t *testing.T) { } func TestServer_ListIAMMembers(t *testing.T) { - user := Instance.CreateHumanUserVerified(AdminCTX, Instance.DefaultOrg.Id, gofakeit.Email()) + user := Instance.CreateHumanUserVerified(AdminCTX, Instance.DefaultOrg.Id, gofakeit.Email(), gofakeit.Phone()) _, err := Client.AddIAMMember(AdminCTX, &admin_pb.AddIAMMemberRequest{ UserId: user.GetUserId(), Roles: iamRoles, @@ -116,7 +116,7 @@ func TestServer_ListIAMMembers(t *testing.T) { } func TestServer_AddIAMMember(t *testing.T) { - user := Instance.CreateHumanUserVerified(AdminCTX, Instance.DefaultOrg.Id, gofakeit.Email()) + user := Instance.CreateHumanUserVerified(AdminCTX, Instance.DefaultOrg.Id, gofakeit.Email(), gofakeit.Phone()) type args struct { ctx context.Context req *admin_pb.AddIAMMemberRequest @@ -190,7 +190,7 @@ func TestServer_AddIAMMember(t *testing.T) { } func TestServer_UpdateIAMMember(t *testing.T) { - user := Instance.CreateHumanUserVerified(AdminCTX, Instance.DefaultOrg.Id, gofakeit.Email()) + user := Instance.CreateHumanUserVerified(AdminCTX, Instance.DefaultOrg.Id, gofakeit.Email(), gofakeit.Phone()) _, err := Client.AddIAMMember(AdminCTX, &admin_pb.AddIAMMemberRequest{ UserId: user.GetUserId(), Roles: []string{"IAM_OWNER"}, @@ -271,7 +271,7 @@ func TestServer_UpdateIAMMember(t *testing.T) { } func TestServer_RemoveIAMMember(t *testing.T) { - user := Instance.CreateHumanUserVerified(AdminCTX, Instance.DefaultOrg.Id, gofakeit.Email()) + user := Instance.CreateHumanUserVerified(AdminCTX, Instance.DefaultOrg.Id, gofakeit.Email(), gofakeit.Phone()) _, err := Client.AddIAMMember(AdminCTX, &admin_pb.AddIAMMemberRequest{ UserId: user.GetUserId(), Roles: []string{"IAM_OWNER"}, diff --git a/internal/api/grpc/management/integration_test/org_test.go b/internal/api/grpc/management/integration_test/org_test.go index 8288ceb9e9..46693f14d7 100644 --- a/internal/api/grpc/management/integration_test/org_test.go +++ b/internal/api/grpc/management/integration_test/org_test.go @@ -39,7 +39,7 @@ func TestServer_ListOrgMemberRoles(t *testing.T) { } func TestServer_ListOrgMembers(t *testing.T) { - user := Instance.CreateHumanUserVerified(OrgCTX, Instance.DefaultOrg.Id, gofakeit.Email()) + user := Instance.CreateHumanUserVerified(OrgCTX, Instance.DefaultOrg.Id, gofakeit.Email(), gofakeit.Phone()) _, err := Client.AddOrgMember(OrgCTX, &mgmt_pb.AddOrgMemberRequest{ UserId: user.GetUserId(), Roles: iamRoles[1:], @@ -120,7 +120,7 @@ func TestServer_ListOrgMembers(t *testing.T) { } func TestServer_AddOrgMember(t *testing.T) { - user := Instance.CreateHumanUserVerified(OrgCTX, Instance.DefaultOrg.Id, gofakeit.Email()) + user := Instance.CreateHumanUserVerified(OrgCTX, Instance.DefaultOrg.Id, gofakeit.Email(), gofakeit.Phone()) type args struct { ctx context.Context req *mgmt_pb.AddOrgMemberRequest @@ -194,7 +194,7 @@ func TestServer_AddOrgMember(t *testing.T) { } func TestServer_UpdateOrgMember(t *testing.T) { - user := Instance.CreateHumanUserVerified(OrgCTX, Instance.DefaultOrg.Id, gofakeit.Email()) + user := Instance.CreateHumanUserVerified(OrgCTX, Instance.DefaultOrg.Id, gofakeit.Email(), gofakeit.Phone()) _, err := Client.AddOrgMember(OrgCTX, &mgmt_pb.AddOrgMemberRequest{ UserId: user.GetUserId(), Roles: []string{"ORG_OWNER"}, @@ -275,7 +275,7 @@ func TestServer_UpdateOrgMember(t *testing.T) { } func TestServer_RemoveIAMMember(t *testing.T) { - user := Instance.CreateHumanUserVerified(OrgCTX, Instance.DefaultOrg.Id, gofakeit.Email()) + user := Instance.CreateHumanUserVerified(OrgCTX, Instance.DefaultOrg.Id, gofakeit.Email(), gofakeit.Phone()) _, err := Client.AddOrgMember(OrgCTX, &mgmt_pb.AddOrgMemberRequest{ UserId: user.GetUserId(), Roles: []string{"ORG_OWNER"}, diff --git a/internal/api/grpc/user/v2/integration_test/email_test.go b/internal/api/grpc/user/v2/integration_test/email_test.go index de53bc68aa..ad63c2ce5e 100644 --- a/internal/api/grpc/user/v2/integration_test/email_test.go +++ b/internal/api/grpc/user/v2/integration_test/email_test.go @@ -147,7 +147,7 @@ func TestServer_SetEmail(t *testing.T) { func TestServer_ResendEmailCode(t *testing.T) { userID := Instance.CreateHumanUser(CTX).GetUserId() - verifiedUserID := Instance.CreateHumanUserVerified(CTX, Instance.DefaultOrg.Id, gofakeit.Email()).GetUserId() + verifiedUserID := Instance.CreateHumanUserVerified(CTX, Instance.DefaultOrg.Id, gofakeit.Email(), gofakeit.Phone()).GetUserId() tests := []struct { name string @@ -251,7 +251,7 @@ func TestServer_ResendEmailCode(t *testing.T) { func TestServer_SendEmailCode(t *testing.T) { userID := Instance.CreateHumanUser(CTX).GetUserId() - verifiedUserID := Instance.CreateHumanUserVerified(CTX, Instance.DefaultOrg.Id, gofakeit.Email()).GetUserId() + verifiedUserID := Instance.CreateHumanUserVerified(CTX, Instance.DefaultOrg.Id, gofakeit.Email(), gofakeit.Phone()).GetUserId() tests := []struct { name string diff --git a/internal/api/grpc/user/v2/integration_test/idp_link_test.go b/internal/api/grpc/user/v2/integration_test/idp_link_test.go index 116a095216..9d8160ab74 100644 --- a/internal/api/grpc/user/v2/integration_test/idp_link_test.go +++ b/internal/api/grpc/user/v2/integration_test/idp_link_test.go @@ -102,17 +102,17 @@ func TestServer_ListIDPLinks(t *testing.T) { orgResp := Instance.CreateOrganization(IamCTX, fmt.Sprintf("ListIDPLinks-%s", gofakeit.AppName()), gofakeit.Email()) instanceIdpResp := Instance.AddGenericOAuthProvider(IamCTX, Instance.DefaultOrg.Id) - userInstanceResp := Instance.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, gofakeit.Email()) + userInstanceResp := Instance.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, gofakeit.Email(), gofakeit.Phone()) _, err := Instance.CreateUserIDPlink(IamCTX, userInstanceResp.GetUserId(), "external_instance", instanceIdpResp.Id, "externalUsername_instance") require.NoError(t, err) ctxOrg := metadata.AppendToOutgoingContext(IamCTX, "x-zitadel-orgid", orgResp.GetOrganizationId()) orgIdpResp := Instance.AddOrgGenericOAuthProvider(ctxOrg, orgResp.OrganizationId) - userOrgResp := Instance.CreateHumanUserVerified(ctxOrg, orgResp.OrganizationId, gofakeit.Email()) + userOrgResp := Instance.CreateHumanUserVerified(ctxOrg, orgResp.OrganizationId, gofakeit.Email(), gofakeit.Phone()) _, err = Instance.CreateUserIDPlink(ctxOrg, userOrgResp.GetUserId(), "external_org", orgIdpResp.Id, "externalUsername_org") require.NoError(t, err) - userMultipleResp := Instance.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, gofakeit.Email()) + userMultipleResp := Instance.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, gofakeit.Email(), gofakeit.Phone()) _, err = Instance.CreateUserIDPlink(IamCTX, userMultipleResp.GetUserId(), "external_multi", instanceIdpResp.Id, "externalUsername_multi") require.NoError(t, err) _, err = Instance.CreateUserIDPlink(ctxOrg, userMultipleResp.GetUserId(), "external_multi", orgIdpResp.Id, "externalUsername_multi") @@ -256,17 +256,17 @@ func TestServer_RemoveIDPLink(t *testing.T) { orgResp := Instance.CreateOrganization(IamCTX, fmt.Sprintf("ListIDPLinks-%s", gofakeit.AppName()), gofakeit.Email()) instanceIdpResp := Instance.AddGenericOAuthProvider(IamCTX, Instance.DefaultOrg.Id) - userInstanceResp := Instance.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, gofakeit.Email()) + userInstanceResp := Instance.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, gofakeit.Email(), gofakeit.Phone()) _, err := Instance.CreateUserIDPlink(IamCTX, userInstanceResp.GetUserId(), "external_instance", instanceIdpResp.Id, "externalUsername_instance") require.NoError(t, err) ctxOrg := metadata.AppendToOutgoingContext(IamCTX, "x-zitadel-orgid", orgResp.GetOrganizationId()) orgIdpResp := Instance.AddOrgGenericOAuthProvider(ctxOrg, orgResp.OrganizationId) - userOrgResp := Instance.CreateHumanUserVerified(ctxOrg, orgResp.OrganizationId, gofakeit.Email()) + userOrgResp := Instance.CreateHumanUserVerified(ctxOrg, orgResp.OrganizationId, gofakeit.Email(), gofakeit.Phone()) _, err = Instance.CreateUserIDPlink(ctxOrg, userOrgResp.GetUserId(), "external_org", orgIdpResp.Id, "externalUsername_org") require.NoError(t, err) - userNoLinkResp := Instance.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, gofakeit.Email()) + userNoLinkResp := Instance.CreateHumanUserVerified(IamCTX, orgResp.OrganizationId, gofakeit.Email(), gofakeit.Phone()) type args struct { ctx context.Context diff --git a/internal/api/grpc/user/v2/integration_test/phone_test.go b/internal/api/grpc/user/v2/integration_test/phone_test.go index 1c1f75854d..49050c5fe6 100644 --- a/internal/api/grpc/user/v2/integration_test/phone_test.go +++ b/internal/api/grpc/user/v2/integration_test/phone_test.go @@ -123,7 +123,7 @@ func TestServer_SetPhone(t *testing.T) { func TestServer_ResendPhoneCode(t *testing.T) { userID := Instance.CreateHumanUser(CTX).GetUserId() - verifiedUserID := Instance.CreateHumanUserVerified(CTX, Instance.DefaultOrg.Id, gofakeit.Email()).GetUserId() + verifiedUserID := Instance.CreateHumanUserVerified(CTX, Instance.DefaultOrg.Id, gofakeit.Email(), gofakeit.Phone()).GetUserId() tests := []struct { name string diff --git a/internal/api/grpc/user/v2/integration_test/query_test.go b/internal/api/grpc/user/v2/integration_test/query_test.go index a00d1b1a48..2551a4a833 100644 --- a/internal/api/grpc/user/v2/integration_test/query_test.go +++ b/internal/api/grpc/user/v2/integration_test/query_test.go @@ -5,6 +5,7 @@ package user_test import ( "context" "fmt" + "slices" "testing" "time" @@ -24,7 +25,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 @@ -39,8 +40,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, @@ -52,8 +53,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, @@ -63,10 +64,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{ @@ -90,7 +91,6 @@ func TestServer_GetUserByID(t *testing.T) { IsVerified: true, }, Phone: &user.HumanPhone{ - Phone: "+41791234567", IsVerified: true, }, }, @@ -107,11 +107,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{ @@ -135,7 +134,6 @@ func TestServer_GetUserByID(t *testing.T) { IsVerified: true, }, Phone: &user.HumanPhone{ - Phone: "+41791234567", IsVerified: true, }, PasswordChangeRequired: true, @@ -152,9 +150,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) { @@ -174,11 +170,12 @@ 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 } } - assert.Equal(ttt, tt.want.User, got.User) + assert.EqualExportedValues(ttt, tt.want.User, got.User) integration.AssertDetails(ttt, tt.want, got) }, retryDuration, tick) }) @@ -325,21 +322,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 @@ -351,11 +387,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{ @@ -371,22 +407,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{ @@ -412,7 +441,6 @@ func TestServer_ListUsers(t *testing.T) { IsVerified: true, }, Phone: &user.HumanPhone{ - Phone: "+41791234567", IsVerified: true, }, }, @@ -425,23 +453,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{ @@ -467,7 +487,6 @@ func TestServer_ListUsers(t *testing.T) { IsVerified: true, }, Phone: &user.HumanPhone{ - Phone: "+41791234567", IsVerified: true, }, PasswordChangeRequired: true, @@ -482,22 +501,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{ @@ -523,7 +535,27 @@ func TestServer_ListUsers(t *testing.T) { IsVerified: true, }, Phone: &user.HumanPhone{ - Phone: "+41791234567", + IsVerified: true, + }, + }, + }, + }, + { + 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, }, }, @@ -544,28 +576,6 @@ func TestServer_ListUsers(t *testing.T) { IsVerified: true, }, Phone: &user.HumanPhone{ - Phone: "+41791234567", - IsVerified: true, - }, - }, - }, - }, { - 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{ - Phone: "+41791234567", IsVerified: true, }, }, @@ -578,22 +588,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{ @@ -619,7 +622,6 @@ func TestServer_ListUsers(t *testing.T) { IsVerified: true, }, Phone: &user.HumanPhone{ - Phone: "+41791234567", IsVerified: true, }, }, @@ -632,20 +634,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{ @@ -671,7 +668,6 @@ func TestServer_ListUsers(t *testing.T) { IsVerified: true, }, Phone: &user.HumanPhone{ - Phone: "+41791234567", IsVerified: true, }, }, @@ -684,20 +680,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{ @@ -723,7 +714,6 @@ func TestServer_ListUsers(t *testing.T) { IsVerified: true, }, Phone: &user.HumanPhone{ - Phone: "+41791234567", IsVerified: true, }, }, @@ -744,7 +734,6 @@ func TestServer_ListUsers(t *testing.T) { IsVerified: true, }, Phone: &user.HumanPhone{ - Phone: "+41791234567", IsVerified: true, }, }, @@ -765,7 +754,6 @@ func TestServer_ListUsers(t *testing.T) { IsVerified: true, }, Phone: &user.HumanPhone{ - Phone: "+41791234567", IsVerified: true, }, }, @@ -778,14 +766,81 @@ 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{ + Details: &object.ListDetails{ + TotalResult: 0, + Timestamp: timestamppb.Now(), + }, + SortingColumn: 0, + 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.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 in emails no found, ok", + args: args{ + IamCTX, + &user.ListUsersRequest{Queries: []*user.SearchQuery{ + OrganizationIdQuery(orgResp.OrganizationId), + InUserEmailsQuery([]string{"notfound"}), + }, + }, + func(ctx context.Context, request *user.ListUsersRequest) userAttrs { + return []userAttr{} }, }, want: &user.ListUsersResponse{ @@ -801,19 +856,14 @@ func TestServer_ListUsers(t *testing.T) { 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{ @@ -839,7 +889,6 @@ func TestServer_ListUsers(t *testing.T) { IsVerified: true, }, Phone: &user.HumanPhone{ - Phone: "+41791234567", IsVerified: true, }, }, @@ -860,7 +909,6 @@ func TestServer_ListUsers(t *testing.T) { IsVerified: true, }, Phone: &user.HumanPhone{ - Phone: "+41791234567", IsVerified: true, }, }, @@ -881,7 +929,6 @@ func TestServer_ListUsers(t *testing.T) { IsVerified: true, }, Phone: &user.HumanPhone{ - Phone: "+41791234567", IsVerified: true, }, }, @@ -893,12 +940,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) { @@ -924,6 +966,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 } @@ -931,7 +974,7 @@ func TestServer_ListUsers(t *testing.T) { tt.want.Result[i].Details = 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) @@ -958,6 +1001,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{ diff --git a/internal/api/grpc/user/v2/query.go b/internal/api/grpc/user/v2/query.go index 564d4c1c0a..4cfbb46a51 100644 --- a/internal/api/grpc/user/v2/query.go +++ b/internal/api/grpc/user/v2/query.go @@ -238,6 +238,8 @@ func userQueryToQuery(query *user.SearchQuery, level uint8) (query.SearchQuery, return displayNameQueryToQuery(q.DisplayNameQuery) case *user.SearchQuery_EmailQuery: return emailQueryToQuery(q.EmailQuery) + case *user.SearchQuery_PhoneQuery: + return phoneQueryToQuery(q.PhoneQuery) case *user.SearchQuery_StateQuery: return stateQueryToQuery(q.StateQuery) case *user.SearchQuery_TypeQuery: @@ -285,6 +287,10 @@ func emailQueryToQuery(q *user.EmailQuery) (query.SearchQuery, error) { return query.NewUserEmailSearchQuery(q.EmailAddress, object.TextMethodToQuery(q.Method)) } +func phoneQueryToQuery(q *user.PhoneQuery) (query.SearchQuery, error) { + return query.NewUserPhoneSearchQuery(q.Number, object.TextMethodToQuery(q.Method)) +} + func stateQueryToQuery(q *user.StateQuery) (query.SearchQuery, error) { return query.NewUserStateSearchQuery(int32(q.State)) } diff --git a/internal/api/grpc/user/v2beta/integration_test/email_test.go b/internal/api/grpc/user/v2beta/integration_test/email_test.go index d22355978a..48957e99d9 100644 --- a/internal/api/grpc/user/v2beta/integration_test/email_test.go +++ b/internal/api/grpc/user/v2beta/integration_test/email_test.go @@ -147,7 +147,7 @@ func TestServer_SetEmail(t *testing.T) { func TestServer_ResendEmailCode(t *testing.T) { userID := Instance.CreateHumanUser(CTX).GetUserId() - verifiedUserID := Instance.CreateHumanUserVerified(CTX, Instance.DefaultOrg.Id, gofakeit.Email()).GetUserId() + verifiedUserID := Instance.CreateHumanUserVerified(CTX, Instance.DefaultOrg.Id, gofakeit.Email(), gofakeit.Phone()).GetUserId() tests := []struct { name string diff --git a/internal/api/grpc/user/v2beta/integration_test/phone_test.go b/internal/api/grpc/user/v2beta/integration_test/phone_test.go index cd7199dcea..73d065231c 100644 --- a/internal/api/grpc/user/v2beta/integration_test/phone_test.go +++ b/internal/api/grpc/user/v2beta/integration_test/phone_test.go @@ -125,7 +125,7 @@ func TestServer_SetPhone(t *testing.T) { func TestServer_ResendPhoneCode(t *testing.T) { userID := Instance.CreateHumanUser(CTX).GetUserId() - verifiedUserID := Instance.CreateHumanUserVerified(CTX, Instance.DefaultOrg.Id, gofakeit.Email()).GetUserId() + verifiedUserID := Instance.CreateHumanUserVerified(CTX, Instance.DefaultOrg.Id, gofakeit.Email(), gofakeit.Phone()).GetUserId() tests := []struct { name string diff --git a/internal/api/grpc/user/v2beta/integration_test/query_test.go b/internal/api/grpc/user/v2beta/integration_test/query_test.go index fc1d71926e..67fc609212 100644 --- a/internal/api/grpc/user/v2beta/integration_test/query_test.go +++ b/internal/api/grpc/user/v2beta/integration_test/query_test.go @@ -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{ diff --git a/internal/api/grpc/user/v2beta/query.go b/internal/api/grpc/user/v2beta/query.go index 4567259d15..e3602abc33 100644 --- a/internal/api/grpc/user/v2beta/query.go +++ b/internal/api/grpc/user/v2beta/query.go @@ -238,6 +238,8 @@ func userQueryToQuery(query *user.SearchQuery, level uint8) (query.SearchQuery, return displayNameQueryToQuery(q.DisplayNameQuery) case *user.SearchQuery_EmailQuery: return emailQueryToQuery(q.EmailQuery) + case *user.SearchQuery_PhoneQuery: + return phoneQueryToQuery(q.PhoneQuery) case *user.SearchQuery_StateQuery: return stateQueryToQuery(q.StateQuery) case *user.SearchQuery_TypeQuery: @@ -285,6 +287,10 @@ func emailQueryToQuery(q *user.EmailQuery) (query.SearchQuery, error) { return query.NewUserEmailSearchQuery(q.EmailAddress, object.TextMethodToQuery(q.Method)) } +func phoneQueryToQuery(q *user.PhoneQuery) (query.SearchQuery, error) { + return query.NewUserPhoneSearchQuery(q.Number, object.TextMethodToQuery(q.Method)) +} + func stateQueryToQuery(q *user.StateQuery) (query.SearchQuery, error) { return query.NewUserStateSearchQuery(int32(q.State)) } diff --git a/internal/integration/client.go b/internal/integration/client.go index 34d4302ef4..c2297f7a09 100644 --- a/internal/integration/client.go +++ b/internal/integration/client.go @@ -271,7 +271,7 @@ func (i *Instance) CreateOrganizationWithUserID(ctx context.Context, name, userI return resp } -func (i *Instance) CreateHumanUserVerified(ctx context.Context, org, email string) *user_v2.AddHumanUserResponse { +func (i *Instance) CreateHumanUserVerified(ctx context.Context, org, email, phone string) *user_v2.AddHumanUserResponse { resp, err := i.Client.UserV2.AddHumanUser(ctx, &user_v2.AddHumanUserRequest{ Organization: &object.Organization{ Org: &object.Organization_OrgId{ @@ -292,7 +292,7 @@ func (i *Instance) CreateHumanUserVerified(ctx context.Context, org, email strin }, }, Phone: &user_v2.SetHumanPhone{ - Phone: "+41791234567", + Phone: phone, Verification: &user_v2.SetHumanPhone_IsVerified{ IsVerified: true, }, diff --git a/proto/zitadel/user/v2/query.proto b/proto/zitadel/user/v2/query.proto index 53f3446bca..71bb6dc594 100644 --- a/proto/zitadel/user/v2/query.proto +++ b/proto/zitadel/user/v2/query.proto @@ -30,6 +30,7 @@ message SearchQuery { NotQuery not_query = 13; InUserEmailsQuery in_user_emails_query = 14; OrganizationIdQuery organization_id_query = 15; + PhoneQuery phone_query = 16; } } @@ -184,6 +185,26 @@ message EmailQuery { ]; } +// Query for users with a specific phone. +message PhoneQuery { + string number = 1 [ + (validate.rules).string = {max_len: 20}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Phone number of the user" + min_length: 1; + max_length: 20; + example: "\"+41791234567\""; + } + ]; + zitadel.object.v2.TextQueryMethod method = 2 [ + (validate.rules).enum.defined_only = true, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines which text equality method is used"; + } + ]; +} + // Query for users with a specific state. message LoginNameQuery { string login_name = 1 [ diff --git a/proto/zitadel/user/v2beta/query.proto b/proto/zitadel/user/v2beta/query.proto index e339cdde71..caf02df747 100644 --- a/proto/zitadel/user/v2beta/query.proto +++ b/proto/zitadel/user/v2beta/query.proto @@ -30,6 +30,7 @@ message SearchQuery { NotQuery not_query = 13; InUserEmailsQuery in_user_emails_query = 14; OrganizationIdQuery organization_id_query = 15; + PhoneQuery phone_query = 16; } } @@ -184,6 +185,26 @@ message EmailQuery { ]; } +// Query for users with a specific phone. +message PhoneQuery { + string number = 1 [ + (validate.rules).string = {max_len: 20}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "Phone number of the user" + min_length: 1; + max_length: 20; + example: "\"+41791234567\""; + } + ]; + zitadel.object.v2beta.TextQueryMethod method = 2 [ + (validate.rules).enum.defined_only = true, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "defines which text equality method is used"; + } + ]; +} + // Query for users with a specific state. message LoginNameQuery { string login_name = 1 [