diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index 8aa67b3b0a..de54f21d94 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -436,26 +436,6 @@ func SearchUsers(ctx *context.APIContext) { listOptions := utils.GetListOptions(ctx) - sort := ctx.FormString("sort") - var orderBy db.SearchOrderBy - - switch sort { - case "oldest": - orderBy = db.SearchOrderByOldest - case "newest": - orderBy = db.SearchOrderByNewest - case "alphabetically": - orderBy = db.SearchOrderByAlphabetically - case "reversealphabetically": - orderBy = db.SearchOrderByAlphabeticallyReverse - case "recentupdate": - orderBy = db.SearchOrderByRecentUpdated - case "leastupdate": - orderBy = db.SearchOrderByLeastUpdated - default: - orderBy = db.SearchOrderByAlphabetically - } - intSource, err := strconv.ParseInt(ctx.FormString("source_id"), 10, 64) var sourceID optional.Option[int64] if ctx.FormString("source_id") == "" || err != nil { @@ -469,7 +449,7 @@ func SearchUsers(ctx *context.APIContext) { Type: user_model.UserTypeIndividual, LoginName: ctx.FormTrim("login_name"), SourceID: sourceID, - OrderBy: orderBy, + OrderBy: utils.GetDbSearchOrder(ctx), ListOptions: listOptions, }) if err != nil { diff --git a/routers/api/v1/user/user.go b/routers/api/v1/user/user.go index 5bdd56c892..19f7440047 100644 --- a/routers/api/v1/user/user.go +++ b/routers/api/v1/user/user.go @@ -33,6 +33,11 @@ func Search(ctx *context.APIContext) { // description: ID of the user to search for // type: integer // format: int64 + // - name: sort + // in: query + // description: sort order of results + // type: string + // enum: [oldest, newest, alphabetically, reversealphabetically, recentupdate, leastupdate] // - name: page // in: query // description: page number of results to return (1-based) @@ -81,6 +86,7 @@ func Search(ctx *context.APIContext) { SearchByEmail: true, Visible: visible, ListOptions: listOptions, + OrderBy: utils.GetDbSearchOrder(ctx), }) if err != nil { ctx.JSON(http.StatusInternalServerError, map[string]any{ diff --git a/routers/api/v1/utils/db_search_order.go b/routers/api/v1/utils/db_search_order.go new file mode 100644 index 0000000000..f089ba5f16 --- /dev/null +++ b/routers/api/v1/utils/db_search_order.go @@ -0,0 +1,28 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package utils + +import ( + "forgejo.org/models/db" + "forgejo.org/services/context" +) + +func GetDbSearchOrder(ctx *context.APIContext) db.SearchOrderBy { + switch ctx.FormString("sort") { + case "oldest": + return db.SearchOrderByOldest + case "newest": + return db.SearchOrderByNewest + case "alphabetically": + return db.SearchOrderByAlphabetically + case "reversealphabetically": + return db.SearchOrderByAlphabeticallyReverse + case "recentupdate": + return db.SearchOrderByRecentUpdated + case "leastupdate": + return db.SearchOrderByLeastUpdated + default: + return db.SearchOrderByAlphabetically + } +} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 7cf3d4301f..e03f5d57c1 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -20284,6 +20284,20 @@ "name": "uid", "in": "query" }, + { + "enum": [ + "oldest", + "newest", + "alphabetically", + "reversealphabetically", + "recentupdate", + "leastupdate" + ], + "type": "string", + "description": "sort order of results", + "name": "sort", + "in": "query" + }, { "type": "integer", "description": "page number of results to return (1-based)", diff --git a/tests/integration/api_user_search_test.go b/tests/integration/api_user_search_test.go index 6252c2ebde..0e8f7a123e 100644 --- a/tests/integration/api_user_search_test.go +++ b/tests/integration/api_user_search_test.go @@ -4,18 +4,25 @@ package integration import ( + "context" + "fmt" "net/http" + "strconv" "testing" + "time" auth_model "forgejo.org/models/auth" + "forgejo.org/models/db" "forgejo.org/models/unittest" user_model "forgejo.org/models/user" "forgejo.org/modules/setting" api "forgejo.org/modules/structs" "forgejo.org/modules/test" + "forgejo.org/modules/timeutil" "forgejo.org/tests" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) type SearchResults struct { @@ -179,3 +186,61 @@ func TestAPIUserSearchByEmail(t *testing.T) { assert.Len(t, results.Data, 1) assert.Equal(t, query, results.Data[0].Email) } + +func TestUsersSearchSorted(t *testing.T) { + defer tests.PrepareTestEnv(t)() + createTimestamp := time.Now().Unix() - 1000 + updateTimestamp := time.Now().Unix() - 500 + sess := db.GetEngine(context.Background()) + + for i := int64(1); i <= 10; i++ { + name := "sorttest" + strconv.Itoa(int(i)) + user := &user_model.User{ + Name: name, + LowerName: name, + LoginName: name, + Email: name + "@example.com", + Passwd: name + ".password", + Avatar: "xyz", + Type: user_model.UserTypeIndividual, + LoginType: auth_model.OAuth2, + CreatedUnix: timeutil.TimeStamp(createTimestamp - i), + UpdatedUnix: timeutil.TimeStamp(updateTimestamp - i), + } + _, err := sess.NoAutoTime().Insert(user) + require.NoError(t, err) + } + + session := loginUser(t, "user1") + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser) + + testCases := []struct { + sortType string + expectedUsers []string + }{ + {"alphabetically", []string{"sorttest1", "sorttest10", "sorttest2", "sorttest3"}}, + {"reversealphabetically", []string{"sorttest9", "sorttest8", "sorttest7", "sorttest6"}}, + {"newest", []string{"sorttest1", "sorttest2", "sorttest3", "sorttest4"}}, + {"oldest", []string{"sorttest10", "sorttest9", "sorttest8", "sorttest7"}}, + {"recentupdate", []string{"sorttest1", "sorttest2", "sorttest3", "sorttest4"}}, + {"leastupdate", []string{"sorttest10", "sorttest9", "sorttest8", "sorttest7"}}, + } + + for _, testCase := range testCases { + req := NewRequest( + t, + "GET", + fmt.Sprintf("/api/v1/users/search?q=sorttest&sort=%s&limit=4", + testCase.sortType, + ), + ).AddTokenAuth(token) + resp := session.MakeRequest(t, req, http.StatusOK) + + var results SearchResults + DecodeJSON(t, resp, &results) + assert.Len(t, results.Data, 4) + for i, searchData := range results.Data { + assert.Equalf(t, testCase.expectedUsers[i], searchData.UserName, "Sort type: %s, index %d", testCase.sortType, i) + } + } +}