Add convert method for ListUsersByMetadata request

This commit is contained in:
Marco Ardizzone
2025-08-01 12:41:45 +02:00
parent 1182cdcdd9
commit e633ef87b3
5 changed files with 579 additions and 0 deletions

View File

@@ -49,6 +49,17 @@ func TimestampMethodPbToQuery(method filter.TimestampFilterMethod) query.Timesta
}
}
func ByteMethodPbToQuery(method filter.ByteFilterMethod) query.BytesComparison {
switch method {
case filter.ByteFilterMethod_BYTE_FILTER_METHOD_EQUALS:
return query.BytesEquals
case filter.ByteFilterMethod_BYTE_FILTER_METHOD_NOT_EQUALS:
return query.BytesNotEquals
default:
return -1
}
}
func PaginationPbToQuery(defaults systemdefaults.SystemDefaults, query *filter.PaginationRequest) (offset, limit uint64, asc bool, err error) {
limit = defaults.DefaultQueryLimit
if query == nil {

View File

@@ -0,0 +1,164 @@
package filter
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/config/systemdefaults"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/pkg/grpc/filter/v2"
)
func TestTextMethodPbToQuery(t *testing.T) {
t.Parallel()
tt := []struct {
name string
input filter.TextFilterMethod
output query.TextComparison
}{
{"Equals", filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS, query.TextEquals},
{"EqualsIgnoreCase", filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS_IGNORE_CASE, query.TextEqualsIgnoreCase},
{"StartsWith", filter.TextFilterMethod_TEXT_FILTER_METHOD_STARTS_WITH, query.TextStartsWith},
{"StartsWithIgnoreCase", filter.TextFilterMethod_TEXT_FILTER_METHOD_STARTS_WITH_IGNORE_CASE, query.TextStartsWithIgnoreCase},
{"Contains", filter.TextFilterMethod_TEXT_FILTER_METHOD_CONTAINS, query.TextContains},
{"ContainsIgnoreCase", filter.TextFilterMethod_TEXT_FILTER_METHOD_CONTAINS_IGNORE_CASE, query.TextContainsIgnoreCase},
{"EndsWith", filter.TextFilterMethod_TEXT_FILTER_METHOD_ENDS_WITH, query.TextEndsWith},
{"EndsWithIgnoreCase", filter.TextFilterMethod_TEXT_FILTER_METHOD_ENDS_WITH_IGNORE_CASE, query.TextEndsWithIgnoreCase},
{"Unknown", filter.TextFilterMethod(999), -1},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got := TextMethodPbToQuery(tc.input)
assert.Equal(t, tc.output, got)
})
}
}
func TestTimestampMethodPbToQuery(t *testing.T) {
t.Parallel()
tt := []struct {
name string
input filter.TimestampFilterMethod
output query.TimestampComparison
}{
{"Equals", filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_EQUALS, query.TimestampEquals},
{"Before", filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_BEFORE, query.TimestampLess},
{"After", filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_AFTER, query.TimestampGreater},
{"BeforeOrEquals", filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_BEFORE_OR_EQUALS, query.TimestampLessOrEquals},
{"AfterOrEquals", filter.TimestampFilterMethod_TIMESTAMP_FILTER_METHOD_AFTER_OR_EQUALS, query.TimestampGreaterOrEquals},
{"Unknown", filter.TimestampFilterMethod(999), -1},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got := TimestampMethodPbToQuery(tc.input)
assert.Equal(t, tc.output, got)
})
}
}
func TestByteMethodPbToQuery(t *testing.T) {
t.Parallel()
tt := []struct {
name string
input filter.ByteFilterMethod
output query.BytesComparison
}{
{"Equals", filter.ByteFilterMethod_BYTE_FILTER_METHOD_EQUALS, query.BytesEquals},
{"NotEquals", filter.ByteFilterMethod_BYTE_FILTER_METHOD_NOT_EQUALS, query.BytesNotEquals},
{"Unknown", filter.ByteFilterMethod(999), -1},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got := ByteMethodPbToQuery(tc.input)
assert.Equal(t, tc.output, got)
})
}
}
func TestPaginationPbToQuery(t *testing.T) {
t.Parallel()
defaults := systemdefaults.SystemDefaults{
DefaultQueryLimit: 10,
MaxQueryLimit: 100,
}
tt := []struct {
name string
query *filter.PaginationRequest
wantOff uint64
wantLim uint64
wantAsc bool
wantErr bool
}{
{
name: "nil query",
query: nil,
wantOff: 0,
wantLim: 10,
wantAsc: false,
wantErr: false,
},
{
name: "limit not set",
query: &filter.PaginationRequest{Offset: 5, Limit: 0, Asc: true},
wantOff: 5,
wantLim: 10,
wantAsc: true,
wantErr: false,
},
{
name: "limit set below max",
query: &filter.PaginationRequest{Offset: 2, Limit: 50, Asc: false},
wantOff: 2,
wantLim: 50,
wantAsc: false,
wantErr: false,
},
{
name: "limit exceeds max",
query: &filter.PaginationRequest{Offset: 1, Limit: 101, Asc: true},
wantOff: 0,
wantLim: 0,
wantAsc: false,
wantErr: true,
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
off, lim, asc, err := PaginationPbToQuery(defaults, tc.query)
require.Equal(t, tc.wantErr, err != nil)
assert.Equal(t, tc.wantOff, off)
assert.Equal(t, tc.wantLim, lim)
assert.Equal(t, tc.wantAsc, asc)
})
}
}
func TestQueryToPaginationPb(t *testing.T) {
t.Parallel()
req := query.SearchRequest{Limit: 20}
resp := query.SearchResponse{Count: 123}
got := QueryToPaginationPb(req, resp)
assert.Equal(t, req.Limit, got.AppliedLimit)
assert.Equal(t, resp.Count, got.TotalResult)
}

