From 74f70ca873fa516e19eeb379dccd0f1fc8fba73b Mon Sep 17 00:00:00 2001
From: Earl Warren <contact@earl-warren.org>
Date: Sun, 10 Sep 2023 16:28:52 +0200
Subject: [PATCH] [GITEA] enable system users search via the API

Refs: https://codeberg.org/forgejo/forgejo/issues/1403
(cherry picked from commit 87bd40411e3af7eefce55e2a05475a8b366caa6f)

Conflicts:
	routers/api/v1/user/user.go
	https://codeberg.org/forgejo/forgejo/pulls/1469
---
 models/user/user_system.go                |  4 ++-
 routers/api/v1/user/user.go               | 38 ++++++++++++++++-------
 tests/integration/api_user_search_test.go | 22 +++++++++++++
 3 files changed, 51 insertions(+), 13 deletions(-)

diff --git a/models/user/user_system.go b/models/user/user_system.go
index f54f4e3ffb..ecf235abc3 100644
--- a/models/user/user_system.go
+++ b/models/user/user_system.go
@@ -9,10 +9,12 @@ import (
 	"code.gitea.io/gitea/modules/structs"
 )
 
+const GhostUserID = -1
+
 // NewGhostUser creates and returns a fake user for someone has deleted their account.
 func NewGhostUser() *User {
 	return &User{
-		ID:        -1,
+		ID:        GhostUserID,
 		Name:      "Ghost",
 		LowerName: "ghost",
 	}
diff --git a/routers/api/v1/user/user.go b/routers/api/v1/user/user.go
index 251e7a6013..32f5e84250 100644
--- a/routers/api/v1/user/user.go
+++ b/routers/api/v1/user/user.go
@@ -54,19 +54,33 @@ func Search(ctx *context.APIContext) {
 
 	listOptions := utils.GetListOptions(ctx)
 
-	users, maxResults, err := user_model.SearchUsers(ctx, &user_model.SearchUserOptions{
-		Actor:       ctx.Doer,
-		Keyword:     ctx.FormTrim("q"),
-		UID:         ctx.FormInt64("uid"),
-		Type:        user_model.UserTypeIndividual,
-		ListOptions: listOptions,
-	})
-	if err != nil {
-		ctx.JSON(http.StatusInternalServerError, map[string]any{
-			"ok":    false,
-			"error": err.Error(),
+	uid := ctx.FormInt64("uid")
+	var users []*user_model.User
+	var maxResults int64
+	var err error
+
+	switch uid {
+	case user_model.GhostUserID:
+		maxResults = 1
+		users = []*user_model.User{user_model.NewGhostUser()}
+	case user_model.ActionsUserID:
+		maxResults = 1
+		users = []*user_model.User{user_model.NewActionsUser()}
+	default:
+		users, maxResults, err = user_model.SearchUsers(ctx, &user_model.SearchUserOptions{
+			Actor:       ctx.Doer,
+			Keyword:     ctx.FormTrim("q"),
+			UID:         uid,
+			Type:        user_model.UserTypeIndividual,
+			ListOptions: listOptions,
 		})
-		return
+		if err != nil {
+			ctx.JSON(http.StatusInternalServerError, map[string]any{
+				"ok":    false,
+				"error": err.Error(),
+			})
+			return
+		}
 	}
 
 	ctx.SetLinkHeader(int(maxResults), listOptions.PageSize)
diff --git a/tests/integration/api_user_search_test.go b/tests/integration/api_user_search_test.go
index c5b202b319..ddfeb25234 100644
--- a/tests/integration/api_user_search_test.go
+++ b/tests/integration/api_user_search_test.go
@@ -56,6 +56,28 @@ func TestAPIUserSearchNotLoggedIn(t *testing.T) {
 	}
 }
 
+func TestAPIUserSearchSystemUsers(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+	for _, systemUser := range []*user_model.User{
+		user_model.NewGhostUser(),
+		user_model.NewActionsUser(),
+	} {
+		t.Run(systemUser.Name, func(t *testing.T) {
+			req := NewRequestf(t, "GET", "/api/v1/users/search?uid=%d", systemUser.ID)
+			resp := MakeRequest(t, req, http.StatusOK)
+
+			var results SearchResults
+			DecodeJSON(t, resp, &results)
+			assert.NotEmpty(t, results.Data)
+			if assert.EqualValues(t, 1, len(results.Data)) {
+				user := results.Data[0]
+				assert.EqualValues(t, user.UserName, systemUser.Name)
+				assert.EqualValues(t, user.ID, systemUser.ID)
+			}
+		})
+	}
+}
+
 func TestAPIUserSearchAdminLoggedInUserHidden(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 	adminUsername := "user1"