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  }