View File

@@ -0,0 +1,115 @@
package convert
import (
"github.com/zitadel/zitadel/internal/api/grpc/filter/v2"
"github.com/zitadel/zitadel/internal/config/systemdefaults"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
metadata "github.com/zitadel/zitadel/pkg/grpc/metadata/v2"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)
func ListUsersByMetadataRequestToModel(req *user.ListUsersByMetadataRequest, sysDefaults systemdefaults.SystemDefaults) (*query.UserSearchQueries, error) {
offset, limit, asc, err := filter.PaginationPbToQuery(sysDefaults, req.GetPagination())
if err != nil {
return nil, err
}
queries, err := usersByMetadataQueries(req.GetFilters(), 0)
if err != nil {
return nil, err
}
return &query.UserSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: usersByMetadataSorting(req.GetSortingColumn()),
},
Queries: queries,
}, nil
}
func usersByMetadataSorting(sortingColumn user.UsersByMetadataSorting) query.Column {
switch sortingColumn {
case user.UsersByMetadataSorting_USERS_BY_METADATA_SORT_BY_DISPLAY_NAME:
return query.HumanDisplayNameCol
case user.UsersByMetadataSorting_USERS_BY_METADATA_SORT_BY_EMAIL:
return query.HumanEmailCol
case user.UsersByMetadataSorting_USERS_BY_METADATA_SORT_BY_FIRST_NAME:
return query.HumanFirstNameCol
case user.UsersByMetadataSorting_USERS_BY_METADATA_SORT_BY_LAST_NAME:
return query.HumanLastNameCol
case user.UsersByMetadataSorting_USERS_BY_METADATA_SORT_BY_METADATA_KEY:
return query.UserMetadataKeyCol
case user.UsersByMetadataSorting_USERS_BY_METADATA_SORT_BY_METADATA_VALUE:
return query.UserMetadataValueCol
case user.UsersByMetadataSorting_USERS_BY_METADATA_SORT_BY_NICK_NAME:
return query.HumanNickNameCol
case user.UsersByMetadataSorting_USERS_BY_METADATA_SORT_BY_STATE:
return query.UserStateCol
case user.UsersByMetadataSorting_USERS_BY_METADATA_SORT_BY_TYPE:
return query.UserTypeCol
case user.UsersByMetadataSorting_USERS_BY_METADATA_SORT_BY_USER_NAME:
return query.UserUsernameCol
case user.UsersByMetadataSorting_USERS_BY_METADATA_SORT_BY_USER_ID, user.UsersByMetadataSorting_USERS_BY_METADATA_SORT_BY_UNSPECIFIED:
fallthrough
default:
return query.UserIDCol
}
}
func usersByMetadataQueries(queries []*metadata.UserByMetadataSearchFilter, nesting uint) ([]query.SearchQuery, error) {
toReturn := make([]query.SearchQuery, len(queries))
for i, query := range queries {
res, err := userByMetadataQuery(query, nesting)
if err != nil {
return nil, err
}
toReturn[i] = res
}
return toReturn, nil
}
func userByMetadataQuery(q *metadata.UserByMetadataSearchFilter, nesting uint) (query.SearchQuery, error) {
if nesting > 20 {
return nil, zerrors.ThrowInvalidArgument(nil, "CONV-Jhaltm", "Errors.Query.TooManyNestingLevels")
}
switch t := q.GetFilter().(type) {
case *metadata.UserByMetadataSearchFilter_KeyFilter:
return query.NewUserMetadataKeySearchQuery(t.KeyFilter.GetKey(), filter.TextMethodPbToQuery(t.KeyFilter.GetMethod()))
case *metadata.UserByMetadataSearchFilter_ValueFilter:
return query.NewUserMetadataValueSearchQuery(t.ValueFilter.GetValue(), filter.ByteMethodPbToQuery(t.ValueFilter.GetMethod()))
case *metadata.UserByMetadataSearchFilter_AndFilter:
mappedQueries, err := usersByMetadataQueries(t.AndFilter.GetQueries(), nesting+1)
if err != nil {
return nil, err
}
return query.NewUserMetadataAndSearchQuery(mappedQueries)
case *metadata.UserByMetadataSearchFilter_OrFilter:
mappedQueries, err := usersByMetadataQueries(t.OrFilter.GetQueries(), nesting+1)
if err != nil {
return nil, err
}
return query.NewUserMetadataOrSearchQuery(mappedQueries)
case *metadata.UserByMetadataSearchFilter_NotFilter:
mappedQuery, err := userByMetadataQuery(t.NotFilter.GetQuery(), nesting+1)
if err != nil {
return nil, err
}
return query.NewUserMetadataNotSearchQuery(mappedQuery)
default:
return nil, zerrors.ThrowInvalidArgument(nil, "CONV-GG1Jnh", "List.Query.Invalid")
}
}

