Files
zitadel/internal/api/grpc/user/v2/convert/user_test.go
Marco A. df0f033880 chore: move converter methods users v2 to separate converter package + add tests (#10567)
# Which Problems Are Solved

As requested by @adlerhurst in
https://github.com/zitadel/zitadel/pull/10415#discussion_r2298087711 , I
am moving the refactoring of v2 user converter methods to a separate PR

# How the Problems Are Solved

Cherry-pick 648c234caf

# Additional Context

Parent of https://github.com/zitadel/zitadel/pull/10415

(cherry picked from commit b604615cab)
2025-08-28 09:23:04 +02:00

1026 lines
23 KiB
Go

package convert
import (
"testing"
"time"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/pkg/grpc/filter/v2"
metadata "github.com/zitadel/zitadel/pkg/grpc/metadata/v2"
"github.com/zitadel/zitadel/pkg/grpc/object/v2"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
func Test_userQueryToQuery(t *testing.T) {
t.Parallel()
tt := []struct {
name string
input *user.SearchQuery
level uint8
wantErr bool
}{
{
name: "UserNameQuery",
input: &user.SearchQuery{
Query: &user.SearchQuery_UserNameQuery{
UserNameQuery: &user.UserNameQuery{
UserName: "testuser",
Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS,
},
},
},
level: 0,
wantErr: false,
},
{
name: "FirstNameQuery",
input: &user.SearchQuery{
Query: &user.SearchQuery_FirstNameQuery{
FirstNameQuery: &user.FirstNameQuery{
FirstName: "John",
Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS,
},
},
},
level: 0,
wantErr: false,
},
{
name: "LastNameQuery",
input: &user.SearchQuery{
Query: &user.SearchQuery_LastNameQuery{
LastNameQuery: &user.LastNameQuery{
LastName: "Doe",
Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS,
},
},
},
level: 0,
wantErr: false,
},
{
name: "NickNameQuery",
input: &user.SearchQuery{
Query: &user.SearchQuery_NickNameQuery{
NickNameQuery: &user.NickNameQuery{
NickName: "JD",
Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS,
},
},
},
level: 0,
wantErr: false,
},
{
name: "DisplayNameQuery",
input: &user.SearchQuery{
Query: &user.SearchQuery_DisplayNameQuery{
DisplayNameQuery: &user.DisplayNameQuery{
DisplayName: "John Doe",
Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS,
},
},
},
level: 0,
wantErr: false,
},
{
name: "EmailQuery",
input: &user.SearchQuery{
Query: &user.SearchQuery_EmailQuery{
EmailQuery: &user.EmailQuery{
EmailAddress: "john@doe.com",
Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS,
},
},
},
level: 0,
wantErr: false,
},
{
name: "PhoneQuery",
input: &user.SearchQuery{
Query: &user.SearchQuery_PhoneQuery{
PhoneQuery: &user.PhoneQuery{
Number: "123456789",
Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS,
},
},
},
level: 0,
wantErr: false,
},
{
name: "StateQuery",
input: &user.SearchQuery{
Query: &user.SearchQuery_StateQuery{
StateQuery: &user.StateQuery{
State: user.UserState_USER_STATE_ACTIVE,
},
},
},
level: 0,
wantErr: false,
},
{
name: "TypeQuery",
input: &user.SearchQuery{
Query: &user.SearchQuery_TypeQuery{
TypeQuery: &user.TypeQuery{
Type: user.Type_TYPE_HUMAN,
},
},
},
level: 0,
wantErr: false,
},
{
name: "LoginNameQuery",
input: &user.SearchQuery{
Query: &user.SearchQuery_LoginNameQuery{
LoginNameQuery: &user.LoginNameQuery{
LoginName: "login",
Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS,
},
},
},
level: 0,
wantErr: false,
},
{
name: "OrganizationIdQuery",
input: &user.SearchQuery{
Query: &user.SearchQuery_OrganizationIdQuery{
OrganizationIdQuery: &user.OrganizationIdQuery{
OrganizationId: "org1",
},
},
},
level: 0,
wantErr: false,
},
{
name: "InUserIdsQuery",
input: &user.SearchQuery{
Query: &user.SearchQuery_InUserIdsQuery{
InUserIdsQuery: &user.InUserIDQuery{
UserIds: []string{"id1", "id2"},
},
},
},
level: 0,
wantErr: false,
},
{
name: "OrQuery",
input: &user.SearchQuery{
Query: &user.SearchQuery_OrQuery{
OrQuery: &user.OrQuery{
Queries: []*user.SearchQuery{
{
Query: &user.SearchQuery_UserNameQuery{
UserNameQuery: &user.UserNameQuery{
UserName: "testuser",
Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS,
},
},
},
},
},
},
},
level: 0,
wantErr: false,
},
{
name: "AndQuery",
input: &user.SearchQuery{
Query: &user.SearchQuery_AndQuery{
AndQuery: &user.AndQuery{
Queries: []*user.SearchQuery{
{
Query: &user.SearchQuery_UserNameQuery{
UserNameQuery: &user.UserNameQuery{
UserName: "testuser",
Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS,
},
},
},
},
},
},
},
level: 0,
wantErr: false,
},
{
name: "NotQuery",
input: &user.SearchQuery{
Query: &user.SearchQuery_NotQuery{
NotQuery: &user.NotQuery{
Query: &user.SearchQuery{
Query: &user.SearchQuery_UserNameQuery{
UserNameQuery: &user.UserNameQuery{
UserName: "testuser",
Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS,
},
},
},
},
},
},
level: 0,
wantErr: false,
},
{
name: "InUserEmailsQuery",
input: &user.SearchQuery{
Query: &user.SearchQuery_InUserEmailsQuery{
InUserEmailsQuery: &user.InUserEmailsQuery{
UserEmails: []string{"john@doe.com", "jane@doe.com"},
},
},
},
level: 0,
wantErr: false,
},
{
name: "MetadataKeyFilter",
input: &user.SearchQuery{
Query: &user.SearchQuery_MetadataKeyFilter{
MetadataKeyFilter: &metadata.MetadataKeyFilter{
Key: "key1",
Method: filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS,
},
},
},
level: 0,
wantErr: false,
},
{
name: "MetadataValueFilter",
input: &user.SearchQuery{
Query: &user.SearchQuery_MetadataValueFilter{
MetadataValueFilter: &metadata.MetadataValueFilter{
Value: []byte("value1"),
Method: filter.ByteFilterMethod_BYTE_FILTER_METHOD_EQUALS,
},
},
},
level: 0,
wantErr: false,
},
{
name: "TooManyNestingLevels",
input: &user.SearchQuery{
Query: &user.SearchQuery_UserNameQuery{
UserNameQuery: &user.UserNameQuery{
UserName: "testuser",
Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS,
},
},
},
level: 21,
wantErr: true,
},
{
name: "InvalidQueryType",
input: &user.SearchQuery{
Query: nil,
},
level: 0,
wantErr: true,
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got, err := userQueryToQuery(tc.input, tc.level)
if tc.wantErr {
assert.Error(t, err)
assert.Nil(t, got)
} else {
assert.NoError(t, err)
assert.NotNil(t, got)
}
})
}
}
func Test_userQueriesToQuery(t *testing.T) {
t.Parallel()
tt := []struct {
name string
queries []*user.SearchQuery
level uint8
wantErr bool
}{
{
name: "single valid query",
queries: []*user.SearchQuery{
{
Query: &user.SearchQuery_UserNameQuery{
UserNameQuery: &user.UserNameQuery{
UserName: "testuser",
Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS,
},
},
},
},
level: 0,
wantErr: false,
},
{
name: "multiple valid queries",
queries: []*user.SearchQuery{
{
Query: &user.SearchQuery_UserNameQuery{
UserNameQuery: &user.UserNameQuery{
UserName: "user1",
Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS,
},
},
},
{
Query: &user.SearchQuery_EmailQuery{
EmailQuery: &user.EmailQuery{
EmailAddress: "user1@example.com",
Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS,
},
},
},
},
level: 0,
wantErr: false,
},
{
name: "empty queries slice",
queries: []*user.SearchQuery{},
level: 0,
wantErr: false,
},
{
name: "query with error (too many nesting levels)",
queries: []*user.SearchQuery{
{
Query: &user.SearchQuery_UserNameQuery{
UserNameQuery: &user.UserNameQuery{
UserName: "testuser",
Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS,
},
},
},
},
level: 21,
wantErr: true,
},
{
name: "query with invalid type",
queries: []*user.SearchQuery{
{
Query: nil,
},
},
level: 0,
wantErr: true,
},
{
name: "mixed valid and invalid queries",
queries: []*user.SearchQuery{
{
Query: &user.SearchQuery_UserNameQuery{
UserNameQuery: &user.UserNameQuery{
UserName: "testuser",
Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS,
},
},
},
{
Query: nil,
},
},
level: 0,
wantErr: true,
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got, err := userQueriesToQuery(tc.queries, tc.level)
if tc.wantErr {
assert.Error(t, err)
assert.Nil(t, got)
} else {
assert.NoError(t, err)
assert.NotNil(t, got)
assert.Equal(t, len(tc.queries), len(got))
}
})
}
}
func Test_userFieldNameToSortingColumn(t *testing.T) {
t.Parallel()
type testCase struct {
name string
input user.UserFieldName
expected query.Column
}
tt := []testCase{
{
name: "EMAIL",
input: user.UserFieldName_USER_FIELD_NAME_EMAIL,
expected: query.HumanEmailCol,
},
{
name: "FIRST_NAME",
input: user.UserFieldName_USER_FIELD_NAME_FIRST_NAME,
expected: query.HumanFirstNameCol,
},
{
name: "LAST_NAME",
input: user.UserFieldName_USER_FIELD_NAME_LAST_NAME,
expected: query.HumanLastNameCol,
},
{
name: "DISPLAY_NAME",
input: user.UserFieldName_USER_FIELD_NAME_DISPLAY_NAME,
expected: query.HumanDisplayNameCol,
},
{
name: "USER_NAME",
input: user.UserFieldName_USER_FIELD_NAME_USER_NAME,
expected: query.UserUsernameCol,
},
{
name: "STATE",
input: user.UserFieldName_USER_FIELD_NAME_STATE,
expected: query.UserStateCol,
},
{
name: "TYPE",
input: user.UserFieldName_USER_FIELD_NAME_TYPE,
expected: query.UserTypeCol,
},
{
name: "NICK_NAME",
input: user.UserFieldName_USER_FIELD_NAME_NICK_NAME,
expected: query.HumanNickNameCol,
},
{
name: "CREATION_DATE",
input: user.UserFieldName_USER_FIELD_NAME_CREATION_DATE,
expected: query.UserCreationDateCol,
},
{
name: "UNSPECIFIED",
input: user.UserFieldName_USER_FIELD_NAME_UNSPECIFIED,
expected: query.UserIDCol,
},
{
name: "Unknown value (default)",
input: user.UserFieldName(999),
expected: query.UserIDCol,
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got := userFieldNameToSortingColumn(tc.input)
assert.Equal(t, tc.expected, got)
})
}
}
func Test_userStateToPb(t *testing.T) {
t.Parallel()
type testCase struct {
name string
input domain.UserState
expected user.UserState
}
tt := []testCase{
{
name: "Active",
input: domain.UserStateActive,
expected: user.UserState_USER_STATE_ACTIVE,
},
{
name: "Inactive",
input: domain.UserStateInactive,
expected: user.UserState_USER_STATE_INACTIVE,
},
{
name: "Deleted",
input: domain.UserStateDeleted,
expected: user.UserState_USER_STATE_DELETED,
},
{
name: "Initial",
input: domain.UserStateInitial,
expected: user.UserState_USER_STATE_INITIAL,
},
{
name: "Locked",
input: domain.UserStateLocked,
expected: user.UserState_USER_STATE_LOCKED,
},
{
name: "Unspecified",
input: domain.UserStateUnspecified,
expected: user.UserState_USER_STATE_UNSPECIFIED,
},
{
name: "Suspend",
input: domain.UserStateSuspend,
expected: user.UserState_USER_STATE_UNSPECIFIED,
},
{
name: "Unknown value (default)",
input: domain.UserState(999),
expected: user.UserState_USER_STATE_UNSPECIFIED,
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got := userStateToPb(tc.input)
assert.Equal(t, tc.expected, got)
})
}
}
func Test_userTypeToPb(t *testing.T) {
t.Parallel()
tt := []struct {
testName string
inputQueryUser *query.User
expectedUserType user.UserType
}{
{
testName: "when query user is nil should return nil",
expectedUserType: nil,
},
{
testName: "when query user is human should return usertype human",
inputQueryUser: &query.User{Human: &query.Human{}},
expectedUserType: &user.User_Human{
Human: &user.HumanUser{
Profile: &user.HumanProfile{
NickName: new(string),
DisplayName: new(string),
PreferredLanguage: gu.Ptr("und"),
Gender: gu.Ptr(user.Gender_GENDER_UNSPECIFIED),
},
Email: &user.HumanEmail{},
Phone: &user.HumanPhone{},
},
},
},
{
testName: "when query user is machine should return usertype machine",
inputQueryUser: &query.User{Machine: &query.Machine{}},
expectedUserType: &user.User_Machine{
Machine: &user.MachineUser{},
},
},
}
for _, tc := range tt {
t.Run(tc.testName, func(t *testing.T) {
t.Parallel()
res := userTypeToPb(tc.inputQueryUser, "asset")
assert.Equal(t, tc.expectedUserType, res)
})
}
}
func Test_UserToPb(t *testing.T) {
t.Parallel()
now := time.Now().UTC()
type args struct {
userQ *query.User
assetPrefix string
}
tt := []struct {
name string
args args
want *user.User
}{
{
name: "nil userQ returns nil",
args: args{
userQ: nil,
assetPrefix: "prefix",
},
want: nil,
},
{
name: "basic user with human type",
args: args{
userQ: &query.User{
ID: "user-id",
Sequence: 1,
ChangeDate: now,
ResourceOwner: "owner",
CreationDate: now,
State: domain.UserStateActive,
Username: "username",
LoginNames: []string{"login1", "login2"},
PreferredLoginName: "preferred",
Human: &query.Human{},
},
assetPrefix: "prefix",
},
want: &user.User{
UserId: "user-id",
Details: &object.Details{
Sequence: 1,
ChangeDate: timestamppb.New(now),
ResourceOwner: "owner",
CreationDate: timestamppb.New(now),
},
State: user.UserState_USER_STATE_ACTIVE,
Username: "username",
LoginNames: []string{"login1", "login2"},
PreferredLoginName: "preferred",
Type: &user.User_Human{
Human: humanToPb(&query.Human{}, "prefix", "owner"),
},
},
},
{
name: "basic user with machine type",
args: args{
userQ: &query.User{
ID: "user-id2",
Sequence: 2,
ChangeDate: now,
ResourceOwner: "owner2",
CreationDate: now,
State: domain.UserStateInactive,
Username: "machineuser",
LoginNames: []string{"machine1"},
PreferredLoginName: "machinepreferred",
Machine: &query.Machine{},
},
assetPrefix: "prefix2",
},
want: &user.User{
UserId: "user-id2",
Details: &object.Details{
Sequence: 2,
ChangeDate: timestamppb.New(now),
ResourceOwner: "owner2",
CreationDate: timestamppb.New(now),
},
State: user.UserState_USER_STATE_INACTIVE,
Username: "machineuser",
LoginNames: []string{"machine1"},
PreferredLoginName: "machinepreferred",
Type: &user.User_Machine{
Machine: machineToPb(&query.Machine{}),
},
},
},
{
name: "user with no type returns nil Type",
args: args{
userQ: &query.User{
ID: "user-id3",
Sequence: 3,
ChangeDate: now,
ResourceOwner: "owner3",
CreationDate: now,
State: domain.UserStateDeleted,
Username: "notypeuser",
LoginNames: []string{"notype1"},
PreferredLoginName: "notypepreferred",
},
assetPrefix: "prefix3",
},
want: &user.User{
UserId: "user-id3",
Details: &object.Details{
Sequence: 3,
ChangeDate: timestamppb.New(now),
ResourceOwner: "owner3",
CreationDate: timestamppb.New(now),
},
State: user.UserState_USER_STATE_DELETED,
Username: "notypeuser",
LoginNames: []string{"notype1"},
PreferredLoginName: "notypepreferred",
Type: nil,
},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got := UserToPb(tc.args.userQ, tc.args.assetPrefix)
assert.Equal(t, tc.want, got)
})
}
}
func Test_UsersToPb(t *testing.T) {
t.Parallel()
now := time.Now().UTC()
type args struct {
users []*query.User
assetPrefix string
}
tt := []struct {
name string
args args
want []*user.User
}{
{
name: "nil slice returns nils",
args: args{
users: nil,
assetPrefix: "prefix",
},
want: []*user.User{},
},
{
name: "empty slice returns empty slice",
args: args{
users: []*query.User{},
assetPrefix: "prefix",
},
want: []*user.User{},
},
{
name: "slice with nil user returns slice with nil",
args: args{
users: []*query.User{nil},
assetPrefix: "prefix",
},
want: []*user.User{nil},
},
{
name: "slice with one human user",
args: args{
users: []*query.User{
{
ID: "user-id",
Sequence: 1,
ChangeDate: now,
ResourceOwner: "owner",
CreationDate: now,
State: domain.UserStateActive,
Username: "username",
LoginNames: []string{"login1", "login2"},
PreferredLoginName: "preferred",
Human: &query.Human{},
},
},
assetPrefix: "prefix",
},
want: []*user.User{
{
UserId: "user-id",
Details: &object.Details{
Sequence: 1,
ChangeDate: timestamppb.New(now),
ResourceOwner: "owner",
CreationDate: timestamppb.New(now),
},
State: user.UserState_USER_STATE_ACTIVE,
Username: "username",
LoginNames: []string{"login1", "login2"},
PreferredLoginName: "preferred",
Type: &user.User_Human{
Human: humanToPb(&query.Human{}, "prefix", "owner"),
},
},
},
},
{
name: "slice with one machine user",
args: args{
users: []*query.User{
{
ID: "user-id2",
Sequence: 2,
ChangeDate: now,
ResourceOwner: "owner2",
CreationDate: now,
State: domain.UserStateInactive,
Username: "machineuser",
LoginNames: []string{"machine1"},
PreferredLoginName: "machinepreferred",
Machine: &query.Machine{},
},
},
assetPrefix: "prefix2",
},
want: []*user.User{
{
UserId: "user-id2",
Details: &object.Details{
Sequence: 2,
ChangeDate: timestamppb.New(now),
ResourceOwner: "owner2",
CreationDate: timestamppb.New(now),
},
State: user.UserState_USER_STATE_INACTIVE,
Username: "machineuser",
LoginNames: []string{"machine1"},
PreferredLoginName: "machinepreferred",
Type: &user.User_Machine{
Machine: machineToPb(&query.Machine{}),
},
},
},
},
{
name: "slice with mixed users",
args: args{
users: []*query.User{
nil,
{
ID: "user-id3",
Sequence: 3,
ChangeDate: now,
ResourceOwner: "owner3",
CreationDate: now,
State: domain.UserStateDeleted,
Username: "notypeuser",
LoginNames: []string{"notype1"},
PreferredLoginName: "notypepreferred",
},
},
assetPrefix: "prefix3",
},
want: []*user.User{
nil,
{
UserId: "user-id3",
Details: &object.Details{
Sequence: 3,
ChangeDate: timestamppb.New(now),
ResourceOwner: "owner3",
CreationDate: timestamppb.New(now),
},
State: user.UserState_USER_STATE_DELETED,
Username: "notypeuser",
LoginNames: []string{"notype1"},
PreferredLoginName: "notypepreferred",
Type: nil,
},
},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got := UsersToPb(tc.args.users, tc.args.assetPrefix)
assert.Equal(t, tc.want, got)
})
}
}
func Test_ListUsersRequestToModel(t *testing.T) {
t.Parallel()
firstNameQuery, err := query.NewUserFirstNameSearchQuery("first name", query.TextEquals)
require.Nil(t, err)
type args struct {
req *user.ListUsersRequest
}
tt := []struct {
name string
args args
want *query.UserSearchQueries
wantErr bool
}{
{
name: "valid request with one query",
args: args{
req: &user.ListUsersRequest{
Query: &object.ListQuery{
Offset: 10,
Limit: 5,
Asc: true,
},
SortingColumn: user.UserFieldName_USER_FIELD_NAME_EMAIL,
Queries: []*user.SearchQuery{
{
Query: &user.SearchQuery_FirstNameQuery{
FirstNameQuery: &user.FirstNameQuery{
FirstName: "first name",
Method: object.TextQueryMethod_TEXT_QUERY_METHOD_EQUALS,
},
},
},
},
},
},
want: &query.UserSearchQueries{
SearchRequest: query.SearchRequest{
Offset: 10,
Limit: 5,
Asc: true,
SortingColumn: query.HumanEmailCol,
},
Queries: []query.SearchQuery{firstNameQuery},
},
wantErr: false,
},
{
name: "valid request with no queries",
args: args{
req: &user.ListUsersRequest{
Query: &object.ListQuery{Offset: 0, Limit: 0, Asc: false},
SortingColumn: user.UserFieldName_USER_FIELD_NAME_UNSPECIFIED,
Queries: []*user.SearchQuery{},
},
},
want: &query.UserSearchQueries{
SearchRequest: query.SearchRequest{
Offset: 0,
Limit: 0,
Asc: false,
SortingColumn: query.UserIDCol,
},
Queries: []query.SearchQuery{},
},
wantErr: false,
},
{
name: "invalid query type returns error",
args: args{
req: &user.ListUsersRequest{
Query: &object.ListQuery{Offset: 0, Limit: 0, Asc: false},
SortingColumn: user.UserFieldName_USER_FIELD_NAME_EMAIL,
Queries: []*user.SearchQuery{
{Query: nil},
},
},
},
want: nil,
wantErr: true,
},
{
name: "nil request returns zero values",
args: args{
req: nil,
},
want: &query.UserSearchQueries{
SearchRequest: query.SearchRequest{
Offset: 0,
Limit: 0,
Asc: false,
SortingColumn: query.UserIDCol,
},
Queries: []query.SearchQuery{},
},
wantErr: false,
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got, err := ListUsersRequestToModel(tc.args.req)
if tc.wantErr {
assert.Error(t, err)
assert.Nil(t, got)
} else {
assert.NoError(t, err)
assert.Equal(t, tc.want, got)
}
})
}
}