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  }