github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/chat/convsource_test.go (about) 1 package chat 2 3 import ( 4 "errors" 5 "fmt" 6 "testing" 7 "time" 8 9 "github.com/keybase/client/go/chat/globals" 10 "github.com/keybase/client/go/chat/utils" 11 "github.com/keybase/client/go/kbtest" 12 "github.com/keybase/client/go/protocol/chat1" 13 "github.com/keybase/client/go/protocol/gregor1" 14 "github.com/keybase/client/go/protocol/keybase1" 15 "github.com/stretchr/testify/require" 16 "golang.org/x/net/context" 17 ) 18 19 func TestGetThreadSupersedes(t *testing.T) { 20 testGetThreadSupersedes(t, false) 21 testGetThreadSupersedes(t, true) 22 } 23 24 func testGetThreadSupersedes(t *testing.T, deleteHistory bool) { 25 t.Logf("stage deleteHistory:%v", deleteHistory) 26 ctx, world, ri, _, sender, _ := setupTest(t, 1) 27 defer world.Cleanup() 28 29 u := world.GetUsers()[0] 30 tc := world.Tcs[u.Username] 31 trip := newConvTriple(ctx, t, tc, u.Username) 32 firstMessagePlaintext := chat1.MessagePlaintext{ 33 ClientHeader: chat1.MessageClientHeader{ 34 Conv: trip, 35 TlfName: u.Username, 36 TlfPublic: false, 37 MessageType: chat1.MessageType_TLFNAME, 38 }, 39 MessageBody: chat1.MessageBody{}, 40 } 41 prepareRes, err := sender.Prepare(ctx, firstMessagePlaintext, 42 chat1.ConversationMembersType_KBFS, nil, nil) 43 require.NoError(t, err) 44 firstMessageBoxed := prepareRes.Boxed 45 res, err := ri.NewConversationRemote2(ctx, chat1.NewConversationRemote2Arg{ 46 IdTriple: trip, 47 TLFMessage: firstMessageBoxed, 48 }) 49 require.NoError(t, err) 50 51 t.Logf("basic test") 52 _, msgBoxed, err := sender.Send(ctx, res.ConvID, chat1.MessagePlaintext{ 53 ClientHeader: chat1.MessageClientHeader{ 54 Conv: trip, 55 Sender: u.User.GetUID().ToBytes(), 56 TlfName: u.Username, 57 TlfPublic: false, 58 MessageType: chat1.MessageType_TEXT, 59 }, 60 MessageBody: chat1.NewMessageBodyWithText(chat1.MessageText{ 61 Body: "HIHI", 62 }), 63 }, 0, nil, nil, nil) 64 require.NoError(t, err) 65 msgID := msgBoxed.GetMessageID() 66 thread, err := tc.ChatG.ConvSource.Pull(ctx, res.ConvID, u.User.GetUID().ToBytes(), 67 chat1.GetThreadReason_GENERAL, nil, 68 &chat1.GetThreadQuery{ 69 MessageTypes: []chat1.MessageType{chat1.MessageType_TEXT}, 70 }, nil) 71 require.NoError(t, err) 72 require.Equal(t, 1, len(thread.Messages), "wrong length") 73 require.Equal(t, msgID, thread.Messages[0].GetMessageID(), "wrong msgID") 74 75 _, editMsgBoxed, err := sender.Send(ctx, res.ConvID, chat1.MessagePlaintext{ 76 ClientHeader: chat1.MessageClientHeader{ 77 Conv: trip, 78 Sender: u.User.GetUID().ToBytes(), 79 TlfName: u.Username, 80 TlfPublic: false, 81 MessageType: chat1.MessageType_EDIT, 82 Supersedes: msgID, 83 }, 84 MessageBody: chat1.NewMessageBodyWithEdit(chat1.MessageEdit{ 85 MessageID: msgID, 86 Body: "EDITED", 87 }), 88 }, 0, nil, nil, nil) 89 require.NoError(t, err) 90 editMsgID := editMsgBoxed.GetMessageID() 91 92 t.Logf("testing an edit") 93 thread, err = tc.ChatG.ConvSource.Pull(ctx, res.ConvID, u.User.GetUID().ToBytes(), 94 chat1.GetThreadReason_GENERAL, nil, 95 &chat1.GetThreadQuery{ 96 MessageTypes: []chat1.MessageType{chat1.MessageType_TEXT}, 97 }, nil) 98 require.NoError(t, err) 99 require.Equal(t, 1, len(thread.Messages), "wrong length") 100 require.Equal(t, msgID, thread.Messages[0].GetMessageID(), "wrong msgID") 101 require.Equal(t, editMsgID, thread.Messages[0].Valid().ServerHeader.SupersededBy, "wrong super") 102 require.Equal(t, "EDITED", thread.Messages[0].Valid().MessageBody.Text().Body, "wrong body") 103 104 t.Logf("testing a delete") 105 delTyp := chat1.MessageType_DELETE 106 delBody := chat1.NewMessageBodyWithDelete(chat1.MessageDelete{ 107 MessageIDs: []chat1.MessageID{msgID, editMsgID}, 108 }) 109 delSupersedes := msgID 110 var delHeader *chat1.MessageDeleteHistory 111 if deleteHistory { 112 delTyp = chat1.MessageType_DELETEHISTORY 113 delHeader = &chat1.MessageDeleteHistory{ 114 Upto: editMsgID + 1, 115 } 116 delBody = chat1.NewMessageBodyWithDeletehistory(*delHeader) 117 delSupersedes = 0 118 } 119 _, deleteMsgBoxed, err := sender.Send(ctx, res.ConvID, chat1.MessagePlaintext{ 120 ClientHeader: chat1.MessageClientHeader{ 121 Conv: trip, 122 Sender: u.User.GetUID().ToBytes(), 123 TlfName: u.Username, 124 TlfPublic: false, 125 MessageType: delTyp, 126 Supersedes: delSupersedes, 127 DeleteHistory: delHeader, 128 }, 129 MessageBody: delBody, 130 }, 0, nil, nil, nil) 131 require.NoError(t, err) 132 deleteMsgID := deleteMsgBoxed.GetMessageID() 133 thread, err = tc.ChatG.ConvSource.Pull(ctx, res.ConvID, u.User.GetUID().ToBytes(), 134 chat1.GetThreadReason_GENERAL, nil, 135 &chat1.GetThreadQuery{ 136 MessageTypes: []chat1.MessageType{chat1.MessageType_TEXT}, 137 }, nil) 138 require.NoError(t, err) 139 require.Equal(t, 0, len(thread.Messages), "wrong length") 140 141 t.Logf("testing disabling resolve") 142 thread, err = tc.ChatG.ConvSource.Pull(ctx, res.ConvID, u.User.GetUID().ToBytes(), 143 chat1.GetThreadReason_GENERAL, nil, 144 &chat1.GetThreadQuery{ 145 MessageTypes: []chat1.MessageType{ 146 chat1.MessageType_TEXT, 147 chat1.MessageType_EDIT, 148 chat1.MessageType_DELETE, 149 chat1.MessageType_DELETEHISTORY, 150 }, 151 DisableResolveSupersedes: true, 152 }, nil) 153 require.NoError(t, err) 154 require.Equal(t, 3, len(thread.Messages), "wrong length") 155 require.Equal(t, msgID, thread.Messages[2].GetMessageID(), "wrong msgID") 156 require.Equal(t, deleteMsgID, thread.Messages[2].Valid().ServerHeader.SupersededBy, "wrong super") 157 } 158 159 func TestExplodeNow(t *testing.T) { 160 ctx, world, ri, _, sender, _ := setupTest(t, 1) 161 defer world.Cleanup() 162 163 u := world.GetUsers()[0] 164 tc := world.Tcs[u.Username] 165 trip := newConvTriple(ctx, t, tc, u.Username) 166 firstMessagePlaintext := chat1.MessagePlaintext{ 167 ClientHeader: chat1.MessageClientHeader{ 168 Conv: trip, 169 TlfName: u.Username, 170 TlfPublic: false, 171 MessageType: chat1.MessageType_TLFNAME, 172 }, 173 MessageBody: chat1.MessageBody{}, 174 } 175 prepareRes, err := sender.Prepare(ctx, firstMessagePlaintext, 176 chat1.ConversationMembersType_TEAM, nil, nil) 177 require.NoError(t, err) 178 firstMessageBoxed := prepareRes.Boxed 179 res, err := ri.NewConversationRemote2(ctx, chat1.NewConversationRemote2Arg{ 180 IdTriple: trip, 181 TLFMessage: firstMessageBoxed, 182 }) 183 require.NoError(t, err) 184 185 t.Logf("basic test") 186 ephemeralMetadata := chat1.MsgEphemeralMetadata{ 187 Lifetime: 30, 188 } 189 _, msgBoxed, err := sender.Send(ctx, res.ConvID, chat1.MessagePlaintext{ 190 ClientHeader: chat1.MessageClientHeader{ 191 Conv: trip, 192 Sender: u.User.GetUID().ToBytes(), 193 TlfName: u.Username, 194 TlfPublic: false, 195 MessageType: chat1.MessageType_TEXT, 196 EphemeralMetadata: &ephemeralMetadata, 197 }, 198 MessageBody: chat1.NewMessageBodyWithText(chat1.MessageText{ 199 Body: "30s ephemeral", 200 }), 201 }, 0, nil, nil, nil) 202 require.NoError(t, err) 203 204 msgID := msgBoxed.GetMessageID() 205 thread, err := tc.ChatG.ConvSource.Pull(ctx, res.ConvID, u.User.GetUID().ToBytes(), 206 chat1.GetThreadReason_GENERAL, nil, 207 &chat1.GetThreadQuery{ 208 MessageTypes: []chat1.MessageType{chat1.MessageType_TEXT}, 209 }, nil) 210 211 require.NoError(t, err) 212 require.Equal(t, 1, len(thread.Messages), "wrong length") 213 msg1 := thread.Messages[0] 214 require.Equal(t, msgID, msg1.GetMessageID(), "wrong msgID") 215 require.True(t, msg1.IsValid()) 216 require.True(t, msg1.Valid().IsEphemeral()) 217 require.False(t, msg1.Valid().IsEphemeralExpired(time.Now())) 218 require.Nil(t, msg1.Valid().ExplodedBy()) 219 220 _, editMsgBoxed, err := sender.Send(ctx, res.ConvID, chat1.MessagePlaintext{ 221 ClientHeader: chat1.MessageClientHeader{ 222 Conv: trip, 223 Sender: u.User.GetUID().ToBytes(), 224 TlfName: u.Username, 225 TlfPublic: false, 226 MessageType: chat1.MessageType_EDIT, 227 Supersedes: msgID, 228 EphemeralMetadata: &ephemeralMetadata, 229 }, 230 MessageBody: chat1.NewMessageBodyWithEdit(chat1.MessageEdit{ 231 MessageID: msgID, 232 Body: "EDITED ephemeral", 233 }), 234 }, 0, nil, nil, nil) 235 require.NoError(t, err) 236 editMsgID := editMsgBoxed.GetMessageID() 237 238 t.Logf("testing an edit") 239 thread, err = tc.ChatG.ConvSource.Pull(ctx, res.ConvID, u.User.GetUID().ToBytes(), 240 chat1.GetThreadReason_GENERAL, nil, 241 &chat1.GetThreadQuery{ 242 MessageTypes: []chat1.MessageType{chat1.MessageType_TEXT}, 243 }, nil) 244 require.NoError(t, err) 245 require.Equal(t, 1, len(thread.Messages), "wrong length") 246 msg2 := thread.Messages[0] 247 require.Equal(t, msgID, msg2.GetMessageID(), "wrong msgID") 248 require.Equal(t, editMsgID, msg2.Valid().ServerHeader.SupersededBy, "wrong super") 249 require.Equal(t, "EDITED ephemeral", msg2.Valid().MessageBody.Text().Body, "wrong body") 250 require.True(t, msg2.Valid().IsEphemeral()) 251 require.False(t, msg2.Valid().IsEphemeralExpired(time.Now())) 252 require.Nil(t, msg2.Valid().ExplodedBy()) 253 254 t.Logf("testing a delete") 255 delBody := chat1.NewMessageBodyWithDelete(chat1.MessageDelete{ 256 MessageIDs: []chat1.MessageID{msgID, editMsgID}, 257 }) 258 delSupersedes := msgID 259 _, deleteMsgBoxed, err := sender.Send(ctx, res.ConvID, chat1.MessagePlaintext{ 260 ClientHeader: chat1.MessageClientHeader{ 261 Conv: trip, 262 Sender: u.User.GetUID().ToBytes(), 263 TlfName: u.Username, 264 TlfPublic: false, 265 MessageType: chat1.MessageType_DELETE, 266 Supersedes: delSupersedes, 267 }, 268 MessageBody: delBody, 269 }, 0, nil, nil, nil) 270 require.NoError(t, err) 271 272 deleteMsgID := deleteMsgBoxed.GetMessageID() 273 thread, err = tc.ChatG.ConvSource.Pull(ctx, res.ConvID, u.User.GetUID().ToBytes(), 274 chat1.GetThreadReason_GENERAL, nil, 275 &chat1.GetThreadQuery{ 276 MessageTypes: []chat1.MessageType{chat1.MessageType_TEXT}, 277 }, nil) 278 require.NoError(t, err) 279 require.Equal(t, 1, len(thread.Messages), "wrong length") 280 // Since we deleted an exploding message, it will still show up in the 281 // thread with the deleter set as "explodedBy" 282 msg3 := thread.Messages[0] 283 require.Equal(t, msgID, msg3.GetMessageID(), "wrong msgID") 284 require.Equal(t, deleteMsgID, msg3.Valid().ServerHeader.SupersededBy, "wrong super") 285 require.Equal(t, chat1.MessageBody{}, msg3.Valid().MessageBody, "wrong body") 286 require.True(t, msg3.Valid().IsEphemeral()) 287 // This is true since we did an explode now! 288 require.True(t, msg3.Valid().IsEphemeralExpired(time.Now())) 289 require.Equal(t, u.Username, *msg3.Valid().ExplodedBy()) 290 } 291 292 func TestReactions(t *testing.T) { 293 ctx, world, ri, _, sender, _ := setupTest(t, 1) 294 defer world.Cleanup() 295 296 u := world.GetUsers()[0] 297 uid := u.User.GetUID().ToBytes() 298 tc := world.Tcs[u.Username] 299 trip := newConvTriple(ctx, t, tc, u.Username) 300 firstMessagePlaintext := chat1.MessagePlaintext{ 301 ClientHeader: chat1.MessageClientHeader{ 302 Conv: trip, 303 TlfName: u.Username, 304 TlfPublic: false, 305 MessageType: chat1.MessageType_TLFNAME, 306 }, 307 MessageBody: chat1.MessageBody{}, 308 } 309 prepareRes, err := sender.Prepare(ctx, firstMessagePlaintext, 310 chat1.ConversationMembersType_TEAM, nil, nil) 311 require.NoError(t, err) 312 firstMessageBoxed := prepareRes.Boxed 313 314 res, err := ri.NewConversationRemote2(ctx, chat1.NewConversationRemote2Arg{ 315 IdTriple: trip, 316 TLFMessage: firstMessageBoxed, 317 }) 318 require.NoError(t, err) 319 320 verifyThread := func(msgID, supersededBy chat1.MessageID, body string, 321 reactionIDs []chat1.MessageID, reactionMap chat1.ReactionMap) { 322 thread, err := tc.ChatG.ConvSource.Pull(ctx, res.ConvID, uid, 323 chat1.GetThreadReason_GENERAL, nil, 324 &chat1.GetThreadQuery{ 325 MessageTypes: []chat1.MessageType{chat1.MessageType_TEXT}, 326 }, nil) 327 require.NoError(t, err) 328 require.Equal(t, 1, len(thread.Messages), "wrong length") 329 330 msg := thread.Messages[0] 331 require.Equal(t, msgID, msg.GetMessageID(), "wrong msgID") 332 require.True(t, msg.IsValid()) 333 require.Equal(t, body, msg.Valid().MessageBody.Text().Body, "wrong body") 334 require.Equal(t, supersededBy, msg.Valid().ServerHeader.SupersededBy, "wrong super") 335 require.Equal(t, reactionIDs, msg.Valid().ServerHeader.ReactionIDs, "wrong reactionIDs") 336 337 // Verify the ctimes are not zero, but we don't care about the actual 338 // value for the test. 339 for _, reactions := range msg.Valid().Reactions.Reactions { 340 for k, r := range reactions { 341 require.NotZero(t, r.Ctime) 342 r.Ctime = 0 343 reactions[k] = r 344 } 345 } 346 require.Equal(t, reactionMap, msg.Valid().Reactions, "wrong reactions") 347 } 348 349 sendText := func(body string) chat1.MessageID { 350 _, msgBoxed, err := sender.Send(ctx, res.ConvID, chat1.MessagePlaintext{ 351 ClientHeader: chat1.MessageClientHeader{ 352 Conv: trip, 353 Sender: uid, 354 TlfName: u.Username, 355 TlfPublic: false, 356 MessageType: chat1.MessageType_TEXT, 357 }, 358 MessageBody: chat1.NewMessageBodyWithText(chat1.MessageText{ 359 Body: body, 360 }), 361 }, 0, nil, nil, nil) 362 require.NoError(t, err) 363 return msgBoxed.GetMessageID() 364 } 365 366 sendEdit := func(editText string, supersedes chat1.MessageID) chat1.MessageID { 367 _, editMsgBoxed, err := sender.Send(ctx, res.ConvID, chat1.MessagePlaintext{ 368 ClientHeader: chat1.MessageClientHeader{ 369 Conv: trip, 370 Sender: uid, 371 TlfName: u.Username, 372 TlfPublic: false, 373 MessageType: chat1.MessageType_EDIT, 374 Supersedes: supersedes, 375 }, 376 MessageBody: chat1.NewMessageBodyWithEdit(chat1.MessageEdit{ 377 MessageID: supersedes, 378 Body: editText, 379 }), 380 }, 0, nil, nil, nil) 381 require.NoError(t, err) 382 return editMsgBoxed.GetMessageID() 383 } 384 385 sendReaction := func(reactionText string, supersedes chat1.MessageID) chat1.MessageID { 386 _, reactionMsgboxed, err := sender.Send(ctx, res.ConvID, chat1.MessagePlaintext{ 387 ClientHeader: chat1.MessageClientHeader{ 388 Conv: trip, 389 Sender: uid, 390 TlfName: u.Username, 391 TlfPublic: false, 392 MessageType: chat1.MessageType_REACTION, 393 Supersedes: supersedes, 394 }, 395 MessageBody: chat1.NewMessageBodyWithReaction(chat1.MessageReaction{ 396 MessageID: supersedes, 397 Body: reactionText, 398 }), 399 }, 0, nil, nil, nil) 400 require.NoError(t, err) 401 return reactionMsgboxed.GetMessageID() 402 } 403 404 sendDelete := func(supsersedes chat1.MessageID, deletes []chat1.MessageID) chat1.MessageID { 405 delBody := chat1.NewMessageBodyWithDelete(chat1.MessageDelete{ 406 MessageIDs: deletes, 407 }) 408 _, deleteMsgBoxed, err := sender.Send(ctx, res.ConvID, chat1.MessagePlaintext{ 409 ClientHeader: chat1.MessageClientHeader{ 410 Conv: trip, 411 Sender: uid, 412 TlfName: u.Username, 413 TlfPublic: false, 414 MessageType: chat1.MessageType_DELETE, 415 Supersedes: supsersedes, 416 }, 417 MessageBody: delBody, 418 }, 0, nil, nil, nil) 419 require.NoError(t, err) 420 return deleteMsgBoxed.GetMessageID() 421 422 } 423 424 t.Logf("send text") 425 body := "hi" 426 msgID := sendText(body) 427 verifyThread(msgID, 0 /* supersededBy */, body, nil, chat1.ReactionMap{}) 428 429 // Verify edits can happen around reactions and don't get clobbered 430 t.Logf("testing an edit") 431 body = "edited" 432 editMsgID := sendEdit(body, msgID) 433 verifyThread(msgID, editMsgID, body, nil, chat1.ReactionMap{}) 434 435 t.Logf("test +1 reaction") 436 reactionMsgID := sendReaction(":+1:", msgID) 437 expectedReactionMap := chat1.ReactionMap{ 438 Reactions: map[string]map[string]chat1.Reaction{ 439 ":+1:": { 440 u.Username: { 441 ReactionMsgID: reactionMsgID, 442 }, 443 }, 444 }, 445 } 446 verifyThread(msgID, editMsgID, body, []chat1.MessageID{reactionMsgID}, expectedReactionMap) 447 448 t.Logf("test -1 reaction") 449 reactionMsgID2 := sendReaction(":-1:", msgID) 450 expectedReactionMap.Reactions[":-1:"] = map[string]chat1.Reaction{ 451 u.Username: { 452 ReactionMsgID: reactionMsgID2, 453 }, 454 } 455 verifyThread(msgID, editMsgID, body, []chat1.MessageID{reactionMsgID, reactionMsgID2}, expectedReactionMap) 456 457 t.Logf("testing an edit2") 458 body = "edited2" 459 editMsgID2 := sendEdit(body, msgID) 460 verifyThread(msgID, editMsgID2, body, []chat1.MessageID{reactionMsgID, reactionMsgID2}, expectedReactionMap) 461 462 t.Logf("test multiple pulls") 463 // Verify pulling again returns the correct state 464 verifyThread(msgID, editMsgID2, body, []chat1.MessageID{reactionMsgID, reactionMsgID2}, expectedReactionMap) 465 466 t.Logf("test reaction deletion") 467 sendDelete(reactionMsgID2, []chat1.MessageID{reactionMsgID2}) 468 delete(expectedReactionMap.Reactions, ":-1:") 469 verifyThread(msgID, editMsgID2, body, []chat1.MessageID{reactionMsgID}, expectedReactionMap) 470 471 t.Logf("testing an edit3") 472 body = "edited3" 473 editMsgID3 := sendEdit(body, msgID) 474 verifyThread(msgID, editMsgID3, body, []chat1.MessageID{reactionMsgID}, expectedReactionMap) 475 476 t.Logf("test reaction after delete") 477 reactionMsgID3 := sendReaction(":-1:", msgID) 478 479 expectedReactionMap.Reactions[":-1:"] = map[string]chat1.Reaction{ 480 u.Username: { 481 ReactionMsgID: reactionMsgID3, 482 }, 483 } 484 verifyThread(msgID, editMsgID3, body, []chat1.MessageID{reactionMsgID, reactionMsgID3}, expectedReactionMap) 485 486 t.Logf("testing a delete") 487 sendDelete(msgID, []chat1.MessageID{msgID, reactionMsgID, reactionMsgID3}) 488 489 thread, err := tc.ChatG.ConvSource.Pull(ctx, res.ConvID, uid, 490 chat1.GetThreadReason_GENERAL, nil, 491 &chat1.GetThreadQuery{ 492 MessageTypes: []chat1.MessageType{chat1.MessageType_TEXT}, 493 }, nil) 494 require.NoError(t, err) 495 require.Equal(t, 0, len(thread.Messages), "wrong length") 496 497 // Post illegal supersedes=0, fails on send 498 _, _, err = sender.Send(ctx, res.ConvID, chat1.MessagePlaintext{ 499 ClientHeader: chat1.MessageClientHeader{ 500 Conv: trip, 501 Sender: uid, 502 TlfName: u.Username, 503 TlfPublic: false, 504 MessageType: chat1.MessageType_REACTION, 505 Supersedes: 0, 506 }, 507 MessageBody: chat1.NewMessageBodyWithReaction(chat1.MessageReaction{ 508 MessageID: 0, 509 Body: ":wave:", 510 }), 511 }, 0, nil, nil, nil) 512 require.Error(t, err) 513 } 514 515 type noGetThreadRemote struct { 516 *kbtest.ChatRemoteMock 517 } 518 519 func newNoGetThreadRemote(mock *kbtest.ChatRemoteMock) *noGetThreadRemote { 520 return &noGetThreadRemote{ 521 ChatRemoteMock: mock, 522 } 523 } 524 525 func (n *noGetThreadRemote) GetThreadRemote(ctx context.Context, arg chat1.GetThreadRemoteArg) (chat1.GetThreadRemoteRes, error) { 526 return chat1.GetThreadRemoteRes{}, errors.New("GetThreadRemote banned") 527 } 528 529 func TestGetThreadHoleResolution(t *testing.T) { 530 ctx, world, ri2, _, sender, _ := setupTest(t, 1) 531 defer world.Cleanup() 532 533 ri := ri2.(*kbtest.ChatRemoteMock) 534 u := world.GetUsers()[0] 535 uid := u.User.GetUID().ToBytes() 536 tc := world.Tcs[u.Username] 537 syncer := NewSyncer(tc.Context()) 538 syncer.isConnected = true 539 <-tc.ChatG.ConvLoader.Stop(context.Background()) 540 541 conv, remoteConv := newConv(ctx, t, tc, uid, ri, sender, u.Username) 542 convID := conv.GetConvID() 543 pt := chat1.MessagePlaintext{ 544 ClientHeader: chat1.MessageClientHeader{ 545 Conv: conv.Info.Triple, 546 Sender: u.User.GetUID().ToBytes(), 547 TlfName: u.Username, 548 TlfPublic: false, 549 MessageType: chat1.MessageType_TEXT, 550 }, 551 MessageBody: chat1.NewMessageBodyWithText(chat1.MessageText{ 552 Body: "HIHI", 553 }), 554 } 555 556 var msg *chat1.MessageBoxed 557 var err error 558 holes := 3 559 for i := 0; i < holes; i++ { 560 pt.MessageBody = chat1.NewMessageBodyWithText(chat1.MessageText{ 561 Body: fmt.Sprintf("MIKE: %d", i), 562 }) 563 prepareRes, err := sender.Prepare(ctx, pt, chat1.ConversationMembersType_KBFS, &conv, nil) 564 require.NoError(t, err) 565 msg = &prepareRes.Boxed 566 567 res, err := ri.PostRemote(ctx, chat1.PostRemoteArg{ 568 ConversationID: conv.GetConvID(), 569 MessageBoxed: *msg, 570 }) 571 require.NoError(t, err) 572 msg.ServerHeader = &res.MsgHeader 573 } 574 575 remoteConv.MaxMsgs = []chat1.MessageBoxed{*msg} 576 remoteConv.MaxMsgSummaries = []chat1.MessageSummary{msg.Summary()} 577 remoteConv.ReaderInfo.MaxMsgid = msg.GetMessageID() 578 ri.SyncInboxFunc = func(m *kbtest.ChatRemoteMock, ctx context.Context, vers chat1.InboxVers) (chat1.SyncInboxRes, error) { 579 return chat1.NewSyncInboxResWithIncremental(chat1.SyncIncrementalRes{ 580 Vers: vers + 1, 581 Convs: []chat1.Conversation{remoteConv}, 582 }), nil 583 } 584 doSync(t, syncer, ri, uid) 585 586 localThread, err := tc.Context().ConvSource.PullLocalOnly(ctx, convID, uid, chat1.GetThreadReason_GENERAL, nil, nil, 0) 587 require.NoError(t, err) 588 require.Equal(t, 2, len(localThread.Messages)) 589 590 tc.Context().ConvSource.SetRemoteInterface(func() chat1.RemoteInterface { 591 return newNoGetThreadRemote(ri) 592 }) 593 thread, err := tc.Context().ConvSource.Pull(ctx, convID, uid, chat1.GetThreadReason_GENERAL, nil, nil, 594 nil) 595 require.NoError(t, err) 596 require.Equal(t, holes+2, len(thread.Messages)) 597 require.Equal(t, msg.GetMessageID(), thread.Messages[0].GetMessageID()) 598 require.Equal(t, "MIKE: 2", thread.Messages[0].Valid().MessageBody.Text().Body) 599 600 // Make sure we don't consider it a hit if we end the fetch with a hole 601 require.NoError(t, tc.Context().ConvSource.Clear(ctx, convID, uid, nil)) 602 _, err = tc.Context().ConvSource.Pull(ctx, convID, uid, chat1.GetThreadReason_GENERAL, nil, nil, nil) 603 require.Error(t, err) 604 } 605 606 type acquireRes struct { 607 blocked bool 608 err error 609 } 610 611 func timedAcquire(ctx context.Context, t *testing.T, hcs *HybridConversationSource, uid gregor1.UID, convID chat1.ConversationID) (ret bool, err error) { 612 cb := make(chan struct{}) 613 go func() { 614 ret, err = hcs.lockTab.Acquire(ctx, uid, convID) 615 close(cb) 616 }() 617 select { 618 case <-cb: 619 case <-time.After(20 * time.Second): 620 require.Fail(t, "acquire timeout") 621 } 622 return ret, err 623 } 624 625 func TestConversationLocking(t *testing.T) { 626 ctx, world, ri2, _, sender, _ := setupTest(t, 1) 627 defer world.Cleanup() 628 629 ri := ri2.(*kbtest.ChatRemoteMock) 630 u := world.GetUsers()[0] 631 uid := u.User.GetUID().ToBytes() 632 tc := world.Tcs[u.Username] 633 syncer := NewSyncer(tc.Context()) 634 syncer.isConnected = true 635 <-tc.Context().ConvLoader.Stop(context.TODO()) 636 hcs := tc.Context().ConvSource.(*HybridConversationSource) 637 if hcs == nil { 638 t.Skip() 639 } 640 641 conv, _ := newConv(ctx, t, tc, uid, ri, sender, u.Username) 642 643 t.Logf("Trace 1 can get multiple locks") 644 var breaks []keybase1.TLFIdentifyFailure 645 ctx = globals.ChatCtx(context.TODO(), tc.Context(), keybase1.TLFIdentifyBehavior_CHAT_CLI, &breaks, 646 NewCachingIdentifyNotifier(tc.Context())) 647 acquires := 5 648 for i := 0; i < acquires; i++ { 649 _, err := timedAcquire(ctx, t, hcs, uid, conv.GetConvID()) 650 require.NoError(t, err) 651 } 652 for i := 0; i < acquires; i++ { 653 hcs.lockTab.Release(ctx, uid, conv.GetConvID()) 654 } 655 require.Zero(t, hcs.lockTab.NumLocks()) 656 657 t.Logf("Trace 2 properly blocked by Trace 1") 658 ctx2 := globals.ChatCtx(context.TODO(), tc.Context(), keybase1.TLFIdentifyBehavior_CHAT_CLI, 659 &breaks, NewCachingIdentifyNotifier(tc.Context())) 660 blockCb := make(chan struct{}, 5) 661 hcs.lockTab.SetBlockCb(&blockCb) 662 cb := make(chan acquireRes) 663 blocked, err := timedAcquire(ctx, t, hcs, uid, conv.GetConvID()) 664 require.NoError(t, err) 665 require.False(t, blocked) 666 go func() { 667 blocked, err = timedAcquire(ctx2, t, hcs, uid, conv.GetConvID()) 668 cb <- acquireRes{blocked: blocked, err: err} 669 }() 670 select { 671 case <-cb: 672 require.Fail(t, "should have blocked") 673 default: 674 } 675 // Wait for the thread to get blocked 676 select { 677 case <-blockCb: 678 case <-time.After(20 * time.Second): 679 require.Fail(t, "not blocked") 680 } 681 682 require.True(t, hcs.lockTab.Release(ctx, uid, conv.GetConvID())) 683 select { 684 case res := <-cb: 685 require.NoError(t, res.err) 686 require.True(t, res.blocked) 687 case <-time.After(20 * time.Second): 688 require.Fail(t, "not blocked") 689 } 690 require.True(t, hcs.lockTab.Release(ctx2, uid, conv.GetConvID())) 691 require.Zero(t, hcs.lockTab.NumLocks()) 692 693 t.Logf("No trace") 694 blocked, err = timedAcquire(context.TODO(), t, hcs, uid, conv.GetConvID()) 695 require.NoError(t, err) 696 require.False(t, blocked) 697 blocked, err = timedAcquire(context.TODO(), t, hcs, uid, conv.GetConvID()) 698 require.NoError(t, err) 699 require.False(t, blocked) 700 require.Zero(t, hcs.lockTab.NumLocks()) 701 } 702 703 func TestConversationLockingDeadlock(t *testing.T) { 704 ctx, world, ri2, _, sender, _ := setupTest(t, 3) 705 defer world.Cleanup() 706 707 ri := ri2.(*kbtest.ChatRemoteMock) 708 u := world.GetUsers()[0] 709 u2 := world.GetUsers()[1] 710 u3 := world.GetUsers()[2] 711 uid := u.User.GetUID().ToBytes() 712 tc := world.Tcs[u.Username] 713 syncer := NewSyncer(tc.Context()) 714 syncer.isConnected = true 715 <-tc.Context().ConvLoader.Stop(context.TODO()) 716 hcs := tc.Context().ConvSource.(*HybridConversationSource) 717 if hcs == nil { 718 t.Skip() 719 } 720 conv := newBlankConvWithMembersType(ctx, t, tc, uid, ri, sender, u.Username, 721 chat1.ConversationMembersType_KBFS) 722 conv2 := newBlankConvWithMembersType(ctx, t, tc, uid, ri, sender, u2.Username+","+u.Username, 723 chat1.ConversationMembersType_KBFS) 724 conv3 := newBlankConvWithMembersType(ctx, t, tc, uid, ri, sender, u3.Username+","+u.Username, 725 chat1.ConversationMembersType_KBFS) 726 727 var breaks []keybase1.TLFIdentifyFailure 728 ctx = globals.ChatCtx(context.TODO(), tc.Context(), keybase1.TLFIdentifyBehavior_CHAT_CLI, &breaks, 729 NewCachingIdentifyNotifier(tc.Context())) 730 ctx2 := globals.ChatCtx(context.TODO(), tc.Context(), keybase1.TLFIdentifyBehavior_CHAT_CLI, &breaks, 731 NewCachingIdentifyNotifier(tc.Context())) 732 ctx3 := globals.ChatCtx(context.TODO(), tc.Context(), keybase1.TLFIdentifyBehavior_CHAT_CLI, &breaks, 733 NewCachingIdentifyNotifier(tc.Context())) 734 735 blocked, err := timedAcquire(ctx, t, hcs, uid, conv.GetConvID()) 736 require.NoError(t, err) 737 require.False(t, blocked) 738 blocked, err = timedAcquire(ctx2, t, hcs, uid, conv2.GetConvID()) 739 require.NoError(t, err) 740 require.False(t, blocked) 741 blocked, err = timedAcquire(ctx3, t, hcs, uid, conv3.GetConvID()) 742 require.NoError(t, err) 743 require.False(t, blocked) 744 745 blockCb := make(chan struct{}, 5) 746 hcs.lockTab.SetBlockCb(&blockCb) 747 cb := make(chan acquireRes) 748 go func() { 749 blocked, err = hcs.lockTab.Acquire(ctx, uid, conv2.GetConvID()) 750 cb <- acquireRes{blocked: blocked, err: err} 751 }() 752 select { 753 case <-blockCb: 754 case <-time.After(20 * time.Second): 755 require.Fail(t, "not blocked") 756 } 757 758 hcs.lockTab.SetMaxAcquireRetries(1) 759 cb2 := make(chan acquireRes) 760 go func() { 761 blocked, err = hcs.lockTab.Acquire(ctx2, uid, conv3.GetConvID()) 762 cb2 <- acquireRes{blocked: blocked, err: err} 763 }() 764 select { 765 case <-blockCb: 766 case <-time.After(20 * time.Second): 767 require.Fail(t, "not blocked") 768 } 769 770 cb3 := make(chan acquireRes) 771 go func() { 772 blocked, err = hcs.lockTab.Acquire(ctx3, uid, conv.GetConvID()) 773 cb3 <- acquireRes{blocked: blocked, err: err} 774 }() 775 select { 776 case <-blockCb: 777 case <-time.After(20 * time.Second): 778 require.Fail(t, "not blocked") 779 } 780 select { 781 case res := <-cb3: 782 require.Error(t, res.err) 783 require.IsType(t, utils.ErrConvLockTabDeadlock, res.err) 784 case <-time.After(20 * time.Second): 785 require.Fail(t, "never failed") 786 } 787 788 require.True(t, hcs.lockTab.Release(ctx, uid, conv.GetConvID())) 789 blocked, err = timedAcquire(ctx3, t, hcs, uid, conv.GetConvID()) 790 require.NoError(t, err) 791 require.False(t, blocked) 792 require.True(t, hcs.lockTab.Release(ctx2, uid, conv2.GetConvID())) 793 select { 794 case res := <-cb: 795 require.NoError(t, res.err) 796 require.True(t, res.blocked) 797 case <-time.After(20 * time.Second): 798 require.Fail(t, "not blocked") 799 } 800 require.True(t, hcs.lockTab.Release(ctx3, uid, conv3.GetConvID())) 801 select { 802 case res := <-cb2: 803 require.NoError(t, res.err) 804 require.True(t, res.blocked) 805 case <-time.After(20 * time.Second): 806 require.Fail(t, "not blocked") 807 } 808 809 require.True(t, hcs.lockTab.Release(ctx, uid, conv2.GetConvID())) 810 require.True(t, hcs.lockTab.Release(ctx2, uid, conv3.GetConvID())) 811 }