From 6fca53f6ec390a29a11fc2f979cdfc8cf3a47ffd Mon Sep 17 00:00:00 2001 From: Zihan Dai Date: Tue, 10 Nov 2026 20:24:27 +1100 Subject: [PATCH] fix(search): stop user:me matching other usernames parseUserID checked strings.Contains(q, "user:me"), so a search for "user:melissa" (or any username that starts with "me") was treated as "user:me": it filtered by the searcher's own ID, or by no one for a visitor, and left "lissa" behind as a search word. Treat the query as "user:me" only when the username in the user: token is exactly "me". --- .../service/search_parser/search_parser.go | 7 +- .../search_parser/search_parser_test.go | 71 +++++++++++++++++++ 2 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 internal/service/search_parser/search_parser_test.go diff --git a/internal/service/search_parser/search_parser.go b/internal/service/search_parser/search_parser.go index 3e6182e15..20bbcf949 100644 --- a/internal/service/search_parser/search_parser.go +++ b/internal/service/search_parser/search_parser.go @@ -155,15 +155,16 @@ func (sp *SearchParser) parseTags(ctx context.Context, query *string) (tags [][] func (sp *SearchParser) parseUserID(ctx context.Context, query *string, currentUserID string) (userID string) { var ( exprUsername = `user:(\S+)` - exprMe = "user:me" + me = "me" q = *query ) re := regexp.MustCompile(exprUsername) res := re.FindStringSubmatch(q) - if strings.Contains(q, exprMe) { + // only "user:me" itself means the current user, not a username that starts with "me" + if len(res) > 1 && res[1] == me { userID = currentUserID - q = strings.ReplaceAll(q, exprMe, "") + q = re.ReplaceAllString(q, "") } else if len(res) > 1 { name := res[1] user, has, err := sp.userCommon.GetUserBasicInfoByUserName(ctx, name) diff --git a/internal/service/search_parser/search_parser_test.go b/internal/service/search_parser/search_parser_test.go new file mode 100644 index 000000000..5421591e8 --- /dev/null +++ b/internal/service/search_parser/search_parser_test.go @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package search_parser + +import ( + "context" + "testing" + + "github.com/apache/answer/internal/entity" + "github.com/apache/answer/internal/schema" + "github.com/apache/answer/internal/service/mock" + usercommon "github.com/apache/answer/internal/service/user_common" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" +) + +type usernameRepo struct { + usercommon.UserRepo + users map[string]*entity.User +} + +func (r *usernameRepo) GetByUsername(_ context.Context, username string) (*entity.User, bool, error) { + user, ok := r.users[username] + return user, ok, nil +} + +func TestParseStructureUser(t *testing.T) { + ctrl := gomock.NewController(t) + siteInfo := mock.NewMockSiteInfoCommonService(ctrl) + siteInfo.EXPECT().FormatAvatar(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(&schema.AvatarInfo{}).AnyTimes() + repo := &usernameRepo{users: map[string]*entity.User{ + "melissa": {ID: "2", Username: "melissa"}, + }} + sp := NewSearchParser(nil, usercommon.NewUserCommon(repo, nil, nil, siteInfo)) + + tests := []struct { + query string + wantUser string + wantWords []string + }{ + {query: "user:me", wantUser: "1"}, + {query: "user:me service", wantUser: "1", wantWords: []string{"service"}}, + {query: "user:melissa", wantUser: "2"}, + {query: "user:melissa service", wantUser: "2", wantWords: []string{"service"}}, + } + for _, tt := range tests { + t.Run(tt.query, func(t *testing.T) { + cond := sp.ParseStructure(context.Background(), &schema.SearchDTO{Query: tt.query, UserID: "1"}) + assert.Equal(t, tt.wantUser, cond.UserID) + assert.ElementsMatch(t, tt.wantWords, cond.Words) + }) + } +}