/
githubmirror
/
client
Обзор
Документация
Войти
/
githubmirror
/
client
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
go/chat/search_test.go
1 031 строка
32 KB
chrisnojima
Backport lock issue from 6.7.0 branch (#29132)
13 апр 2026, 19:11
Не верифицирован
13 апр 2026, 19:11
8504503
Код
Авторство
О чём код?
package chat import ( "context" "fmt" "testing" "time" "github.com/keybase/client/go/chat/globals" "github.com/keybase/client/go/chat/search" "github.com/keybase/client/go/kbtest" "github.com/keybase/client/go/libkb" "github.com/keybase/client/go/protocol/chat1" "github.com/keybase/client/go/protocol/gregor1" "github.com/keybase/client/go/protocol/keybase1" "github.com/keybase/client/go/protocol/stellar1" "github.com/stretchr/testify/require" ) func TestChatSearchConvRegexp(t *testing.T) { runWithMemberTypes(t, func(mt chat1.ConversationMembersType) { // Only test against IMPTEAMNATIVE. There is a bug in ChatRemoteMock // with using Pagination Next/Prev and we don't need to triple test // here. switch mt { case chat1.ConversationMembersType_IMPTEAMNATIVE: default: return } ctc := makeChatTestContext(t, "SearchRegexp", 2) defer ctc.cleanup() users := ctc.users() u1 := users[0] u2 := users[1] conv := mustCreateConversationForTest(t, ctc, u1, chat1.TopicType_CHAT, mt, ctc.as(t, u2).user()) convID := conv.Id tc1 := ctc.as(t, u1) tc2 := ctc.as(t, u2) chatUI := kbtest.NewChatUI() tc1.h.mockChatUI = chatUI listener1 := newServerChatListener() tc1.h.G().NotifyRouter.AddListener(listener1) listener2 := newServerChatListener() tc2.h.G().NotifyRouter.AddListener(listener2) sendMessage := func(msgBody chat1.MessageBody, user *kbtest.FakeUser) chat1.MessageID { msgID := mustPostLocalForTest(t, ctc, user, conv, msgBody) typ, err := msgBody.MessageType() require.NoError(t, err) consumeNewMsgRemote(t, listener1, typ) consumeNewMsgRemote(t, listener2, typ) return msgID } verifyHit := func(beforeMsgIDs []chat1.MessageID, hitMessageID chat1.MessageID, afterMsgIDs []chat1.MessageID, matches []chat1.ChatSearchMatch, searchHit chat1.ChatSearchHit, ) { _verifyHit := func(searchHit chat1.ChatSearchHit) { if beforeMsgIDs == nil { require.Nil(t, searchHit.BeforeMessages) } else { require.Equal(t, len(beforeMsgIDs), len(searchHit.BeforeMessages)) for i, msgID := range beforeMsgIDs { msg := searchHit.BeforeMessages[i] t.Logf("msg: %v", msg.Valid()) require.True(t, msg.IsValid()) require.Equal(t, msgID, msg.GetMessageID()) } } require.EqualValues(t, hitMessageID, searchHit.HitMessage.Valid().MessageID) require.Equal(t, matches, searchHit.Matches) if afterMsgIDs == nil { require.Nil(t, searchHit.AfterMessages) } else { require.Equal(t, len(afterMsgIDs), len(searchHit.AfterMessages)) for i, msgID := range afterMsgIDs { msg := searchHit.AfterMessages[i] require.True(t, msg.IsValid()) require.Equal(t, msgID, msg.GetMessageID()) } } } _verifyHit(searchHit) select { case searchHitRes := <-chatUI.SearchHitCb: _verifyHit(searchHitRes.SearchHit) case <-time.After(20 * time.Second): require.Fail(t, "no search result received") } } verifySearchDone := func(numHits int) { select { case searchDone := <-chatUI.SearchDoneCb: require.Equal(t, numHits, searchDone.NumHits) case <-time.After(20 * time.Second): require.Fail(t, "no search result received") } } runSearch := func(query string, isRegex bool, opts chat1.SearchOpts) chat1.SearchRegexpRes { opts.IsRegex = isRegex res, err := tc1.chatLocalHandler().SearchRegexp(tc1.startCtx, chat1.SearchRegexpArg{ ConvID: convID, Query: query, Opts: opts, }) require.NoError(t, err) t.Logf("query: %v, searchRes: %+v", query, res) return res } isRegex := false opts := chat1.SearchOpts{ MaxHits: 5, BeforeContext: 2, AfterContext: 2, MaxMessages: 1000, } // Test basic equality match query := "hi" msgBody := "hi @here" msgID1 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{ Body: msgBody, }), u1) searchMatch := chat1.ChatSearchMatch{ StartIndex: 0, EndIndex: 2, Match: query, } res := runSearch(query, isRegex, opts) require.Equal(t, 1, len(res.Hits)) verifyHit(nil, msgID1, nil, []chat1.ChatSearchMatch{searchMatch}, res.Hits[0]) verifySearchDone(1) // Test basic no results query = "hey" res = runSearch(query, isRegex, opts) require.Equal(t, 0, len(res.Hits)) verifySearchDone(0) // Test maxHits opts.MaxHits = 1 query = "hi" msgBody = "hi there" msgID2 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{ Body: msgBody, }), u1) res = runSearch(query, isRegex, opts) require.Equal(t, 1, len(res.Hits)) verifyHit([]chat1.MessageID{msgID1}, msgID2, nil, []chat1.ChatSearchMatch{searchMatch}, res.Hits[0]) verifySearchDone(1) opts.MaxHits = 5 res = runSearch(query, isRegex, opts) require.Equal(t, 2, len(res.Hits)) verifyHit([]chat1.MessageID{msgID1}, msgID2, nil, []chat1.ChatSearchMatch{searchMatch}, res.Hits[0]) verifyHit(nil, msgID1, []chat1.MessageID{msgID2}, []chat1.ChatSearchMatch{searchMatch}, res.Hits[1]) verifySearchDone(2) msgID3 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{ Body: msgBody, }), u1) res = runSearch(query, isRegex, opts) require.Equal(t, 3, len(res.Hits)) verifyHit([]chat1.MessageID{msgID1, msgID2}, msgID3, nil, []chat1.ChatSearchMatch{searchMatch}, res.Hits[0]) verifyHit([]chat1.MessageID{msgID1}, msgID2, []chat1.MessageID{msgID3}, []chat1.ChatSearchMatch{searchMatch}, res.Hits[1]) verifyHit(nil, msgID1, []chat1.MessageID{msgID2, msgID3}, []chat1.ChatSearchMatch{searchMatch}, res.Hits[2]) verifySearchDone(3) // test sentBy // invalid username opts.SentBy = u1.Username + "foo" res = runSearch(query, isRegex, opts) require.Zero(t, len(res.Hits)) verifySearchDone(0) // send from user2 and make sure we can filter, @mention user1 to test // SentTo later. opts.SentBy = u2.Username msgBody = fmt.Sprintf("hi @%s", u1.Username) msgID4 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{ Body: msgBody, }), u2) res = runSearch(query, isRegex, opts) require.Equal(t, 1, len(res.Hits)) verifyHit([]chat1.MessageID{msgID2, msgID3}, msgID4, nil, []chat1.ChatSearchMatch{searchMatch}, res.Hits[0]) verifySearchDone(1) opts.SentBy = "" // test sentTo // invalid username opts.SentTo = u1.Username + "foo" res = runSearch(query, isRegex, opts) require.Zero(t, len(res.Hits)) verifySearchDone(0) opts.SentTo = u1.Username res = runSearch(query, isRegex, opts) require.Equal(t, 2, len(res.Hits)) verifyHit([]chat1.MessageID{msgID2, msgID3}, msgID4, nil, []chat1.ChatSearchMatch{searchMatch}, res.Hits[0]) verifyHit(nil, msgID1, []chat1.MessageID{msgID2, msgID3}, []chat1.ChatSearchMatch{searchMatch}, res.Hits[1]) verifySearchDone(2) opts.SentTo = "" // test sentBefore/sentAfter msgRes, err := tc1.chatLocalHandler().GetMessagesLocal(tc1.startCtx, chat1.GetMessagesLocalArg{ ConversationID: convID, MessageIDs: []chat1.MessageID{msgID1, msgID4}, }) require.NoError(t, err) require.Equal(t, 2, len(msgRes.Messages)) msg1 := msgRes.Messages[0] msg4 := msgRes.Messages[1] // nothing sent after msg4 opts.SentAfter = msg4.Ctime() + 500 res = runSearch(query, isRegex, opts) require.Zero(t, len(res.Hits)) opts.SentAfter = msg1.Ctime() res = runSearch(query, isRegex, opts) require.Equal(t, 4, len(res.Hits)) // nothing sent before msg1 opts.SentAfter = 0 opts.SentBefore = msg1.Ctime() - 500 res = runSearch(query, isRegex, opts) require.Zero(t, len(res.Hits)) opts.SentBefore = msg4.Ctime() res = runSearch(query, isRegex, opts) require.Equal(t, 4, len(res.Hits)) opts.SentBefore = 0 // drain the cbs, 8 hits and 4 dones timeout := 20 * time.Second for range 8 + 4 { select { case <-chatUI.SearchHitCb: case <-chatUI.SearchDoneCb: case <-time.After(timeout): require.Fail(t, "no search result received") } } query = "edited" msgBody = "edited" searchMatch = chat1.ChatSearchMatch{ StartIndex: 0, EndIndex: len(msgBody), Match: msgBody, } mustEditMsg(tc2.startCtx, t, ctc, u2, conv, msgID4) consumeNewMsgRemote(t, listener1, chat1.MessageType_EDIT) consumeNewMsgRemote(t, listener2, chat1.MessageType_EDIT) res = runSearch(query, isRegex, opts) require.Equal(t, 1, len(res.Hits)) verifyHit([]chat1.MessageID{msgID2, msgID3}, msgID4, nil, []chat1.ChatSearchMatch{searchMatch}, res.Hits[0]) verifySearchDone(1) // Test delete mustDeleteMsg(tc2.startCtx, t, ctc, u2, conv, msgID4) consumeNewMsgRemote(t, listener1, chat1.MessageType_DELETE) consumeNewMsgRemote(t, listener2, chat1.MessageType_DELETE) res = runSearch(query, isRegex, opts) require.Equal(t, 0, len(res.Hits)) verifySearchDone(0) // Test request payment query = "payment :moneybag:" msgBody = "payment :moneybag:" searchMatch = chat1.ChatSearchMatch{ StartIndex: 0, EndIndex: len(msgBody), Match: msgBody, } msgID7 := sendMessage(chat1.NewMessageBodyWithRequestpayment(chat1.MessageRequestPayment{ RequestID: stellar1.KeybaseRequestID("dummy id"), Note: msgBody, }), u1) res = runSearch(query, isRegex, opts) require.Equal(t, 1, len(res.Hits)) verifyHit([]chat1.MessageID{msgID2, msgID3}, msgID7, nil, []chat1.ChatSearchMatch{searchMatch}, res.Hits[0]) verifySearchDone(1) // Test regex functionality isRegex = true // Test utf8 msgBody = `约书亚和约翰屌爆了` query = `约.*` searchMatch = chat1.ChatSearchMatch{ StartIndex: 0, EndIndex: len(msgBody), Match: msgBody, } msgID8 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{ Body: msgBody, }), u1) res = runSearch(query, isRegex, opts) require.Equal(t, 1, len(res.Hits)) verifyHit([]chat1.MessageID{msgID3, msgID7}, msgID8, nil, []chat1.ChatSearchMatch{searchMatch}, res.Hits[0]) verifySearchDone(1) msgBody = "hihihi" query = "hi" matches := []chat1.ChatSearchMatch{} startIndex := 0 for range 3 { matches = append(matches, chat1.ChatSearchMatch{ StartIndex: startIndex, EndIndex: startIndex + 2, Match: query, }) startIndex += 2 } opts.MaxHits = 1 msgID9 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{ Body: msgBody, }), u1) res = runSearch(query, isRegex, opts) require.Equal(t, 1, len(res.Hits)) verifyHit([]chat1.MessageID{msgID7, msgID8}, msgID9, nil, matches, res.Hits[0]) verifySearchDone(1) query = "h.*" lowercase := "abcdefghijklmnopqrstuvwxyz" for _, char := range lowercase { sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{ Body: "h." + string(char), }), u1) } opts.MaxHits = len(lowercase) res = runSearch(query, isRegex, opts) require.Equal(t, opts.MaxHits, len(res.Hits)) verifySearchDone(opts.MaxHits) // Test maxMessages opts.MaxMessages = 2 res = runSearch(query, isRegex, opts) require.Equal(t, opts.MaxMessages, len(res.Hits)) verifySearchDone(opts.MaxMessages) query = `[A-Z]*` res = runSearch(query, isRegex, opts) require.Equal(t, 0, len(res.Hits)) verifySearchDone(0) // Test invalid regex _, err = tc1.chatLocalHandler().SearchRegexp(tc1.startCtx, chat1.SearchRegexpArg{ ConvID: convID, Query: "(", Opts: chat1.SearchOpts{ IsRegex: true, }, }) require.Error(t, err) }) } func TestChatSearchRemoveMsg(t *testing.T) { useRemoteMock = false defer func() { useRemoteMock = true }() ctc := makeChatTestContext(t, "TestChatSearchRemoveMsg", 2) defer ctc.cleanup() users := ctc.users() ctx := ctc.as(t, users[0]).startCtx tc := ctc.world.Tcs[users[0].Username] chatUI := kbtest.NewChatUI() uid := gregor1.UID(users[0].GetUID().ToBytes()) ctc.as(t, users[0]).h.mockChatUI = chatUI conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, chat1.ConversationMembersType_IMPTEAMNATIVE) conv1 := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, chat1.ConversationMembersType_IMPTEAMNATIVE, users[1]) msgID0 := mustPostLocalForTest(t, ctc, users[0], conv, chat1.NewMessageBodyWithText(chat1.MessageText{ Body: "MIKEMAXIM", })) msgID1 := mustPostLocalForTest(t, ctc, users[0], conv, chat1.NewMessageBodyWithText(chat1.MessageText{ Body: "MIKEMAXIM", })) msgID2 := mustPostLocalForTest(t, ctc, users[0], conv1, chat1.NewMessageBodyWithText(chat1.MessageText{ Body: "MIKEMAXIM", })) mustPostLocalForTest(t, ctc, users[0], conv, chat1.NewMessageBodyWithText(chat1.MessageText{ Body: "CRICKETS", })) res, err := ctc.as(t, users[0]).chatLocalHandler().SearchInbox(ctx, chat1.SearchInboxArg{ Query: "MIKEM", Opts: chat1.SearchOpts{ MaxConvsHit: 5, MaxHits: 5, }, }) require.NoError(t, err) require.NotNil(t, res.Res) require.Equal(t, 2, len(res.Res.Hits)) if res.Res.Hits[0].ConvID.Eq(conv.Id) { require.Equal(t, 2, len(res.Res.Hits[0].Hits)) require.Equal(t, 1, len(res.Res.Hits[1].Hits)) } else { require.Equal(t, 1, len(res.Res.Hits[0].Hits)) require.Equal(t, 2, len(res.Res.Hits[1].Hits)) } mustDeleteMsg(ctx, t, ctc, users[0], conv1, msgID2) res, err = ctc.as(t, users[0]).chatLocalHandler().SearchInbox(ctx, chat1.SearchInboxArg{ Query: "MIKEM", Opts: chat1.SearchOpts{ MaxConvsHit: 5, MaxHits: 5, }, }) require.NoError(t, err) require.NotNil(t, res.Res) require.Equal(t, 1, len(res.Res.Hits)) require.Equal(t, 2, len(res.Res.Hits[0].Hits)) mustDeleteMsg(ctx, t, ctc, users[0], conv, msgID0) mustDeleteMsg(ctx, t, ctc, users[0], conv, msgID1) hres, err := tc.ChatG.Indexer.(*search.Indexer).GetStoreHits(ctx, uid, conv.Id, "MIKEM") require.NoError(t, err) require.Zero(t, len(hres)) } func TestChatSearchInbox(t *testing.T) { runWithMemberTypes(t, func(mt chat1.ConversationMembersType) { // Only test against IMPTEAMNATIVE. There is a bug in ChatRemoteMock // with using Pagination Next/Prev and we don't need to triple test // here. switch mt { case chat1.ConversationMembersType_IMPTEAMNATIVE: default: return } ctx := context.TODO() ctc := makeChatTestContext(t, "SearchInbox", 2) defer ctc.cleanup() users := ctc.users() u1 := users[0] u2 := users[1] tc1 := ctc.as(t, u1) tc2 := ctc.as(t, u2) g1 := ctc.world.Tcs[u1.Username].Context() g2 := ctc.world.Tcs[u2.Username].Context() uid1 := u1.User.GetUID().ToBytes() uid2 := u2.User.GetUID().ToBytes() chatUI := kbtest.NewChatUI() tc1.h.mockChatUI = chatUI listener1 := newServerChatListener() tc1.h.G().NotifyRouter.AddListener(listener1) listener2 := newServerChatListener() tc2.h.G().NotifyRouter.AddListener(listener2) // Create our own Indexer instances so we have access to non-interface methods indexer1 := search.NewIndexer(g1) consumeCh1 := make(chan chat1.ConversationID, 100) reindexCh1 := make(chan chat1.ConversationID, 100) indexer1.SetConsumeCh(consumeCh1) indexer1.SetReindexCh(reindexCh1) indexer1.SetStartSyncDelay(0) indexer1.SetUID(uid1) indexer1.SetFlushDelay(100 * time.Millisecond) indexer1.StartFlushLoop() indexer1.StartStorageLoop() // Stop the original select { case <-g1.Indexer.Stop(ctx): case <-time.After(20 * time.Second): require.Fail(t, "g1 Indexer did not stop") } g1.Indexer = indexer1 indexer2 := search.NewIndexer(g2) consumeCh2 := make(chan chat1.ConversationID, 100) reindexCh2 := make(chan chat1.ConversationID, 100) indexer2.SetConsumeCh(consumeCh2) indexer2.SetReindexCh(reindexCh2) indexer2.SetStartSyncDelay(0) indexer2.SetUID(uid2) indexer2.SetFlushDelay(10 * time.Millisecond) indexer2.StartFlushLoop() indexer2.StartStorageLoop() // Stop the original select { case <-g2.Indexer.Stop(ctx): case <-time.After(20 * time.Second): require.Fail(t, "g2 Indexer did not stop") } g2.Indexer = indexer2 conv := mustCreateConversationForTest(t, ctc, u1, chat1.TopicType_CHAT, mt, ctc.as(t, u2).user()) convID := conv.Id // verify zero messages case fi, err := indexer1.FullyIndexed(ctx, convID) require.NoError(t, err) require.True(t, fi) pi, err := indexer1.PercentIndexed(ctx, convID) require.NoError(t, err) require.Equal(t, 100, pi) fi, err = indexer2.FullyIndexed(ctx, convID) require.NoError(t, err) require.True(t, fi) pi, err = indexer2.PercentIndexed(ctx, convID) require.NoError(t, err) require.Equal(t, 100, pi) sendMessage := func(msgBody chat1.MessageBody, user *kbtest.FakeUser) chat1.MessageID { msgID := mustPostLocalForTest(t, ctc, user, conv, msgBody) typ, err := msgBody.MessageType() require.NoError(t, err) consumeNewMsgRemote(t, listener1, typ) consumeNewMsgRemote(t, listener2, typ) return msgID } verifyHit := func(_ chat1.ConversationID, beforeMsgIDs []chat1.MessageID, hitMessageID chat1.MessageID, afterMsgIDs []chat1.MessageID, matches []chat1.ChatSearchMatch, searchHit chat1.ChatSearchHit, ) { if beforeMsgIDs == nil { require.Nil(t, searchHit.BeforeMessages) } else { require.Equal(t, len(beforeMsgIDs), len(searchHit.BeforeMessages)) for i, msgID := range beforeMsgIDs { msg := searchHit.BeforeMessages[i] require.True(t, msg.IsValid()) require.Equal(t, msgID, msg.GetMessageID()) } } require.EqualValues(t, hitMessageID, searchHit.HitMessage.Valid().MessageID) require.Equal(t, matches, searchHit.Matches) if afterMsgIDs == nil { require.Nil(t, searchHit.AfterMessages) } else { require.Equal(t, len(afterMsgIDs), len(searchHit.AfterMessages)) for i, msgID := range afterMsgIDs { msg := searchHit.AfterMessages[i] require.True(t, msg.IsValid()) require.Equal(t, msgID, msg.GetMessageID()) } } } verifySearchDone := func(numHits int, delegated bool) { select { case <-chatUI.InboxSearchConvHitsCb: case <-time.After(20 * time.Second): require.Fail(t, "no name hits") } select { case searchDone := <-chatUI.InboxSearchDoneCb: require.Equal(t, numHits, searchDone.Res.NumHits) numConvs := 1 if numHits == 0 { numConvs = 0 } require.Equal(t, numConvs, searchDone.Res.NumConvs) if delegated { require.True(t, searchDone.Res.Delegated) } else { require.Equal(t, 100, searchDone.Res.PercentIndexed) } case <-time.After(20 * time.Second): require.Fail(t, "no search result received") } } verifyIndexConsumption := func(ch chan chat1.ConversationID) { select { case id := <-ch: require.Equal(t, convID, id) case <-time.After(5 * time.Second): require.Fail(t, "indexer didn't consume") } } verifyIndexNoConsumption := func(ch chan chat1.ConversationID) { select { case <-ch: require.Fail(t, "indexer reindexed") default: } } verifyIndex := func() { t.Logf("verify user 1 index") verifyIndexConsumption(consumeCh1) t.Logf("verify user 2 index") verifyIndexConsumption(consumeCh2) } runSearch := func(query string, opts chat1.SearchOpts, expectedReindex bool) *chat1.ChatSearchInboxResults { res, err := tc1.chatLocalHandler().SearchInbox(tc1.startCtx, chat1.SearchInboxArg{ Query: query, Opts: opts, }) require.NoError(t, err) t.Logf("query: %v, searchRes: %+v", query, res) if expectedReindex { verifyIndexConsumption(reindexCh1) } else { verifyIndexNoConsumption(reindexCh1) } return res.Res } opts := chat1.SearchOpts{ MaxHits: 5, BeforeContext: 2, AfterContext: 2, MaxMessages: 1000, MaxNameConvs: 1, } // Test basic equality match msgBody := "hello, byE" msgID1 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{ Body: msgBody, }), u1) queries := []string{"hello", "hello, ByE"} matches := []chat1.ChatSearchMatch{ { StartIndex: 0, EndIndex: 5, Match: "hello", }, { StartIndex: 0, EndIndex: 10, Match: "hello, byE", }, } for i, query := range queries { res := runSearch(query, opts, false /* expectedReindex */) require.Equal(t, 1, len(res.Hits)) convHit := res.Hits[0] require.Equal(t, convID, convHit.ConvID) require.Equal(t, 1, len(convHit.Hits)) verifyHit(convID, nil, msgID1, nil, []chat1.ChatSearchMatch{matches[i]}, convHit.Hits[0]) verifySearchDone(1, false) } // We get a hit but without any highlighting highlighting fails query := "hell bye" res := runSearch(query, opts, false /* expectedReindex */) require.Equal(t, 1, len(res.Hits)) convHit := res.Hits[0] verifyHit(convID, nil, msgID1, nil, nil, convHit.Hits[0]) verifySearchDone(1, false) // Test basic no results query = "hey" res = runSearch(query, opts, false /* expectedReindex*/) require.Equal(t, 0, len(res.Hits)) verifySearchDone(0, false) // Test maxHits opts.MaxHits = 1 query = "hello" searchMatch := chat1.ChatSearchMatch{ StartIndex: 0, EndIndex: len(query), Match: query, } msgID2 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{ Body: msgBody, }), u1) verifyIndex() res = runSearch(query, opts, false /* expectedReindex*/) require.Equal(t, 1, len(res.Hits)) convHit = res.Hits[0] require.Equal(t, convID, convHit.ConvID) require.Equal(t, 1, len(convHit.Hits)) verifyHit(convID, []chat1.MessageID{msgID1}, msgID2, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0]) verifySearchDone(1, false) opts.MaxHits = 5 res = runSearch(query, opts, false /* expectedReindex*/) require.Equal(t, 1, len(res.Hits)) convHit = res.Hits[0] require.Equal(t, convID, convHit.ConvID) require.Equal(t, 2, len(convHit.Hits)) verifyHit(convID, []chat1.MessageID{msgID1}, msgID2, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0]) verifyHit(convID, nil, msgID1, []chat1.MessageID{msgID2}, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[1]) verifySearchDone(2, false) msgID3 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{ Body: msgBody, }), u1) verifyIndex() res = runSearch(query, opts, false /* expectedReindex*/) require.Equal(t, 1, len(res.Hits)) convHit = res.Hits[0] require.Equal(t, convID, convHit.ConvID) require.Equal(t, 3, len(convHit.Hits)) verifyHit(convID, []chat1.MessageID{msgID1, msgID2}, msgID3, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0]) verifyHit(convID, []chat1.MessageID{msgID1}, msgID2, []chat1.MessageID{msgID3}, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[1]) verifyHit(convID, nil, msgID1, []chat1.MessageID{msgID2, msgID3}, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[2]) verifySearchDone(3, false) // test sentBy // invalid username opts.SentBy = u1.Username + "foo" res = runSearch(query, opts, false /* expectedReindex*/) require.Zero(t, len(res.Hits)) verifySearchDone(0, false) // send from user2 and make sure we can filter opts.SentBy = u2.Username msgBody = "hello" query = "hello" msgID4 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{ Body: msgBody, }), u2) verifyIndex() res = runSearch(query, opts, false /* expectedReindex*/) require.Equal(t, 1, len(res.Hits)) convHit = res.Hits[0] require.Equal(t, convID, convHit.ConvID) require.Equal(t, 1, len(convHit.Hits)) verifyHit(convID, []chat1.MessageID{msgID2, msgID3}, msgID4, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0]) verifySearchDone(1, false) opts.SentBy = "" // test sentBefore/sentAfter msgRes, err := tc1.chatLocalHandler().GetMessagesLocal(tc1.startCtx, chat1.GetMessagesLocalArg{ ConversationID: convID, MessageIDs: []chat1.MessageID{msgID1, msgID4}, }) require.NoError(t, err) require.Equal(t, 2, len(msgRes.Messages)) msg1 := msgRes.Messages[0] msg4 := msgRes.Messages[1] // nothing sent after msg4 opts.SentAfter = msg4.Ctime() + 500 res = runSearch(query, opts, false /* expectedReindex*/) require.Zero(t, len(res.Hits)) verifySearchDone(0, false) opts.SentAfter = msg1.Ctime() res = runSearch(query, opts, false /* expectedReindex*/) require.Equal(t, 1, len(res.Hits)) require.Equal(t, 4, len(res.Hits[0].Hits)) verifySearchDone(4, false) // nothing sent before msg1 opts.SentAfter = 0 opts.SentBefore = msg1.Ctime() - 500 res = runSearch(query, opts, false /* expectedReindex*/) require.Zero(t, len(res.Hits)) verifySearchDone(0, false) opts.SentBefore = msg4.Ctime() res = runSearch(query, opts, false /* expectedReindex*/) require.Equal(t, 1, len(res.Hits)) require.Equal(t, 4, len(res.Hits[0].Hits)) verifySearchDone(4, false) opts.SentBefore = 0 // Test edit query = "edited" msgBody = "edited" searchMatch = chat1.ChatSearchMatch{ StartIndex: 0, EndIndex: len(msgBody), Match: msgBody, } mustEditMsg(tc2.startCtx, t, ctc, u2, conv, msgID4) consumeNewMsgRemote(t, listener1, chat1.MessageType_EDIT) consumeNewMsgRemote(t, listener2, chat1.MessageType_EDIT) verifyIndex() res = runSearch(query, opts, false /* expectedReindex*/) t.Logf("%+v", res) require.Equal(t, 1, len(res.Hits)) convHit = res.Hits[0] require.Equal(t, convID, convHit.ConvID) require.Equal(t, 1, len(convHit.Hits)) verifyHit(convID, []chat1.MessageID{msgID2, msgID3}, msgID4, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0]) verifySearchDone(1, false) // Test delete mustDeleteMsg(tc2.startCtx, t, ctc, u2, conv, msgID4) consumeNewMsgRemote(t, listener1, chat1.MessageType_DELETE) consumeNewMsgRemote(t, listener2, chat1.MessageType_DELETE) verifyIndex() res = runSearch(query, opts, false /* expectedReindex*/) require.Equal(t, 0, len(res.Hits)) verifySearchDone(0, false) // Test request payment query = "payment :moneybag:" msgBody = "payment :moneybag:" searchMatch = chat1.ChatSearchMatch{ StartIndex: 0, EndIndex: len(msgBody), Match: msgBody, } msgID7 := sendMessage(chat1.NewMessageBodyWithRequestpayment(chat1.MessageRequestPayment{ RequestID: stellar1.KeybaseRequestID("dummy id"), Note: msgBody, }), u1) verifyIndex() res = runSearch(query, opts, false /* expectedReindex*/) require.Equal(t, 1, len(res.Hits)) convHit = res.Hits[0] require.Equal(t, convID, convHit.ConvID) require.Equal(t, 1, len(convHit.Hits)) verifyHit(convID, []chat1.MessageID{msgID2, msgID3}, msgID7, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0]) verifySearchDone(1, false) // Test utf8 msgBody = `约书亚和约翰屌爆了` query = `约书亚和约翰屌爆了` searchMatch = chat1.ChatSearchMatch{ StartIndex: 0, EndIndex: len(msgBody), Match: msgBody, } msgID8 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{ Body: msgBody, }), u1) // NOTE other prefixes are cut off since they exceed the max length verifyIndex() res = runSearch(query, opts, false /* expectedReindex*/) require.Equal(t, 1, len(res.Hits)) convHit = res.Hits[0] require.Equal(t, convID, convHit.ConvID) require.Equal(t, 1, len(convHit.Hits)) verifyHit(convID, []chat1.MessageID{msgID3, msgID7}, msgID8, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0]) verifySearchDone(1, false) // DB nuke, ensure that we reindex after the search _, err = g1.LocalChatDb.Nuke() require.NoError(t, indexer1.OnDbNuke(libkb.NewMetaContext(context.TODO(), g1.ExternalG()))) require.NoError(t, err) opts.ReindexMode = chat1.ReIndexingMode_PRESEARCH_SYNC // force reindex so we're fully up to date. res = runSearch(query, opts, true /* expectedReindex*/) require.Equal(t, 1, len(res.Hits)) convHit = res.Hits[0] require.Equal(t, convID, convHit.ConvID) require.Equal(t, 1, len(convHit.Hits)) verifyHit(convID, []chat1.MessageID{msgID3, msgID7}, msgID8, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0]) verifySearchDone(1, false) verifyIndex() // since our index is full, we shouldn't fire off any calls to get messages runSearch(query, opts, false /* expectedReindex*/) verifySearchDone(1, false) // Verify POSTSEARCH_SYNC ictx := globals.CtxAddIdentifyMode(ctx, keybase1.TLFIdentifyBehavior_CHAT_SKIP, nil) _, err = g1.LocalChatDb.Nuke() require.NoError(t, err) require.NoError(t, indexer1.OnDbNuke(libkb.NewMetaContext(context.TODO(), g1.ExternalG()))) err = indexer1.SelectiveSync(ictx) require.NoError(t, err) opts.ReindexMode = chat1.ReIndexingMode_POSTSEARCH_SYNC res = runSearch(query, opts, true /* expectedReindex*/) require.Equal(t, 1, len(res.Hits)) convHit = res.Hits[0] require.Equal(t, convID, convHit.ConvID) require.Equal(t, 1, len(convHit.Hits)) verifyHit(convID, []chat1.MessageID{msgID3, msgID7}, msgID8, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0]) verifySearchDone(1, false) verifyIndex() // since our index is full, we shouldn't fire off any calls to get messages runSearch(query, opts, false /* expectedReindex*/) verifySearchDone(1, false) // Test prefix searching query = "pay" searchMatch = chat1.ChatSearchMatch{ StartIndex: 0, EndIndex: 3, Match: "pay", } res = runSearch(query, opts, false /* expectedReindex*/) require.Equal(t, 1, len(res.Hits)) convHit = res.Hits[0] require.Equal(t, convID, convHit.ConvID) require.Equal(t, 1, len(convHit.Hits)) verifyHit(convID, []chat1.MessageID{msgID2, msgID3}, msgID7, []chat1.MessageID{msgID8}, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0]) verifySearchDone(1, false) query = "payments" res = runSearch(query, opts, false /* expectedReindex*/) require.Equal(t, 0, len(res.Hits)) verifySearchDone(0, false) // Test deletehistory mustDeleteHistory(tc2.startCtx, t, ctc, u2, conv, msgID8+1) consumeNewMsgRemote(t, listener1, chat1.MessageType_DELETEHISTORY) consumeNewMsgRemote(t, listener2, chat1.MessageType_DELETEHISTORY) verifyIndex() // test sentTo msgBody = "hello @" + u1.Username query = "hello" searchMatch = chat1.ChatSearchMatch{ StartIndex: 0, EndIndex: 5, Match: "hello", } msgID10 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{ Body: msgBody, }), u2) // invalid username opts.SentTo = u1.Username + "foo" res = runSearch(query, opts, false /* expectedReindex*/) require.Zero(t, len(res.Hits)) verifySearchDone(0, false) opts.SentTo = u1.Username res = runSearch(query, opts, false /* expectedReindex*/) require.Equal(t, 1, len(res.Hits)) convHit = res.Hits[0] require.Equal(t, convID, convHit.ConvID) require.Equal(t, 1, len(convHit.Hits)) verifyHit(convID, []chat1.MessageID{}, msgID10, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0]) verifySearchDone(1, false) opts.SentTo = "" // Test canceling sync loop syncLoopCh := make(chan struct{}) indexer1.SetSyncLoopCh(syncLoopCh) indexer1.StartSyncLoop() waitForFail := func() bool { for range 5 { indexer1.CancelSync(ctx) select { case <-time.After(2 * time.Second): case <-syncLoopCh: return true } } return false } require.True(t, waitForFail()) indexer1.PokeSync(ctx) require.True(t, waitForFail()) // test search delegation with a specific conv // delegate on queries shorter than search.MinTokenLength opts.ConvID = &convID // delegate if a single conv is not fully indexed query = "hello" _, err = g1.LocalChatDb.Nuke() require.NoError(t, err) require.NoError(t, indexer1.OnDbNuke(libkb.NewMetaContext(context.TODO(), g1.ExternalG()))) res = runSearch(query, opts, false /* expectedReindex*/) require.Equal(t, 1, len(res.Hits)) convHit = res.Hits[0] require.Equal(t, convID, convHit.ConvID) require.Equal(t, 1, len(convHit.Hits)) verifyHit(convID, []chat1.MessageID{}, msgID10, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0]) verifySearchDone(1, true) // delegate on regexp searches query = "/hello/" res = runSearch(query, opts, false /* expectedReindex*/) require.Equal(t, 1, len(res.Hits)) convHit = res.Hits[0] require.Equal(t, convID, convHit.ConvID) require.Equal(t, 1, len(convHit.Hits)) verifyHit(convID, []chat1.MessageID{}, msgID10, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0]) verifySearchDone(1, true) query = "hi" searchMatch = chat1.ChatSearchMatch{ StartIndex: 0, EndIndex: 2, Match: "hi", } msgID11 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{ Body: query, }), u1) res = runSearch(query, opts, false /* expectedReindex*/) require.Equal(t, 1, len(res.Hits)) convHit = res.Hits[0] require.Equal(t, convID, convHit.ConvID) require.Equal(t, 1, len(convHit.Hits)) verifyHit(convID, []chat1.MessageID{msgID10}, msgID11, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0]) verifySearchDone(1, true) err = indexer1.Clear(ctx, uid1, convID) require.NoError(t, err) pi, err = indexer1.PercentIndexed(ctx, convID) require.NoError(t, err) require.Zero(t, pi) err = indexer2.Clear(ctx, uid2, convID) require.NoError(t, err) pi, err = indexer2.PercentIndexed(ctx, convID) require.NoError(t, err) require.Zero(t, pi) }) }