github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/chat/search_test.go (about) 1 package chat 2 3 import ( 4 "context" 5 "fmt" 6 "testing" 7 "time" 8 9 "github.com/keybase/client/go/chat/globals" 10 "github.com/keybase/client/go/chat/search" 11 "github.com/keybase/client/go/kbtest" 12 "github.com/keybase/client/go/libkb" 13 "github.com/keybase/client/go/protocol/chat1" 14 "github.com/keybase/client/go/protocol/gregor1" 15 "github.com/keybase/client/go/protocol/keybase1" 16 "github.com/keybase/client/go/protocol/stellar1" 17 "github.com/stretchr/testify/require" 18 ) 19 20 func TestChatSearchConvRegexp(t *testing.T) { 21 runWithMemberTypes(t, func(mt chat1.ConversationMembersType) { 22 23 // Only test against IMPTEAMNATIVE. There is a bug in ChatRemoteMock 24 // with using Pagination Next/Prev and we don't need to triple test 25 // here. 26 switch mt { 27 case chat1.ConversationMembersType_IMPTEAMNATIVE: 28 default: 29 return 30 } 31 32 ctc := makeChatTestContext(t, "SearchRegexp", 2) 33 defer ctc.cleanup() 34 users := ctc.users() 35 u1 := users[0] 36 u2 := users[1] 37 38 conv := mustCreateConversationForTest(t, ctc, u1, chat1.TopicType_CHAT, 39 mt, ctc.as(t, u2).user()) 40 convID := conv.Id 41 42 tc1 := ctc.as(t, u1) 43 tc2 := ctc.as(t, u2) 44 45 chatUI := kbtest.NewChatUI() 46 tc1.h.mockChatUI = chatUI 47 48 listener1 := newServerChatListener() 49 tc1.h.G().NotifyRouter.AddListener(listener1) 50 listener2 := newServerChatListener() 51 tc2.h.G().NotifyRouter.AddListener(listener2) 52 53 sendMessage := func(msgBody chat1.MessageBody, user *kbtest.FakeUser) chat1.MessageID { 54 msgID := mustPostLocalForTest(t, ctc, user, conv, msgBody) 55 typ, err := msgBody.MessageType() 56 require.NoError(t, err) 57 consumeNewMsgRemote(t, listener1, typ) 58 consumeNewMsgRemote(t, listener2, typ) 59 return msgID 60 } 61 62 verifyHit := func(beforeMsgIDs []chat1.MessageID, hitMessageID chat1.MessageID, afterMsgIDs []chat1.MessageID, 63 matches []chat1.ChatSearchMatch, searchHit chat1.ChatSearchHit) { 64 _verifyHit := func(searchHit chat1.ChatSearchHit) { 65 if beforeMsgIDs == nil { 66 require.Nil(t, searchHit.BeforeMessages) 67 } else { 68 require.Equal(t, len(beforeMsgIDs), len(searchHit.BeforeMessages)) 69 for i, msgID := range beforeMsgIDs { 70 msg := searchHit.BeforeMessages[i] 71 t.Logf("msg: %v", msg.Valid()) 72 require.True(t, msg.IsValid()) 73 require.Equal(t, msgID, msg.GetMessageID()) 74 } 75 } 76 require.EqualValues(t, hitMessageID, searchHit.HitMessage.Valid().MessageID) 77 require.Equal(t, matches, searchHit.Matches) 78 79 if afterMsgIDs == nil { 80 require.Nil(t, searchHit.AfterMessages) 81 } else { 82 require.Equal(t, len(afterMsgIDs), len(searchHit.AfterMessages)) 83 for i, msgID := range afterMsgIDs { 84 msg := searchHit.AfterMessages[i] 85 require.True(t, msg.IsValid()) 86 require.Equal(t, msgID, msg.GetMessageID()) 87 } 88 } 89 90 } 91 _verifyHit(searchHit) 92 select { 93 case searchHitRes := <-chatUI.SearchHitCb: 94 _verifyHit(searchHitRes.SearchHit) 95 case <-time.After(20 * time.Second): 96 require.Fail(t, "no search result received") 97 } 98 } 99 verifySearchDone := func(numHits int) { 100 select { 101 case searchDone := <-chatUI.SearchDoneCb: 102 require.Equal(t, numHits, searchDone.NumHits) 103 case <-time.After(20 * time.Second): 104 require.Fail(t, "no search result received") 105 } 106 } 107 108 runSearch := func(query string, isRegex bool, opts chat1.SearchOpts) chat1.SearchRegexpRes { 109 opts.IsRegex = isRegex 110 res, err := tc1.chatLocalHandler().SearchRegexp(tc1.startCtx, chat1.SearchRegexpArg{ 111 ConvID: convID, 112 Query: query, 113 Opts: opts, 114 }) 115 require.NoError(t, err) 116 t.Logf("query: %v, searchRes: %+v", query, res) 117 return res 118 } 119 120 isRegex := false 121 opts := chat1.SearchOpts{ 122 MaxHits: 5, 123 BeforeContext: 2, 124 AfterContext: 2, 125 MaxMessages: 1000, 126 } 127 128 // Test basic equality match 129 query := "hi" 130 msgBody := "hi @here" 131 msgID1 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{ 132 Body: msgBody, 133 }), u1) 134 searchMatch := chat1.ChatSearchMatch{ 135 StartIndex: 0, 136 EndIndex: 2, 137 Match: query, 138 } 139 res := runSearch(query, isRegex, opts) 140 require.Equal(t, 1, len(res.Hits)) 141 verifyHit(nil, msgID1, nil, []chat1.ChatSearchMatch{searchMatch}, res.Hits[0]) 142 verifySearchDone(1) 143 144 // Test basic no results 145 query = "hey" 146 res = runSearch(query, isRegex, opts) 147 require.Equal(t, 0, len(res.Hits)) 148 verifySearchDone(0) 149 150 // Test maxHits 151 opts.MaxHits = 1 152 query = "hi" 153 msgBody = "hi there" 154 msgID2 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{ 155 Body: msgBody, 156 }), u1) 157 res = runSearch(query, isRegex, opts) 158 require.Equal(t, 1, len(res.Hits)) 159 verifyHit([]chat1.MessageID{msgID1}, msgID2, nil, []chat1.ChatSearchMatch{searchMatch}, res.Hits[0]) 160 verifySearchDone(1) 161 162 opts.MaxHits = 5 163 res = runSearch(query, isRegex, opts) 164 require.Equal(t, 2, len(res.Hits)) 165 verifyHit([]chat1.MessageID{msgID1}, msgID2, nil, []chat1.ChatSearchMatch{searchMatch}, res.Hits[0]) 166 verifyHit(nil, msgID1, []chat1.MessageID{msgID2}, []chat1.ChatSearchMatch{searchMatch}, res.Hits[1]) 167 verifySearchDone(2) 168 169 msgID3 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{ 170 Body: msgBody, 171 }), u1) 172 res = runSearch(query, isRegex, opts) 173 require.Equal(t, 3, len(res.Hits)) 174 verifyHit([]chat1.MessageID{msgID1, msgID2}, msgID3, nil, []chat1.ChatSearchMatch{searchMatch}, res.Hits[0]) 175 verifyHit([]chat1.MessageID{msgID1}, msgID2, []chat1.MessageID{msgID3}, []chat1.ChatSearchMatch{searchMatch}, res.Hits[1]) 176 verifyHit(nil, msgID1, []chat1.MessageID{msgID2, msgID3}, []chat1.ChatSearchMatch{searchMatch}, res.Hits[2]) 177 verifySearchDone(3) 178 179 // test sentBy 180 // invalid username 181 opts.SentBy = u1.Username + "foo" 182 res = runSearch(query, isRegex, opts) 183 require.Zero(t, len(res.Hits)) 184 verifySearchDone(0) 185 186 // send from user2 and make sure we can filter, @mention user1 to test 187 // SentTo later. 188 opts.SentBy = u2.Username 189 msgBody = fmt.Sprintf("hi @%s", u1.Username) 190 msgID4 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{ 191 Body: msgBody, 192 }), u2) 193 res = runSearch(query, isRegex, opts) 194 require.Equal(t, 1, len(res.Hits)) 195 verifyHit([]chat1.MessageID{msgID2, msgID3}, msgID4, nil, []chat1.ChatSearchMatch{searchMatch}, res.Hits[0]) 196 verifySearchDone(1) 197 opts.SentBy = "" 198 199 // test sentTo 200 // invalid username 201 opts.SentTo = u1.Username + "foo" 202 res = runSearch(query, isRegex, opts) 203 require.Zero(t, len(res.Hits)) 204 verifySearchDone(0) 205 206 opts.SentTo = u1.Username 207 res = runSearch(query, isRegex, opts) 208 require.Equal(t, 2, len(res.Hits)) 209 verifyHit([]chat1.MessageID{msgID2, msgID3}, msgID4, nil, []chat1.ChatSearchMatch{searchMatch}, res.Hits[0]) 210 verifyHit(nil, msgID1, []chat1.MessageID{msgID2, msgID3}, []chat1.ChatSearchMatch{searchMatch}, res.Hits[1]) 211 verifySearchDone(2) 212 opts.SentTo = "" 213 214 // test sentBefore/sentAfter 215 msgRes, err := tc1.chatLocalHandler().GetMessagesLocal(tc1.startCtx, chat1.GetMessagesLocalArg{ 216 ConversationID: convID, 217 MessageIDs: []chat1.MessageID{msgID1, msgID4}, 218 }) 219 require.NoError(t, err) 220 require.Equal(t, 2, len(msgRes.Messages)) 221 msg1 := msgRes.Messages[0] 222 msg4 := msgRes.Messages[1] 223 224 // nothing sent after msg4 225 opts.SentAfter = msg4.Ctime() + 500 226 res = runSearch(query, isRegex, opts) 227 require.Zero(t, len(res.Hits)) 228 229 opts.SentAfter = msg1.Ctime() 230 res = runSearch(query, isRegex, opts) 231 require.Equal(t, 4, len(res.Hits)) 232 233 // nothing sent before msg1 234 opts.SentAfter = 0 235 opts.SentBefore = msg1.Ctime() - 500 236 res = runSearch(query, isRegex, opts) 237 require.Zero(t, len(res.Hits)) 238 239 opts.SentBefore = msg4.Ctime() 240 res = runSearch(query, isRegex, opts) 241 require.Equal(t, 4, len(res.Hits)) 242 243 opts.SentBefore = 0 244 245 // drain the cbs, 8 hits and 4 dones 246 timeout := 20 * time.Second 247 for i := 0; i < 8+4; i++ { 248 select { 249 case <-chatUI.SearchHitCb: 250 case <-chatUI.SearchDoneCb: 251 case <-time.After(timeout): 252 require.Fail(t, "no search result received") 253 } 254 } 255 256 query = "edited" 257 msgBody = "edited" 258 searchMatch = chat1.ChatSearchMatch{ 259 StartIndex: 0, 260 EndIndex: len(msgBody), 261 Match: msgBody, 262 } 263 mustEditMsg(tc2.startCtx, t, ctc, u2, conv, msgID4) 264 consumeNewMsgRemote(t, listener1, chat1.MessageType_EDIT) 265 consumeNewMsgRemote(t, listener2, chat1.MessageType_EDIT) 266 267 res = runSearch(query, isRegex, opts) 268 require.Equal(t, 1, len(res.Hits)) 269 verifyHit([]chat1.MessageID{msgID2, msgID3}, msgID4, nil, []chat1.ChatSearchMatch{searchMatch}, res.Hits[0]) 270 verifySearchDone(1) 271 272 // Test delete 273 mustDeleteMsg(tc2.startCtx, t, ctc, u2, conv, msgID4) 274 consumeNewMsgRemote(t, listener1, chat1.MessageType_DELETE) 275 consumeNewMsgRemote(t, listener2, chat1.MessageType_DELETE) 276 res = runSearch(query, isRegex, opts) 277 require.Equal(t, 0, len(res.Hits)) 278 verifySearchDone(0) 279 280 // Test request payment 281 query = "payment :moneybag:" 282 msgBody = "payment :moneybag:" 283 searchMatch = chat1.ChatSearchMatch{ 284 StartIndex: 0, 285 EndIndex: len(msgBody), 286 Match: msgBody, 287 } 288 msgID7 := sendMessage(chat1.NewMessageBodyWithRequestpayment(chat1.MessageRequestPayment{ 289 RequestID: stellar1.KeybaseRequestID("dummy id"), 290 Note: msgBody, 291 }), u1) 292 res = runSearch(query, isRegex, opts) 293 require.Equal(t, 1, len(res.Hits)) 294 verifyHit([]chat1.MessageID{msgID2, msgID3}, msgID7, nil, []chat1.ChatSearchMatch{searchMatch}, res.Hits[0]) 295 verifySearchDone(1) 296 297 // Test regex functionality 298 isRegex = true 299 300 // Test utf8 301 msgBody = `约书亚和约翰屌爆了` 302 query = `约.*` 303 searchMatch = chat1.ChatSearchMatch{ 304 StartIndex: 0, 305 EndIndex: len(msgBody), 306 Match: msgBody, 307 } 308 msgID8 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{ 309 Body: msgBody, 310 }), u1) 311 res = runSearch(query, isRegex, opts) 312 require.Equal(t, 1, len(res.Hits)) 313 verifyHit([]chat1.MessageID{msgID3, msgID7}, msgID8, nil, []chat1.ChatSearchMatch{searchMatch}, res.Hits[0]) 314 verifySearchDone(1) 315 316 msgBody = "hihihi" 317 query = "hi" 318 matches := []chat1.ChatSearchMatch{} 319 startIndex := 0 320 for i := 0; i < 3; i++ { 321 matches = append(matches, chat1.ChatSearchMatch{ 322 StartIndex: startIndex, 323 EndIndex: startIndex + 2, 324 Match: query, 325 }) 326 startIndex += 2 327 } 328 329 opts.MaxHits = 1 330 msgID9 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{ 331 Body: msgBody, 332 }), u1) 333 res = runSearch(query, isRegex, opts) 334 require.Equal(t, 1, len(res.Hits)) 335 verifyHit([]chat1.MessageID{msgID7, msgID8}, msgID9, nil, matches, res.Hits[0]) 336 verifySearchDone(1) 337 338 query = "h.*" 339 lowercase := "abcdefghijklmnopqrstuvwxyz" 340 for _, char := range lowercase { 341 sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{ 342 Body: "h." + string(char), 343 }), u1) 344 } 345 opts.MaxHits = len(lowercase) 346 res = runSearch(query, isRegex, opts) 347 require.Equal(t, opts.MaxHits, len(res.Hits)) 348 verifySearchDone(opts.MaxHits) 349 350 // Test maxMessages 351 opts.MaxMessages = 2 352 res = runSearch(query, isRegex, opts) 353 require.Equal(t, opts.MaxMessages, len(res.Hits)) 354 verifySearchDone(opts.MaxMessages) 355 356 query = `[A-Z]*` 357 res = runSearch(query, isRegex, opts) 358 require.Equal(t, 0, len(res.Hits)) 359 verifySearchDone(0) 360 361 // Test invalid regex 362 _, err = tc1.chatLocalHandler().SearchRegexp(tc1.startCtx, chat1.SearchRegexpArg{ 363 ConvID: convID, 364 Query: "(", 365 Opts: chat1.SearchOpts{ 366 IsRegex: true, 367 }, 368 }) 369 require.Error(t, err) 370 }) 371 } 372 373 func TestChatSearchRemoveMsg(t *testing.T) { 374 useRemoteMock = false 375 defer func() { useRemoteMock = true }() 376 ctc := makeChatTestContext(t, "TestChatSearchRemoveMsg", 2) 377 defer ctc.cleanup() 378 379 users := ctc.users() 380 ctx := ctc.as(t, users[0]).startCtx 381 tc := ctc.world.Tcs[users[0].Username] 382 chatUI := kbtest.NewChatUI() 383 uid := gregor1.UID(users[0].GetUID().ToBytes()) 384 ctc.as(t, users[0]).h.mockChatUI = chatUI 385 conv := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, 386 chat1.ConversationMembersType_IMPTEAMNATIVE) 387 conv1 := mustCreateConversationForTest(t, ctc, users[0], chat1.TopicType_CHAT, 388 chat1.ConversationMembersType_IMPTEAMNATIVE, users[1]) 389 390 msgID0 := mustPostLocalForTest(t, ctc, users[0], conv, chat1.NewMessageBodyWithText(chat1.MessageText{ 391 Body: "MIKEMAXIM", 392 })) 393 msgID1 := mustPostLocalForTest(t, ctc, users[0], conv, chat1.NewMessageBodyWithText(chat1.MessageText{ 394 Body: "MIKEMAXIM", 395 })) 396 msgID2 := mustPostLocalForTest(t, ctc, users[0], conv1, chat1.NewMessageBodyWithText(chat1.MessageText{ 397 Body: "MIKEMAXIM", 398 })) 399 mustPostLocalForTest(t, ctc, users[0], conv, chat1.NewMessageBodyWithText(chat1.MessageText{ 400 Body: "CRICKETS", 401 })) 402 res, err := ctc.as(t, users[0]).chatLocalHandler().SearchInbox(ctx, chat1.SearchInboxArg{ 403 Query: "MIKEM", 404 Opts: chat1.SearchOpts{ 405 MaxConvsHit: 5, 406 MaxHits: 5, 407 }, 408 }) 409 require.NoError(t, err) 410 require.NotNil(t, res.Res) 411 require.Equal(t, 2, len(res.Res.Hits)) 412 if res.Res.Hits[0].ConvID.Eq(conv.Id) { 413 require.Equal(t, 2, len(res.Res.Hits[0].Hits)) 414 require.Equal(t, 1, len(res.Res.Hits[1].Hits)) 415 } else { 416 require.Equal(t, 1, len(res.Res.Hits[0].Hits)) 417 require.Equal(t, 2, len(res.Res.Hits[1].Hits)) 418 } 419 420 mustDeleteMsg(ctx, t, ctc, users[0], conv1, msgID2) 421 422 res, err = ctc.as(t, users[0]).chatLocalHandler().SearchInbox(ctx, chat1.SearchInboxArg{ 423 Query: "MIKEM", 424 Opts: chat1.SearchOpts{ 425 MaxConvsHit: 5, 426 MaxHits: 5, 427 }, 428 }) 429 require.NoError(t, err) 430 require.NotNil(t, res.Res) 431 require.Equal(t, 1, len(res.Res.Hits)) 432 require.Equal(t, 2, len(res.Res.Hits[0].Hits)) 433 434 mustDeleteMsg(ctx, t, ctc, users[0], conv, msgID0) 435 mustDeleteMsg(ctx, t, ctc, users[0], conv, msgID1) 436 437 hres, err := tc.ChatG.Indexer.(*search.Indexer).GetStoreHits(ctx, uid, conv.Id, "MIKEM") 438 require.NoError(t, err) 439 require.Zero(t, len(hres)) 440 } 441 442 func TestChatSearchInbox(t *testing.T) { 443 runWithMemberTypes(t, func(mt chat1.ConversationMembersType) { 444 445 // Only test against IMPTEAMNATIVE. There is a bug in ChatRemoteMock 446 // with using Pagination Next/Prev and we don't need to triple test 447 // here. 448 switch mt { 449 case chat1.ConversationMembersType_IMPTEAMNATIVE: 450 default: 451 return 452 } 453 454 ctx := context.TODO() 455 ctc := makeChatTestContext(t, "SearchInbox", 2) 456 defer ctc.cleanup() 457 users := ctc.users() 458 u1 := users[0] 459 u2 := users[1] 460 461 tc1 := ctc.as(t, u1) 462 tc2 := ctc.as(t, u2) 463 g1 := ctc.world.Tcs[u1.Username].Context() 464 g2 := ctc.world.Tcs[u2.Username].Context() 465 uid1 := u1.User.GetUID().ToBytes() 466 uid2 := u2.User.GetUID().ToBytes() 467 468 chatUI := kbtest.NewChatUI() 469 tc1.h.mockChatUI = chatUI 470 471 listener1 := newServerChatListener() 472 tc1.h.G().NotifyRouter.AddListener(listener1) 473 listener2 := newServerChatListener() 474 tc2.h.G().NotifyRouter.AddListener(listener2) 475 476 // Create our own Indexer instances so we have access to non-interface methods 477 indexer1 := search.NewIndexer(g1) 478 consumeCh1 := make(chan chat1.ConversationID, 100) 479 reindexCh1 := make(chan chat1.ConversationID, 100) 480 indexer1.SetConsumeCh(consumeCh1) 481 indexer1.SetReindexCh(reindexCh1) 482 indexer1.SetStartSyncDelay(0) 483 indexer1.SetUID(uid1) 484 indexer1.SetFlushDelay(100 * time.Millisecond) 485 indexer1.StartFlushLoop() 486 indexer1.StartStorageLoop() 487 // Stop the original 488 select { 489 case <-g1.Indexer.Stop(ctx): 490 case <-time.After(20 * time.Second): 491 require.Fail(t, "g1 Indexer did not stop") 492 } 493 g1.Indexer = indexer1 494 495 indexer2 := search.NewIndexer(g2) 496 consumeCh2 := make(chan chat1.ConversationID, 100) 497 reindexCh2 := make(chan chat1.ConversationID, 100) 498 indexer2.SetConsumeCh(consumeCh2) 499 indexer2.SetReindexCh(reindexCh2) 500 indexer2.SetStartSyncDelay(0) 501 indexer2.SetUID(uid2) 502 indexer2.SetFlushDelay(10 * time.Millisecond) 503 indexer2.StartFlushLoop() 504 indexer2.StartStorageLoop() 505 // Stop the original 506 select { 507 case <-g2.Indexer.Stop(ctx): 508 case <-time.After(20 * time.Second): 509 require.Fail(t, "g2 Indexer did not stop") 510 } 511 g2.Indexer = indexer2 512 513 conv := mustCreateConversationForTest(t, ctc, u1, chat1.TopicType_CHAT, 514 mt, ctc.as(t, u2).user()) 515 convID := conv.Id 516 517 // verify zero messages case 518 fi, err := indexer1.FullyIndexed(ctx, convID) 519 require.NoError(t, err) 520 require.True(t, fi) 521 pi, err := indexer1.PercentIndexed(ctx, convID) 522 require.NoError(t, err) 523 require.Equal(t, 100, pi) 524 525 fi, err = indexer2.FullyIndexed(ctx, convID) 526 require.NoError(t, err) 527 require.True(t, fi) 528 pi, err = indexer2.PercentIndexed(ctx, convID) 529 require.NoError(t, err) 530 require.Equal(t, 100, pi) 531 532 sendMessage := func(msgBody chat1.MessageBody, user *kbtest.FakeUser) chat1.MessageID { 533 msgID := mustPostLocalForTest(t, ctc, user, conv, msgBody) 534 typ, err := msgBody.MessageType() 535 require.NoError(t, err) 536 consumeNewMsgRemote(t, listener1, typ) 537 consumeNewMsgRemote(t, listener2, typ) 538 return msgID 539 } 540 541 verifyHit := func(convID chat1.ConversationID, beforeMsgIDs []chat1.MessageID, hitMessageID chat1.MessageID, 542 afterMsgIDs []chat1.MessageID, matches []chat1.ChatSearchMatch, searchHit chat1.ChatSearchHit) { 543 if beforeMsgIDs == nil { 544 require.Nil(t, searchHit.BeforeMessages) 545 } else { 546 require.Equal(t, len(beforeMsgIDs), len(searchHit.BeforeMessages)) 547 for i, msgID := range beforeMsgIDs { 548 msg := searchHit.BeforeMessages[i] 549 require.True(t, msg.IsValid()) 550 require.Equal(t, msgID, msg.GetMessageID()) 551 } 552 } 553 require.EqualValues(t, hitMessageID, searchHit.HitMessage.Valid().MessageID) 554 require.Equal(t, matches, searchHit.Matches) 555 556 if afterMsgIDs == nil { 557 require.Nil(t, searchHit.AfterMessages) 558 } else { 559 require.Equal(t, len(afterMsgIDs), len(searchHit.AfterMessages)) 560 for i, msgID := range afterMsgIDs { 561 msg := searchHit.AfterMessages[i] 562 require.True(t, msg.IsValid()) 563 require.Equal(t, msgID, msg.GetMessageID()) 564 } 565 } 566 } 567 verifySearchDone := func(numHits int, delegated bool) { 568 select { 569 case <-chatUI.InboxSearchConvHitsCb: 570 case <-time.After(20 * time.Second): 571 require.Fail(t, "no name hits") 572 } 573 select { 574 case searchDone := <-chatUI.InboxSearchDoneCb: 575 require.Equal(t, numHits, searchDone.Res.NumHits) 576 numConvs := 1 577 if numHits == 0 { 578 numConvs = 0 579 } 580 require.Equal(t, numConvs, searchDone.Res.NumConvs) 581 if delegated { 582 require.True(t, searchDone.Res.Delegated) 583 } else { 584 require.Equal(t, 100, searchDone.Res.PercentIndexed) 585 } 586 case <-time.After(20 * time.Second): 587 require.Fail(t, "no search result received") 588 } 589 } 590 591 verifyIndexConsumption := func(ch chan chat1.ConversationID) { 592 select { 593 case id := <-ch: 594 require.Equal(t, convID, id) 595 case <-time.After(5 * time.Second): 596 require.Fail(t, "indexer didn't consume") 597 } 598 } 599 600 verifyIndexNoConsumption := func(ch chan chat1.ConversationID) { 601 select { 602 case <-ch: 603 require.Fail(t, "indexer reindexed") 604 default: 605 } 606 } 607 608 verifyIndex := func() { 609 t.Logf("verify user 1 index") 610 verifyIndexConsumption(consumeCh1) 611 t.Logf("verify user 2 index") 612 verifyIndexConsumption(consumeCh2) 613 } 614 615 runSearch := func(query string, opts chat1.SearchOpts, expectedReindex bool) *chat1.ChatSearchInboxResults { 616 res, err := tc1.chatLocalHandler().SearchInbox(tc1.startCtx, chat1.SearchInboxArg{ 617 Query: query, 618 Opts: opts, 619 }) 620 require.NoError(t, err) 621 t.Logf("query: %v, searchRes: %+v", query, res) 622 if expectedReindex { 623 verifyIndexConsumption(reindexCh1) 624 } else { 625 verifyIndexNoConsumption(reindexCh1) 626 } 627 return res.Res 628 } 629 630 opts := chat1.SearchOpts{ 631 MaxHits: 5, 632 BeforeContext: 2, 633 AfterContext: 2, 634 MaxMessages: 1000, 635 MaxNameConvs: 1, 636 } 637 638 // Test basic equality match 639 msgBody := "hello, byE" 640 msgID1 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{ 641 Body: msgBody, 642 }), u1) 643 644 queries := []string{"hello", "hello, ByE"} 645 matches := []chat1.ChatSearchMatch{ 646 { 647 StartIndex: 0, 648 EndIndex: 5, 649 Match: "hello", 650 }, 651 { 652 StartIndex: 0, 653 EndIndex: 10, 654 Match: "hello, byE", 655 }, 656 } 657 for i, query := range queries { 658 res := runSearch(query, opts, false /* expectedReindex */) 659 require.Equal(t, 1, len(res.Hits)) 660 convHit := res.Hits[0] 661 require.Equal(t, convID, convHit.ConvID) 662 require.Equal(t, 1, len(convHit.Hits)) 663 verifyHit(convID, nil, msgID1, nil, []chat1.ChatSearchMatch{matches[i]}, convHit.Hits[0]) 664 verifySearchDone(1, false) 665 } 666 667 // We get a hit but without any highlighting highlighting fails 668 query := "hell bye" 669 res := runSearch(query, opts, false /* expectedReindex */) 670 require.Equal(t, 1, len(res.Hits)) 671 convHit := res.Hits[0] 672 verifyHit(convID, nil, msgID1, nil, nil, convHit.Hits[0]) 673 verifySearchDone(1, false) 674 675 // Test basic no results 676 query = "hey" 677 res = runSearch(query, opts, false /* expectedReindex*/) 678 require.Equal(t, 0, len(res.Hits)) 679 verifySearchDone(0, false) 680 681 // Test maxHits 682 opts.MaxHits = 1 683 query = "hello" 684 searchMatch := chat1.ChatSearchMatch{ 685 StartIndex: 0, 686 EndIndex: len(query), 687 Match: query, 688 } 689 msgID2 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{ 690 Body: msgBody, 691 }), u1) 692 verifyIndex() 693 694 res = runSearch(query, opts, false /* expectedReindex*/) 695 require.Equal(t, 1, len(res.Hits)) 696 convHit = res.Hits[0] 697 require.Equal(t, convID, convHit.ConvID) 698 require.Equal(t, 1, len(convHit.Hits)) 699 verifyHit(convID, []chat1.MessageID{msgID1}, msgID2, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0]) 700 verifySearchDone(1, false) 701 702 opts.MaxHits = 5 703 res = runSearch(query, opts, false /* expectedReindex*/) 704 require.Equal(t, 1, len(res.Hits)) 705 convHit = res.Hits[0] 706 require.Equal(t, convID, convHit.ConvID) 707 require.Equal(t, 2, len(convHit.Hits)) 708 verifyHit(convID, []chat1.MessageID{msgID1}, msgID2, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0]) 709 verifyHit(convID, nil, msgID1, []chat1.MessageID{msgID2}, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[1]) 710 verifySearchDone(2, false) 711 712 msgID3 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{ 713 Body: msgBody, 714 }), u1) 715 716 verifyIndex() 717 718 res = runSearch(query, opts, false /* expectedReindex*/) 719 require.Equal(t, 1, len(res.Hits)) 720 convHit = res.Hits[0] 721 require.Equal(t, convID, convHit.ConvID) 722 require.Equal(t, 3, len(convHit.Hits)) 723 verifyHit(convID, []chat1.MessageID{msgID1, msgID2}, msgID3, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0]) 724 verifyHit(convID, []chat1.MessageID{msgID1}, msgID2, []chat1.MessageID{msgID3}, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[1]) 725 verifyHit(convID, nil, msgID1, []chat1.MessageID{msgID2, msgID3}, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[2]) 726 verifySearchDone(3, false) 727 728 // test sentBy 729 // invalid username 730 opts.SentBy = u1.Username + "foo" 731 res = runSearch(query, opts, false /* expectedReindex*/) 732 require.Zero(t, len(res.Hits)) 733 verifySearchDone(0, false) 734 735 // send from user2 and make sure we can filter 736 opts.SentBy = u2.Username 737 msgBody = "hello" 738 query = "hello" 739 msgID4 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{ 740 Body: msgBody, 741 }), u2) 742 verifyIndex() 743 744 res = runSearch(query, opts, false /* expectedReindex*/) 745 require.Equal(t, 1, len(res.Hits)) 746 convHit = res.Hits[0] 747 require.Equal(t, convID, convHit.ConvID) 748 require.Equal(t, 1, len(convHit.Hits)) 749 verifyHit(convID, []chat1.MessageID{msgID2, msgID3}, msgID4, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0]) 750 verifySearchDone(1, false) 751 opts.SentBy = "" 752 753 // test sentBefore/sentAfter 754 msgRes, err := tc1.chatLocalHandler().GetMessagesLocal(tc1.startCtx, chat1.GetMessagesLocalArg{ 755 ConversationID: convID, 756 MessageIDs: []chat1.MessageID{msgID1, msgID4}, 757 }) 758 require.NoError(t, err) 759 require.Equal(t, 2, len(msgRes.Messages)) 760 msg1 := msgRes.Messages[0] 761 msg4 := msgRes.Messages[1] 762 763 // nothing sent after msg4 764 opts.SentAfter = msg4.Ctime() + 500 765 res = runSearch(query, opts, false /* expectedReindex*/) 766 require.Zero(t, len(res.Hits)) 767 verifySearchDone(0, false) 768 769 opts.SentAfter = msg1.Ctime() 770 res = runSearch(query, opts, false /* expectedReindex*/) 771 require.Equal(t, 1, len(res.Hits)) 772 require.Equal(t, 4, len(res.Hits[0].Hits)) 773 verifySearchDone(4, false) 774 775 // nothing sent before msg1 776 opts.SentAfter = 0 777 opts.SentBefore = msg1.Ctime() - 500 778 res = runSearch(query, opts, false /* expectedReindex*/) 779 require.Zero(t, len(res.Hits)) 780 verifySearchDone(0, false) 781 782 opts.SentBefore = msg4.Ctime() 783 res = runSearch(query, opts, false /* expectedReindex*/) 784 require.Equal(t, 1, len(res.Hits)) 785 require.Equal(t, 4, len(res.Hits[0].Hits)) 786 verifySearchDone(4, false) 787 opts.SentBefore = 0 788 789 // Test edit 790 query = "edited" 791 msgBody = "edited" 792 searchMatch = chat1.ChatSearchMatch{ 793 StartIndex: 0, 794 EndIndex: len(msgBody), 795 Match: msgBody, 796 } 797 mustEditMsg(tc2.startCtx, t, ctc, u2, conv, msgID4) 798 consumeNewMsgRemote(t, listener1, chat1.MessageType_EDIT) 799 consumeNewMsgRemote(t, listener2, chat1.MessageType_EDIT) 800 verifyIndex() 801 802 res = runSearch(query, opts, false /* expectedReindex*/) 803 t.Logf("%+v", res) 804 require.Equal(t, 1, len(res.Hits)) 805 convHit = res.Hits[0] 806 require.Equal(t, convID, convHit.ConvID) 807 require.Equal(t, 1, len(convHit.Hits)) 808 verifyHit(convID, []chat1.MessageID{msgID2, msgID3}, msgID4, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0]) 809 verifySearchDone(1, false) 810 811 // Test delete 812 mustDeleteMsg(tc2.startCtx, t, ctc, u2, conv, msgID4) 813 consumeNewMsgRemote(t, listener1, chat1.MessageType_DELETE) 814 consumeNewMsgRemote(t, listener2, chat1.MessageType_DELETE) 815 verifyIndex() 816 817 res = runSearch(query, opts, false /* expectedReindex*/) 818 require.Equal(t, 0, len(res.Hits)) 819 verifySearchDone(0, false) 820 821 // Test request payment 822 query = "payment :moneybag:" 823 msgBody = "payment :moneybag:" 824 searchMatch = chat1.ChatSearchMatch{ 825 StartIndex: 0, 826 EndIndex: len(msgBody), 827 Match: msgBody, 828 } 829 msgID7 := sendMessage(chat1.NewMessageBodyWithRequestpayment(chat1.MessageRequestPayment{ 830 RequestID: stellar1.KeybaseRequestID("dummy id"), 831 Note: msgBody, 832 }), u1) 833 verifyIndex() 834 835 res = runSearch(query, opts, false /* expectedReindex*/) 836 require.Equal(t, 1, len(res.Hits)) 837 convHit = res.Hits[0] 838 require.Equal(t, convID, convHit.ConvID) 839 require.Equal(t, 1, len(convHit.Hits)) 840 verifyHit(convID, []chat1.MessageID{msgID2, msgID3}, msgID7, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0]) 841 verifySearchDone(1, false) 842 843 // Test utf8 844 msgBody = `约书亚和约翰屌爆了` 845 query = `约书亚和约翰屌爆了` 846 searchMatch = chat1.ChatSearchMatch{ 847 StartIndex: 0, 848 EndIndex: len(msgBody), 849 Match: msgBody, 850 } 851 msgID8 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{ 852 Body: msgBody, 853 }), u1) 854 // NOTE other prefixes are cut off since they exceed the max length 855 verifyIndex() 856 res = runSearch(query, opts, false /* expectedReindex*/) 857 require.Equal(t, 1, len(res.Hits)) 858 convHit = res.Hits[0] 859 require.Equal(t, convID, convHit.ConvID) 860 require.Equal(t, 1, len(convHit.Hits)) 861 verifyHit(convID, []chat1.MessageID{msgID3, msgID7}, msgID8, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0]) 862 verifySearchDone(1, false) 863 864 // DB nuke, ensure that we reindex after the search 865 _, err = g1.LocalChatDb.Nuke() 866 require.NoError(t, indexer1.OnDbNuke(libkb.NewMetaContext(context.TODO(), g1.ExternalG()))) 867 require.NoError(t, err) 868 opts.ReindexMode = chat1.ReIndexingMode_PRESEARCH_SYNC // force reindex so we're fully up to date. 869 res = runSearch(query, opts, true /* expectedReindex*/) 870 require.Equal(t, 1, len(res.Hits)) 871 convHit = res.Hits[0] 872 require.Equal(t, convID, convHit.ConvID) 873 require.Equal(t, 1, len(convHit.Hits)) 874 verifyHit(convID, []chat1.MessageID{msgID3, msgID7}, msgID8, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0]) 875 verifySearchDone(1, false) 876 verifyIndex() 877 878 // since our index is full, we shouldn't fire off any calls to get messages 879 runSearch(query, opts, false /* expectedReindex*/) 880 verifySearchDone(1, false) 881 882 // Verify POSTSEARCH_SYNC 883 ictx := globals.CtxAddIdentifyMode(ctx, keybase1.TLFIdentifyBehavior_CHAT_SKIP, nil) 884 _, err = g1.LocalChatDb.Nuke() 885 require.NoError(t, err) 886 require.NoError(t, indexer1.OnDbNuke(libkb.NewMetaContext(context.TODO(), g1.ExternalG()))) 887 err = indexer1.SelectiveSync(ictx) 888 require.NoError(t, err) 889 opts.ReindexMode = chat1.ReIndexingMode_POSTSEARCH_SYNC 890 res = runSearch(query, opts, true /* expectedReindex*/) 891 require.Equal(t, 1, len(res.Hits)) 892 convHit = res.Hits[0] 893 require.Equal(t, convID, convHit.ConvID) 894 require.Equal(t, 1, len(convHit.Hits)) 895 verifyHit(convID, []chat1.MessageID{msgID3, msgID7}, msgID8, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0]) 896 verifySearchDone(1, false) 897 verifyIndex() 898 899 // since our index is full, we shouldn't fire off any calls to get messages 900 runSearch(query, opts, false /* expectedReindex*/) 901 verifySearchDone(1, false) 902 903 // Test prefix searching 904 query = "pay" 905 searchMatch = chat1.ChatSearchMatch{ 906 StartIndex: 0, 907 EndIndex: 3, 908 Match: "pay", 909 } 910 res = runSearch(query, opts, false /* expectedReindex*/) 911 require.Equal(t, 1, len(res.Hits)) 912 convHit = res.Hits[0] 913 require.Equal(t, convID, convHit.ConvID) 914 require.Equal(t, 1, len(convHit.Hits)) 915 verifyHit(convID, []chat1.MessageID{msgID2, msgID3}, msgID7, []chat1.MessageID{msgID8}, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0]) 916 verifySearchDone(1, false) 917 918 query = "payments" 919 res = runSearch(query, opts, false /* expectedReindex*/) 920 require.Equal(t, 0, len(res.Hits)) 921 verifySearchDone(0, false) 922 923 // Test deletehistory 924 mustDeleteHistory(tc2.startCtx, t, ctc, u2, conv, msgID8+1) 925 consumeNewMsgRemote(t, listener1, chat1.MessageType_DELETEHISTORY) 926 consumeNewMsgRemote(t, listener2, chat1.MessageType_DELETEHISTORY) 927 verifyIndex() 928 929 // test sentTo 930 msgBody = "hello @" + u1.Username 931 query = "hello" 932 searchMatch = chat1.ChatSearchMatch{ 933 StartIndex: 0, 934 EndIndex: 5, 935 Match: "hello", 936 } 937 msgID10 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{ 938 Body: msgBody, 939 }), u2) 940 941 // invalid username 942 opts.SentTo = u1.Username + "foo" 943 res = runSearch(query, opts, false /* expectedReindex*/) 944 require.Zero(t, len(res.Hits)) 945 verifySearchDone(0, false) 946 947 opts.SentTo = u1.Username 948 res = runSearch(query, opts, false /* expectedReindex*/) 949 require.Equal(t, 1, len(res.Hits)) 950 convHit = res.Hits[0] 951 require.Equal(t, convID, convHit.ConvID) 952 require.Equal(t, 1, len(convHit.Hits)) 953 verifyHit(convID, []chat1.MessageID{}, msgID10, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0]) 954 verifySearchDone(1, false) 955 opts.SentTo = "" 956 957 // Test canceling sync loop 958 syncLoopCh := make(chan struct{}) 959 indexer1.SetSyncLoopCh(syncLoopCh) 960 indexer1.StartSyncLoop() 961 waitForFail := func() bool { 962 for i := 0; i < 5; i++ { 963 indexer1.CancelSync(ctx) 964 select { 965 case <-time.After(2 * time.Second): 966 case <-syncLoopCh: 967 return true 968 } 969 } 970 return false 971 } 972 require.True(t, waitForFail()) 973 indexer1.PokeSync(ctx) 974 require.True(t, waitForFail()) 975 976 // test search delegation with a specific conv 977 // delegate on queries shorter than search.MinTokenLength 978 opts.ConvID = &convID 979 // delegate if a single conv is not fully indexed 980 query = "hello" 981 _, err = g1.LocalChatDb.Nuke() 982 require.NoError(t, err) 983 require.NoError(t, indexer1.OnDbNuke(libkb.NewMetaContext(context.TODO(), g1.ExternalG()))) 984 res = runSearch(query, opts, false /* expectedReindex*/) 985 require.Equal(t, 1, len(res.Hits)) 986 convHit = res.Hits[0] 987 require.Equal(t, convID, convHit.ConvID) 988 require.Equal(t, 1, len(convHit.Hits)) 989 verifyHit(convID, []chat1.MessageID{}, msgID10, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0]) 990 verifySearchDone(1, true) 991 992 // delegate on regexp searches 993 query = "/hello/" 994 res = runSearch(query, opts, false /* expectedReindex*/) 995 require.Equal(t, 1, len(res.Hits)) 996 convHit = res.Hits[0] 997 require.Equal(t, convID, convHit.ConvID) 998 require.Equal(t, 1, len(convHit.Hits)) 999 verifyHit(convID, []chat1.MessageID{}, msgID10, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0]) 1000 verifySearchDone(1, true) 1001 1002 query = "hi" 1003 searchMatch = chat1.ChatSearchMatch{ 1004 StartIndex: 0, 1005 EndIndex: 2, 1006 Match: "hi", 1007 } 1008 msgID11 := sendMessage(chat1.NewMessageBodyWithText(chat1.MessageText{ 1009 Body: query, 1010 }), u1) 1011 1012 res = runSearch(query, opts, false /* expectedReindex*/) 1013 require.Equal(t, 1, len(res.Hits)) 1014 convHit = res.Hits[0] 1015 require.Equal(t, convID, convHit.ConvID) 1016 require.Equal(t, 1, len(convHit.Hits)) 1017 verifyHit(convID, []chat1.MessageID{msgID10}, msgID11, nil, []chat1.ChatSearchMatch{searchMatch}, convHit.Hits[0]) 1018 verifySearchDone(1, true) 1019 1020 err = indexer1.Clear(ctx, uid1, convID) 1021 require.NoError(t, err) 1022 pi, err = indexer1.PercentIndexed(ctx, convID) 1023 require.NoError(t, err) 1024 require.Zero(t, pi) 1025 1026 err = indexer2.Clear(ctx, uid2, convID) 1027 require.NoError(t, err) 1028 pi, err = indexer2.PercentIndexed(ctx, convID) 1029 require.NoError(t, err) 1030 require.Zero(t, pi) 1031 }) 1032 }