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

     1  package storage
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"testing"
     7  
     8  	"time"
     9  
    10  	"github.com/keybase/client/go/kbtest"
    11  	"github.com/keybase/client/go/libkb"
    12  	"github.com/keybase/client/go/protocol/chat1"
    13  	"github.com/keybase/client/go/protocol/gregor1"
    14  	"github.com/keybase/client/go/protocol/keybase1"
    15  	"github.com/keybase/clockwork"
    16  	"github.com/stretchr/testify/require"
    17  )
    18  
    19  func setupOutboxTest(t testing.TB, name string) (kbtest.ChatTestContext, *Outbox, gregor1.UID, clockwork.FakeClock) {
    20  	ctc := setupCommonTest(t, name)
    21  	u, err := kbtest.CreateAndSignupFakeUser("ob", ctc.TestContext.G)
    22  	require.NoError(t, err)
    23  	uid := gregor1.UID(u.User.GetUID().ToBytes())
    24  	cl := clockwork.NewFakeClock()
    25  	tp := ctc.Context().Env.Test
    26  	ctc.G.Env = libkb.NewEnv(libkb.AppConfig{
    27  		HomeDir: ctc.Context().GetEnv().GetHome(),
    28  		RunMode: ctc.Context().GetRunMode(),
    29  	}, nil, ctc.Context().GetLog)
    30  	ctc.Context().Env.Test = tp
    31  	ob := NewOutbox(ctc.Context(), uid)
    32  	ob.SetClock(cl)
    33  	return ctc, ob, uid, cl
    34  }
    35  
    36  func makeMsgPlaintext(body string, uid gregor1.UID) chat1.MessagePlaintext {
    37  	return chat1.MessagePlaintext{
    38  		ClientHeader: chat1.MessageClientHeader{
    39  			Sender:     uid,
    40  			OutboxInfo: &chat1.OutboxInfo{},
    41  		},
    42  		MessageBody: chat1.NewMessageBodyWithText(chat1.MessageText{Body: body}),
    43  	}
    44  }
    45  
    46  func makeMsgPlaintextEphemeral(body string, uid gregor1.UID, ephemeralMetadata *chat1.MsgEphemeralMetadata) chat1.MessagePlaintext {
    47  	msg := makeMsgPlaintext(body, uid)
    48  	msg.ClientHeader.EphemeralMetadata = ephemeralMetadata
    49  	return msg
    50  }
    51  
    52  func TestChatOutbox(t *testing.T) {
    53  	tc, ob, uid, cl := setupOutboxTest(t, "outbox")
    54  	defer tc.Cleanup()
    55  
    56  	var obrs []chat1.OutboxRecord
    57  	conv := makeConvo(gregor1.Time(5), 1, 1)
    58  
    59  	prevOrdinal := outboxOrdinalStart
    60  	for i := 0; i < 5; i++ {
    61  		obr, err := ob.PushMessage(context.TODO(), conv.GetConvID(), makeMsgPlaintext("hi", uid),
    62  			nil, nil, nil, keybase1.TLFIdentifyBehavior_CHAT_CLI)
    63  		require.Equal(t, obr.Ordinal, prevOrdinal)
    64  		prevOrdinal++
    65  		require.NoError(t, err)
    66  		obrs = append(obrs, obr)
    67  		cl.Advance(time.Millisecond)
    68  	}
    69  
    70  	// Basic pull
    71  	res, err := ob.PullAllConversations(context.TODO(), true, false)
    72  	require.NoError(t, err)
    73  	require.Equal(t, obrs, res, "wrong obids")
    74  
    75  	// Pull with remove
    76  	res, err = ob.PullAllConversations(context.TODO(), true, true)
    77  	require.NoError(t, err)
    78  	require.Equal(t, obrs, res, "wrong obids")
    79  	emptyRes, err := ob.PullAllConversations(context.TODO(), true, true)
    80  	require.NoError(t, err)
    81  	require.Zero(t, len(emptyRes), "not empty")
    82  
    83  	// Record a failed attempt
    84  	require.NoError(t, ob.RecordFailedAttempt(context.TODO(), obrs[3]))
    85  
    86  	// Check to make sure this record now has a failure
    87  	res, err = ob.PullAllConversations(context.TODO(), true, false)
    88  	require.NoError(t, err)
    89  	require.Equal(t, 1, len(res), "wrong len")
    90  	state, err := res[0].State.State()
    91  	require.NoError(t, err)
    92  	require.Equal(t, chat1.OutboxStateType_SENDING, state, "wrong state")
    93  	require.Equal(t, 1, res[0].State.Sending(), "wrong attempts")
    94  
    95  	// Mark one as an error
    96  	newObr, err := ob.MarkAsError(context.TODO(), obrs[2], chat1.OutboxStateError{
    97  		Message: "failed",
    98  		Typ:     chat1.OutboxErrorType_MISC,
    99  	})
   100  	require.NoError(t, err)
   101  	st, err := newObr.State.State()
   102  	require.NoError(t, err)
   103  	require.Equal(t, chat1.OutboxStateType_ERROR, st)
   104  	require.Equal(t, chat1.OutboxErrorType_MISC, newObr.State.Error().Typ)
   105  
   106  	// Check for correct order
   107  	res, err = ob.PullAllConversations(context.TODO(), true, false)
   108  	require.NoError(t, err)
   109  	require.Equal(t, 2, len(res), "wrong len")
   110  	state, err = res[0].State.State()
   111  	require.NoError(t, err)
   112  	require.Equal(t, chat1.OutboxStateType_ERROR, state, "wrong state")
   113  	state, err = res[1].State.State()
   114  	require.NoError(t, err)
   115  	require.Equal(t, chat1.OutboxStateType_SENDING, state, "wrong state")
   116  
   117  	// Pull without errors
   118  	res, err = ob.PullAllConversations(context.TODO(), false, false)
   119  	require.NoError(t, err)
   120  	require.Equal(t, 1, len(res), "wrong len")
   121  
   122  	// Retry the error
   123  	t.Logf("retrying the error: %s", obrs[2].OutboxID)
   124  	_, err = ob.RetryMessage(context.TODO(), obrs[2].OutboxID, nil)
   125  	require.NoError(t, err)
   126  	res, err = ob.PullAllConversations(context.TODO(), true, false)
   127  	require.NoError(t, err)
   128  	require.Equal(t, 2, len(res), "wrong len")
   129  	state, err = res[1].State.State()
   130  	require.NoError(t, err)
   131  	require.Equal(t, chat1.OutboxStateType_SENDING, state, "wrong state")
   132  	require.Equal(t, 0, res[1].State.Sending(), "wrong attempts")
   133  
   134  	// Remove 2
   135  	_, err = ob.RemoveMessage(context.TODO(), obrs[2].OutboxID)
   136  	require.NoError(t, err)
   137  	res, err = ob.PullAllConversations(context.TODO(), true, false)
   138  	require.NoError(t, err)
   139  	require.Equal(t, 1, len(res), "wrong len")
   140  	require.Equal(t, obrs[3].OutboxID, res[0].OutboxID, "wrong element")
   141  
   142  	var tv chat1.ThreadView
   143  	require.NoError(t, ob.AppendToThread(context.TODO(), conv.GetConvID(), &tv))
   144  	require.Equal(t, 1, len(tv.Messages))
   145  	newObr, err = ob.MarkAsError(context.TODO(), obrs[3], chat1.OutboxStateError{
   146  		Message: "failed",
   147  		Typ:     chat1.OutboxErrorType_DUPLICATE,
   148  	})
   149  	require.NoError(t, err)
   150  	st, err = newObr.State.State()
   151  	require.NoError(t, err)
   152  	require.Equal(t, chat1.OutboxStateType_ERROR, st)
   153  	require.Equal(t, chat1.OutboxErrorType_DUPLICATE, newObr.State.Error().Typ)
   154  	tv.Messages = nil
   155  	require.NoError(t, ob.AppendToThread(context.TODO(), conv.GetConvID(), &tv))
   156  	require.Zero(t, len(tv.Messages))
   157  }
   158  
   159  func TestChatOutboxPurge(t *testing.T) {
   160  	tc, ob, uid, cl := setupOutboxTest(t, "outbox")
   161  	defer tc.Cleanup()
   162  
   163  	var obrs []chat1.OutboxRecord
   164  	conv := makeConvo(gregor1.Time(5), 1, 1)
   165  
   166  	prevOrdinal := outboxOrdinalStart
   167  	ephemeralMetadata := &chat1.MsgEphemeralMetadata{Lifetime: 0}
   168  	for i := 0; i < 9; i++ {
   169  		// send some exploding and some non exploding msgs
   170  		if i > 3 {
   171  			ephemeralMetadata = nil
   172  		}
   173  		obr, err := ob.PushMessage(context.TODO(), conv.GetConvID(),
   174  			makeMsgPlaintextEphemeral("hi", uid, ephemeralMetadata),
   175  			nil, nil, nil, keybase1.TLFIdentifyBehavior_CHAT_CLI)
   176  		require.Equal(t, obr.Ordinal, prevOrdinal)
   177  		prevOrdinal++
   178  		require.NoError(t, err)
   179  		obrs = append(obrs, obr)
   180  		cl.Advance(time.Millisecond)
   181  	}
   182  
   183  	res, err := ob.PullAllConversations(context.TODO(), true, false)
   184  	require.NoError(t, err)
   185  	require.Equal(t, obrs, res, "wrong obids")
   186  
   187  	// Nothing is in the error state & expired, so we should not have purged anything
   188  	_, err = ob.OutboxPurge(context.TODO())
   189  	require.NoError(t, err)
   190  
   191  	res, err = ob.PullAllConversations(context.TODO(), true, false)
   192  	require.NoError(t, err)
   193  	require.Equal(t, obrs, res, "wrong obids")
   194  
   195  	// Mark 6/9 records as an error, three of these are ephemeral message, 3
   196  	// regular messages.
   197  	for i := 0; i < 6; i++ {
   198  		errRec := chat1.OutboxStateError{
   199  			Message: "failed",
   200  			Typ:     chat1.OutboxErrorType_MISC,
   201  		}
   202  		obrs[i], err = ob.MarkAsError(context.TODO(), obrs[i], errRec)
   203  		require.NoError(t, err)
   204  	}
   205  
   206  	_, err = ob.OutboxPurge(context.TODO())
   207  	require.NoError(t, err)
   208  	res, err = ob.PullAllConversations(context.TODO(), true, false)
   209  	require.NoError(t, err)
   210  	require.Equal(t, obrs, res, "wrong obids")
   211  
   212  	// move the clock forward the ephemeralPurgeCutoff duration and we'll
   213  	// remove the ephemeral records.
   214  	cl.Advance(ephemeralPurgeCutoff)
   215  	_, err = ob.OutboxPurge(context.TODO())
   216  	require.NoError(t, err)
   217  	res, err = ob.PullAllConversations(context.TODO(), true, false)
   218  	require.NoError(t, err)
   219  	require.Equal(t, obrs[4:], res, "wrong obids")
   220  
   221  	// move the clock forward the errorPurgeCutoff duration and we'll remove
   222  	// the records that are marked as an error.
   223  	cl.Advance(errorPurgeCutoff)
   224  	_, err = ob.OutboxPurge(context.TODO())
   225  	require.NoError(t, err)
   226  	res, err = ob.PullAllConversations(context.TODO(), true, false)
   227  	require.NoError(t, err)
   228  	require.Equal(t, obrs[6:], res, "wrong obids")
   229  }
   230  
   231  func TestChatOutboxMarkConv(t *testing.T) {
   232  	tc, ob, uid, cl := setupOutboxTest(t, "outbox")
   233  	defer tc.Cleanup()
   234  
   235  	var obrs []chat1.OutboxRecord
   236  	conv := makeConvo(gregor1.Time(5), 1, 1)
   237  	conv2 := makeConvo(gregor1.Time(5), 1, 1)
   238  	for i := 0; i < 5; i++ {
   239  		convID := conv.GetConvID()
   240  		if i%2 == 0 {
   241  			convID = conv2.GetConvID()
   242  		}
   243  		obr, err := ob.PushMessage(context.TODO(), convID,
   244  			makeMsgPlaintext("hi", uid),
   245  			nil, nil, nil, keybase1.TLFIdentifyBehavior_CHAT_CLI)
   246  		require.NoError(t, err)
   247  		obrs = append(obrs, obr)
   248  		cl.Advance(time.Millisecond)
   249  	}
   250  
   251  	errConvID := obrs[0].ConvID
   252  	newObr, err := ob.MarkAsError(context.TODO(), obrs[0], chat1.OutboxStateError{
   253  		Message: "failed",
   254  		Typ:     chat1.OutboxErrorType_MISC,
   255  	})
   256  	require.NoError(t, err)
   257  	st, err := newObr.State.State()
   258  	require.NoError(t, err)
   259  	require.Equal(t, chat1.OutboxStateType_ERROR, st)
   260  	require.Equal(t, chat1.OutboxErrorType_MISC, newObr.State.Error().Typ)
   261  
   262  	newObrs, err := ob.MarkConvAsError(context.TODO(), errConvID, chat1.OutboxStateError{
   263  		Message: "failed",
   264  		Typ:     chat1.OutboxErrorType_MISC,
   265  	})
   266  	require.NoError(t, err)
   267  	require.Equal(t, 2, len(newObrs))
   268  	for _, newObr := range newObrs {
   269  		st, err := newObr.State.State()
   270  		require.NoError(t, err)
   271  		require.True(t, newObr.ConvID.Eq(errConvID))
   272  		require.Equal(t, chat1.OutboxStateType_ERROR, st)
   273  		require.Equal(t, chat1.OutboxErrorType_MISC, newObr.State.Error().Typ)
   274  	}
   275  
   276  	obrs, err = ob.PullAllConversations(context.TODO(), true, false)
   277  	require.NoError(t, err)
   278  	require.Len(t, obrs, 5)
   279  	for _, obr := range obrs {
   280  		st, err := obr.State.State()
   281  		require.NoError(t, err)
   282  		if obr.ConvID.Eq(errConvID) {
   283  			require.Equal(t, chat1.OutboxStateType_ERROR, st)
   284  			require.Equal(t, chat1.OutboxErrorType_MISC, newObr.State.Error().Typ)
   285  		} else {
   286  			require.Equal(t, chat1.OutboxStateType_SENDING, st)
   287  			require.Equal(t, 0, obr.State.Sending())
   288  		}
   289  	}
   290  }
   291  
   292  func TestChatOutboxCancelMessagesWithPredicate(t *testing.T) {
   293  	tc, ob, uid, cl := setupOutboxTest(t, "outbox")
   294  	defer tc.Cleanup()
   295  	ctx := context.TODO()
   296  
   297  	conv := makeConvo(gregor1.Time(5), 1, 1)
   298  	for i := 0; i < 5; i++ {
   299  		_, err := ob.PushMessage(ctx, conv.GetConvID(),
   300  			makeMsgPlaintext(fmt.Sprintf("hi%d", i), uid),
   301  			nil, nil, nil, keybase1.TLFIdentifyBehavior_CHAT_CLI)
   302  		require.NoError(t, err)
   303  		cl.Advance(time.Millisecond)
   304  	}
   305  
   306  	allFalse := func(obr chat1.OutboxRecord) bool { return false }
   307  	numCancelled, err := ob.CancelMessagesWithPredicate(ctx, allFalse)
   308  	require.NoError(t, err)
   309  	require.Zero(t, numCancelled)
   310  	res, err := ob.PullAllConversations(ctx, false, false)
   311  	require.NoError(t, err)
   312  	require.Len(t, res, 5)
   313  
   314  	ith := func(obr chat1.OutboxRecord) bool {
   315  		txt := obr.Msg.MessageBody.Text().Body
   316  		return txt == "hi0" || txt == "hi1"
   317  	}
   318  	t.Logf("DEBUG: ith")
   319  	numCancelled, err = ob.CancelMessagesWithPredicate(ctx, ith)
   320  	require.NoError(t, err)
   321  	require.Equal(t, 2, numCancelled)
   322  	res, err = ob.PullAllConversations(ctx, false, false)
   323  	require.NoError(t, err)
   324  	require.Len(t, res, 3)
   325  
   326  	allTrue := func(obr chat1.OutboxRecord) bool { return true }
   327  	numCancelled, err = ob.CancelMessagesWithPredicate(ctx, allTrue)
   328  	require.NoError(t, err)
   329  	require.Equal(t, 3, numCancelled)
   330  	res, err = ob.PullAllConversations(ctx, false, false)
   331  	require.NoError(t, err)
   332  	require.Zero(t, len(res))
   333  }