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 }