github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/chat/ephemeral_tracker_test.go (about)

     1  package chat
     2  
     3  import (
     4  	"context"
     5  	"sort"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/keybase/client/go/chat/globals"
    10  	"github.com/keybase/client/go/chat/storage"
    11  	"github.com/keybase/client/go/chat/types"
    12  	"github.com/keybase/client/go/protocol/chat1"
    13  	"github.com/keybase/client/go/protocol/gregor1"
    14  	"github.com/keybase/clockwork"
    15  	"github.com/stretchr/testify/require"
    16  )
    17  
    18  func mustMerge(t testing.TB, chatStorage *storage.Storage,
    19  	convID chat1.ConversationID, uid gregor1.UID, msgs []chat1.MessageUnboxed) storage.MergeResult {
    20  	conv, err := storage.NewInbox(chatStorage.G()).GetConversation(context.Background(), uid, convID)
    21  	switch err.(type) {
    22  	case nil:
    23  	case storage.MissError:
    24  		conv = types.NewEmptyRemoteConversation(convID)
    25  	default:
    26  		require.NoError(t, err)
    27  	}
    28  	res, err := chatStorage.Merge(context.Background(), conv, uid, msgs)
    29  	require.NoError(t, err)
    30  	return res
    31  }
    32  
    33  func TestEphemeralPurgeTracker(t *testing.T) {
    34  	// Uses this conversation:
    35  	// A start                            <not deletable>
    36  	// B text                             <not ephemeral>
    37  	// C text <----\        edited by E   <ephemeral with 1 "lifetime">
    38  	// D headline  |                      <not deletable>
    39  	// E edit -----^        edits C       <ephemeral with 3 "lifetime"s>
    40  	// F text <---\         deleted by G  <ephemeral with 2 "lifetime"s>
    41  	// G delete --^    ___  deletes F     <not deletable>
    42  	// H text           |                 <ephemeral with 1 "lifetime">
    43  
    44  	ctx, tc, world, _, _, _, _ := setupLoaderTest(t)
    45  	defer world.Cleanup()
    46  
    47  	g := globals.NewContext(tc.G, tc.ChatG)
    48  	u := world.GetUsers()[0]
    49  	uid := gregor1.UID(u.GetUID().ToBytes())
    50  	// we manually run purging in this
    51  	<-g.Indexer.Stop(context.TODO())
    52  	<-g.ConvLoader.Stop(context.TODO())
    53  	<-g.EphemeralPurger.Stop(context.TODO())
    54  	g.EphemeralPurger = types.DummyEphemeralPurger{}
    55  	g.EphemeralTracker = NewEphemeralTracker(g)
    56  	clock := clockwork.NewFakeClockAt(time.Now())
    57  	chatStorage := storage.New(g, tc.ChatG.ConvSource)
    58  	chatStorage.SetClock(clock)
    59  	convID := storage.MakeConvID()
    60  
    61  	type expectedM struct {
    62  		Name         string // letter label
    63  		MsgID        chat1.MessageID
    64  		BodyPresent  bool
    65  		SupersededBy chat1.MessageID
    66  	}
    67  	dontCare := chat1.MessageID(12341234)
    68  
    69  	var expectedState []expectedM // expectations sorted by ID ascending
    70  	setExpected := func(name string, msg chat1.MessageUnboxed, bodyPresent bool, supersededBy chat1.MessageID) {
    71  		xset := expectedM{name, msg.GetMessageID(), bodyPresent, supersededBy}
    72  		var found bool
    73  		for i, x := range expectedState {
    74  			if x.Name == name {
    75  				found = true
    76  				expectedState[i] = xset
    77  			}
    78  		}
    79  		if !found {
    80  			expectedState = append(expectedState, xset)
    81  		}
    82  		sort.Slice(expectedState, func(i, j int) bool {
    83  			return expectedState[i].MsgID < expectedState[j].MsgID
    84  		})
    85  	}
    86  
    87  	assertState := func(maxMsgID chat1.MessageID) {
    88  		var rc storage.ResultCollector
    89  		fetchRes, err := chatStorage.Fetch(ctx, storage.MakeConversationAt(convID, maxMsgID), uid, rc,
    90  			nil, nil)
    91  		res := fetchRes.Thread
    92  		require.NoError(t, err)
    93  		if len(res.Messages) != len(expectedState) {
    94  			t.Logf("wrong number of messages")
    95  			for _, m := range res.Messages {
    96  				t.Logf("msgid:%v type:%v", m.GetMessageID(), m.GetMessageType())
    97  			}
    98  			require.Equal(t, len(expectedState), len(res.Messages), "wrong number of messages")
    99  		}
   100  		for i, x := range expectedState {
   101  			t.Logf("[%v] checking msgID:%v supersededBy:%v", x.Name, x.MsgID, x.SupersededBy)
   102  			m := res.Messages[len(res.Messages)-1-i]
   103  			require.True(t, m.IsValid(), "[%v] message should be valid", x.Name)
   104  			require.Equal(t, x.MsgID, m.Valid().ServerHeader.MessageID, "[%v] message ID", x.Name)
   105  			if m.GetMessageType() != chat1.MessageType_TLFNAME {
   106  				if !x.BodyPresent && x.SupersededBy == 0 {
   107  					t.Fatalf("You expected the body to be deleted but the message not to be superseded. Are you sure?")
   108  				}
   109  			}
   110  			if x.SupersededBy != dontCare {
   111  				require.Equal(t, x.SupersededBy, m.Valid().ServerHeader.SupersededBy, "[%v] superseded by", x.Name)
   112  			}
   113  			if x.BodyPresent {
   114  				require.False(t, m.Valid().MessageBody.IsNil(), "[%v] message body should not be deleted", x.Name)
   115  			} else {
   116  				require.True(t, m.Valid().MessageBody.IsNil(), "[%v] message body should be deleted", x.Name)
   117  			}
   118  		}
   119  	}
   120  
   121  	verifyTrackerState := func(expectedPurgeInfo *chat1.EphemeralPurgeInfo) {
   122  		purgeInfo, err := g.EphemeralTracker.GetPurgeInfo(ctx, uid, convID)
   123  		if expectedPurgeInfo == nil {
   124  			require.Error(t, err)
   125  			require.IsType(t, storage.MissError{}, err, "wrong error type")
   126  		} else {
   127  			require.NoError(t, err)
   128  			require.Equal(t, *expectedPurgeInfo, purgeInfo)
   129  		}
   130  	}
   131  
   132  	ephemeralPurgeAndVerify := func(expectedPurgeInfo *chat1.EphemeralPurgeInfo, msgIDs []chat1.MessageID) {
   133  		purgeInfo, _ := g.EphemeralTracker.GetPurgeInfo(ctx, uid, convID)
   134  		newPurgeInfo, purgedMsgs, err := chatStorage.EphemeralPurge(ctx, convID, uid, &purgeInfo)
   135  		require.NoError(t, err)
   136  		if msgIDs == nil {
   137  			require.Nil(t, purgedMsgs)
   138  		} else {
   139  			purgedIDs := []chat1.MessageID{}
   140  			for _, purgedMsg := range purgedMsgs {
   141  				purgedIDs = append(purgedIDs, purgedMsg.GetMessageID())
   142  			}
   143  			require.Equal(t, msgIDs, purgedIDs)
   144  		}
   145  		require.Equal(t, expectedPurgeInfo, newPurgeInfo)
   146  		verifyTrackerState(expectedPurgeInfo)
   147  	}
   148  
   149  	lifetime := gregor1.DurationSec(1)
   150  	sleepLifetime := lifetime.ToDuration()
   151  	now := gregor1.ToTime(clock.Now())
   152  	msgA := storage.MakeMsgWithType(1, chat1.MessageType_TLFNAME)
   153  	msgB := storage.MakeText(2, "some text")
   154  	msgC := storage.MakeEphemeralText(3, "some text", &chat1.MsgEphemeralMetadata{Lifetime: lifetime}, now)
   155  	msgD := storage.MakeHeadlineMessage(4)
   156  	msgE := storage.MakeEphemeralEdit(5, msgC.GetMessageID(), &chat1.MsgEphemeralMetadata{Lifetime: lifetime * 3}, now)
   157  	msgF := storage.MakeEphemeralText(6, "some text", &chat1.MsgEphemeralMetadata{Lifetime: lifetime * 2}, now)
   158  	msgG := storage.MakeDelete(7, msgF.GetMessageID(), nil)
   159  	msgH := storage.MakeEphemeralText(8, "some text", &chat1.MsgEphemeralMetadata{Lifetime: lifetime}, now)
   160  
   161  	t.Logf("initial merge")
   162  	mustMerge(t, chatStorage, convID, uid, storage.SortMessagesDesc([]chat1.MessageUnboxed{msgA, msgB, msgC, msgD, msgE, msgF, msgG}))
   163  
   164  	// We set the initial tracker info when we merge in
   165  	expectedPurgeInfo := &chat1.EphemeralPurgeInfo{
   166  		ConvID:          convID,
   167  		NextPurgeTime:   msgC.Valid().Etime(),
   168  		MinUnexplodedID: msgC.GetMessageID(),
   169  		IsActive:        true,
   170  	}
   171  	verifyTrackerState(expectedPurgeInfo)
   172  	// Running purge has no effect since nothing is expired
   173  	ephemeralPurgeAndVerify(expectedPurgeInfo, nil)
   174  
   175  	setExpected("A", msgA, false, 0) // TLFNAME messages have no body
   176  	setExpected("B", msgB, true, 0)
   177  	setExpected("C", msgC, true, msgE.GetMessageID())
   178  	setExpected("D", msgD, true, 0)
   179  	setExpected("E", msgE, true, 0)
   180  	setExpected("F", msgF, false, msgG.GetMessageID())
   181  	setExpected("G", msgG, true, 0)
   182  	assertState(msgG.GetMessageID())
   183  	// After fetching messages tracker state is unchanged since nothing is
   184  	// expired.
   185  	verifyTrackerState(expectedPurgeInfo)
   186  
   187  	t.Logf("sleep and fetch")
   188  	// We sleep for `lifetime`, so we expect C to get purged on fetch (msg H is
   189  	// not yet merged in)
   190  	clock.Advance(sleepLifetime)
   191  	setExpected("C", msgC, false, dontCare)
   192  	assertState(msgG.GetMessageID())
   193  	// We don't update the  tracker state is updated from a fetch
   194  	verifyTrackerState(expectedPurgeInfo)
   195  	// Once we run EphemeralPurge and sweep all messages, we update our tracker
   196  	// state
   197  	expectedPurgeInfo = &chat1.EphemeralPurgeInfo{
   198  		ConvID:          convID,
   199  		NextPurgeTime:   msgE.Valid().Etime(),
   200  		MinUnexplodedID: msgE.GetMessageID(),
   201  		IsActive:        true,
   202  	}
   203  	// msgIDs is nil since assertState pulled the conversation and exploded msgC on load.
   204  	ephemeralPurgeAndVerify(expectedPurgeInfo, nil)
   205  
   206  	t.Logf("mergeH")
   207  	// We add msgH, which is already expired, so it should get purged on entry,
   208  	// but our nextPurgeTime should be unchanged, since msgE's etime is still
   209  	// the min.
   210  	mustMerge(t, chatStorage, convID, uid, storage.SortMessagesDesc([]chat1.MessageUnboxed{msgH}))
   211  	verifyTrackerState(expectedPurgeInfo)
   212  	// H should have it's body nuked off the bat.
   213  	setExpected("H", msgH, false, dontCare)
   214  	assertState(msgH.GetMessageID())
   215  	verifyTrackerState(expectedPurgeInfo)
   216  
   217  	// we've slept for ~ lifetime*2, F's lifetime is up
   218  	clock.Advance(sleepLifetime)
   219  	expectedPurgeInfo = &chat1.EphemeralPurgeInfo{
   220  		ConvID:          convID,
   221  		NextPurgeTime:   msgE.Valid().Etime(),
   222  		MinUnexplodedID: msgE.GetMessageID(),
   223  		IsActive:        true,
   224  	}
   225  	ephemeralPurgeAndVerify(expectedPurgeInfo, nil)
   226  	setExpected("F", msgF, false, dontCare)
   227  	assertState(msgH.GetMessageID())
   228  
   229  	// we've slept for ~ lifetime*3, E's lifetime is up
   230  	clock.Advance(sleepLifetime)
   231  	expectedPurgeInfo = &chat1.EphemeralPurgeInfo{
   232  		ConvID:          convID,
   233  		NextPurgeTime:   0,
   234  		MinUnexplodedID: msgH.GetMessageID(),
   235  		IsActive:        false,
   236  	}
   237  	ephemeralPurgeAndVerify(expectedPurgeInfo, []chat1.MessageID{msgE.GetMessageID()})
   238  	setExpected("E", msgE, false, dontCare)
   239  	assertState(msgH.GetMessageID())
   240  
   241  	t.Logf("purge with no effect")
   242  	ephemeralPurgeAndVerify(expectedPurgeInfo, nil)
   243  	assertState(msgH.GetMessageID())
   244  
   245  	t.Logf("another purge with no effect")
   246  	ephemeralPurgeAndVerify(expectedPurgeInfo, nil)
   247  	assertState(msgH.GetMessageID())
   248  
   249  	// Force a purge with 0 messages, and make sure we process it correctly.
   250  	newPurgeInfo, purgedMsgs, err := chatStorage.EphemeralPurge(ctx, convID, uid,
   251  		&chat1.EphemeralPurgeInfo{
   252  			ConvID:          convID,
   253  			NextPurgeTime:   0,
   254  			MinUnexplodedID: msgH.GetMessageID() + 1,
   255  			IsActive:        false,
   256  		})
   257  	require.NoError(t, err)
   258  	require.Nil(t, newPurgeInfo)
   259  	require.EqualValues(t, []chat1.MessageUnboxed(nil), purgedMsgs)
   260  	verifyTrackerState(expectedPurgeInfo)
   261  }