github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/chat/storage/storage_test.go (about)

     1  package storage
     2  
     3  import (
     4  	"crypto/rand"
     5  	"math/big"
     6  	"sort"
     7  	"testing"
     8  
     9  	"github.com/keybase/client/go/chat/globals"
    10  	"github.com/keybase/client/go/chat/pager"
    11  	"github.com/keybase/client/go/chat/types"
    12  	"github.com/keybase/client/go/chat/utils"
    13  	"github.com/keybase/client/go/externalstest"
    14  	"github.com/keybase/client/go/kbtest"
    15  	"github.com/keybase/client/go/libkb"
    16  	"github.com/keybase/client/go/protocol/chat1"
    17  	"github.com/keybase/client/go/protocol/gregor1"
    18  	insecureTriplesec "github.com/keybase/go-triplesec-insecure"
    19  	"github.com/stretchr/testify/require"
    20  	"golang.org/x/net/context"
    21  )
    22  
    23  func setupCommonTest(t testing.TB, name string) kbtest.ChatTestContext {
    24  	tc := externalstest.SetupTest(t, name, 2)
    25  
    26  	// use an insecure triplesec in tests
    27  	tc.G.NewTriplesec = func(passphrase []byte, salt []byte) (libkb.Triplesec, error) {
    28  		warner := func() { tc.G.Log.Warning("Installing insecure Triplesec with weak stretch parameters") }
    29  		isProduction := func() bool {
    30  			return tc.G.Env.GetRunMode() == libkb.ProductionRunMode
    31  		}
    32  		return insecureTriplesec.NewCipher(passphrase, salt, libkb.ClientTriplesecVersion, warner, isProduction)
    33  	}
    34  	ctc := kbtest.ChatTestContext{
    35  		TestContext: tc,
    36  		ChatG: &globals.ChatContext{
    37  			AttachmentUploader: types.DummyAttachmentUploader{},
    38  			Unfurler:           types.DummyUnfurler{},
    39  			EphemeralPurger:    types.DummyEphemeralPurger{},
    40  			EphemeralTracker:   types.DummyEphemeralTracker{},
    41  			Indexer:            types.DummyIndexer{},
    42  			CtxFactory:         dummyContextFactory{},
    43  		},
    44  	}
    45  	ctc.Context().ServerCacheVersions = NewServerVersions(ctc.Context())
    46  	return ctc
    47  }
    48  
    49  func setupStorageTest(t testing.TB, name string) (kbtest.ChatTestContext, *Storage, gregor1.UID) {
    50  	ctc := setupCommonTest(t, name)
    51  	u, err := kbtest.CreateAndSignupFakeUser("cs", ctc.TestContext.G)
    52  	require.NoError(t, err)
    53  	return ctc, New(ctc.Context(), kbtest.NewDummyAssetDeleter()), gregor1.UID(u.User.GetUID().ToBytes())
    54  }
    55  
    56  func mustMerge(t testing.TB, storage *Storage,
    57  	convID chat1.ConversationID, uid gregor1.UID, msgs []chat1.MessageUnboxed) MergeResult {
    58  	conv, err := NewInbox(storage.G()).GetConversation(context.Background(), uid, convID)
    59  	switch err.(type) {
    60  	case nil:
    61  	case MissError:
    62  		conv = types.NewEmptyRemoteConversation(convID)
    63  	default:
    64  		require.NoError(t, err)
    65  	}
    66  	res, err := storage.Merge(context.Background(), conv, uid, msgs)
    67  	require.NoError(t, err)
    68  	return res
    69  }
    70  
    71  func makeMsgRange(max int) (res []chat1.MessageUnboxed) {
    72  	for i := max; i > 0; i-- {
    73  		res = append(res, MakeText(chat1.MessageID(i), "junk text"))
    74  	}
    75  	return res
    76  }
    77  
    78  func addMsgs(num int, msgs []chat1.MessageUnboxed) []chat1.MessageUnboxed {
    79  	maxID := msgs[0].GetMessageID()
    80  	for i := 0; i < num; i++ {
    81  		msgs = append([]chat1.MessageUnboxed{MakeText(chat1.MessageID(int(maxID)+i+1), "addMsgs junk text")},
    82  			msgs...)
    83  	}
    84  	return msgs
    85  }
    86  
    87  func doSimpleBench(b *testing.B, storage *Storage, uid gregor1.UID) {
    88  	msgs := makeMsgRange(100000)
    89  	conv := MakeConversation(msgs[0].GetMessageID())
    90  	b.ResetTimer()
    91  
    92  	for i := 0; i < b.N; i++ {
    93  		mustMerge(b, storage, conv.Metadata.ConversationID, uid, msgs)
    94  		_, err := storage.Fetch(context.TODO(), conv, uid, nil, nil, nil)
    95  		require.NoError(b, err)
    96  		err = storage.ClearAll(context.TODO(), conv.Metadata.ConversationID, uid)
    97  		require.NoError(b, err)
    98  	}
    99  }
   100  
   101  func doCommonBench(b *testing.B, storage *Storage, uid gregor1.UID) {
   102  	msgs := makeMsgRange(107)
   103  	conv := MakeConversation(msgs[0].GetMessageID())
   104  	b.ResetTimer()
   105  	for i := 0; i < b.N; i++ {
   106  		mustMerge(b, storage, conv.Metadata.ConversationID, uid, msgs)
   107  		_, err := storage.Fetch(context.TODO(), conv, uid, nil, nil, nil)
   108  		require.NoError(b, err)
   109  
   110  		// Add some msgs
   111  		b.StopTimer()
   112  		newmsgs := addMsgs(15, msgs)
   113  		newconv := MakeConversation(newmsgs[0].GetMessageID())
   114  		b.StartTimer()
   115  
   116  		mustMerge(b, storage, conv.Metadata.ConversationID, uid, newmsgs)
   117  		_, err = storage.Fetch(context.TODO(), newconv, uid, nil, nil, nil)
   118  		require.NoError(b, err)
   119  	}
   120  }
   121  
   122  func doRandomBench(b *testing.B, storage *Storage, uid gregor1.UID, num, len int) {
   123  	msgs := makeMsgRange(num)
   124  	conv := MakeConversation(msgs[0].GetMessageID())
   125  	b.ResetTimer()
   126  	for i := 0; i < b.N; i++ {
   127  		mustMerge(b, storage, conv.Metadata.ConversationID, uid, msgs)
   128  		for j := 0; j < 300; j++ {
   129  
   130  			b.StopTimer()
   131  			var bi *big.Int
   132  			var err error
   133  			for {
   134  				bi, err = rand.Int(rand.Reader, big.NewInt(int64(num)))
   135  				require.NoError(b, err)
   136  				if bi.Int64() > 1 {
   137  					break
   138  				}
   139  			}
   140  			next, err := encode(chat1.MessageID(bi.Int64()))
   141  			require.NoError(b, err)
   142  			p := chat1.Pagination{
   143  				Num:  len,
   144  				Next: next,
   145  			}
   146  			b.StartTimer()
   147  
   148  			_, err = storage.Fetch(context.TODO(), conv, uid, nil, nil, &p)
   149  			require.NoError(b, err)
   150  		}
   151  	}
   152  }
   153  
   154  func BenchmarkStorageSimpleBlockEngine(b *testing.B) {
   155  	tc, storage, uid := setupStorageTest(b, "basic")
   156  	defer tc.Cleanup()
   157  	storage.setEngine(newBlockEngine(tc.Context()))
   158  	doSimpleBench(b, storage, uid)
   159  }
   160  
   161  func BenchmarkStorageCommonBlockEngine(b *testing.B) {
   162  	tc, storage, uid := setupStorageTest(b, "basic")
   163  	defer tc.Cleanup()
   164  	storage.setEngine(newBlockEngine(tc.Context()))
   165  	doCommonBench(b, storage, uid)
   166  }
   167  
   168  func BenchmarkStorageRandomBlockEngine(b *testing.B) {
   169  	tc, storage, uid := setupStorageTest(b, "basic")
   170  	defer tc.Cleanup()
   171  	storage.setEngine(newBlockEngine(tc.Context()))
   172  	doRandomBench(b, storage, uid, 127, 1)
   173  }
   174  
   175  func BenchmarkStorageRandomLongBlockEngine(b *testing.B) {
   176  	tc, storage, uid := setupStorageTest(b, "basic")
   177  	defer tc.Cleanup()
   178  	storage.setEngine(newBlockEngine(tc.Context()))
   179  	doRandomBench(b, storage, uid, 127, 1)
   180  }
   181  
   182  func TestStorageBasic(t *testing.T) {
   183  	tc, storage, uid := setupStorageTest(t, "basic")
   184  	defer tc.Cleanup()
   185  
   186  	msgs := makeMsgRange(10)
   187  	conv := MakeConversation(msgs[0].GetMessageID())
   188  
   189  	mustMerge(t, storage, conv.Metadata.ConversationID, uid, msgs)
   190  	fetchRes, err := storage.Fetch(context.TODO(), conv, uid, nil, nil, nil)
   191  	require.NoError(t, err)
   192  	res := fetchRes.Thread
   193  	require.Equal(t, len(msgs), len(res.Messages), "wrong amount of messages")
   194  	for i := 0; i < len(res.Messages); i++ {
   195  		require.Equal(t, msgs[i].GetMessageID(), res.Messages[i].GetMessageID(), "msg mismatch")
   196  	}
   197  }
   198  
   199  func TestStorageLargeList(t *testing.T) {
   200  	tc, storage, uid := setupStorageTest(t, "large list")
   201  	defer tc.Cleanup()
   202  
   203  	msgs := makeMsgRange(1000)
   204  	conv := MakeConversation(msgs[0].GetMessageID())
   205  
   206  	mustMerge(t, storage, conv.Metadata.ConversationID, uid, msgs)
   207  	fetchRes, err := storage.Fetch(context.TODO(), conv, uid, nil, nil, nil)
   208  	require.NoError(t, err)
   209  	res := fetchRes.Thread
   210  	require.Equal(t, len(msgs), len(res.Messages), "wrong amount of messages")
   211  	require.Equal(t, utils.PluckMUMessageIDs(msgs), utils.PluckMUMessageIDs(res.Messages))
   212  
   213  }
   214  
   215  func TestStorageBlockBoundary(t *testing.T) {
   216  	tc, storage, uid := setupStorageTest(t, "block boundary")
   217  	defer tc.Cleanup()
   218  	msgs := makeMsgRange(blockSize - 1)
   219  	conv := MakeConversation(msgs[0].GetMessageID())
   220  	mustMerge(t, storage, conv.Metadata.ConversationID, uid, msgs)
   221  	fetchRes, err := storage.Fetch(context.TODO(), conv, uid, nil, nil, nil)
   222  	require.NoError(t, err)
   223  	res := fetchRes.Thread
   224  	require.Equal(t, len(msgs), len(res.Messages), "wrong amount of messages")
   225  	require.Equal(t, utils.PluckMUMessageIDs(msgs), utils.PluckMUMessageIDs(res.Messages))
   226  	appendMsg := MakeText(chat1.MessageID(blockSize), "COMBOBREAKER")
   227  	mustMerge(t, storage, conv.Metadata.ConversationID, uid, []chat1.MessageUnboxed{appendMsg})
   228  	conv.ReaderInfo.MaxMsgid = chat1.MessageID(blockSize)
   229  	fetchRes, err = storage.Fetch(context.TODO(), conv, uid, nil, nil, nil)
   230  	msgs = append([]chat1.MessageUnboxed{appendMsg}, msgs...)
   231  	require.NoError(t, err)
   232  	res = fetchRes.Thread
   233  	require.Equal(t, len(msgs), len(res.Messages), "wrong amount of messages")
   234  	require.Equal(t, utils.PluckMUMessageIDs(msgs), utils.PluckMUMessageIDs(res.Messages))
   235  }
   236  
   237  func TestStorageSupersedes(t *testing.T) {
   238  	var err error
   239  
   240  	tc, storage, uid := setupStorageTest(t, "supersedes")
   241  	defer tc.Cleanup()
   242  
   243  	// First test an Edit message.
   244  	supersedingEdit := MakeEdit(chat1.MessageID(111), 6)
   245  
   246  	msgs := makeMsgRange(110)
   247  	msgs = append([]chat1.MessageUnboxed{supersedingEdit}, msgs...)
   248  	conv := MakeConversation(msgs[0].GetMessageID())
   249  
   250  	mustMerge(t, storage, conv.Metadata.ConversationID, uid, msgs)
   251  	fetchRes, err := storage.Fetch(context.TODO(), conv, uid, nil, nil, nil)
   252  	require.NoError(t, err)
   253  	res := fetchRes.Thread
   254  	require.Equal(t, len(msgs), len(res.Messages), "wrong amount of messages")
   255  	for i := 0; i < len(res.Messages); i++ {
   256  		require.Equal(t, msgs[i].GetMessageID(), res.Messages[i].GetMessageID(), "msg mismatch")
   257  	}
   258  	sheader := res.Messages[len(msgs)-6].Valid().ServerHeader
   259  	require.Equal(t, chat1.MessageID(6), sheader.MessageID, "MessageID incorrect")
   260  	require.Equal(t, chat1.MessageID(111), sheader.SupersededBy, "supersededBy incorrect")
   261  
   262  	// Now test a delete message. This should result in the deletion of *both*
   263  	// the original message's body and the body of the edit above.
   264  	supersedingDelete := MakeDelete(chat1.MessageID(112), 6, []chat1.MessageID{111})
   265  
   266  	mustMerge(t, storage, conv.Metadata.ConversationID, uid, []chat1.MessageUnboxed{supersedingDelete})
   267  	conv.ReaderInfo.MaxMsgid = 112
   268  	msgs = append([]chat1.MessageUnboxed{supersedingDelete}, msgs...)
   269  	fetchRes, err = storage.Fetch(context.TODO(), conv, uid, nil, nil, nil)
   270  	require.NoError(t, err)
   271  	res = fetchRes.Thread
   272  
   273  	deletedMessage := res.Messages[len(msgs)-6].Valid()
   274  	deletedHeader := deletedMessage.ServerHeader
   275  	require.Equal(t, chat1.MessageID(6), deletedHeader.MessageID, "MessageID incorrect")
   276  	require.Equal(t, chat1.MessageID(112), deletedHeader.SupersededBy, "supersededBy incorrect")
   277  	// Check that the body is deleted.
   278  	deletedBodyType, err := deletedMessage.MessageBody.MessageType()
   279  	require.NoError(t, err)
   280  	require.Equal(t, chat1.MessageType_NONE, deletedBodyType, "expected the body to be deleted, but it's not!!!")
   281  	// Check that the body of the edit is *also* is deleted.
   282  	deletedEdit := res.Messages[len(msgs)-111].Valid()
   283  	deletedEditBodyType, err := deletedEdit.MessageBody.MessageType()
   284  	require.NoError(t, err)
   285  	require.Equal(t, chat1.MessageType_NONE, deletedEditBodyType, "expected the edit's body to be deleted also, but it's not!!!")
   286  }
   287  
   288  func TestStorageDeleteHistory(t *testing.T) {
   289  	// Uses this conversation:
   290  	// A start                            <not deletable>
   291  	// B text
   292  	// C text <----\        edited by E
   293  	// D headline  |
   294  	// E edit -----^        edits C
   295  	// F text <---\         deleted by G
   296  	// G delete --^    ___  deletes F     <not deletable>
   297  	// H text           |
   298  	// I headline       |                 <not deletable>
   299  	// J delete-history ^ upto H
   300  	// K delete-history upto itself
   301  	// L text
   302  
   303  	tc, storage, uid := setupStorageTest(t, "delh")
   304  	defer tc.Cleanup()
   305  	inbox := NewInbox(tc.Context())
   306  	conv := makeConvo(gregor1.Time(0), 1, 1)
   307  	conv.Conv.MaxMsgSummaries = append(conv.Conv.MaxMsgSummaries, chat1.MessageSummary{MessageType: chat1.MessageType_HEADLINE, MsgID: 9})
   308  	require.NoError(t, inbox.Merge(context.TODO(), uid, 1, utils.PluckConvs([]types.RemoteConversation{conv}), nil))
   309  
   310  	convID := conv.GetConvID()
   311  	msgA := MakeMsgWithType(1, chat1.MessageType_TLFNAME)
   312  	msgB := MakeText(2, "some text")
   313  	msgC := MakeText(3, "some text")
   314  	msgD := MakeHeadlineMessage(4)
   315  	msgE := MakeEdit(5, msgC.GetMessageID())
   316  	msgF := MakeText(6, "some text")
   317  	msgG := MakeDelete(7, msgF.GetMessageID(), nil)
   318  	msgH := MakeText(8, "some text")
   319  	msgI := MakeHeadlineMessage(9)
   320  	msgJ := MakeDeleteHistory(10, msgI.GetMessageID())
   321  	msgK := MakeDeleteHistory(11, 11)
   322  	msgL := MakeText(12, "some text")
   323  
   324  	type expectedM struct {
   325  		Name         string // letter label
   326  		MsgID        chat1.MessageID
   327  		BodyPresent  bool
   328  		SupersededBy chat1.MessageID
   329  	}
   330  
   331  	var expectedState []expectedM // expectations sorted by ID ascending
   332  	setExpected := func(name string, msg chat1.MessageUnboxed, bodyPresent bool, supersededBy chat1.MessageID) {
   333  		xset := expectedM{name, msg.GetMessageID(), bodyPresent, supersededBy}
   334  		var found bool
   335  		for i, x := range expectedState {
   336  			if x.Name == name {
   337  				found = true
   338  				expectedState[i] = xset
   339  			}
   340  		}
   341  		if !found {
   342  			expectedState = append(expectedState, xset)
   343  		}
   344  		sort.Slice(expectedState, func(i, j int) bool {
   345  			return expectedState[i].MsgID < expectedState[j].MsgID
   346  		})
   347  	}
   348  	assertStateHelper := func(maxMsgID chat1.MessageID, allowHoles bool) {
   349  		var rc ResultCollector
   350  		if allowHoles {
   351  			rc = NewInsatiableResultCollector()
   352  		}
   353  		fetchRes, err := storage.Fetch(context.Background(), MakeConversationAt(convID, maxMsgID), uid, rc,
   354  			nil, nil)
   355  		require.NoError(t, err)
   356  		res := fetchRes.Thread
   357  		if len(res.Messages) != len(expectedState) {
   358  			t.Logf("wrong number of messages")
   359  			for _, m := range res.Messages {
   360  				t.Logf("msgid:%v type:%v", m.GetMessageID(), m.GetMessageType())
   361  			}
   362  			require.Equal(t, len(expectedState), len(res.Messages), "wrong number of messages")
   363  		}
   364  		for i, x := range expectedState {
   365  			t.Logf("[%v] checking msgID:%v supersededBy:%v", x.Name, x.MsgID, x.SupersededBy)
   366  			m := res.Messages[len(res.Messages)-1-i]
   367  			require.True(t, m.IsValid(), "[%v] message should be valid", x.Name)
   368  			require.Equal(t, x.MsgID, m.Valid().ServerHeader.MessageID, "[%v] message ID", x.Name)
   369  			if m.GetMessageType() != chat1.MessageType_TLFNAME {
   370  				if !x.BodyPresent && x.SupersededBy == 0 {
   371  					t.Fatalf("You expected the body to be deleted but the message not to be superseded. Are you sure?")
   372  				}
   373  			}
   374  			require.Equal(t, x.SupersededBy, m.Valid().ServerHeader.SupersededBy, "[%v] superseded by", x.Name)
   375  			if x.BodyPresent {
   376  				require.False(t, m.Valid().MessageBody.IsNil(), "[%v] message body should not be deleted", x.Name)
   377  			} else {
   378  				require.True(t, m.Valid().MessageBody.IsNil(), "[%v] message body should be deleted", x.Name)
   379  			}
   380  		}
   381  	}
   382  	assertState := func(maxMsgID chat1.MessageID) {
   383  		assertStateHelper(maxMsgID, false)
   384  	}
   385  	assertStateAllowHoles := func(maxMsgID chat1.MessageID) {
   386  		assertStateHelper(maxMsgID, true)
   387  	}
   388  	merge := func(msgsUnsorted []chat1.MessageUnboxed, expectedDeletedHistory bool) {
   389  		res := mustMerge(t, storage, convID, uid, SortMessagesDesc(msgsUnsorted))
   390  		if expectedDeletedHistory {
   391  			require.NotNil(t, res.Expunged, "deleted history merge response")
   392  		} else {
   393  			require.Nil(t, res.Expunged, "deleted history merge response")
   394  		}
   395  		t.Logf("merge complete")
   396  	}
   397  
   398  	t.Logf("initial merge")
   399  	// merge with no delh messages
   400  	merge([]chat1.MessageUnboxed{msgA, msgB, msgC, msgD, msgE, msgF, msgG}, false)
   401  	setExpected("A", msgA, false, 0) // TLFNAME messages have no body
   402  	setExpected("B", msgB, true, 0)
   403  	setExpected("C", msgC, true, msgE.GetMessageID())
   404  	setExpected("D", msgD, true, 0)
   405  	setExpected("E", msgE, true, 0)
   406  	setExpected("F", msgF, false, msgG.GetMessageID())
   407  	setExpected("G", msgG, true, 0)
   408  	assertState(msgG.GetMessageID())
   409  
   410  	t.Logf("merge first delh")
   411  	// merge with one delh
   412  	merge([]chat1.MessageUnboxed{msgH, msgI, msgJ}, true)
   413  	setExpected("A", msgA, false, 0)
   414  	setExpected("B", msgB, false, msgJ.GetMessageID())
   415  	setExpected("C", msgC, false, msgJ.GetMessageID())
   416  	setExpected("D", msgD, false, msgJ.GetMessageID())
   417  	setExpected("E", msgE, false, msgJ.GetMessageID())
   418  	setExpected("F", msgF, false, msgG.GetMessageID())
   419  	setExpected("G", msgG, true, 0)                    // delete does not get deleted
   420  	setExpected("H", msgH, false, msgJ.GetMessageID()) // after the cutoff
   421  	setExpected("I", msgI, true, 0)                    // not deletable
   422  	setExpected("J", msgJ, true, 0)
   423  	assertState(msgJ.GetMessageID())
   424  
   425  	t.Logf("merge an already-processed delh")
   426  	merge([]chat1.MessageUnboxed{msgI, msgJ}, false)
   427  	assertState(msgJ.GetMessageID())
   428  
   429  	t.Logf("merge second delh (J)")
   430  	merge([]chat1.MessageUnboxed{msgK, msgL}, true)
   431  	setExpected("I", msgI, true, 0) // last headline can't be deleted
   432  	setExpected("J", msgJ, true, 0) // delh can't be deleted
   433  	setExpected("K", msgK, true, 0) // delh can't be deleted
   434  	setExpected("L", msgL, true, 0) // after the cutoff
   435  	assertState(msgL.GetMessageID())
   436  
   437  	t.Logf("merge non-latest delh")
   438  	merge([]chat1.MessageUnboxed{msgJ}, false)
   439  	assertState(msgL.GetMessageID())
   440  
   441  	t.Logf("discard storage")
   442  	// Start over on storage, this time try things while missing
   443  	// the beginning of the chat, and having holes in storage.
   444  	_, err := storage.G().LocalChatDb.Nuke()
   445  	require.NoError(t, err)
   446  	expectedState = nil
   447  
   448  	t.Logf("merge early part")
   449  	merge([]chat1.MessageUnboxed{msgA, msgB}, false)
   450  	setExpected("A", msgA, false, 0)
   451  	setExpected("B", msgB, true, 0)
   452  	assertState(msgB.GetMessageID())
   453  
   454  	t.Logf("merge after gap")
   455  	merge([]chat1.MessageUnboxed{msgH, msgI, msgJ, msgK, msgL}, true)
   456  	// B gets deleted even through it was across a gap
   457  	setExpected("B", msgB, false, msgK.GetMessageID())
   458  	setExpected("H", msgH, false, msgK.GetMessageID())
   459  	setExpected("I", msgI, true, 0) // headline can't be deleted
   460  	setExpected("J", msgJ, true, 0) // delh can't be deleted
   461  	setExpected("K", msgK, true, 0) // delh can't be deleted
   462  	setExpected("L", msgL, true, 0) // after the cutoff
   463  	assertStateAllowHoles(msgL.GetMessageID())
   464  }
   465  
   466  func TestStorageExpunge(t *testing.T) {
   467  	// Uses this conversation:
   468  	// A start                            <not deletable>
   469  	// B text
   470  	// C text <----\        edited by E
   471  	// D headline  |
   472  	// E edit -----^        edits C
   473  	// F text <---\         deleted by G
   474  	// G delete --^    ___  deletes F     <not deletable>
   475  	// H text           |
   476  	// I headline
   477  	// J delete-history ^ upto I
   478  
   479  	tc, storage, uid := setupStorageTest(t, "delh")
   480  	defer tc.Cleanup()
   481  
   482  	inbox := NewInbox(tc.Context())
   483  	conv := makeConvo(gregor1.Time(0), 1, 1)
   484  	conv.Conv.MaxMsgSummaries = append(conv.Conv.MaxMsgSummaries, chat1.MessageSummary{MessageType: chat1.MessageType_HEADLINE, MsgID: 9})
   485  	require.NoError(t, inbox.Merge(context.TODO(), uid, 1, utils.PluckConvs([]types.RemoteConversation{conv}), nil))
   486  
   487  	convID := conv.GetConvID()
   488  	msgA := MakeMsgWithType(1, chat1.MessageType_TLFNAME)
   489  	msgB := MakeText(2, "some text")
   490  	msgC := MakeText(3, "some text")
   491  	msgD := MakeHeadlineMessage(4)
   492  	msgE := MakeEdit(5, msgC.GetMessageID())
   493  	msgF := MakeText(6, "some text")
   494  	msgG := MakeDelete(7, msgF.GetMessageID(), nil)
   495  	msgH := MakeText(8, "some text")
   496  	msgI := MakeHeadlineMessage(9)
   497  	msgJ := MakeDeleteHistory(10, msgI.GetMessageID())
   498  
   499  	type expectedM struct {
   500  		Name         string // letter label
   501  		MsgID        chat1.MessageID
   502  		BodyPresent  bool
   503  		SupersededBy chat1.MessageID
   504  	}
   505  	dontCare := chat1.MessageID(12341234)
   506  
   507  	var expectedState []expectedM // expectations sorted by ID ascending
   508  	setExpected := func(name string, msg chat1.MessageUnboxed, bodyPresent bool, supersededBy chat1.MessageID) {
   509  		xset := expectedM{name, msg.GetMessageID(), bodyPresent, supersededBy}
   510  		var found bool
   511  		for i, x := range expectedState {
   512  			if x.Name == name {
   513  				found = true
   514  				expectedState[i] = xset
   515  			}
   516  		}
   517  		if !found {
   518  			expectedState = append(expectedState, xset)
   519  		}
   520  		sort.Slice(expectedState, func(i, j int) bool {
   521  			return expectedState[i].MsgID < expectedState[j].MsgID
   522  		})
   523  	}
   524  	assertState := func(maxMsgID chat1.MessageID) {
   525  		var rc ResultCollector
   526  		fetchRes, err := storage.Fetch(context.Background(), MakeConversationAt(convID, maxMsgID), uid, rc,
   527  			nil, nil)
   528  		require.NoError(t, err)
   529  		res := fetchRes.Thread
   530  		if len(res.Messages) != len(expectedState) {
   531  			t.Logf("wrong number of messages")
   532  			for _, m := range res.Messages {
   533  				t.Logf("msgid:%v type:%v", m.GetMessageID(), m.GetMessageType())
   534  			}
   535  			require.Equal(t, len(expectedState), len(res.Messages), "wrong number of messages")
   536  		}
   537  		for i, x := range expectedState {
   538  			t.Logf("[%v] checking msgID:%v supersededBy:%v", x.Name, x.MsgID, x.SupersededBy)
   539  			m := res.Messages[len(res.Messages)-1-i]
   540  			require.True(t, m.IsValid(), "[%v] message should be valid", x.Name)
   541  			require.Equal(t, x.MsgID, m.Valid().ServerHeader.MessageID, "[%v] message ID", x.Name)
   542  			if m.GetMessageType() != chat1.MessageType_TLFNAME && !x.BodyPresent && x.SupersededBy == 0 {
   543  				t.Fatalf("You expected the body to be deleted but the message not to be superseded. Are you sure?")
   544  			}
   545  			if x.SupersededBy != dontCare {
   546  				require.Equal(t, x.SupersededBy, m.Valid().ServerHeader.SupersededBy, "[%v] superseded by", x.Name)
   547  			}
   548  			if x.BodyPresent {
   549  				require.False(t, m.Valid().MessageBody.IsNil(), "[%v] message body should not be deleted", x.Name)
   550  			} else {
   551  				require.True(t, m.Valid().MessageBody.IsNil(), "[%v] message body should be deleted", x.Name)
   552  			}
   553  		}
   554  	}
   555  	merge := func(msgsUnsorted []chat1.MessageUnboxed, expectedDeletedHistory bool) {
   556  		res := mustMerge(t, storage, convID, uid, SortMessagesDesc(msgsUnsorted))
   557  		if expectedDeletedHistory {
   558  			require.NotNil(t, res.Expunged, "deleted history merge response")
   559  		} else {
   560  			require.Nil(t, res.Expunged, "deleted history merge response")
   561  		}
   562  		t.Logf("merge complete")
   563  	}
   564  	expunge := func(upto chat1.MessageID, expectedDeletedHistory bool) {
   565  		res, err := storage.Expunge(context.Background(), conv, uid, chat1.Expunge{Upto: upto})
   566  		require.NoError(t, err)
   567  		if expectedDeletedHistory {
   568  			require.NotNil(t, res.Expunged, "deleted history merge response")
   569  		} else {
   570  			require.Nil(t, res.Expunged, "deleted history merge response")
   571  		}
   572  	}
   573  
   574  	t.Logf("initial merge")
   575  	// merge with no delh messages
   576  	merge([]chat1.MessageUnboxed{msgA, msgB, msgC, msgD, msgE, msgF, msgG}, false)
   577  	setExpected("A", msgA, false, 0) // TLFNAME messages have no body
   578  	setExpected("B", msgB, true, 0)
   579  	setExpected("C", msgC, true, msgE.GetMessageID())
   580  	setExpected("D", msgD, true, 0)
   581  	setExpected("E", msgE, true, 0)
   582  	setExpected("F", msgF, false, msgG.GetMessageID())
   583  	setExpected("G", msgG, true, 0)
   584  	assertState(msgG.GetMessageID())
   585  
   586  	t.Logf("expunge up to E")
   587  	setExpected("B", msgB, false, dontCare)
   588  	setExpected("C", msgC, false, dontCare)
   589  	setExpected("D", msgD, false, dontCare)
   590  	expunge(msgE.GetMessageID(), true)
   591  	assertState(msgG.GetMessageID())
   592  
   593  	t.Logf("expunge with no effect")
   594  	expunge(msgE.GetMessageID(), false)
   595  
   596  	t.Logf("another expunge with no effect")
   597  	expunge(msgC.GetMessageID(), false)
   598  
   599  	t.Logf("merge first delh")
   600  	// merge with one delh
   601  	merge([]chat1.MessageUnboxed{msgH, msgI, msgJ}, true)
   602  	setExpected("E", msgE, false, msgJ.GetMessageID())
   603  	setExpected("F", msgF, false, msgG.GetMessageID())
   604  	setExpected("H", msgH, false, msgJ.GetMessageID())
   605  	setExpected("I", msgI, true, 0) // after the cutoff
   606  	setExpected("J", msgJ, true, 0)
   607  	assertState(msgJ.GetMessageID())
   608  
   609  	t.Logf("expunge the rest")
   610  	setExpected("I", msgI, false, dontCare) // after the cutoff
   611  	expunge(msgJ.GetMessageID()+12, true)
   612  }
   613  
   614  func TestStorageMiss(t *testing.T) {
   615  	tc, storage, uid := setupStorageTest(t, "miss")
   616  	defer tc.Cleanup()
   617  
   618  	msgs := makeMsgRange(10)
   619  	conv := MakeConversation(15)
   620  
   621  	mustMerge(t, storage, conv.Metadata.ConversationID, uid, msgs)
   622  	_, err := storage.Fetch(context.TODO(), conv, uid, nil, nil, nil)
   623  	require.Error(t, err, "expected error")
   624  	require.IsType(t, MissError{}, err, "wrong error type")
   625  }
   626  
   627  func TestStoragePagination(t *testing.T) {
   628  
   629  	tc, storage, uid := setupStorageTest(t, "basic")
   630  	defer tc.Cleanup()
   631  
   632  	msgs := makeMsgRange(300)
   633  	conv := MakeConversation(msgs[0].GetMessageID())
   634  	mustMerge(t, storage, conv.Metadata.ConversationID, uid, msgs)
   635  
   636  	t.Logf("test next input")
   637  	tp := pager.NewThreadPager()
   638  	index, err := tp.MakeIndex(MakeText(120, "TestStoragePagination junk text"))
   639  	require.NoError(t, err)
   640  	p := chat1.Pagination{
   641  		Num:  100,
   642  		Next: index,
   643  	}
   644  	fetchRes, err := storage.Fetch(context.TODO(), conv, uid, nil, nil, &p)
   645  	require.NoError(t, err)
   646  	res := fetchRes.Thread
   647  	require.Equal(t, chat1.MessageID(119), msgs[181].GetMessageID(), "wrong msg id at border")
   648  	require.Equal(t, 100, len(res.Messages), "wrong amount of messages")
   649  	for i := 0; i < len(res.Messages); i++ {
   650  		require.Equal(t, msgs[i+181].GetMessageID(), res.Messages[i].GetMessageID(), "msg mismatch")
   651  	}
   652  	p = chat1.Pagination{
   653  		Num:      100,
   654  		Previous: res.Pagination.Previous,
   655  	}
   656  	t.Logf("fetching previous from result")
   657  	fetchRes, err = storage.Fetch(context.TODO(), conv, uid, nil, nil, &p)
   658  	require.NoError(t, err)
   659  	res = fetchRes.Thread
   660  	require.Equal(t, chat1.MessageID(219), msgs[81].GetMessageID(), "wrong msg id at broder")
   661  	require.Equal(t, 100, len(res.Messages), "wrong amount of messages")
   662  	for i := 0; i < len(res.Messages); i++ {
   663  		require.Equal(t, msgs[i+81].GetMessageID(), res.Messages[i].GetMessageID(), "msg mismatch")
   664  	}
   665  
   666  	t.Logf("test prev input")
   667  	index, err = tp.MakeIndex(MakeText(120, "TestStoragePagination junk text #2"))
   668  	require.NoError(t, err)
   669  	p = chat1.Pagination{
   670  		Num:      100,
   671  		Previous: index,
   672  	}
   673  	fetchRes, err = storage.Fetch(context.TODO(), conv, uid, nil, nil, &p)
   674  	require.NoError(t, err)
   675  	res = fetchRes.Thread
   676  	require.Equal(t, chat1.MessageID(220), msgs[80].GetMessageID(), "wrong msg id at border")
   677  	require.Equal(t, 100, len(res.Messages), "wrong amount of messages")
   678  	for i := 0; i < len(res.Messages); i++ {
   679  		require.Equal(t, msgs[i+80].GetMessageID(), res.Messages[i].GetMessageID(), "msg mismatch")
   680  	}
   681  	p = chat1.Pagination{
   682  		Num:  100,
   683  		Next: res.Pagination.Next,
   684  	}
   685  	t.Logf("fetching next from result")
   686  	fetchRes, err = storage.Fetch(context.TODO(), conv, uid, nil, nil, &p)
   687  	require.NoError(t, err)
   688  	res = fetchRes.Thread
   689  	require.Equal(t, 100, len(res.Messages), "wrong amount of messages")
   690  	for i := 0; i < len(res.Messages); i++ {
   691  		require.Equal(t, msgs[i+180].GetMessageID(), res.Messages[i].GetMessageID(), "msg mismatch")
   692  	}
   693  }
   694  
   695  func mkarray(m chat1.MessageUnboxed) []chat1.MessageUnboxed {
   696  	return []chat1.MessageUnboxed{m}
   697  }
   698  
   699  func TestStorageTypeFilter(t *testing.T) {
   700  	tc, storage, uid := setupStorageTest(t, "basic")
   701  	defer tc.Cleanup()
   702  
   703  	textmsgs := makeMsgRange(300)
   704  	msgs := append(mkarray(MakeMsgWithType(chat1.MessageID(301), chat1.MessageType_EDIT)), textmsgs...)
   705  	msgs = append(mkarray(MakeMsgWithType(chat1.MessageID(302), chat1.MessageType_TLFNAME)), msgs...)
   706  	msgs = append(mkarray(MakeMsgWithType(chat1.MessageID(303), chat1.MessageType_ATTACHMENT)), msgs...)
   707  	msgs = append(mkarray(MakeMsgWithType(chat1.MessageID(304), chat1.MessageType_TEXT)), msgs...)
   708  	textmsgs = append(mkarray(MakeMsgWithType(chat1.MessageID(304), chat1.MessageType_TEXT)), textmsgs...)
   709  	conv := MakeConversation(msgs[0].GetMessageID())
   710  
   711  	query := chat1.GetThreadQuery{
   712  		MessageTypes: []chat1.MessageType{chat1.MessageType_TEXT},
   713  	}
   714  
   715  	mustMerge(t, storage, conv.Metadata.ConversationID, uid, msgs)
   716  	fetchRes, err := storage.Fetch(context.TODO(), conv, uid, nil, &query, nil)
   717  	require.NoError(t, err)
   718  	res := fetchRes.Thread
   719  	require.Equal(t, len(msgs), len(res.Messages), "wrong amount of messages")
   720  	restexts := utils.FilterByType(res.Messages, &query, true)
   721  	require.Equal(t, len(textmsgs), len(restexts), "wrong amount of text messages")
   722  	for i := 0; i < len(restexts); i++ {
   723  		require.Equal(t, textmsgs[i].GetMessageID(), restexts[i].GetMessageID(), "msg mismatch")
   724  	}
   725  
   726  }
   727  
   728  func TestStorageLocalMax(t *testing.T) {
   729  	tc, storage, uid := setupStorageTest(t, "local-max")
   730  	defer tc.Cleanup()
   731  
   732  	msgs := makeMsgRange(10)
   733  	conv := MakeConversation(15)
   734  
   735  	_, err := storage.FetchUpToLocalMaxMsgID(context.TODO(), conv.Metadata.ConversationID, uid, nil, 0,
   736  		nil, nil)
   737  	require.Error(t, err)
   738  	require.IsType(t, MissError{}, err, "wrong error type")
   739  
   740  	mustMerge(t, storage, conv.Metadata.ConversationID, uid, msgs)
   741  	tv, err := storage.FetchUpToLocalMaxMsgID(context.TODO(), conv.Metadata.ConversationID, uid, nil, 0,
   742  		nil, nil)
   743  	require.NoError(t, err)
   744  	require.Len(t, tv.Thread.Messages, 10)
   745  }
   746  
   747  func TestStorageFetchMessages(t *testing.T) {
   748  	tc, storage, uid := setupStorageTest(t, "fetchMessages")
   749  	defer tc.Cleanup()
   750  
   751  	msgs := makeMsgRange(20)
   752  	conv := MakeConversation(25)
   753  
   754  	mustMerge(t, storage, conv.Metadata.ConversationID, uid, msgs)
   755  
   756  	msgIDs := []chat1.MessageID{10, 15, 6}
   757  	umsgs, err := storage.FetchMessages(context.TODO(), conv.Metadata.ConversationID, uid, msgIDs)
   758  	require.NoError(t, err)
   759  	require.Equal(t, len(msgIDs), len(umsgs), "size mismatch")
   760  	for _, umsg := range umsgs {
   761  		require.NotNil(t, umsg, "msg not found")
   762  	}
   763  
   764  	msgIDs = []chat1.MessageID{10, 15, 6, 21}
   765  	umsgs, err = storage.FetchMessages(context.TODO(), conv.Metadata.ConversationID, uid, msgIDs)
   766  	require.NoError(t, err)
   767  	require.Equal(t, len(msgIDs), len(umsgs), "size mismatch")
   768  	nils := 0
   769  	for _, umsg := range umsgs {
   770  		if umsg == nil {
   771  			nils++
   772  		}
   773  	}
   774  	require.Equal(t, 1, nils, "wrong number of nils")
   775  }
   776  
   777  func TestStorageClearMessages(t *testing.T) {
   778  	tc, storage, uid := setupStorageTest(t, "clearMessages")
   779  	defer tc.Cleanup()
   780  
   781  	msgs := makeMsgRange(20)
   782  	conv := MakeConversation(20)
   783  	mustMerge(t, storage, conv.Metadata.ConversationID, uid, msgs)
   784  
   785  	ctx := context.TODO()
   786  	tv, err := storage.Fetch(ctx, conv, uid, nil, nil, nil)
   787  	require.NoError(t, err)
   788  	require.Equal(t, 20, len(tv.Thread.Messages))
   789  	require.NoError(t, storage.ClearBefore(ctx, conv.GetConvID(), uid, 10))
   790  	tv, err = storage.Fetch(ctx, conv, uid, NewInsatiableResultCollector(), nil, nil)
   791  	require.NoError(t, err)
   792  	require.Equal(t, 11, len(tv.Thread.Messages))
   793  	require.Equal(t, chat1.MessageID(20), tv.Thread.Messages[0].GetMessageID())
   794  	require.Equal(t, chat1.MessageID(10), tv.Thread.Messages[len(tv.Thread.Messages)-1].GetMessageID())
   795  }
   796  
   797  func TestStorageServerVersion(t *testing.T) {
   798  	tc, storage, uid := setupStorageTest(t, "serverVersion")
   799  	defer tc.Cleanup()
   800  
   801  	msgs := makeMsgRange(300)
   802  	conv := MakeConversation(msgs[0].GetMessageID())
   803  	mustMerge(t, storage, conv.Metadata.ConversationID, uid, msgs)
   804  	res, err := storage.Fetch(context.TODO(), conv, uid, nil, nil, nil)
   805  	require.NoError(t, err)
   806  	require.Equal(t, len(msgs), len(res.Thread.Messages))
   807  
   808  	cerr := tc.Context().ServerCacheVersions.Set(context.TODO(), chat1.ServerCacheVers{
   809  		BodiesVers: 5,
   810  	})
   811  	require.NoError(t, cerr)
   812  
   813  	res, err = storage.Fetch(context.TODO(), conv, uid, nil, nil, nil)
   814  	require.Error(t, err)
   815  	require.IsType(t, MissError{}, err)
   816  
   817  	mustMerge(t, storage, conv.Metadata.ConversationID, uid, msgs)
   818  	res, err = storage.Fetch(context.TODO(), conv, uid, nil, nil, nil)
   819  	require.NoError(t, err)
   820  	require.Equal(t, len(msgs), len(res.Thread.Messages))
   821  }
   822  
   823  func TestStorageDetectBodyHashReplay(t *testing.T) {
   824  	tc, _, _ := setupStorageTest(t, "fetchMessages")
   825  	defer tc.Cleanup()
   826  
   827  	// The first time we encounter a body hash it's stored.
   828  	err := CheckAndRecordBodyHash(context.Background(), tc.Context(), chat1.Hash("foo"), 1, chat1.ConversationID("bar"))
   829  	require.NoError(t, err)
   830  
   831  	// Seeing the same body hash again in the same message is fine. That just
   832  	// means we uboxed it twice.
   833  	err = CheckAndRecordBodyHash(context.Background(), tc.Context(), chat1.Hash("foo"), 1, chat1.ConversationID("bar"))
   834  	require.NoError(t, err)
   835  
   836  	// But seeing the hash again with a different convID/msgID is a replay, and
   837  	// it must trigger an error.
   838  	err = CheckAndRecordBodyHash(context.Background(), tc.Context(), chat1.Hash("foo"), 1, chat1.ConversationID("bar2"))
   839  	require.Error(t, err)
   840  	err = CheckAndRecordBodyHash(context.Background(), tc.Context(), chat1.Hash("foo"), 2, chat1.ConversationID("bar"))
   841  	require.Error(t, err)
   842  }
   843  
   844  func TestStorageDetectPrevPtrInconsistency(t *testing.T) {
   845  	tc, _, _ := setupStorageTest(t, "fetchMessages")
   846  	defer tc.Cleanup()
   847  
   848  	// The first time we encounter a message ID (either in unboxing or in
   849  	// another message's prev pointer) its header hash is stored.
   850  	err := CheckAndRecordPrevPointer(context.Background(), tc.Context(), 1, chat1.ConversationID("bar"), chat1.Hash("foo"))
   851  	require.NoError(t, err)
   852  
   853  	// Seeing the same header hash again in the same message is fine. That just
   854  	// means we uboxed it twice.
   855  	err = CheckAndRecordPrevPointer(context.Background(), tc.Context(), 1, chat1.ConversationID("bar"), chat1.Hash("foo"))
   856  	require.NoError(t, err)
   857  
   858  	// But seeing the same convID/msgID with a different header hash is a
   859  	// consistency violation, and it must trigger an error.
   860  	err = CheckAndRecordPrevPointer(context.Background(), tc.Context(), 1, chat1.ConversationID("bar"), chat1.Hash("foo2"))
   861  	require.Error(t, err)
   862  }
   863  
   864  func TestStorageMultipleEdits(t *testing.T) {
   865  	tc, s, uid := setupStorageTest(t, "multiEdits")
   866  	defer tc.Cleanup()
   867  
   868  	msgText := MakeText(1, "initial")
   869  	edit1 := MakeEdit(2, msgText.GetMessageID())
   870  	edit2 := MakeEdit(3, msgText.GetMessageID())
   871  	conv := MakeConversation(edit2.GetMessageID())
   872  
   873  	// Merge in text message
   874  	mustMerge(t, s, conv.GetConvID(), uid, []chat1.MessageUnboxed{msgText})
   875  	conv.ReaderInfo.MaxMsgid = msgText.GetMessageID()
   876  	fetchRes, err := s.Fetch(context.TODO(), conv, uid, nil, nil, nil)
   877  	require.NoError(t, err)
   878  	require.Equal(t, 1, len(fetchRes.Thread.Messages))
   879  	require.Equal(t, msgText.GetMessageID(), fetchRes.Thread.Messages[0].GetMessageID())
   880  	require.Zero(t, fetchRes.Thread.Messages[0].Valid().ServerHeader.SupersededBy)
   881  
   882  	// Merge in both edits
   883  	mustMerge(t, s, conv.GetConvID(), uid, []chat1.MessageUnboxed{edit2, edit1})
   884  	conv.ReaderInfo.MaxMsgid = edit2.GetMessageID()
   885  	fetchRes, err = s.Fetch(context.TODO(), conv, uid, nil, nil, nil)
   886  	require.NoError(t, err)
   887  	require.Equal(t, 3, len(fetchRes.Thread.Messages))
   888  	require.Equal(t, msgText.GetMessageID(), fetchRes.Thread.Messages[2].GetMessageID())
   889  	require.Equal(t, edit2.GetMessageID(), fetchRes.Thread.Messages[2].Valid().ServerHeader.SupersededBy)
   890  }