github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/chat/ephemeral_purger_test.go (about) 1 package chat 2 3 import ( 4 "context" 5 "testing" 6 "time" 7 8 "github.com/keybase/client/go/chat/globals" 9 "github.com/keybase/client/go/chat/s3" 10 "github.com/keybase/client/go/chat/storage" 11 "github.com/keybase/client/go/chat/types" 12 "github.com/keybase/client/go/chat/utils" 13 "github.com/keybase/client/go/protocol/chat1" 14 "github.com/keybase/client/go/protocol/gregor1" 15 "github.com/stretchr/testify/require" 16 ) 17 18 type mockHTTPSrv struct { 19 types.DummyAttachmentHTTPSrv 20 fetcher types.AttachmentFetcher 21 } 22 23 func newMockHTTPSrv(fetcher types.AttachmentFetcher) *mockHTTPSrv { 24 return &mockHTTPSrv{ 25 fetcher: fetcher, 26 } 27 } 28 29 func (d mockHTTPSrv) GetAttachmentFetcher() types.AttachmentFetcher { 30 return d.fetcher 31 } 32 33 type mockAssetDeleter struct { 34 types.DummyAttachmentFetcher 35 delCh chan struct{} 36 } 37 38 func newMockAssetDeleter() *mockAssetDeleter { 39 return &mockAssetDeleter{ 40 delCh: make(chan struct{}, 100), 41 } 42 } 43 44 func (d mockAssetDeleter) DeleteAssets(ctx context.Context, convID chat1.ConversationID, assets []chat1.Asset, 45 ri func() chat1.RemoteInterface, signer s3.Signer) error { 46 if len(assets) > 0 { 47 d.delCh <- struct{}{} 48 } 49 return nil 50 } 51 52 func TestBackgroundPurge(t *testing.T) { 53 ctx, tc, world, ri, baseSender, listener, conv1 := setupLoaderTest(t) 54 defer world.Cleanup() 55 56 g := globals.NewContext(tc.G, tc.ChatG) 57 u := world.GetUsers()[0] 58 uid := gregor1.UID(u.GetUID().ToBytes()) 59 trip1 := newConvTriple(ctx, t, tc, u.Username) 60 clock := world.Fc 61 g.EphemeralTracker = NewEphemeralTracker(g) 62 fetcher := newMockAssetDeleter() 63 httpSrv := newMockHTTPSrv(fetcher) 64 g.AttachmentURLSrv = httpSrv 65 chatStorage := storage.New(g, tc.ChatG.ConvSource) 66 chatStorage.SetClock(clock) 67 68 <-g.EphemeralPurger.Stop(ctx) 69 purger := NewBackgroundEphemeralPurger(g) 70 purger.SetClock(world.Fc) 71 g.EphemeralPurger = purger 72 g.ConvSource.(*HybridConversationSource).storage = chatStorage 73 purger.Start(ctx, uid) 74 75 trip2 := newConvTriple(ctx, t, tc, u.Username) 76 trip2.TopicType = chat1.TopicType_DEV 77 firstMessagePlaintext := chat1.MessagePlaintext{ 78 ClientHeader: chat1.MessageClientHeader{ 79 Conv: trip2, 80 TlfName: u.Username, 81 TlfPublic: false, 82 MessageType: chat1.MessageType_TLFNAME, 83 }, 84 MessageBody: chat1.MessageBody{}, 85 } 86 prepareRes, err := baseSender.Prepare(ctx, firstMessagePlaintext, 87 chat1.ConversationMembersType_IMPTEAMNATIVE, nil, nil) 88 firstMessageBoxed := prepareRes.Boxed 89 require.NoError(t, err) 90 conv2, err := ri().NewConversationRemote2(ctx, chat1.NewConversationRemote2Arg{ 91 IdTriple: trip2, 92 TLFMessage: firstMessageBoxed, 93 }) 94 require.NoError(t, err) 95 96 assertListener := func(convID chat1.ConversationID, queueSize int) { 97 select { 98 case loadID := <-listener.bgConvLoads: 99 require.Equal(t, convID, loadID) 100 require.Equal(t, queueSize, purger.Len()) 101 case <-time.After(10 * time.Second): 102 require.Fail(t, "timeout waiting for conversation load") 103 } 104 } 105 106 sendEphemeral := func(convID chat1.ConversationID, trip chat1.ConversationIDTriple, 107 lifetime gregor1.DurationSec, body chat1.MessageBody) chat1.MessageUnboxed { 108 typ, err := body.MessageType() 109 require.NoError(t, err) 110 _, _, err = baseSender.Send(ctx, convID, chat1.MessagePlaintext{ 111 ClientHeader: chat1.MessageClientHeader{ 112 Conv: trip, 113 Sender: uid, 114 TlfName: u.Username, 115 TlfPublic: false, 116 MessageType: typ, 117 EphemeralMetadata: &chat1.MsgEphemeralMetadata{Lifetime: lifetime}, 118 }, 119 MessageBody: body, 120 }, 0, nil, nil, nil) 121 require.NoError(t, err) 122 thread, err := tc.ChatG.ConvSource.Pull(ctx, convID, uid, 123 chat1.GetThreadReason_GENERAL, nil, 124 &chat1.GetThreadQuery{ 125 MessageTypes: []chat1.MessageType{chat1.MessageType_TEXT, chat1.MessageType_ATTACHMENT}, 126 }, nil) 127 require.NoError(t, err) 128 require.True(t, len(thread.Messages) > 0) 129 return thread.Messages[0] 130 } 131 132 assertTrackerState := func(convID chat1.ConversationID, expectedPurgeInfo chat1.EphemeralPurgeInfo) { 133 purgeInfo, err := g.EphemeralTracker.GetPurgeInfo(ctx, uid, convID) 134 if expectedPurgeInfo.IsNil() { 135 require.Error(t, err) 136 require.IsType(t, storage.MissError{}, err) 137 } else { 138 require.NoError(t, err) 139 require.Equal(t, expectedPurgeInfo, purgeInfo) 140 } 141 } 142 143 assertEphemeralPurgeNotifInfo := func(convID chat1.ConversationID, msgIDs []chat1.MessageID, localVers chat1.LocalConversationVers) { 144 info := listener.consumeEphemeralPurge(t) 145 require.Equal(t, info.ConvID, convID) 146 if msgIDs == nil { 147 require.Nil(t, info.Msgs) 148 } else { 149 purgedIDs := []chat1.MessageID{} 150 for _, purgedMsg := range info.Msgs { 151 purgedIDs = append(purgedIDs, purgedMsg.GetMessageID()) 152 } 153 require.Equal(t, msgIDs, purgedIDs) 154 } 155 updateID := listener.consumeConvUpdate(t) 156 require.Equal(t, updateID, convID) 157 158 rc, err := utils.GetUnverifiedConv(ctx, g, uid, convID, types.InboxSourceDataSourceLocalOnly) 159 require.NoError(t, err) 160 require.Equal(t, localVers, rc.Conv.Metadata.LocalVersion) 161 162 conv, err := utils.GetVerifiedConv(ctx, g, uid, convID, types.InboxSourceDataSourceLocalOnly) 163 require.NoError(t, err) 164 require.Equal(t, localVers, conv.Info.LocalVersion) 165 } 166 167 // Load our conv with the initial tlf msg 168 t.Logf("assert listener 0") 169 require.NoError(t, tc.Context().ConvLoader.Queue(context.TODO(), 170 types.NewConvLoaderJob(conv1.ConvID, &chat1.Pagination{Num: 3}, types.ConvLoaderPriorityHigh, 171 types.ConvLoaderUnique, nil))) 172 assertListener(conv1.ConvID, 0) 173 require.NoError(t, tc.Context().ConvLoader.Queue(context.TODO(), 174 types.NewConvLoaderJob(conv2.ConvID, &chat1.Pagination{Num: 3}, types.ConvLoaderPriorityHigh, 175 types.ConvLoaderUnique, nil))) 176 assertListener(conv2.ConvID, 0) 177 178 // Nothing is up for purging yet 179 assertTrackerState(conv1.ConvID, chat1.EphemeralPurgeInfo{ 180 ConvID: conv1.ConvID, 181 MinUnexplodedID: 1, 182 NextPurgeTime: 0, 183 IsActive: false, 184 }) 185 assertTrackerState(conv2.ConvID, chat1.EphemeralPurgeInfo{ 186 ConvID: conv2.ConvID, 187 MinUnexplodedID: 1, 188 NextPurgeTime: 0, 189 IsActive: false, 190 }) 191 192 // Send ephemeral messages, and ensure all get purged 193 lifetime := gregor1.DurationSec(1) 194 lifetimeDuration := time.Second 195 msgs := []chat1.MessageUnboxed{} 196 body := chat1.NewMessageBodyWithText(chat1.MessageText{ 197 Body: "hi", 198 }) 199 for i := 1; i <= 5; i++ { 200 convID := conv1.ConvID 201 trip := trip1 202 if i%2 == 0 { 203 convID = conv2.ConvID 204 trip = trip2 205 } 206 if i == 1 { 207 body = chat1.NewMessageBodyWithAttachment(chat1.MessageAttachment{ 208 Object: chat1.Asset{ 209 Path: "miketown", 210 }, 211 }) 212 } else { 213 chat1.NewMessageBodyWithText(chat1.MessageText{ 214 Body: "hi", 215 }) 216 } 217 msg := sendEphemeral(convID, trip, lifetime*gregor1.DurationSec(i), body) 218 if i == 1 { 219 t.Logf("DEBUG: attachment msgid: %d", msg.GetMessageID()) 220 } 221 msgs = append(msgs, msg) 222 } 223 224 t.Logf("assert listener 1") 225 world.Fc.Advance(lifetimeDuration) 226 assertListener(conv1.ConvID, 2) 227 localVers1 := chat1.LocalConversationVers(1) 228 localVers2 := chat1.LocalConversationVers(1) 229 assertEphemeralPurgeNotifInfo(conv1.ConvID, []chat1.MessageID{msgs[0].GetMessageID()}, localVers1) 230 assertTrackerState(conv1.ConvID, chat1.EphemeralPurgeInfo{ 231 ConvID: conv1.ConvID, 232 MinUnexplodedID: msgs[2].GetMessageID(), 233 NextPurgeTime: msgs[2].Valid().Etime(), 234 IsActive: true, 235 }) 236 assertTrackerState(conv2.ConvID, chat1.EphemeralPurgeInfo{ 237 ConvID: conv2.ConvID, 238 MinUnexplodedID: msgs[1].GetMessageID(), 239 NextPurgeTime: msgs[1].Valid().Etime(), 240 IsActive: true, 241 }) 242 assertListener(conv1.ConvID, 2) 243 // make sure the single asset got deleted 244 select { 245 case <-fetcher.delCh: 246 case <-time.After(2 * time.Second): 247 require.Fail(t, "no asset deleted") 248 } 249 250 // Ensure things run smoothly even with extraneous start/stop calls to 251 // purger 252 g.EphemeralPurger.Start(context.Background(), uid) 253 <-g.EphemeralPurger.Stop(context.Background()) 254 <-g.EphemeralPurger.Stop(context.Background()) 255 g.EphemeralPurger.Start(context.Background(), uid) 256 g.EphemeralPurger.Start(context.Background(), uid) 257 258 t.Logf("assert listener 2") 259 world.Fc.Advance(lifetimeDuration) 260 assertListener(conv2.ConvID, 2) 261 assertEphemeralPurgeNotifInfo(conv2.ConvID, []chat1.MessageID{msgs[1].GetMessageID()}, localVers1) 262 assertTrackerState(conv1.ConvID, chat1.EphemeralPurgeInfo{ 263 ConvID: conv1.ConvID, 264 MinUnexplodedID: msgs[2].GetMessageID(), 265 NextPurgeTime: msgs[2].Valid().Etime(), 266 IsActive: true, 267 }) 268 assertTrackerState(conv2.ConvID, chat1.EphemeralPurgeInfo{ 269 ConvID: conv2.ConvID, 270 MinUnexplodedID: msgs[3].GetMessageID(), 271 NextPurgeTime: msgs[3].Valid().Etime(), 272 IsActive: true, 273 }) 274 // asset deletion job 275 assertListener(conv2.ConvID, 2) 276 277 // Stop the Purger, and ensure the next message gets purged when we Pull 278 // the conversation and the GUI get's a notification 279 <-g.EphemeralPurger.Stop(context.Background()) 280 t.Logf("assert listener 3") 281 world.Fc.Advance(lifetimeDuration) 282 thread, err := tc.ChatG.ConvSource.Pull(ctx, conv1.ConvID, uid, 283 chat1.GetThreadReason_GENERAL, nil, 284 &chat1.GetThreadQuery{ 285 MessageTypes: []chat1.MessageType{chat1.MessageType_TEXT, chat1.MessageType_ATTACHMENT}, 286 }, nil) 287 require.NoError(t, err) 288 require.Len(t, thread.Messages, 3) 289 localVers1++ 290 assertEphemeralPurgeNotifInfo(conv1.ConvID, []chat1.MessageID{msgs[2].GetMessageID()}, localVers1) 291 assertTrackerState(conv1.ConvID, chat1.EphemeralPurgeInfo{ 292 ConvID: conv1.ConvID, 293 MinUnexplodedID: msgs[2].GetMessageID(), 294 NextPurgeTime: msgs[2].Valid().Etime(), 295 IsActive: true, 296 }) 297 assertTrackerState(conv2.ConvID, chat1.EphemeralPurgeInfo{ 298 ConvID: conv2.ConvID, 299 MinUnexplodedID: msgs[3].GetMessageID(), 300 NextPurgeTime: msgs[3].Valid().Etime(), 301 IsActive: true, 302 }) 303 // asset deletion job 304 assertListener(conv1.ConvID, 2) 305 306 for _, item := range purger.pq.queue { 307 t.Logf("queue item: %+v", item) 308 } 309 require.Equal(t, 2, purger.Len(), "expected conv %v and %v", conv1.ConvID, conv2.ConvID) 310 311 g.EphemeralPurger.Start(ctx, uid) 312 world.Fc.Advance(lifetimeDuration * 3) 313 // keep the fakeclock ticking 314 go func() { 315 for i := 0; i < 10; i++ { 316 world.Fc.Advance(lifetimeDuration) 317 time.Sleep(lifetimeDuration) 318 } 319 }() 320 321 t.Logf("assert listener 4 & 5") 322 assertListener(conv1.ConvID, 0) 323 assertListener(conv2.ConvID, 0) 324 localVers1++ 325 assertEphemeralPurgeNotifInfo(conv1.ConvID, []chat1.MessageID{msgs[4].GetMessageID()}, localVers1) 326 localVers2++ 327 assertEphemeralPurgeNotifInfo(conv2.ConvID, []chat1.MessageID{msgs[3].GetMessageID()}, localVers2) 328 assertTrackerState(conv1.ConvID, chat1.EphemeralPurgeInfo{ 329 ConvID: conv1.ConvID, 330 MinUnexplodedID: msgs[4].GetMessageID(), 331 NextPurgeTime: 0, 332 IsActive: false, 333 }) 334 assertTrackerState(conv2.ConvID, chat1.EphemeralPurgeInfo{ 335 ConvID: conv2.ConvID, 336 MinUnexplodedID: msgs[3].GetMessageID(), 337 NextPurgeTime: 0, 338 IsActive: false, 339 }) 340 // asset deletion job 341 assertListener(conv1.ConvID, 0) 342 assertListener(conv2.ConvID, 0) 343 344 world.Fc.Advance(lifetimeDuration * 5) 345 select { 346 case <-listener.bgConvLoads: 347 require.Fail(t, "unexpected load") 348 case <-listener.ephemeralPurge: 349 require.Fail(t, "unexpected purge") 350 case <-time.After(1 * time.Second): 351 } 352 assertTrackerState(conv1.ConvID, chat1.EphemeralPurgeInfo{ 353 ConvID: conv1.ConvID, 354 MinUnexplodedID: msgs[4].GetMessageID(), 355 NextPurgeTime: 0, 356 IsActive: false, 357 }) 358 assertTrackerState(conv2.ConvID, chat1.EphemeralPurgeInfo{ 359 ConvID: conv2.ConvID, 360 MinUnexplodedID: msgs[3].GetMessageID(), 361 NextPurgeTime: 0, 362 IsActive: false, 363 }) 364 require.Equal(t, 0, purger.Len()) 365 } 366 367 func TestQueueState(t *testing.T) { 368 ctx, tc, world, _, _, _, _ := setupLoaderTest(t) 369 defer world.Cleanup() 370 371 g := globals.NewContext(tc.G, tc.ChatG) 372 u := world.GetUsers()[0] 373 uid := gregor1.UID(u.GetUID().ToBytes()) 374 375 g.EphemeralTracker = NewEphemeralTracker(g) 376 purger := NewBackgroundEphemeralPurger(g) 377 purger.SetClock(world.Fc) 378 purger.Start(context.Background(), uid) 379 <-purger.Stop(context.Background()) 380 381 pq := purger.pq 382 require.NotNil(t, pq) 383 require.Zero(t, pq.Len()) 384 require.Nil(t, pq.Peek()) 385 386 now := world.Fc.Now() 387 388 purgeInfo := chat1.EphemeralPurgeInfo{ 389 ConvID: chat1.ConversationID("conv1"), 390 MinUnexplodedID: 0, 391 NextPurgeTime: gregor1.ToTime(now.Add(time.Hour)), 392 IsActive: true, 393 } 394 395 err := purger.Queue(ctx, purgeInfo) 396 require.NoError(t, err) 397 require.Equal(t, 1, pq.Len()) 398 queueItem := pq.Peek() 399 require.NotNil(t, queueItem) 400 require.Zero(t, queueItem.index) 401 require.Equal(t, purgeInfo, queueItem.purgeInfo) 402 403 // Insert an item with a shorter time and make sure it's updated appropiated 404 purgeInfo2 := chat1.EphemeralPurgeInfo{ 405 ConvID: chat1.ConversationID("conv1"), 406 MinUnexplodedID: 5, 407 NextPurgeTime: gregor1.ToTime(now.Add(time.Hour).Add(time.Minute)), 408 IsActive: true, 409 } 410 err = purger.Queue(ctx, purgeInfo2) 411 require.NoError(t, err) 412 require.Equal(t, 1, pq.Len()) 413 queueItem = pq.Peek() 414 require.NotNil(t, queueItem) 415 require.Zero(t, queueItem.index) 416 require.Equal(t, purgeInfo2, queueItem.purgeInfo) 417 418 // Insert a second item make sure it is distinct 419 purgeInfo3 := chat1.EphemeralPurgeInfo{ 420 ConvID: chat1.ConversationID("conv2"), 421 MinUnexplodedID: 0, 422 NextPurgeTime: gregor1.ToTime(now.Add(30 * time.Minute)), 423 IsActive: true, 424 } 425 err = purger.Queue(ctx, purgeInfo3) 426 require.NoError(t, err) 427 require.Equal(t, 2, pq.Len()) 428 queueItem = pq.Peek() 429 require.NotNil(t, queueItem) 430 require.Zero(t, queueItem.index) 431 require.Equal(t, purgeInfo3, queueItem.purgeInfo) 432 433 }