View File

@@ -0,0 +1,273 @@
package convert
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/config/systemdefaults"
"github.com/zitadel/zitadel/internal/query"
filter "github.com/zitadel/zitadel/pkg/grpc/filter/v2"
metadata "github.com/zitadel/zitadel/pkg/grpc/metadata/v2"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2beta"
)
func Test_usersByMetadataSorting(t *testing.T) {
t.Parallel()
tt := []struct {
name string
input user.UsersByMetadataSorting
expected query.Column
}{
{"DisplayName", user.UsersByMetadataSorting_USERS_BY_METADATA_SORT_BY_DISPLAY_NAME, query.HumanDisplayNameCol},
{"Email", user.UsersByMetadataSorting_USERS_BY_METADATA_SORT_BY_EMAIL, query.HumanEmailCol},
{"FirstName", user.UsersByMetadataSorting_USERS_BY_METADATA_SORT_BY_FIRST_NAME, query.HumanFirstNameCol},
{"LastName", user.UsersByMetadataSorting_USERS_BY_METADATA_SORT_BY_LAST_NAME, query.HumanLastNameCol},
{"MetadataKey", user.UsersByMetadataSorting_USERS_BY_METADATA_SORT_BY_METADATA_KEY, query.UserMetadataKeyCol},
{"MetadataValue", user.UsersByMetadataSorting_USERS_BY_METADATA_SORT_BY_METADATA_VALUE, query.UserMetadataValueCol},
{"NickName", user.UsersByMetadataSorting_USERS_BY_METADATA_SORT_BY_NICK_NAME, query.HumanNickNameCol},
{"State", user.UsersByMetadataSorting_USERS_BY_METADATA_SORT_BY_STATE, query.UserStateCol},
{"Type", user.UsersByMetadataSorting_USERS_BY_METADATA_SORT_BY_TYPE, query.UserTypeCol},
{"UserName", user.UsersByMetadataSorting_USERS_BY_METADATA_SORT_BY_USER_NAME, query.UserUsernameCol},
{"UserID", user.UsersByMetadataSorting_USERS_BY_METADATA_SORT_BY_USER_ID, query.UserIDCol},
{"Unspecified", user.UsersByMetadataSorting_USERS_BY_METADATA_SORT_BY_UNSPECIFIED, query.UserIDCol},
{"Default", user.UsersByMetadataSorting(999), query.UserIDCol},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got := usersByMetadataSorting(tc.input)
assert.Equal(t, tc.expected, got)
})
}
}
func Test_userByMetadataQuery(t *testing.T) {
t.Parallel()
tt := []struct {
name string
input *metadata.UserByMetadataSearchFilter
wantErr bool
}{
{
name: "KeyFilter",
input: &metadata.UserByMetadataSearchFilter{
Filter: &metadata.UserByMetadataSearchFilter_KeyFilter{
KeyFilter: &metadata.MetadataKeyFilter{
Key: "foo",
Method: filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS,
},
},
},
wantErr: false,
},
{
name: "ValueFilter",
input: &metadata.UserByMetadataSearchFilter{
Filter: &metadata.UserByMetadataSearchFilter_ValueFilter{
ValueFilter: &metadata.MetadataValueFilter{
Value: []byte("bar"),
Method: filter.ByteFilterMethod_BYTE_FILTER_METHOD_EQUALS,
},
},
},
wantErr: false,
},
{
name: "AndFilter",
input: &metadata.UserByMetadataSearchFilter{
Filter: &metadata.UserByMetadataSearchFilter_AndFilter{
AndFilter: &metadata.MetadataAndFilter{
Queries: []*metadata.UserByMetadataSearchFilter{
{
Filter: &metadata.UserByMetadataSearchFilter_KeyFilter{
KeyFilter: &metadata.MetadataKeyFilter{
Key: "foo",
Method: filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS,
},
},
},
},
},
},
},
wantErr: false,
},
{
name: "OrFilter",
input: &metadata.UserByMetadataSearchFilter{
Filter: &metadata.UserByMetadataSearchFilter_OrFilter{
OrFilter: &metadata.MetadataOrFilter{
Queries: []*metadata.UserByMetadataSearchFilter{
{
Filter: &metadata.UserByMetadataSearchFilter_ValueFilter{
ValueFilter: &metadata.MetadataValueFilter{
Value: []byte("baz"),
Method: filter.ByteFilterMethod_BYTE_FILTER_METHOD_EQUALS,
},
},
},
},
},
},
},
wantErr: false,
},
{
name: "NotFilter",
input: &metadata.UserByMetadataSearchFilter{
Filter: &metadata.UserByMetadataSearchFilter_NotFilter{
NotFilter: &metadata.MetadataNotFilter{
Query: &metadata.UserByMetadataSearchFilter{
Filter: &metadata.UserByMetadataSearchFilter_KeyFilter{
KeyFilter: &metadata.MetadataKeyFilter{
Key: "not",
Method: filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS,
},
},
},
},
},
},
wantErr: false,
},
{
name: "TooManyNestingLevels",
input: &metadata.UserByMetadataSearchFilter{
Filter: &metadata.UserByMetadataSearchFilter_AndFilter{
AndFilter: &metadata.MetadataAndFilter{
Queries: []*metadata.UserByMetadataSearchFilter{},
},
},
},
wantErr: true,
},
{
name: "InvalidFilter",
input: &metadata.UserByMetadataSearchFilter{},
wantErr: true,
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
nesting := uint(0)
if tc.name == "TooManyNestingLevels" {
nesting = 21
}
got, err := userByMetadataQuery(tc.input, nesting)
if tc.wantErr {
require.Error(t, err)
assert.Nil(t, got)
} else {
require.NoError(t, err)
assert.NotNil(t, got)
}
})
}
}
func Test_usersByMetadataQueries(t *testing.T) {
t.Parallel()
t.Run("empty", func(t *testing.T) {
t.Parallel()
queries, err := usersByMetadataQueries([]*metadata.UserByMetadataSearchFilter{}, 0)
require.NoError(t, err)
assert.Len(t, queries, 0)
})
t.Run("single valid", func(t *testing.T) {
t.Parallel()
queries, err := usersByMetadataQueries([]*metadata.UserByMetadataSearchFilter{
{
Filter: &metadata.UserByMetadataSearchFilter_KeyFilter{
KeyFilter: &metadata.MetadataKeyFilter{
Key: "foo",
Method: filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS,
},
},
},
}, 0)
require.NoError(t, err)
assert.Len(t, queries, 1)
})
t.Run("invalid filter", func(t *testing.T) {
t.Parallel()
queries, err := usersByMetadataQueries([]*metadata.UserByMetadataSearchFilter{
{},
}, 0)
require.Error(t, err)
assert.Nil(t, queries)
})
}
func Test_ListUsersByMetadataRequestToModel(t *testing.T) {
t.Parallel()
sysDefaults := systemdefaults.SystemDefaults{
MaxQueryLimit: 100,
}
t.Run("valid", func(t *testing.T) {
t.Parallel()
req := &user.ListUsersByMetadataRequest{
Pagination: &filter.PaginationRequest{
Limit: 10,
Offset: 5,
Asc: true,
},
SortingColumn: user.UsersByMetadataSorting_USERS_BY_METADATA_SORT_BY_EMAIL,
Filters: []*metadata.UserByMetadataSearchFilter{
{
Filter: &metadata.UserByMetadataSearchFilter_KeyFilter{
KeyFilter: &metadata.MetadataKeyFilter{
Key: "foo",
Method: filter.TextFilterMethod_TEXT_FILTER_METHOD_EQUALS,
},
},
},
},
}
model, err := ListUsersByMetadataRequestToModel(req, sysDefaults)
require.NoError(t, err)
assert.NotNil(t, model)
assert.EqualValues(t, 5, model.Offset)
assert.EqualValues(t, 10, model.Limit)
assert.True(t, model.Asc)
assert.Equal(t, query.HumanEmailCol, model.SortingColumn)
assert.Len(t, model.Queries, 1)
})
t.Run("invalid pagination", func(t *testing.T) {
t.Parallel()
req := &user.ListUsersByMetadataRequest{
Pagination: &filter.PaginationRequest{
Limit: 200,
},
Filters: []*metadata.UserByMetadataSearchFilter{},
}
model, err := ListUsersByMetadataRequestToModel(req, sysDefaults)
require.Error(t, err)
assert.Nil(t, model)
})
t.Run("invalid filter", func(t *testing.T) {
t.Parallel()
req := &user.ListUsersByMetadataRequest{
Pagination: &filter.PaginationRequest{
Limit: 1,
},
Filters: []*metadata.UserByMetadataSearchFilter{
{},
},
}
model, err := ListUsersByMetadataRequestToModel(req, sysDefaults)
require.Error(t, err)
assert.Nil(t, model)
})
}